Skip to content

Commit 0181226

Browse files
committed
feat(healthcheck): add accessurl check
1 parent fa5387c commit 0181226

File tree

6 files changed

+242
-45
lines changed

6 files changed

+242
-45
lines changed

coderd/healthcheck/accessurl.go

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package healthcheck
2+
3+
import (
4+
"context"
5+
"io"
6+
"net/http"
7+
"net/url"
8+
"time"
9+
10+
"golang.org/x/xerrors"
11+
)
12+
13+
type AccessURLReport struct {
14+
Healthy bool
15+
Reachable bool
16+
StatusCode int
17+
HealthzResponse string
18+
Err error
19+
}
20+
21+
type AccessURLOptions struct {
22+
AccessURL *url.URL
23+
Client *http.Client
24+
}
25+
26+
func (r *AccessURLReport) Run(ctx context.Context, opts *AccessURLOptions) {
27+
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
28+
defer cancel()
29+
30+
if opts.Client == nil {
31+
opts.Client = http.DefaultClient
32+
}
33+
34+
accessURL, err := opts.AccessURL.Parse("/healthz")
35+
if err != nil {
36+
r.Err = xerrors.Errorf("parse healthz endpoint: %w", err)
37+
return
38+
}
39+
40+
req, err := http.NewRequestWithContext(ctx, "GET", accessURL.String(), nil)
41+
if err != nil {
42+
r.Err = xerrors.Errorf("create healthz request: %w", err)
43+
return
44+
}
45+
46+
res, err := opts.Client.Do(req)
47+
if err != nil {
48+
r.Err = xerrors.Errorf("get healthz endpoint: %w", err)
49+
return
50+
}
51+
defer res.Body.Close()
52+
53+
body, err := io.ReadAll(res.Body)
54+
if err != nil {
55+
r.Err = xerrors.Errorf("read healthz response: %w", err)
56+
return
57+
}
58+
59+
r.Reachable = true
60+
r.Healthy = res.StatusCode == http.StatusOK
61+
r.StatusCode = res.StatusCode
62+
r.HealthzResponse = string(body)
63+
}

coderd/healthcheck/accessurl_test.go

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package healthcheck_test
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"net/url"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
"golang.org/x/xerrors"
13+
14+
"github.com/coder/coder/coderd/coderdtest"
15+
"github.com/coder/coder/coderd/healthcheck"
16+
)
17+
18+
func TestAccessURL(t *testing.T) {
19+
t.Parallel()
20+
21+
t.Run("OK", func(t *testing.T) {
22+
t.Parallel()
23+
24+
var (
25+
ctx, cancel = context.WithCancel(context.Background())
26+
report healthcheck.AccessURLReport
27+
client = coderdtest.New(t, nil)
28+
)
29+
defer cancel()
30+
31+
report.Run(ctx, &healthcheck.AccessURLOptions{
32+
AccessURL: client.URL,
33+
})
34+
35+
assert.True(t, report.Healthy)
36+
assert.True(t, report.Reachable)
37+
assert.Equal(t, http.StatusOK, report.StatusCode)
38+
assert.Equal(t, "OK", report.HealthzResponse)
39+
assert.NoError(t, report.Err)
40+
})
41+
42+
t.Run("404", func(t *testing.T) {
43+
t.Parallel()
44+
45+
var (
46+
ctx, cancel = context.WithCancel(context.Background())
47+
report healthcheck.AccessURLReport
48+
resp = []byte("NOT OK")
49+
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
50+
w.WriteHeader(http.StatusNotFound)
51+
w.Write(resp)
52+
}))
53+
)
54+
defer cancel()
55+
defer srv.Close()
56+
57+
u, err := url.Parse(srv.URL)
58+
require.NoError(t, err)
59+
60+
report.Run(ctx, &healthcheck.AccessURLOptions{
61+
Client: srv.Client(),
62+
AccessURL: u,
63+
})
64+
65+
assert.False(t, report.Healthy)
66+
assert.True(t, report.Reachable)
67+
assert.Equal(t, http.StatusNotFound, report.StatusCode)
68+
assert.Equal(t, string(resp), report.HealthzResponse)
69+
assert.NoError(t, report.Err)
70+
})
71+
72+
t.Run("ClientErr", func(t *testing.T) {
73+
t.Parallel()
74+
75+
var (
76+
ctx, cancel = context.WithCancel(context.Background())
77+
report healthcheck.AccessURLReport
78+
resp = []byte("OK")
79+
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
80+
w.WriteHeader(http.StatusOK)
81+
w.Write(resp)
82+
}))
83+
client = srv.Client()
84+
)
85+
defer cancel()
86+
defer srv.Close()
87+
88+
expErr := xerrors.New("client error")
89+
client.Transport = roundTripFunc(func(r *http.Request) (*http.Response, error) {
90+
return nil, expErr
91+
})
92+
93+
u, err := url.Parse(srv.URL)
94+
require.NoError(t, err)
95+
96+
report.Run(ctx, &healthcheck.AccessURLOptions{
97+
Client: client,
98+
AccessURL: u,
99+
})
100+
101+
assert.False(t, report.Healthy)
102+
assert.False(t, report.Reachable)
103+
assert.Equal(t, 0, report.StatusCode)
104+
assert.Equal(t, "", report.HealthzResponse)
105+
assert.ErrorIs(t, report.Err, expErr)
106+
})
107+
}
108+
109+
type roundTripFunc func(r *http.Request) (*http.Response, error)
110+
111+
func (rt roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
112+
return rt(r)
113+
}

coderd/healthcheck/derp.go

+21-27
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type DERPReport struct {
3030
Regions map[int]*DERPRegionReport `json:"regions"`
3131

3232
Netcheck *netcheck.Report `json:"netcheck"`
33+
NetcheckErr error `json:"netcheck_err"`
3334
NetcheckLogs []string `json:"netcheck_logs"`
3435
}
3536

@@ -66,32 +67,30 @@ type DERPReportOptions struct {
6667
DERPMap *tailcfg.DERPMap
6768
}
6869

69-
func (r *DERPReport) Run(ctx context.Context, opts *DERPReportOptions) error {
70+
func (r *DERPReport) Run(ctx context.Context, opts *DERPReportOptions) {
7071
r.Healthy = true
7172
r.Regions = map[int]*DERPRegionReport{}
7273

73-
eg, ctx := errgroup.WithContext(ctx)
74+
wg := &sync.WaitGroup{}
7475

76+
wg.Add(len(opts.DERPMap.Regions))
7577
for _, region := range opts.DERPMap.Regions {
7678
region := region
77-
eg.Go(func() error {
79+
go func() {
80+
defer wg.Done()
7881
regionReport := DERPRegionReport{
7982
Region: region,
8083
}
8184

82-
err := regionReport.Run(ctx)
83-
if err != nil {
84-
return xerrors.Errorf("run region report: %w", err)
85-
}
85+
regionReport.Run(ctx)
8686

8787
r.mu.Lock()
8888
r.Regions[region.RegionID] = &regionReport
8989
if !regionReport.Healthy {
9090
r.Healthy = false
9191
}
9292
r.mu.Unlock()
93-
return nil
94-
})
93+
}()
9594
}
9695

9796
ncLogf := func(format string, args ...interface{}) {
@@ -103,44 +102,40 @@ func (r *DERPReport) Run(ctx context.Context, opts *DERPReportOptions) error {
103102
PortMapper: portmapper.NewClient(tslogger.WithPrefix(ncLogf, "portmap: "), nil),
104103
Logf: tslogger.WithPrefix(ncLogf, "netcheck: "),
105104
}
106-
ncReport, err := nc.GetReport(ctx, opts.DERPMap)
107-
if err != nil {
108-
return xerrors.Errorf("run netcheck: %w", err)
109-
}
110-
r.Netcheck = ncReport
105+
r.Netcheck, r.NetcheckErr = nc.GetReport(ctx, opts.DERPMap)
111106

112-
return eg.Wait()
107+
wg.Wait()
113108
}
114109

115-
func (r *DERPRegionReport) Run(ctx context.Context) error {
110+
func (r *DERPRegionReport) Run(ctx context.Context) {
116111
r.Healthy = true
117112
r.NodeReports = []*DERPNodeReport{}
118-
eg, ctx := errgroup.WithContext(ctx)
119113

114+
wg := &sync.WaitGroup{}
115+
116+
wg.Add(len(r.Region.Nodes))
120117
for _, node := range r.Region.Nodes {
121118
node := node
122-
eg.Go(func() error {
119+
go func() {
120+
defer wg.Done()
121+
123122
nodeReport := DERPNodeReport{
124123
Node: node,
125124
Healthy: true,
126125
}
127126

128-
err := nodeReport.Run(ctx)
129-
if err != nil {
130-
return xerrors.Errorf("run node report: %w", err)
131-
}
127+
nodeReport.Run(ctx)
132128

133129
r.mu.Lock()
134130
r.NodeReports = append(r.NodeReports, &nodeReport)
135131
if !nodeReport.Healthy {
136132
r.Healthy = false
137133
}
138134
r.mu.Unlock()
139-
return nil
140-
})
135+
}()
141136
}
142137

143-
return eg.Wait()
138+
wg.Wait()
144139
}
145140

146141
func (r *DERPNodeReport) derpURL() *url.URL {
@@ -159,7 +154,7 @@ func (r *DERPNodeReport) derpURL() *url.URL {
159154
return derpURL
160155
}
161156

162-
func (r *DERPNodeReport) Run(ctx context.Context) error {
157+
func (r *DERPNodeReport) Run(ctx context.Context) {
163158
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
164159
defer cancel()
165160

@@ -179,7 +174,6 @@ func (r *DERPNodeReport) Run(ctx context.Context) error {
179174
r.STUN.Error != nil {
180175
r.Healthy = false
181176
}
182-
return nil
183177
}
184178

185179
func (r *DERPNodeReport) doExchangeMessage(ctx context.Context) {

coderd/healthcheck/derp_test.go

+3-6
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,7 @@ func TestDERP(t *testing.T) {
5858
}
5959
)
6060

61-
err := report.Run(ctx, opts)
62-
require.NoError(t, err)
61+
report.Run(ctx, opts)
6362

6463
assert.True(t, report.Healthy)
6564
for _, region := range report.Regions {
@@ -100,8 +99,7 @@ func TestDERP(t *testing.T) {
10099
// Only include the Dallas region
101100
opts.DERPMap.Regions = map[int]*tailcfg.DERPRegion{9: opts.DERPMap.Regions[9]}
102101

103-
err := report.Run(ctx, opts)
104-
require.NoError(t, err)
102+
report.Run(ctx, opts)
105103

106104
assert.True(t, report.Healthy)
107105
for _, region := range report.Regions {
@@ -215,8 +213,7 @@ func TestDERP(t *testing.T) {
215213
}
216214
)
217215

218-
err := report.Run(ctx, opts)
219-
require.NoError(t, err)
216+
report.Run(ctx, opts)
220217

221218
assert.True(t, report.Healthy)
222219
for _, region := range report.Regions {

0 commit comments

Comments
 (0)