Skip to content

Commit 99c8c9c

Browse files
committed
feat: Updating workspace prompts new parameters
1 parent 69b7eed commit 99c8c9c

File tree

6 files changed

+188
-84
lines changed

6 files changed

+188
-84
lines changed

cli/create.go

Lines changed: 121 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -120,87 +120,11 @@ func create() *cobra.Command {
120120
schedSpec = ptr.Ref(sched.String())
121121
}
122122

123-
templateVersion, err := client.TemplateVersion(cmd.Context(), template.ActiveVersionID)
124-
if err != nil {
125-
return err
126-
}
127-
parameterSchemas, err := client.TemplateVersionSchema(cmd.Context(), templateVersion.ID)
128-
if err != nil {
129-
return err
130-
}
131-
132-
// parameterMapFromFile can be nil if parameter file is not specified
133-
var parameterMapFromFile map[string]string
134-
if parameterFile != "" {
135-
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render("Attempting to read the variables from the parameter file.")+"\r\n")
136-
parameterMapFromFile, err = createParameterMapFromFile(parameterFile)
137-
if err != nil {
138-
return err
139-
}
140-
}
141-
142-
disclaimerPrinted := false
143-
parameters := make([]codersdk.CreateParameterRequest, 0)
144-
for _, parameterSchema := range parameterSchemas {
145-
if !parameterSchema.AllowOverrideSource {
146-
continue
147-
}
148-
if !disclaimerPrinted {
149-
_, _ = 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")
150-
disclaimerPrinted = true
151-
}
152-
parameterValue, err := getParameterValueFromMapOrInput(cmd, parameterMapFromFile, parameterSchema)
153-
if err != nil {
154-
return err
155-
}
156-
parameters = append(parameters, codersdk.CreateParameterRequest{
157-
Name: parameterSchema.Name,
158-
SourceValue: parameterValue,
159-
SourceScheme: codersdk.ParameterSourceSchemeData,
160-
DestinationScheme: parameterSchema.DefaultDestinationScheme,
161-
})
162-
}
163-
_, _ = fmt.Fprintln(cmd.OutOrStdout())
164-
165-
// Run a dry-run with the given parameters to check correctness
166-
after := time.Now()
167-
dryRun, err := client.CreateTemplateVersionDryRun(cmd.Context(), templateVersion.ID, codersdk.CreateTemplateVersionDryRunRequest{
168-
WorkspaceName: workspaceName,
169-
ParameterValues: parameters,
170-
})
171-
if err != nil {
172-
return xerrors.Errorf("begin workspace dry-run: %w", err)
173-
}
174-
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "Planning workspace...")
175-
err = cliui.ProvisionerJob(cmd.Context(), cmd.OutOrStdout(), cliui.ProvisionerJobOptions{
176-
Fetch: func() (codersdk.ProvisionerJob, error) {
177-
return client.TemplateVersionDryRun(cmd.Context(), templateVersion.ID, dryRun.ID)
178-
},
179-
Cancel: func() error {
180-
return client.CancelTemplateVersionDryRun(cmd.Context(), templateVersion.ID, dryRun.ID)
181-
},
182-
Logs: func() (<-chan codersdk.ProvisionerJobLog, error) {
183-
return client.TemplateVersionDryRunLogsAfter(cmd.Context(), templateVersion.ID, dryRun.ID, after)
184-
},
185-
// Don't show log output for the dry-run unless there's an error.
186-
Silent: true,
187-
})
188-
if err != nil {
189-
// TODO (Dean): reprompt for parameter values if we deem it to
190-
// be a validation error
191-
return xerrors.Errorf("dry-run workspace: %w", err)
192-
}
193-
194-
resources, err := client.TemplateVersionDryRunResources(cmd.Context(), templateVersion.ID, dryRun.ID)
195-
if err != nil {
196-
return xerrors.Errorf("get workspace dry-run resources: %w", err)
197-
}
198-
199-
err = cliui.WorkspaceResources(cmd.OutOrStdout(), resources, cliui.WorkspaceResourcesOptions{
200-
WorkspaceName: workspaceName,
201-
// Since agent's haven't connected yet, hiding this makes more sense.
202-
HideAgentState: true,
203-
Title: "Workspace Preview",
123+
parameters, err := prepWorkspaceBuild(cmd, client, prepWorkspaceBuildArgs{
124+
Template: template,
125+
ExistingParams: []codersdk.Parameter{},
126+
ParameterFile: parameterFile,
127+
NewWorkspaceName: workspaceName,
204128
})
205129
if err != nil {
206130
return err
@@ -214,6 +138,7 @@ func create() *cobra.Command {
214138
return err
215139
}
216140

141+
after := time.Now()
217142
workspace, err := client.CreateWorkspace(cmd.Context(), organization.ID, codersdk.CreateWorkspaceRequest{
218143
TemplateID: template.ID,
219144
Name: workspaceName,
@@ -242,3 +167,118 @@ func create() *cobra.Command {
242167
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).")
243168
return cmd
244169
}
170+
171+
type prepWorkspaceBuildArgs struct {
172+
Template codersdk.Template
173+
ExistingParams []codersdk.Parameter
174+
ParameterFile string
175+
NewWorkspaceName string
176+
}
177+
178+
// prepWorkspaceBuild will ensure a workspace build will succeed on the latest template version.
179+
// Any missing params will be prompted to the user.
180+
func prepWorkspaceBuild(cmd *cobra.Command, client *codersdk.Client, args prepWorkspaceBuildArgs) ([]codersdk.CreateParameterRequest, error) {
181+
ctx := cmd.Context()
182+
templateVersion, err := client.TemplateVersion(ctx, args.Template.ActiveVersionID)
183+
if err != nil {
184+
return nil, err
185+
}
186+
parameterSchemas, err := client.TemplateVersionSchema(ctx, templateVersion.ID)
187+
if err != nil {
188+
return nil, err
189+
}
190+
191+
// parameterMapFromFile can be nil if parameter file is not specified
192+
var parameterMapFromFile map[string]string
193+
useParamFile := false
194+
if args.ParameterFile != "" {
195+
useParamFile = true
196+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render("Attempting to read the variables from the parameter file.")+"\r\n")
197+
parameterMapFromFile, err = createParameterMapFromFile(args.ParameterFile)
198+
if err != nil {
199+
return nil, err
200+
}
201+
}
202+
disclaimerPrinted := false
203+
parameters := make([]codersdk.CreateParameterRequest, 0)
204+
PromptParamLoop:
205+
for _, parameterSchema := range parameterSchemas {
206+
if !parameterSchema.AllowOverrideSource {
207+
continue
208+
}
209+
if !disclaimerPrinted {
210+
_, _ = 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")
211+
disclaimerPrinted = true
212+
}
213+
214+
// Param file is all or nothing
215+
if !useParamFile {
216+
for _, e := range args.ExistingParams {
217+
if e.Name == parameterSchema.Name {
218+
// If the param already exists, we do not need to prompt it again.
219+
// The workspace scope will reuse params for each build.
220+
continue PromptParamLoop
221+
}
222+
}
223+
}
224+
225+
parameterValue, err := getParameterValueFromMapOrInput(cmd, parameterMapFromFile, parameterSchema)
226+
if err != nil {
227+
return nil, err
228+
}
229+
230+
parameters = append(parameters, codersdk.CreateParameterRequest{
231+
Name: parameterSchema.Name,
232+
SourceValue: parameterValue,
233+
SourceScheme: codersdk.ParameterSourceSchemeData,
234+
DestinationScheme: parameterSchema.DefaultDestinationScheme,
235+
})
236+
}
237+
_, _ = fmt.Fprintln(cmd.OutOrStdout())
238+
239+
// Run a dry-run with the given parameters to check correctness
240+
after := time.Now()
241+
dryRun, err := client.CreateTemplateVersionDryRun(cmd.Context(), templateVersion.ID, codersdk.CreateTemplateVersionDryRunRequest{
242+
WorkspaceName: args.NewWorkspaceName,
243+
ParameterValues: parameters,
244+
})
245+
if err != nil {
246+
return nil, xerrors.Errorf("begin workspace dry-run: %w", err)
247+
}
248+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "Planning workspace...")
249+
err = cliui.ProvisionerJob(cmd.Context(), cmd.OutOrStdout(), cliui.ProvisionerJobOptions{
250+
Fetch: func() (codersdk.ProvisionerJob, error) {
251+
return client.TemplateVersionDryRun(cmd.Context(), templateVersion.ID, dryRun.ID)
252+
},
253+
Cancel: func() error {
254+
return client.CancelTemplateVersionDryRun(cmd.Context(), templateVersion.ID, dryRun.ID)
255+
},
256+
Logs: func() (<-chan codersdk.ProvisionerJobLog, error) {
257+
return client.TemplateVersionDryRunLogsAfter(cmd.Context(), templateVersion.ID, dryRun.ID, after)
258+
},
259+
// Don't show log output for the dry-run unless there's an error.
260+
Silent: true,
261+
})
262+
if err != nil {
263+
// TODO (Dean): reprompt for parameter values if we deem it to
264+
// be a validation error
265+
return nil, xerrors.Errorf("dry-run workspace: %w", err)
266+
}
267+
268+
resources, err := client.TemplateVersionDryRunResources(cmd.Context(), templateVersion.ID, dryRun.ID)
269+
if err != nil {
270+
return nil, xerrors.Errorf("get workspace dry-run resources: %w", err)
271+
}
272+
273+
err = cliui.WorkspaceResources(cmd.OutOrStdout(), resources, cliui.WorkspaceResourcesOptions{
274+
WorkspaceName: args.NewWorkspaceName,
275+
// Since agent's haven't connected yet, hiding this makes more sense.
276+
HideAgentState: true,
277+
Title: "Workspace Preview",
278+
})
279+
if err != nil {
280+
return nil, err
281+
}
282+
283+
return parameters, nil
284+
}

cli/delete.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
)
1111

1212
// nolint
13-
func delete() *cobra.Command {
13+
func deleteWorkspace() *cobra.Command {
1414
cmd := &cobra.Command{
1515
Annotations: workspaceCommand,
1616
Use: "delete <workspace>",

cli/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func Root() *cobra.Command {
6767
cmd.AddCommand(
6868
configSSH(),
6969
create(),
70-
delete(),
70+
deleteWorkspace(),
7171
dotfiles(),
7272
gitssh(),
7373
list(),

cli/update.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,20 @@ import (
44
"fmt"
55
"time"
66

7+
"github.com/coder/coder/cli/cliflag"
8+
79
"github.com/spf13/cobra"
810

911
"github.com/coder/coder/codersdk"
1012
)
1113

1214
func update() *cobra.Command {
13-
return &cobra.Command{
15+
var (
16+
parameterFile string
17+
alwaysPrompt bool
18+
)
19+
20+
cmd := &cobra.Command{
1421
Annotations: workspaceCommand,
1522
Use: "update",
1623
Short: "Update a workspace to the latest template version",
@@ -31,10 +38,27 @@ func update() *cobra.Command {
3138
if err != nil {
3239
return nil
3340
}
41+
42+
var existingParams []codersdk.Parameter
43+
if !alwaysPrompt {
44+
existingParams, err = client.Parameters(cmd.Context(), codersdk.ParameterWorkspace, workspace.ID)
45+
if err != nil {
46+
return nil
47+
}
48+
}
49+
50+
parameters, err := prepWorkspaceBuild(cmd, client, prepWorkspaceBuildArgs{
51+
Template: template,
52+
ExistingParams: existingParams,
53+
ParameterFile: parameterFile,
54+
NewWorkspaceName: workspace.Name,
55+
})
56+
3457
before := time.Now()
3558
build, err := client.CreateWorkspaceBuild(cmd.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
3659
TemplateVersionID: template.ActiveVersionID,
3760
Transition: workspace.LatestBuild.Transition,
61+
ParameterValues: parameters,
3862
})
3963
if err != nil {
4064
return err
@@ -53,4 +77,8 @@ func update() *cobra.Command {
5377
return nil
5478
},
5579
}
80+
81+
cmd.Flags().BoolVar(&alwaysPrompt, "always-prompt", false, "Always prompt all parameters. Does not pull parameter values from existing workspace")
82+
cliflag.StringVarP(cmd.Flags(), &parameterFile, "parameter-file", "", "CODER_PARAMETER_FILE", "", "Specify a file path with parameter values.")
83+
return cmd
5684
}

coderd/workspacebuilds.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,40 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
400400
// This must happen in a transaction to ensure history can be inserted, and
401401
// the prior history can update it's "after" column to point at the new.
402402
err = api.Database.InTx(func(db database.Store) error {
403+
existing, err := db.ParameterValues(r.Context(), database.ParameterValuesParams{
404+
Scopes: []database.ParameterScope{database.ParameterScopeWorkspace},
405+
ScopeIds: []uuid.UUID{workspace.ID},
406+
})
407+
408+
// Write/Update any new params
409+
now := database.Now()
410+
for _, param := range createBuild.ParameterValues {
411+
for _, exists := range existing {
412+
// If the param exists, delete the old param before inserting the new one
413+
if exists.Name == param.Name {
414+
err = db.DeleteParameterValueByID(r.Context(), exists.ID)
415+
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
416+
return xerrors.Errorf("Failed to delete old param %q: %w", exists.Name, err)
417+
}
418+
}
419+
}
420+
421+
_, err = db.InsertParameterValue(r.Context(), database.InsertParameterValueParams{
422+
ID: uuid.New(),
423+
Name: param.Name,
424+
CreatedAt: now,
425+
UpdatedAt: now,
426+
Scope: database.ParameterScopeWorkspace,
427+
ScopeID: workspace.ID,
428+
SourceScheme: database.ParameterSourceScheme(param.SourceScheme),
429+
SourceValue: param.SourceValue,
430+
DestinationScheme: database.ParameterDestinationScheme(param.DestinationScheme),
431+
})
432+
if err != nil {
433+
return xerrors.Errorf("insert parameter value: %w", err)
434+
}
435+
}
436+
403437
workspaceBuildID := uuid.New()
404438
input, err := json.Marshal(workspaceProvisionJob{
405439
WorkspaceBuildID: workspaceBuildID,

codersdk/workspaces.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ type CreateWorkspaceBuildRequest struct {
3737
Transition WorkspaceTransition `json:"transition" validate:"oneof=create start stop delete,required"`
3838
DryRun bool `json:"dry_run,omitempty"`
3939
ProvisionerState []byte `json:"state,omitempty"`
40+
// ParameterValues are optional. It will write params to the 'workspace' scope.
41+
ParameterValues []CreateParameterRequest `json:"parameter_values,omitempty"`
4042
}
4143

4244
type WorkspaceOptions struct {

0 commit comments

Comments
 (0)