diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 6a04db73d1..177c4230c9 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -4309,6 +4309,43 @@ const docTemplate = `{ } } }, + "/workspacebuilds/{workspacebuild}/parameters": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Builds" + ], + "summary": "Get build parameters for workspace build", + "operationId": "get-build-parameters-for-workspace-build", + "parameters": [ + { + "type": "string", + "description": "Workspace build ID", + "name": "workspacebuild", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.WorkspaceBuildParameter" + } + } + } + } + } + }, "/workspacebuilds/{workspacebuild}/resources": { "get": { "security": [ @@ -5568,6 +5605,12 @@ const docTemplate = `{ "$ref": "#/definitions/codersdk.CreateParameterRequest" } }, + "rich_parameter_values": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.WorkspaceBuildParameter" + } + }, "state": { "type": "array", "items": { @@ -7896,6 +7939,17 @@ const docTemplate = `{ } } }, + "codersdk.WorkspaceBuildParameter": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, "codersdk.WorkspaceQuota": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 5887fb2a7c..b9a34ede0b 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -3793,6 +3793,39 @@ } } }, + "/workspacebuilds/{workspacebuild}/parameters": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Builds"], + "summary": "Get build parameters for workspace build", + "operationId": "get-build-parameters-for-workspace-build", + "parameters": [ + { + "type": "string", + "description": "Workspace build ID", + "name": "workspacebuild", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.WorkspaceBuildParameter" + } + } + } + } + } + }, "/workspacebuilds/{workspacebuild}/resources": { "get": { "security": [ @@ -4930,6 +4963,12 @@ "$ref": "#/definitions/codersdk.CreateParameterRequest" } }, + "rich_parameter_values": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.WorkspaceBuildParameter" + } + }, "state": { "type": "array", "items": { @@ -7108,6 +7147,17 @@ } } }, + "codersdk.WorkspaceBuildParameter": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, "codersdk.WorkspaceQuota": { "type": "object", "properties": { diff --git a/coderd/coderd.go b/coderd/coderd.go index 8fbf1722a1..a06b1aedce 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -582,6 +582,7 @@ func New(options *Options) *API { r.Get("/", api.workspaceBuild) r.Patch("/cancel", api.patchCancelWorkspaceBuild) r.Get("/logs", api.workspaceBuildLogs) + r.Get("/parameters", api.workspaceBuildParameters) r.Get("/resources", api.workspaceBuildResources) r.Get("/state", api.workspaceBuildState) }) diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index 61f09994fe..c2db0b9998 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -1372,7 +1372,7 @@ func (q *fakeQuerier) GetWorkspaceBuildParameters(_ context.Context, workspaceBu defer q.mutex.RUnlock() params := make([]database.WorkspaceBuildParameter, 0) - for _, param := range params { + for _, param := range q.workspaceBuildParameters { if param.WorkspaceBuildID != workspaceBuildID { continue } diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 5e7a9badc0..f62b82a07b 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -377,6 +377,11 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { state = createBuild.ProvisionerState } + var parameters []codersdk.WorkspaceBuildParameter + if createBuild.RichParameterValues != nil { + parameters = createBuild.RichParameterValues + } + if createBuild.Orphan { if createBuild.Transition != codersdk.WorkspaceTransitionDelete { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ @@ -449,6 +454,24 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { state = priorHistory.ProvisionerState } + if parameters == nil { + buildParameters, 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 + } + parameters = make([]codersdk.WorkspaceBuildParameter, 0, len(buildParameters)) + for _, param := range buildParameters { + parameters = append(parameters, codersdk.WorkspaceBuildParameter{ + Name: param.Name, + Value: param.Value, + }) + } + } + var workspaceBuild database.WorkspaceBuild var provisionerJob database.ProvisionerJob // This must happen in a transaction to ensure history can be inserted, and @@ -532,6 +555,21 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { 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 parameter: %w", err) + } + return nil }, nil) if err != nil { @@ -716,6 +754,42 @@ func (api *API) workspaceBuildResources(rw http.ResponseWriter, r *http.Request) api.provisionerJobResources(rw, r, job) } +// @Summary Get build parameters for workspace build +// @ID get-build-parameters-for-workspace-build +// @Security CoderSessionToken +// @Produce json +// @Tags Builds +// @Param workspacebuild path string true "Workspace build ID" +// @Success 200 {array} codersdk.WorkspaceBuildParameter +// @Router /workspacebuilds/{workspacebuild}/parameters [get] +func (api *API) workspaceBuildParameters(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + workspaceBuild := httpmw.WorkspaceBuildParam(r) + workspace, err := api.Database.GetWorkspaceByID(ctx, workspaceBuild.WorkspaceID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "No workspace exists for this job.", + }) + return + } + + if !api.Authorize(r, rbac.ActionRead, workspace) { + httpapi.ResourceNotFound(rw) + return + } + + parameters, err := api.Database.GetWorkspaceBuildParameters(ctx, workspaceBuild.ID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace build parameters.", + Detail: err.Error(), + }) + return + } + apiParameters := convertWorkspaceBuildParameters(parameters) + httpapi.Write(ctx, rw, http.StatusOK, apiParameters) +} + // @Summary Get workspace build logs // @ID get-workspace-build-logs // @Security CoderSessionToken @@ -1084,3 +1158,16 @@ func convertWorkspaceStatus(jobStatus codersdk.ProvisionerJobStatus, transition // return error status since we should never get here return codersdk.WorkspaceStatusFailed } + +func convertWorkspaceBuildParameters(parameters []database.WorkspaceBuildParameter) []codersdk.WorkspaceBuildParameter { + var apiParameters = make([]codersdk.WorkspaceBuildParameter, 0, len(parameters)) + + for _, p := range parameters { + apiParameter := codersdk.WorkspaceBuildParameter{ + Name: p.Name, + Value: p.Value, + } + apiParameters = append(apiParameters, apiParameter) + } + return apiParameters +} diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index a162ac801d..b3b446e55f 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -636,52 +636,106 @@ func TestWorkspaceBuildStatus(t *testing.T) { func TestWorkspaceBuildWithRichParameters(t *testing.T) { t.Parallel() - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) - user := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + const ( + firstParameterName = "first_parameter" + firstParameterDescription = "This is first parameter" + firstParameterValue = "1" + + secondParameterName = "second_parameter" + secondParameterDescription = "This is second parameter" + secondParameterValue = "2" + ) + + initialBuildParameters := []codersdk.WorkspaceBuildParameter{ + {Name: firstParameterName, Value: firstParameterValue}, + {Name: secondParameterName, Value: secondParameterValue}, + } + + echoResponses := &echo.Responses{ Parse: echo.ParseComplete, ProvisionPlan: []*proto.Provision_Response{ { Type: &proto.Provision_Response_Complete{ Complete: &proto.Provision_Complete{ Parameters: []*proto.RichParameter{ - { - Name: "first_parameter", - Description: "This is first parameter", - }, - { - Name: "second_parameter", - Description: "This is second parameter", - }, + {Name: firstParameterName, Description: firstParameterDescription}, + {Name: secondParameterName, Description: secondParameterDescription}, }, }, }, }}, - }) - coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - - richParameters, err := client.TemplateVersionRichParameters(ctx, version.ID) - require.NoError(t, err) - require.Len(t, richParameters, 2) - require.Equal(t, richParameters[0].Name, "first_parameter") - require.Equal(t, richParameters[1].Name, "second_parameter") - - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) { - cwr.RichParameterValues = []codersdk.WorkspaceBuildParameter{ - { - Name: "first_parameter", - Value: "1", - }, - { - Name: "second_parameter", - Value: "2", + ProvisionApply: []*proto.Provision_Response{{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{}, }, + }}, + } + + t.Run("UpdateParameterValues", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) { + cwr.RichParameterValues = initialBuildParameters + }) + + workspaceBuild := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + require.Equal(t, codersdk.WorkspaceStatusRunning, workspaceBuild.Status) + + // Update build parameters + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + const updatedParameterValue = "3" + nextBuildParameters := []codersdk.WorkspaceBuildParameter{ + {Name: firstParameterName, Value: firstParameterValue}, + {Name: secondParameterName, Value: updatedParameterValue}, } + nextWorkspaceBuild, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionStart, + RichParameterValues: nextBuildParameters, + }) + require.NoError(t, err) + coderdtest.AwaitWorkspaceBuildJob(t, client, nextWorkspaceBuild.ID) + + workspaceBuildParameters, err := client.WorkspaceBuildParameters(ctx, nextWorkspaceBuild.ID) + require.NoError(t, err) + require.ElementsMatch(t, nextBuildParameters, workspaceBuildParameters) + }) + t.Run("UsePreviousParameterValues", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) { + cwr.RichParameterValues = initialBuildParameters + }) + + firstWorkspaceBuild := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + require.Equal(t, codersdk.WorkspaceStatusRunning, firstWorkspaceBuild.Status) + + // Start new workspace build + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + nextWorkspaceBuild, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionStart, + }) + require.NoError(t, err) + require.NotEqual(t, firstWorkspaceBuild, nextWorkspaceBuild) + coderdtest.AwaitWorkspaceBuildJob(t, client, nextWorkspaceBuild.ID) + + workspaceBuildParameters, err := client.WorkspaceBuildParameters(ctx, nextWorkspaceBuild.ID) + require.NoError(t, err) + require.ElementsMatch(t, initialBuildParameters, workspaceBuildParameters) }) - _, err = client.WorkspaceBuild(ctx, workspace.LatestBuild.ID) - require.NoError(t, err) } diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 6144612c0f..75ccfd2cba 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -502,6 +502,21 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req if err != nil { return xerrors.Errorf("insert workspace build: %w", err) } + + names := make([]string, 0, len(createWorkspace.RichParameterValues)) + values := make([]string, 0, len(createWorkspace.RichParameterValues)) + for _, param := range createWorkspace.RichParameterValues { + 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 { diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 505a8fe4c0..58531d31d5 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -1781,3 +1781,66 @@ func TestWorkspaceResource(t *testing.T) { }}, metadata) }) } + +func TestWorkspaceWithRichParameters(t *testing.T) { + t.Parallel() + + const ( + firstParameterName = "first_parameter" + firstParameterDescription = "This is first parameter" + firstParameterValue = "1" + + secondParameterName = "second_parameter" + secondParameterDescription = "This is second parameter" + secondParameterValue = "2" + ) + + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: []*proto.Provision_Response{ + { + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Parameters: []*proto.RichParameter{ + {Name: firstParameterName, Description: firstParameterDescription}, + {Name: secondParameterName, Description: secondParameterDescription}, + }, + }, + }, + }}, + ProvisionApply: []*proto.Provision_Response{{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{}, + }, + }}, + }) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + templateRichParameters, err := client.TemplateVersionRichParameters(ctx, version.ID) + require.NoError(t, err) + require.Len(t, templateRichParameters, 2) + require.Equal(t, templateRichParameters[0].Name, firstParameterName) + require.Equal(t, templateRichParameters[1].Name, secondParameterName) + + expectedBuildParameters := []codersdk.WorkspaceBuildParameter{ + {Name: firstParameterName, Value: firstParameterValue}, + {Name: secondParameterName, Value: secondParameterValue}, + } + + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) { + cwr.RichParameterValues = expectedBuildParameters + }) + + workspaceBuild := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + require.Equal(t, codersdk.WorkspaceStatusRunning, workspaceBuild.Status) + + workspaceBuildParameters, err := client.WorkspaceBuildParameters(ctx, workspaceBuild.ID) + require.NoError(t, err) + require.ElementsMatch(t, expectedBuildParameters, workspaceBuildParameters) +} diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index 7a77ae7045..ffa875a2cc 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -164,3 +164,16 @@ func (c *Client) WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber(ctx cont var workspaceBuild WorkspaceBuild return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild) } + +func (c *Client) WorkspaceBuildParameters(ctx context.Context, build uuid.UUID) ([]WorkspaceBuildParameter, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspacebuilds/%s/parameters", build), nil) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, readBodyAsError(res) + } + var params []WorkspaceBuildParameter + return params, json.NewDecoder(res.Body).Decode(¶ms) +} diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 3c1f1a8bda..45e73aaa55 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -56,7 +56,8 @@ type CreateWorkspaceBuildRequest struct { // ParameterValues are optional. It will write params to the 'workspace' scope. // This will overwrite any existing parameters with the same name. // This will not delete old params not included in this list. - ParameterValues []CreateParameterRequest `json:"parameter_values,omitempty"` + ParameterValues []CreateParameterRequest `json:"parameter_values,omitempty"` + RichParameterValues []WorkspaceBuildParameter `json:"rich_parameter_values,omitempty"` } type WorkspaceOptions struct { diff --git a/docs/api/builds.md b/docs/api/builds.md index 77e7f666b8..38782af2fe 100644 --- a/docs/api/builds.md +++ b/docs/api/builds.md @@ -405,6 +405,56 @@ Status Code **200** To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Get build parameters for workspace build + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/parameters \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /workspacebuilds/{workspacebuild}/parameters` + +### Parameters + +| Name | In | Type | Required | Description | +| ---------------- | ---- | ------ | -------- | ------------------ | +| `workspacebuild` | path | string | true | Workspace build ID | + +### Example responses + +> 200 Response + +```json +[ + { + "name": "string", + "value": "string" + } +] +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | --------------------------------------------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.WorkspaceBuildParameter](schemas.md#codersdkworkspacebuildparameter) | + +