Skip to content

Commit da5f656

Browse files
committed
Merge branch 'main' into dean/app-tokens
2 parents d9b404d + d30945c commit da5f656

37 files changed

+925
-188
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/coderd.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -234,11 +234,7 @@ func New(options *Options) *API {
234234
r.Route("/api/v2", func(r chi.Router) {
235235
api.APIHandler = r
236236

237-
r.NotFound(func(rw http.ResponseWriter, r *http.Request) {
238-
httpapi.Write(rw, http.StatusNotFound, codersdk.Response{
239-
Message: "Route not found.",
240-
})
241-
})
237+
r.NotFound(func(rw http.ResponseWriter, r *http.Request) { httpapi.RouteNotFound(rw) })
242238
r.Use(
243239
tracing.Middleware(api.TracerProvider),
244240
// Specific routes can specify smaller limits.

coderd/httpapi/httpapi.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ func InternalServerError(rw http.ResponseWriter, err error) {
7575
})
7676
}
7777

78+
func RouteNotFound(rw http.ResponseWriter) {
79+
Write(rw, http.StatusNotFound, codersdk.Response{
80+
Message: "Route not found.",
81+
})
82+
}
83+
7884
// Write outputs a standardized format to an HTTP response body.
7985
func Write(rw http.ResponseWriter, status int, response interface{}) {
8086
buf := &bytes.Buffer{}

coderd/userauth.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ func (api *API) oauthLogin(r *http.Request, params oauthLoginParams) (*http.Cook
378378
organizationID = organizations[0].ID
379379
}
380380

381-
user, _, err = api.createUser(ctx, tx, createUserRequest{
381+
user, _, err = api.CreateUser(ctx, tx, CreateUserRequest{
382382
CreateUserRequest: codersdk.CreateUserRequest{
383383
Email: params.Email,
384384
Username: params.Username,

coderd/users.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
8383
return
8484
}
8585

86-
user, organizationID, err := api.createUser(r.Context(), api.Database, createUserRequest{
86+
user, organizationID, err := api.CreateUser(r.Context(), api.Database, CreateUserRequest{
8787
CreateUserRequest: codersdk.CreateUserRequest{
8888
Email: createUser.Email,
8989
Username: createUser.Username,
@@ -317,7 +317,7 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
317317
return
318318
}
319319

320-
user, _, err := api.createUser(r.Context(), api.Database, createUserRequest{
320+
user, _, err := api.CreateUser(r.Context(), api.Database, CreateUserRequest{
321321
CreateUserRequest: req,
322322
LoginType: database.LoginTypePassword,
323323
})
@@ -1108,12 +1108,12 @@ func (api *API) createAPIKey(r *http.Request, params createAPIKeyParams) (*http.
11081108
}, nil
11091109
}
11101110

1111-
type createUserRequest struct {
1111+
type CreateUserRequest struct {
11121112
codersdk.CreateUserRequest
11131113
LoginType database.LoginType
11141114
}
11151115

1116-
func (api *API) createUser(ctx context.Context, store database.Store, req createUserRequest) (database.User, uuid.UUID, error) {
1116+
func (api *API) CreateUser(ctx context.Context, store database.Store, req CreateUserRequest) (database.User, uuid.UUID, error) {
11171117
var user database.User
11181118
return user, req.OrganizationID, store.InTx(func(tx database.Store) error {
11191119
orgRoles := make([]string, 0)

coderd/workspaceagents.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,8 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques
616616
)
617617

618618
if updateDB {
619+
go activityBumpWorkspace(api.Logger.Named("activity_bump"), api.Database, workspace)
620+
619621
lastReport = rep
620622

621623
_, err = api.Database.InsertAgentStat(ctx, database.InsertAgentStatParams{

coderd/workspaceapps_test.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const (
4040
// setupProxyTest creates a workspace with an agent and some apps. It returns a
4141
// codersdk client, the first user, the workspace, and the port number the test
4242
// listener is running on.
43-
func setupProxyTest(t *testing.T) (*codersdk.Client, codersdk.CreateFirstUserResponse, codersdk.Workspace, uint16) {
43+
func setupProxyTest(t *testing.T, workspaceMutators ...func(*codersdk.CreateWorkspaceRequest)) (*codersdk.Client, codersdk.CreateFirstUserResponse, codersdk.Workspace, uint16) {
4444
// #nosec
4545
ln, err := net.Listen("tcp", ":0")
4646
require.NoError(t, err)
@@ -62,8 +62,10 @@ func setupProxyTest(t *testing.T) (*codersdk.Client, codersdk.CreateFirstUserRes
6262
require.True(t, ok)
6363

6464
client := coderdtest.New(t, &coderdtest.Options{
65-
AppHostname: proxyTestSubdomain,
66-
IncludeProvisionerDaemon: true,
65+
AppHostname: proxyTestSubdomain,
66+
IncludeProvisionerDaemon: true,
67+
AgentStatsRefreshInterval: time.Millisecond * 100,
68+
MetricsCacheRefreshInterval: time.Millisecond * 100,
6769
})
6870
user := coderdtest.CreateFirstUser(t, client)
6971
authToken := uuid.NewString()
@@ -100,7 +102,7 @@ func setupProxyTest(t *testing.T) (*codersdk.Client, codersdk.CreateFirstUserRes
100102
})
101103
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
102104
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
103-
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
105+
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, workspaceMutators...)
104106
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
105107

106108
agentClient := codersdk.New(client.URL)
@@ -109,6 +111,7 @@ func setupProxyTest(t *testing.T) (*codersdk.Client, codersdk.CreateFirstUserRes
109111
FetchMetadata: agentClient.WorkspaceAgentMetadata,
110112
CoordinatorDialer: agentClient.ListenWorkspaceAgentTailnet,
111113
Logger: slogtest.Make(t, nil).Named("agent"),
114+
StatsReporter: agentClient.AgentReportStats,
112115
})
113116
t.Cleanup(func() {
114117
_ = agentCloser.Close()

codersdk/features.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ const (
1717
const (
1818
FeatureUserLimit = "user_limit"
1919
FeatureAuditLog = "audit_log"
20+
FeatureSCIM = "scim"
2021
)
2122

22-
var FeatureNames = []string{FeatureUserLimit, FeatureAuditLog}
23+
var FeatureNames = []string{FeatureUserLimit, FeatureAuditLog, FeatureSCIM}
2324

2425
type Feature struct {
2526
Entitlement Entitlement `json:"entitlement"`

0 commit comments

Comments
 (0)