Skip to content

Commit 59b842b

Browse files
committed
Merge branch 'main' into provisionerdaemons
2 parents 3022f7b + da05bbb commit 59b842b

31 files changed

+1981
-1007
lines changed

.github/workflows/mlc_config.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@
99
{
1010
"pattern": "developer.github.com"
1111
},
12+
{
13+
"pattern": "docs.github.com"
14+
},
15+
{
16+
"pattern": "support.google.com"
17+
},
1218
{
1319
"pattern": "tailscale.com"
1420
}

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
"promptui",
8989
"protobuf",
9090
"provisionerd",
91+
"provisionerdserver",
9192
"provisionersdk",
9293
"ptty",
9394
"ptys",

agent/apphealth.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"time"
88

99
"golang.org/x/xerrors"
10+
"github.com/google/uuid"
1011

1112
"cdr.dev/slog"
1213
"github.com/coder/coder/codersdk"
@@ -31,9 +32,9 @@ func NewWorkspaceAppHealthReporter(logger slog.Logger, apps []codersdk.Workspace
3132
}
3233

3334
hasHealthchecksEnabled := false
34-
health := make(map[string]codersdk.WorkspaceAppHealth, 0)
35+
health := make(map[uuid.UUID]codersdk.WorkspaceAppHealth, 0)
3536
for _, app := range apps {
36-
health[app.DisplayName] = app.Health
37+
health[app.ID] = app.Health
3738
if !hasHealthchecksEnabled && app.Health != codersdk.WorkspaceAppHealthDisabled {
3839
hasHealthchecksEnabled = true
3940
}
@@ -46,7 +47,7 @@ func NewWorkspaceAppHealthReporter(logger slog.Logger, apps []codersdk.Workspace
4647

4748
// run a ticker for each app health check.
4849
var mu sync.RWMutex
49-
failures := make(map[string]int, 0)
50+
failures := make(map[uuid.UUID]int, 0)
5051
for _, nextApp := range apps {
5152
if !shouldStartTicker(nextApp) {
5253
continue
@@ -85,21 +86,21 @@ func NewWorkspaceAppHealthReporter(logger slog.Logger, apps []codersdk.Workspace
8586
}()
8687
if err != nil {
8788
mu.Lock()
88-
if failures[app.DisplayName] < int(app.Healthcheck.Threshold) {
89+
if failures[app.ID] < int(app.Healthcheck.Threshold) {
8990
// increment the failure count and keep status the same.
9091
// we will change it when we hit the threshold.
91-
failures[app.DisplayName]++
92+
failures[app.ID]++
9293
} else {
9394
// set to unhealthy if we hit the failure threshold.
9495
// we stop incrementing at the threshold to prevent the failure value from increasing forever.
95-
health[app.DisplayName] = codersdk.WorkspaceAppHealthUnhealthy
96+
health[app.ID] = codersdk.WorkspaceAppHealthUnhealthy
9697
}
9798
mu.Unlock()
9899
} else {
99100
mu.Lock()
100101
// we only need one successful health check to be considered healthy.
101-
health[app.DisplayName] = codersdk.WorkspaceAppHealthHealthy
102-
failures[app.DisplayName] = 0
102+
health[app.ID] = codersdk.WorkspaceAppHealthHealthy
103+
failures[app.ID] = 0
103104
mu.Unlock()
104105
}
105106

@@ -155,7 +156,7 @@ func shouldStartTicker(app codersdk.WorkspaceApp) bool {
155156
return app.Healthcheck.URL != "" && app.Healthcheck.Interval > 0 && app.Healthcheck.Threshold > 0
156157
}
157158

158-
func healthChanged(old map[string]codersdk.WorkspaceAppHealth, new map[string]codersdk.WorkspaceAppHealth) bool {
159+
func healthChanged(old map[uuid.UUID]codersdk.WorkspaceAppHealth, new map[uuid.UUID]codersdk.WorkspaceAppHealth) bool {
159160
for name, newValue := range new {
160161
oldValue, found := old[name]
161162
if !found {
@@ -169,8 +170,8 @@ func healthChanged(old map[string]codersdk.WorkspaceAppHealth, new map[string]co
169170
return false
170171
}
171172

172-
func copyHealth(h1 map[string]codersdk.WorkspaceAppHealth) map[string]codersdk.WorkspaceAppHealth {
173-
h2 := make(map[string]codersdk.WorkspaceAppHealth, 0)
173+
func copyHealth(h1 map[uuid.UUID]codersdk.WorkspaceAppHealth) map[uuid.UUID]codersdk.WorkspaceAppHealth {
174+
h2 := make(map[uuid.UUID]codersdk.WorkspaceAppHealth, 0)
174175
for k, v := range h1 {
175176
h2[k] = v
176177
}

agent/apphealth_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ func TestAppHealth(t *testing.T) {
2727
defer cancel()
2828
apps := []codersdk.WorkspaceApp{
2929
{
30-
DisplayName: "app1",
30+
Slug: "app1",
3131
Healthcheck: codersdk.Healthcheck{},
3232
Health: codersdk.WorkspaceAppHealthDisabled,
3333
},
3434
{
35-
DisplayName: "app2",
35+
Slug: "app2",
3636
Healthcheck: codersdk.Healthcheck{
3737
// URL: We don't set the URL for this test because the setup will
3838
// create a httptest server for us and set it for us.
@@ -69,7 +69,7 @@ func TestAppHealth(t *testing.T) {
6969
defer cancel()
7070
apps := []codersdk.WorkspaceApp{
7171
{
72-
DisplayName: "app2",
72+
Slug: "app2",
7373
Healthcheck: codersdk.Healthcheck{
7474
// URL: We don't set the URL for this test because the setup will
7575
// create a httptest server for us and set it for us.
@@ -102,7 +102,7 @@ func TestAppHealth(t *testing.T) {
102102
defer cancel()
103103
apps := []codersdk.WorkspaceApp{
104104
{
105-
DisplayName: "app2",
105+
Slug: "app2",
106106
Healthcheck: codersdk.Healthcheck{
107107
// URL: We don't set the URL for this test because the setup will
108108
// create a httptest server for us and set it for us.
@@ -137,7 +137,7 @@ func TestAppHealth(t *testing.T) {
137137
defer cancel()
138138
apps := []codersdk.WorkspaceApp{
139139
{
140-
DisplayName: "app2",
140+
Slug: "app2",
141141
Healthcheck: codersdk.Healthcheck{
142142
// URL: We don't set the URL for this test because the setup will
143143
// create a httptest server for us and set it for us.
@@ -185,9 +185,9 @@ func setupAppReporter(ctx context.Context, t *testing.T, apps []codersdk.Workspa
185185
}
186186
postWorkspaceAgentAppHealth := func(_ context.Context, req codersdk.PostWorkspaceAppHealthsRequest) error {
187187
mu.Lock()
188-
for name, health := range req.Healths {
188+
for id, health := range req.Healths {
189189
for i, app := range apps {
190-
if app.DisplayName != name {
190+
if app.ID != id {
191191
continue
192192
}
193193
app.Health = health

cli/loadtest_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ func TestLoadTest(t *testing.T) {
138138

139139
t.Run("OutputFormats", func(t *testing.T) {
140140
t.Parallel()
141+
t.Skip("This test is flakey. See: https://github.com/coder/coder/actions/runs/3415360091/jobs/5684401383")
141142

142143
type outputFlag struct {
143144
format string

cli/root.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,14 +417,25 @@ func isTTY(cmd *cobra.Command) bool {
417417
// This accepts a reader to work with Cobra's "OutOrStdout"
418418
// function for simple testing.
419419
func isTTYOut(cmd *cobra.Command) bool {
420+
return isTTYWriter(cmd, cmd.OutOrStdout)
421+
}
422+
423+
// isTTYErr returns whether the passed reader is a TTY or not.
424+
// This accepts a reader to work with Cobra's "ErrOrStderr"
425+
// function for simple testing.
426+
func isTTYErr(cmd *cobra.Command) bool {
427+
return isTTYWriter(cmd, cmd.ErrOrStderr)
428+
}
429+
430+
func isTTYWriter(cmd *cobra.Command, writer func() io.Writer) bool {
420431
// If the `--force-tty` command is available, and set,
421432
// assume we're in a tty. This is primarily for cases on Windows
422433
// where we may not be able to reliably detect this automatically (ie, tests)
423434
forceTty, err := cmd.Flags().GetBool(varForceTty)
424435
if forceTty && err == nil {
425436
return true
426437
}
427-
file, ok := cmd.OutOrStdout().(*os.File)
438+
file, ok := writer().(*os.File)
428439
if !ok {
429440
return false
430441
}

cli/ssh.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"io"
8+
"net/url"
89
"os"
910
"path/filepath"
1011
"strings"
@@ -72,6 +73,11 @@ func ssh() *cobra.Command {
7273
return err
7374
}
7475

76+
updateWorkspaceBanner, outdated := verifyWorkspaceOutdated(client, workspace)
77+
if outdated && isTTYErr(cmd) {
78+
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), updateWorkspaceBanner)
79+
}
80+
7581
// OpenSSH passes stderr directly to the calling TTY.
7682
// This is required in "stdio" mode so a connecting indicator can be displayed.
7783
err = cliui.Agent(ctx, cmd.ErrOrStderr(), cliui.AgentOptions{
@@ -343,3 +349,18 @@ func notifyCondition(ctx context.Context, client *codersdk.Client, workspaceID u
343349
return deadline.Truncate(time.Minute), callback
344350
}
345351
}
352+
353+
// Verify if the user workspace is outdated and prepare an actionable message for user.
354+
func verifyWorkspaceOutdated(client *codersdk.Client, workspace codersdk.Workspace) (string, bool) {
355+
if !workspace.Outdated {
356+
return "", false // workspace is up-to-date
357+
}
358+
359+
workspaceLink := buildWorkspaceLink(client.URL, workspace)
360+
return fmt.Sprintf("👋 Your workspace is outdated! Update it here: %s\n", workspaceLink), true
361+
}
362+
363+
// Build the user workspace link which navigates to the Coder web UI.
364+
func buildWorkspaceLink(serverURL *url.URL, workspace codersdk.Workspace) *url.URL {
365+
return serverURL.ResolveReference(&url.URL{Path: fmt.Sprintf("@%s/%s", workspace.OwnerName, workspace.Name)})
366+
}

cli/ssh_internal_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package cli
2+
3+
import (
4+
"net/url"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/coder/coder/codersdk"
11+
)
12+
13+
const (
14+
fakeOwnerName = "fake-owner-name"
15+
fakeServerURL = "https://fake-foo-url"
16+
fakeWorkspaceName = "fake-workspace-name"
17+
)
18+
19+
func TestVerifyWorkspaceOutdated(t *testing.T) {
20+
t.Parallel()
21+
22+
serverURL, err := url.Parse(fakeServerURL)
23+
require.NoError(t, err)
24+
25+
client := codersdk.Client{URL: serverURL}
26+
27+
t.Run("Up-to-date", func(t *testing.T) {
28+
t.Parallel()
29+
30+
workspace := codersdk.Workspace{Name: fakeWorkspaceName, OwnerName: fakeOwnerName}
31+
32+
_, outdated := verifyWorkspaceOutdated(&client, workspace)
33+
34+
assert.False(t, outdated, "workspace should be up-to-date")
35+
})
36+
t.Run("Outdated", func(t *testing.T) {
37+
t.Parallel()
38+
39+
workspace := codersdk.Workspace{Name: fakeWorkspaceName, OwnerName: fakeOwnerName, Outdated: true}
40+
41+
updateWorkspaceBanner, outdated := verifyWorkspaceOutdated(&client, workspace)
42+
43+
assert.True(t, outdated, "workspace should be outdated")
44+
assert.NotEmpty(t, updateWorkspaceBanner, "workspace banner should be present")
45+
})
46+
}
47+
48+
func TestBuildWorkspaceLink(t *testing.T) {
49+
t.Parallel()
50+
51+
serverURL, err := url.Parse(fakeServerURL)
52+
require.NoError(t, err)
53+
54+
workspace := codersdk.Workspace{Name: fakeWorkspaceName, OwnerName: fakeOwnerName}
55+
workspaceLink := buildWorkspaceLink(serverURL, workspace)
56+
57+
assert.Equal(t, workspaceLink.String(), fakeServerURL+"/@"+fakeOwnerName+"/"+fakeWorkspaceName)
58+
}

coderd/coderd.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,8 @@ func New(options *Options) *API {
233233
httpmw.CSRF(options.SecureAuthCookie),
234234
)
235235

236+
r.Get("/healthz", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("OK")) })
237+
236238
apps := func(r chi.Router) {
237239
r.Use(
238240
tracing.Middleware(api.TracerProvider),

coderd/coderd_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package coderd_test
22

33
import (
44
"context"
5+
"io"
56
"net/http"
67
"net/netip"
78
"strconv"
@@ -114,3 +115,17 @@ func TestDERPLatencyCheck(t *testing.T) {
114115
defer res.Body.Close()
115116
require.Equal(t, http.StatusOK, res.StatusCode)
116117
}
118+
func TestHealthz(t *testing.T) {
119+
t.Parallel()
120+
client := coderdtest.New(t, nil)
121+
122+
res, err := client.Request(context.Background(), http.MethodGet, "/healthz", nil)
123+
require.NoError(t, err)
124+
defer res.Body.Close()
125+
126+
require.Equal(t, http.StatusOK, res.StatusCode)
127+
body, err := io.ReadAll(res.Body)
128+
require.NoError(t, err)
129+
130+
assert.Equal(t, "OK", string(body))
131+
}

coderd/coderdtest/authorize.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
3737

3838
assertRoute := map[string]RouteCheck{
3939
// These endpoints do not require auth
40+
"GET:/healthz": {NoAuthorize: true},
4041
"GET:/api/v2": {NoAuthorize: true},
4142
"GET:/api/v2/buildinfo": {NoAuthorize: true},
4243
"GET:/api/v2/users/first": {NoAuthorize: true},

0 commit comments

Comments
 (0)