feat: Expose workspace build parameters via API (#5743)

This commit is contained in:
Marcin Tojek
2023-01-17 16:24:45 +01:00
committed by GitHub
parent 985fac642e
commit 1b0560ceb4
13 changed files with 463 additions and 45 deletions

54
coderd/apidoc/docs.go generated
View File

@ -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": {

View File

@ -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": {

View File

@ -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)
})

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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)
}