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

Skip to content

Commit bbb208e

Browse files
authored
feat: Add CLI support for workspace build parameters (#5768)
* WIP * WIP * CLI: handle workspace build parameters * fix: golintci * Fix: dry run * fix * CLI: is mutable * coderd: mutable * fix: golanci * fix: richParameterFile * CLI: create unit tests * CLI: update test * Fix * fix: order * fix
1 parent 6a245ab commit bbb208e

23 files changed

+822
-257
lines changed

cli/cliui/parameter.go

+53
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,56 @@ func ParameterSchema(cmd *cobra.Command, parameterSchema codersdk.ParameterSchem
6060

6161
return value, nil
6262
}
63+
64+
func RichParameter(cmd *cobra.Command, templateVersionParameter codersdk.TemplateVersionParameter) (string, error) {
65+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), Styles.Bold.Render(templateVersionParameter.Name))
66+
if templateVersionParameter.Description != "" {
67+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+strings.TrimSpace(strings.Join(strings.Split(templateVersionParameter.Description, "\n"), "\n "))+"\n")
68+
}
69+
70+
// TODO Implement full validation and show descriptions.
71+
var err error
72+
var value string
73+
if len(templateVersionParameter.Options) > 0 {
74+
// Move the cursor up a single line for nicer display!
75+
_, _ = fmt.Fprint(cmd.OutOrStdout(), "\033[1A")
76+
value, err = Select(cmd, SelectOptions{
77+
Options: templateVersionParameterOptionValues(templateVersionParameter),
78+
Default: templateVersionParameter.DefaultValue,
79+
HideSearch: true,
80+
})
81+
if err == nil {
82+
_, _ = fmt.Fprintln(cmd.OutOrStdout())
83+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+Styles.Prompt.String()+Styles.Field.Render(value))
84+
}
85+
} else {
86+
text := "Enter a value"
87+
if templateVersionParameter.DefaultValue != "" {
88+
text += fmt.Sprintf(" (default: %q)", templateVersionParameter.DefaultValue)
89+
}
90+
text += ":"
91+
92+
value, err = Prompt(cmd, PromptOptions{
93+
Text: Styles.Bold.Render(text),
94+
})
95+
value = strings.TrimSpace(value)
96+
}
97+
if err != nil {
98+
return "", err
99+
}
100+
101+
// If they didn't specify anything, use the default value if set.
102+
if len(templateVersionParameter.Options) == 0 && value == "" {
103+
value = templateVersionParameter.DefaultValue
104+
}
105+
106+
return value, nil
107+
}
108+
109+
func templateVersionParameterOptionValues(param codersdk.TemplateVersionParameter) []string {
110+
var options []string
111+
for _, opt := range param.Options {
112+
options = append(options, opt.Value)
113+
}
114+
return options
115+
}

cli/create.go

+104-27
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@ import (
1717

1818
func create() *cobra.Command {
1919
var (
20-
parameterFile string
21-
templateName string
22-
startAt string
23-
stopAfter time.Duration
24-
workspaceName string
20+
parameterFile string
21+
richParameterFile string
22+
templateName string
23+
startAt string
24+
stopAfter time.Duration
25+
workspaceName string
2526
)
2627
cmd := &cobra.Command{
2728
Annotations: workspaceCommand,
@@ -121,11 +122,12 @@ func create() *cobra.Command {
121122
schedSpec = ptr.Ref(sched.String())
122123
}
123124

124-
parameters, err := prepWorkspaceBuild(cmd, client, prepWorkspaceBuildArgs{
125-
Template: template,
126-
ExistingParams: []codersdk.Parameter{},
127-
ParameterFile: parameterFile,
128-
NewWorkspaceName: workspaceName,
125+
buildParams, err := prepWorkspaceBuild(cmd, client, prepWorkspaceBuildArgs{
126+
Template: template,
127+
ExistingParams: []codersdk.Parameter{},
128+
ParameterFile: parameterFile,
129+
RichParameterFile: richParameterFile,
130+
NewWorkspaceName: workspaceName,
129131
})
130132
if err != nil {
131133
return err
@@ -140,11 +142,12 @@ func create() *cobra.Command {
140142
}
141143

142144
workspace, err := client.CreateWorkspace(cmd.Context(), organization.ID, codersdk.Me, codersdk.CreateWorkspaceRequest{
143-
TemplateID: template.ID,
144-
Name: workspaceName,
145-
AutostartSchedule: schedSpec,
146-
TTLMillis: ptr.Ref(stopAfter.Milliseconds()),
147-
ParameterValues: parameters,
145+
TemplateID: template.ID,
146+
Name: workspaceName,
147+
AutostartSchedule: schedSpec,
148+
TTLMillis: ptr.Ref(stopAfter.Milliseconds()),
149+
ParameterValues: buildParams.parameters,
150+
RichParameterValues: buildParams.richParameters,
148151
})
149152
if err != nil {
150153
return err
@@ -163,26 +166,40 @@ func create() *cobra.Command {
163166
cliui.AllowSkipPrompt(cmd)
164167
cliflag.StringVarP(cmd.Flags(), &templateName, "template", "t", "CODER_TEMPLATE_NAME", "", "Specify a template name.")
165168
cliflag.StringVarP(cmd.Flags(), &parameterFile, "parameter-file", "", "CODER_PARAMETER_FILE", "", "Specify a file path with parameter values.")
169+
cliflag.StringVarP(cmd.Flags(), &richParameterFile, "rich-parameter-file", "", "CODER_RICH_PARAMETER_FILE", "", "Specify a file path with values for rich parameters defined in the template.")
166170
cliflag.StringVarP(cmd.Flags(), &startAt, "start-at", "", "CODER_WORKSPACE_START_AT", "", "Specify the workspace autostart schedule. Check `coder schedule start --help` for the syntax.")
167171
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).")
168172
return cmd
169173
}
170174

171175
type prepWorkspaceBuildArgs struct {
172-
Template codersdk.Template
173-
ExistingParams []codersdk.Parameter
174-
ParameterFile string
175-
NewWorkspaceName string
176+
Template codersdk.Template
177+
ExistingParams []codersdk.Parameter
178+
ParameterFile string
179+
ExistingRichParams []codersdk.WorkspaceBuildParameter
180+
RichParameterFile string
181+
NewWorkspaceName string
182+
183+
UpdateWorkspace bool
184+
}
185+
186+
type buildParameters struct {
187+
// Parameters contains legacy parameters stored in /parameters.
188+
parameters []codersdk.CreateParameterRequest
189+
// Rich parameters stores values for build parameters annotated with description, icon, type, etc.
190+
richParameters []codersdk.WorkspaceBuildParameter
176191
}
177192

178193
// 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) {
194+
// Any missing params will be prompted to the user. It supports legacy and rich parameters.
195+
func prepWorkspaceBuild(cmd *cobra.Command, client *codersdk.Client, args prepWorkspaceBuildArgs) (*buildParameters, error) {
181196
ctx := cmd.Context()
182197
templateVersion, err := client.TemplateVersion(ctx, args.Template.ActiveVersionID)
183198
if err != nil {
184199
return nil, err
185200
}
201+
202+
// Legacy parameters
186203
parameterSchemas, err := client.TemplateVersionSchema(ctx, templateVersion.ID)
187204
if err != nil {
188205
return nil, err
@@ -200,7 +217,7 @@ func prepWorkspaceBuild(cmd *cobra.Command, client *codersdk.Client, args prepWo
200217
}
201218
}
202219
disclaimerPrinted := false
203-
parameters := make([]codersdk.CreateParameterRequest, 0)
220+
legacyParameters := make([]codersdk.CreateParameterRequest, 0)
204221
PromptParamLoop:
205222
for _, parameterSchema := range parameterSchemas {
206223
if !parameterSchema.AllowOverrideSource {
@@ -227,19 +244,76 @@ PromptParamLoop:
227244
return nil, err
228245
}
229246

230-
parameters = append(parameters, codersdk.CreateParameterRequest{
247+
legacyParameters = append(legacyParameters, codersdk.CreateParameterRequest{
231248
Name: parameterSchema.Name,
232249
SourceValue: parameterValue,
233250
SourceScheme: codersdk.ParameterSourceSchemeData,
234251
DestinationScheme: parameterSchema.DefaultDestinationScheme,
235252
})
236253
}
237-
_, _ = fmt.Fprintln(cmd.OutOrStdout())
254+
255+
if disclaimerPrinted {
256+
_, _ = fmt.Fprintln(cmd.OutOrStdout())
257+
}
258+
259+
// Rich parameters
260+
templateVersionParameters, err := client.TemplateVersionRichParameters(cmd.Context(), templateVersion.ID)
261+
if err != nil {
262+
return nil, xerrors.Errorf("get template version rich parameters: %w", err)
263+
}
264+
265+
parameterMapFromFile = map[string]string{}
266+
useParamFile = false
267+
if args.RichParameterFile != "" {
268+
useParamFile = true
269+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render("Attempting to read the variables from the rich parameter file.")+"\r\n")
270+
parameterMapFromFile, err = createParameterMapFromFile(args.RichParameterFile)
271+
if err != nil {
272+
return nil, err
273+
}
274+
}
275+
disclaimerPrinted = false
276+
richParameters := make([]codersdk.WorkspaceBuildParameter, 0)
277+
PromptRichParamLoop:
278+
for _, templateVersionParameter := range templateVersionParameters {
279+
if !disclaimerPrinted {
280+
_, _ = 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")
281+
disclaimerPrinted = true
282+
}
283+
284+
// Param file is all or nothing
285+
if !useParamFile {
286+
for _, e := range args.ExistingRichParams {
287+
if e.Name == templateVersionParameter.Name {
288+
// If the param already exists, we do not need to prompt it again.
289+
// The workspace scope will reuse params for each build.
290+
continue PromptRichParamLoop
291+
}
292+
}
293+
}
294+
295+
if args.UpdateWorkspace && !templateVersionParameter.Mutable {
296+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Warn.Render(fmt.Sprintf(`Parameter %q is not mutable, so can't be customized after workspace creation.`, templateVersionParameter.Name)))
297+
continue
298+
}
299+
300+
parameterValue, err := getWorkspaceBuildParameterValueFromMapOrInput(cmd, parameterMapFromFile, templateVersionParameter)
301+
if err != nil {
302+
return nil, err
303+
}
304+
305+
richParameters = append(richParameters, *parameterValue)
306+
}
307+
308+
if disclaimerPrinted {
309+
_, _ = fmt.Fprintln(cmd.OutOrStdout())
310+
}
238311

239312
// Run a dry-run with the given parameters to check correctness
240313
dryRun, err := client.CreateTemplateVersionDryRun(cmd.Context(), templateVersion.ID, codersdk.CreateTemplateVersionDryRunRequest{
241-
WorkspaceName: args.NewWorkspaceName,
242-
ParameterValues: parameters,
314+
WorkspaceName: args.NewWorkspaceName,
315+
ParameterValues: legacyParameters,
316+
RichParameterValues: richParameters,
243317
})
244318
if err != nil {
245319
return nil, xerrors.Errorf("begin workspace dry-run: %w", err)
@@ -279,5 +353,8 @@ PromptParamLoop:
279353
return nil, err
280354
}
281355

282-
return parameters, nil
356+
return &buildParameters{
357+
parameters: legacyParameters,
358+
richParameters: richParameters,
359+
}, nil
283360
}

cli/create_test.go

+118
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,124 @@ func TestCreate(t *testing.T) {
321321
})
322322
}
323323

324+
func TestCreateWithRichParameters(t *testing.T) {
325+
t.Parallel()
326+
327+
const (
328+
firstParameterName = "first_parameter"
329+
firstParameterDescription = "This is first parameter"
330+
firstParameterValue = "1"
331+
332+
secondParameterName = "second_parameter"
333+
secondParameterDescription = "This is second parameter"
334+
secondParameterValue = "2"
335+
336+
immutableParameterName = "third_parameter"
337+
immutableParameterDescription = "This is not mutable parameter"
338+
immutableParameterValue = "3"
339+
)
340+
341+
echoResponses := &echo.Responses{
342+
Parse: echo.ParseComplete,
343+
ProvisionPlan: []*proto.Provision_Response{
344+
{
345+
Type: &proto.Provision_Response_Complete{
346+
Complete: &proto.Provision_Complete{
347+
Parameters: []*proto.RichParameter{
348+
{Name: firstParameterName, Description: firstParameterDescription, Mutable: true},
349+
{Name: secondParameterName, Description: secondParameterDescription, Mutable: true},
350+
{Name: immutableParameterName, Description: immutableParameterDescription, Mutable: false},
351+
},
352+
},
353+
},
354+
}},
355+
ProvisionApply: []*proto.Provision_Response{{
356+
Type: &proto.Provision_Response_Complete{
357+
Complete: &proto.Provision_Complete{},
358+
},
359+
}},
360+
}
361+
362+
t.Run("InputParameters", 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+
cmd, root := clitest.New(t, "create", "my-workspace", "--template", template.Name)
373+
clitest.SetupConfig(t, client, root)
374+
doneChan := make(chan struct{})
375+
pty := ptytest.New(t)
376+
cmd.SetIn(pty.Input())
377+
cmd.SetOut(pty.Output())
378+
go func() {
379+
defer close(doneChan)
380+
err := cmd.Execute()
381+
assert.NoError(t, err)
382+
}()
383+
384+
matches := []string{
385+
firstParameterDescription, firstParameterValue,
386+
secondParameterDescription, secondParameterValue,
387+
immutableParameterDescription, immutableParameterValue,
388+
"Confirm create?", "yes",
389+
}
390+
for i := 0; i < len(matches); i += 2 {
391+
match := matches[i]
392+
value := matches[i+1]
393+
pty.ExpectMatch(match)
394+
pty.WriteLine(value)
395+
}
396+
<-doneChan
397+
})
398+
399+
t.Run("RichParametersFile", func(t *testing.T) {
400+
t.Parallel()
401+
402+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
403+
user := coderdtest.CreateFirstUser(t, client)
404+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
405+
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
406+
407+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
408+
409+
tempDir := t.TempDir()
410+
removeTmpDirUntilSuccessAfterTest(t, tempDir)
411+
parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml")
412+
_, _ = parameterFile.WriteString(
413+
firstParameterName + ": " + firstParameterValue + "\n" +
414+
secondParameterName + ": " + secondParameterValue + "\n" +
415+
immutableParameterName + ": " + immutableParameterValue)
416+
cmd, root := clitest.New(t, "create", "my-workspace", "--template", template.Name, "--rich-parameter-file", parameterFile.Name())
417+
clitest.SetupConfig(t, client, root)
418+
419+
doneChan := make(chan struct{})
420+
pty := ptytest.New(t)
421+
cmd.SetIn(pty.Input())
422+
cmd.SetOut(pty.Output())
423+
go func() {
424+
defer close(doneChan)
425+
err := cmd.Execute()
426+
assert.NoError(t, err)
427+
}()
428+
429+
matches := []string{
430+
"Confirm create?", "yes",
431+
}
432+
for i := 0; i < len(matches); i += 2 {
433+
match := matches[i]
434+
value := matches[i+1]
435+
pty.ExpectMatch(match)
436+
pty.WriteLine(value)
437+
}
438+
<-doneChan
439+
})
440+
}
441+
324442
func createTestParseResponseWithDefault(defaultValue string) []*proto.Parse_Response {
325443
return []*proto.Parse_Response{{
326444
Type: &proto.Parse_Response_Complete{

0 commit comments

Comments
 (0)