8000 feat: run a terraform plan before creating workspaces with the given template parameters by deansheather · Pull Request #1732 · coder/coder · GitHub
[go: up one dir, main page]

Skip to content

feat: run a terraform plan before creating workspaces with the given template parameters #1732

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jun 1, 2022
Next Next commit
feat: add new provisioner job type template_version_plan
  • Loading branch information
deansheather committed May 25, 2022
commit 3e9ecc33c579a74502c49bfae2cd197036e4d6c2
26 changes: 26 additions & 0 deletions cli/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,32 @@ func create() *cobra.Command {
return err
}

// Run a plan with the given parameters to check correctness
planJob, err := client.TemplateVersionPlan(cmd.Context(), templateVersion.ID, codersdk.TemplateVersionPlanRequest{
ParameterValues: parameters,
})
if err != nil {
return xerrors.Errorf("plan workspace: %w", err)
}
err = cliui.ProvisionerJob(cmd.Context(), cmd.OutOrStdout(), cliui.ProvisionerJobOptions{
Fetch: func() (codersdk.ProvisionerJob, error) {
return planJob, nil
},
Cancel: func() error {
// TODO: workspace plan cancellation endpoint
return nil
},
Logs: func() (<-chan codersdk.ProvisionerJobLog, error) {
// TODO: workspace plan log endpoint
return make(chan codersdk.ProvisionerJobLog), nil
},
})
if err != nil {
// TODO: reprompt for parameter values if we deem it to be a
// validation error
return xerrors.Errorf("error occurred during workspace plan: %w", err)
}

_, err = cliui.Prompt(cmd, cliui.PromptOptions{
Text: "Confirm create?",
IsConfirm: true,
Expand Down
1 change: 1 addition & 0 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ func newRouter(options *Options, a *api) chi.Router {
r.Get("/parameters", a.templateVersionParameters)
r.Get("/resources", a.templateVersionResources)
r.Get("/logs", a.templateVersionLogs)
r.Post("/plan", a.templateVersionPlan)
})
r.Route("/users", func(r chi.Router) {
r.Get("/first", a.firstUser)
Expand Down
3 changes: 2 additions & 1 deletion coderd/database/dump.sql

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- It's not possible to drop enum values from enum types, so the UP has "IF NOT
-- EXISTS".

-- Delete all jobs that use the new enum value.
DELETE FROM
provisioner_jobs
WHERE
provisioner_job_type = 'template_version_plan'
;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TYPE provisioner_job_type
ADD VALUE IF NOT EXISTS 'template_version_plan';
1 change: 1 addition & 0 deletions coderd/database/models.go

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

2 changes: 1 addition & 1 deletion coderd/database/queries.sql.go

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

19 changes: 14 additions & 5 deletions coderd/parameter/compute.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ import (

// ComputeScope targets identifiers to pull parameters from.
type ComputeScope struct {
TemplateImportJobID uuid.UUID
OrganizationID uuid.UUID
UserID uuid.UUID
TemplateID uuid.NullUUID
WorkspaceID uuid.NullUUID
TemplateImportJobID uuid.UUID
OrganizationID uuid.UUID
UserID uuid.UUID
TemplateID uuid.NullUUID
WorkspaceID uuid.NullUUID
AdditionalParameterValues []database.ParameterValue
}

type ComputeOptions struct {
Expand Down Expand Up @@ -142,6 +143,14 @@ func Compute(ctx context.Context, db database.Store, scope ComputeScope, options
}
}

// Finally, any additional parameter values declared in the input
for _, v := range scope.AdditionalParameterValues {
err = compute.injectSingle(v, false)
if err != nil {
return nil, xerrors.Errorf("inject single parameter value: %w", err)
}
}

values := make([]ComputedValue, 0, len(compute.computedParameterByName))
for _, value := range compute.computedParameterByName {
values = append(values, value)
Expand Down
73 changes: 64 additions & 9 deletions coderd/provisionerdaemons.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ type workspaceProvisionJob struct {
DryRun bool `json:"dry_run"`
}

// The input for a "template_version_plan" job.
type templateVersionPlanJob struct {
TemplateVersionID uuid.UUID `json:"template_version_id"`
ParameterValues []database.ParameterValue `json:"parameter_values"`
}

// Implementation of the provisioner daemon protobuf server.
type provisionerdServer struct {
AccessURL *url.URL
Expand Down Expand Up @@ -216,18 +222,15 @@ func (server *provisionerdServer) AcquireJob(ctx context.Context, _ *proto.Empty
if err != nil {
return nil, failJob(fmt.Sprintf("compute parameters: %s", err))
}
// Convert parameters to the protobuf type.
protoParameters := make([]*sdkproto.ParameterValue, 0, len(parameters))
for _, computedParameter := range parameters {
converted, err := convertComputedParameterValue(computedParameter)
if err != nil {
return nil, failJob(fmt.Sprintf("convert parameter: %s", err))
}
protoParameters = append(protoParameters, converted)

// Convert types to their corresponding protobuf types.
protoParameters, err := convertComputedParameterValues(parameters)
if err != nil {
return nil, failJob(fmt.Sprintf("convert computed parameters to protobuf: %s", err))
}
transition, err := convertWorkspaceTransition(workspaceBuild.Transition)
if err != nil {
return nil, failJob(fmt.Sprint("convert workspace transition: %w", err))
return nil, failJob(fmt.Sprintf("convert workspace transition: %s", err))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No wrapping? I guess this gets protobuffed in the end, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't wrap with Sprintf, and these just get condensed to strings sadly :(

}

protoJob.Type = &proto.AcquiredJob_WorkspaceBuild_{
Expand All @@ -246,6 +249,45 @@ func (server *provisionerdServer) AcquireJob(ctx context.Context, _ *proto.Empty
},
},
}
case database.ProvisionerJobTypeTemplateVersionPlan:
var input templateVersionPlanJob
err = json.Unmarshal(job.Input, &input)
if err != nil {
return nil, failJob(fmt.Sprintf("unmarshal job input %q: %s", job.Input, err))
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It scares me to add another job with such similar scope to the others, but I understand why.

It'd be helpful if you explained why we couldn't use the workspace_build job to do similarly. I believe it'd just require not being attached to a literal workspace build, which seems like a reasonable change anyways.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to have a separate job for this because the job endpoints have different schemas and different endpoints to access the job details, not a unified /jobs endpoint. You've said in the past that you'd like to split these different job types apart more in the future (so they don't share DB tables for metadata etc.), so merging these two jobs together would be counterintuitive to that goal

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inputs are slightly different, but the outputs are the same, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Effectively, yes

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably worth opening a ticket considering merging those job types, or at least stating what the impl would look like to merge them. I'm always nervous about adding more job types.


templateVersion, err := server.Database.GetTemplateVersionByID(ctx, input.TemplateVersionID)
if err != nil {
return nil, failJob(fmt.Sprintf("get template version: %s", err))
}

// Compute parameters for the plan to consume.
parameters, err := parameter.Compute(ctx, server.Database, parameter.ComputeScope{
TemplateImportJobID: templateVersion.JobID,
OrganizationID: job.OrganizationID,
TemplateID: templateVersion.TemplateID,
UserID: user.ID,
WorkspaceID: uuid.NullUUID{},
AdditionalParameterValues: input.ParameterValues,
}, nil)
if err != nil {
return nil, failJob(fmt.Sprintf("compute parameters: %s", err))
}

// Convert types to their corresponding protobuf types.
protoParameters, err := convertComputedParameterValues(parameters)
if err != nil {
return nil, failJob(fmt.Sprintf("convert computed parameters to protobuf: %s", err))
}

protoJob.Type = &proto.AcquiredJob_TemplatePlan_{
TemplatePlan: &proto.AcquiredJob_TemplatePlan{
ParameterValues: protoParameters,
Metadata: &sdkproto.Provision_Metadata{
CoderUrl: server.AccessURL.String(),
},
},
}
case database.ProvisionerJobTypeTemplateVersionImport:
protoJob.Type = &proto.AcquiredJob_TemplateImport_{
TemplateImport: &proto.AcquiredJob_TemplateImport{
Expand Down Expand Up @@ -716,6 +758,19 @@ func convertLogSource(logSource proto.LogSource) (database.LogSource, error) {
}
}

func convertComputedParameterValues(parameters []parameter.ComputedValue) ([]*sdkproto.ParameterValue, error) {
protoParameters := make([]*sdkproto.ParameterValue, len(parameters))
for i, computedParameter := range parameters {
converted, err := convertComputedParameterValue(computedParameter)
if err != nil {
return nil, xerrors.Errorf("convert parameter: %w", err)
}
protoParameters[i] = converted
}

return protoParameters, nil
}

func convertComputedParameterValue(param parameter.ComputedValue) (*sdkproto.ParameterValue, error) {
var scheme sdkproto.ParameterDestination_Scheme
switch param.DestinationScheme {
Expand Down
75 changes: 75 additions & 0 deletions coderd/templateversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package coderd

import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
Expand Down Expand Up @@ -146,6 +147,80 @@ func (api *api) templateVersionParameters(rw http.ResponseWriter, r *http.Reques
httpapi.Write(rw, http.StatusOK, values)
}

func (api *api) templateVersionPlan(rw http.ResponseWriter, r *http.Request) {
apiKey := httpmw.APIKey(r)
organization := httpmw.OrganizationParam(r)
templateVersion := httpmw.TemplateVersionParam(r)

var req codersdk.TemplateVersionPlanRequest
if !httpapi.Read(rw, r, &req) {
return
}

job, err := api.Database.GetProvisionerJobByID(r.Context(), templateVersion.JobID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job: %s", err),
})
return
}
if !job.CompletedAt.Valid {
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
Message: "Template version import job hasn't completed!",
})
return
}

// Convert parameters from request to parameters for the job
parameterValues := make([]database.ParameterValue, len(req.ParameterValues))
for i, v := range req.ParameterValues {
parameterValues[i] = database.ParameterValue{
ID: uuid.Nil,
Scope: database.ParameterScopeWorkspace,
ScopeID: uuid.Nil,
Name: v.Name,
SourceScheme: database.ParameterSourceSchemeData,
SourceValue: v.SourceValue,
DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable,
}
}

// Marshal template version plan job with the parameters from the request.
input, err := json.Marshal(templateVersionPlanJob{
TemplateVersionID: templateVersion.ID,
ParameterValues: parameterValues,
})
if err != nil {
< 57AE /td> httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
Message: fmt.Sprintf("marshal new provisioner job: %s", err),
})
return
}

// Create a plan job
jobID := uuid.New()
provisionerJob, err := api.Database.InsertProvisionerJob(r.Context(), database.InsertProvisionerJobParams{
ID: jobID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
OrganizationID: organization.ID,
InitiatorID: apiKey.UserID,
Provisioner: job.Provisioner,
StorageMethod: job.StorageMethod,
StorageSource: job.StorageSource,
Type: database.ProvisionerJobTypeTemplateVersionPlan,
Input: input,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("insert provisioner job: %s", err),
})
return
}

httpapi.Write(rw, http.StatusCreated, convertProvisionerJob(provisionerJob))
}

func (api *api) templateVersionsByTemplate(rw http.ResponseWriter, r *http.Request) {
template := httpmw.TemplateParam(r)

Expand Down
22 changes: 22 additions & 0 deletions codersdk/templateversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,25 @@ func (c *Client) TemplateVersionLogsBefore(ctx context.Context, version uuid.UUI
func (c *Client) TemplateVersionLogsAfter(ctx context.Context, version uuid.UUID, after time.Time) (<-chan ProvisionerJobLog, error) {
return c.provisionerJobLogsAfter(ctx, fmt.Sprintf("/api/v2/templateversions/%s/logs", version), after)
}

// TemplateVersionPlanRequest defines the request parameters for
// TemplateVersionPlan.
type TemplateVersionPlanRequest struct {
ParameterValues []CreateParameterRequest
}

// TemplateVersionPlan begins a dry-run provisioner job against the given
// template version with the given parameter values.
func (c *Client) TemplateVersionPlan(ctx context.Context, version uuid.UUID, req TemplateVersionPlanRequest) (ProvisionerJob, error) {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/templateversions/%s/plan", version), req)
if err != nil {
return ProvisionerJob{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return ProvisionerJob{}, readBodyAsError(res)
}

var job ProvisionerJob
return job, json.NewDecoder(res.Body).Decode(&job)
}
Loading
0