diff --git a/cli/create.go b/cli/create.go index 258e272c01dbf..f76e497982978 100644 --- a/cli/create.go +++ b/cli/create.go @@ -120,87 +120,11 @@ func create() *cobra.Command { schedSpec = ptr.Ref(sched.String()) } - templateVersion, err := client.TemplateVersion(cmd.Context(), template.ActiveVersionID) - if err != nil { - return err - } - parameterSchemas, err := client.TemplateVersionSchema(cmd.Context(), templateVersion.ID) - if err != nil { - return err - } - - // parameterMapFromFile can be nil if parameter file is not specified - var parameterMapFromFile map[string]string - if parameterFile != "" { - _, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render("Attempting to read the variables from the parameter file.")+"\r\n") - parameterMapFromFile, err = createParameterMapFromFile(parameterFile) - if err != nil { - return err - } - } - - disclaimerPrinted := false - parameters := make([]codersdk.CreateParameterRequest, 0) - for _, parameterSchema := range parameterSchemas { - if !parameterSchema.AllowOverrideSource { - continue - } - if !disclaimerPrinted { - _, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render("This template has customizable parameters. Values can be changed after create, but may have unintended side effects (like data loss).")+"\r\n") - disclaimerPrinted = true - } - parameterValue, err := getParameterValueFromMapOrInput(cmd, parameterMapFromFile, parameterSchema) - if err != nil { - return err - } - parameters = append(parameters, codersdk.CreateParameterRequest{ - Name: parameterSchema.Name, - SourceValue: parameterValue, - SourceScheme: codersdk.ParameterSourceSchemeData, - DestinationScheme: parameterSchema.DefaultDestinationScheme, - }) - } - _, _ = fmt.Fprintln(cmd.OutOrStdout()) - - // Run a dry-run with the given parameters to check correctness - after := time.Now() - dryRun, err := client.CreateTemplateVersionDryRun(cmd.Context(), templateVersion.ID, codersdk.CreateTemplateVersionDryRunRequest{ - WorkspaceName: workspaceName, - ParameterValues: parameters, - }) - if err != nil { - return xerrors.Errorf("begin workspace dry-run: %w", err) - } - _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Planning workspace...") - err = cliui.ProvisionerJob(cmd.Context(), cmd.OutOrStdout(), cliui.ProvisionerJobOptions{ - Fetch: func() (codersdk.ProvisionerJob, error) { - return client.TemplateVersionDryRun(cmd.Context(), templateVersion.ID, dryRun.ID) - }, - Cancel: func() error { - return client.CancelTemplateVersionDryRun(cmd.Context(), templateVersion.ID, dryRun.ID) - }, - Logs: func() (<-chan codersdk.ProvisionerJobLog, error) { - return client.TemplateVersionDryRunLogsAfter(cmd.Context(), templateVersion.ID, dryRun.ID, after) - }, - // Don't show log output for the dry-run unless there's an error. - Silent: true, - }) - if err != nil { - // TODO (Dean): reprompt for parameter values if we deem it to - // be a validation error - return xerrors.Errorf("dry-run workspace: %w", err) - } - - resources, err := client.TemplateVersionDryRunResources(cmd.Context(), templateVersion.ID, dryRun.ID) - if err != nil { - return xerrors.Errorf("get workspace dry-run resources: %w", err) - } - - err = cliui.WorkspaceResources(cmd.OutOrStdout(), resources, cliui.WorkspaceResourcesOptions{ - WorkspaceName: workspaceName, - // Since agent's haven't connected yet, hiding this makes more sense. - HideAgentState: true, - Title: "Workspace Preview", + parameters, err := prepWorkspaceBuild(cmd, client, prepWorkspaceBuildArgs{ + Template: template, + ExistingParams: []codersdk.Parameter{}, + ParameterFile: parameterFile, + NewWorkspaceName: workspaceName, }) if err != nil { return err @@ -214,6 +138,7 @@ func create() *cobra.Command { return err } + after := time.Now() workspace, err := client.CreateWorkspace(cmd.Context(), organization.ID, codersdk.CreateWorkspaceRequest{ TemplateID: template.ID, Name: workspaceName, @@ -242,3 +167,118 @@ func create() *cobra.Command { cliflag.DurationVarP(cmd.Flags(), &stopAfter, "stop-after", "", "CODER_WORKSPACE_STOP_AFTER", 8*time.Hour, "Specify a duration after which the workspace should shut down (e.g. 8h).") return cmd } + +type prepWorkspaceBuildArgs struct { + Template codersdk.Template + ExistingParams []codersdk.Parameter + ParameterFile string + NewWorkspaceName string +} + +// prepWorkspaceBuild will ensure a workspace build will succeed on the latest template version. +// Any missing params will be prompted to the user. +func prepWorkspaceBuild(cmd *cobra.Command, client *codersdk.Client, args prepWorkspaceBuildArgs) ([]codersdk.CreateParameterRequest, error) { + ctx := cmd.Context() + templateVersion, err := client.TemplateVersion(ctx, args.Template.ActiveVersionID) + if err != nil { + return nil, err + } + parameterSchemas, err := client.TemplateVersionSchema(ctx, templateVersion.ID) + if err != nil { + return nil, err + } + + // parameterMapFromFile can be nil if parameter file is not specified + var parameterMapFromFile map[string]string + useParamFile := false + if args.ParameterFile != "" { + useParamFile = true + _, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render("Attempting to read the variables from the parameter file.")+"\r\n") + parameterMapFromFile, err = createParameterMapFromFile(args.ParameterFile) + if err != nil { + return nil, err + } + } + disclaimerPrinted := false + parameters := make([]codersdk.CreateParameterRequest, 0) +PromptParamLoop: + for _, parameterSchema := range parameterSchemas { + if !parameterSchema.AllowOverrideSource { + continue + } + if !disclaimerPrinted { + _, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render("This template has customizable parameters. Values can be changed after create, but may have unintended side effects (like data loss).")+"\r\n") + disclaimerPrinted = true + } + + // Param file is all or nothing + if !useParamFile { + for _, e := range args.ExistingParams { + if e.Name == parameterSchema.Name { + // If the param already exists, we do not need to prompt it again. + // The workspace scope will reuse params for each build. + continue PromptParamLoop + } + } + } + + parameterValue, err := getParameterValueFromMapOrInput(cmd, parameterMapFromFile, parameterSchema) + if err != nil { + return nil, err + } + + parameters = append(parameters, codersdk.CreateParameterRequest{ + Name: parameterSchema.Name, + SourceValue: parameterValue, + SourceScheme: codersdk.ParameterSourceSchemeData, + DestinationScheme: parameterSchema.DefaultDestinationScheme, + }) + } + _, _ = fmt.Fprintln(cmd.OutOrStdout()) + + // Run a dry-run with the given parameters to check correctness + after := time.Now() + dryRun, err := client.CreateTemplateVersionDryRun(cmd.Context(), templateVersion.ID, codersdk.CreateTemplateVersionDryRunRequest{ + WorkspaceName: args.NewWorkspaceName, + ParameterValues: parameters, + }) + if err != nil { + return nil, xerrors.Errorf("begin workspace dry-run: %w", err) + } + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Planning workspace...") + err = cliui.ProvisionerJob(cmd.Context(), cmd.OutOrStdout(), cliui.ProvisionerJobOptions{ + Fetch: func() (codersdk.ProvisionerJob, error) { + return client.TemplateVersionDryRun(cmd.Context(), templateVersion.ID, dryRun.ID) + }, + Cancel: func() error { + return client.CancelTemplateVersionDryRun(cmd.Context(), templateVersion.ID, dryRun.ID) + }, + Logs: func() (<-chan codersdk.ProvisionerJobLog, error) { + return client.TemplateVersionDryRunLogsAfter(cmd.Context(), templateVersion.ID, dryRun.ID, after) + }, + // Don't show log output for the dry-run unless there's an error. + Silent: true, + }) + if err != nil { + // TODO (Dean): reprompt for parameter values if we deem it to + // be a validation error + return nil, xerrors.Errorf("dry-run workspace: %w", err) + } + + resources, err := client.TemplateVersionDryRunResources(cmd.Context(), templateVersion.ID, dryRun.ID) + if err != nil { + return nil, xerrors.Errorf("get workspace dry-run resources: %w", err) + } + + err = cliui.WorkspaceResources(cmd.OutOrStdout(), resources, cliui.WorkspaceResourcesOptions{ + WorkspaceName: args.NewWorkspaceName, + // Since agents haven't connected yet, hiding this makes more sense. + HideAgentState: true, + Title: "Workspace Preview", + }) + if err != nil { + return nil, err + } + + return parameters, nil +} diff --git a/cli/delete.go b/cli/delete.go index bed17ee9ff483..774225c262212 100644 --- a/cli/delete.go +++ b/cli/delete.go @@ -10,7 +10,7 @@ import ( ) // nolint -func delete() *cobra.Command { +func deleteWorkspace() *cobra.Command { cmd := &cobra.Command{ Annotations: workspaceCommand, Use: "delete ", diff --git a/cli/root.go b/cli/root.go index 2f7ef384a6060..dc177f6ffd5dc 100644 --- a/cli/root.go +++ b/cli/root.go @@ -67,7 +67,7 @@ func Root() *cobra.Command { cmd.AddCommand( configSSH(), create(), - delete(), + deleteWorkspace(), dotfiles(), gitssh(), list(), diff --git a/cli/update.go b/cli/update.go index 288d9dd570162..5875c56b67c3f 100644 --- a/cli/update.go +++ b/cli/update.go @@ -6,11 +6,17 @@ import ( "github.com/spf13/cobra" + "github.com/coder/coder/cli/cliflag" "github.com/coder/coder/codersdk" ) func update() *cobra.Command { - return &cobra.Command{ + var ( + parameterFile string + alwaysPrompt bool + ) + + cmd := &cobra.Command{ Annotations: workspaceCommand, Use: "update", Short: "Update a workspace to the latest template version", @@ -23,7 +29,7 @@ func update() *cobra.Command { if err != nil { return err } - if !workspace.Outdated { + if !workspace.Outdated && !alwaysPrompt { _, _ = fmt.Printf("Workspace isn't outdated!\n") return nil } @@ -31,10 +37,30 @@ func update() *cobra.Command { if err != nil { return nil } + + var existingParams []codersdk.Parameter + if !alwaysPrompt { + existingParams, err = client.Parameters(cmd.Context(), codersdk.ParameterWorkspace, workspace.ID) + if err != nil { + return nil + } + } + + parameters, err := prepWorkspaceBuild(cmd, client, prepWorkspaceBuildArgs{ + Template: template, + ExistingParams: existingParams, + ParameterFile: parameterFile, + NewWorkspaceName: workspace.Name, + }) + if err != nil { + return nil + } + before := time.Now() build, err := client.CreateWorkspaceBuild(cmd.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{ TemplateVersionID: template.ActiveVersionID, Transition: workspace.LatestBuild.Transition, + ParameterValues: parameters, }) if err != nil { return err @@ -53,4 +79,8 @@ func update() *cobra.Command { return nil }, } + + cmd.Flags().BoolVar(&alwaysPrompt, "always-prompt", false, "Always prompt all parameters. Does not pull parameter values from existing workspace") + cliflag.StringVarP(cmd.Flags(), ¶meterFile, "parameter-file", "", "CODER_PARAMETER_FILE", "", "Specify a file path with parameter values.") + return cmd } diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index f6cac24ade77e..f62a113d040d9 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -400,6 +400,43 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { // This must happen in a transaction to ensure history can be inserted, and // the prior history can update it's "after" column to point at the new. err = api.Database.InTx(func(db database.Store) error { + existing, err := db.ParameterValues(r.Context(), database.ParameterValuesParams{ + Scopes: []database.ParameterScope{database.ParameterScopeWorkspace}, + ScopeIds: []uuid.UUID{workspace.ID}, + }) + if err != nil && !xerrors.Is(err, sql.ErrNoRows) { + return xerrors.Errorf("Fetch previous parameters: %w", err) + } + + // Write/Update any new params + now := database.Now() + for _, param := range createBuild.ParameterValues { + for _, exists := range existing { + // If the param exists, delete the old param before inserting the new one + if exists.Name == param.Name { + err = db.DeleteParameterValueByID(r.Context(), exists.ID) + if err != nil && !xerrors.Is(err, sql.ErrNoRows) { + return xerrors.Errorf("Failed to delete old param %q: %w", exists.Name, err) + } + } + } + + _, err = db.InsertParameterValue(r.Context(), database.InsertParameterValueParams{ + ID: uuid.New(), + Name: param.Name, + CreatedAt: now, + UpdatedAt: now, + Scope: database.ParameterScopeWorkspace, + ScopeID: workspace.ID, + SourceScheme: database.ParameterSourceScheme(param.SourceScheme), + SourceValue: param.SourceValue, + DestinationScheme: database.ParameterDestinationScheme(param.DestinationScheme), + }) + if err != nil { + return xerrors.Errorf("insert parameter value: %w", err) + } + } + workspaceBuildID := uuid.New() input, err := json.Marshal(workspaceProvisionJob{ WorkspaceBuildID: workspaceBuildID, diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index f5139c8e5200a..c596b684896d6 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -37,6 +37,10 @@ type CreateWorkspaceBuildRequest struct { Transition WorkspaceTransition `json:"transition" validate:"oneof=create start stop delete,required"` DryRun bool `json:"dry_run,omitempty"` ProvisionerState []byte `json:"state,omitempty"` + // ParameterValues 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. + ParameterValues []CreateParameterRequest `json:"parameter_values,omitempty"` } type WorkspaceOptions struct { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 4c318f0bf0bc0..c3562b887e656 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -104,6 +104,7 @@ export interface CreateWorkspaceBuildRequest { readonly transition: WorkspaceTransition readonly dry_run?: boolean readonly state?: string + readonly parameter_values?: CreateParameterRequest[] } // From codersdk/organizations.go:76:6 @@ -232,7 +233,7 @@ export interface ProvisionerJobLog { readonly output: string } -// From codersdk/workspaces.go:202:6 +// From codersdk/workspaces.go:206:6 export interface PutExtendWorkspaceRequest { readonly deadline: string } @@ -305,12 +306,12 @@ export interface UpdateUserProfileRequest { readonly username: string } -// From codersdk/workspaces.go:161:6 +// From codersdk/workspaces.go:165:6 export interface UpdateWorkspaceAutostartRequest { readonly schedule?: string } -// From codersdk/workspaces.go:181:6 +// From codersdk/workspaces.go:185:6 export interface UpdateWorkspaceTTLRequest { readonly ttl_ms?: number } @@ -456,17 +457,17 @@ export interface WorkspaceBuild { readonly reason: BuildReason } -// From codersdk/workspaces.go:84:6 +// From codersdk/workspaces.go:88:6 export interface WorkspaceBuildsRequest extends Pagination { readonly WorkspaceID: string } -// From codersdk/workspaces.go:220:6 +// From codersdk/workspaces.go:224:6 export interface WorkspaceFilter { readonly q?: string } -// From codersdk/workspaces.go:42:6 +// From codersdk/workspaces.go:46:6 export interface WorkspaceOptions { readonly include_deleted?: boolean }