From 7a314a52cbe7c050921f491e529ec8f9b8ff7373 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Thu, 30 Mar 2023 13:47:22 +0000 Subject: [PATCH 1/7] added new restart button and wrote tests --- .../components/DropdownButton/ActionCtas.tsx | 35 ++++++++++++++----- site/src/components/Workspace/Workspace.tsx | 1 + .../WorkspaceActions.stories.tsx | 1 + .../WorkspaceActions.test.tsx | 7 +++- .../WorkspaceActions/WorkspaceActions.tsx | 7 ++++ .../components/WorkspaceActions/constants.ts | 2 ++ site/src/i18n/en/workspacePage.json | 1 + 7 files changed, 44 insertions(+), 10 deletions(-) diff --git a/site/src/components/DropdownButton/ActionCtas.tsx b/site/src/components/DropdownButton/ActionCtas.tsx index 29b63c5fd6a6a..64297518bb5d1 100644 --- a/site/src/components/DropdownButton/ActionCtas.tsx +++ b/site/src/components/DropdownButton/ActionCtas.tsx @@ -6,9 +6,10 @@ import CloudQueueIcon from "@material-ui/icons/CloudQueue" import SettingsOutlined from "@material-ui/icons/SettingsOutlined" import CropSquareIcon from "@material-ui/icons/CropSquare" import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline" +import ReplayIcon from "@material-ui/icons/Replay" import PlayCircleOutlineIcon from "@material-ui/icons/PlayCircleOutline" import { LoadingButton } from "components/LoadingButton/LoadingButton" -import { FC } from "react" +import { FC, PropsWithChildren } from "react" import { useTranslation } from "react-i18next" import { combineClasses } from "util/combineClasses" import { WorkspaceActionButton } from "../WorkspaceActionButton/WorkspaceActionButton" @@ -17,7 +18,7 @@ interface WorkspaceAction { handleAction: () => void } -export const UpdateButton: FC> = ({ +export const UpdateButton: FC> = ({ handleAction, }) => { const styles = useStyles() @@ -35,7 +36,7 @@ export const UpdateButton: FC> = ({ ) } -export const SettingsButton: FC> = ({ +export const SettingsButton: FC> = ({ handleAction, }) => { const styles = useStyles() @@ -53,7 +54,7 @@ export const SettingsButton: FC> = ({ ) } -export const StartButton: FC> = ({ +export const StartButton: FC> = ({ handleAction, }) => { const styles = useStyles() @@ -69,7 +70,7 @@ export const StartButton: FC> = ({ ) } -export const StopButton: FC> = ({ +export const StopButton: FC> = ({ handleAction, }) => { const styles = useStyles() @@ -85,7 +86,23 @@ export const StopButton: FC> = ({ ) } -export const DeleteButton: FC> = ({ +export const RestartButton: FC> = ({ + handleAction, +}) => { + const styles = useStyles() + const { t } = useTranslation("workspacePage") + + return ( + } + onClick={handleAction} + label={t("actionButton.restart")} + /> + ) +} + +export const DeleteButton: FC> = ({ handleAction, }) => { const styles = useStyles() @@ -101,7 +118,7 @@ export const DeleteButton: FC> = ({ ) } -export const CancelButton: FC> = ({ +export const CancelButton: FC> = ({ handleAction, }) => { const styles = useStyles() @@ -128,7 +145,7 @@ interface DisabledProps { label: string } -export const DisabledButton: FC> = ({ +export const DisabledButton: FC> = ({ label, }) => { const styles = useStyles() @@ -144,7 +161,7 @@ interface LoadingProps { label: string } -export const ActionLoadingButton: FC> = ({ +export const ActionLoadingButton: FC> = ({ label, }) => { const styles = useStyles() diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index d2161fd17cb6a..ff6fe481979c8 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -125,6 +125,7 @@ export const Workspace: FC> = ({ isOutdated={workspace.outdated} handleStart={handleStart} handleStop={handleStop} + handleRestart={() => console.log("restarting!")} handleDelete={handleDelete} handleUpdate={handleUpdate} handleCancel={handleCancel} diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.stories.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.stories.tsx index 090e81cb2bb03..b2b2526811d0d 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.stories.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.stories.tsx @@ -15,6 +15,7 @@ const Template: Story = (args) => ( const defaultArgs = { handleStart: action("start"), handleStop: action("stop"), + handleRestart: action("restart"), handleDelete: action("delete"), handleUpdate: action("update"), handleCancel: action("cancel"), diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx index 98c8c3e7fd1af..a90ecaac93a07 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx @@ -15,6 +15,7 @@ const renderComponent = async (props: Partial = {}) => { isOutdated={props.isOutdated ?? false} handleStart={jest.fn()} handleStop={jest.fn()} + handleRestart={jest.fn()} handleDelete={jest.fn()} handleUpdate={jest.fn()} handleCancel={jest.fn()} @@ -33,6 +34,7 @@ const renderAndClick = async (props: Partial = {}) => { isOutdated={props.isOutdated ?? false} handleStart={jest.fn()} handleStop={jest.fn()} + handleRestart={jest.fn()} handleDelete={jest.fn()} handleUpdate={jest.fn()} handleCancel={jest.fn()} @@ -62,7 +64,7 @@ describe("WorkspaceActions", () => { }) }) describe("when the workspace is started", () => { - it("primary is stop; secondary is delete", async () => { + it("primary is stop; secondary are delete, restart", async () => { await renderAndClick({ workspaceStatus: Mocks.MockWorkspace.latest_build.status, }) @@ -72,6 +74,9 @@ describe("WorkspaceActions", () => { expect(screen.getByTestId("secondary-ctas")).toHaveTextContent( t("actionButton.delete", { ns: "workspacePage" }), ) + expect(screen.getByTestId("secondary-ctas")).toHaveTextContent( + t("actionButton.restart", { ns: "workspacePage" }), + ) }) }) describe("when the workspace is started", () => { diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.tsx index c4e9fde2d25d9..7bdf58b0dca04 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.tsx @@ -9,6 +9,7 @@ import { SettingsButton, StartButton, StopButton, + RestartButton, UpdateButton, } from "../DropdownButton/ActionCtas" import { ButtonMapping, ButtonTypesEnum, buttonAbilities } from "./constants" @@ -18,6 +19,7 @@ export interface WorkspaceActionsProps { isOutdated: boolean handleStart: () => void handleStop: () => void + handleRestart: () => void handleDelete: () => void handleUpdate: () => void handleCancel: () => void @@ -31,6 +33,7 @@ export const WorkspaceActions: FC = ({ isOutdated, handleStart, handleStop, + handleRestart, handleDelete, handleUpdate, handleCancel, @@ -58,6 +61,10 @@ export const WorkspaceActions: FC = ({ [ButtonTypesEnum.stopping]: ( ), + [ButtonTypesEnum.restart]: , + [ButtonTypesEnum.starting]: ( + + ), [ButtonTypesEnum.delete]: , [ButtonTypesEnum.deleting]: ( diff --git a/site/src/components/WorkspaceActions/constants.ts b/site/src/components/WorkspaceActions/constants.ts index 383a708f73011..0a6a5c2d7500e 100644 --- a/site/src/components/WorkspaceActions/constants.ts +++ b/site/src/components/WorkspaceActions/constants.ts @@ -7,6 +7,7 @@ export enum ButtonTypesEnum { starting = "starting", stop = "stop", stopping = "stopping", + restart = "restart", delete = "delete", deleting = "deleting", update = "update", @@ -45,6 +46,7 @@ const statusToAbilities: Record = { ButtonTypesEnum.stop, ButtonTypesEnum.settings, ButtonTypesEnum.delete, + ButtonTypesEnum.restart, ], canCancel: false, canAcceptJobs: true, diff --git a/site/src/i18n/en/workspacePage.json b/site/src/i18n/en/workspacePage.json index b9b63d1d3d9da..fc484e9639325 100644 --- a/site/src/i18n/en/workspacePage.json +++ b/site/src/i18n/en/workspacePage.json @@ -21,6 +21,7 @@ "actionButton": { "start": "Start", "stop": "Stop", + "restart": "Restart", "delete": "Delete", "cancel": "Cancel", "update": "Update", From a3cfc8557f9943000ba917dce17e6075d7320a96 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Mon, 10 Apr 2023 17:04:30 +0000 Subject: [PATCH 2/7] moved restart fn into workspacebuilds.go --- cli/restart.go | 5 + coderd/workspacebuilds.go | 477 ++++++++++++++++++++++++++++++++++---- 2 files changed, 441 insertions(+), 41 deletions(-) diff --git a/cli/restart.go b/cli/restart.go index 51ffb2abbf871..3daa09bc636ea 100644 --- a/cli/restart.go +++ b/cli/restart.go @@ -34,28 +34,33 @@ func (r *RootCmd) restart() *clibase.Cmd { return err } + // get the workspace - this can probably stay in here workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0]) if err != nil { return err } + // create a build - stop the workspace build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ Transition: codersdk.WorkspaceTransitionStop, }) if err != nil { return err } + // this seems to return the provisioner job - perhaps we are watching for an error err = cliui.WorkspaceBuild(ctx, out, client, build.ID) if err != nil { return err } + // create a build - start the workspace build, err = client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ Transition: codersdk.WorkspaceTransitionStart, }) if err != nil { return err } + // this seems to return the provisioner job - perhaps we are watching for an error err = cliui.WorkspaceBuild(ctx, out, client, build.ID) if err != nil { return err diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 8a238b6994f4d..673790c81bbde 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -288,27 +288,14 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ httpapi.Write(ctx, rw, http.StatusOK, apiBuild) } -// Azure supports instance identity verification: -// https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=linux#tabgroup_14 -// -// @Summary Create workspace build -// @ID create-workspace-build -// @Security CoderSessionToken -// @Accept json -// @Produce json -// @Tags Builds -// @Param workspace path string true "Workspace ID" format(uuid) -// @Param request body codersdk.CreateWorkspaceBuildRequest true "Create workspace build request" -// @Success 200 {object} codersdk.WorkspaceBuild -// @Router /workspaces/{workspace}/builds [post] -// nolint:gocyclo -func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { +func (api *API) postBuild(rw http.ResponseWriter, r *http.Request) (codersdk.WorkspaceBuild, error) { ctx := r.Context() apiKey := httpmw.APIKey(r) workspace := httpmw.WorkspaceParam(r) + var createBuild codersdk.CreateWorkspaceBuildRequest if !httpapi.Read(ctx, rw, r, &createBuild) { - return + return codersdk.WorkspaceBuild{}, xerrors.Errorf("Decode CreateWorkspaceBuildRequest") } // Doing this up front saves a lot of work if the user doesn't have permission. @@ -324,11 +311,11 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: fmt.Sprintf("Transition %q not supported.", createBuild.Transition), }) - return + return codersdk.WorkspaceBuild{}, xerrors.Errorf("Transition %q not supported.", createBuild.Transition) } if !api.Authorize(r, action, workspace) { httpapi.ResourceNotFound(rw) - return + return codersdk.WorkspaceBuild{}, xerrors.Errorf("Resource not found.") } if createBuild.TemplateVersionID == uuid.Nil { @@ -338,7 +325,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { Message: "Internal error fetching the latest workspace build.", Detail: latestBuildErr.Error(), }) - return + return codersdk.WorkspaceBuild{}, xerrors.Errorf("Fetch the latest workspace build: %w", latestBuildErr.Error()) } createBuild.TemplateVersionID = latestBuild.TemplateVersionID } @@ -352,14 +339,18 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { Detail: "template version not found", }}, }) - return + return codersdk.WorkspaceBuild{}, xerrors.Errorf("Template version not found: %w", []codersdk.ValidationError{{ + Field: "template_version_id", + Detail: "template version not found", + }}) + } if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error fetching template version.", Detail: err.Error(), }) - return + return codersdk.WorkspaceBuild{}, xerrors.Errorf("Fetch template version: %w", err.Error()) } template, err := api.Database.GetTemplateByID(ctx, templateVersion.TemplateID.UUID) @@ -368,7 +359,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { Message: "Failed to get template", Detail: err.Error(), }) - return + return codersdk.WorkspaceBuild{}, xerrors.Errorf("Get template: %w", err.Error()) } var state []byte @@ -379,7 +370,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ Message: "Only template managers may provide custom state", }) - return + return codersdk.WorkspaceBuild{}, xerrors.Errorf("Unauthorized request.") } state = createBuild.ProvisionerState } @@ -390,14 +381,14 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { Message: "Orphan is only permitted when deleting a workspace.", Detail: err.Error(), }) - return + return codersdk.WorkspaceBuild{}, xerrors.Errorf("Orphan is only permitted when deleting a workspace: %w", err.Error()) } if createBuild.ProvisionerState != nil && createBuild.Orphan { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "ProvisionerState cannot be set alongside Orphan since state intent is unclear.", }) - return + return codersdk.WorkspaceBuild{}, xerrors.Errorf("ProvisionerState cannot be set alongside Orphan since state intent is unclear.") } state = []byte{} } @@ -408,7 +399,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { Message: "Internal error fetching provisioner job.", Detail: err.Error(), }) - return + return codersdk.WorkspaceBuild{}, xerrors.Errorf("Fetch provisioner job: %w", err.Error()) } templateVersionJobStatus := convertProvisionerJob(templateVersionJob).Status switch templateVersionJobStatus { @@ -416,17 +407,17 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusNotAcceptable, codersdk.Response{ Message: fmt.Sprintf("The provided template version is %s. Wait for it to complete importing!", templateVersionJobStatus), }) - return + return codersdk.WorkspaceBuild{}, xerrors.Errorf("The provided template version is %s.", templateVersionJobStatus) case codersdk.ProvisionerJobFailed: httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: fmt.Sprintf("The provided template version %q has failed to import: %q. You cannot build workspaces with it!", templateVersion.Name, templateVersionJob.Error.String), }) - return + return codersdk.WorkspaceBuild{}, xerrors.Errorf("The provided template version %q failed to import.", templateVersion.Name) case codersdk.ProvisionerJobCanceled: httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "The provided template version was canceled during import. You cannot builds workspaces with it!", }) - return + return codersdk.WorkspaceBuild{}, xerrors.Errorf("The provided template version was canceled during import.") } tags := provisionerdserver.MutateTags(workspace.OwnerID, templateVersionJob.Tags) @@ -440,7 +431,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ Message: "A workspace build is already active.", }) - return + return codersdk.WorkspaceBuild{}, xerrors.Errorf("Workspace build is already active.") } priorBuildNum = priorHistory.BuildNumber @@ -449,7 +440,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { Message: "Internal error fetching prior workspace build.", Detail: err.Error(), }) - return + return codersdk.WorkspaceBuild{}, xerrors.Errorf("Fetch prior workspace build: %w", err.Error()) } if state == nil { @@ -462,7 +453,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { Message: "Internal error fetching template version parameters.", Detail: err.Error(), }) - return + return codersdk.WorkspaceBuild{}, xerrors.Errorf("Fetch template version parameters: %w", err.Error()) } templateVersionParameters, err := convertTemplateVersionParameters(dbTemplateVersionParameters) if err != nil { @@ -470,7 +461,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { Message: "Internal error converting template version parameters.", Detail: err.Error(), }) - return + return codersdk.WorkspaceBuild{}, xerrors.Errorf("Convert template version parameters: %w", err.Error()) } lastBuildParameters, err := api.Database.GetWorkspaceBuildParameters(ctx, priorHistory.ID) @@ -479,7 +470,8 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { Message: "Internal error fetching prior workspace build parameters.", Detail: err.Error(), }) - return + return codersdk.WorkspaceBuild{}, xerrors.Errorf("Fetch prior workspace build parameters: %w", err.Error()) + } apiLastBuildParameters := convertWorkspaceBuildParameters(lastBuildParameters) @@ -492,7 +484,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { Message: "Error fetching previous legacy parameters.", Detail: err.Error(), }) - return + return codersdk.WorkspaceBuild{}, xerrors.Errorf("Fetch previous legacy parameters: %w", err.Error()) } // Rich parameters migration: include legacy variables to the last build parameters @@ -522,7 +514,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { Message: "Error validating workspace build parameters.", Detail: err.Error(), }) - return + return codersdk.WorkspaceBuild{}, xerrors.Errorf("Validate workspace build parameters: %w", err.Error()) } var parameters []codersdk.WorkspaceBuildParameter @@ -533,7 +525,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: fmt.Sprintf("Parameter %q is not mutable, so it can't be updated after creating a workspace.", templateVersionParameter.Name), }) - return + return codersdk.WorkspaceBuild{}, xerrors.Errorf("Parameter %q is not mutable", templateVersionParameter.Name) } parameters = append(parameters, *buildParameter) continue @@ -642,7 +634,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { Message: "Internal error inserting workspace build.", Detail: err.Error(), }) - return + xerrors.Errorf("Insert workspace build: %w", err.Error()) } users, err := api.Database.GetUsersByIDs(ctx, []uuid.UUID{ @@ -654,7 +646,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { Message: "Internal error getting user.", Detail: err.Error(), }) - return + xerrors.Errorf("Get user: %w", err.Error()) } apiBuild, err := api.convertWorkspaceBuild( @@ -673,12 +665,415 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { Message: "Internal error converting workspace build.", Detail: err.Error(), }) - return + xerrors.Errorf("Convert workspace build: %w", err.Error()) } api.publishWorkspaceUpdate(ctx, workspace.ID) - httpapi.Write(ctx, rw, http.StatusCreated, apiBuild) + return apiBuild, nil +} + +// Azure supports instance identity verification: +// https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=linux#tabgroup_14 +// +// @Summary Create workspace build +// @ID create-workspace-build +// @Security CoderSessionToken +// @Accept json +// @Produce json +// @Tags Builds +// @Param workspace path string true "Workspace ID" format(uuid) +// @Param request body codersdk.CreateWorkspaceBuildRequest true "Create workspace build request" +// @Success 200 {object} codersdk.WorkspaceBuild +// @Router /workspaces/{workspace}/builds [post] +// nolint:gocyclo +func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + build, buildErr := api.postBuild(rw, r) + if buildErr != nil { + // all errors handled in api.postBuild + return + } + + httpapi.Write(ctx, rw, http.StatusCreated, build) + + // ctx := r.Context() + // apiKey := httpmw.APIKey(r) + // workspace := httpmw.WorkspaceParam(r) + // var createBuild codersdk.CreateWorkspaceBuildRequest + // if !httpapi.Read(ctx, rw, r, &createBuild) { + // return + // } + + // // Doing this up front saves a lot of work if the user doesn't have permission. + // // This is checked again in the dbauthz layer, but the check is cached + // // and will be a noop later. + // var action rbac.Action + // switch createBuild.Transition { + // case codersdk.WorkspaceTransitionDelete: + // action = rbac.ActionDelete + // case codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop: + // action = rbac.ActionUpdate + // default: + // httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + // Message: fmt.Sprintf("Transition %q not supported.", createBuild.Transition), + // }) + // return + // } + // if !api.Authorize(r, action, workspace) { + // httpapi.ResourceNotFound(rw) + // return + // } + + // if createBuild.TemplateVersionID == uuid.Nil { + // latestBuild, latestBuildErr := api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) + // if latestBuildErr != nil { + // httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + // Message: "Internal error fetching the latest workspace build.", + // Detail: latestBuildErr.Error(), + // }) + // return + // } + // createBuild.TemplateVersionID = latestBuild.TemplateVersionID + // } + + // templateVersion, err := api.Database.GetTemplateVersionByID(ctx, createBuild.TemplateVersionID) + // if errors.Is(err, sql.ErrNoRows) { + // httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + // Message: "Template version not found.", + // Validations: []codersdk.ValidationError{{ + // Field: "template_version_id", + // Detail: "template version not found", + // }}, + // }) + // return + // } + // if err != nil { + // httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + // Message: "Internal error fetching template version.", + // Detail: err.Error(), + // }) + // return + // } + + // template, err := api.Database.GetTemplateByID(ctx, templateVersion.TemplateID.UUID) + // if err != nil { + // httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + // Message: "Failed to get template", + // Detail: err.Error(), + // }) + // return + // } + + // var state []byte + // // If custom state, deny request since user could be corrupting or leaking + // // cloud state. + // if createBuild.ProvisionerState != nil || createBuild.Orphan { + // if !api.Authorize(r, rbac.ActionUpdate, template.RBACObject()) { + // httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ + // Message: "Only template managers may provide custom state", + // }) + // return + // } + // state = createBuild.ProvisionerState + // } + + // if createBuild.Orphan { + // if createBuild.Transition != codersdk.WorkspaceTransitionDelete { + // httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + // Message: "Orphan is only permitted when deleting a workspace.", + // Detail: err.Error(), + // }) + // return + // } + + // if createBuild.ProvisionerState != nil && createBuild.Orphan { + // httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + // Message: "ProvisionerState cannot be set alongside Orphan since state intent is unclear.", + // }) + // return + // } + // state = []byte{} + // } + + // templateVersionJob, err := api.Database.GetProvisionerJobByID(ctx, templateVersion.JobID) + // if err != nil { + // httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + // Message: "Internal error fetching provisioner job.", + // Detail: err.Error(), + // }) + // return + // } + // templateVersionJobStatus := convertProvisionerJob(templateVersionJob).Status + // switch templateVersionJobStatus { + // case codersdk.ProvisionerJobPending, codersdk.ProvisionerJobRunning: + // httpapi.Write(ctx, rw, http.StatusNotAcceptable, codersdk.Response{ + // Message: fmt.Sprintf("The provided template version is %s. Wait for it to complete importing!", templateVersionJobStatus), + // }) + // return + // case codersdk.ProvisionerJobFailed: + // httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + // Message: fmt.Sprintf("The provided template version %q has failed to import: %q. You cannot build workspaces with it!", templateVersion.Name, templateVersionJob.Error.String), + // }) + // return + // case codersdk.ProvisionerJobCanceled: + // httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + // Message: "The provided template version was canceled during import. You cannot builds workspaces with it!", + // }) + // return + // } + + // tags := provisionerdserver.MutateTags(workspace.OwnerID, templateVersionJob.Tags) + + // // Store prior build number to compute new build number + // var priorBuildNum int32 + // priorHistory, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) + // if err == nil { + // priorJob, err := api.Database.GetProvisionerJobByID(ctx, priorHistory.JobID) + // if err == nil && convertProvisionerJob(priorJob).Status.Active() { + // httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ + // Message: "A workspace build is already active.", + // }) + // return + // } + + // priorBuildNum = priorHistory.BuildNumber + // } else if !errors.Is(err, sql.ErrNoRows) { + // httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + // Message: "Internal error fetching prior workspace build.", + // Detail: err.Error(), + // }) + // return + // } + + // if state == nil { + // state = priorHistory.ProvisionerState + // } + + // dbTemplateVersionParameters, err := api.Database.GetTemplateVersionParameters(ctx, createBuild.TemplateVersionID) + // if err != nil { + // httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + // Message: "Internal error fetching template version parameters.", + // Detail: err.Error(), + // }) + // return + // } + // templateVersionParameters, err := convertTemplateVersionParameters(dbTemplateVersionParameters) + // if err != nil { + // httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + // Message: "Internal error converting template version parameters.", + // Detail: err.Error(), + // }) + // return + // } + + // lastBuildParameters, err := api.Database.GetWorkspaceBuildParameters(ctx, priorHistory.ID) + // if err != nil { + // httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + // Message: "Internal error fetching prior workspace build parameters.", + // Detail: err.Error(), + // }) + // return + // } + // apiLastBuildParameters := convertWorkspaceBuildParameters(lastBuildParameters) + + // legacyParameters, err := api.Database.ParameterValues(ctx, database.ParameterValuesParams{ + // Scopes: []database.ParameterScope{database.ParameterScopeWorkspace}, + // ScopeIds: []uuid.UUID{workspace.ID}, + // }) + // if err != nil && !xerrors.Is(err, sql.ErrNoRows) { + // httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + // Message: "Error fetching previous legacy parameters.", + // Detail: err.Error(), + // }) + // return + // } + + // // Rich parameters migration: include legacy variables to the last build parameters + // for _, templateVersionParameter := range templateVersionParameters { + // // Check if parameter is defined in previous build + // if _, found := findWorkspaceBuildParameter(apiLastBuildParameters, templateVersionParameter.Name); found { + // continue + // } + + // // Check if legacy variable is defined + // for _, legacyParameter := range legacyParameters { + // if legacyParameter.Name != templateVersionParameter.LegacyVariableName { + // continue + // } + + // apiLastBuildParameters = append(apiLastBuildParameters, codersdk.WorkspaceBuildParameter{ + // Name: templateVersionParameter.Name, + // Value: legacyParameter.SourceValue, + // }) + // break + // } + // } + + // err = codersdk.ValidateWorkspaceBuildParameters(templateVersionParameters, createBuild.RichParameterValues, apiLastBuildParameters) + // if err != nil { + // httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + // Message: "Error validating workspace build parameters.", + // Detail: err.Error(), + // }) + // return + // } + + // var parameters []codersdk.WorkspaceBuildParameter + // for _, templateVersionParameter := range templateVersionParameters { + // // Check if parameter value is in request + // if buildParameter, found := findWorkspaceBuildParameter(createBuild.RichParameterValues, templateVersionParameter.Name); found { + // if !templateVersionParameter.Mutable { + // httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + // Message: fmt.Sprintf("Parameter %q is not mutable, so it can't be updated after creating a workspace.", templateVersionParameter.Name), + // }) + // return + // } + // parameters = append(parameters, *buildParameter) + // continue + // } + + // // Check if parameter is defined in previous build + // if buildParameter, found := findWorkspaceBuildParameter(apiLastBuildParameters, templateVersionParameter.Name); found { + // parameters = append(parameters, *buildParameter) + // } + // } + + // var workspaceBuild database.WorkspaceBuild + // var provisionerJob database.ProvisionerJob + // // This must happen in a transaction to ensure history can be inserted, and + // // the prior history can update it's "after" column to point at the new. + // err = api.Database.InTx(func(db database.Store) error { + // // Write/Update any new params + // now := database.Now() + // for _, param := range createBuild.ParameterValues { + // for _, exists := range legacyParameters { + // // If the param exists, delete the old param before inserting the new one + // if exists.Name == param.Name { + // err = db.DeleteParameterValueByID(ctx, exists.ID) + // if err != nil && !xerrors.Is(err, sql.ErrNoRows) { + // return xerrors.Errorf("Failed to delete old param %q: %w", exists.Name, err) + // } + // } + // } + + // _, err = db.InsertParameterValue(ctx, database.InsertParameterValueParams{ + // ID: uuid.New(), + // Name: param.Name, + // CreatedAt: now, + // UpdatedAt: now, + // Scope: database.ParameterScopeWorkspace, + // ScopeID: workspace.ID, + // SourceScheme: database.ParameterSourceScheme(param.SourceScheme), + // SourceValue: param.SourceValue, + // DestinationScheme: database.ParameterDestinationScheme(param.DestinationScheme), + // }) + // if err != nil { + // return xerrors.Errorf("insert parameter value: %w", err) + // } + // } + + // workspaceBuildID := uuid.New() + // input, err := json.Marshal(provisionerdserver.WorkspaceProvisionJob{ + // WorkspaceBuildID: workspaceBuildID, + // }) + // if err != nil { + // return xerrors.Errorf("marshal provision job: %w", err) + // } + // provisionerJob, err = db.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{ + // ID: uuid.New(), + // CreatedAt: database.Now(), + // UpdatedAt: database.Now(), + // InitiatorID: apiKey.UserID, + // OrganizationID: template.OrganizationID, + // Provisioner: template.Provisioner, + // Type: database.ProvisionerJobTypeWorkspaceBuild, + // StorageMethod: templateVersionJob.StorageMethod, + // FileID: templateVersionJob.FileID, + // Input: input, + // Tags: tags, + // }) + // if err != nil { + // return xerrors.Errorf("insert provisioner job: %w", err) + // } + + // workspaceBuild, err = db.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{ + // ID: workspaceBuildID, + // CreatedAt: database.Now(), + // UpdatedAt: database.Now(), + // WorkspaceID: workspace.ID, + // TemplateVersionID: templateVersion.ID, + // BuildNumber: priorBuildNum + 1, + // ProvisionerState: state, + // InitiatorID: apiKey.UserID, + // Transition: database.WorkspaceTransition(createBuild.Transition), + // JobID: provisionerJob.ID, + // Reason: database.BuildReasonInitiator, + // }) + // if err != nil { + // return xerrors.Errorf("insert workspace build: %w", err) + // } + + // names := make([]string, 0, len(parameters)) + // values := make([]string, 0, len(parameters)) + // for _, param := range parameters { + // names = append(names, param.Name) + // values = append(values, param.Value) + // } + // err = db.InsertWorkspaceBuildParameters(ctx, database.InsertWorkspaceBuildParametersParams{ + // WorkspaceBuildID: workspaceBuildID, + // Name: names, + // Value: values, + // }) + // if err != nil { + // return xerrors.Errorf("insert workspace build parameters: %w", err) + // } + + // return nil + // }, nil) + // if err != nil { + // httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + // Message: "Internal error inserting workspace build.", + // Detail: err.Error(), + // }) + // return + // } + + // users, err := api.Database.GetUsersByIDs(ctx, []uuid.UUID{ + // workspace.OwnerID, + // workspaceBuild.InitiatorID, + // }) + // if err != nil { + // httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + // Message: "Internal error getting user.", + // Detail: err.Error(), + // }) + // return + // } + + // apiBuild, err := api.convertWorkspaceBuild( + // workspaceBuild, + // workspace, + // provisionerJob, + // users, + // []database.WorkspaceResource{}, + // []database.WorkspaceResourceMetadatum{}, + // []database.WorkspaceAgent{}, + // []database.WorkspaceApp{}, + // database.TemplateVersion{}, + // ) + // if err != nil { + // httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + // Message: "Internal error converting workspace build.", + // Detail: err.Error(), + // }) + // return + // } + + // api.publishWorkspaceUpdate(ctx, workspace.ID) + + // httpapi.Write(ctx, rw, http.StatusCreated, apiBuild) } // @Summary Cancel workspace build From 4f064d45153c56ab7d45bda2decc317767d6d33a Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Mon, 10 Apr 2023 19:46:02 +0000 Subject: [PATCH 3/7] added new route --- cli/restart.go | 2 +- coderd/apidoc/docs.go | 38 +++++ coderd/apidoc/swagger.json | 32 ++++ coderd/coderd.go | 1 + coderd/workspacebuilds.go | 68 +++++++- docs/api/builds.md | 152 ++++++++++++++++++ site/src/api/api.ts | 9 ++ site/src/components/Workspace/Workspace.tsx | 4 +- .../WorkspacePage/WorkspaceReadyPage.tsx | 1 + .../xServices/workspace/workspaceXService.ts | 32 ++++ 10 files changed, 329 insertions(+), 10 deletions(-) diff --git a/cli/restart.go b/cli/restart.go index 3daa09bc636ea..420379c7c8c69 100644 --- a/cli/restart.go +++ b/cli/restart.go @@ -34,7 +34,7 @@ func (r *RootCmd) restart() *clibase.Cmd { return err } - // get the workspace - this can probably stay in here + // get the workspace workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0]) if err != nil { return err diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 1ece708702624..e668062aff0ef 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -5198,6 +5198,44 @@ const docTemplate = `{ } } }, + "/workspaces/{workspace}/builds/restart": { + "post": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Builds" + ], + "summary": "Restart workspace", + "operationId": "restart-workspace", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Workspace ID", + "name": "workspace", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.WorkspaceBuild" + } + } + } + } + }, "/workspaces/{workspace}/extend": { "put": { "security": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index c342722a24484..88e78f61e9a84 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -4581,6 +4581,38 @@ } } }, + "/workspaces/{workspace}/builds/restart": { + "post": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Builds"], + "summary": "Restart workspace", + "operationId": "restart-workspace", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Workspace ID", + "name": "workspace", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.WorkspaceBuild" + } + } + } + } + }, "/workspaces/{workspace}/extend": { "put": { "security": [ diff --git a/coderd/coderd.go b/coderd/coderd.go index 6a94083cce19a..9aa12487c5d99 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -646,6 +646,7 @@ func New(options *Options) *API { r.Route("/builds", func(r chi.Router) { r.Get("/", api.workspaceBuilds) r.Post("/", api.postWorkspaceBuilds) + r.Post("/restart", api.restartWorkspace) }) r.Route("/autostart", func(r chi.Router) { r.Put("/", api.putWorkspaceAutostart) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 673790c81bbde..97aed3a7ad502 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -288,16 +288,11 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ httpapi.Write(ctx, rw, http.StatusOK, apiBuild) } -func (api *API) postBuild(rw http.ResponseWriter, r *http.Request) (codersdk.WorkspaceBuild, error) { +func (api *API) postBuild(rw http.ResponseWriter, r *http.Request, createBuild codersdk.CreateWorkspaceBuildRequest) (codersdk.WorkspaceBuild, error) { ctx := r.Context() apiKey := httpmw.APIKey(r) workspace := httpmw.WorkspaceParam(r) - var createBuild codersdk.CreateWorkspaceBuildRequest - if !httpapi.Read(ctx, rw, r, &createBuild) { - return codersdk.WorkspaceBuild{}, xerrors.Errorf("Decode CreateWorkspaceBuildRequest") - } - // Doing this up front saves a lot of work if the user doesn't have permission. // This is checked again in the dbauthz layer, but the check is cached // and will be a noop later. @@ -673,6 +668,58 @@ func (api *API) postBuild(rw http.ResponseWriter, r *http.Request) (codersdk.Wor return apiBuild, nil } +// Restarts a workspace. +// +// @Summary Restart workspace +// @ID restart-workspace +// @Security CoderSessionToken +// @Accept json +// @Produce json +// @Tags Builds +// @Param workspace path string true "Workspace ID" format(uuid) +// @Success 200 {object} codersdk.WorkspaceBuild +// @Router /workspaces/{workspace}/builds/restart [post] +func (api *API) restartWorkspace(rw http.ResponseWriter, r *http.Request) { + // what about audit + ctx := r.Context() + fmt.Println("in restartWorkspace!!!!!") + + // create a build - stop the workspace + build, err := api.postBuild(rw, r, codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionStop, + }) + if err != nil { + fmt.Println("in postBuildStop err!!!!!") + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error stopping workspace.", + Detail: err.Error(), + }) + return + } + + // this seems to return the provisioner job - perhaps we are watching for an error - do i need this + + // create a build - start the workspace + build, err = api.postBuild(rw, r, codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionStart, + }) + if err != nil { + fmt.Println("in postBuildStart err!!!!!") + + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error starting workspace.", + Detail: err.Error(), + }) + return + } + + // this seems to return the provisioner job - perhaps we are watching for an error - do i need this + + // not sure what to return + httpapi.Write(ctx, rw, http.StatusOK, build) + +} + // Azure supports instance identity verification: // https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=linux#tabgroup_14 // @@ -690,8 +737,13 @@ func (api *API) postBuild(rw http.ResponseWriter, r *http.Request) (codersdk.Wor func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() - build, buildErr := api.postBuild(rw, r) - if buildErr != nil { + var createBuild codersdk.CreateWorkspaceBuildRequest + if !httpapi.Read(ctx, rw, r, &createBuild) { + return + } + + build, err := api.postBuild(rw, r, createBuild) + if err != nil { // all errors handled in api.postBuild return } diff --git a/docs/api/builds.md b/docs/api/builds.md index ddb1539773506..73c08fbcc3503 100644 --- a/docs/api/builds.md +++ b/docs/api/builds.md @@ -1327,3 +1327,155 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ | 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.WorkspaceBuild](schemas.md#codersdkworkspacebuild) | To perform this operation, you must be authenticated. [Learn more](authentication.md). + +## Restart workspace + +### Code samples + +```shell +# Example request using curl +curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds/restart \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`POST /workspaces/{workspace}/builds/restart` + +### Parameters + +| Name | In | Type | Required | Description | +| ----------- | ---- | ------------ | -------- | ------------ | +| `workspace` | path | string(uuid) | true | Workspace ID | + +### Example responses + +> 200 Response + +```json +{ + "build_number": 0, + "created_at": "2019-08-24T14:15:22Z", + "daily_cost": 0, + "deadline": "2019-08-24T14:15:22Z", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3", + "initiator_name": "string", + "job": { + "canceled_at": "2019-08-24T14:15:22Z", + "completed_at": "2019-08-24T14:15:22Z", + "created_at": "2019-08-24T14:15:22Z", + "error": "string", + "error_code": "MISSING_TEMPLATE_PARAMETER", + "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "started_at": "2019-08-24T14:15:22Z", + "status": "pending", + "tags": { + "property1": "string", + "property2": "string" + }, + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + }, + "max_deadline": "2019-08-24T14:15:22Z", + "reason": "initiator", + "resources": [ + { + "agents": [ + { + "apps": [ + { + "command": "string", + "display_name": "string", + "external": true, + "health": "disabled", + "healthcheck": { + "interval": 0, + "threshold": 0, + "url": "string" + }, + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "sharing_level": "owner", + "slug": "string", + "subdomain": true, + "url": "string" + } + ], + "architecture": "string", + "connection_timeout_seconds": 0, + "created_at": "2019-08-24T14:15:22Z", + "directory": "string", + "disconnected_at": "2019-08-24T14:15:22Z", + "environment_variables": { + "property1": "string", + "property2": "string" + }, + "expanded_directory": "string", + "first_connected_at": "2019-08-24T14:15:22Z", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "instance_id": "string", + "last_connected_at": "2019-08-24T14:15:22Z", + "latency": { + "property1": { + "latency_ms": 0, + "preferred": true + }, + "property2": { + "latency_ms": 0, + "preferred": true + } + }, + "lifecycle_state": "created", + "login_before_ready": true, + "name": "string", + "operating_system": "string", + "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", + "shutdown_script": "string", + "shutdown_script_timeout_seconds": 0, + "startup_logs_length": 0, + "startup_logs_overflowed": true, + "startup_script": "string", + "startup_script_timeout_seconds": 0, + "status": "connecting", + "troubleshooting_url": "string", + "updated_at": "2019-08-24T14:15:22Z", + "version": "string" + } + ], + "created_at": "2019-08-24T14:15:22Z", + "daily_cost": 0, + "hide": true, + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "job_id": "453bd7d7-5355-4d6d-a38e-d9e7eb218c3f", + "metadata": [ + { + "key": "string", + "sensitive": true, + "value": "string" + } + ], + "name": "string", + "type": "string", + "workspace_transition": "start" + } + ], + "status": "pending", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "template_version_name": "string", + "transition": "start", + "updated_at": "2019-08-24T14:15:22Z", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string", + "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", + "workspace_owner_name": "string" +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------ | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.WorkspaceBuild](schemas.md#codersdkworkspacebuild) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/site/src/api/api.ts b/site/src/api/api.ts index a1763ab75fcb6..3544139511cb4 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -499,6 +499,15 @@ export const stopWorkspace = (workspaceId: string) => export const deleteWorkspace = (workspaceId: string) => postWorkspaceBuild(workspaceId, { transition: "delete" }) +export const restartWorkspace = async ( + workspaceId: string, +): Promise => { + const response = await axios.post( + `/api/v2/workspaces/${workspaceId}/builds/restart`, + ) + return response.data +} + export const cancelWorkspaceBuild = async ( workspaceBuildId: TypesGen.WorkspaceBuild["id"], ): Promise => { diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index ff6fe481979c8..5e415ee1e737c 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -39,6 +39,7 @@ export interface WorkspaceProps { } handleStart: () => void handleStop: () => void + handleRestart: () => void handleDelete: () => void handleUpdate: () => void handleCancel: () => void @@ -64,6 +65,7 @@ export const Workspace: FC> = ({ scheduleProps, handleStart, handleStop, + handleRestart, handleDelete, handleUpdate, handleCancel, @@ -125,7 +127,7 @@ export const Workspace: FC> = ({ isOutdated={workspace.outdated} handleStart={handleStart} handleStop={handleStop} - handleRestart={() => console.log("restarting!")} + handleRestart={handleRestart} handleDelete={handleDelete} handleUpdate={handleUpdate} handleCancel={handleCancel} diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 88a614df0b234..0f96c339c835b 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -108,6 +108,7 @@ export const WorkspaceReadyPage = ({ workspace={workspace} handleStart={() => workspaceSend({ type: "START" })} handleStop={() => workspaceSend({ type: "STOP" })} + handleRestart={() => workspaceSend({ type: "RESTART" })} handleDelete={() => workspaceSend({ type: "ASK_DELETE" })} handleUpdate={() => workspaceSend({ type: "UPDATE" })} handleCancel={() => workspaceSend({ type: "CANCEL" })} diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index 1fc1c543fd78e..8f7717437f06d 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -79,6 +79,7 @@ export type WorkspaceEvent = | { type: "REFRESH_WORKSPACE"; data: TypesGen.ServerSentEvent["data"] } | { type: "START" } | { type: "STOP" } + | { type: "RESTART" } | { type: "ASK_DELETE" } | { type: "DELETE" } | { type: "CANCEL_DELETE" } @@ -275,6 +276,7 @@ export const workspaceMachine = createMachine( on: { START: "requestingStart", STOP: "requestingStop", + RESTART: "requestingRestart", ASK_DELETE: "askingDelete", UPDATE: "requestingUpdate", CANCEL: "requestingCancel", @@ -355,6 +357,25 @@ export const workspaceMachine = createMachine( ], }, }, + requestingRestart: { + entry: ["clearBuildError", "updateStatusToPending"], + invoke: { + src: "restartWorkspace", + id: "restartWorkspace", + onDone: [ + { + actions: ["assignBuild"], + target: "idle", + }, + ], + onError: [ + { + actions: "assignBuildError", + target: "idle", + }, + ], + }, + }, requestingDelete: { entry: ["clearBuildError", "updateStatusToPending"], invoke: { @@ -662,6 +683,17 @@ export const workspaceMachine = createMachine( throw Error("Cannot stop workspace without workspace id") } }, + restartWorkspace: (context) => async (send) => { + if (context.workspace) { + const restartWorkspacePromise = await API.restartWorkspace( + context.workspace.id, + ) + send({ type: "REFRESH_TIMELINE" }) + return restartWorkspacePromise + } else { + throw Error("Cannot restart workspace without workspace id") + } + }, deleteWorkspace: async (context) => { if (context.workspace) { const deleteWorkspacePromise = await API.deleteWorkspace( From b35051911195cfc96b80cfd7ed9917af23a1b280 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Mon, 10 Apr 2023 19:58:39 +0000 Subject: [PATCH 4/7] cleanup --- cli/restart.go | 4 - coderd/workspacebuilds.go | 388 -------------------------------------- 2 files changed, 392 deletions(-) diff --git a/cli/restart.go b/cli/restart.go index 420379c7c8c69..5bc423502705e 100644 --- a/cli/restart.go +++ b/cli/restart.go @@ -34,26 +34,22 @@ func (r *RootCmd) restart() *clibase.Cmd { return err } - // get the workspace workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0]) if err != nil { return err } - // create a build - stop the workspace build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ Transition: codersdk.WorkspaceTransitionStop, }) if err != nil { return err } - // this seems to return the provisioner job - perhaps we are watching for an error err = cliui.WorkspaceBuild(ctx, out, client, build.ID) if err != nil { return err } - // create a build - start the workspace build, err = client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ Transition: codersdk.WorkspaceTransitionStart, }) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 97aed3a7ad502..d9fd01ea88d3b 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -680,16 +680,13 @@ func (api *API) postBuild(rw http.ResponseWriter, r *http.Request, createBuild c // @Success 200 {object} codersdk.WorkspaceBuild // @Router /workspaces/{workspace}/builds/restart [post] func (api *API) restartWorkspace(rw http.ResponseWriter, r *http.Request) { - // what about audit ctx := r.Context() - fmt.Println("in restartWorkspace!!!!!") // create a build - stop the workspace build, err := api.postBuild(rw, r, codersdk.CreateWorkspaceBuildRequest{ Transition: codersdk.WorkspaceTransitionStop, }) if err != nil { - fmt.Println("in postBuildStop err!!!!!") httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error stopping workspace.", Detail: err.Error(), @@ -697,15 +694,11 @@ func (api *API) restartWorkspace(rw http.ResponseWriter, r *http.Request) { return } - // this seems to return the provisioner job - perhaps we are watching for an error - do i need this - // create a build - start the workspace build, err = api.postBuild(rw, r, codersdk.CreateWorkspaceBuildRequest{ Transition: codersdk.WorkspaceTransitionStart, }) if err != nil { - fmt.Println("in postBuildStart err!!!!!") - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error starting workspace.", Detail: err.Error(), @@ -713,11 +706,7 @@ func (api *API) restartWorkspace(rw http.ResponseWriter, r *http.Request) { return } - // this seems to return the provisioner job - perhaps we are watching for an error - do i need this - - // not sure what to return httpapi.Write(ctx, rw, http.StatusOK, build) - } // Azure supports instance identity verification: @@ -749,383 +738,6 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { } httpapi.Write(ctx, rw, http.StatusCreated, build) - - // ctx := r.Context() - // apiKey := httpmw.APIKey(r) - // workspace := httpmw.WorkspaceParam(r) - // var createBuild codersdk.CreateWorkspaceBuildRequest - // if !httpapi.Read(ctx, rw, r, &createBuild) { - // return - // } - - // // Doing this up front saves a lot of work if the user doesn't have permission. - // // This is checked again in the dbauthz layer, but the check is cached - // // and will be a noop later. - // var action rbac.Action - // switch createBuild.Transition { - // case codersdk.WorkspaceTransitionDelete: - // action = rbac.ActionDelete - // case codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop: - // action = rbac.ActionUpdate - // default: - // httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - // Message: fmt.Sprintf("Transition %q not supported.", createBuild.Transition), - // }) - // return - // } - // if !api.Authorize(r, action, workspace) { - // httpapi.ResourceNotFound(rw) - // return - // } - - // if createBuild.TemplateVersionID == uuid.Nil { - // latestBuild, latestBuildErr := api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) - // if latestBuildErr != nil { - // httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - // Message: "Internal error fetching the latest workspace build.", - // Detail: latestBuildErr.Error(), - // }) - // return - // } - // createBuild.TemplateVersionID = latestBuild.TemplateVersionID - // } - - // templateVersion, err := api.Database.GetTemplateVersionByID(ctx, createBuild.TemplateVersionID) - // if errors.Is(err, sql.ErrNoRows) { - // httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - // Message: "Template version not found.", - // Validations: []codersdk.ValidationError{{ - // Field: "template_version_id", - // Detail: "template version not found", - // }}, - // }) - // return - // } - // if err != nil { - // httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - // Message: "Internal error fetching template version.", - // Detail: err.Error(), - // }) - // return - // } - - // template, err := api.Database.GetTemplateByID(ctx, templateVersion.TemplateID.UUID) - // if err != nil { - // httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - // Message: "Failed to get template", - // Detail: err.Error(), - // }) - // return - // } - - // var state []byte - // // If custom state, deny request since user could be corrupting or leaking - // // cloud state. - // if createBuild.ProvisionerState != nil || createBuild.Orphan { - // if !api.Authorize(r, rbac.ActionUpdate, template.RBACObject()) { - // httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ - // Message: "Only template managers may provide custom state", - // }) - // return - // } - // state = createBuild.ProvisionerState - // } - - // if createBuild.Orphan { - // if createBuild.Transition != codersdk.WorkspaceTransitionDelete { - // httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - // Message: "Orphan is only permitted when deleting a workspace.", - // Detail: err.Error(), - // }) - // return - // } - - // if createBuild.ProvisionerState != nil && createBuild.Orphan { - // httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - // Message: "ProvisionerState cannot be set alongside Orphan since state intent is unclear.", - // }) - // return - // } - // state = []byte{} - // } - - // templateVersionJob, err := api.Database.GetProvisionerJobByID(ctx, templateVersion.JobID) - // if err != nil { - // httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - // Message: "Internal error fetching provisioner job.", - // Detail: err.Error(), - // }) - // return - // } - // templateVersionJobStatus := convertProvisionerJob(templateVersionJob).Status - // switch templateVersionJobStatus { - // case codersdk.ProvisionerJobPending, codersdk.ProvisionerJobRunning: - // httpapi.Write(ctx, rw, http.StatusNotAcceptable, codersdk.Response{ - // Message: fmt.Sprintf("The provided template version is %s. Wait for it to complete importing!", templateVersionJobStatus), - // }) - // return - // case codersdk.ProvisionerJobFailed: - // httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - // Message: fmt.Sprintf("The provided template version %q has failed to import: %q. You cannot build workspaces with it!", templateVersion.Name, templateVersionJob.Error.String), - // }) - // return - // case codersdk.ProvisionerJobCanceled: - // httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - // Message: "The provided template version was canceled during import. You cannot builds workspaces with it!", - // }) - // return - // } - - // tags := provisionerdserver.MutateTags(workspace.OwnerID, templateVersionJob.Tags) - - // // Store prior build number to compute new build number - // var priorBuildNum int32 - // priorHistory, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) - // if err == nil { - // priorJob, err := api.Database.GetProvisionerJobByID(ctx, priorHistory.JobID) - // if err == nil && convertProvisionerJob(priorJob).Status.Active() { - // httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ - // Message: "A workspace build is already active.", - // }) - // return - // } - - // priorBuildNum = priorHistory.BuildNumber - // } else if !errors.Is(err, sql.ErrNoRows) { - // httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - // Message: "Internal error fetching prior workspace build.", - // Detail: err.Error(), - // }) - // return - // } - - // if state == nil { - // state = priorHistory.ProvisionerState - // } - - // dbTemplateVersionParameters, err := api.Database.GetTemplateVersionParameters(ctx, createBuild.TemplateVersionID) - // if err != nil { - // httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - // Message: "Internal error fetching template version parameters.", - // Detail: err.Error(), - // }) - // return - // } - // templateVersionParameters, err := convertTemplateVersionParameters(dbTemplateVersionParameters) - // if err != nil { - // httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - // Message: "Internal error converting template version parameters.", - // Detail: err.Error(), - // }) - // return - // } - - // lastBuildParameters, err := api.Database.GetWorkspaceBuildParameters(ctx, priorHistory.ID) - // if err != nil { - // httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - // Message: "Internal error fetching prior workspace build parameters.", - // Detail: err.Error(), - // }) - // return - // } - // apiLastBuildParameters := convertWorkspaceBuildParameters(lastBuildParameters) - - // legacyParameters, err := api.Database.ParameterValues(ctx, database.ParameterValuesParams{ - // Scopes: []database.ParameterScope{database.ParameterScopeWorkspace}, - // ScopeIds: []uuid.UUID{workspace.ID}, - // }) - // if err != nil && !xerrors.Is(err, sql.ErrNoRows) { - // httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - // Message: "Error fetching previous legacy parameters.", - // Detail: err.Error(), - // }) - // return - // } - - // // Rich parameters migration: include legacy variables to the last build parameters - // for _, templateVersionParameter := range templateVersionParameters { - // // Check if parameter is defined in previous build - // if _, found := findWorkspaceBuildParameter(apiLastBuildParameters, templateVersionParameter.Name); found { - // continue - // } - - // // Check if legacy variable is defined - // for _, legacyParameter := range legacyParameters { - // if legacyParameter.Name != templateVersionParameter.LegacyVariableName { - // continue - // } - - // apiLastBuildParameters = append(apiLastBuildParameters, codersdk.WorkspaceBuildParameter{ - // Name: templateVersionParameter.Name, - // Value: legacyParameter.SourceValue, - // }) - // break - // } - // } - - // err = codersdk.ValidateWorkspaceBuildParameters(templateVersionParameters, createBuild.RichParameterValues, apiLastBuildParameters) - // if err != nil { - // httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - // Message: "Error validating workspace build parameters.", - // Detail: err.Error(), - // }) - // return - // } - - // var parameters []codersdk.WorkspaceBuildParameter - // for _, templateVersionParameter := range templateVersionParameters { - // // Check if parameter value is in request - // if buildParameter, found := findWorkspaceBuildParameter(createBuild.RichParameterValues, templateVersionParameter.Name); found { - // if !templateVersionParameter.Mutable { - // httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - // Message: fmt.Sprintf("Parameter %q is not mutable, so it can't be updated after creating a workspace.", templateVersionParameter.Name), - // }) - // return - // } - // parameters = append(parameters, *buildParameter) - // continue - // } - - // // Check if parameter is defined in previous build - // if buildParameter, found := findWorkspaceBuildParameter(apiLastBuildParameters, templateVersionParameter.Name); found { - // parameters = append(parameters, *buildParameter) - // } - // } - - // var workspaceBuild database.WorkspaceBuild - // var provisionerJob database.ProvisionerJob - // // This must happen in a transaction to ensure history can be inserted, and - // // the prior history can update it's "after" column to point at the new. - // err = api.Database.InTx(func(db database.Store) error { - // // Write/Update any new params - // now := database.Now() - // for _, param := range createBuild.ParameterValues { - // for _, exists := range legacyParameters { - // // If the param exists, delete the old param before inserting the new one - // if exists.Name == param.Name { - // err = db.DeleteParameterValueByID(ctx, exists.ID) - // if err != nil && !xerrors.Is(err, sql.ErrNoRows) { - // return xerrors.Errorf("Failed to delete old param %q: %w", exists.Name, err) - // } - // } - // } - - // _, err = db.InsertParameterValue(ctx, database.InsertParameterValueParams{ - // ID: uuid.New(), - // Name: param.Name, - // CreatedAt: now, - // UpdatedAt: now, - // Scope: database.ParameterScopeWorkspace, - // ScopeID: workspace.ID, - // SourceScheme: database.ParameterSourceScheme(param.SourceScheme), - // SourceValue: param.SourceValue, - // DestinationScheme: database.ParameterDestinationScheme(param.DestinationScheme), - // }) - // if err != nil { - // return xerrors.Errorf("insert parameter value: %w", err) - // } - // } - - // workspaceBuildID := uuid.New() - // input, err := json.Marshal(provisionerdserver.WorkspaceProvisionJob{ - // WorkspaceBuildID: workspaceBuildID, - // }) - // if err != nil { - // return xerrors.Errorf("marshal provision job: %w", err) - // } - // provisionerJob, err = db.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{ - // ID: uuid.New(), - // CreatedAt: database.Now(), - // UpdatedAt: database.Now(), - // InitiatorID: apiKey.UserID, - // OrganizationID: template.OrganizationID, - // Provisioner: template.Provisioner, - // Type: database.ProvisionerJobTypeWorkspaceBuild, - // StorageMethod: templateVersionJob.StorageMethod, - // FileID: templateVersionJob.FileID, - // Input: input, - // Tags: tags, - // }) - // if err != nil { - // return xerrors.Errorf("insert provisioner job: %w", err) - // } - - // workspaceBuild, err = db.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{ - // ID: workspaceBuildID, - // CreatedAt: database.Now(), - // UpdatedAt: database.Now(), - // WorkspaceID: workspace.ID, - // TemplateVersionID: templateVersion.ID, - // BuildNumber: priorBuildNum + 1, - // ProvisionerState: state, - // InitiatorID: apiKey.UserID, - // Transition: database.WorkspaceTransition(createBuild.Transition), - // JobID: provisionerJob.ID, - // Reason: database.BuildReasonInitiator, - // }) - // if err != nil { - // return xerrors.Errorf("insert workspace build: %w", err) - // } - - // names := make([]string, 0, len(parameters)) - // values := make([]string, 0, len(parameters)) - // for _, param := range parameters { - // names = append(names, param.Name) - // values = append(values, param.Value) - // } - // err = db.InsertWorkspaceBuildParameters(ctx, database.InsertWorkspaceBuildParametersParams{ - // WorkspaceBuildID: workspaceBuildID, - // Name: names, - // Value: values, - // }) - // if err != nil { - // return xerrors.Errorf("insert workspace build parameters: %w", err) - // } - - // return nil - // }, nil) - // if err != nil { - // httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - // Message: "Internal error inserting workspace build.", - // Detail: err.Error(), - // }) - // return - // } - - // users, err := api.Database.GetUsersByIDs(ctx, []uuid.UUID{ - // workspace.OwnerID, - // workspaceBuild.InitiatorID, - // }) - // if err != nil { - // httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - // Message: "Internal error getting user.", - // Detail: err.Error(), - // }) - // return - // } - - // apiBuild, err := api.convertWorkspaceBuild( - // workspaceBuild, - // workspace, - // provisionerJob, - // users, - // []database.WorkspaceResource{}, - // []database.WorkspaceResourceMetadatum{}, - // []database.WorkspaceAgent{}, - // []database.WorkspaceApp{}, - // database.TemplateVersion{}, - // ) - // if err != nil { - // httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - // Message: "Internal error converting workspace build.", - // Detail: err.Error(), - // }) - // return - // } - - // api.publishWorkspaceUpdate(ctx, workspace.ID) - - // httpapi.Write(ctx, rw, http.StatusCreated, apiBuild) } // @Summary Cancel workspace build From 05a03f26531fcf382efad906cf5c9896596eb2a3 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Mon, 10 Apr 2023 19:59:10 +0000 Subject: [PATCH 5/7] remove comment --- cli/restart.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cli/restart.go b/cli/restart.go index 5bc423502705e..51ffb2abbf871 100644 --- a/cli/restart.go +++ b/cli/restart.go @@ -56,7 +56,6 @@ func (r *RootCmd) restart() *clibase.Cmd { if err != nil { return err } - // this seems to return the provisioner job - perhaps we are watching for an error err = cliui.WorkspaceBuild(ctx, out, client, build.ID) if err != nil { return err From 054663fc2780bb7ea77b362a4ab61db9921ebf9c Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Wed, 12 Apr 2023 16:33:15 +0000 Subject: [PATCH 6/7] added a new restart transition --- coderd/apidoc/docs.go | 55 +-- coderd/apidoc/swagger.json | 47 +-- coderd/coderd.go | 1 - coderd/database/dbfake/databasefake.go | 16 + coderd/database/dump.sql | 3 +- ..._add_workspace_transition_restart.down.sql | 2 + ...17_add_workspace_transition_restart.up.sql | 1 + coderd/database/modelmethods.go | 24 +- coderd/database/models.go | 11 +- coderd/workspacebuilds.go | 338 ++++++++++-------- codersdk/workspacebuilds.go | 29 +- codersdk/workspaces.go | 2 +- docs/api/builds.md | 152 -------- docs/api/schemas.md | 52 +-- site/src/api/api.ts | 15 +- site/src/api/typesGenerated.ts | 7 +- .../xServices/workspace/workspaceXService.ts | 5 +- 17 files changed, 318 insertions(+), 442 deletions(-) create mode 100644 coderd/database/migrations/000117_add_workspace_transition_restart.down.sql create mode 100644 coderd/database/migrations/000117_add_workspace_transition_restart.up.sql diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index d46e8bd568aef..db7dc1c1ba6c1 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -5329,44 +5329,6 @@ const docTemplate = `{ } } }, - "/workspaces/{workspace}/builds/restart": { - "post": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Builds" - ], - "summary": "Restart workspace", - "operationId": "restart-workspace", - "parameters": [ - { - "type": "string", - "format": "uuid", - "description": "Workspace ID", - "name": "workspace", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.WorkspaceBuild" - } - } - } - } - }, "/workspaces/{workspace}/extend": { "put": { "security": [ @@ -6784,7 +6746,8 @@ const docTemplate = `{ "create", "start", "stop", - "delete" + "delete", + "restart" ], "allOf": [ { @@ -9602,7 +9565,9 @@ const docTemplate = `{ "canceling", "canceled", "deleting", - "deleted" + "deleted", + "restarting", + "restarted" ], "x-enum-varnames": [ "WorkspaceStatusPending", @@ -9614,7 +9579,9 @@ const docTemplate = `{ "WorkspaceStatusCanceling", "WorkspaceStatusCanceled", "WorkspaceStatusDeleting", - "WorkspaceStatusDeleted" + "WorkspaceStatusDeleted", + "WorkspaceStatusRestarting", + "WorkspaceStatusRestarted" ] }, "codersdk.WorkspaceTransition": { @@ -9622,12 +9589,14 @@ const docTemplate = `{ "enum": [ "start", "stop", - "delete" + "delete", + "restart" ], "x-enum-varnames": [ "WorkspaceTransitionStart", "WorkspaceTransitionStop", - "WorkspaceTransitionDelete" + "WorkspaceTransitionDelete", + "WorkspaceTransitionRestart" ] }, "codersdk.WorkspacesResponse": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 41423d131311b..6d265209aeca3 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -4696,38 +4696,6 @@ } } }, - "/workspaces/{workspace}/builds/restart": { - "post": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Builds"], - "summary": "Restart workspace", - "operationId": "restart-workspace", - "parameters": [ - { - "type": "string", - "format": "uuid", - "description": "Workspace ID", - "name": "workspace", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.WorkspaceBuild" - } - } - } - } - }, "/workspaces/{workspace}/extend": { "put": { "security": [ @@ -6034,7 +6002,7 @@ "format": "uuid" }, "transition": { - "enum": ["create", "start", "stop", "delete"], + "enum": ["create", "start", "stop", "delete", "restart"], "allOf": [ { "$ref": "#/definitions/codersdk.WorkspaceTransition" @@ -8670,7 +8638,9 @@ "canceling", "canceled", "deleting", - "deleted" + "deleted", + "restarting", + "restarted" ], "x-enum-varnames": [ "WorkspaceStatusPending", @@ -8682,16 +8652,19 @@ "WorkspaceStatusCanceling", "WorkspaceStatusCanceled", "WorkspaceStatusDeleting", - "WorkspaceStatusDeleted" + "WorkspaceStatusDeleted", + "WorkspaceStatusRestarting", + "WorkspaceStatusRestarted" ] }, "codersdk.WorkspaceTransition": { "type": "string", - "enum": ["start", "stop", "delete"], + "enum": ["start", "stop", "delete", "restart"], "x-enum-varnames": [ "WorkspaceTransitionStart", "WorkspaceTransitionStop", - "WorkspaceTransitionDelete" + "WorkspaceTransitionDelete", + "WorkspaceTransitionRestart" ] }, "codersdk.WorkspacesResponse": { diff --git a/coderd/coderd.go b/coderd/coderd.go index 97175a072f7e3..afc87b20bd73e 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -683,7 +683,6 @@ func New(options *Options) *API { r.Route("/builds", func(r chi.Router) { r.Get("/", api.workspaceBuilds) r.Post("/", api.postWorkspaceBuilds) - r.Post("/restart", api.restartWorkspace) }) r.Route("/autostart", func(r chi.Router) { r.Put("/", api.putWorkspaceAutostart) diff --git a/coderd/database/dbfake/databasefake.go b/coderd/database/dbfake/databasefake.go index cb4ada860a8a2..bbdf25465ddab 100644 --- a/coderd/database/dbfake/databasefake.go +++ b/coderd/database/dbfake/databasefake.go @@ -1255,6 +1255,22 @@ func (q *fakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database. continue } + case database.WorkspaceStatusRestarted: + if !job.CompletedAt.Valid && + job.CanceledAt.Valid && + job.Error.Valid && + build.Transition != database.WorkspaceTransitionRestart { + continue + } + + case database.WorkspaceStatusRestarting: + if !job.CompletedAt.Valid && + job.CanceledAt.Valid && + job.Error.Valid && + build.Transition != database.WorkspaceTransitionRestart { + continue + } + default: return nil, xerrors.Errorf("unknown workspace status in filter: %q", arg.Status) } diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index f5bbe0cc04a85..4c5f6f205499b 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -125,7 +125,8 @@ CREATE TYPE workspace_app_health AS ENUM ( CREATE TYPE workspace_transition AS ENUM ( 'start', 'stop', - 'delete' + 'delete', + 'restart' ); CREATE TABLE api_keys ( diff --git a/coderd/database/migrations/000117_add_workspace_transition_restart.down.sql b/coderd/database/migrations/000117_add_workspace_transition_restart.down.sql new file mode 100644 index 0000000000000..d1d1637f4fa90 --- /dev/null +++ b/coderd/database/migrations/000117_add_workspace_transition_restart.down.sql @@ -0,0 +1,2 @@ +-- It's not possible to drop enum values from enum types, so the UP has "IF NOT +-- EXISTS". diff --git a/coderd/database/migrations/000117_add_workspace_transition_restart.up.sql b/coderd/database/migrations/000117_add_workspace_transition_restart.up.sql new file mode 100644 index 0000000000000..e0a3e5e512a1e --- /dev/null +++ b/coderd/database/migrations/000117_add_workspace_transition_restart.up.sql @@ -0,0 +1 @@ +ALTER TYPE workspace_transition ADD VALUE IF NOT EXISTS 'restart'; diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index 1c20a0cced741..24520fe8b90b1 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -13,16 +13,18 @@ import ( type WorkspaceStatus string const ( - WorkspaceStatusPending WorkspaceStatus = "pending" - WorkspaceStatusStarting WorkspaceStatus = "starting" - WorkspaceStatusRunning WorkspaceStatus = "running" - WorkspaceStatusStopping WorkspaceStatus = "stopping" - WorkspaceStatusStopped WorkspaceStatus = "stopped" - WorkspaceStatusFailed WorkspaceStatus = "failed" - WorkspaceStatusCanceling WorkspaceStatus = "canceling" - WorkspaceStatusCanceled WorkspaceStatus = "canceled" - WorkspaceStatusDeleting WorkspaceStatus = "deleting" - WorkspaceStatusDeleted WorkspaceStatus = "deleted" + WorkspaceStatusPending WorkspaceStatus = "pending" + WorkspaceStatusStarting WorkspaceStatus = "starting" + WorkspaceStatusRunning WorkspaceStatus = "running" + WorkspaceStatusStopping WorkspaceStatus = "stopping" + WorkspaceStatusStopped WorkspaceStatus = "stopped" + WorkspaceStatusFailed WorkspaceStatus = "failed" + WorkspaceStatusCanceling WorkspaceStatus = "canceling" + WorkspaceStatusCanceled WorkspaceStatus = "canceled" + WorkspaceStatusDeleting WorkspaceStatus = "deleting" + WorkspaceStatusDeleted WorkspaceStatus = "deleted" + WorkspaceStatusRestarting WorkspaceStatus = "restarting" + WorkspaceStatusRestarted WorkspaceStatus = "restarted" ) func (s WorkspaceStatus) Valid() bool { @@ -30,7 +32,7 @@ func (s WorkspaceStatus) Valid() bool { case WorkspaceStatusPending, WorkspaceStatusStarting, WorkspaceStatusRunning, WorkspaceStatusStopping, WorkspaceStatusStopped, WorkspaceStatusFailed, WorkspaceStatusCanceling, WorkspaceStatusCanceled, WorkspaceStatusDeleting, - WorkspaceStatusDeleted: + WorkspaceStatusDeleted, WorkspaceStatusRestarting, WorkspaceStatusRestarted: return true default: return false diff --git a/coderd/database/models.go b/coderd/database/models.go index 529bb413e48f6..1304697c7093d 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1160,9 +1160,10 @@ func AllWorkspaceAppHealthValues() []WorkspaceAppHealth { type WorkspaceTransition string const ( - WorkspaceTransitionStart WorkspaceTransition = "start" - WorkspaceTransitionStop WorkspaceTransition = "stop" - WorkspaceTransitionDelete WorkspaceTransition = "delete" + WorkspaceTransitionStart WorkspaceTransition = "start" + WorkspaceTransitionStop WorkspaceTransition = "stop" + WorkspaceTransitionDelete WorkspaceTransition = "delete" + WorkspaceTransitionRestart WorkspaceTransition = "restart" ) func (e *WorkspaceTransition) Scan(src interface{}) error { @@ -1204,7 +1205,8 @@ func (e WorkspaceTransition) Valid() bool { switch e { case WorkspaceTransitionStart, WorkspaceTransitionStop, - WorkspaceTransitionDelete: + WorkspaceTransitionDelete, + WorkspaceTransitionRestart: return true } return false @@ -1215,6 +1217,7 @@ func AllWorkspaceTransitionValues() []WorkspaceTransition { WorkspaceTransitionStart, WorkspaceTransitionStop, WorkspaceTransitionDelete, + WorkspaceTransitionRestart, } } diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 78ba0a1a68a1f..66854faa310e3 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -288,10 +288,28 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ httpapi.Write(ctx, rw, http.StatusOK, apiBuild) } -func (api *API) postBuild(rw http.ResponseWriter, r *http.Request, createBuild codersdk.CreateWorkspaceBuildRequest) (codersdk.WorkspaceBuild, error) { +// Azure supports instance identity verification: +// https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=linux#tabgroup_14 +// +// @Summary Create workspace build +// @ID create-workspace-build +// @Security CoderSessionToken +// @Accept json +// @Produce json +// @Tags Builds +// @Param workspace path string true "Workspace ID" format(uuid) +// @Param request body codersdk.CreateWorkspaceBuildRequest true "Create workspace build request" +// @Success 200 {object} codersdk.WorkspaceBuild +// @Router /workspaces/{workspace}/builds [post] +// nolint:gocyclo +func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() apiKey := httpmw.APIKey(r) workspace := httpmw.WorkspaceParam(r) + var createBuild codersdk.CreateWorkspaceBuildRequest + if !httpapi.Read(ctx, rw, r, &createBuild) { + return + } // Doing this up front saves a lot of work if the user doesn't have permission. // This is checked again in the dbauthz layer, but the check is cached @@ -300,17 +318,17 @@ func (api *API) postBuild(rw http.ResponseWriter, r *http.Request, createBuild c switch createBuild.Transition { case codersdk.WorkspaceTransitionDelete: action = rbac.ActionDelete - case codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop: + case codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop, codersdk.WorkspaceTransitionRestart: action = rbac.ActionUpdate default: httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: fmt.Sprintf("Transition %q not supported.", createBuild.Transition), }) - return codersdk.WorkspaceBuild{}, xerrors.Errorf("Transition %q not supported.", createBuild.Transition) + return } if !api.Authorize(r, action, workspace) { httpapi.ResourceNotFound(rw) - return codersdk.WorkspaceBuild{}, xerrors.Errorf("Resource not found.") + return } if createBuild.TemplateVersionID == uuid.Nil { @@ -320,7 +338,7 @@ func (api *API) postBuild(rw http.ResponseWriter, r *http.Request, createBuild c Message: "Internal error fetching the latest workspace build.", Detail: latestBuildErr.Error(), }) - return codersdk.WorkspaceBuild{}, xerrors.Errorf("Fetch the latest workspace build: %w", latestBuildErr.Error()) + return } createBuild.TemplateVersionID = latestBuild.TemplateVersionID } @@ -334,18 +352,14 @@ func (api *API) postBuild(rw http.ResponseWriter, r *http.Request, createBuild c Detail: "template version not found", }}, }) - return codersdk.WorkspaceBuild{}, xerrors.Errorf("Template version not found: %w", []codersdk.ValidationError{{ - Field: "template_version_id", - Detail: "template version not found", - }}) - + return } if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error fetching template version.", Detail: err.Error(), }) - return codersdk.WorkspaceBuild{}, xerrors.Errorf("Fetch template version: %w", err.Error()) + return } template, err := api.Database.GetTemplateByID(ctx, templateVersion.TemplateID.UUID) @@ -354,7 +368,7 @@ func (api *API) postBuild(rw http.ResponseWriter, r *http.Request, createBuild c Message: "Failed to get template", Detail: err.Error(), }) - return codersdk.WorkspaceBuild{}, xerrors.Errorf("Get template: %w", err.Error()) + return } var state []byte @@ -365,7 +379,7 @@ func (api *API) postBuild(rw http.ResponseWriter, r *http.Request, createBuild c httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ Message: "Only template managers may provide custom state", }) - return codersdk.WorkspaceBuild{}, xerrors.Errorf("Unauthorized request.") + return } state = createBuild.ProvisionerState } @@ -376,14 +390,14 @@ func (api *API) postBuild(rw http.ResponseWriter, r *http.Request, createBuild c Message: "Orphan is only permitted when deleting a workspace.", Detail: err.Error(), }) - return codersdk.WorkspaceBuild{}, xerrors.Errorf("Orphan is only permitted when deleting a workspace: %w", err.Error()) + return } if createBuild.ProvisionerState != nil && createBuild.Orphan { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "ProvisionerState cannot be set alongside Orphan since state intent is unclear.", }) - return codersdk.WorkspaceBuild{}, xerrors.Errorf("ProvisionerState cannot be set alongside Orphan since state intent is unclear.") + return } state = []byte{} } @@ -394,7 +408,7 @@ func (api *API) postBuild(rw http.ResponseWriter, r *http.Request, createBuild c Message: "Internal error fetching provisioner job.", Detail: err.Error(), }) - return codersdk.WorkspaceBuild{}, xerrors.Errorf("Fetch provisioner job: %w", err.Error()) + return } templateVersionJobStatus := convertProvisionerJob(templateVersionJob).Status switch templateVersionJobStatus { @@ -402,17 +416,17 @@ func (api *API) postBuild(rw http.ResponseWriter, r *http.Request, createBuild c httpapi.Write(ctx, rw, http.StatusNotAcceptable, codersdk.Response{ Message: fmt.Sprintf("The provided template version is %s. Wait for it to complete importing!", templateVersionJobStatus), }) - return codersdk.WorkspaceBuild{}, xerrors.Errorf("The provided template version is %s.", templateVersionJobStatus) + return case codersdk.ProvisionerJobFailed: httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: fmt.Sprintf("The provided template version %q has failed to import: %q. You cannot build workspaces with it!", templateVersion.Name, templateVersionJob.Error.String), }) - return codersdk.WorkspaceBuild{}, xerrors.Errorf("The provided template version %q failed to import.", templateVersion.Name) + return case codersdk.ProvisionerJobCanceled: httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "The provided template version was canceled during import. You cannot builds workspaces with it!", }) - return codersdk.WorkspaceBuild{}, xerrors.Errorf("The provided template version was canceled during import.") + return } tags := provisionerdserver.MutateTags(workspace.OwnerID, templateVersionJob.Tags) @@ -426,7 +440,7 @@ func (api *API) postBuild(rw http.ResponseWriter, r *http.Request, createBuild c httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ Message: "A workspace build is already active.", }) - return codersdk.WorkspaceBuild{}, xerrors.Errorf("Workspace build is already active.") + return } priorBuildNum = priorHistory.BuildNumber @@ -435,7 +449,7 @@ func (api *API) postBuild(rw http.ResponseWriter, r *http.Request, createBuild c Message: "Internal error fetching prior workspace build.", Detail: err.Error(), }) - return codersdk.WorkspaceBuild{}, xerrors.Errorf("Fetch prior workspace build: %w", err.Error()) + return } if state == nil { @@ -448,7 +462,7 @@ func (api *API) postBuild(rw http.ResponseWriter, r *http.Request, createBuild c Message: "Internal error fetching template version parameters.", Detail: err.Error(), }) - return codersdk.WorkspaceBuild{}, xerrors.Errorf("Fetch template version parameters: %w", err.Error()) + return } templateVersionParameters, err := convertTemplateVersionParameters(dbTemplateVersionParameters) if err != nil { @@ -456,7 +470,7 @@ func (api *API) postBuild(rw http.ResponseWriter, r *http.Request, createBuild c Message: "Internal error converting template version parameters.", Detail: err.Error(), }) - return codersdk.WorkspaceBuild{}, xerrors.Errorf("Convert template version parameters: %w", err.Error()) + return } lastBuildParameters, err := api.Database.GetWorkspaceBuildParameters(ctx, priorHistory.ID) @@ -465,8 +479,7 @@ func (api *API) postBuild(rw http.ResponseWriter, r *http.Request, createBuild c Message: "Internal error fetching prior workspace build parameters.", Detail: err.Error(), }) - return codersdk.WorkspaceBuild{}, xerrors.Errorf("Fetch prior workspace build parameters: %w", err.Error()) - + return } apiLastBuildParameters := convertWorkspaceBuildParameters(lastBuildParameters) @@ -479,7 +492,7 @@ func (api *API) postBuild(rw http.ResponseWriter, r *http.Request, createBuild c Message: "Error fetching previous legacy parameters.", Detail: err.Error(), }) - return codersdk.WorkspaceBuild{}, xerrors.Errorf("Fetch previous legacy parameters: %w", err.Error()) + return } // Rich parameters migration: include legacy variables to the last build parameters @@ -509,7 +522,7 @@ func (api *API) postBuild(rw http.ResponseWriter, r *http.Request, createBuild c Message: "Error validating workspace build parameters.", Detail: err.Error(), }) - return codersdk.WorkspaceBuild{}, xerrors.Errorf("Validate workspace build parameters: %w", err.Error()) + return } var parameters []codersdk.WorkspaceBuildParameter @@ -521,7 +534,7 @@ func (api *API) postBuild(rw http.ResponseWriter, r *http.Request, createBuild c httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: fmt.Sprintf("Parameter %q is not mutable, so it can't be updated after creating a workspace.", templateVersionParameter.Name), }) - return codersdk.WorkspaceBuild{}, xerrors.Errorf("Parameter not mutable: %w", err.Error()) + return } } parameters = append(parameters, *buildParameter) @@ -575,44 +588,133 @@ func (api *API) postBuild(rw http.ResponseWriter, r *http.Request, createBuild c } } - workspaceBuildID := uuid.New() - input, err := json.Marshal(provisionerdserver.WorkspaceProvisionJob{ - WorkspaceBuildID: workspaceBuildID, - LogLevel: string(createBuild.LogLevel), - }) - if err != nil { - return xerrors.Errorf("marshal provision job: %w", err) - } - provisionerJob, err = db.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{ - ID: uuid.New(), - CreatedAt: database.Now(), - UpdatedAt: database.Now(), - InitiatorID: apiKey.UserID, - OrganizationID: template.OrganizationID, - Provisioner: template.Provisioner, - Type: database.ProvisionerJobTypeWorkspaceBuild, - StorageMethod: templateVersionJob.StorageMethod, - FileID: templateVersionJob.FileID, - Input: input, - Tags: tags, - }) - if err != nil { - return xerrors.Errorf("insert provisioner job: %w", err) + var ( + workspaceBuildID uuid.UUID + stopBuildID uuid.UUID + startBuildID uuid.UUID + ) + + if database.WorkspaceTransition(createBuild.Transition) == database.WorkspaceTransitionRestart { + fmt.Println("in restart block") + stopBuildID = uuid.New() + input, err := json.Marshal(provisionerdserver.WorkspaceProvisionJob{ + WorkspaceBuildID: stopBuildID, + LogLevel: string(createBuild.LogLevel), + }) + if err != nil { + return xerrors.Errorf("marshal provision job: %w", err) + } + provisionerJob, err = db.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{ + ID: uuid.New(), + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + InitiatorID: apiKey.UserID, + OrganizationID: template.OrganizationID, + Provisioner: template.Provisioner, + Type: database.ProvisionerJobTypeWorkspaceBuild, + StorageMethod: templateVersionJob.StorageMethod, + FileID: templateVersionJob.FileID, + Input: input, + Tags: tags, + }) + if err != nil { + return xerrors.Errorf("insert provisioner job: %w", err) + } + + workspaceBuild, err = db.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{ + ID: stopBuildID, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + WorkspaceID: workspace.ID, + TemplateVersionID: templateVersion.ID, + BuildNumber: priorBuildNum + 1, + ProvisionerState: state, + InitiatorID: apiKey.UserID, + Transition: database.WorkspaceTransitionStop, + JobID: provisionerJob.ID, + Reason: database.BuildReasonInitiator, + }) + + startBuildID = uuid.New() + input, err = json.Marshal(provisionerdserver.WorkspaceProvisionJob{ + WorkspaceBuildID: startBuildID, + LogLevel: string(createBuild.LogLevel), + }) + if err != nil { + return xerrors.Errorf("marshal provision job: %w", err) + } + provisionerJob, err = db.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{ + ID: uuid.New(), + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + InitiatorID: apiKey.UserID, + OrganizationID: template.OrganizationID, + Provisioner: template.Provisioner, + Type: database.ProvisionerJobTypeWorkspaceBuild, + StorageMethod: templateVersionJob.StorageMethod, + FileID: templateVersionJob.FileID, + Input: input, + Tags: tags, + }) + if err != nil { + return xerrors.Errorf("insert provisioner job: %w", err) + } + + workspaceBuild, err = db.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{ + ID: startBuildID, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + WorkspaceID: workspace.ID, + TemplateVersionID: templateVersion.ID, + BuildNumber: priorBuildNum + 1, + ProvisionerState: state, + InitiatorID: apiKey.UserID, + Transition: database.WorkspaceTransitionStart, + JobID: provisionerJob.ID, + Reason: database.BuildReasonInitiator, + }) + + } else { + workspaceBuildID = uuid.New() + input, err := json.Marshal(provisionerdserver.WorkspaceProvisionJob{ + WorkspaceBuildID: workspaceBuildID, + LogLevel: string(createBuild.LogLevel), + }) + if err != nil { + return xerrors.Errorf("marshal provision job: %w", err) + } + provisionerJob, err = db.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{ + ID: uuid.New(), + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + InitiatorID: apiKey.UserID, + OrganizationID: template.OrganizationID, + Provisioner: template.Provisioner, + Type: database.ProvisionerJobTypeWorkspaceBuild, + StorageMethod: templateVersionJob.StorageMethod, + FileID: templateVersionJob.FileID, + Input: input, + Tags: tags, + }) + if err != nil { + return xerrors.Errorf("insert provisioner job: %w", err) + } + + workspaceBuild, err = db.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{ + ID: workspaceBuildID, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + WorkspaceID: workspace.ID, + TemplateVersionID: templateVersion.ID, + BuildNumber: priorBuildNum + 1, + ProvisionerState: state, + InitiatorID: apiKey.UserID, + Transition: database.WorkspaceTransition(createBuild.Transition), + JobID: provisionerJob.ID, + Reason: database.BuildReasonInitiator, + }) } - workspaceBuild, err = db.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{ - ID: workspaceBuildID, - CreatedAt: database.Now(), - UpdatedAt: database.Now(), - WorkspaceID: workspace.ID, - TemplateVersionID: templateVersion.ID, - BuildNumber: priorBuildNum + 1, - ProvisionerState: state, - InitiatorID: apiKey.UserID, - Transition: database.WorkspaceTransition(createBuild.Transition), - JobID: provisionerJob.ID, - Reason: database.BuildReasonInitiator, - }) if err != nil { return xerrors.Errorf("insert workspace build: %w", err) } @@ -623,11 +725,25 @@ func (api *API) postBuild(rw http.ResponseWriter, r *http.Request, createBuild c names = append(names, param.Name) values = append(values, param.Value) } - err = db.InsertWorkspaceBuildParameters(ctx, database.InsertWorkspaceBuildParametersParams{ - WorkspaceBuildID: workspaceBuildID, - Name: names, - Value: values, - }) + + if database.WorkspaceTransition(createBuild.Transition) == database.WorkspaceTransitionRestart { + err = db.InsertWorkspaceBuildParameters(ctx, database.InsertWorkspaceBuildParametersParams{ + WorkspaceBuildID: stopBuildID, + Name: names, + Value: values, + }) + err = db.InsertWorkspaceBuildParameters(ctx, database.InsertWorkspaceBuildParametersParams{ + WorkspaceBuildID: startBuildID, + Name: names, + Value: values, + }) + } else { + err = db.InsertWorkspaceBuildParameters(ctx, database.InsertWorkspaceBuildParametersParams{ + WorkspaceBuildID: workspaceBuildID, + Name: names, + Value: values, + }) + } if err != nil { return xerrors.Errorf("insert workspace build parameters: %w", err) } @@ -639,7 +755,7 @@ func (api *API) postBuild(rw http.ResponseWriter, r *http.Request, createBuild c Message: "Internal error inserting workspace build.", Detail: err.Error(), }) - xerrors.Errorf("Insert workspace build: %w", err.Error()) + return } users, err := api.Database.GetUsersByIDs(ctx, []uuid.UUID{ @@ -651,7 +767,7 @@ func (api *API) postBuild(rw http.ResponseWriter, r *http.Request, createBuild c Message: "Internal error getting user.", Detail: err.Error(), }) - xerrors.Errorf("Get user: %w", err.Error()) + return } apiBuild, err := api.convertWorkspaceBuild( @@ -670,84 +786,12 @@ func (api *API) postBuild(rw http.ResponseWriter, r *http.Request, createBuild c Message: "Internal error converting workspace build.", Detail: err.Error(), }) - xerrors.Errorf("Convert workspace build: %w", err.Error()) - } - - api.publishWorkspaceUpdate(ctx, workspace.ID) - - return apiBuild, nil -} - -// Restarts a workspace. -// -// @Summary Restart workspace -// @ID restart-workspace -// @Security CoderSessionToken -// @Accept json -// @Produce json -// @Tags Builds -// @Param workspace path string true "Workspace ID" format(uuid) -// @Success 200 {object} codersdk.WorkspaceBuild -// @Router /workspaces/{workspace}/builds/restart [post] -func (api *API) restartWorkspace(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - // create a build - stop the workspace - build, err := api.postBuild(rw, r, codersdk.CreateWorkspaceBuildRequest{ - Transition: codersdk.WorkspaceTransitionStop, - }) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error stopping workspace.", - Detail: err.Error(), - }) return } - // create a build - start the workspace - build, err = api.postBuild(rw, r, codersdk.CreateWorkspaceBuildRequest{ - Transition: codersdk.WorkspaceTransitionStart, - }) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error starting workspace.", - Detail: err.Error(), - }) - return - } - - httpapi.Write(ctx, rw, http.StatusOK, build) -} - -// Azure supports instance identity verification: -// https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=linux#tabgroup_14 -// -// @Summary Create workspace build -// @ID create-workspace-build -// @Security CoderSessionToken -// @Accept json -// @Produce json -// @Tags Builds -// @Param workspace path string true "Workspace ID" format(uuid) -// @Param request body codersdk.CreateWorkspaceBuildRequest true "Create workspace build request" -// @Success 200 {object} codersdk.WorkspaceBuild -// @Router /workspaces/{workspace}/builds [post] -// nolint:gocyclo -func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - var createBuild codersdk.CreateWorkspaceBuildRequest - if !httpapi.Read(ctx, rw, r, &createBuild) { - return - } - - build, err := api.postBuild(rw, r, createBuild) - if err != nil { - // all errors handled in api.postBuild - return - } + api.publishWorkspaceUpdate(ctx, workspace.ID) - httpapi.Write(ctx, rw, http.StatusCreated, build) + httpapi.Write(ctx, rw, http.StatusCreated, apiBuild) } // @Summary Cancel workspace build @@ -1250,6 +1294,8 @@ func convertWorkspaceStatus(jobStatus codersdk.ProvisionerJobStatus, transition return codersdk.WorkspaceStatusStopping case codersdk.WorkspaceTransitionDelete: return codersdk.WorkspaceStatusDeleting + case codersdk.WorkspaceTransitionRestart: + return codersdk.WorkspaceStatusRestarting } case codersdk.ProvisionerJobSucceeded: switch transition { @@ -1259,6 +1305,8 @@ func convertWorkspaceStatus(jobStatus codersdk.ProvisionerJobStatus, transition return codersdk.WorkspaceStatusStopped case codersdk.WorkspaceTransitionDelete: return codersdk.WorkspaceStatusDeleted + case codersdk.WorkspaceTransitionRestart: + return codersdk.WorkspaceStatusRestarted } case codersdk.ProvisionerJobCanceling: return codersdk.WorkspaceStatusCanceling diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index c7bdf022d238f..901f4ce253f08 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -14,24 +14,27 @@ import ( type WorkspaceTransition string const ( - WorkspaceTransitionStart WorkspaceTransition = "start" - WorkspaceTransitionStop WorkspaceTransition = "stop" - WorkspaceTransitionDelete WorkspaceTransition = "delete" + WorkspaceTransitionStart WorkspaceTransition = "start" + WorkspaceTransitionStop WorkspaceTransition = "stop" + WorkspaceTransitionDelete WorkspaceTransition = "delete" + WorkspaceTransitionRestart WorkspaceTransition = "restart" ) type WorkspaceStatus string const ( - WorkspaceStatusPending WorkspaceStatus = "pending" - WorkspaceStatusStarting WorkspaceStatus = "starting" - WorkspaceStatusRunning WorkspaceStatus = "running" - WorkspaceStatusStopping WorkspaceStatus = "stopping" - WorkspaceStatusStopped WorkspaceStatus = "stopped" - WorkspaceStatusFailed WorkspaceStatus = "failed" - WorkspaceStatusCanceling WorkspaceStatus = "canceling" - WorkspaceStatusCanceled WorkspaceStatus = "canceled" - WorkspaceStatusDeleting WorkspaceStatus = "deleting" - WorkspaceStatusDeleted WorkspaceStatus = "deleted" + WorkspaceStatusPending WorkspaceStatus = "pending" + WorkspaceStatusStarting WorkspaceStatus = "starting" + WorkspaceStatusRunning WorkspaceStatus = "running" + WorkspaceStatusStopping WorkspaceStatus = "stopping" + WorkspaceStatusStopped WorkspaceStatus = "stopped" + WorkspaceStatusFailed WorkspaceStatus = "failed" + WorkspaceStatusCanceling WorkspaceStatus = "canceling" + WorkspaceStatusCanceled WorkspaceStatus = "canceled" + WorkspaceStatusDeleting WorkspaceStatus = "deleting" + WorkspaceStatusDeleted WorkspaceStatus = "deleted" + WorkspaceStatusRestarting WorkspaceStatus = "restarting" + WorkspaceStatusRestarted WorkspaceStatus = "restarted" ) type BuildReason string diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 768ebc27c86ee..efaf48097f570 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -55,7 +55,7 @@ const ( // CreateWorkspaceBuildRequest provides options to update the latest workspace build. type CreateWorkspaceBuildRequest struct { TemplateVersionID uuid.UUID `json:"template_version_id,omitempty" format:"uuid"` - Transition WorkspaceTransition `json:"transition" validate:"oneof=create start stop delete,required"` + Transition WorkspaceTransition `json:"transition" validate:"oneof=create start stop delete restart,required"` DryRun bool `json:"dry_run,omitempty"` ProvisionerState []byte `json:"state,omitempty"` // Orphan may be set for the Destroy transition. diff --git a/docs/api/builds.md b/docs/api/builds.md index 811844a9f65e9..9e05671efb003 100644 --- a/docs/api/builds.md +++ b/docs/api/builds.md @@ -1328,155 +1328,3 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ | 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.WorkspaceBuild](schemas.md#codersdkworkspacebuild) | To perform this operation, you must be authenticated. [Learn more](authentication.md). - -## Restart workspace - -### Code samples - -```shell -# Example request using curl -curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds/restart \ - -H 'Accept: application/json' \ - -H 'Coder-Session-Token: API_KEY' -``` - -`POST /workspaces/{workspace}/builds/restart` - -### Parameters - -| Name | In | Type | Required | Description | -| ----------- | ---- | ------------ | -------- | ------------ | -| `workspace` | path | string(uuid) | true | Workspace ID | - -### Example responses - -> 200 Response - -```json -{ - "build_number": 0, - "created_at": "2019-08-24T14:15:22Z", - "daily_cost": 0, - "deadline": "2019-08-24T14:15:22Z", - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3", - "initiator_name": "string", - "job": { - "canceled_at": "2019-08-24T14:15:22Z", - "completed_at": "2019-08-24T14:15:22Z", - "created_at": "2019-08-24T14:15:22Z", - "error": "string", - "error_code": "MISSING_TEMPLATE_PARAMETER", - "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "started_at": "2019-08-24T14:15:22Z", - "status": "pending", - "tags": { - "property1": "string", - "property2": "string" - }, - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" - }, - "max_deadline": "2019-08-24T14:15:22Z", - "reason": "initiator", - "resources": [ - { - "agents": [ - { - "apps": [ - { - "command": "string", - "display_name": "string", - "external": true, - "health": "disabled", - "healthcheck": { - "interval": 0, - "threshold": 0, - "url": "string" - }, - "icon": "string", - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "sharing_level": "owner", - "slug": "string", - "subdomain": true, - "url": "string" - } - ], - "architecture": "string", - "connection_timeout_seconds": 0, - "created_at": "2019-08-24T14:15:22Z", - "directory": "string", - "disconnected_at": "2019-08-24T14:15:22Z", - "environment_variables": { - "property1": "string", - "property2": "string" - }, - "expanded_directory": "string", - "first_connected_at": "2019-08-24T14:15:22Z", - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "instance_id": "string", - "last_connected_at": "2019-08-24T14:15:22Z", - "latency": { - "property1": { - "latency_ms": 0, - "preferred": true - }, - "property2": { - "latency_ms": 0, - "preferred": true - } - }, - "lifecycle_state": "created", - "login_before_ready": true, - "name": "string", - "operating_system": "string", - "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", - "shutdown_script": "string", - "shutdown_script_timeout_seconds": 0, - "startup_logs_length": 0, - "startup_logs_overflowed": true, - "startup_script": "string", - "startup_script_timeout_seconds": 0, - "status": "connecting", - "troubleshooting_url": "string", - "updated_at": "2019-08-24T14:15:22Z", - "version": "string" - } - ], - "created_at": "2019-08-24T14:15:22Z", - "daily_cost": 0, - "hide": true, - "icon": "string", - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "job_id": "453bd7d7-5355-4d6d-a38e-d9e7eb218c3f", - "metadata": [ - { - "key": "string", - "sensitive": true, - "value": "string" - } - ], - "name": "string", - "type": "string", - "workspace_transition": "start" - } - ], - "status": "pending", - "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", - "template_version_name": "string", - "transition": "start", - "updated_at": "2019-08-24T14:15:22Z", - "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", - "workspace_name": "string", - "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", - "workspace_owner_name": "string" -} -``` - -### Responses - -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------ | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.WorkspaceBuild](schemas.md#codersdkworkspacebuild) | - -To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 1a6fed6b99e5a..7d4b54e5841b1 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -1538,13 +1538,14 @@ CreateParameterRequest is a structure used to create a new parameter value for a #### Enumerated Values -| Property | Value | -| ------------ | -------- | -| `log_level` | `debug` | -| `transition` | `create` | -| `transition` | `start` | -| `transition` | `stop` | -| `transition` | `delete` | +| Property | Value | +| ------------ | --------- | +| `log_level` | `debug` | +| `transition` | `create` | +| `transition` | `start` | +| `transition` | `stop` | +| `transition` | `delete` | +| `transition` | `restart` | ## codersdk.CreateWorkspaceProxyRequest @@ -5294,18 +5295,20 @@ Parameter represents a set value for the scope. #### Enumerated Values -| Value | -| ----------- | -| `pending` | -| `starting` | -| `running` | -| `stopping` | -| `stopped` | -| `failed` | -| `canceling` | -| `canceled` | -| `deleting` | -| `deleted` | +| Value | +| ------------ | +| `pending` | +| `starting` | +| `running` | +| `stopping` | +| `stopped` | +| `failed` | +| `canceling` | +| `canceled` | +| `deleting` | +| `deleted` | +| `restarting` | +| `restarted` | ## codersdk.WorkspaceTransition @@ -5317,11 +5320,12 @@ Parameter represents a set value for the scope. #### Enumerated Values -| Value | -| -------- | -| `start` | -| `stop` | -| `delete` | +| Value | +| --------- | +| `start` | +| `stop` | +| `delete` | +| `restart` | ## codersdk.WorkspacesResponse diff --git a/site/src/api/api.ts b/site/src/api/api.ts index a0302164ef9c6..50be80f0a91cf 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -513,15 +513,14 @@ export const deleteWorkspace = ( transition: "delete", log_level: logLevel, }) - -export const restartWorkspace = async ( +export const restartWorkspace = ( workspaceId: string, -): Promise => { - const response = await axios.post( - `/api/v2/workspaces/${workspaceId}/builds/restart`, - ) - return response.data -} + logLevel?: TypesGen.CreateWorkspaceBuildRequest["log_level"], +) => + postWorkspaceBuild(workspaceId, { + transition: "restart", + log_level: logLevel, + }) export const cancelWorkspaceBuild = async ( workspaceBuildId: TypesGen.WorkspaceBuild["id"], diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index e9eb7ce0d0771..640a3fda1259b 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1532,6 +1532,8 @@ export type WorkspaceStatus = | "deleting" | "failed" | "pending" + | "restarted" + | "restarting" | "running" | "starting" | "stopped" @@ -1543,6 +1545,8 @@ export const WorkspaceStatuses: WorkspaceStatus[] = [ "deleting", "failed", "pending", + "restarted", + "restarting", "running", "starting", "stopped", @@ -1550,9 +1554,10 @@ export const WorkspaceStatuses: WorkspaceStatus[] = [ ] // From codersdk/workspacebuilds.go -export type WorkspaceTransition = "delete" | "start" | "stop" +export type WorkspaceTransition = "delete" | "restart" | "start" | "stop" export const WorkspaceTransitions: WorkspaceTransition[] = [ "delete", + "restart", "start", "stop", ] diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index 3a796459e83ea..10690d9746c36 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -174,6 +174,9 @@ export const workspaceMachine = createMachine( stopWorkspace: { data: TypesGen.WorkspaceBuild } + restartWorkspace: { + data: TypesGen.WorkspaceBuild + } deleteWorkspace: { data: TypesGen.WorkspaceBuild } @@ -435,7 +438,7 @@ export const workspaceMachine = createMachine( id: "restartWorkspace", onDone: [ { - actions: ["assignBuild"], + actions: ["assignBuild", "disableDebugMode"], target: "idle", }, ], From b3b4a4743e149a28dd614ebdef1711bbc98534a5 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Thu, 13 Apr 2023 15:19:37 +0000 Subject: [PATCH 7/7] add err blocks; fix build number --- coderd/workspacebuilds.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 66854faa310e3..9eede773997ee 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -604,6 +604,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { if err != nil { return xerrors.Errorf("marshal provision job: %w", err) } + provisionerJob, err = db.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{ ID: uuid.New(), CreatedAt: database.Now(), @@ -634,6 +635,9 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { JobID: provisionerJob.ID, Reason: database.BuildReasonInitiator, }) + if err != nil { + return xerrors.Errorf("insert workspace build: %w", err) + } startBuildID = uuid.New() input, err = json.Marshal(provisionerdserver.WorkspaceProvisionJob{ @@ -643,6 +647,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { if err != nil { return xerrors.Errorf("marshal provision job: %w", err) } + provisionerJob, err = db.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{ ID: uuid.New(), CreatedAt: database.Now(), @@ -666,14 +671,16 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { UpdatedAt: database.Now(), WorkspaceID: workspace.ID, TemplateVersionID: templateVersion.ID, - BuildNumber: priorBuildNum + 1, + BuildNumber: priorBuildNum + 2, ProvisionerState: state, InitiatorID: apiKey.UserID, Transition: database.WorkspaceTransitionStart, JobID: provisionerJob.ID, Reason: database.BuildReasonInitiator, }) - + if err != nil { + return xerrors.Errorf("insert workspace build: %w", err) + } } else { workspaceBuildID = uuid.New() input, err := json.Marshal(provisionerdserver.WorkspaceProvisionJob{ @@ -683,6 +690,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { if err != nil { return xerrors.Errorf("marshal provision job: %w", err) } + provisionerJob, err = db.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{ ID: uuid.New(), CreatedAt: database.Now(), @@ -713,10 +721,9 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { JobID: provisionerJob.ID, Reason: database.BuildReasonInitiator, }) - } - - if err != nil { - return xerrors.Errorf("insert workspace build: %w", err) + if err != nil { + return xerrors.Errorf("insert workspace build: %w", err) + } } names := make([]string, 0, len(parameters))