mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
feat: Updating workspace prompts new parameters (#2598)
This commit is contained in:
202
cli/create.go
202
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
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
// nolint
|
||||
func delete() *cobra.Command {
|
||||
func deleteWorkspace() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Annotations: workspaceCommand,
|
||||
Use: "delete <workspace>",
|
||||
|
@ -69,7 +69,7 @@ func Root() *cobra.Command {
|
||||
cmd.AddCommand(
|
||||
configSSH(),
|
||||
create(),
|
||||
delete(),
|
||||
deleteWorkspace(),
|
||||
dotfiles(),
|
||||
gitssh(),
|
||||
list(),
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
@ -464,17 +465,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
|
||||
}
|
||||
|
Reference in New Issue
Block a user