Skip to content

Commit 5432c3f

Browse files
authored
feat(cli): support ephemeral parameters (#8415)
1 parent cdf9b90 commit 5432c3f

18 files changed

+507
-21
lines changed

cli/cliui/parameter.go

+5
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.Te
1515
label = templateVersionParameter.DisplayName
1616
}
1717

18+
if templateVersionParameter.Ephemeral {
19+
label += DefaultStyles.Warn.Render(" (build option)")
20+
}
21+
1822
_, _ = fmt.Fprintln(inv.Stdout, DefaultStyles.Bold.Render(label))
23+
1924
if templateVersionParameter.DescriptionPlaintext != "" {
2025
_, _ = fmt.Fprintln(inv.Stdout, " "+strings.TrimSpace(strings.Join(strings.Split(templateVersionParameter.DescriptionPlaintext, "\n"), "\n "))+"\n")
2126
}

cli/create.go

+10-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ func (r *RootCmd) create() *clibase.Cmd {
2323
startAt string
2424
stopAfter time.Duration
2525
workspaceName string
26+
27+
parameterFlags workspaceParameterFlags
2628
)
2729
client := new(codersdk.Client)
2830
cmd := &clibase.Cmd{
@@ -123,6 +125,7 @@ func (r *RootCmd) create() *clibase.Cmd {
123125
Template: template,
124126
RichParameterFile: richParameterFile,
125127
NewWorkspaceName: workspaceName,
128+
BuildOptions: parameterFlags.buildOptions,
126129
})
127130
if err != nil {
128131
return xerrors.Errorf("prepare build: %w", err)
@@ -191,6 +194,7 @@ func (r *RootCmd) create() *clibase.Cmd {
191194
},
192195
cliui.SkipPromptOption(),
193196
)
197+
cmd.Options = append(cmd.Options, parameterFlags.options()...)
194198

195199
return cmd
196200
}
@@ -202,6 +206,7 @@ type prepWorkspaceBuildArgs struct {
202206
NewWorkspaceName string
203207

204208
UpdateWorkspace bool
209+
BuildOptions bool
205210
WorkspaceID uuid.UUID
206211
}
207212

@@ -240,13 +245,17 @@ func prepWorkspaceBuild(inv *clibase.Invocation, client *codersdk.Client, args p
240245
richParameters := make([]codersdk.WorkspaceBuildParameter, 0)
241246
PromptRichParamLoop:
242247
for _, templateVersionParameter := range templateVersionParameters {
248+
if !args.BuildOptions && templateVersionParameter.Ephemeral {
249+
continue
250+
}
251+
243252
if !disclaimerPrinted {
244253
_, _ = 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")
245254
disclaimerPrinted = true
246255
}
247256

248257
// Param file is all or nothing
249-
if !useParamFile {
258+
if !useParamFile && !templateVersionParameter.Ephemeral {
250259
for _, e := range args.ExistingRichParams {
251260
if e.Name == templateVersionParameter.Name {
252261
// If the param already exists, we do not need to prompt it again.

cli/create_test.go

+60-1
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,13 @@ func TestCreateWithRichParameters(t *testing.T) {
195195
secondParameterDescription = "This is second parameter"
196196
secondParameterValue = "2"
197197

198+
ephemeralParameterName = "ephemeral_parameter"
199+
ephemeralParameterDescription = "This is ephemeral parameter"
200+
ephemeralParameterValue = "3"
201+
198202
immutableParameterName = "third_parameter"
199203
immutableParameterDescription = "This is not mutable parameter"
200-
immutableParameterValue = "3"
204+
immutableParameterValue = "4"
201205
)
202206

203207
echoResponses := &echo.Responses{
@@ -209,6 +213,7 @@ func TestCreateWithRichParameters(t *testing.T) {
209213
Parameters: []*proto.RichParameter{
210214
{Name: firstParameterName, Description: firstParameterDescription, Mutable: true},
211215
{Name: secondParameterName, DisplayName: secondParameterDisplayName, Description: secondParameterDescription, Mutable: true},
216+
{Name: ephemeralParameterName, Description: ephemeralParameterDescription, Mutable: true, Ephemeral: true},
212217
{Name: immutableParameterName, Description: immutableParameterDescription, Mutable: false},
213218
},
214219
},
@@ -300,6 +305,60 @@ func TestCreateWithRichParameters(t *testing.T) {
300305
}
301306
<-doneChan
302307
})
308+
309+
t.Run("BuildOptions", func(t *testing.T) {
310+
t.Parallel()
311+
312+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
313+
user := coderdtest.CreateFirstUser(t, client)
314+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
315+
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
316+
317+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
318+
319+
const workspaceName = "my-workspace"
320+
inv, root := clitest.New(t, "create", workspaceName, "--template", template.Name, "--build-options")
321+
clitest.SetupConfig(t, client, root)
322+
doneChan := make(chan struct{})
323+
pty := ptytest.New(t).Attach(inv)
324+
go func() {
325+
defer close(doneChan)
326+
err := inv.Run()
327+
assert.NoError(t, err)
328+
}()
329+
330+
matches := []string{
331+
ephemeralParameterDescription, ephemeralParameterValue,
332+
firstParameterDescription, firstParameterValue,
333+
secondParameterDisplayName, "",
334+
secondParameterDescription, secondParameterValue,
335+
immutableParameterDescription, immutableParameterValue,
336+
"Confirm create?", "yes",
337+
}
338+
for i := 0; i < len(matches); i += 2 {
339+
match := matches[i]
340+
value := matches[i+1]
341+
pty.ExpectMatch(match)
342+
343+
if value != "" {
344+
pty.WriteLine(value)
345+
}
346+
}
347+
<-doneChan
348+
349+
// Verify if build option is set
350+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
351+
defer cancel()
352+
353+
workspace, err := client.WorkspaceByOwnerAndName(ctx, user.UserID.String(), workspaceName, codersdk.WorkspaceOptions{})
354+
require.NoError(t, err)
355+
actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
356+
require.NoError(t, err)
357+
require.Contains(t, actualParameters, codersdk.WorkspaceBuildParameter{
358+
Name: ephemeralParameterName,
359+
Value: ephemeralParameterValue,
360+
})
361+
})
303362
}
304363

305364
func TestCreateValidateRichParameters(t *testing.T) {

cli/restart.go

+22-8
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
)
1111

1212
func (r *RootCmd) restart() *clibase.Cmd {
13+
var parameterFlags workspaceParameterFlags
14+
1315
client := new(codersdk.Client)
1416
cmd := &clibase.Cmd{
1517
Annotations: workspaceCommand,
@@ -19,22 +21,33 @@ func (r *RootCmd) restart() *clibase.Cmd {
1921
clibase.RequireNArgs(1),
2022
r.InitClient(client),
2123
),
22-
Options: clibase.OptionSet{
23-
cliui.SkipPromptOption(),
24-
},
24+
Options: append(parameterFlags.options(), cliui.SkipPromptOption()),
2525
Handler: func(inv *clibase.Invocation) error {
2626
ctx := inv.Context()
2727
out := inv.Stdout
2828

29-
_, err := cliui.Prompt(inv, cliui.PromptOptions{
30-
Text: "Confirm restart workspace?",
31-
IsConfirm: true,
29+
workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0])
30+
if err != nil {
31+
return err
32+
}
33+
34+
template, err := client.Template(inv.Context(), workspace.TemplateID)
35+
if err != nil {
36+
return err
37+
}
38+
39+
buildParams, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{
40+
Template: template,
41+
BuildOptions: parameterFlags.buildOptions,
3242
})
3343
if err != nil {
3444
return err
3545
}
3646

37-
workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0])
47+
_, err = cliui.Prompt(inv, cliui.PromptOptions{
48+
Text: "Confirm restart workspace?",
49+
IsConfirm: true,
50+
})
3851
if err != nil {
3952
return err
4053
}
@@ -51,7 +64,8 @@ func (r *RootCmd) restart() *clibase.Cmd {
5164
}
5265

5366
build, err = client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
54-
Transition: codersdk.WorkspaceTransitionStart,
67+
Transition: codersdk.WorkspaceTransitionStart,
68+
RichParameterValues: buildParams.richParameters,
5569
})
5670
if err != nil {
5771
return err

cli/restart_test.go

+83
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,49 @@
11
package cli_test
22

33
import (
4+
"context"
45
"testing"
56

7+
"github.com/stretchr/testify/assert"
68
"github.com/stretchr/testify/require"
79

810
"github.com/coder/coder/cli/clitest"
911
"github.com/coder/coder/coderd/coderdtest"
12+
"github.com/coder/coder/codersdk"
13+
"github.com/coder/coder/provisioner/echo"
14+
"github.com/coder/coder/provisionersdk/proto"
1015
"github.com/coder/coder/pty/ptytest"
1116
"github.com/coder/coder/testutil"
1217
)
1318

1419
func TestRestart(t *testing.T) {
1520
t.Parallel()
1621

22+
echoResponses := &echo.Responses{
23+
Parse: echo.ParseComplete,
24+
ProvisionPlan: []*proto.Provision_Response{
25+
{
26+
Type: &proto.Provision_Response_Complete{
27+
Complete: &proto.Provision_Complete{
28+
Parameters: []*proto.RichParameter{
29+
{
30+
Name: ephemeralParameterName,
31+
Description: ephemeralParameterDescription,
32+
Mutable: true,
33+
Ephemeral: true,
34+
},
35+
},
36+
},
37+
},
38+
},
39+
},
40+
ProvisionApply: []*proto.Provision_Response{{
41+
Type: &proto.Provision_Response_Complete{
42+
Complete: &proto.Provision_Complete{},
43+
},
44+
}},
45+
}
46+
1747
t.Run("OK", func(t *testing.T) {
1848
t.Parallel()
1949

@@ -43,4 +73,57 @@ func TestRestart(t *testing.T) {
4373
err := <-done
4474
require.NoError(t, err, "execute failed")
4575
})
76+
77+
t.Run("BuildOptions", func(t *testing.T) {
78+
t.Parallel()
79+
80+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
81+
user := coderdtest.CreateFirstUser(t, client)
82+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
83+
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
84+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
85+
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
86+
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
87+
88+
inv, root := clitest.New(t, "restart", workspace.Name, "--build-options")
89+
clitest.SetupConfig(t, client, root)
90+
doneChan := make(chan struct{})
91+
pty := ptytest.New(t).Attach(inv)
92+
go func() {
93+
defer close(doneChan)
94+
err := inv.Run()
95+
assert.NoError(t, err)
96+
}()
97+
98+
matches := []string{
99+
ephemeralParameterDescription, ephemeralParameterValue,
100+
"Confirm restart workspace?", "yes",
101+
"Stopping workspace", "",
102+
"Starting workspace", "",
103+
"workspace has been restarted", "",
104+
}
105+
for i := 0; i < len(matches); i += 2 {
106+
match := matches[i]
107+
value := matches[i+1]
108+
pty.ExpectMatch(match)
109+
110+
if value != "" {
111+
pty.WriteLine(value)
112+
}
113+
}
114+
<-doneChan
115+
116+
// Verify if build option is set
117+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
118+
defer cancel()
119+
120+
workspace, err := client.WorkspaceByOwnerAndName(ctx, user.UserID.String(), workspace.Name, codersdk.WorkspaceOptions{})
121+
require.NoError(t, err)
122+
actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
123+
require.NoError(t, err)
124+
require.Contains(t, actualParameters, codersdk.WorkspaceBuildParameter{
125+
Name: ephemeralParameterName,
126+
Value: ephemeralParameterValue,
127+
})
128+
})
46129
}

0 commit comments

Comments
 (0)