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

* Adds deadline column to workspace_builds, associated DB/API plumbing
* database: Upon inserting a row into workspace_builds, deadline will 
  initially be zero.
* autobuild: Executor now checks the Deadline field of the workspace_build
  for the purpose of autostop logic.
* coderd: Adds a new route /api/v2/workspaces/:workspace/extend which allows
  updating the deadline of the currently active workspace build. The new
  deadline must be after the existing deadline, and not the zero time.
* provisionerd: updates workspace_build.deadline upon successful workspace 
  build completion (equal to now plus workspace TTL, if it exists).
This commit is contained in:
Cian Johnston
2022-05-26 18:08:11 +01:00
committed by GitHub
parent c04d045279
commit 8f0a5a81f1
18 changed files with 306 additions and 34 deletions

View File

@ -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)
@ -570,6 +571,69 @@ func (api *API) putWorkspaceTTL(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, workspace) {
return
}
var req codersdk.PutExtendWorkspaceRequest
if !httpapi.Read(rw, r, &req) {
return
}
var code = http.StatusOK
err := api.Database.InTx(func(s database.Store) error {
build, err := s.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID)
if err != nil {
code = http.StatusInternalServerError
return xerrors.Errorf("get latest workspace build: %w", err)
}
if build.Transition != database.WorkspaceTransitionStart {
code = http.StatusConflict
return xerrors.Errorf("workspace must be started, current status: %s", build.Transition)
}
newDeadline := req.Deadline.Truncate(time.Minute).UTC()
if newDeadline.IsZero() {
// This should not be possible because the struct validation field enforces a non-zero value.
code = http.StatusBadRequest
return xerrors.New("new deadline cannot be zero")
}
if newDeadline.Before(build.Deadline) || newDeadline.Before(time.Now()) {
code = http.StatusBadRequest
return xerrors.Errorf("new deadline %q must be after existing deadline %q", newDeadline.Format(time.RFC3339), build.Deadline.Format(time.RFC3339))
}
// both newDeadline and build.Deadline are truncated to time.Minute
if newDeadline == build.Deadline {
code = http.StatusNotModified
return nil
}
if err := s.UpdateWorkspaceBuildByID(r.Context(), database.UpdateWorkspaceBuildByIDParams{
ID: build.ID,
UpdatedAt: build.UpdatedAt,
ProvisionerState: build.ProvisionerState,
Deadline: newDeadline,
}); err != nil {
return xerrors.Errorf("update workspace build: %w", err)
}
return nil
})
var resp = httpapi.Response{}
if err != nil {
resp.Message = err.Error()
}
httpapi.Write(rw, code, resp)
}
func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
workspace := httpmw.WorkspaceParam(r)