Skip to content

Commit 38ceb5d

Browse files
committed
feat: add endpoint for resolving workspace autostart
1 parent 23f0265 commit 38ceb5d

File tree

6 files changed

+291
-0
lines changed

6 files changed

+291
-0
lines changed

coderd/coderd.go

+1
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,7 @@ func New(options *Options) *API {
885885
r.Put("/extend", api.putExtendWorkspace)
886886
r.Put("/dormant", api.putWorkspaceDormant)
887887
r.Put("/autoupdates", api.putWorkspaceAutoupdates)
888+
r.Get("/resolve", api.resolveAutostart)
888889
})
889890
})
890891
r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) {

coderd/database/db2sdk/db2sdk.go

+14
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

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

1112
"github.com/coder/coder/v2/coderd/database"
1213
"github.com/coder/coder/v2/coderd/parameter"
@@ -30,6 +31,19 @@ func WorkspaceBuildParameter(p database.WorkspaceBuildParameter) codersdk.Worksp
3031
}
3132
}
3233

34+
func TemplateVersionParameters(params []database.TemplateVersionParameter) ([]codersdk.TemplateVersionParameter, error) {
35+
out := make([]codersdk.TemplateVersionParameter, len(params))
36+
var err error
37+
for i, p := range params {
38+
out[i], err = TemplateVersionParameter(p)
39+
if err != nil {
40+
return nil, xerrors.Errorf("convert template version parameter %q: %w", p.Name, err)
41+
}
42+
}
43+
44+
return out, nil
45+
}
46+
3347
func TemplateVersionParameter(param database.TemplateVersionParameter) (codersdk.TemplateVersionParameter, error) {
3448
options, err := templateVersionParameterOptions(param.Options)
3549
if err != nil {

coderd/workspaces.go

+100
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"cdr.dev/slog"
1818
"github.com/coder/coder/v2/coderd/audit"
1919
"github.com/coder/coder/v2/coderd/database"
20+
"github.com/coder/coder/v2/coderd/database/db2sdk"
2021
"github.com/coder/coder/v2/coderd/database/dbauthz"
2122
"github.com/coder/coder/v2/coderd/database/dbtime"
2223
"github.com/coder/coder/v2/coderd/database/provisionerjobs"
@@ -1059,6 +1060,105 @@ func (api *API) putWorkspaceAutoupdates(rw http.ResponseWriter, r *http.Request)
10591060
rw.WriteHeader(http.StatusNoContent)
10601061
}
10611062

1063+
func (api *API) resolveAutostart(rw http.ResponseWriter, r *http.Request) {
1064+
var (
1065+
ctx = r.Context()
1066+
workspace = httpmw.WorkspaceParam(r)
1067+
)
1068+
1069+
template, err := api.Database.GetTemplateByID(ctx, workspace.TemplateID)
1070+
if err != nil {
1071+
httpapi.InternalServerError(rw, err)
1072+
return
1073+
}
1074+
1075+
useActiveVersion := template.RequireActiveVersion || workspace.AutomaticUpdates == database.AutomaticUpdatesAlways
1076+
if !useActiveVersion {
1077+
httpapi.Write(ctx, rw, http.StatusOK, codersdk.ResolveAutostart{
1078+
ParameterMismatch: true,
1079+
})
1080+
return
1081+
}
1082+
1083+
build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
1084+
if err != nil {
1085+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
1086+
Message: "Internal error fetching latest workspace build.",
1087+
Detail: err.Error(),
1088+
})
1089+
return
1090+
}
1091+
1092+
if build.TemplateVersionID == template.ActiveVersionID {
1093+
httpapi.Write(ctx, rw, http.StatusOK, codersdk.ResolveAutostart{
1094+
ParameterMismatch: false,
1095+
})
1096+
return
1097+
}
1098+
1099+
version, err := api.Database.GetTemplateVersionByID(ctx, template.ActiveVersionID)
1100+
if httpapi.Is404Error(err) {
1101+
httpapi.ResourceNotFound(rw)
1102+
return
1103+
}
1104+
if err != nil {
1105+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
1106+
Message: "Internal error fetching template version.",
1107+
Detail: err.Error(),
1108+
})
1109+
return
1110+
}
1111+
1112+
if version.TemplateID.UUID != workspace.TemplateID {
1113+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
1114+
Message: "Workspace cannot be resolved against unrelated template.",
1115+
Detail: err.Error(),
1116+
})
1117+
return
1118+
}
1119+
1120+
dbVersionParams, err := api.Database.GetTemplateVersionParameters(ctx, version.ID)
1121+
if err != nil {
1122+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
1123+
Message: "Internal error fetching template version parameters.",
1124+
Detail: err.Error(),
1125+
})
1126+
return
1127+
}
1128+
1129+
dbBuildParams, err := api.Database.GetWorkspaceBuildParameters(ctx, build.ID)
1130+
if err != nil {
1131+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
1132+
Message: "Internal error fetching latest workspace build parameters.",
1133+
Detail: err.Error(),
1134+
})
1135+
return
1136+
}
1137+
1138+
versionParams, err := db2sdk.TemplateVersionParameters(dbVersionParams)
1139+
if err != nil {
1140+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
1141+
Message: "Internal error converting template version parameters.",
1142+
Detail: err.Error(),
1143+
})
1144+
return
1145+
}
1146+
1147+
resolver := codersdk.ParameterResolver{
1148+
Rich: db2sdk.WorkspaceBuildParameters(dbBuildParams),
1149+
}
1150+
1151+
var response codersdk.ResolveAutostart
1152+
for i := 0; i < len(versionParams) && !response.ParameterMismatch; i++ {
1153+
_, err := resolver.ValidateResolve(versionParams[i], nil)
1154+
// There's a parameter mismatch if we get an error back from the
1155+
// resolver.
1156+
response.ParameterMismatch = err != nil
1157+
}
1158+
1159+
httpapi.Write(ctx, rw, http.StatusOK, response)
1160+
}
1161+
10621162
// @Summary Watch workspace by ID
10631163
// @ID watch-workspace-by-id
10641164
// @Security CoderSessionToken

coderd/workspaces_test.go

+93
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,99 @@ func TestWorkspace(t *testing.T) {
340340
})
341341
}
342342

343+
func TestResolveAutostart(t *testing.T) {
344+
t.Parallel()
345+
346+
t.Run("OK", func(t *testing.T) {
347+
t.Parallel()
348+
ownerClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
349+
owner := coderdtest.CreateFirstUser(t, ownerClient)
350+
version1 := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, nil)
351+
coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, version1.ID)
352+
template := coderdtest.CreateTemplate(t, ownerClient, owner.OrganizationID, version1.ID)
353+
354+
params := &echo.Responses{
355+
Parse: echo.ParseComplete,
356+
ProvisionPlan: []*proto.Response{
357+
{
358+
Type: &proto.Response_Plan{
359+
Plan: &proto.PlanComplete{
360+
Parameters: []*proto.RichParameter{
361+
{
362+
Name: "param",
363+
Description: "param",
364+
Required: true,
365+
Mutable: true,
366+
},
367+
},
368+
},
369+
},
370+
},
371+
},
372+
ProvisionApply: echo.ApplyComplete,
373+
}
374+
version2 := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, params, func(ctvr *codersdk.CreateTemplateVersionRequest) {
375+
ctvr.TemplateID = template.ID
376+
})
377+
coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, version2.ID)
378+
379+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
380+
defer cancel()
381+
382+
client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
383+
workspace := coderdtest.CreateWorkspace(t, client, owner.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
384+
cwr.AutomaticUpdates = codersdk.AutomaticUpdatesAlways
385+
})
386+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
387+
388+
err := ownerClient.UpdateActiveTemplateVersion(ctx, template.ID, codersdk.UpdateActiveTemplateVersion{
389+
ID: version2.ID,
390+
})
391+
require.NoError(t, err)
392+
393+
// Autostart shouldn't be possible if parameters do not match.
394+
resp, err := client.ResolveAutostart(ctx, workspace.ID.String())
395+
require.NoError(t, err)
396+
require.True(t, resp.ParameterMismatch)
397+
398+
update, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
399+
TemplateVersionID: version2.ID,
400+
Transition: codersdk.WorkspaceTransitionStart,
401+
RichParameterValues: []codersdk.WorkspaceBuildParameter{
402+
{
403+
Name: "param",
404+
Value: "Hello",
405+
},
406+
},
407+
})
408+
require.NoError(t, err)
409+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, update.ID)
410+
411+
// We should be able to autostart since parameters are updated.
412+
resp, err = client.ResolveAutostart(ctx, workspace.ID.String())
413+
require.NoError(t, err)
414+
require.False(t, resp.ParameterMismatch)
415+
416+
// Create one last version where the parameters are the same as the previous
417+
// version.
418+
version3 := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, params, func(ctvr *codersdk.CreateTemplateVersionRequest) {
419+
ctvr.TemplateID = template.ID
420+
})
421+
coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, version3.ID)
422+
423+
err = ownerClient.UpdateActiveTemplateVersion(ctx, template.ID, codersdk.UpdateActiveTemplateVersion{
424+
ID: version3.ID,
425+
})
426+
require.NoError(t, err)
427+
428+
// Even though we're out of date we should still be able to autostart
429+
// since parameters resolve.
430+
resp, err = client.ResolveAutostart(ctx, workspace.ID.String())
431+
require.NoError(t, err)
432+
require.False(t, resp.ParameterMismatch)
433+
})
434+
}
435+
343436
func TestAdminViewAllWorkspaces(t *testing.T) {
344437
t.Parallel()
345438
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})

codersdk/workspaces.go

+17
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,23 @@ func (c *Client) WorkspaceQuota(ctx context.Context, userID string) (WorkspaceQu
449449
return quota, json.NewDecoder(res.Body).Decode(&quota)
450450
}
451451

452+
type ResolveAutostart struct {
453+
ParameterMismatch bool `json:"parameter_mismatch"`
454+
}
455+
456+
func (c *Client) ResolveAutostart(ctx context.Context, workspaceID string) (ResolveAutostart, error) {
457+
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/resolve", workspaceID), nil)
458+
if err != nil {
459+
return ResolveAutostart{}, err
460+
}
461+
defer res.Body.Close()
462+
if res.StatusCode != http.StatusOK {
463+
return ResolveAutostart{}, ReadBodyAsError(res)
464+
}
465+
var response ResolveAutostart
466+
return response, json.NewDecoder(res.Body).Decode(&response)
467+
}
468+
452469
// WorkspaceNotifyChannel is the PostgreSQL NOTIFY
453470
// channel to listen for updates on. The payload is empty,
454471
// because the size of a workspace payload can be very large.

enterprise/coderd/workspaces_test.go

+66
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/coder/coder/v2/enterprise/coderd/license"
2525
"github.com/coder/coder/v2/enterprise/coderd/schedule"
2626
"github.com/coder/coder/v2/provisioner/echo"
27+
"github.com/coder/coder/v2/provisionersdk/proto"
2728
"github.com/coder/coder/v2/testutil"
2829
)
2930

@@ -1061,6 +1062,71 @@ func TestWorkspaceLock(t *testing.T) {
10611062
})
10621063
}
10631064

1065+
func TestResolveAutostart(t *testing.T) {
1066+
t.Parallel()
1067+
1068+
ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{
1069+
Options: &coderdtest.Options{
1070+
IncludeProvisionerDaemon: true,
1071+
TemplateScheduleStore: &schedule.EnterpriseTemplateScheduleStore{},
1072+
},
1073+
LicenseOptions: &coderdenttest.LicenseOptions{
1074+
Features: license.Features{
1075+
codersdk.FeatureAccessControl: 1,
1076+
},
1077+
},
1078+
})
1079+
1080+
version1 := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, nil)
1081+
coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, version1.ID)
1082+
template := coderdtest.CreateTemplate(t, ownerClient, owner.OrganizationID, version1.ID, func(ctr *codersdk.CreateTemplateRequest) {
1083+
ctr.RequireActiveVersion = true
1084+
})
1085+
1086+
params := &echo.Responses{
1087+
Parse: echo.ParseComplete,
1088+
ProvisionPlan: []*proto.Response{
1089+
{
1090+
Type: &proto.Response_Plan{
1091+
Plan: &proto.PlanComplete{
1092+
Parameters: []*proto.RichParameter{
1093+
{
1094+
Name: "param",
1095+
Description: "param",
1096+
Required: true,
1097+
Mutable: true,
1098+
},
1099+
},
1100+
},
1101+
},
1102+
},
1103+
},
1104+
ProvisionApply: echo.ApplyComplete,
1105+
}
1106+
version2 := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, params, func(ctvr *codersdk.CreateTemplateVersionRequest) {
1107+
ctvr.TemplateID = template.ID
1108+
})
1109+
coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, version2.ID)
1110+
1111+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
1112+
defer cancel()
1113+
1114+
client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
1115+
workspace := coderdtest.CreateWorkspace(t, client, owner.OrganizationID, template.ID)
1116+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
1117+
1118+
err := ownerClient.UpdateActiveTemplateVersion(ctx, template.ID, codersdk.UpdateActiveTemplateVersion{
1119+
ID: version2.ID,
1120+
})
1121+
require.NoError(t, err)
1122+
1123+
// Autostart shouldn't be possible since the template requires automatic
1124+
// updates.
1125+
resp, err := client.ResolveAutostart(ctx, workspace.ID.String())
1126+
require.NoError(t, err)
1127+
require.True(t, resp.ParameterMismatch)
1128+
}
1129+
10641130
func must[T any](value T, err error) T {
10651131
if err != nil {
10661132
panic(err)

0 commit comments

Comments
 (0)