diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 34e8f928e5daf..7d583695b66c4 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -412,6 +412,14 @@ const docTemplate = `{ ], "summary": "Debug Info Deployment Health", "operationId": "debug-info-deployment-health", + "parameters": [ + { + "type": "boolean", + "description": "Force a healthcheck to run", + "name": "force", + "in": "query" + } + ], "responses": { "200": { "description": "OK", diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 7b82ad33bc58d..550a90c6805d7 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -348,6 +348,14 @@ "tags": ["Debug"], "summary": "Debug Info Deployment Health", "operationId": "debug-info-deployment-health", + "parameters": [ + { + "type": "boolean", + "description": "Force a healthcheck to run", + "name": "force", + "in": "query" + } + ], "responses": { "200": { "description": "OK", diff --git a/coderd/debug.go b/coderd/debug.go index b1609398a0630..ba6eaf9696d99 100644 --- a/coderd/debug.go +++ b/coderd/debug.go @@ -41,16 +41,22 @@ func (api *API) debugTailnet(rw http.ResponseWriter, r *http.Request) { // @Tags Debug // @Success 200 {object} healthcheck.Report // @Router /debug/health [get] +// @Param force query boolean false "Force a healthcheck to run" func (api *API) debugDeploymentHealth(rw http.ResponseWriter, r *http.Request) { apiKey := httpmw.APITokenFromRequest(r) ctx, cancel := context.WithTimeout(r.Context(), api.Options.HealthcheckTimeout) defer cancel() - // Get cached report if it exists. - if report := api.healthCheckCache.Load(); report != nil { - if time.Since(report.Time) < api.Options.HealthcheckRefresh { - formatHealthcheck(ctx, rw, r, report) - return + // Check if the forced query parameter is set. + forced := r.URL.Query().Get("force") == "true" + + // Get cached report if it exists and the requester did not force a refresh. + if !forced { + if report := api.healthCheckCache.Load(); report != nil { + if time.Since(report.Time) < api.Options.HealthcheckRefresh { + formatHealthcheck(ctx, rw, r, report) + return + } } } diff --git a/coderd/debug_test.go b/coderd/debug_test.go index 2136ca2d9d6ac..186539b82d90f 100644 --- a/coderd/debug_test.go +++ b/coderd/debug_test.go @@ -4,6 +4,7 @@ import ( "context" "io" "net/http" + "sync/atomic" "testing" "time" @@ -22,24 +23,66 @@ func TestDebugHealth(t *testing.T) { t.Parallel() var ( + calls = atomic.Int64{} ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort) sessionToken string client = coderdtest.New(t, &coderdtest.Options{ HealthcheckFunc: func(_ context.Context, apiKey string) *healthcheck.Report { + calls.Add(1) assert.Equal(t, sessionToken, apiKey) - return &healthcheck.Report{} + return &healthcheck.Report{ + Time: time.Now(), + } }, + HealthcheckRefresh: time.Hour, // Avoid flakes. }) _ = coderdtest.CreateFirstUser(t, client) ) defer cancel() sessionToken = client.SessionToken() - res, err := client.Request(ctx, "GET", "/api/v2/debug/health", nil) - require.NoError(t, err) - defer res.Body.Close() - _, _ = io.ReadAll(res.Body) - require.Equal(t, http.StatusOK, res.StatusCode) + for i := 0; i < 10; i++ { + res, err := client.Request(ctx, "GET", "/api/v2/debug/health", nil) + require.NoError(t, err) + _, _ = io.ReadAll(res.Body) + res.Body.Close() + require.Equal(t, http.StatusOK, res.StatusCode) + } + // The healthcheck should only have been called once. + require.EqualValues(t, 1, calls.Load()) + }) + + t.Run("Forced", func(t *testing.T) { + t.Parallel() + + var ( + calls = atomic.Int64{} + ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort) + sessionToken string + client = coderdtest.New(t, &coderdtest.Options{ + HealthcheckFunc: func(_ context.Context, apiKey string) *healthcheck.Report { + calls.Add(1) + assert.Equal(t, sessionToken, apiKey) + return &healthcheck.Report{ + Time: time.Now(), + } + }, + HealthcheckRefresh: time.Hour, // Avoid flakes. + }) + _ = coderdtest.CreateFirstUser(t, client) + ) + defer cancel() + + sessionToken = client.SessionToken() + for i := 0; i < 10; i++ { + res, err := client.Request(ctx, "GET", "/api/v2/debug/health?force=true", nil) + require.NoError(t, err) + _, _ = io.ReadAll(res.Body) + res.Body.Close() + require.Equal(t, http.StatusOK, res.StatusCode) + } + // The healthcheck func should have been called each time. + require.EqualValues(t, 10, calls.Load()) }) t.Run("Timeout", func(t *testing.T) { diff --git a/docs/api/debug.md b/docs/api/debug.md index e5d0c0cd505de..afa42728a1df6 100644 --- a/docs/api/debug.md +++ b/docs/api/debug.md @@ -33,6 +33,12 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ `GET /debug/health` +### Parameters + +| Name | In | Type | Required | Description | +| ------- | ----- | ------- | -------- | -------------------------- | +| `force` | query | boolean | false | Force a healthcheck to run | + ### Example responses > 200 Response