8000 feat: add API/SDK support for autostop extension by johnstcn · Pull Request #1778 · coder/coder · GitHub
[go: up one dir, main page]

Skip to content

feat: add API/SDK support for autostop extension #1778

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
May 26, 2022
Merged
Prev Previous commit
Next Next commit
plumb deadline field through database
  • Loading branch information
johnstcn committed May 26, 2022
commit f6cb70520c6a3f2ee0c39e19d1d21064d7759005
2 changes: 1 addition & 1 deletion coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ func New(options *Options) *API {
r.Put("/", api.putWorkspaceTTL)
})
r.Get("/watch", api.watchWorkspace)
r.Put("/deadline", api.putWorkspaceDeadline)
r.Put("/extend", api.putExtendWorkspace)
})
})
r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) {
Expand Down
11 changes: 9 additions & 2 deletions coderd/database/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion coderd/database/queries/workspacebuilds.sql
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ UPDATE
workspace_builds
SET
updated_at = $2,
provisioner_state = $3
provisioner_state = $3,
deadline = $4
WHERE
id = $1;
16 changes: 15 additions & 1 deletion coderd/provisionerdaemons.go
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,7 @@ func (server *provisionerdServer) FailJob(ctx context.Context, failJob *proto.Fa
ID: input.WorkspaceBuildID,
UpdatedAt: database.Now(),
ProvisionerState: jobType.WorkspaceBuild.State,
// We are explicitly not updating deadline here.
})
if err != nil {
return nil, xerrors.Errorf("update workspace build state: %w", err)
Expand Down Expand Up @@ -544,6 +545,18 @@ func (server *provisionerdServer) CompleteJob(ctx context.Context, completed *pr
}

err = server.Database.InTx(func(db database.Store) error {
now := database.Now()
var workspaceDeadline time.Time
workspace, err := db.GetWorkspaceByID(ctx, workspaceBuild.WorkspaceID)
if err == nil {
if workspace.Ttl.Valid {
workspaceDeadline = now.Add(time.Duration(workspace.Ttl.Int64))
}
} else {
// Huh? Did the workspace get deleted?
// In any case, since this is just for the TTL, try and continue anyway.
server.Logger.Error(ctx, "fetch workspace for build", slog.F("workspace_build_id", workspaceBuild.ID), slog.F("workspace_id", workspaceBuild.WorkspaceID))
}
err = db.UpdateProvisionerJobWithCompleteByID(ctx, database.UpdateProvisionerJobWithCompleteByIDParams{
ID: jobID,
UpdatedAt: database.Now(),
Expand All @@ -556,9 +569,10 @@ func (server *provisionerdServer) CompleteJob(ctx context.Context, completed *pr
return xerrors.Errorf("update provisioner job: %w", err)
}
err = db.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{
Deadline: now,
ID: workspaceBuild.ID,
UpdatedAt: database.Now(),
ProvisionerState: jobType.WorkspaceBuild.State,
UpdatedAt: workspaceDeadline,
})
if err != nil {
return xerrors.Errorf("update workspace build: %w", err)
Expand Down
1 change: 1 addition & 0 deletions coderd/workspacebuilds.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ func convertWorkspaceBuild(workspaceBuild database.WorkspaceBuild, job codersdk.
Transition: codersdk.WorkspaceTransition(workspaceBuild.Transition),
InitiatorID: workspaceBuild.InitiatorID,
Job: job,
Deadline: workspaceBuild.Deadline,
}
}

Expand Down
40 changes: 38 additions & 2 deletions coderd/workspaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,8 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
InitiatorID: apiKey.UserID,
Transition: database.WorkspaceTransitionStart,
JobID: provisionerJob.ID,
BuildNumber: 1, // First build!
BuildNumber: 1, // First build!
Deadline: time.Time{}, // provisionerd will set this upon success
})
if err != nil {
return xerrors.Errorf("insert workspace build: %w", err)
Expand Down Expand Up @@ -570,7 +571,7 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
}
}

func (api *API) putWorkspaceDeadline(rw http.ResponseWriter, r *http.Request) {
func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
workspace := httpmw.WorkspaceParam(r)

if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceWorkspace.
Expand All @@ -597,6 +598,41 @@ func (api *API) putWorkspaceDeadline(rw http.ResponseWriter, r *http.Request) {
})
return
}

newDeadline := req.Deadline.UTC()
if newDeadline.IsZero() {
// This should not be possible because the validation requires a non-zero value.
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("new deadline %q cannot be zero", newDeadline),
})
return
}

if newDeadline.Before(build.Deadline) {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("new deadline %q must be after existing deadline %q", newDeadline, build.Deadline),
})
return
}

if newDeadline == build.Deadline {
httpapi.Write(rw, http.StatusNotModified, httpapi.Response{})
return
}

if err := api.Database.UpdateWorkspaceBuildByID(r.Context(), database.UpdateWorkspaceBuildByIDParams{
ID: build.ID,
UpdatedAt: build.UpdatedAt,
ProvisionerState: build.ProvisionerState,
Deadline: newDeadline,
}); err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: err.Error(),
})
return
}

httpapi.Write(rw, http.StatusOK, httpapi.Response{})
}

func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
Expand Down
69 changes: 49 additions & 20 deletions coderd/workspaces_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -617,27 +617,56 @@ func TestWorkspaceUpdateAutostop(t *testing.T) {

func TestWorkspaceExtendAutostop(t *testing.T) {
t.Parallel()
var (
ctx = context.Background()
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user = coderdtest.CreateFirstUser(t, client)
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID)
extend = 90 * time.Minute
)

initTTL := time.Now()
req := codersdk.PutExtendWorkspaceRequest{
Deadline: initTTL.Add(extend),
}
err := client.PutExtendWorkspace(ctx, workspace.ID, req)
require.NoError(t, err, "failed to update workspace ttl")
t.Run("OK", func(t *testing.T) {
t.Parallel()
var (
ctx = context.Background()
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user = coderdtest.CreateFirstUser(t, client)
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID)
extend = 90 * time.Minute
_ = coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
)

workspace, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "fetch provisioned workspace")
req := codersdk.PutExtendWorkspaceRequest{
Deadline: workspace.LatestBuild.UpdatedAt.Add(extend),
}
err = client.PutExtendWorkspace(ctx, workspace.ID, req)
require.NoError(t, err, "failed to extend workspace")

updated, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "failed to fetch updated workspace")
require.Equal(t, workspace.LatestBuild.Deadline.Add(extend), updated.LatestBuild.Deadline)
})

updated, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "failed to fetch updated workspace")
require.Equal(t, workspace.LatestBuild.Deadline.Add(extend), updated.LatestBuild.Deadline)
t.Run("DeadlineZero", func(t *testing.T) {
t.Parallel()
var (
ctx = context.Background()
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user = coderdtest.CreateFirstUser(t, client)
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID)
_ = coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
)

req := codersdk.PutExtendWorkspaceRequest{
Deadline: time.Time{},
}
err := client.PutExtendWorkspace(ctx, workspace.ID, req)
require.ErrorContains(t, err, "deadline: required", "failed to update workspace ttl")

updated, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "failed to fetch updated workspace")
require.Equal(t, workspace.LatestBuild.Deadline, updated.LatestBuild.Deadline)
})
}

func TestWorkspaceWatcher(t *testing.T) {
Expand Down
5 changes: 2 additions & 3 deletions codersdk/workspaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ type Workspace struct {
Name string `json:"name"`
AutostartSchedule string `json:"autostart_schedule"`
TTL *time.Duration `json:"ttl"`
Deadline time.Time `json:"deadline"`
}

// CreateWorkspaceBuildRequest provides options to update the latest workspace build.
Expand Down Expand Up @@ -181,7 +180,7 @@ func (c *Client) UpdateWorkspaceTTL(ctx context.Context, id uuid.UUID, req Updat
// PutExtendWorkspaceRequest is a request to extend the deadline of
// the active workspace build.
type PutExtendWorkspaceRequest struct {
Deadline time.Time `json:"deadline" validate:"required, datetime=RFC3339"`
Deadline time.Time `json:"deadline" validate:"required"`
}

// PutExtendWorkspace updates the deadline for resources of the latest workspace build.
Expand All @@ -192,7 +191,7 @@ func (c *Client) PutExtendWorkspace(ctx context.Context, id uuid.UUID, req PutEx
return xerrors.Errorf("extend workspace ttl: %w", err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusNotModified {
return readBodyAsError(res)
}
return nil
Expand Down
13 changes: 6 additions & 7 deletions site/src/api/typesGenerated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export interface CreateUserRequest {
readonly organization_id: string
}

// From codersdk/workspaces.go:35:6
// From codersdk/workspaces.go:34:6
export interface CreateWorkspaceBuildRequest {
readonly template_version_id?: string
readonly transition: WorkspaceTransition
Expand Down Expand Up @@ -216,7 +216,7 @@ export interface ProvisionerJobLog {
readonly output: string
}

// From codersdk/workspaces.go:183:6
// From codersdk/workspaces.go:182:6
export interface PutExtendWorkspaceRequest {
readonly deadline: string
}
Expand Down Expand Up @@ -292,12 +292,12 @@ export interface UpdateUserProfileRequest {
readonly username: string
}

// From codersdk/workspaces.go:142:6
// From codersdk/workspaces.go:141:6
export interface UpdateWorkspaceAutostartRequest {
readonly schedule: string
}

// From codersdk/workspaces.go:162:6
// From codersdk/workspaces.go:161:6
export interface UpdateWorkspaceTTLRequest {
// This is likely an enum in an external package ("time.Duration")
readonly ttl?: number
Expand Down Expand Up @@ -368,7 +368,6 @@ export interface Workspace {
readonly autostart_schedule: string
// This is likely an enum in an external package ("time.Duration")
readonly ttl?: number
readonly deadline: string
}

// From codersdk/workspaceresources.go:31:6
Expand Down Expand Up @@ -432,12 +431,12 @@ export interface WorkspaceBuild {
readonly deadline: string
}

// From codersdk/workspaces.go:65:6
// From codersdk/workspaces.go:64:6
export interface WorkspaceBuildsRequest extends Pagination {
readonly WorkspaceID: string
}

// From codersdk/workspaces.go:201:6
// From codersdk/workspaces.go:200:6
export interface WorkspaceFilter {
readonly OrganizationID: string
readonly Owner: string
Expand Down
0