Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f41b50a

Browse files
authored
feat: Updating workspace prompts new parameters (coder#2598)
1 parent 407c47f commit f41b50a

File tree

7 files changed

+203
-91
lines changed

7 files changed

+203
-91
lines changed

cli/create.go

+121-81
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 agents 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

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
)
1212

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

cli/root.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func Root() *cobra.Command {
6969
cmd.AddCommand(
7070
configSSH(),
7171
create(),
72-
delete(),
72+
deleteWorkspace(),
7373
dotfiles(),
7474
gitssh(),
7575
list(),

cli/update.go

+32-2
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,17 @@ import (
66

77
"github.com/spf13/cobra"
88

9+
"github.com/coder/coder/cli/cliflag"
910
"github.com/coder/coder/codersdk"
1011
)
1112

1213
func update() *cobra.Command {
13-
return &cobra.Command{
14+
var (
15+
parameterFile string
16+
alwaysPrompt bool
17+
)
18+
19+
cmd := &cobra.Command{
1420
Annotations: workspaceCommand,
1521
Use: "update",
1622
Short: "Update a workspace to the latest template version",
@@ -23,18 +29,38 @@ func update() *cobra.Command {
2329
if err != nil {
2430
return err
2531
}
26-
if !workspace.Outdated {
32+
if !workspace.Outdated && !alwaysPrompt {
2733
_, _ = fmt.Printf("Workspace isn't outdated!\n")
2834
return nil
2935
}
3036
template, err := client.Template(cmd.Context(), workspace.TemplateID)
3137
if err != nil {
3238
return nil
3339
}
40+
41+
var existingParams []codersdk.Parameter
42+
if !alwaysPrompt {
43+
existingParams, err = client.Parameters(cmd.Context(), codersdk.ParameterWorkspace, workspace.ID)
44+
if err != nil {
45+
return nil
46+
}
47+
}
48+
49+
parameters, err := prepWorkspaceBuild(cmd, client, prepWorkspaceBuildArgs{
50+
Template: template,
51+
ExistingParams: existingParams,
52+
ParameterFile: parameterFile,
53+
NewWorkspaceName: workspace.Name,
54+
})
55+
if err != nil {
56+
return nil
57+
}
58+
3459
before := time.Now()
3560
build, err := client.CreateWorkspaceBuild(cmd.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
3661
TemplateVersionID: template.ActiveVersionID,
3762
Transition: workspace.LatestBuild.Transition,
63+
ParameterValues: parameters,
3864
})
3965
if err != nil {
4066
return err
@@ -53,4 +79,8 @@ func update() *cobra.Command {
5379
return nil
5480
},
5581
}
82+
83+
cmd.Flags().BoolVar(&alwaysPrompt, "always-prompt", false, "Always prompt all parameters. Does not pull parameter values from existing workspace")
84+
cliflag.StringVarP(cmd.Flags(), &parameterFile, "parameter-file", "", "CODER_PARAMETER_FILE", "", "Specify a file path with parameter values.")
85+
return cmd
5686
}

coderd/workspacebuilds.go

+37
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,43 @@ 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+
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
408+
return xerrors.Errorf("Fetch previous parameters: %w", err)
409+
}
410+
411+
// Write/Update any new params
412+
now := database.Now()
413+
for _, param := range createBuild.ParameterValues {
414+
for _, exists := range existing {
415+
// If the param exists, delete the old param before inserting the new one
416+
if exists.Name == param.Name {
417+
err = db.DeleteParameterValueByID(r.Context(), exists.ID)
418+
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
419+
return xerrors.Errorf("Failed to delete old param %q: %w", exists.Name, err)
420+
}
421+
}
422+
}
423+
424+
_, err = db.InsertParameterValue(r.Context(), database.InsertParameterValueParams{
425+
ID: uuid.New(),
426+
Name: param.Name,
427+
CreatedAt: now,
428+
UpdatedAt: now,
429+
Scope: database.ParameterScopeWorkspace,
430+
ScopeID: workspace.ID,
431+
SourceScheme: database.ParameterSourceScheme(param.SourceScheme),
432+
SourceValue: param.SourceValue,
433+
DestinationScheme: database.ParameterDestinationScheme(param.DestinationScheme),
434+
})
435+
if err != nil {
436+
return xerrors.Errorf("insert parameter value: %w", err)
437+
}
438+
}
439+
403440
workspaceBuildID := uuid.New()
404441
input, err := json.Marshal(workspaceProvisionJob{
405442
WorkspaceBuildID: workspaceBuildID,

codersdk/workspaces.go

+4
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ 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+
// This will overwrite any existing parameters with the same name.
42+
// This will not delete old params not included in this list.
43+
ParameterValues []CreateParameterRequest `json:"parameter_values,omitempty"`
4044
}
4145

4246
type WorkspaceOptions struct {

0 commit comments

Comments
 (0)