feat: Updating workspace prompts new parameters (#2598)

This commit is contained in:
Steven Masley
2022-06-27 11:19:10 -05:00
committed by GitHub
parent 407c47fd65
commit f41b50a253
7 changed files with 203 additions and 91 deletions

View File

@ -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
}

View File

@ -11,7 +11,7 @@ import (
)
// nolint
func delete() *cobra.Command {
func deleteWorkspace() *cobra.Command {
cmd := &cobra.Command{
Annotations: workspaceCommand,
Use: "delete <workspace>",

View File

@ -69,7 +69,7 @@ func Root() *cobra.Command {
cmd.AddCommand(
configSSH(),
create(),
delete(),
deleteWorkspace(),
dotfiles(),
gitssh(),
list(),

View File

@ -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(), &parameterFile, "parameter-file", "", "CODER_PARAMETER_FILE", "", "Specify a file path with parameter values.")
return cmd
}

View File

@ -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,

View File

@ -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 {

View File

@ -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
}