mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(healthcheck): add accessurl check (#7193)
This commit is contained in:
Generated
+22
@@ -9749,6 +9749,24 @@ const docTemplate = `{
|
||||
"ParameterSourceSchemeData"
|
||||
]
|
||||
},
|
||||
"healthcheck.AccessURLReport": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"err": {},
|
||||
"healthy": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"healthzResponse": {
|
||||
"type": "string"
|
||||
},
|
||||
"reachable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"statusCode": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"healthcheck.DERPNodeReport": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -9814,6 +9832,7 @@ const docTemplate = `{
|
||||
"netcheck": {
|
||||
"$ref": "#/definitions/netcheck.Report"
|
||||
},
|
||||
"netcheck_err": {},
|
||||
"netcheck_logs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -9843,6 +9862,9 @@ const docTemplate = `{
|
||||
"healthcheck.Report": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"access_url": {
|
||||
"$ref": "#/definitions/healthcheck.AccessURLReport"
|
||||
},
|
||||
"derp": {
|
||||
"$ref": "#/definitions/healthcheck.DERPReport"
|
||||
},
|
||||
|
||||
Generated
+22
@@ -8812,6 +8812,24 @@
|
||||
"ParameterSourceSchemeData"
|
||||
]
|
||||
},
|
||||
"healthcheck.AccessURLReport": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"err": {},
|
||||
"healthy": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"healthzResponse": {
|
||||
"type": "string"
|
||||
},
|
||||
"reachable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"statusCode": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"healthcheck.DERPNodeReport": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -8877,6 +8895,7 @@
|
||||
"netcheck": {
|
||||
"$ref": "#/definitions/netcheck.Report"
|
||||
},
|
||||
"netcheck_err": {},
|
||||
"netcheck_logs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -8906,6 +8925,9 @@
|
||||
"healthcheck.Report": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"access_url": {
|
||||
"$ref": "#/definitions/healthcheck.AccessURLReport"
|
||||
},
|
||||
"derp": {
|
||||
"$ref": "#/definitions/healthcheck.DERPReport"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package healthcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
type AccessURLReport struct {
|
||||
Healthy bool
|
||||
Reachable bool
|
||||
StatusCode int
|
||||
HealthzResponse string
|
||||
Err error
|
||||
}
|
||||
|
||||
type AccessURLOptions struct {
|
||||
AccessURL *url.URL
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
func (r *AccessURLReport) Run(ctx context.Context, opts *AccessURLOptions) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if opts.Client == nil {
|
||||
opts.Client = http.DefaultClient
|
||||
}
|
||||
|
||||
accessURL, err := opts.AccessURL.Parse("/healthz")
|
||||
if err != nil {
|
||||
r.Err = xerrors.Errorf("parse healthz endpoint: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", accessURL.String(), nil)
|
||||
if err != nil {
|
||||
r.Err = xerrors.Errorf("create healthz request: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := opts.Client.Do(req)
|
||||
if err != nil {
|
||||
r.Err = xerrors.Errorf("get healthz endpoint: %w", err)
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
r.Err = xerrors.Errorf("read healthz response: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
r.Reachable = true
|
||||
r.Healthy = res.StatusCode == http.StatusOK
|
||||
r.StatusCode = res.StatusCode
|
||||
r.HealthzResponse = string(body)
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
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/coderdtest"
|
||||
"github.com/coder/coder/coderd/healthcheck"
|
||||
)
|
||||
|
||||
func TestAccessURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ctx, cancel = context.WithCancel(context.Background())
|
||||
report healthcheck.AccessURLReport
|
||||
client = coderdtest.New(t, nil)
|
||||
)
|
||||
defer cancel()
|
||||
|
||||
report.Run(ctx, &healthcheck.AccessURLOptions{
|
||||
AccessURL: client.URL,
|
||||
})
|
||||
|
||||
assert.True(t, report.Healthy)
|
||||
assert.True(t, report.Reachable)
|
||||
assert.Equal(t, http.StatusOK, report.StatusCode)
|
||||
assert.Equal(t, "OK", report.HealthzResponse)
|
||||
assert.NoError(t, report.Err)
|
||||
})
|
||||
|
||||
t.Run("404", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ctx, cancel = context.WithCancel(context.Background())
|
||||
report healthcheck.AccessURLReport
|
||||
resp = []byte("NOT OK")
|
||||
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write(resp)
|
||||
}))
|
||||
)
|
||||
defer cancel()
|
||||
defer srv.Close()
|
||||
|
||||
u, err := url.Parse(srv.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
report.Run(ctx, &healthcheck.AccessURLOptions{
|
||||
Client: srv.Client(),
|
||||
AccessURL: u,
|
||||
})
|
||||
|
||||
assert.False(t, report.Healthy)
|
||||
assert.True(t, report.Reachable)
|
||||
assert.Equal(t, http.StatusNotFound, report.StatusCode)
|
||||
assert.Equal(t, string(resp), report.HealthzResponse)
|
||||
assert.NoError(t, report.Err)
|
||||
})
|
||||
|
||||
t.Run("ClientErr", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ctx, cancel = context.WithCancel(context.Background())
|
||||
report healthcheck.AccessURLReport
|
||||
resp = []byte("OK")
|
||||
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(resp)
|
||||
}))
|
||||
client = srv.Client()
|
||||
)
|
||||
defer cancel()
|
||||
defer srv.Close()
|
||||
|
||||
expErr := xerrors.New("client error")
|
||||
client.Transport = roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
return nil, expErr
|
||||
})
|
||||
|
||||
u, err := url.Parse(srv.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
report.Run(ctx, &healthcheck.AccessURLOptions{
|
||||
Client: client,
|
||||
AccessURL: u,
|
||||
})
|
||||
|
||||
assert.False(t, report.Healthy)
|
||||
assert.False(t, report.Reachable)
|
||||
assert.Equal(t, 0, report.StatusCode)
|
||||
assert.Equal(t, "", report.HealthzResponse)
|
||||
assert.ErrorIs(t, report.Err, expErr)
|
||||
})
|
||||
}
|
||||
|
||||
type roundTripFunc func(r *http.Request) (*http.Response, error)
|
||||
|
||||
func (rt roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
return rt(r)
|
||||
}
|
||||
+21
-27
@@ -30,6 +30,7 @@ type DERPReport struct {
|
||||
Regions map[int]*DERPRegionReport `json:"regions"`
|
||||
|
||||
Netcheck *netcheck.Report `json:"netcheck"`
|
||||
NetcheckErr error `json:"netcheck_err"`
|
||||
NetcheckLogs []string `json:"netcheck_logs"`
|
||||
}
|
||||
|
||||
@@ -66,23 +67,22 @@ type DERPReportOptions struct {
|
||||
DERPMap *tailcfg.DERPMap
|
||||
}
|
||||
|
||||
func (r *DERPReport) Run(ctx context.Context, opts *DERPReportOptions) error {
|
||||
func (r *DERPReport) Run(ctx context.Context, opts *DERPReportOptions) {
|
||||
r.Healthy = true
|
||||
r.Regions = map[int]*DERPRegionReport{}
|
||||
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
wg := &sync.WaitGroup{}
|
||||
|
||||
wg.Add(len(opts.DERPMap.Regions))
|
||||
for _, region := range opts.DERPMap.Regions {
|
||||
region := region
|
||||
eg.Go(func() error {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
regionReport := DERPRegionReport{
|
||||
Region: region,
|
||||
}
|
||||
|
||||
err := regionReport.Run(ctx)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("run region report: %w", err)
|
||||
}
|
||||
regionReport.Run(ctx)
|
||||
|
||||
r.mu.Lock()
|
||||
r.Regions[region.RegionID] = ®ionReport
|
||||
@@ -90,8 +90,7 @@ func (r *DERPReport) Run(ctx context.Context, opts *DERPReportOptions) error {
|
||||
r.Healthy = false
|
||||
}
|
||||
r.mu.Unlock()
|
||||
return nil
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
ncLogf := func(format string, args ...interface{}) {
|
||||
@@ -103,32 +102,29 @@ func (r *DERPReport) Run(ctx context.Context, opts *DERPReportOptions) error {
|
||||
PortMapper: portmapper.NewClient(tslogger.WithPrefix(ncLogf, "portmap: "), nil),
|
||||
Logf: tslogger.WithPrefix(ncLogf, "netcheck: "),
|
||||
}
|
||||
ncReport, err := nc.GetReport(ctx, opts.DERPMap)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("run netcheck: %w", err)
|
||||
}
|
||||
r.Netcheck = ncReport
|
||||
r.Netcheck, r.NetcheckErr = nc.GetReport(ctx, opts.DERPMap)
|
||||
|
||||
return eg.Wait()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (r *DERPRegionReport) Run(ctx context.Context) error {
|
||||
func (r *DERPRegionReport) Run(ctx context.Context) {
|
||||
r.Healthy = true
|
||||
r.NodeReports = []*DERPNodeReport{}
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
|
||||
wg.Add(len(r.Region.Nodes))
|
||||
for _, node := range r.Region.Nodes {
|
||||
node := node
|
||||
eg.Go(func() error {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
nodeReport := DERPNodeReport{
|
||||
Node: node,
|
||||
Healthy: true,
|
||||
}
|
||||
|
||||
err := nodeReport.Run(ctx)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("run node report: %w", err)
|
||||
}
|
||||
nodeReport.Run(ctx)
|
||||
|
||||
r.mu.Lock()
|
||||
r.NodeReports = append(r.NodeReports, &nodeReport)
|
||||
@@ -136,11 +132,10 @@ func (r *DERPRegionReport) Run(ctx context.Context) error {
|
||||
r.Healthy = false
|
||||
}
|
||||
r.mu.Unlock()
|
||||
return nil
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
return eg.Wait()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (r *DERPNodeReport) derpURL() *url.URL {
|
||||
@@ -159,7 +154,7 @@ func (r *DERPNodeReport) derpURL() *url.URL {
|
||||
return derpURL
|
||||
}
|
||||
|
||||
func (r *DERPNodeReport) Run(ctx context.Context) error {
|
||||
func (r *DERPNodeReport) Run(ctx context.Context) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -179,7 +174,6 @@ func (r *DERPNodeReport) Run(ctx context.Context) error {
|
||||
r.STUN.Error != nil {
|
||||
r.Healthy = false
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *DERPNodeReport) doExchangeMessage(ctx context.Context) {
|
||||
|
||||
@@ -58,8 +58,7 @@ func TestDERP(t *testing.T) {
|
||||
}
|
||||
)
|
||||
|
||||
err := report.Run(ctx, opts)
|
||||
require.NoError(t, err)
|
||||
report.Run(ctx, opts)
|
||||
|
||||
assert.True(t, report.Healthy)
|
||||
for _, region := range report.Regions {
|
||||
@@ -100,8 +99,7 @@ func TestDERP(t *testing.T) {
|
||||
// Only include the Dallas region
|
||||
opts.DERPMap.Regions = map[int]*tailcfg.DERPRegion{9: opts.DERPMap.Regions[9]}
|
||||
|
||||
err := report.Run(ctx, opts)
|
||||
require.NoError(t, err)
|
||||
report.Run(ctx, opts)
|
||||
|
||||
assert.True(t, report.Healthy)
|
||||
for _, region := range report.Regions {
|
||||
@@ -215,8 +213,7 @@ func TestDERP(t *testing.T) {
|
||||
}
|
||||
)
|
||||
|
||||
err := report.Run(ctx, opts)
|
||||
require.NoError(t, err)
|
||||
report.Run(ctx, opts)
|
||||
|
||||
assert.True(t, report.Healthy)
|
||||
for _, region := range report.Regions {
|
||||
|
||||
@@ -2,9 +2,11 @@ package healthcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
@@ -14,28 +16,49 @@ type Report struct {
|
||||
// Healthy is true if the report returns no errors.
|
||||
Healthy bool `json:"pass"`
|
||||
|
||||
DERP DERPReport `json:"derp"`
|
||||
DERP DERPReport `json:"derp"`
|
||||
AccessURL AccessURLReport `json:"access_url"`
|
||||
|
||||
// TODO
|
||||
// AccessURL AccessURLReport
|
||||
// Websocket WebsocketReport
|
||||
// TODO:
|
||||
// Websocket WebsocketReport `json:"websocket"`
|
||||
}
|
||||
|
||||
type ReportOptions struct {
|
||||
// TODO: support getting this over HTTP?
|
||||
DERPMap *tailcfg.DERPMap
|
||||
DERPMap *tailcfg.DERPMap
|
||||
AccessURL *url.URL
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
func Run(ctx context.Context, opts *ReportOptions) (*Report, error) {
|
||||
var report Report
|
||||
|
||||
err := report.DERP.Run(ctx, &DERPReportOptions{
|
||||
DERPMap: opts.DERPMap,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("run derp: %w", err)
|
||||
}
|
||||
wg := &sync.WaitGroup{}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
report.DERP.Run(ctx, &DERPReportOptions{
|
||||
DERPMap: opts.DERPMap,
|
||||
})
|
||||
}()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
report.AccessURL.Run(ctx, &AccessURLOptions{
|
||||
AccessURL: opts.AccessURL,
|
||||
Client: opts.Client,
|
||||
})
|
||||
}()
|
||||
|
||||
// wg.Add(1)
|
||||
// go func() {
|
||||
// defer wg.Done()
|
||||
// report.Websocket.Run(ctx, opts.AccessURL)
|
||||
// }()
|
||||
|
||||
wg.Wait()
|
||||
report.Time = time.Now()
|
||||
report.Healthy = report.DERP.Healthy
|
||||
return &report, nil
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
package healthcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type WebsocketReport struct{}
|
||||
|
||||
func (*WebsocketReport) Run(ctx context.Context, accessURL *url.URL) {
|
||||
_, _ = ctx, accessURL
|
||||
}
|
||||
|
||||
@@ -39,6 +39,13 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \
|
||||
|
||||
```json
|
||||
{
|
||||
"access_url": {
|
||||
"err": null,
|
||||
"healthy": true,
|
||||
"healthzResponse": "string",
|
||||
"reachable": true,
|
||||
"statusCode": 0
|
||||
},
|
||||
"derp": {
|
||||
"healthy": true,
|
||||
"netcheck": {
|
||||
@@ -71,6 +78,7 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \
|
||||
"udp": true,
|
||||
"upnP": "string"
|
||||
},
|
||||
"netcheck_err": null,
|
||||
"netcheck_logs": ["string"],
|
||||
"regions": {
|
||||
"property1": {
|
||||
|
||||
+38
-5
@@ -5563,6 +5563,28 @@ Parameter represents a set value for the scope.
|
||||
| `none` |
|
||||
| `data` |
|
||||
|
||||
## healthcheck.AccessURLReport
|
||||
|
||||
```json
|
||||
{
|
||||
"err": null,
|
||||
"healthy": true,
|
||||
"healthzResponse": "string",
|
||||
"reachable": true,
|
||||
"statusCode": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ----------------- | ------- | -------- | ------------ | ----------- |
|
||||
| `err` | any | false | | |
|
||||
| `healthy` | boolean | false | | |
|
||||
| `healthzResponse` | string | false | | |
|
||||
| `reachable` | boolean | false | | |
|
||||
| `statusCode` | integer | false | | |
|
||||
|
||||
## healthcheck.DERPNodeReport
|
||||
|
||||
```json
|
||||
@@ -5711,6 +5733,7 @@ Parameter represents a set value for the scope.
|
||||
"udp": true,
|
||||
"upnP": "string"
|
||||
},
|
||||
"netcheck_err": null,
|
||||
"netcheck_logs": ["string"],
|
||||
"regions": {
|
||||
"property1": {
|
||||
@@ -5833,6 +5856,7 @@ Parameter represents a set value for the scope.
|
||||
| ------------------ | ------------------------------------------------------------ | -------- | ------------ | ----------- |
|
||||
| `healthy` | boolean | false | | |
|
||||
| `netcheck` | [netcheck.Report](#netcheckreport) | false | | |
|
||||
| `netcheck_err` | any | false | | |
|
||||
| `netcheck_logs` | array of string | false | | |
|
||||
| `regions` | object | false | | |
|
||||
| » `[any property]` | [healthcheck.DERPRegionReport](#healthcheckderpregionreport) | false | | |
|
||||
@@ -5859,6 +5883,13 @@ Parameter represents a set value for the scope.
|
||||
|
||||
```json
|
||||
{
|
||||
"access_url": {
|
||||
"err": null,
|
||||
"healthy": true,
|
||||
"healthzResponse": "string",
|
||||
"reachable": true,
|
||||
"statusCode": 0
|
||||
},
|
||||
"derp": {
|
||||
"healthy": true,
|
||||
"netcheck": {
|
||||
@@ -5891,6 +5922,7 @@ Parameter represents a set value for the scope.
|
||||
"udp": true,
|
||||
"upnP": "string"
|
||||
},
|
||||
"netcheck_err": null,
|
||||
"netcheck_logs": ["string"],
|
||||
"regions": {
|
||||
"property1": {
|
||||
@@ -6012,11 +6044,12 @@ Parameter represents a set value for the scope.
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ------ | ------------------------------------------------ | -------- | ------------ | ------------------------------------------------ |
|
||||
| `derp` | [healthcheck.DERPReport](#healthcheckderpreport) | false | | |
|
||||
| `pass` | boolean | false | | Healthy is true if the report returns no errors. |
|
||||
| `time` | string | false | | Time is the time the report was generated at. |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ------------ | ---------------------------------------------------------- | -------- | ------------ | ------------------------------------------------ |
|
||||
| `access_url` | [healthcheck.AccessURLReport](#healthcheckaccessurlreport) | false | | |
|
||||
| `derp` | [healthcheck.DERPReport](#healthcheckderpreport) | false | | |
|
||||
| `pass` | boolean | false | | Healthy is true if the report returns no errors. |
|
||||
| `time` | string | false | | Time is the time the report was generated at. |
|
||||
|
||||
## netcheck.Report
|
||||
|
||||
|
||||
Reference in New Issue
Block a user