diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 87aea6919a351..6f398b34488d6 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -1052,6 +1052,18 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) { return xerrors.Errorf("workspace shutdown is manual") } + tmpl, err := s.GetTemplateByID(ctx, workspace.TemplateID) + if err != nil { + code = http.StatusInternalServerError + resp.Message = "Error fetching template." + return xerrors.Errorf("get template: %w", err) + } + if !tmpl.AllowUserAutostop { + code = http.StatusBadRequest + resp.Message = "Cannot extend workspace: template does not allow user autostop." + return xerrors.New("cannot extend workspace: template does not allow user autostop") + } + newDeadline := req.Deadline.UTC() if err := validWorkspaceDeadline(job.CompletedAt.Time, newDeadline); err != nil { // NOTE(Cian): Putting the error in the Message field on request from the FE folks. diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index b44357c5b5dde..9cb86f55ba55f 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -913,8 +913,12 @@ func TestWorkspaceAutobuild(t *testing.T) { ws = coderdtest.MustWorkspace(t, client, ws.ID) require.Equal(t, version2.ID, ws.LatestBuild.TemplateVersionID) }) +} + +func TestTemplateDoesNotAllowUserAutostop(t *testing.T) { + t.Parallel() - t.Run("TemplateDoesNotAllowUserAutostop", func(t *testing.T) { + t.Run("TTLSetByTemplate", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{ IncludeProvisionerDaemon: true, @@ -951,6 +955,34 @@ func TestWorkspaceAutobuild(t *testing.T) { require.Equal(t, templateTTL, template.DefaultTTLMillis) require.Equal(t, templateTTL, *workspace.TTLMillis) }) + + t.Run("ExtendIsNotEnabledByTemplate", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{ + IncludeProvisionerDaemon: true, + TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore()), + }) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { + ctr.AllowUserAutostop = ptr.Ref(false) + }) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + + require.Equal(t, false, template.AllowUserAutostop, "template should have AllowUserAutostop as false") + + ctx := testutil.Context(t, testutil.WaitShort) + ttl := 8 * time.Hour + newDeadline := time.Now().Add(ttl + time.Hour).UTC() + + err := client.PutExtendWorkspace(ctx, workspace.ID, codersdk.PutExtendWorkspaceRequest{ + Deadline: newDeadline, + }) + + require.ErrorContains(t, err, "template does not allow user autostop") + }) } // Blocked by autostart requirements diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx index 8b0334b1dccfc..1cf8eeec78a67 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx @@ -275,3 +275,21 @@ export const WithQuota: Story = { ], }, }; + +export const TemplateDoesNotAllowAutostop: Story = { + args: { + workspace: { + ...MockWorkspace, + latest_build: { + ...MockWorkspace.latest_build, + get deadline() { + return addHours(new Date(), 8).toISOString(); + }, + }, + }, + template: { + ...MockTemplate, + allow_user_autostop: false, + }, + }, +}; diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index e2337fd8a0bd5..d05f7c7c66453 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -204,7 +204,9 @@ export const WorkspaceTopbar: FC = ({ )}