8000 feat: run a terraform plan before creating workspaces with the given … · coder/coder@b41750d · GitHub
[go: up one dir, main page]

Skip to content

Commit b41750d

Browse files
deansheatherkylecarbs
authored andcommitted
feat: run a terraform plan before creating workspaces with the given template parameters (#1732)
1 parent 0c7bc32 commit b41750d

File tree

22 files changed

+1422
-218
lines changed
  • codersdk
  • provisionerd
  • provisioner/echo
  • site/src/api
  • 22 files changed

    +1422
    -218
    lines changed

    cli/cliui/provisionerjob.go

    Lines changed: 29 additions & 2 deletions
    Original file line numberDiff line numberDiff line change
    @@ -1,6 +1,7 @@
    11
    package cliui
    22

    33
    import (
    4+
    "bytes"
    45
    "context"
    56
    "fmt"
    67
    "io"
    @@ -35,6 +36,9 @@ type ProvisionerJobOptions struct {
    3536
    FetchInterval time.Duration
    3637
    // Verbose determines whether debug and trace logs will be shown.
    3738
    Verbose bool
    39+
    // Silent determines whether log output will be shown unless there is an
    40+
    // error.
    41+
    Silent bool
    3842
    }
    3943

    4044
    // ProvisionerJob renders a provisioner job with interactive cancellation.
    @@ -133,12 +137,30 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
    133137
    return xerrors.Errorf("logs: %w", err)
    134138
    }
    135139

    140+
    var (
    141+
    // logOutput is where log output is written
    142+
    logOutput = writer
    143+
    // logBuffer is where logs are buffered if opts.Silent is true
    144+
    logBuffer = &bytes.Buffer{}
    145+
    )
    146+
    if opts.Silent {
    147+
    logOutput = logBuffer
    148+
    }
    149+
    flushLogBuffer := func() {
    150+
    if opts.Silent {
    151+
    _, _ = io.Copy(writer, logBuffer)
    152+
    }
    153+
    }
    154+
    136155
    ticker := time.NewTicker(opts.FetchInterval)
    156+
    defer ticker.Stop()
    137157
    for {
    138158
    select {
    139159
    case err = <-errChan:
    160+
    flushLogBuffer()
    140161
    return err
    141162
    case <-ctx.Done():
    163+
    flushLogBuffer()
    142164
    return ctx.Err()
    143165
    case <-ticker.C:
    144166
    updateJob()
    @@ -160,8 +182,10 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
    160182
    }
    161183
    err = xerrors.New(job.Error)
    162184
    jobMutex.Unlock()
    185+
    flushLogBuffer()
    163186
    return err
    164187
    }
    188+
    165189
    output := ""
    166190
    switch log.Level {
    167191
    case codersdk.LogLevelTrace, codersdk.LogLevelDebug:
    @@ -176,14 +200,17 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
    176200
    case codersdk.LogLevelInfo:
    177201
    output = log.Output
    178202
    }
    203+
    179204
    jobMutex.Lock()
    180205
    if log.Stage != currentStage && log.Stage != "" {
    181206
    updateStage(log.Stage, log.CreatedAt)
    182207
    jobMutex.Unlock()
    183208
    continue
    184209
    }
    185-
    _, _ = fmt.Fprintf(writer, "%s %s\n", Styles.Placeholder.Render(" "), output)
    186-
    didLogBetweenStage = true
    210+
    _, _ = fmt.Fprintf(logOutput, "%s %s\n", Styles.Placeholder.Render(" "), output)
    211+
    if !opts.Silent {
    212+
    didLogBetweenStage = true
    213+
    }
    187214
    jobMutex.Unlock()
    188215
    }
    189216
    }

    cli/create.go

    Lines changed: 33 additions & 4 deletions
    Original file line numberDiff line numberDiff line change
    @@ -170,10 +170,40 @@ func create() *cobra.Command {
    170170
    }
    171171
    _, _ = fmt.Fprintln(cmd.OutOrStdout())
    172172

    173-
    resources, err := client.TemplateVersionResources(cmd.Context(), templateVersion.ID)
    173+
    // Run a dry-run with the given parameters to check correctness
    174+
    after := time.Now()
    175+
    dryRun, err := client.CreateTemplateVersionDryRun(cmd.Context(), templateVersion.ID, codersdk.CreateTemplateVersionDryRunRequest{
    176+
    WorkspaceName: workspaceName,
    177+
    ParameterValues: parameters,
    178+
    })
    174179
    if err != nil {
    175-
    return err
    180+
    return xerrors.Errorf("begin workspace dry-run: %w", err)
    181+
    }
    182+
    _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Planning workspace...")
    183+
    err = cliui.ProvisionerJob(cmd.Context(), cmd.OutOrStdout(), cliui.ProvisionerJobOptions{
    184+
    Fetch: func() (codersdk.ProvisionerJob, error) {
    185+
    return client.TemplateVersionDryRun(cmd.Context(), templateVersion.ID, dryRun.ID)
    186+
    },
    187+
    Cancel: func() error {
    188+
    return client.CancelTemplateVersionDryRun(cmd.Context(), templateVersion.ID, dryRun.ID)
    189+
    },
    190+
    Logs: func() (<-chan codersdk.ProvisionerJobLog, error) {
    191+
    return client.TemplateVersionDryRunLogsAfter(cmd.Context(), templateVersion.ID, dryRun.ID, after)
    192+
    },
    193+
    // Don't show log output for the dry-run unless there's an error.
    194+
    Silent: true,
    195+
    })
    196+
    if err != nil {
    197+
    // TODO (Dean): reprompt for parameter values if we deem it to
    198+
    // be a validation error
    199+
    return xerrors.Errorf("dry-run workspace: %w", err)
    176200
    }
    201+
    202+
    resources, err := client.TemplateVersionDryRunResources(cmd.Context(), templateVersion.ID, dryRun.ID)
    203+
    if err != nil {
    204+
    return xerrors.Errorf("get workspace dry-run resources: %w", err)
    205+
    }
    206+
    177207
    err = cliui.WorkspaceResources(cmd.OutOrStdout(), resources, cliui.WorkspaceResourcesOptions{
    178208
    WorkspaceName: workspaceName,
    179209
    // Since agent's haven't connected yet, hiding this makes more sense.
    @@ -192,7 +222,6 @@ func create() *cobra.Command {
    192222
    return err
    193223
    }
    194224

    195-
    before := time.Now()
    196225
    workspace, err := client.CreateWorkspace(cmd.Context(), organization.ID, codersdk.CreateWorkspaceRequest{
    197226
    TemplateID: template.ID,
    198227
    Name: workspaceName,
    @@ -204,7 +233,7 @@ func create() *cobra.Command {
    204233
    return err
    205234
    }
    206235

    207-
    err = cliui.WorkspaceBuild(cmd.Context(), cmd.OutOrStdout(), client, workspace.LatestBuild.ID, before)
    236+
    err = cliui.WorkspaceBuild(cmd.Context(), cmd.OutOrStdout(), client, workspace.LatestBuild.ID, after)
    208237
    if err != nil {
    209238
    return err
    210239
    }

    cli/create_test.go

    Lines changed: 48 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -2,6 +2,7 @@ package cli_test
    22

    33
    import (
    44
    "context"
    5+
    "database/sql"
    56
    "fmt"
    67
    "os"
    78
    "testing"
    @@ -12,6 +13,8 @@ import (
    1213

    1314
    "github.com/coder/coder/cli/clitest"
    1415
    "github.com/coder/coder/coderd/coderdtest"
    16+
    "github.com/coder/coder/coderd/database"
    17+
    "github.com/coder/coder/codersdk"
    1518
    "github.com/coder/coder/provisioner/echo"
    1619
    "github.com/coder/coder/provisionersdk/proto"
    1720
    "github.com/coder/coder/pty/ptytest"
    @@ -249,6 +252,7 @@ func TestCreate(t *testing.T) {
    249252
    <-doneChan
    250253
    removeTmpDirUntilSuccess(t, tempDir)
    251254
    })
    255+
    252256
    t.Run("WithParameterFileNotContainingTheValue", func(t *testing.T) {
    253257
    t.Parallel()
    254258
    client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
    @@ -279,6 +283,50 @@ func TestCreate(t *testing.T) {
    279283
    <-doneChan
    280284
    removeTmpDirUntilSuccess(t, tempDir)
    281285
    })
    286+
    287+
    t.Run("FailedDryRun", func(t *testing.T) {
    288+
    t.Parallel()
    289+
    client, api := coderdtest.NewWithAPI(t, &coderdtest.Options{IncludeProvisionerD: true})
    290+
    user := coderdtest.CreateFirstUser(t, client)
    291+
    version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
    292+
    Parse: echo.ParseComplete,
    293+
    ProvisionDryRun: []*proto.Provision_Response{
    294+
    {
    295+
    Type: &proto.Provision_Response_Complete{
    296+
    Complete: &proto.Provision_Complete{
    297+
    Error: "test error",
    298+
    },
    299+
    },
    300+
    },
    301+
    },
    302+
    })
    303+
    304+
    // The template import job should end up failed, but we need it to be
    305+
    // succeeded so the dry-run can begin.
    306+
    version = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
    307+
    require.Equal(t, codersdk.ProvisionerJobFailed, version.Job.Status, "job is not failed")
    308+
    err := api.Database.UpdateProvisionerJobWithCompleteByID(context.Background(), database.UpdateProvisionerJobWithCompleteByIDParams{
    309+
    ID: version.Job.ID,
    310+
    CompletedAt: sql.NullTime{
    311+
    Time: time.Now(),
    312+
    Valid: true,
    313+
    },
    314+
    UpdatedAt: time.Now(),
    315+
    Error: sql.NullString{},
    316+
    })
    317+
    require.NoError(t, err, "update provisioner job")
    318+
    319+
    _ = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
    320+
    cmd, root := clitest.New(t, "create", "test")
    321+
    clitest.SetupConfig(t, client, root)
    322+
    pty := ptytest.New(t)
    323+
    cmd.SetIn(pty.Input())
    324+
    cmd.SetOut(pty.Output())
    325+
    326+
    err = cmd.Execute()
    327+
    require.Error(t, err)
    328+
    require.ErrorContains(t, err, "dry-run workspace")
    329+
    })
    282330
    }
    283331

    284332
    func createTestParseResponseWithDefault(defaultValue string) []*proto.Parse_Response {

    coderd/coderd.go

    Lines changed: 7 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -207,6 +207,13 @@ func New(options *Options) *API {
    207207
    r.Get("/parameters", api.templateVersionParameters)
    208208
    r.Get("/resources", api.templateVersionResources)
    209209
    r.Get("/logs", api.templateVersionLogs)
    210+
    r.Route("/dry-run", func(r chi.Router) {
    211+
    r.Post("/", api.postTemplateVersionDryRun)
    212+
    r.Get("/{jobID}", api.templateVersionDryRun)
    213+
    r.Get("/{jobID}/resources", api.templateVersionDryRunResources)
    214+
    r.Get("/{jobID}/logs", api.templateVersionDryRunLogs)
    215+
    r.Patch("/{jobID}/cancel", api.patchTemplateVersionDryRunCancel)
    216+
    })
    210217
    })
    211218
    r.Route("/users", func(r chi.Router) {
    212219
    r.Get("/first", api.firstUser)

    coderd/coderd_test.go

    Lines changed: 26 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -96,6 +96,10 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
    9696
    require.NoError(t, err, "upload file")
    9797
    workspaceResources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
    9898
    require.NoError(t, err, "workspace resources")
    99+
    templateVersionDryRun, err := client.CreateTemplateVersionDryRun(ctx, version.ID, codersdk.CreateTemplateVersionDryRunRequest{
    100+
    ParameterValues: []codersdk.CreateParameterRequest{},
    101+
    })
    102+
    require.NoError(t, err, "template version dry-run")
    99103

    100104
    // Always fail auth from this point forward
    101105
    authorizer.AlwaysReturn = rbac.ForbiddenWithInternal(xerrors.New("fake implementation"), nil, nil)
    @@ -262,6 +266,27 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
    262266
    AssertAction: rbac.ActionRead,
    263267
    AssertObject: rbac.ResourceTemplate.InOrg(template.OrganizationID).WithID(template.ID.String()),
    264268
    },
    269+
    "POST:/api/v2/templateversions/{templateversion}/dry-run": {
    270+
    // The first check is to read the template
    271+
    AssertAction: rbac.ActionRead,
    272+
    AssertObject: rbac.ResourceTemplate.InOrg(version.OrganizationID).WithID(template.ID.String()),
    273+
    },
    274+
    "GET:/api/v2/templateversions/{templateversion}/dry-run/{templateversiondryrun}": {
    275+
    AssertAction: rbac.ActionRead,
    276+
    AssertObject: rbac.ResourceTemplate.InOrg(version.OrganizationID).WithID(template.ID.String()),
    277+
    },
    278+
    "GET:/api/v2/templateversions/{templateversion}/dry-run/{templateversiondryrun}/resources": {
    279+
    AssertAction: rbac.ActionRead,
    280+
    AssertObject: rbac.ResourceTemplate.InOrg(version.OrganizationID).WithID(template.ID.String()),
    281+
    },
    282+
    "GET:/api/v2/templateversions/{templateversion}/dry-run/{templateversiondryrun}/logs": {
    283+
    AssertAction: rbac.ActionRead,
    284+
    AssertObject: rbac.ResourceTemplate.InOrg(version.OrganizationID).WithID(template.ID.String()),
    285+
    },
    286+
    "PATCH:/api/v2/templateversions/{templateversion}/dry-run/{templateversiondryrun}/cancel": {
    287+
    AssertAction: rbac.ActionRead,
    288+
    AssertObject: rbac.ResourceTemplate.InOrg(version.OrganizationID).WithID(template.ID.String()),
    289+
    },
    265290
    "GET:/api/v2/provisionerdaemons": {
    266291
    StatusCode: http.StatusOK,
    267292
    AssertObject: rbac.ResourceProvisionerDaemon.WithID(provisionerds[0].ID.String()),
    @@ -350,6 +375,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
    350375
    route = strings.ReplaceAll(route, "{hash}", file.Hash)
    351376
    route = strings.ReplaceAll(route, "{workspaceresource}", workspaceResources[0].ID.String())
    352377
    route = strings.ReplaceAll(route, "{templateversion}", version.ID.String())
    378+
    route = strings.ReplaceAll(route, "{templateversiondryrun}", templateVersionDryRun.ID.String())
    353379
    route = strings.ReplaceAll(route, "{templatename}", template.Name)
    354380
    // Only checking org scoped params here
    355381
    route = strings.ReplaceAll(route, "{scope}", string(organizationParam.Scope))

    coderd/database/dump.sql

    Lines changed: 2 additions & 1 deletion
    Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
    Lines changed: 9 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -0,0 +1,9 @@
    1+
    -- It's not possible to drop enum values from enum types, so the UP has "IF NOT
    2+
    -- EXISTS".
    3+
    4+
    -- Delete all jobs that use the new enum value.
    5+
    DELETE FROM
    6+
    provisioner_jobs
    7+
    WHERE
    8+
    type = 'template_version_dry_run'
    9+
    ;
    Lines changed: 2 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -0,0 +1,2 @@
    1+
    ALTER TYPE provisioner_job_type
    2+
    ADD VALUE IF NOT EXISTS 'template_version_dry_run';

    coderd/database/models.go

    Lines changed: 1 addition & 0 deletions
    Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

    coderd/parameter/compute.go

    Lines changed: 14 additions & 5 deletions
    Original file line numberDiff line numberDiff line change
    @@ -13,11 +13,12 @@ import (
    1313

    1414
    // ComputeScope targets identifiers to pull parameters from.
    1515
    type ComputeScope struct {
    16-
    TemplateImportJobID uuid.UUID
    17-
    OrganizationID uuid.UUID
    18-
    UserID uuid.UUID
    19-
    TemplateID uuid.NullUUID
    20-
    WorkspaceID uuid.NullUUID
    16+
    TemplateImportJobID uuid.UUID
    17+
    OrganizationID uuid.UUID
    18+
    UserID uuid.UUID
    19+
    TemplateID uuid.NullUUID
    20+
    WorkspaceID uuid.NullUUID
    21+
    AdditionalParameterValues []database.ParameterValue
    2122
    }
    2223

    2324
    type ComputeOptions struct {
    @@ -142,6 +143,14 @@ func Compute(ctx context.Context, db database.Store, scope ComputeScope, options
    142143
    }
    143144
    }
    144145

    146+
    // Finally, any additional parameter values declared in the input
    147+
    for _, v := range scope.AdditionalParameterValues {
    148+
    err = compute.injectSingle(v, false)
    149+
    if err != nil {
    150+
    return nil, xerrors.Errorf("inject single parameter value: %w", err)
    151+
    }
    152+
    }
    153+
    145154
    values := make([]ComputedValue, 0, len(compute.computedParameterByName))
    146155
    for _, value := range compute.computedParameterByName {
    147156
    values = append(values, value)

    0 commit comments

    Comments
     (0)
    0