diff --git a/cli/support.go b/cli/support.go index 55f8e209da..f66bcda13b 100644 --- a/cli/support.go +++ b/cli/support.go @@ -184,11 +184,12 @@ func (r *RootCmd) supportBundle() *serpent.Command { _ = os.Remove(outputPath) // best effort return xerrors.Errorf("create support bundle: %w", err) } - deployHealthSummary := bun.Deployment.HealthReport.Summarize() + docsURL := bun.Deployment.Config.Values.DocsURL.String() + deployHealthSummary := bun.Deployment.HealthReport.Summarize(docsURL) if len(deployHealthSummary) > 0 { cliui.Warn(inv.Stdout, "Deployment health issues detected:", deployHealthSummary...) } - clientNetcheckSummary := bun.Network.Netcheck.Summarize("Client netcheck:") + clientNetcheckSummary := bun.Network.Netcheck.Summarize("Client netcheck:", docsURL) if len(clientNetcheckSummary) > 0 { cliui.Warn(inv.Stdout, "Networking issues detected:", deployHealthSummary...) } diff --git a/coderd/healthcheck/health/model.go b/coderd/healthcheck/health/model.go index 33b5e9711b..ce332a0fe3 100644 --- a/coderd/healthcheck/health/model.go +++ b/coderd/healthcheck/health/model.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/coder/coder/v2/buildinfo" "github.com/coder/coder/v2/coderd/util/ptr" ) @@ -44,6 +45,11 @@ const ( CodeProvisionerDaemonAPIMajorVersionDeprecated Code = `EPD03` ) +// Default docs URL +var ( + docsURLDefault = "https://coder.com/docs/v2" +) + // @typescript-generate Severity type Severity string @@ -72,6 +78,30 @@ func (m Message) String() string { return sb.String() } +// URL returns a link to the admin/healthcheck docs page for the given Message. +// NOTE: if using a custom docs URL, specify base. +func (m Message) URL(base string) string { + var codeAnchor string + if m.Code == "" { + codeAnchor = strings.ToLower(string(CodeUnknown)) + } else { + codeAnchor = strings.ToLower(string(m.Code)) + } + + if base == "" { + base = docsURLDefault + versionPath := buildinfo.Version() + if buildinfo.IsDev() { + // for development versions, just use latest + versionPath = "latest" + } + return fmt.Sprintf("%s/%s/admin/healthcheck#%s", base, versionPath, codeAnchor) + } + + // We don't assume that custom docs URLs are versioned. + return fmt.Sprintf("%s/admin/healthcheck#%s", base, codeAnchor) +} + // Code is a stable identifier used to link to documentation. // @typescript-generate Code type Code string diff --git a/coderd/healthcheck/health/model_test.go b/coderd/healthcheck/health/model_test.go new file mode 100644 index 0000000000..3e8cc1ea07 --- /dev/null +++ b/coderd/healthcheck/health/model_test.go @@ -0,0 +1,32 @@ +package health_test + +import ( + "testing" + + "github.com/coder/coder/v2/coderd/healthcheck/health" + + "github.com/stretchr/testify/assert" +) + +func Test_MessageURL(t *testing.T) { + t.Parallel() + + for _, tt := range []struct { + name string + code health.Code + base string + expected string + }{ + {"empty", "", "", "https://coder.com/docs/v2/latest/admin/healthcheck#eunknown"}, + {"default", health.CodeAccessURLFetch, "", "https://coder.com/docs/v2/latest/admin/healthcheck#eacs03"}, + {"custom docs base", health.CodeAccessURLFetch, "https://example.com/docs", "https://example.com/docs/admin/healthcheck#eacs03"}, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + uut := health.Message{Code: tt.code} + actual := uut.URL(tt.base) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/codersdk/healthsdk/healthsdk.go b/codersdk/healthsdk/healthsdk.go index 51cf4b6957..8a00a8a3d6 100644 --- a/codersdk/healthsdk/healthsdk.go +++ b/codersdk/healthsdk/healthsdk.go @@ -120,14 +120,14 @@ type HealthcheckReport struct { } // Summarize returns a summary of all errors and warnings of components of HealthcheckReport. -func (r *HealthcheckReport) Summarize() []string { +func (r *HealthcheckReport) Summarize(docsURL string) []string { var msgs []string - msgs = append(msgs, r.AccessURL.Summarize("Access URL:")...) - msgs = append(msgs, r.Database.Summarize("Database:")...) - msgs = append(msgs, r.DERP.Summarize("DERP:")...) - msgs = append(msgs, r.ProvisionerDaemons.Summarize("Provisioner Daemons:")...) - msgs = append(msgs, r.Websocket.Summarize("Websocket:")...) - msgs = append(msgs, r.WorkspaceProxy.Summarize("Workspace Proxies:")...) + msgs = append(msgs, r.AccessURL.Summarize("Access URL:", docsURL)...) + msgs = append(msgs, r.Database.Summarize("Database:", docsURL)...) + msgs = append(msgs, r.DERP.Summarize("DERP:", docsURL)...) + msgs = append(msgs, r.ProvisionerDaemons.Summarize("Provisioner Daemons:", docsURL)...) + msgs = append(msgs, r.Websocket.Summarize("Websocket:", docsURL)...) + msgs = append(msgs, r.WorkspaceProxy.Summarize("Workspace Proxies:", docsURL)...) return msgs } @@ -141,7 +141,7 @@ type BaseReport struct { // Summarize returns a list of strings containing the errors and warnings of BaseReport, if present. // All strings are prefixed with prefix. -func (b *BaseReport) Summarize(prefix string) []string { +func (b *BaseReport) Summarize(prefix, docsURL string) []string { if b == nil { return []string{} } @@ -165,6 +165,7 @@ func (b *BaseReport) Summarize(prefix string) []string { _, _ = sb.WriteString("Warn: ") _, _ = sb.WriteString(warn.String()) msgs = append(msgs, sb.String()) + msgs = append(msgs, "See: "+warn.URL(docsURL)) } return msgs } diff --git a/codersdk/healthsdk/healthsdk_test.go b/codersdk/healthsdk/healthsdk_test.go index d89c999125..b751a14f62 100644 --- a/codersdk/healthsdk/healthsdk_test.go +++ b/codersdk/healthsdk/healthsdk_test.go @@ -41,18 +41,24 @@ func TestSummarize(t *testing.T) { expected := []string{ "Access URL: Error: test error", "Access URL: Warn: TEST: testing", + "See: https://coder.com/docs/v2/latest/admin/healthcheck#test", "Database: Error: test error", "Database: Warn: TEST: testing", + "See: https://coder.com/docs/v2/latest/admin/healthcheck#test", "DERP: Error: test error", "DERP: Warn: TEST: testing", + "See: https://coder.com/docs/v2/latest/admin/healthcheck#test", "Provisioner Daemons: Error: test error", "Provisioner Daemons: Warn: TEST: testing", + "See: https://coder.com/docs/v2/latest/admin/healthcheck#test", "Websocket: Error: test error", "Websocket: Warn: TEST: testing", + "See: https://coder.com/docs/v2/latest/admin/healthcheck#test", "Workspace Proxies: Error: test error", "Workspace Proxies: Warn: TEST: testing", + "See: https://coder.com/docs/v2/latest/admin/healthcheck#test", } - actual := hr.Summarize() + actual := hr.Summarize("") assert.Equal(t, expected, actual) }) @@ -87,7 +93,9 @@ func TestSummarize(t *testing.T) { expected: []string{ "Error: testing", "Warn: TEST01: testing one", + "See: https://coder.com/docs/v2/latest/admin/healthcheck#test01", "Warn: TEST02: testing two", + "See: https://coder.com/docs/v2/latest/admin/healthcheck#test02", }, }, { @@ -109,14 +117,16 @@ func TestSummarize(t *testing.T) { expected: []string{ "TEST: Error: testing", "TEST: Warn: TEST01: testing one", + "See: https://coder.com/docs/v2/latest/admin/healthcheck#test01", "TEST: Warn: TEST02: testing two", + "See: https://coder.com/docs/v2/latest/admin/healthcheck#test02", }, }, } { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - actual := tt.br.Summarize(tt.pfx) + actual := tt.br.Summarize(tt.pfx, "") if len(tt.expected) == 0 { assert.Empty(t, actual) return