Skip to content

Commit c00d1f0

Browse files
committed
Merge branch 'main' of github.com:coder/coder into bq/settings-page
2 parents 3cb5edc + 656dcc0 commit c00d1f0

File tree

95 files changed

+2063
-1079
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

95 files changed

+2063
-1079
lines changed

cli/server_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import (
2424
"testing"
2525
"time"
2626

27-
"github.com/go-chi/chi"
27+
"github.com/go-chi/chi/v5"
2828
"github.com/stretchr/testify/assert"
2929
"github.com/stretchr/testify/require"
3030
"go.uber.org/goleak"

coderd/activitybump.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package coderd
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"errors"
7+
"time"
8+
9+
"golang.org/x/xerrors"
10+
11+
"cdr.dev/slog"
12+
"github.com/coder/coder/coderd/database"
13+
)
14+
15+
// activityBumpWorkspace automatically bumps the workspace's auto-off timer
16+
// if it is set to expire soon.
17+
func activityBumpWorkspace(log slog.Logger, db database.Store, workspace database.Workspace) {
18+
// We set a short timeout so if the app is under load, these
19+
// low priority operations fail first.
20+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
21+
defer cancel()
22+
23+
err := db.InTx(func(s database.Store) error {
24+
build, err := s.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
25+
if errors.Is(err, sql.ErrNoRows) {
26+
return nil
27+
} else if err != nil {
28+
return xerrors.Errorf("get latest workspace build: %w", err)
29+
}
30+
31+
job, err := s.GetProvisionerJobByID(ctx, build.JobID)
32+
if err != nil {
33+
return xerrors.Errorf("get provisioner job: %w", err)
34+
}
35+
36+
if build.Transition != database.WorkspaceTransitionStart || !job.CompletedAt.Valid {
37+
return nil
38+
}
39+
40+
if build.Deadline.IsZero() {
41+
// Workspace shutdown is manual
42+
return nil
43+
}
44+
45+
// We sent bumpThreshold slightly under bumpAmount to minimize DB writes.
46+
const (
47+
bumpAmount = time.Hour
48+
bumpThreshold = time.Hour - (time.Minute * 10)
49+
)
50+
51+
if !build.Deadline.Before(time.Now().Add(bumpThreshold)) {
52+
return nil
53+
}
54+
55+
newDeadline := database.Now().Add(bumpAmount)
56+
57+
if err := s.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{
58+
ID: build.ID,
59+
UpdatedAt: database.Now(),
60+
ProvisionerState: build.ProvisionerState,
61+
Deadline: newDeadline,
62+
}); err != nil {
63+
return xerrors.Errorf("update workspace build: %w", err)
64+
}
65+
return nil
66+
})
67+
if err != nil {
68+
log.Error(
69+
ctx, "bump failed",
70+
slog.Error(err),
71+
slog.F("workspace_id", workspace.ID),
72+
)
73+
} else {
74+
log.Debug(
75+
ctx, "bumped deadline from activity",
76+
slog.F("workspace_id", workspace.ID),
77+
)
78+
}
79+
}

coderd/activitybump_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package coderd_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"github.com/stretchr/testify/require"
9+
10+
"cdr.dev/slog/sloggers/slogtest"
11+
12+
"github.com/coder/coder/coderd/coderdtest"
13+
"github.com/coder/coder/coderd/database"
14+
"github.com/coder/coder/codersdk"
15+
"github.com/coder/coder/testutil"
16+
)
17+
18+
func TestWorkspaceActivityBump(t *testing.T) {
19+
t.Parallel()
20+
21+
ctx := context.Background()
22+
23+
setupActivityTest := func(t *testing.T) (client *codersdk.Client, workspace codersdk.Workspace, assertBumped func(want bool)) {
24+
var ttlMillis int64 = 60 * 1000
25+
26+
client, _, workspace, _ = setupProxyTest(t, func(cwr *codersdk.CreateWorkspaceRequest) {
27+
cwr.TTLMillis = &ttlMillis
28+
})
29+
30+
// Sanity-check that deadline is near.
31+
workspace, err := client.Workspace(ctx, workspace.ID)
32+
require.NoError(t, err)
33+
require.WithinDuration(t,
34+
time.Now().Add(time.Duration(ttlMillis)*time.Millisecond),
35+
workspace.LatestBuild.Deadline.Time, testutil.WaitShort,
36+
)
37+
firstDeadline := workspace.LatestBuild.Deadline.Time
38+
39+
_ = coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
40+
41+
return client, workspace, func(want bool) {
42+
if !want {
43+
// It is difficult to test the absence of a call in a non-racey
44+
// way. In general, it is difficult for the API to generate
45+
// false positive activity since Agent networking event
46+
// is required. The Activity Bump behavior is also coupled with
47+
// Last Used, so it would be obvious to the user if we
48+
// are falsely recognizing activity.
49+
time.Sleep(testutil.IntervalMedium)
50+
workspace, err = client.Workspace(ctx, workspace.ID)
51+
require.NoError(t, err)
52+
require.Equal(t, workspace.LatestBuild.Deadline.Time, firstDeadline)
53+
return
54+
}
55+
56+
// The Deadline bump occurs asynchronously.
57+
require.Eventuallyf(t,
58+
func() bool {
59+
workspace, err = client.Workspace(ctx, workspace.ID)
60+
require.NoError(t, err)
61+
return workspace.LatestBuild.Deadline.Time != firstDeadline
62+
},
63+
testutil.WaitShort, testutil.IntervalFast,
64+
"deadline %v never updated", firstDeadline,
65+
)
66+
67+
require.WithinDuration(t, database.Now().Add(time.Hour), workspace.LatestBuild.Deadline.Time, time.Second)
68+
}
69+
}
70+
71+
t.Run("Dial", func(t *testing.T) {
72+
t.Parallel()
73+
74+
client, workspace, assertBumped := setupActivityTest(t)
75+
76+
resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
77+
conn, err := client.DialWorkspaceAgentTailnet(ctx, slogtest.Make(t, nil), resources[0].Agents[0].ID)
78+
require.NoError(t, err)
79+
defer conn.Close()
80+
81+
sshConn, err := conn.SSHClient()
82+
require.NoError(t, err)
83+
_ = sshConn.Close()
84+
85+
assertBumped(true)
86+
})
87+
88+
t.Run("NoBump", func(t *testing.T) {
89+
t.Parallel()
90+
91+
client, workspace, assertBumped := setupActivityTest(t)
92+
93+
// Benign operations like retrieving resources must not
94+
// bump the deadline.
95+
_, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
96+
require.NoError(t, err)
97+
98+
assertBumped(false)
99+
})
100+
}

coderd/audit.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ import (
2121
)
2222

2323
func (api *API) auditLogs(rw http.ResponseWriter, r *http.Request) {
24+
ctx := r.Context()
2425
if !api.Authorize(r, rbac.ActionRead, rbac.ResourceAuditLog) {
2526
httpapi.Forbidden(rw)
2627
return
2728
}
2829

29-
ctx := r.Context()
3030
page, ok := parsePagination(rw, r)
3131
if !ok {
3232
return
@@ -35,7 +35,7 @@ func (api *API) auditLogs(rw http.ResponseWriter, r *http.Request) {
3535
queryStr := r.URL.Query().Get("q")
3636
filter, errs := auditSearchQuery(queryStr)
3737
if len(errs) > 0 {
38-
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
38+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
3939
Message: "Invalid audit search query.",
4040
Validations: errs,
4141
})
@@ -56,7 +56,7 @@ func (api *API) auditLogs(rw http.ResponseWriter, r *http.Request) {
5656
return
5757
}
5858

59-
httpapi.Write(rw, http.StatusOK, codersdk.AuditLogResponse{
59+
httpapi.Write(ctx, rw, http.StatusOK, codersdk.AuditLogResponse{
6060
AuditLogs: convertAuditLogs(dblogs),
6161
})
6262
}
@@ -71,7 +71,7 @@ func (api *API) auditLogCount(rw http.ResponseWriter, r *http.Request) {
7171
queryStr := r.URL.Query().Get("q")
7272
filter, errs := auditSearchQuery(queryStr)
7373
if len(errs) > 0 {
74-
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
74+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
7575
Message: "Invalid audit search query.",
7676
Validations: errs,
7777
})
@@ -90,7 +90,7 @@ func (api *API) auditLogCount(rw http.ResponseWriter, r *http.Request) {
9090
return
9191
}
9292

93-
httpapi.Write(rw, http.StatusOK, codersdk.AuditLogCountResponse{
93+
httpapi.Write(ctx, rw, http.StatusOK, codersdk.AuditLogCountResponse{
9494
Count: count,
9595
})
9696
}
@@ -131,7 +131,7 @@ func (api *API) generateFakeAuditLog(rw http.ResponseWriter, r *http.Request) {
131131
}
132132

133133
var params codersdk.CreateTestAuditLogRequest
134-
if !httpapi.Read(rw, r, &params) {
134+
if !httpapi.Read(ctx, rw, r, &params) {
135135
return
136136
}
137137
if params.Action == "" {

coderd/coderd.go

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ func New(options *Options) *API {
188188
// Build-Version is helpful for debugging.
189189
func(next http.Handler) http.Handler {
190190
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
191-
w.Header().Add("Build-Version", buildinfo.Version())
191+
w.Header().Add("X-Coder-Build-Version", buildinfo.Version())
192192
next.ServeHTTP(w, r)
193193
})
194194
},
@@ -222,18 +222,14 @@ func New(options *Options) *API {
222222
r.Route("/api/v2", func(r chi.Router) {
223223
api.APIHandler = r
224224

225-
r.NotFound(func(rw http.ResponseWriter, r *http.Request) {
226-
httpapi.Write(rw, http.StatusNotFound, codersdk.Response{
227-
Message: "Route not found.",
228-
})
229-
})
225+
r.NotFound(func(rw http.ResponseWriter, r *http.Request) { httpapi.RouteNotFound(rw) })
230226
r.Use(
231227
tracing.Middleware(api.TracerProvider),
232228
// Specific routes can specify smaller limits.
233229
httpmw.RateLimitPerMinute(options.APIRateLimit),
234230
)
235231
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
236-
httpapi.Write(w, http.StatusOK, codersdk.Response{
232+
httpapi.Write(r.Context(), w, http.StatusOK, codersdk.Response{
237233
//nolint:gocritic
238234
Message: "👋",
239235
})
@@ -243,7 +239,7 @@ func New(options *Options) *API {
243239

244240
r.Route("/buildinfo", func(r chi.Router) {
245241
r.Get("/", func(rw http.ResponseWriter, r *http.Request) {
246-
httpapi.Write(rw, http.StatusOK, codersdk.BuildInfoResponse{
242+
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.BuildInfoResponse{
247243
ExternalURL: buildinfo.ExternalURL(),
248244
Version: buildinfo.Version(),
249245
})
@@ -434,7 +430,7 @@ func New(options *Options) *API {
434430
// error message when transitioning from WebRTC to Tailscale. See:
435431
// https://github.com/coder/coder/issues/4126
436432
r.Get("/dial", func(w http.ResponseWriter, r *http.Request) {
437-
httpapi.Write(w, http.StatusGone, codersdk.Response{
433+
httpapi.Write(r.Context(), w, http.StatusGone, codersdk.Response{
438434
Message: "Your Coder CLI is out of date, and requires v0.8.15+ to connect!",
439435
})
440436
})

coderd/csp.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func (api *API) logReportCSPViolations(rw http.ResponseWriter, r *http.Request)
2323
err := dec.Decode(&v)
2424
if err != nil {
2525
api.Logger.Warn(ctx, "csp violation", slog.Error(err))
26-
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
26+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
2727
Message: "Failed to read body, invalid json.",
2828
Detail: err.Error(),
2929
})
@@ -36,5 +36,5 @@ func (api *API) logReportCSPViolations(rw http.ResponseWriter, r *http.Request)
3636
}
3737
api.Logger.Warn(ctx, "csp violation", fields...)
3838

39-
httpapi.Write(rw, http.StatusOK, "ok")
39+
httpapi.Write(ctx, rw, http.StatusOK, "ok")
4040
}

coderd/files.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
)
2020

2121
func (api *API) postFile(rw http.ResponseWriter, r *http.Request) {
22+
ctx := r.Context()
2223
apiKey := httpmw.APIKey(r)
2324
// This requires the site wide action to create files.
2425
// Once created, a user can read their own files uploaded
@@ -32,7 +33,7 @@ func (api *API) postFile(rw http.ResponseWriter, r *http.Request) {
3233
switch contentType {
3334
case "application/x-tar":
3435
default:
35-
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
36+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
3637
Message: fmt.Sprintf("Unsupported content type header %q.", contentType),
3738
})
3839
return
@@ -41,57 +42,58 @@ func (api *API) postFile(rw http.ResponseWriter, r *http.Request) {
4142
r.Body = http.MaxBytesReader(rw, r.Body, 10*(10<<20))
4243
data, err := io.ReadAll(r.Body)
4344
if err != nil {
44-
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
45+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
4546
Message: "Failed to read file from request.",
4647
Detail: err.Error(),
4748
})
4849
return
4950
}
5051
hashBytes := sha256.Sum256(data)
5152
hash := hex.EncodeToString(hashBytes[:])
52-
file, err := api.Database.GetFileByHash(r.Context(), hash)
53+
file, err := api.Database.GetFileByHash(ctx, hash)
5354
if err == nil {
5455
// The file already exists!
55-
httpapi.Write(rw, http.StatusOK, codersdk.UploadResponse{
56+
httpapi.Write(ctx, rw, http.StatusOK, codersdk.UploadResponse{
5657
Hash: file.Hash,
5758
})
5859
return
5960
}
60-
file, err = api.Database.InsertFile(r.Context(), database.InsertFileParams{
61+
file, err = api.Database.InsertFile(ctx, database.InsertFileParams{
6162
Hash: hash,
6263
CreatedBy: apiKey.UserID,
6364
CreatedAt: database.Now(),
6465
Mimetype: contentType,
6566
Data: data,
6667
})
6768
if err != nil {
68-
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
69+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
6970
Message: "Internal error saving file.",
7071
Detail: err.Error(),
7172
})
7273
return
7374
}
7475

75-
httpapi.Write(rw, http.StatusCreated, codersdk.UploadResponse{
76+
httpapi.Write(ctx, rw, http.StatusCreated, codersdk.UploadResponse{
7677
Hash: file.Hash,
7778
})
7879
}
7980

8081
func (api *API) fileByHash(rw http.ResponseWriter, r *http.Request) {
82+
ctx := r.Context()
8183
hash := chi.URLParam(r, "hash")
8284
if hash == "" {
83-
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
85+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
8486
Message: "File hash must be provided in url.",
8587
})
8688
return
8789
}
88-
file, err := api.Database.GetFileByHash(r.Context(), hash)
90+
file, err := api.Database.GetFileByHash(ctx, hash)
8991
if errors.Is(err, sql.ErrNoRows) {
9092
httpapi.ResourceNotFound(rw)
9193
return
9294
}
9395
if err != nil {
94-
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
96+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
9597
Message: "Internal error fetching file.",
9698
Detail: err.Error(),
9799
})

0 commit comments

Comments
 (0)