diff --git a/cli/testdata/coder_list_--output_json.golden b/cli/testdata/coder_list_--output_json.golden index ac9bcc2153668..5f293787de719 100644 --- a/cli/testdata/coder_list_--output_json.golden +++ b/cli/testdata/coder_list_--output_json.golden @@ -67,7 +67,8 @@ "count": 0, "available": 0, "most_recently_seen": null - } + }, + "template_version_preset_id": null }, "latest_app_status": null, "outdated": false, diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 6bb177d699501..83cc203af2f9a 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -11509,6 +11509,11 @@ const docTemplate = `{ "type": "string", "format": "uuid" }, + "template_version_preset_id": { + "description": "TemplateVersionPresetID is the ID of the template version preset to use for the build.", + "type": "string", + "format": "uuid" + }, "transition": { "enum": [ "start", @@ -11573,6 +11578,10 @@ const docTemplate = `{ "type": "string", "format": "uuid" }, + "template_version_preset_id": { + "type": "string", + "format": "uuid" + }, "ttl_ms": { "type": "integer" } @@ -17177,6 +17186,10 @@ const docTemplate = `{ "template_version_name": { "type": "string" }, + "template_version_preset_id": { + "type": "string", + "format": "uuid" + }, "transition": { "enum": [ "start", diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index de1d4e41c0673..3a32db425e06e 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -10257,6 +10257,11 @@ "type": "string", "format": "uuid" }, + "template_version_preset_id": { + "description": "TemplateVersionPresetID is the ID of the template version preset to use for the build.", + "type": "string", + "format": "uuid" + }, "transition": { "enum": ["start", "stop", "delete"], "allOf": [ @@ -10313,6 +10318,10 @@ "type": "string", "format": "uuid" }, + "template_version_preset_id": { + "type": "string", + "format": "uuid" + }, "ttl_ms": { "type": "integer" } @@ -15670,6 +15679,10 @@ "template_version_name": { "type": "string" }, + "template_version_preset_id": { + "type": "string", + "format": "uuid" + }, "transition": { "enum": ["start", "stop", "delete"], "allOf": [ diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index deafdc42e0216..1fa5ec9512c65 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -9806,19 +9806,20 @@ func (q *FakeQuerier) InsertWorkspaceBuild(_ context.Context, arg database.Inser defer q.mutex.Unlock() workspaceBuild := database.WorkspaceBuild{ - ID: arg.ID, - CreatedAt: arg.CreatedAt, - UpdatedAt: arg.UpdatedAt, - WorkspaceID: arg.WorkspaceID, - TemplateVersionID: arg.TemplateVersionID, - BuildNumber: arg.BuildNumber, - Transition: arg.Transition, - InitiatorID: arg.InitiatorID, - JobID: arg.JobID, - ProvisionerState: arg.ProvisionerState, - Deadline: arg.Deadline, - MaxDeadline: arg.MaxDeadline, - Reason: arg.Reason, + ID: arg.ID, + CreatedAt: arg.CreatedAt, + UpdatedAt: arg.UpdatedAt, + WorkspaceID: arg.WorkspaceID, + TemplateVersionID: arg.TemplateVersionID, + BuildNumber: arg.BuildNumber, + Transition: arg.Transition, + InitiatorID: arg.InitiatorID, + JobID: arg.JobID, + ProvisionerState: arg.ProvisionerState, + Deadline: arg.Deadline, + MaxDeadline: arg.MaxDeadline, + Reason: arg.Reason, + TemplateVersionPresetID: arg.TemplateVersionPresetID, } q.workspaceBuilds = append(q.workspaceBuilds, workspaceBuild) return nil diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 6f8c3707f7279..47fecfb4a1688 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -27,6 +27,8 @@ import ( "cdr.dev/slog" + "github.com/coder/quartz" + "github.com/coder/coder/v2/coderd/apikey" "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" @@ -46,7 +48,6 @@ import ( "github.com/coder/coder/v2/provisionerd/proto" "github.com/coder/coder/v2/provisionersdk" sdkproto "github.com/coder/coder/v2/provisionersdk/proto" - "github.com/coder/quartz" ) const ( @@ -635,6 +636,7 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo WorkspaceBuildId: workspaceBuild.ID.String(), WorkspaceOwnerLoginType: string(owner.LoginType), WorkspaceOwnerRbacRoles: ownerRbacRoles, + IsPrebuild: input.IsPrebuild, }, LogLevel: input.LogLevel, }, @@ -2460,6 +2462,7 @@ type TemplateVersionImportJob struct { type WorkspaceProvisionJob struct { WorkspaceBuildID uuid.UUID `json:"workspace_build_id"` DryRun bool `json:"dry_run"` + IsPrebuild bool `json:"is_prebuild,omitempty"` LogLevel string `json:"log_level,omitempty"` } diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index 698520d6f8d02..87f6be1507866 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -164,279 +164,286 @@ func TestAcquireJob(t *testing.T) { _, err = tc.acquire(ctx, srv) require.ErrorContains(t, err, "sql: no rows in result set") }) - t.Run(tc.name+"_WorkspaceBuildJob", func(t *testing.T) { - t.Parallel() - // Set the max session token lifetime so we can assert we - // create an API key with an expiration within the bounds of the - // deployment config. - dv := &codersdk.DeploymentValues{ - Sessions: codersdk.SessionLifetime{ - MaximumTokenDuration: serpent.Duration(time.Hour), - }, - } - gitAuthProvider := &sdkproto.ExternalAuthProviderResource{ - Id: "github", - } + for _, prebuiltWorkspace := range []bool{false, true} { + prebuiltWorkspace := prebuiltWorkspace + t.Run(tc.name+"_WorkspaceBuildJob", func(t *testing.T) { + t.Parallel() + // Set the max session token lifetime so we can assert we + // create an API key with an expiration within the bounds of the + // deployment config. + dv := &codersdk.DeploymentValues{ + Sessions: codersdk.SessionLifetime{ + MaximumTokenDuration: serpent.Duration(time.Hour), + }, + } + gitAuthProvider := &sdkproto.ExternalAuthProviderResource{ + Id: "github", + } - srv, db, ps, pd := setup(t, false, &overrides{ - deploymentValues: dv, - externalAuthConfigs: []*externalauth.Config{{ - ID: gitAuthProvider.Id, - InstrumentedOAuth2Config: &testutil.OAuth2Config{}, - }}, - }) - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) - defer cancel() + srv, db, ps, pd := setup(t, false, &overrides{ + deploymentValues: dv, + externalAuthConfigs: []*externalauth.Config{{ + ID: gitAuthProvider.Id, + InstrumentedOAuth2Config: &testutil.OAuth2Config{}, + }}, + }) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() - user := dbgen.User(t, db, database.User{}) - group1 := dbgen.Group(t, db, database.Group{ - Name: "group1", - OrganizationID: pd.OrganizationID, - }) - sshKey := dbgen.GitSSHKey(t, db, database.GitSSHKey{ - UserID: user.ID, - }) - err := db.InsertGroupMember(ctx, database.InsertGroupMemberParams{ - UserID: user.ID, - GroupID: group1.ID, - }) - require.NoError(t, err) - link := dbgen.UserLink(t, db, database.UserLink{ - LoginType: database.LoginTypeOIDC, - UserID: user.ID, - OAuthExpiry: dbtime.Now().Add(time.Hour), - OAuthAccessToken: "access-token", - }) - dbgen.ExternalAuthLink(t, db, database.ExternalAuthLink{ - ProviderID: gitAuthProvider.Id, - UserID: user.ID, - }) - template := dbgen.Template(t, db, database.Template{ - Name: "template", - Provisioner: database.ProvisionerTypeEcho, - OrganizationID: pd.OrganizationID, - }) - file := dbgen.File(t, db, database.File{CreatedBy: user.ID}) - versionFile := dbgen.File(t, db, database.File{CreatedBy: user.ID}) - version := dbgen.TemplateVersion(t, db, database.TemplateVersion{ - OrganizationID: pd.OrganizationID, - TemplateID: uuid.NullUUID{ - UUID: template.ID, - Valid: true, - }, - JobID: uuid.New(), - }) - externalAuthProviders, err := json.Marshal([]database.ExternalAuthProvider{{ - ID: gitAuthProvider.Id, - Optional: gitAuthProvider.Optional, - }}) - require.NoError(t, err) - err = db.UpdateTemplateVersionExternalAuthProvidersByJobID(ctx, database.UpdateTemplateVersionExternalAuthProvidersByJobIDParams{ - JobID: version.JobID, - ExternalAuthProviders: json.RawMessage(externalAuthProviders), - UpdatedAt: dbtime.Now(), - }) - require.NoError(t, err) - // Import version job - _ = dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ - OrganizationID: pd.OrganizationID, - ID: version.JobID, - InitiatorID: user.ID, - FileID: versionFile.ID, - Provisioner: database.ProvisionerTypeEcho, - StorageMethod: database.ProvisionerStorageMethodFile, - Type: database.ProvisionerJobTypeTemplateVersionImport, - Input: must(json.Marshal(provisionerdserver.TemplateVersionImportJob{ - TemplateVersionID: version.ID, - UserVariableValues: []codersdk.VariableValue{ - {Name: "second", Value: "bah"}, + user := dbgen.User(t, db, database.User{}) + group1 := dbgen.Group(t, db, database.Group{ + Name: "group1", + OrganizationID: pd.OrganizationID, + }) + sshKey := dbgen.GitSSHKey(t, db, database.GitSSHKey{ + UserID: user.ID, + }) + err := db.InsertGroupMember(ctx, database.InsertGroupMemberParams{ + UserID: user.ID, + GroupID: group1.ID, + }) + require.NoError(t, err) + link := dbgen.UserLink(t, db, database.UserLink{ + LoginType: database.LoginTypeOIDC, + UserID: user.ID, + OAuthExpiry: dbtime.Now().Add(time.Hour), + OAuthAccessToken: "access-token", + }) + dbgen.ExternalAuthLink(t, db, database.ExternalAuthLink{ + ProviderID: gitAuthProvider.Id, + UserID: user.ID, + }) + template := dbgen.Template(t, db, database.Template{ + Name: "template", + Provisioner: database.ProvisionerTypeEcho, + OrganizationID: pd.OrganizationID, + }) + file := dbgen.File(t, db, database.File{CreatedBy: user.ID}) + versionFile := dbgen.File(t, db, database.File{CreatedBy: user.ID}) + version := dbgen.TemplateVersion(t, db, database.TemplateVersion{ + OrganizationID: pd.OrganizationID, + TemplateID: uuid.NullUUID{ + UUID: template.ID, + Valid: true, }, - })), - }) - _ = dbgen.TemplateVersionVariable(t, db, database.TemplateVersionVariable{ - TemplateVersionID: version.ID, - Name: "first", - Value: "first_value", - DefaultValue: "default_value", - Sensitive: true, - }) - _ = dbgen.TemplateVersionVariable(t, db, database.TemplateVersionVariable{ - TemplateVersionID: version.ID, - Name: "second", - Value: "second_value", - DefaultValue: "default_value", - Required: true, - Sensitive: false, - }) - workspace := dbgen.Workspace(t, db, database.WorkspaceTable{ - TemplateID: template.ID, - OwnerID: user.ID, - OrganizationID: pd.OrganizationID, - }) - build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ - WorkspaceID: workspace.ID, - BuildNumber: 1, - JobID: uuid.New(), - TemplateVersionID: version.ID, - Transition: database.WorkspaceTransitionStart, - Reason: database.BuildReasonInitiator, - }) - _ = dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ - ID: build.ID, - OrganizationID: pd.OrganizationID, - InitiatorID: user.ID, - Provisioner: database.ProvisionerTypeEcho, - StorageMethod: database.ProvisionerStorageMethodFile, - FileID: file.ID, - Type: database.ProvisionerJobTypeWorkspaceBuild, - Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{ - WorkspaceBuildID: build.ID, - })), - }) + JobID: uuid.New(), + }) + externalAuthProviders, err := json.Marshal([]database.ExternalAuthProvider{{ + ID: gitAuthProvider.Id, + Optional: gitAuthProvider.Optional, + }}) + require.NoError(t, err) + err = db.UpdateTemplateVersionExternalAuthProvidersByJobID(ctx, database.UpdateTemplateVersionExternalAuthProvidersByJobIDParams{ + JobID: version.JobID, + ExternalAuthProviders: json.RawMessage(externalAuthProviders), + UpdatedAt: dbtime.Now(), + }) + require.NoError(t, err) + // Import version job + _ = dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ + OrganizationID: pd.OrganizationID, + ID: version.JobID, + InitiatorID: user.ID, + FileID: versionFile.ID, + Provisioner: database.ProvisionerTypeEcho, + StorageMethod: database.ProvisionerStorageMethodFile, + Type: database.ProvisionerJobTypeTemplateVersionImport, + Input: must(json.Marshal(provisionerdserver.TemplateVersionImportJob{ + TemplateVersionID: version.ID, + UserVariableValues: []codersdk.VariableValue{ + {Name: "second", Value: "bah"}, + }, + })), + }) + _ = dbgen.TemplateVersionVariable(t, db, database.TemplateVersionVariable{ + TemplateVersionID: version.ID, + Name: "first", + Value: "first_value", + DefaultValue: "default_value", + Sensitive: true, + }) + _ = dbgen.TemplateVersionVariable(t, db, database.TemplateVersionVariable{ + TemplateVersionID: version.ID, + Name: "second", + Value: "second_value", + DefaultValue: "default_value", + Required: true, + Sensitive: false, + }) + workspace := dbgen.Workspace(t, db, database.WorkspaceTable{ + TemplateID: template.ID, + OwnerID: user.ID, + OrganizationID: pd.OrganizationID, + }) + build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + WorkspaceID: workspace.ID, + BuildNumber: 1, + JobID: uuid.New(), + TemplateVersionID: version.ID, + Transition: database.WorkspaceTransitionStart, + Reason: database.BuildReasonInitiator, + }) + _ = dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ + ID: build.ID, + OrganizationID: pd.OrganizationID, + InitiatorID: user.ID, + Provisioner: database.ProvisionerTypeEcho, + StorageMethod: database.ProvisionerStorageMethodFile, + FileID: file.ID, + Type: database.ProvisionerJobTypeWorkspaceBuild, + Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{ + WorkspaceBuildID: build.ID, + IsPrebuild: prebuiltWorkspace, + })), + }) - startPublished := make(chan struct{}) - var closed bool - closeStartSubscribe, err := ps.SubscribeWithErr(wspubsub.WorkspaceEventChannel(workspace.OwnerID), - wspubsub.HandleWorkspaceEvent( - func(_ context.Context, e wspubsub.WorkspaceEvent, err error) { - if err != nil { - return - } - if e.Kind == wspubsub.WorkspaceEventKindStateChange && e.WorkspaceID == workspace.ID { - if !closed { - close(startPublished) - closed = true + startPublished := make(chan struct{}) + var closed bool + closeStartSubscribe, err := ps.SubscribeWithErr(wspubsub.WorkspaceEventChannel(workspace.OwnerID), + wspubsub.HandleWorkspaceEvent( + func(_ context.Context, e wspubsub.WorkspaceEvent, err error) { + if err != nil { + return } - } - })) - require.NoError(t, err) - defer closeStartSubscribe() + if e.Kind == wspubsub.WorkspaceEventKindStateChange && e.WorkspaceID == workspace.ID { + if !closed { + close(startPublished) + closed = true + } + } + })) + require.NoError(t, err) + defer closeStartSubscribe() - var job *proto.AcquiredJob + var job *proto.AcquiredJob - for { - // Grab jobs until we find the workspace build job. There is also - // an import version job that we need to ignore. - job, err = tc.acquire(ctx, srv) - require.NoError(t, err) - if _, ok := job.Type.(*proto.AcquiredJob_WorkspaceBuild_); ok { - break + for { + // Grab jobs until we find the workspace build job. There is also + // an import version job that we need to ignore. + job, err = tc.acquire(ctx, srv) + require.NoError(t, err) + if _, ok := job.Type.(*proto.AcquiredJob_WorkspaceBuild_); ok { + break + } } - } - <-startPublished + <-startPublished - got, err := json.Marshal(job.Type) - require.NoError(t, err) + got, err := json.Marshal(job.Type) + require.NoError(t, err) - // Validate that a session token is generated during the job. - sessionToken := job.Type.(*proto.AcquiredJob_WorkspaceBuild_).WorkspaceBuild.Metadata.WorkspaceOwnerSessionToken - require.NotEmpty(t, sessionToken) - toks := strings.Split(sessionToken, "-") - require.Len(t, toks, 2, "invalid api key") - key, err := db.GetAPIKeyByID(ctx, toks[0]) - require.NoError(t, err) - require.Equal(t, int64(dv.Sessions.MaximumTokenDuration.Value().Seconds()), key.LifetimeSeconds) - require.WithinDuration(t, time.Now().Add(dv.Sessions.MaximumTokenDuration.Value()), key.ExpiresAt, time.Minute) - - want, err := json.Marshal(&proto.AcquiredJob_WorkspaceBuild_{ - WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{ - WorkspaceBuildId: build.ID.String(), - WorkspaceName: workspace.Name, - VariableValues: []*sdkproto.VariableValue{ - { - Name: "first", - Value: "first_value", - Sensitive: true, - }, - { - Name: "second", - Value: "second_value", + // Validate that a session token is generated during the job. + sessionToken := job.Type.(*proto.AcquiredJob_WorkspaceBuild_).WorkspaceBuild.Metadata.WorkspaceOwnerSessionToken + require.NotEmpty(t, sessionToken) + toks := strings.Split(sessionToken, "-") + require.Len(t, toks, 2, "invalid api key") + key, err := db.GetAPIKeyByID(ctx, toks[0]) + require.NoError(t, err) + require.Equal(t, int64(dv.Sessions.MaximumTokenDuration.Value().Seconds()), key.LifetimeSeconds) + require.WithinDuration(t, time.Now().Add(dv.Sessions.MaximumTokenDuration.Value()), key.ExpiresAt, time.Minute) + + wantedMetadata := &sdkproto.Metadata{ + CoderUrl: (&url.URL{}).String(), + WorkspaceTransition: sdkproto.WorkspaceTransition_START, + WorkspaceName: workspace.Name, + WorkspaceOwner: user.Username, + WorkspaceOwnerEmail: user.Email, + WorkspaceOwnerName: user.Name, + WorkspaceOwnerOidcAccessToken: link.OAuthAccessToken, + WorkspaceOwnerGroups: []string{group1.Name}, + WorkspaceId: workspace.ID.String(), + WorkspaceOwnerId: user.ID.String(), + TemplateId: template.ID.String(), + TemplateName: template.Name, + TemplateVersion: version.Name, + WorkspaceOwnerSessionToken: sessionToken, + WorkspaceOwnerSshPublicKey: sshKey.PublicKey, + WorkspaceOwnerSshPrivateKey: sshKey.PrivateKey, + WorkspaceBuildId: build.ID.String(), + WorkspaceOwnerLoginType: string(user.LoginType), + WorkspaceOwnerRbacRoles: []*sdkproto.Role{{Name: "member", OrgId: pd.OrganizationID.String()}}, + } + if prebuiltWorkspace { + wantedMetadata.IsPrebuild = true + } + want, err := json.Marshal(&proto.AcquiredJob_WorkspaceBuild_{ + WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{ + WorkspaceBuildId: build.ID.String(), + WorkspaceName: workspace.Name, + VariableValues: []*sdkproto.VariableValue{ + { + Name: "first", + Value: "first_value", + Sensitive: true, + }, + { + Name: "second", + Value: "second_value", + }, }, + ExternalAuthProviders: []*sdkproto.ExternalAuthProvider{{ + Id: gitAuthProvider.Id, + AccessToken: "access_token", + }}, + Metadata: wantedMetadata, }, - ExternalAuthProviders: []*sdkproto.ExternalAuthProvider{{ - Id: gitAuthProvider.Id, - AccessToken: "access_token", - }}, - Metadata: &sdkproto.Metadata{ - CoderUrl: (&url.URL{}).String(), - WorkspaceTransition: sdkproto.WorkspaceTransition_START, - WorkspaceName: workspace.Name, - WorkspaceOwner: user.Username, - WorkspaceOwnerEmail: user.Email, - WorkspaceOwnerName: user.Name, - WorkspaceOwnerOidcAccessToken: link.OAuthAccessToken, - WorkspaceOwnerGroups: []string{group1.Name}, - WorkspaceId: workspace.ID.String(), - WorkspaceOwnerId: user.ID.String(), - TemplateId: template.ID.String(), - TemplateName: template.Name, - TemplateVersion: version.Name, - WorkspaceOwnerSessionToken: sessionToken, - WorkspaceOwnerSshPublicKey: sshKey.PublicKey, - WorkspaceOwnerSshPrivateKey: sshKey.PrivateKey, - WorkspaceBuildId: build.ID.String(), - WorkspaceOwnerLoginType: string(user.LoginType), - WorkspaceOwnerRbacRoles: []*sdkproto.Role{{Name: "member", OrgId: pd.OrganizationID.String()}}, - }, - }, - }) - require.NoError(t, err) - - require.JSONEq(t, string(want), string(got)) + }) + require.NoError(t, err) - // Assert that we delete the session token whenever - // a stop is issued. - stopbuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ - WorkspaceID: workspace.ID, - BuildNumber: 2, - JobID: uuid.New(), - TemplateVersionID: version.ID, - Transition: database.WorkspaceTransitionStop, - Reason: database.BuildReasonInitiator, - }) - _ = dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ - ID: stopbuild.ID, - InitiatorID: user.ID, - Provisioner: database.ProvisionerTypeEcho, - StorageMethod: database.ProvisionerStorageMethodFile, - FileID: file.ID, - Type: database.ProvisionerJobTypeWorkspaceBuild, - Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{ - WorkspaceBuildID: stopbuild.ID, - })), - }) + require.JSONEq(t, string(want), string(got)) - stopPublished := make(chan struct{}) - closeStopSubscribe, err := ps.SubscribeWithErr(wspubsub.WorkspaceEventChannel(workspace.OwnerID), - wspubsub.HandleWorkspaceEvent( - func(_ context.Context, e wspubsub.WorkspaceEvent, err error) { - if err != nil { - return - } - if e.Kind == wspubsub.WorkspaceEventKindStateChange && e.WorkspaceID == workspace.ID { - close(stopPublished) - } - })) - require.NoError(t, err) - defer closeStopSubscribe() + // Assert that we delete the session token whenever + // a stop is issued. + stopbuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + WorkspaceID: workspace.ID, + BuildNumber: 2, + JobID: uuid.New(), + TemplateVersionID: version.ID, + Transition: database.WorkspaceTransitionStop, + Reason: database.BuildReasonInitiator, + }) + _ = dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ + ID: stopbuild.ID, + InitiatorID: user.ID, + Provisioner: database.ProvisionerTypeEcho, + StorageMethod: database.ProvisionerStorageMethodFile, + FileID: file.ID, + Type: database.ProvisionerJobTypeWorkspaceBuild, + Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{ + WorkspaceBuildID: stopbuild.ID, + })), + }) - // Grab jobs until we find the workspace build job. There is also - // an import version job that we need to ignore. - job, err = tc.acquire(ctx, srv) - require.NoError(t, err) - _, ok := job.Type.(*proto.AcquiredJob_WorkspaceBuild_) - require.True(t, ok, "acquired job not a workspace build?") + stopPublished := make(chan struct{}) + closeStopSubscribe, err := ps.SubscribeWithErr(wspubsub.WorkspaceEventChannel(workspace.OwnerID), + wspubsub.HandleWorkspaceEvent( + func(_ context.Context, e wspubsub.WorkspaceEvent, err error) { + if err != nil { + return + } + if e.Kind == wspubsub.WorkspaceEventKindStateChange && e.WorkspaceID == workspace.ID { + close(stopPublished) + } + })) + require.NoError(t, err) + defer closeStopSubscribe() - <-stopPublished + // Grab jobs until we find the workspace build job. There is also + // an import version job that we need to ignore. + job, err = tc.acquire(ctx, srv) + require.NoError(t, err) + _, ok := job.Type.(*proto.AcquiredJob_WorkspaceBuild_) + require.True(t, ok, "acquired job not a workspace build?") - // Validate that a session token is deleted during a stop job. - sessionToken = job.Type.(*proto.AcquiredJob_WorkspaceBuild_).WorkspaceBuild.Metadata.WorkspaceOwnerSessionToken - require.Empty(t, sessionToken) - _, err = db.GetAPIKeyByID(ctx, key.ID) - require.ErrorIs(t, err, sql.ErrNoRows) - }) + <-stopPublished + // Validate that a session token is deleted during a stop job. + sessionToken = job.Type.(*proto.AcquiredJob_WorkspaceBuild_).WorkspaceBuild.Metadata.WorkspaceOwnerSessionToken + require.Empty(t, sessionToken) + _, err = db.GetAPIKeyByID(ctx, key.ID) + require.ErrorIs(t, err, sql.ErrNoRows) + }) + } t.Run(tc.name+"_TemplateVersionDryRun", func(t *testing.T) { t.Parallel() srv, db, ps, _ := setup(t, false, nil) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 7bd32e00cd830..94f1822df797c 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -337,7 +337,8 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { Initiator(apiKey.UserID). RichParameterValues(createBuild.RichParameterValues). LogLevel(string(createBuild.LogLevel)). - DeploymentValues(api.Options.DeploymentValues) + DeploymentValues(api.Options.DeploymentValues). + TemplateVersionPresetID(createBuild.TemplateVersionPresetID) var ( previousWorkspaceBuild database.WorkspaceBuild @@ -1065,6 +1066,11 @@ func (api *API) convertWorkspaceBuild( return apiResources[i].Name < apiResources[j].Name }) + var presetID *uuid.UUID + if build.TemplateVersionPresetID.Valid { + presetID = &build.TemplateVersionPresetID.UUID + } + apiJob := convertProvisionerJob(job) transition := codersdk.WorkspaceTransition(build.Transition) return codersdk.WorkspaceBuild{ @@ -1090,6 +1096,7 @@ func (api *API) convertWorkspaceBuild( Status: codersdk.ConvertWorkspaceStatus(apiJob.Status, transition), DailyCost: build.DailyCost, MatchedProvisioners: &matchedProvisioners, + TemplateVersionPresetID: presetID, }, nil } diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index 84efaa7ed0e23..08a8f3f26e0fa 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -1307,6 +1307,50 @@ func TestPostWorkspaceBuild(t *testing.T) { require.Equal(t, wantState, gotState) }) + t.Run("SetsPresetID", 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, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: []*proto.Response{{ + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ + Presets: []*proto.Preset{{ + Name: "test", + }}, + }, + }, + }}, + ProvisionApply: echo.ApplyComplete, + }) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, template.ID) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + require.Nil(t, workspace.LatestBuild.TemplateVersionPresetID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + presets, err := client.TemplateVersionPresets(ctx, version.ID) + require.NoError(t, err) + require.Equal(t, 1, len(presets)) + require.Equal(t, "test", presets[0].Name) + + build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ + TemplateVersionID: version.ID, + Transition: codersdk.WorkspaceTransitionStart, + TemplateVersionPresetID: presets[0].ID, + }) + require.NoError(t, err) + require.NotNil(t, build.TemplateVersionPresetID) + + workspace, err = client.Workspace(ctx, workspace.ID) + require.NoError(t, err) + require.Equal(t, build.TemplateVersionPresetID, workspace.LatestBuild.TemplateVersionPresetID) + }) + t.Run("Delete", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index d49de2388af59..a654597faeadd 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -671,7 +671,8 @@ func createWorkspace( Reason(database.BuildReasonInitiator). Initiator(initiatorID). ActiveVersion(). - RichParameterValues(req.RichParameterValues) + RichParameterValues(req.RichParameterValues). + TemplateVersionPresetID(req.TemplateVersionPresetID) if req.TemplateVersionID != uuid.Nil { builder = builder.VersionID(req.TemplateVersionID) } diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 76e85b0716181..136e259d541f9 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -423,6 +423,51 @@ func TestWorkspace(t *testing.T) { require.ErrorAs(t, err, &apiError) require.Equal(t, http.StatusForbidden, apiError.StatusCode()) }) + + t.Run("TemplateVersionPreset", func(t *testing.T) { + t.Parallel() + client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + authz := coderdtest.AssertRBAC(t, api, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: []*proto.Response{{ + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ + Presets: []*proto.Preset{{ + Name: "test", + }}, + }, + }, + }}, + ProvisionApply: echo.ApplyComplete, + }) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + + ctx := testutil.Context(t, testutil.WaitLong) + + presets, err := client.TemplateVersionPresets(ctx, version.ID) + require.NoError(t, err) + require.Equal(t, 1, len(presets)) + require.Equal(t, "test", presets[0].Name) + + workspace := coderdtest.CreateWorkspace(t, client, template.ID, func(request *codersdk.CreateWorkspaceRequest) { + request.TemplateVersionPresetID = presets[0].ID + }) + + authz.Reset() // Reset all previous checks done in setup. + ws, err := client.Workspace(ctx, workspace.ID) + authz.AssertChecked(t, policy.ActionRead, ws) + require.NoError(t, err) + require.Equal(t, user.UserID, ws.LatestBuild.InitiatorID) + require.Equal(t, codersdk.BuildReasonInitiator, ws.LatestBuild.Reason) + require.Equal(t, presets[0].ID, *ws.LatestBuild.TemplateVersionPresetID) + + org, err := client.Organization(ctx, ws.OrganizationID) + require.NoError(t, err) + require.Equal(t, ws.OrganizationName, org.Name) + }) } func TestResolveAutostart(t *testing.T) { diff --git a/coderd/wsbuilder/wsbuilder.go b/coderd/wsbuilder/wsbuilder.go index f6d6d7381a24f..469c8fbcfdd6d 100644 --- a/coderd/wsbuilder/wsbuilder.go +++ b/coderd/wsbuilder/wsbuilder.go @@ -51,9 +51,10 @@ type Builder struct { logLevel string deploymentValues *codersdk.DeploymentValues - richParameterValues []codersdk.WorkspaceBuildParameter - initiator uuid.UUID - reason database.BuildReason + richParameterValues []codersdk.WorkspaceBuildParameter + initiator uuid.UUID + reason database.BuildReason + templateVersionPresetID uuid.UUID // used during build, makes function arguments less verbose ctx context.Context @@ -73,6 +74,8 @@ type Builder struct { parameterNames *[]string parameterValues *[]string + prebuild bool + verifyNoLegacyParametersOnce bool } @@ -168,6 +171,12 @@ func (b Builder) RichParameterValues(p []codersdk.WorkspaceBuildParameter) Build return b } +func (b Builder) MarkPrebuild() Builder { + // nolint: revive + b.prebuild = true + return b +} + // SetLastWorkspaceBuildInTx prepopulates the Builder's cache with the last workspace build. This allows us // to avoid a repeated database query when the Builder's caller also needs the workspace build, e.g. auto-start & // auto-stop. @@ -192,6 +201,12 @@ func (b Builder) SetLastWorkspaceBuildJobInTx(job *database.ProvisionerJob) Buil return b } +func (b Builder) TemplateVersionPresetID(id uuid.UUID) Builder { + // nolint: revive + b.templateVersionPresetID = id + return b +} + type BuildError struct { // Status is a suitable HTTP status code Status int @@ -295,6 +310,7 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object input, err := json.Marshal(provisionerdserver.WorkspaceProvisionJob{ WorkspaceBuildID: workspaceBuildID, LogLevel: b.logLevel, + IsPrebuild: b.prebuild, }) if err != nil { return nil, nil, nil, BuildError{ @@ -363,20 +379,23 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object var workspaceBuild database.WorkspaceBuild err = b.store.InTx(func(store database.Store) error { err = store.InsertWorkspaceBuild(b.ctx, database.InsertWorkspaceBuildParams{ - ID: workspaceBuildID, - CreatedAt: now, - UpdatedAt: now, - WorkspaceID: b.workspace.ID, - TemplateVersionID: templateVersionID, - BuildNumber: buildNum, - ProvisionerState: state, - InitiatorID: b.initiator, - Transition: b.trans, - JobID: provisionerJob.ID, - Reason: b.reason, - Deadline: time.Time{}, // set by provisioner upon completion - MaxDeadline: time.Time{}, // set by provisioner upon completion - TemplateVersionPresetID: uuid.NullUUID{}, // TODO (sasswart): add this in from the caller + ID: workspaceBuildID, + CreatedAt: now, + UpdatedAt: now, + WorkspaceID: b.workspace.ID, + TemplateVersionID: templateVersionID, + BuildNumber: buildNum, + ProvisionerState: state, + InitiatorID: b.initiator, + Transition: b.trans, + JobID: provisionerJob.ID, + Reason: b.reason, + Deadline: time.Time{}, // set by provisioner upon completion + MaxDeadline: time.Time{}, // set by provisioner upon completion + TemplateVersionPresetID: uuid.NullUUID{ + UUID: b.templateVersionPresetID, + Valid: b.templateVersionPresetID != uuid.Nil, + }, }) if err != nil { code := http.StatusInternalServerError diff --git a/coderd/wsbuilder/wsbuilder_test.go b/coderd/wsbuilder/wsbuilder_test.go index d8f25c5a8cda3..bd6e64a60414a 100644 --- a/coderd/wsbuilder/wsbuilder_test.go +++ b/coderd/wsbuilder/wsbuilder_test.go @@ -41,6 +41,7 @@ var ( lastBuildID = uuid.MustParse("12341234-0000-0000-000b-000000000000") lastBuildJobID = uuid.MustParse("12341234-0000-0000-000c-000000000000") otherUserID = uuid.MustParse("12341234-0000-0000-000d-000000000000") + presetID = uuid.MustParse("12341234-0000-0000-000e-000000000000") ) func TestBuilder_NoOptions(t *testing.T) { @@ -773,6 +774,67 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) { }) } +func TestWorkspaceBuildWithPreset(t *testing.T) { + t.Parallel() + + req := require.New(t) + asrt := assert.New(t) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var buildID uuid.UUID + + mDB := expectDB(t, + // Inputs + withTemplate, + withActiveVersion(nil), + withLastBuildNotFound, + withTemplateVersionVariables(activeVersionID, nil), + withParameterSchemas(activeJobID, nil), + withWorkspaceTags(activeVersionID, nil), + withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}), + + // Outputs + expectProvisionerJob(func(job database.InsertProvisionerJobParams) { + asrt.Equal(userID, job.InitiatorID) + asrt.Equal(activeFileID, job.FileID) + input := provisionerdserver.WorkspaceProvisionJob{} + err := json.Unmarshal(job.Input, &input) + req.NoError(err) + // store build ID for later + buildID = input.WorkspaceBuildID + }), + + withInTx, + expectBuild(func(bld database.InsertWorkspaceBuildParams) { + asrt.Equal(activeVersionID, bld.TemplateVersionID) + asrt.Equal(workspaceID, bld.WorkspaceID) + asrt.Equal(int32(1), bld.BuildNumber) + asrt.Equal(userID, bld.InitiatorID) + asrt.Equal(database.WorkspaceTransitionStart, bld.Transition) + asrt.Equal(database.BuildReasonInitiator, bld.Reason) + asrt.Equal(buildID, bld.ID) + asrt.True(bld.TemplateVersionPresetID.Valid) + asrt.Equal(presetID, bld.TemplateVersionPresetID.UUID) + }), + withBuild, + expectBuildParameters(func(params database.InsertWorkspaceBuildParametersParams) { + asrt.Equal(buildID, params.WorkspaceBuildID) + asrt.Empty(params.Name) + asrt.Empty(params.Value) + }), + ) + + ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID} + uut := wsbuilder.New(ws, database.WorkspaceTransitionStart). + ActiveVersion(). + TemplateVersionPresetID(presetID) + // nolint: dogsled + _, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{}) + req.NoError(err) +} + type txExpect func(mTx *dbmock.MockStore) func expectDB(t *testing.T, opts ...txExpect) *dbmock.MockStore { diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 8a028d46e098c..b981e3bed28fa 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -217,8 +217,9 @@ type CreateWorkspaceRequest struct { TTLMillis *int64 `json:"ttl_ms,omitempty"` // RichParameterValues allows for additional parameters to be provided // during the initial provision. - RichParameterValues []WorkspaceBuildParameter `json:"rich_parameter_values,omitempty"` - AutomaticUpdates AutomaticUpdates `json:"automatic_updates,omitempty"` + RichParameterValues []WorkspaceBuildParameter `json:"rich_parameter_values,omitempty"` + AutomaticUpdates AutomaticUpdates `json:"automatic_updates,omitempty"` + TemplateVersionPresetID uuid.UUID `json:"template_version_preset_id,omitempty" format:"uuid"` } func (c *Client) OrganizationByName(ctx context.Context, name string) (Organization, error) { diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index 2718735f01177..7b67dc3b86171 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -73,6 +73,7 @@ type WorkspaceBuild struct { Status WorkspaceStatus `json:"status" enums:"pending,starting,running,stopping,stopped,failed,canceling,canceled,deleting,deleted"` DailyCost int32 `json:"daily_cost"` MatchedProvisioners *MatchedProvisioners `json:"matched_provisioners,omitempty"` + TemplateVersionPresetID *uuid.UUID `json:"template_version_preset_id" format:"uuid"` } // WorkspaceResource describes resources used to create a workspace, for instance: diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index f9377c1767451..311c4bcba35d4 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -107,6 +107,8 @@ type CreateWorkspaceBuildRequest struct { // Log level changes the default logging verbosity of a provider ("info" if empty). LogLevel ProvisionerLogLevel `json:"log_level,omitempty" validate:"omitempty,oneof=debug"` + // TemplateVersionPresetID is the ID of the template version preset to use for the build. + TemplateVersionPresetID uuid.UUID `json:"template_version_preset_id,omitempty" format:"uuid"` } type WorkspaceOptions struct { diff --git a/docs/reference/api/builds.md b/docs/reference/api/builds.md index 0bb4b2e5b0ef3..1e5ff95026eaf 100644 --- a/docs/reference/api/builds.md +++ b/docs/reference/api/builds.md @@ -212,6 +212,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", @@ -440,6 +441,7 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild} \ "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", @@ -1138,6 +1140,7 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/sta "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", @@ -1439,6 +1442,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", @@ -1605,6 +1609,7 @@ Status Code **200** | `» status` | [codersdk.WorkspaceStatus](schemas.md#codersdkworkspacestatus) | false | | | | `» template_version_id` | string(uuid) | false | | | | `» template_version_name` | string | false | | | +| `» template_version_preset_id` | string(uuid) | false | | | | `» transition` | [codersdk.WorkspaceTransition](schemas.md#codersdkworkspacetransition) | false | | | | `» updated_at` | string(date-time) | false | | | | `» workspace_id` | string(uuid) | false | | | @@ -1707,6 +1712,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ 0 ], "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start" } ``` @@ -1909,6 +1915,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 8d38d0c4e346b..bdb57487ddf7d 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -1451,21 +1451,23 @@ This is required on creation to enable a user-flow of validating a template work 0 ], "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start" } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -|-------------------------|-------------------------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `dry_run` | boolean | false | | | -| `log_level` | [codersdk.ProvisionerLogLevel](#codersdkprovisionerloglevel) | false | | Log level changes the default logging verbosity of a provider ("info" if empty). | -| `orphan` | boolean | false | | Orphan may be set for the Destroy transition. | -| `rich_parameter_values` | array of [codersdk.WorkspaceBuildParameter](#codersdkworkspacebuildparameter) | false | | Rich parameter values 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. | -| `state` | array of integer | false | | | -| `template_version_id` | string | false | | | -| `transition` | [codersdk.WorkspaceTransition](#codersdkworkspacetransition) | true | | | +| Name | Type | Required | Restrictions | Description | +|------------------------------|-------------------------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `dry_run` | boolean | false | | | +| `log_level` | [codersdk.ProvisionerLogLevel](#codersdkprovisionerloglevel) | false | | Log level changes the default logging verbosity of a provider ("info" if empty). | +| `orphan` | boolean | false | | Orphan may be set for the Destroy transition. | +| `rich_parameter_values` | array of [codersdk.WorkspaceBuildParameter](#codersdkworkspacebuildparameter) | false | | Rich parameter values 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. | +| `state` | array of integer | false | | | +| `template_version_id` | string | false | | | +| `template_version_preset_id` | string | false | | Template version preset ID is the ID of the template version preset to use for the build. | +| `transition` | [codersdk.WorkspaceTransition](#codersdkworkspacetransition) | true | | | #### Enumerated Values @@ -1509,6 +1511,7 @@ This is required on creation to enable a user-flow of validating a template work ], "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "ttl_ms": 0 } ``` @@ -1517,15 +1520,16 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o ### Properties -| Name | Type | Required | Restrictions | Description | -|-------------------------|-------------------------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------------------------------------------| -| `automatic_updates` | [codersdk.AutomaticUpdates](#codersdkautomaticupdates) | false | | | -| `autostart_schedule` | string | false | | | -| `name` | string | true | | | -| `rich_parameter_values` | array of [codersdk.WorkspaceBuildParameter](#codersdkworkspacebuildparameter) | false | | Rich parameter values allows for additional parameters to be provided during the initial provision. | -| `template_id` | string | false | | Template ID specifies which template should be used for creating the workspace. | -| `template_version_id` | string | false | | Template version ID can be used to specify a specific version of a template for creating the workspace. | -| `ttl_ms` | integer | false | | | +| Name | Type | Required | Restrictions | Description | +|------------------------------|-------------------------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------------------------------------------| +| `automatic_updates` | [codersdk.AutomaticUpdates](#codersdkautomaticupdates) | false | | | +| `autostart_schedule` | string | false | | | +| `name` | string | true | | | +| `rich_parameter_values` | array of [codersdk.WorkspaceBuildParameter](#codersdkworkspacebuildparameter) | false | | Rich parameter values allows for additional parameters to be provided during the initial provision. | +| `template_id` | string | false | | Template ID specifies which template should be used for creating the workspace. | +| `template_version_id` | string | false | | Template version ID can be used to specify a specific version of a template for creating the workspace. | +| `template_version_preset_id` | string | false | | | +| `ttl_ms` | integer | false | | | ## codersdk.CryptoKey @@ -7825,6 +7829,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", @@ -8776,6 +8781,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", @@ -8805,6 +8811,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `status` | [codersdk.WorkspaceStatus](#codersdkworkspacestatus) | false | | | | `template_version_id` | string | false | | | | `template_version_name` | string | false | | | +| `template_version_preset_id` | string | false | | | | `transition` | [codersdk.WorkspaceTransition](#codersdkworkspacetransition) | false | | | | `updated_at` | string | false | | | | `workspace_id` | string | false | | | @@ -9472,6 +9479,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md index 00400942d34db..5e727cee297fe 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -34,6 +34,7 @@ of the template will be used. ], "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "ttl_ms": 0 } ``` @@ -265,6 +266,7 @@ of the template will be used. "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", @@ -541,6 +543,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", @@ -611,6 +614,7 @@ of the template will be used. ], "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "ttl_ms": 0 } ``` @@ -841,6 +845,7 @@ of the template will be used. "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", @@ -1103,6 +1108,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \ "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", @@ -1380,6 +1386,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \ "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", @@ -1772,6 +1779,7 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \ "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", diff --git a/provisioner/terraform/provision.go b/provisioner/terraform/provision.go index 171deb35c4bbc..f8f82bbad7b9a 100644 --- a/provisioner/terraform/provision.go +++ b/provisioner/terraform/provision.go @@ -270,6 +270,10 @@ func provisionEnv( "CODER_WORKSPACE_TEMPLATE_VERSION="+metadata.GetTemplateVersion(), "CODER_WORKSPACE_BUILD_ID="+metadata.GetWorkspaceBuildId(), ) + if metadata.GetIsPrebuild() { + env = append(env, provider.IsPrebuildEnvironmentVariable()+"=true") + } + for key, value := range provisionersdk.AgentScriptEnv() { env = append(env, key+"="+value) } diff --git a/provisioner/terraform/provision_test.go b/provisioner/terraform/provision_test.go index 00b459ca1df1a..e7b64046f3ab3 100644 --- a/provisioner/terraform/provision_test.go +++ b/provisioner/terraform/provision_test.go @@ -798,6 +798,44 @@ func TestProvision(t *testing.T) { }}, }, }, + { + Name: "is-prebuild", + Files: map[string]string{ + "main.tf": `terraform { + required_providers { + coder = { + source = "coder/coder" + version = "2.3.0-pre2" + } + } + } + data "coder_workspace" "me" {} + resource "null_resource" "example" {} + resource "coder_metadata" "example" { + resource_id = null_resource.example.id + item { + key = "is_prebuild" + value = data.coder_workspace.me.is_prebuild + } + } + `, + }, + Request: &proto.PlanRequest{ + Metadata: &proto.Metadata{ + IsPrebuild: true, + }, + }, + Response: &proto.PlanComplete{ + Resources: []*proto.Resource{{ + Name: "example", + Type: "null_resource", + Metadata: []*proto.Resource_Metadata{{ + Key: "is_prebuild", + Value: "true", + }}, + }}, + }, + }, } for _, testCase := range testCases { diff --git a/provisionerd/provisionerd.go b/provisionerd/provisionerd.go index b461bc593ee36..8e9df48b9a1e8 100644 --- a/provisionerd/provisionerd.go +++ b/provisionerd/provisionerd.go @@ -367,6 +367,7 @@ func (p *Server) acquireAndRunOne(client proto.DRPCProvisionerDaemonClient) { slog.F("workspace_build_id", build.WorkspaceBuildId), slog.F("workspace_id", build.Metadata.WorkspaceId), slog.F("workspace_name", build.WorkspaceName), + slog.F("is_prebuild", build.Metadata.IsPrebuild), ) span.SetAttributes( @@ -376,6 +377,7 @@ func (p *Server) acquireAndRunOne(client proto.DRPCProvisionerDaemonClient) { attribute.String("workspace_owner_id", build.Metadata.WorkspaceOwnerId), attribute.String("workspace_owner", build.Metadata.WorkspaceOwner), attribute.String("workspace_transition", build.Metadata.WorkspaceTransition.String()), + attribute.Bool("is_prebuild", build.Metadata.IsPrebuild), ) } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 09da288ceeb76..d1b19ea8fb6c2 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -447,6 +447,7 @@ export interface CreateWorkspaceBuildRequest { readonly orphan?: boolean; readonly rich_parameter_values?: readonly WorkspaceBuildParameter[]; readonly log_level?: ProvisionerLogLevel; + readonly template_version_preset_id?: string; } // From codersdk/workspaceproxy.go @@ -465,6 +466,7 @@ export interface CreateWorkspaceRequest { readonly ttl_ms?: number; readonly rich_parameter_values?: readonly WorkspaceBuildParameter[]; readonly automatic_updates?: AutomaticUpdates; + readonly template_version_preset_id?: string; } // From codersdk/deployment.go @@ -3413,6 +3415,7 @@ export interface WorkspaceBuild { readonly status: WorkspaceStatus; readonly daily_cost: number; readonly matched_provisioners?: MatchedProvisioners; + readonly template_version_preset_id: string | null; } // From codersdk/workspacebuilds.go diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index 5dc9c8d0a4818..66d0033ea6a74 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -369,6 +369,10 @@ export const CreateWorkspacePageView: FC = ({ return; } setSelectedPresetIndex(index); + form.setFieldValue( + "template_version_preset_id", + option?.value, + ); }} placeholder="Select a preset" selectedOption={presetOptions[selectedPresetIndex]} diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 804291df30729..a434c56200a87 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -1266,6 +1266,7 @@ export const MockWorkspaceBuild: TypesGen.WorkspaceBuild = { count: 1, available: 1, }, + template_version_preset_id: null, }; export const MockWorkspaceBuildAutostart: TypesGen.WorkspaceBuild = { @@ -1289,6 +1290,7 @@ export const MockWorkspaceBuildAutostart: TypesGen.WorkspaceBuild = { resources: [MockWorkspaceResource], status: "running", daily_cost: 20, + template_version_preset_id: null, }; export const MockWorkspaceBuildAutostop: TypesGen.WorkspaceBuild = { @@ -1312,6 +1314,7 @@ export const MockWorkspaceBuildAutostop: TypesGen.WorkspaceBuild = { resources: [MockWorkspaceResource], status: "running", daily_cost: 20, + template_version_preset_id: null, }; export const MockFailedWorkspaceBuild = ( @@ -1337,6 +1340,7 @@ export const MockFailedWorkspaceBuild = ( resources: [], status: "failed", daily_cost: 20, + template_version_preset_id: null, }); export const MockWorkspaceBuildStop: TypesGen.WorkspaceBuild = {