Skip to content

Commit 0d382d1

Browse files
authored
feat(cli): provide parameter values via command line (#8898)
1 parent 1730d35 commit 0d382d1

19 files changed

+682
-247
lines changed

cli/create.go

+50-103
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ import (
1818

1919
func (r *RootCmd) create() *clibase.Cmd {
2020
var (
21-
richParameterFile string
22-
templateName string
23-
startAt string
24-
stopAfter time.Duration
25-
workspaceName string
21+
templateName string
22+
startAt string
23+
stopAfter time.Duration
24+
workspaceName string
25+
26+
parameterFlags workspaceParameterFlags
2627
)
2728
client := new(codersdk.Client)
2829
cmd := &clibase.Cmd{
@@ -129,10 +130,18 @@ func (r *RootCmd) create() *clibase.Cmd {
129130
schedSpec = ptr.Ref(sched.String())
130131
}
131132

132-
buildParams, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
133-
Template: template,
134-
RichParameterFile: richParameterFile,
135-
NewWorkspaceName: workspaceName,
133+
cliRichParameters, err := asWorkspaceBuildParameters(parameterFlags.richParameters)
134+
if err != nil {
135+
return xerrors.Errorf("can't parse given parameter values: %w", err)
136+
}
137+
138+
richParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
139+
Action: WorkspaceCreate,
140+
Template: template,
141+
NewWorkspaceName: workspaceName,
142+
143+
RichParameterFile: parameterFlags.richParameterFile,
144+
RichParameters: cliRichParameters,
136145
})
137146
if err != nil {
138147
return xerrors.Errorf("prepare build: %w", err)
@@ -156,7 +165,7 @@ func (r *RootCmd) create() *clibase.Cmd {
156165
Name: workspaceName,
157166
AutostartSchedule: schedSpec,
158167
TTLMillis: ttlMillis,
159-
RichParameterValues: buildParams.richParameters,
168+
RichParameterValues: richParameters,
160169
})
161170
if err != nil {
162171
return xerrors.Errorf("create workspace: %w", err)
@@ -179,12 +188,6 @@ func (r *RootCmd) create() *clibase.Cmd {
179188
Description: "Specify a template name.",
180189
Value: clibase.StringOf(&templateName),
181190
},
182-
clibase.Option{
183-
Flag: "rich-parameter-file",
184-
Env: "CODER_RICH_PARAMETER_FILE",
185-
Description: "Specify a file path with values for rich parameters defined in the template.",
186-
Value: clibase.StringOf(&richParameterFile),
187-
},
188191
clibase.Option{
189192
Flag: "start-at",
190193
Env: "CODER_WORKSPACE_START_AT",
@@ -199,99 +202,59 @@ func (r *RootCmd) create() *clibase.Cmd {
199202
},
200203
cliui.SkipPromptOption(),
201204
)
205+
cmd.Options = append(cmd.Options, parameterFlags.cliParameters()...)
202206
return cmd
203207
}
204208

205209
type prepWorkspaceBuildArgs struct {
206-
Template codersdk.Template
207-
ExistingRichParams []codersdk.WorkspaceBuildParameter
208-
RichParameterFile string
209-
NewWorkspaceName string
210-
211-
UpdateWorkspace bool
212-
BuildOptions bool
213-
WorkspaceID uuid.UUID
214-
}
210+
Action WorkspaceCLIAction
211+
Template codersdk.Template
212+
NewWorkspaceName string
213+
WorkspaceID uuid.UUID
214+
215+
LastBuildParameters []codersdk.WorkspaceBuildParameter
216+
217+
PromptBuildOptions bool
218+
BuildOptions []codersdk.WorkspaceBuildParameter
215219

216-
type buildParameters struct {
217-
// Rich parameters stores values for build parameters annotated with description, icon, type, etc.
218-
richParameters []codersdk.WorkspaceBuildParameter
220+
PromptRichParameters bool
221+
RichParameters []codersdk.WorkspaceBuildParameter
222+
RichParameterFile string
219223
}
220224

221225
// prepWorkspaceBuild will ensure a workspace build will succeed on the latest template version.
222-
// Any missing params will be prompted to the user. It supports legacy and rich parameters.
223-
func prepWorkspaceBuild(inv *clibase.Invocation, client *codersdk.Client, args prepWorkspaceBuildArgs) (*buildParameters, error) {
226+
// Any missing params will be prompted to the user. It supports rich parameters.
227+
func prepWorkspaceBuild(inv *clibase.Invocation, client *codersdk.Client, args prepWorkspaceBuildArgs) ([]codersdk.WorkspaceBuildParameter, error) {
224228
ctx := inv.Context()
225229

226230
templateVersion, err := client.TemplateVersion(ctx, args.Template.ActiveVersionID)
227231
if err != nil {
228-
return nil, err
232+
return nil, xerrors.Errorf("get template version: %w", err)
229233
}
230234

231-
// Rich parameters
232235
templateVersionParameters, err := client.TemplateVersionRichParameters(inv.Context(), templateVersion.ID)
233236
if err != nil {
234237
return nil, xerrors.Errorf("get template version rich parameters: %w", err)
235238
}
236239

237-
parameterMapFromFile := map[string]string{}
238-
useParamFile := false
240+
parameterFile := map[string]string{}
239241
if args.RichParameterFile != "" {
240-
useParamFile = true
241-
_, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Paragraph.Render("Attempting to read the variables from the rich parameter file.")+"\r\n")
242-
parameterMapFromFile, err = createParameterMapFromFile(args.RichParameterFile)
243-
if err != nil {
244-
return nil, err
245-
}
246-
}
247-
disclaimerPrinted := false
248-
richParameters := make([]codersdk.WorkspaceBuildParameter, 0)
249-
PromptRichParamLoop:
250-
for _, templateVersionParameter := range templateVersionParameters {
251-
if !args.BuildOptions && templateVersionParameter.Ephemeral {
252-
continue
253-
}
254-
255-
if !disclaimerPrinted {
256-
_, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Paragraph.Render("This template has customizable parameters. Values can be changed after create, but may have unintended side effects (like data loss).")+"\r\n")
257-
disclaimerPrinted = true
258-
}
259-
260-
// Param file is all or nothing
261-
if !useParamFile && !templateVersionParameter.Ephemeral {
262-
for _, e := range args.ExistingRichParams {
263-
if e.Name == templateVersionParameter.Name {
264-
// If the param already exists, we do not need to prompt it again.
265-
// The workspace scope will reuse params for each build.
266-
continue PromptRichParamLoop
267-
}
268-
}
269-
}
270-
271-
if args.UpdateWorkspace && !templateVersionParameter.Mutable {
272-
// Check if the immutable parameter was used in the previous build. If so, then it isn't a fresh one
273-
// and the user should be warned.
274-
exists, err := workspaceBuildParameterExists(ctx, client, args.WorkspaceID, templateVersionParameter)
275-
if err != nil {
276-
return nil, err
277-
}
278-
279-
if exists {
280-
_, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Warn.Render(fmt.Sprintf(`Parameter %q is not mutable, so can't be customized after workspace creation.`, templateVersionParameter.Name)))
281-
continue
282-
}
283-
}
284-
285-
parameterValue, err := getWorkspaceBuildParameterValueFromMapOrInput(inv, parameterMapFromFile, templateVersionParameter)
242+
parameterFile, err = parseParameterMapFile(args.RichParameterFile)
286243
if err != nil {
287-
return nil, err
244+
return nil, xerrors.Errorf("can't parse parameter map file: %w", err)
288245
}
289-
290-
richParameters = append(richParameters, *parameterValue)
291246
}
292247

293-
if disclaimerPrinted {
294-
_, _ = fmt.Fprintln(inv.Stdout)
248+
resolver := new(ParameterResolver).
249+
WithLastBuildParameters(args.LastBuildParameters).
250+
WithPromptBuildOptions(args.PromptBuildOptions).
251+
WithBuildOptions(args.BuildOptions).
252+
WithPromptRichParameters(args.PromptRichParameters).
253+
WithRichParameters(args.RichParameters).
254+
WithRichParametersFile(parameterFile)
255+
buildParameters, err := resolver.Resolve(inv, args.Action, templateVersionParameters)
256+
if err != nil {
257+
return nil, err
295258
}
296259

297260
err = cliui.GitAuth(ctx, inv.Stdout, cliui.GitAuthOptions{
@@ -306,7 +269,7 @@ PromptRichParamLoop:
306269
// Run a dry-run with the given parameters to check correctness
307270
dryRun, err := client.CreateTemplateVersionDryRun(inv.Context(), templateVersion.ID, codersdk.CreateTemplateVersionDryRunRequest{
308271
WorkspaceName: args.NewWorkspaceName,
309-
RichParameterValues: richParameters,
272+
RichParameterValues: buildParameters,
310273
})
311274
if err != nil {
312275
return nil, xerrors.Errorf("begin workspace dry-run: %w", err)
@@ -346,21 +309,5 @@ PromptRichParamLoop:
346309
return nil, xerrors.Errorf("get resources: %w", err)
347310
}
348311

349-
return &buildParameters{
350-
richParameters: richParameters,
351-
}, nil
352-
}
353-
354-
func workspaceBuildParameterExists(ctx context.Context, client *codersdk.Client, workspaceID uuid.UUID, templateVersionParameter codersdk.TemplateVersionParameter) (bool, error) {
355-
lastBuildParameters, err := client.WorkspaceBuildParameters(ctx, workspaceID)
356-
if err != nil {
357-
return false, xerrors.Errorf("can't fetch last workspace build parameters: %w", err)
358-
}
359-
360-
for _, p := range lastBuildParameters {
361-
if p.Name == templateVersionParameter.Name {
362-
return true, nil
363-
}
364-
}
365-
return false, nil
312+
return buildParameters, nil
366313
}

cli/create_test.go

+36
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cli_test
22

33
import (
44
"context"
5+
"fmt"
56
"net/http"
67
"os"
78
"regexp"
@@ -357,6 +358,41 @@ func TestCreateWithRichParameters(t *testing.T) {
357358
}
358359
<-doneChan
359360
})
361+
362+
t.Run("ParameterFlags", func(t *testing.T) {
363+
t.Parallel()
364+
365+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
366+
user := coderdtest.CreateFirstUser(t, client)
367+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
368+
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
369+
370+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
371+
372+
inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name,
373+
"--parameter", fmt.Sprintf("%s=%s", firstParameterName, firstParameterValue),
374+
"--parameter", fmt.Sprintf("%s=%s", secondParameterName, secondParameterValue),
375+
"--parameter", fmt.Sprintf("%s=%s", immutableParameterName, immutableParameterValue))
376+
clitest.SetupConfig(t, client, root)
377+
doneChan := make(chan struct{})
378+
pty := ptytest.New(t).Attach(inv)
379+
go func() {
380+
defer close(doneChan)
381+
err := inv.Run()
382+
assert.NoError(t, err)
383+
}()
384+
385+
matches := []string{
386+
"Confirm create?", "yes",
387+
}
388+
for i := 0; i < len(matches); i += 2 {
389+
match := matches[i]
390+
value := matches[i+1]
391+
pty.ExpectMatch(match)
392+
pty.WriteLine(value)
393+
}
394+
<-doneChan
395+
})
360396
}
361397

362398
func TestCreateValidateRichParameters(t *testing.T) {

0 commit comments

Comments
 (0)