diff --git a/coderd/autobuild/executor/lifecycle_executor.go b/coderd/autobuild/executor/lifecycle_executor.go index 3ed07da8f5..1993742cce 100644 --- a/coderd/autobuild/executor/lifecycle_executor.go +++ b/coderd/autobuild/executor/lifecycle_executor.go @@ -268,37 +268,59 @@ func build(ctx context.Context, store database.Store, workspace database.Workspa return xerrors.Errorf("Unsupported transition: %q", trans) } - newProvisionerJob, err := store.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{ - ID: provisionerJobID, - CreatedAt: now, - UpdatedAt: now, - InitiatorID: workspace.OwnerID, - OrganizationID: template.OrganizationID, - Provisioner: template.Provisioner, - Type: database.ProvisionerJobTypeWorkspaceBuild, - StorageMethod: priorJob.StorageMethod, - FileID: priorJob.FileID, - Tags: priorJob.Tags, - Input: input, - }) + lastBuildParameters, err := store.GetWorkspaceBuildParameters(ctx, priorHistory.ID) if err != nil { - return xerrors.Errorf("insert provisioner job: %w", err) + return xerrors.Errorf("fetch prior workspace build parameters: %w", err) } - _, err = store.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{ - ID: workspaceBuildID, - CreatedAt: now, - UpdatedAt: now, - WorkspaceID: workspace.ID, - TemplateVersionID: priorHistory.TemplateVersionID, - BuildNumber: priorBuildNumber + 1, - ProvisionerState: priorHistory.ProvisionerState, - InitiatorID: workspace.OwnerID, - Transition: trans, - JobID: newProvisionerJob.ID, - Reason: buildReason, - }) - if err != nil { - return xerrors.Errorf("insert workspace build: %w", err) - } - return nil + + return store.InTx(func(db database.Store) error { + newProvisionerJob, err := store.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{ + ID: provisionerJobID, + CreatedAt: now, + UpdatedAt: now, + InitiatorID: workspace.OwnerID, + OrganizationID: template.OrganizationID, + Provisioner: template.Provisioner, + Type: database.ProvisionerJobTypeWorkspaceBuild, + StorageMethod: priorJob.StorageMethod, + FileID: priorJob.FileID, + Tags: priorJob.Tags, + Input: input, + }) + if err != nil { + return xerrors.Errorf("insert provisioner job: %w", err) + } + workspaceBuild, err := store.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{ + ID: workspaceBuildID, + CreatedAt: now, + UpdatedAt: now, + WorkspaceID: workspace.ID, + TemplateVersionID: priorHistory.TemplateVersionID, + BuildNumber: priorBuildNumber + 1, + ProvisionerState: priorHistory.ProvisionerState, + InitiatorID: workspace.OwnerID, + Transition: trans, + JobID: newProvisionerJob.ID, + Reason: buildReason, + }) + if err != nil { + return xerrors.Errorf("insert workspace build: %w", err) + } + + names := make([]string, 0, len(lastBuildParameters)) + values := make([]string, 0, len(lastBuildParameters)) + for _, param := range lastBuildParameters { + names = append(names, param.Name) + values = append(values, param.Value) + } + err = db.InsertWorkspaceBuildParameters(ctx, database.InsertWorkspaceBuildParametersParams{ + WorkspaceBuildID: workspaceBuild.ID, + Name: names, + Value: values, + }) + if err != nil { + return xerrors.Errorf("insert workspace build parameters: %w", err) + } + return nil + }, nil) } diff --git a/coderd/autobuild/executor/lifecycle_executor_test.go b/coderd/autobuild/executor/lifecycle_executor_test.go index 4b44e7972d..4e53246213 100644 --- a/coderd/autobuild/executor/lifecycle_executor_test.go +++ b/coderd/autobuild/executor/lifecycle_executor_test.go @@ -8,12 +8,16 @@ import ( "go.uber.org/goleak" + "github.com/google/uuid" + "github.com/coder/coder/coderd/autobuild/executor" "github.com/coder/coder/coderd/autobuild/schedule" "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/util/ptr" "github.com/coder/coder/codersdk" + "github.com/coder/coder/provisioner/echo" + "github.com/coder/coder/provisionersdk/proto" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -540,6 +544,67 @@ func TestExecutorAutostartMultipleOK(t *testing.T) { assert.Len(t, stats2.Transitions, 0) } +func TestExecutorAutostartWithParameters(t *testing.T) { + t.Parallel() + + const ( + stringParameterName = "string_parameter" + stringParameterValue = "abc" + + numberParameterName = "number_parameter" + numberParameterValue = "7" + ) + + var ( + sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *") + tickCh = make(chan time.Time) + statsCh = make(chan executor.Stats) + client = coderdtest.New(t, &coderdtest.Options{ + AutobuildTicker: tickCh, + IncludeProvisionerDaemon: true, + AutobuildStats: statsCh, + }) + + richParameters = []*proto.RichParameter{ + {Name: stringParameterName, Type: "string", Mutable: true}, + {Name: numberParameterName, Type: "number", Mutable: true}, + } + + // Given: we have a user with a workspace that has autostart enabled + workspace = mustProvisionWorkspaceWithParameters(t, client, richParameters, func(cwr *codersdk.CreateWorkspaceRequest) { + cwr.AutostartSchedule = ptr.Ref(sched.String()) + cwr.RichParameterValues = []codersdk.WorkspaceBuildParameter{ + { + Name: stringParameterName, + Value: stringParameterValue, + }, + { + Name: numberParameterName, + Value: numberParameterValue, + }, + } + }) + ) + // Given: workspace is stopped + workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) + + // When: the autobuild executor ticks after the scheduled time + go func() { + tickCh <- sched.Next(workspace.LatestBuild.CreatedAt) + close(tickCh) + }() + + // Then: the workspace with parameters should eventually be started + stats := <-statsCh + assert.NoError(t, stats.Error) + assert.Len(t, stats.Transitions, 1) + assert.Contains(t, stats.Transitions, workspace.ID) + assert.Equal(t, database.WorkspaceTransitionStart, stats.Transitions[workspace.ID]) + + workspace = coderdtest.MustWorkspace(t, client, workspace.ID) + mustWorkspaceParameters(t, client, workspace.LatestBuild.ID) +} + func mustProvisionWorkspace(t *testing.T, client *codersdk.Client, mut ...func(*codersdk.CreateWorkspaceRequest)) codersdk.Workspace { t.Helper() user := coderdtest.CreateFirstUser(t, client) @@ -551,6 +616,34 @@ func mustProvisionWorkspace(t *testing.T, client *codersdk.Client, mut ...func(* return coderdtest.MustWorkspace(t, client, ws.ID) } +func mustProvisionWorkspaceWithParameters(t *testing.T, client *codersdk.Client, richParameters []*proto.RichParameter, mut ...func(*codersdk.CreateWorkspaceRequest)) codersdk.Workspace { + t.Helper() + 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: richParameters, + }, + }, + }}, + ProvisionApply: []*proto.Provision_Response{ + { + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{}, + }, + }, + }, + }) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, mut...) + coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID) + return coderdtest.MustWorkspace(t, client, ws.ID) +} + func mustSchedule(t *testing.T, s string) *schedule.Schedule { t.Helper() sched, err := schedule.Weekly(s) @@ -558,6 +651,13 @@ func mustSchedule(t *testing.T, s string) *schedule.Schedule { return sched } +func mustWorkspaceParameters(t *testing.T, client *codersdk.Client, workspaceID uuid.UUID) { + ctx := context.Background() + buildParameters, err := client.WorkspaceBuildParameters(ctx, workspaceID) + require.NoError(t, err) + require.NotEmpty(t, buildParameters) +} + func TestMain(m *testing.M) { goleak.VerifyTestMain(m) } diff --git a/codersdk/richparameters.go b/codersdk/richparameters.go index ec3263706d..6ab674b77e 100644 --- a/codersdk/richparameters.go +++ b/codersdk/richparameters.go @@ -8,6 +8,9 @@ import ( func ValidateWorkspaceBuildParameters(richParameters []TemplateVersionParameter, buildParameters []WorkspaceBuildParameter) error { for _, buildParameter := range buildParameters { + if buildParameter.Name == "" { + return xerrors.Errorf(`workspace build parameter name is missing`) + } richParameter, found := findTemplateVersionParameter(richParameters, buildParameter.Name) if !found { return xerrors.Errorf(`workspace build parameter is not defined in the template ("coder_parameter"): %s`, buildParameter.Name)