feat(healthcheck): add accessurl check (#7193)

This commit is contained in:
Colin Adler
2023-04-18 15:52:00 -05:00
committed by GitHub
parent fa5387ce07
commit 57c4de4647
10 changed files with 334 additions and 50 deletions
+22
View File
@@ -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"
},
+22
View File
@@ -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"
},
+63
View File
@@ -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)
}
+113
View File
@@ -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
View File
@@ -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] = &regionReport
@@ -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) {
+3 -6
View File
@@ -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 {
+35 -12
View File
@@ -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
+9
View File
@@ -1,3 +1,12 @@
package healthcheck
import (
"context"
"net/url"
)
type WebsocketReport struct{}
func (*WebsocketReport) Run(ctx context.Context, accessURL *url.URL) {
_, _ = ctx, accessURL
}
+8
View File
@@ -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
View File
@@ -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