mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
feat(healthcheck): add websocket report (#7689)
This commit is contained in:
51
coderd/apidoc/docs.go
generated
51
coderd/apidoc/docs.go
generated
@ -427,6 +427,34 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/debug/ws": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Debug"
|
||||||
|
],
|
||||||
|
"summary": "Debug Info Websocket Test",
|
||||||
|
"operationId": "debug-info-websocket-test",
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Created",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.Response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-apidocgen": {
|
||||||
|
"skip": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/deployment/config": {
|
"/deployment/config": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@ -10419,6 +10447,29 @@ const docTemplate = `{
|
|||||||
"time": {
|
"time": {
|
||||||
"description": "Time is the time the report was generated at.",
|
"description": "Time is the time the report was generated at.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"websocket": {
|
||||||
|
"$ref": "#/definitions/healthcheck.WebsocketReport"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"healthcheck.WebsocketReport": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {},
|
||||||
|
"response": {
|
||||||
|
"$ref": "#/definitions/healthcheck.WebsocketResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"healthcheck.WebsocketResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"body": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"code": {
|
||||||
|
"type": "integer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
47
coderd/apidoc/swagger.json
generated
47
coderd/apidoc/swagger.json
generated
@ -363,6 +363,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/debug/ws": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": ["application/json"],
|
||||||
|
"tags": ["Debug"],
|
||||||
|
"summary": "Debug Info Websocket Test",
|
||||||
|
"operationId": "debug-info-websocket-test",
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Created",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.Response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-apidocgen": {
|
||||||
|
"skip": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/deployment/config": {
|
"/deployment/config": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@ -9408,6 +9432,29 @@
|
|||||||
"time": {
|
"time": {
|
||||||
"description": "Time is the time the report was generated at.",
|
"description": "Time is the time the report was generated at.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"websocket": {
|
||||||
|
"$ref": "#/definitions/healthcheck.WebsocketReport"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"healthcheck.WebsocketReport": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {},
|
||||||
|
"response": {
|
||||||
|
"$ref": "#/definitions/healthcheck.WebsocketResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"healthcheck.WebsocketResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"body": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"code": {
|
||||||
|
"type": "integer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -129,7 +129,7 @@ type Options struct {
|
|||||||
// AppSecurityKey is the crypto key used to sign and encrypt tokens related to
|
// AppSecurityKey is the crypto key used to sign and encrypt tokens related to
|
||||||
// workspace applications. It consists of both a signing and encryption key.
|
// workspace applications. It consists of both a signing and encryption key.
|
||||||
AppSecurityKey workspaceapps.SecurityKey
|
AppSecurityKey workspaceapps.SecurityKey
|
||||||
HealthcheckFunc func(ctx context.Context) (*healthcheck.Report, error)
|
HealthcheckFunc func(ctx context.Context, apiKey string) (*healthcheck.Report, error)
|
||||||
HealthcheckTimeout time.Duration
|
HealthcheckTimeout time.Duration
|
||||||
HealthcheckRefresh time.Duration
|
HealthcheckRefresh time.Duration
|
||||||
|
|
||||||
@ -256,10 +256,11 @@ func New(options *Options) *API {
|
|||||||
options.TemplateScheduleStore.Store(&v)
|
options.TemplateScheduleStore.Store(&v)
|
||||||
}
|
}
|
||||||
if options.HealthcheckFunc == nil {
|
if options.HealthcheckFunc == nil {
|
||||||
options.HealthcheckFunc = func(ctx context.Context) (*healthcheck.Report, error) {
|
options.HealthcheckFunc = func(ctx context.Context, apiKey string) (*healthcheck.Report, error) {
|
||||||
return healthcheck.Run(ctx, &healthcheck.ReportOptions{
|
return healthcheck.Run(ctx, &healthcheck.ReportOptions{
|
||||||
AccessURL: options.AccessURL,
|
AccessURL: options.AccessURL,
|
||||||
DERPMap: options.DERPMap.Clone(),
|
DERPMap: options.DERPMap.Clone(),
|
||||||
|
APIKey: apiKey,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -787,6 +788,7 @@ func New(options *Options) *API {
|
|||||||
|
|
||||||
r.Get("/coordinator", api.debugCoordinator)
|
r.Get("/coordinator", api.debugCoordinator)
|
||||||
r.Get("/health", api.debugDeploymentHealth)
|
r.Get("/health", api.debugDeploymentHealth)
|
||||||
|
r.Get("/ws", (&healthcheck.WebsocketEchoServer{}).ServeHTTP)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -874,6 +876,7 @@ type API struct {
|
|||||||
Experiments codersdk.Experiments
|
Experiments codersdk.Experiments
|
||||||
|
|
||||||
healthCheckGroup *singleflight.Group[string, *healthcheck.Report]
|
healthCheckGroup *singleflight.Group[string, *healthcheck.Report]
|
||||||
|
healthCheckCache atomic.Pointer[healthcheck.Report]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close waits for all WebSocket connections to drain before returning.
|
// Close waits for all WebSocket connections to drain before returning.
|
||||||
|
@ -107,7 +107,7 @@ type Options struct {
|
|||||||
TrialGenerator func(context.Context, string) error
|
TrialGenerator func(context.Context, string) error
|
||||||
TemplateScheduleStore schedule.TemplateScheduleStore
|
TemplateScheduleStore schedule.TemplateScheduleStore
|
||||||
|
|
||||||
HealthcheckFunc func(ctx context.Context) (*healthcheck.Report, error)
|
HealthcheckFunc func(ctx context.Context, apiKey string) (*healthcheck.Report, error)
|
||||||
HealthcheckTimeout time.Duration
|
HealthcheckTimeout time.Duration
|
||||||
HealthcheckRefresh time.Duration
|
HealthcheckRefresh time.Duration
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/coder/coder/coderd/healthcheck"
|
"github.com/coder/coder/coderd/healthcheck"
|
||||||
"github.com/coder/coder/coderd/httpapi"
|
"github.com/coder/coder/coderd/httpapi"
|
||||||
|
"github.com/coder/coder/coderd/httpmw"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -29,11 +30,28 @@ func (api *API) debugCoordinator(rw http.ResponseWriter, r *http.Request) {
|
|||||||
// @Success 200 {object} healthcheck.Report
|
// @Success 200 {object} healthcheck.Report
|
||||||
// @Router /debug/health [get]
|
// @Router /debug/health [get]
|
||||||
func (api *API) debugDeploymentHealth(rw http.ResponseWriter, r *http.Request) {
|
func (api *API) debugDeploymentHealth(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
apiKey := httpmw.APITokenFromRequest(r)
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), api.HealthcheckTimeout)
|
ctx, cancel := context.WithTimeout(r.Context(), api.HealthcheckTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
// Get cached report if it exists.
|
||||||
|
if report := api.healthCheckCache.Load(); report != nil {
|
||||||
|
if time.Since(report.Time) < api.HealthcheckRefresh {
|
||||||
|
httpapi.Write(ctx, rw, http.StatusOK, report)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
resChan := api.healthCheckGroup.DoChan("", func() (*healthcheck.Report, error) {
|
resChan := api.healthCheckGroup.DoChan("", func() (*healthcheck.Report, error) {
|
||||||
return api.HealthcheckFunc(ctx)
|
// Create a new context not tied to the request.
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), api.HealthcheckTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
report, err := api.HealthcheckFunc(ctx, apiKey)
|
||||||
|
if err == nil {
|
||||||
|
api.healthCheckCache.Store(report)
|
||||||
|
}
|
||||||
|
return report, err
|
||||||
})
|
})
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@ -43,13 +61,19 @@ func (api *API) debugDeploymentHealth(rw http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
case res := <-resChan:
|
case res := <-resChan:
|
||||||
if time.Since(res.Val.Time) > api.HealthcheckRefresh {
|
|
||||||
api.healthCheckGroup.Forget("")
|
|
||||||
api.debugDeploymentHealth(rw, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
httpapi.Write(ctx, rw, http.StatusOK, res.Val)
|
httpapi.Write(ctx, rw, http.StatusOK, res.Val)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For some reason the swagger docs need to be attached to a function.
|
||||||
|
//
|
||||||
|
// @Summary Debug Info Websocket Test
|
||||||
|
// @ID debug-info-websocket-test
|
||||||
|
// @Security CoderSessionToken
|
||||||
|
// @Produce json
|
||||||
|
// @Tags Debug
|
||||||
|
// @Success 201 {object} codersdk.Response
|
||||||
|
// @Router /debug/ws [get]
|
||||||
|
// @x-apidocgen {"skip": true}
|
||||||
|
func _debugws(http.ResponseWriter, *http.Request) {} //nolint:unused
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/coder/coder/coderd/coderdtest"
|
"github.com/coder/coder/coderd/coderdtest"
|
||||||
@ -14,15 +15,17 @@ import (
|
|||||||
"github.com/coder/coder/testutil"
|
"github.com/coder/coder/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDebug(t *testing.T) {
|
func TestDebugHealth(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
t.Run("Health/OK", func(t *testing.T) {
|
t.Run("OK", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort)
|
ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||||
client = coderdtest.New(t, &coderdtest.Options{
|
sessionToken string
|
||||||
HealthcheckFunc: func(context.Context) (*healthcheck.Report, error) {
|
client = coderdtest.New(t, &coderdtest.Options{
|
||||||
|
HealthcheckFunc: func(_ context.Context, apiKey string) (*healthcheck.Report, error) {
|
||||||
|
assert.Equal(t, sessionToken, apiKey)
|
||||||
return &healthcheck.Report{}, nil
|
return &healthcheck.Report{}, nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -30,6 +33,7 @@ func TestDebug(t *testing.T) {
|
|||||||
)
|
)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
sessionToken = client.SessionToken()
|
||||||
res, err := client.Request(ctx, "GET", "/debug/health", nil)
|
res, err := client.Request(ctx, "GET", "/debug/health", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
@ -37,14 +41,14 @@ func TestDebug(t *testing.T) {
|
|||||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Health/Timeout", func(t *testing.T) {
|
t.Run("Timeout", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort)
|
ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||||
client = coderdtest.New(t, &coderdtest.Options{
|
client = coderdtest.New(t, &coderdtest.Options{
|
||||||
HealthcheckTimeout: time.Microsecond,
|
HealthcheckTimeout: time.Microsecond,
|
||||||
HealthcheckFunc: func(context.Context) (*healthcheck.Report, error) {
|
HealthcheckFunc: func(context.Context, string) (*healthcheck.Report, error) {
|
||||||
t := time.NewTimer(time.Second)
|
t := time.NewTimer(time.Second)
|
||||||
defer t.Stop()
|
defer t.Stop()
|
||||||
|
|
||||||
@ -66,4 +70,48 @@ func TestDebug(t *testing.T) {
|
|||||||
_, _ = io.ReadAll(res.Body)
|
_, _ = io.ReadAll(res.Body)
|
||||||
require.Equal(t, http.StatusNotFound, res.StatusCode)
|
require.Equal(t, http.StatusNotFound, res.StatusCode)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Deduplicated", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var (
|
||||||
|
ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||||
|
calls int
|
||||||
|
client = coderdtest.New(t, &coderdtest.Options{
|
||||||
|
HealthcheckRefresh: time.Hour,
|
||||||
|
HealthcheckTimeout: time.Hour,
|
||||||
|
HealthcheckFunc: func(context.Context, string) (*healthcheck.Report, error) {
|
||||||
|
calls++
|
||||||
|
return &healthcheck.Report{
|
||||||
|
Time: time.Now(),
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
_ = coderdtest.CreateFirstUser(t, client)
|
||||||
|
)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
require.Equal(t, 1, calls)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDebugWebsocket(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("OK", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -19,9 +19,7 @@ type Report struct {
|
|||||||
|
|
||||||
DERP DERPReport `json:"derp"`
|
DERP DERPReport `json:"derp"`
|
||||||
AccessURL AccessURLReport `json:"access_url"`
|
AccessURL AccessURLReport `json:"access_url"`
|
||||||
|
Websocket WebsocketReport `json:"websocket"`
|
||||||
// TODO:
|
|
||||||
// Websocket WebsocketReport `json:"websocket"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReportOptions struct {
|
type ReportOptions struct {
|
||||||
@ -29,6 +27,7 @@ type ReportOptions struct {
|
|||||||
DERPMap *tailcfg.DERPMap
|
DERPMap *tailcfg.DERPMap
|
||||||
AccessURL *url.URL
|
AccessURL *url.URL
|
||||||
Client *http.Client
|
Client *http.Client
|
||||||
|
APIKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
func Run(ctx context.Context, opts *ReportOptions) (*Report, error) {
|
func Run(ctx context.Context, opts *ReportOptions) (*Report, error) {
|
||||||
@ -65,11 +64,19 @@ func Run(ctx context.Context, opts *ReportOptions) (*Report, error) {
|
|||||||
})
|
})
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// wg.Add(1)
|
wg.Add(1)
|
||||||
// go func() {
|
go func() {
|
||||||
// defer wg.Done()
|
defer wg.Done()
|
||||||
// report.Websocket.Run(ctx, opts.AccessURL)
|
defer func() {
|
||||||
// }()
|
if err := recover(); err != nil {
|
||||||
|
report.Websocket.Error = xerrors.Errorf("%v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
report.Websocket.Run(ctx, &WebsocketReportOptions{
|
||||||
|
APIKey: opts.APIKey,
|
||||||
|
AccessURL: opts.AccessURL,
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
report.Time = time.Now()
|
report.Time = time.Now()
|
||||||
|
@ -2,11 +2,149 @@ package healthcheck
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
"nhooyr.io/websocket"
|
||||||
|
|
||||||
|
"github.com/coder/coder/coderd/httpapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WebsocketReport struct{}
|
type WebsocketReportOptions struct {
|
||||||
|
APIKey string
|
||||||
func (*WebsocketReport) Run(ctx context.Context, accessURL *url.URL) {
|
AccessURL *url.URL
|
||||||
_, _ = ctx, accessURL
|
HTTPClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsocketReport struct {
|
||||||
|
Response WebsocketResponse `json:"response"`
|
||||||
|
Error error `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsocketResponse struct {
|
||||||
|
Body string `json:"body"`
|
||||||
|
Code int `json:"code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WebsocketReport) Run(ctx context.Context, opts *WebsocketReportOptions) {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
u, err := opts.AccessURL.Parse("/api/v2/debug/ws")
|
||||||
|
if err != nil {
|
||||||
|
r.Error = xerrors.Errorf("parse access url: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if u.Scheme == "https" {
|
||||||
|
u.Scheme = "wss"
|
||||||
|
} else {
|
||||||
|
u.Scheme = "ws"
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:bodyclose // websocket package closes this for you
|
||||||
|
c, res, err := websocket.Dial(ctx, u.String(), &websocket.DialOptions{
|
||||||
|
HTTPClient: opts.HTTPClient,
|
||||||
|
HTTPHeader: http.Header{"Coder-Session-Token": []string{opts.APIKey}},
|
||||||
|
})
|
||||||
|
if res != nil {
|
||||||
|
var body string
|
||||||
|
if res.Body != nil {
|
||||||
|
b, err := io.ReadAll(res.Body)
|
||||||
|
if err == nil {
|
||||||
|
body = string(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Response = WebsocketResponse{
|
||||||
|
Body: body,
|
||||||
|
Code: res.StatusCode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
r.Error = xerrors.Errorf("websocket dial: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer c.Close(websocket.StatusGoingAway, "goodbye")
|
||||||
|
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
msg := strconv.Itoa(i)
|
||||||
|
err := c.Write(ctx, websocket.MessageText, []byte(msg))
|
||||||
|
if err != nil {
|
||||||
|
r.Error = xerrors.Errorf("write message: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ty, got, err := c.Read(ctx)
|
||||||
|
if err != nil {
|
||||||
|
r.Error = xerrors.Errorf("read message: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ty != websocket.MessageText {
|
||||||
|
r.Error = xerrors.Errorf("received incorrect message type: %v", ty)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(got) != msg {
|
||||||
|
r.Error = xerrors.Errorf("received incorrect message: wanted %q, got %q", msg, string(got))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Close(websocket.StatusGoingAway, "goodbye")
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsocketEchoServer struct {
|
||||||
|
Error error
|
||||||
|
Code int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WebsocketEchoServer) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
if s.Error != nil {
|
||||||
|
rw.WriteHeader(s.Code)
|
||||||
|
_, _ = rw.Write([]byte(s.Error.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
c, err := websocket.Accept(rw, r, &websocket.AcceptOptions{})
|
||||||
|
if err != nil {
|
||||||
|
httpapi.Write(ctx, rw, http.StatusBadRequest, "unable to accept: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer c.Close(websocket.StatusGoingAway, "goodbye")
|
||||||
|
|
||||||
|
echo := func() error {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
typ, r, err := c.Reader(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("get reader: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w, err := c.Writer(ctx, typ)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("get writer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("echo message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = w.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
err := echo()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
69
coderd/healthcheck/websocket_test.go
Normal file
69
coderd/healthcheck/websocket_test.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package healthcheck_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
|
"github.com/coder/coder/coderd/healthcheck"
|
||||||
|
"github.com/coder/coder/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWebsocket(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("OK", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
srv := httptest.NewServer(&healthcheck.WebsocketEchoServer{})
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
u, err := url.Parse(srv.URL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
wsReport := healthcheck.WebsocketReport{}
|
||||||
|
wsReport.Run(ctx, &healthcheck.WebsocketReportOptions{
|
||||||
|
AccessURL: u,
|
||||||
|
HTTPClient: srv.Client(),
|
||||||
|
APIKey: "test",
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, wsReport.Error)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Error", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
srv := httptest.NewServer(&healthcheck.WebsocketEchoServer{
|
||||||
|
Error: xerrors.New("test error"),
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
})
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
u, err := url.Parse(srv.URL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
wsReport := healthcheck.WebsocketReport{}
|
||||||
|
wsReport.Run(ctx, &healthcheck.WebsocketReportOptions{
|
||||||
|
AccessURL: u,
|
||||||
|
HTTPClient: srv.Client(),
|
||||||
|
APIKey: "test",
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Error(t, wsReport.Error)
|
||||||
|
assert.Equal(t, wsReport.Response.Body, "test error")
|
||||||
|
assert.Equal(t, wsReport.Response.Code, http.StatusBadRequest)
|
||||||
|
})
|
||||||
|
}
|
@ -207,7 +207,14 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pass": true,
|
"pass": true,
|
||||||
"time": "string"
|
"time": "string",
|
||||||
|
"websocket": {
|
||||||
|
"error": null,
|
||||||
|
"response": {
|
||||||
|
"body": "string",
|
||||||
|
"code": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -6377,7 +6377,14 @@ Parameter represents a set value for the scope.
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pass": true,
|
"pass": true,
|
||||||
"time": "string"
|
"time": "string",
|
||||||
|
"websocket": {
|
||||||
|
"error": null,
|
||||||
|
"response": {
|
||||||
|
"body": "string",
|
||||||
|
"code": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -6389,6 +6396,42 @@ Parameter represents a set value for the scope.
|
|||||||
| `derp` | [healthcheck.DERPReport](#healthcheckderpreport) | false | | |
|
| `derp` | [healthcheck.DERPReport](#healthcheckderpreport) | false | | |
|
||||||
| `pass` | boolean | false | | Healthy is true if the report returns no errors. |
|
| `pass` | boolean | false | | Healthy is true if the report returns no errors. |
|
||||||
| `time` | string | false | | Time is the time the report was generated at. |
|
| `time` | string | false | | Time is the time the report was generated at. |
|
||||||
|
| `websocket` | [healthcheck.WebsocketReport](#healthcheckwebsocketreport) | false | | |
|
||||||
|
|
||||||
|
## healthcheck.WebsocketReport
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": null,
|
||||||
|
"response": {
|
||||||
|
"body": "string",
|
||||||
|
"code": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
| Name | Type | Required | Restrictions | Description |
|
||||||
|
| ---------- | -------------------------------------------------------------- | -------- | ------------ | ----------- |
|
||||||
|
| `error` | any | false | | |
|
||||||
|
| `response` | [healthcheck.WebsocketResponse](#healthcheckwebsocketresponse) | false | | |
|
||||||
|
|
||||||
|
## healthcheck.WebsocketResponse
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"body": "string",
|
||||||
|
"code": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
| Name | Type | Required | Restrictions | Description |
|
||||||
|
| ------ | ------- | -------- | ------------ | ----------- |
|
||||||
|
| `body` | string | false | | |
|
||||||
|
| `code` | integer | false | | |
|
||||||
|
|
||||||
## netcheck.Report
|
## netcheck.Report
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user