Skip to content

Commit 4839098

Browse files
committed
feat: add cli support for workspace automatic updates
1 parent 0c993ea commit 4839098

22 files changed

+587
-204
lines changed

cli/autoupdate.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"golang.org/x/xerrors"
8+
9+
"github.com/coder/coder/v2/cli/clibase"
10+
"github.com/coder/coder/v2/cli/cliui"
11+
"github.com/coder/coder/v2/codersdk"
12+
)
13+
14+
func (r *RootCmd) autoupdate() *clibase.Cmd {
15+
client := new(codersdk.Client)
16+
cmd := &clibase.Cmd{
17+
Annotations: workspaceCommand,
18+
Use: "autoupdate <workspace> <always|never>",
19+
Short: "Toggle auto-update policy for a workspace",
20+
Middleware: clibase.Chain(
21+
clibase.RequireNArgs(2),
22+
r.InitClient(client),
23+
),
24+
Handler: func(inv *clibase.Invocation) error {
25+
policy := strings.ToLower(inv.Args[1])
26+
err := validateAutoUpdatePolicy(policy)
27+
if err != nil {
28+
return xerrors.Errorf("validate policy: %w", err)
29+
}
30+
31+
workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0])
32+
if err != nil {
33+
return xerrors.Errorf("get workspace: %w", err)
34+
}
35+
36+
err = client.UpdateWorkspaceAutomaticUpdates(inv.Context(), workspace.ID, codersdk.UpdateWorkspaceAutomaticUpdatesRequest{
37+
AutomaticUpdates: codersdk.AutomaticUpdates(policy),
38+
})
39+
if err != nil {
40+
return xerrors.Errorf("update workspace automatic updates policy: %w", err)
41+
}
42+
_, _ = fmt.Fprintf(inv.Stdout, "Updated workspace %q auto-update policy to %q\n", workspace.Name, policy)
43+
return nil
44+
},
45+
}
46+
47+
cmd.Options = append(cmd.Options, cliui.SkipPromptOption())
48+
return cmd
49+
}
50+
51+
func validateAutoUpdatePolicy(arg string) error {
52+
switch codersdk.AutomaticUpdates(arg) {
53+
case codersdk.AutomaticUpdatesAlways, codersdk.AutomaticUpdatesNever:
54+
return nil
55+
default:
56+
return xerrors.Errorf("invalid option %q must be either of %q or %q", arg, codersdk.AutomaticUpdatesAlways, codersdk.AutomaticUpdatesNever)
57+
}
58+
}

cli/autoupdate_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package cli_test
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/coder/coder/v2/cli/clitest"
11+
"github.com/coder/coder/v2/coderd/coderdtest"
12+
"github.com/coder/coder/v2/codersdk"
13+
)
14+
15+
func TestAutoUpdate(t *testing.T) {
16+
t.Parallel()
17+
18+
t.Run("OK", func(t *testing.T) {
19+
t.Parallel()
20+
21+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
22+
owner := coderdtest.CreateFirstUser(t, client)
23+
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
24+
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
25+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
26+
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
27+
workspace := coderdtest.CreateWorkspace(t, member, owner.OrganizationID, template.ID)
28+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
29+
require.Equal(t, codersdk.AutomaticUpdatesNever, workspace.AutomaticUpdates)
30+
31+
expectedPolicy := codersdk.AutomaticUpdatesAlways
32+
inv, root := clitest.New(t, "autoupdate", workspace.Name, string(expectedPolicy))
33+
clitest.SetupConfig(t, member, root)
34+
var buf bytes.Buffer
35+
inv.Stdout = &buf
36+
err := inv.Run()
37+
require.NoError(t, err)
38+
require.Contains(t, buf.String(), fmt.Sprintf("Updated workspace %q auto-update policy to %q", workspace.Name, expectedPolicy))
39+
40+
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
41+
require.Equal(t, expectedPolicy, workspace.AutomaticUpdates)
42+
})
43+
44+
t.Run("InvalidArgs", func(t *testing.T) {
45+
type testcase struct {
46+
Name string
47+
Args []string
48+
ErrorContains string
49+
}
50+
51+
cases := []testcase{
52+
{
53+
Name: "NoPolicy",
54+
Args: []string{"autoupdate", "ws"},
55+
ErrorContains: "wanted 2 args but got 1",
56+
},
57+
{
58+
Name: "InvalidPolicy",
59+
Args: []string{"autoupdate", "ws", "sometimes"},
60+
ErrorContains: fmt.Sprintf("invalid option %q must be either of", "sometimes"),
61+
},
62+
}
63+
64+
for _, c := range cases {
65+
c := c
66+
t.Run(c.Name, func(t *testing.T) {
67+
t.Parallel()
68+
client := coderdtest.New(t, nil)
69+
_ = coderdtest.CreateFirstUser(t, client)
70+
71+
inv, root := clitest.New(t, c.Args...)
72+
clitest.SetupConfig(t, client, root)
73+
err := inv.Run()
74+
require.Error(t, err)
75+
require.Contains(t, err.Error(), c.ErrorContains)
76+
})
77+
}
78+
})
79+
}

cli/create.go

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,9 @@ func (r *RootCmd) create() *clibase.Cmd {
140140
}
141141

142142
richParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
143-
Action: WorkspaceCreate,
144-
Template: template,
145-
NewWorkspaceName: workspaceName,
143+
Action: WorkspaceCreate,
144+
TemplateVersionID: template.ActiveVersionID,
145+
NewWorkspaceName: workspaceName,
146146

147147
RichParameterFile: parameterFlags.richParameterFile,
148148
RichParameters: cliRichParameters,
@@ -224,10 +224,9 @@ func (r *RootCmd) create() *clibase.Cmd {
224224
}
225225

226226
type prepWorkspaceBuildArgs struct {
227-
Action WorkspaceCLIAction
228-
Template codersdk.Template
229-
NewWorkspaceName string
230-
WorkspaceID uuid.UUID
227+
Action WorkspaceCLIAction
228+
TemplateVersionID uuid.UUID
229+
NewWorkspaceName string
231230

232231
LastBuildParameters []codersdk.WorkspaceBuildParameter
233232

@@ -244,7 +243,7 @@ type prepWorkspaceBuildArgs struct {
244243
func prepWorkspaceBuild(inv *clibase.Invocation, client *codersdk.Client, args prepWorkspaceBuildArgs) ([]codersdk.WorkspaceBuildParameter, error) {
245244
ctx := inv.Context()
246245

247-
templateVersion, err := client.TemplateVersion(ctx, args.Template.ActiveVersionID)
246+
templateVersion, err := client.TemplateVersion(ctx, args.TemplateVersionID)
248247
if err != nil {
249248
return nil, xerrors.Errorf("get template version: %w", err)
250249
}

cli/exp_scaletest.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -600,9 +600,9 @@ func (r *RootCmd) scaletestCreateWorkspaces() *clibase.Cmd {
600600
}
601601

602602
richParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
603-
Action: WorkspaceCreate,
604-
Template: tpl,
605-
NewWorkspaceName: "scaletest-N", // TODO: the scaletest runner will pass in a different name here. Does this matter?
603+
Action: WorkspaceCreate,
604+
TemplateVersionID: tpl.ActiveVersionID,
605+
NewWorkspaceName: "scaletest-N", // TODO: the scaletest runner will pass in a different name here. Does this matter?
606606

607607
RichParameterFile: parameterFlags.richParameterFile,
608608
RichParameters: cliRichParameters,

cli/parameter.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ type workspaceParameterFlags struct {
2020

2121
richParameterFile string
2222
richParameters []string
23+
24+
promptRichParameters bool
25+
}
26+
27+
func (wpf *workspaceParameterFlags) allOptions() []clibase.Option {
28+
options := append(wpf.cliBuildOptions(), wpf.cliParameters()...)
29+
return append(options, wpf.alwaysPrompt())
2330
}
2431

2532
func (wpf *workspaceParameterFlags) cliBuildOptions() []clibase.Option {
@@ -55,6 +62,14 @@ func (wpf *workspaceParameterFlags) cliParameters() []clibase.Option {
5562
}
5663
}
5764

65+
func (wpf *workspaceParameterFlags) alwaysPrompt() clibase.Option {
66+
return clibase.Option{
67+
Flag: "always-prompt",
68+
Description: "Always prompt all parameters. Does not pull parameter values from existing workspace.",
69+
Value: clibase.BoolOf(&wpf.promptRichParameters),
70+
}
71+
}
72+
5873
func asWorkspaceBuildParameters(nameValuePairs []string) ([]codersdk.WorkspaceBuildParameter, error) {
5974
var params []codersdk.WorkspaceBuildParameter
6075
for _, nameValue := range nameValuePairs {

cli/parameterresolver.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ func (pr *ParameterResolver) resolveWithInput(resolved []codersdk.WorkspaceBuild
194194
(action == WorkspaceUpdate && promptParameterOption) ||
195195
(action == WorkspaceUpdate && tvp.Mutable && tvp.Required) ||
196196
(action == WorkspaceUpdate && !tvp.Mutable && firstTimeUse) ||
197-
(action == WorkspaceUpdate && tvp.Mutable && !tvp.Ephemeral && pr.promptRichParameters) {
197+
(tvp.Mutable && !tvp.Ephemeral && pr.promptRichParameters) {
198198
parameterValue, err := cliui.RichParameter(inv, tvp)
199199
if err != nil {
200200
return nil, err

cli/restart.go

Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func (r *RootCmd) restart() *clibase.Cmd {
2525
clibase.RequireNArgs(1),
2626
r.InitClient(client),
2727
),
28-
Options: append(parameterFlags.cliBuildOptions(), cliui.SkipPromptOption()),
28+
Options: clibase.OptionSet{cliui.SkipPromptOption()},
2929
Handler: func(inv *clibase.Invocation) error {
3030
ctx := inv.Context()
3131
out := inv.Stdout
@@ -35,24 +35,10 @@ func (r *RootCmd) restart() *clibase.Cmd {
3535
return err
3636
}
3737

38-
lastBuildParameters, err := client.WorkspaceBuildParameters(inv.Context(), workspace.LatestBuild.ID)
39-
if err != nil {
40-
return err
41-
}
42-
43-
buildOptions, err := asWorkspaceBuildParameters(parameterFlags.buildOptions)
44-
if err != nil {
45-
return xerrors.Errorf("can't parse build options: %w", err)
46-
}
47-
48-
buildParameters, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{
49-
Action: WorkspaceRestart,
50-
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
51-
52-
LastBuildParameters: lastBuildParameters,
53-
54-
PromptBuildOptions: parameterFlags.promptBuildOptions,
55-
BuildOptions: buildOptions,
38+
startReq, err := buildWorkspaceStartRequest(inv, client, startWorkspaceArgs{
39+
workspace: workspace,
40+
action: WorkspaceRestart,
41+
parameterFlags: parameterFlags,
5642
})
5743
if err != nil {
5844
return err
@@ -72,26 +58,20 @@ func (r *RootCmd) restart() *clibase.Cmd {
7258
if err != nil {
7359
return err
7460
}
61+
7562
err = cliui.WorkspaceBuild(ctx, out, client, build.ID)
7663
if err != nil {
7764
return err
7865
}
7966

80-
req := codersdk.CreateWorkspaceBuildRequest{
81-
Transition: codersdk.WorkspaceTransitionStart,
82-
RichParameterValues: buildParameters,
83-
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
84-
}
85-
86-
build, err = client.CreateWorkspaceBuild(ctx, workspace.ID, req)
67+
build, err = client.CreateWorkspaceBuild(ctx, workspace.ID, startReq)
8768
// It's possible for a workspace build to fail due to the template requiring starting
8869
// workspaces with the active version.
8970
if cerr, ok := codersdk.AsError(err); ok && cerr.StatusCode() == http.StatusUnauthorized {
90-
build, err = startWorkspaceActiveVersion(inv, client, startWorkspaceActiveVersionArgs{
91-
BuildOptions: buildOptions,
92-
LastBuildParameters: lastBuildParameters,
93-
PromptBuildOptions: parameterFlags.promptBuildOptions,
94-
Workspace: workspace,
71+
build, err = startWorkspace(inv, client, startWorkspaceArgs{
72+
workspace: workspace,
73+
parameterFlags: parameterFlags,
74+
action: WorkspaceUpdate,
9575
})
9676
if err != nil {
9777
return xerrors.Errorf("start workspace with active template version: %w", err)
@@ -112,5 +92,8 @@ func (r *RootCmd) restart() *clibase.Cmd {
11292
return nil
11393
},
11494
}
95+
96+
cmd.Options = append(cmd.Options, parameterFlags.allOptions()...)
97+
11598
return cmd
11699
}

cli/restart_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,4 +239,56 @@ func TestRestartWithParameters(t *testing.T) {
239239
Value: immutableParameterValue,
240240
})
241241
})
242+
243+
t.Run("AlwaysPrompt", func(t *testing.T) {
244+
t.Parallel()
245+
246+
// Create the workspace
247+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
248+
owner := coderdtest.CreateFirstUser(t, client)
249+
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
250+
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, mutableParamsResponse)
251+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
252+
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
253+
workspace := coderdtest.CreateWorkspace(t, member, owner.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
254+
cwr.RichParameterValues = []codersdk.WorkspaceBuildParameter{
255+
{
256+
Name: mutableParameterName,
257+
Value: mutableParameterValue,
258+
},
259+
}
260+
})
261+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
262+
263+
// Restart the workspace again
264+
inv, root := clitest.New(t, "restart", workspace.Name, "-y", "--always-prompt")
265+
clitest.SetupConfig(t, member, root)
266+
doneChan := make(chan struct{})
267+
pty := ptytest.New(t).Attach(inv)
268+
go func() {
269+
defer close(doneChan)
270+
err := inv.Run()
271+
assert.NoError(t, err)
272+
}()
273+
274+
newValue := "xyz"
275+
pty.ExpectMatch(mutableParameterName)
276+
pty.WriteLine(newValue)
277+
pty.ExpectMatch("workspace has been restarted")
278+
<-doneChan
279+
280+
// Verify if immutable parameter is set
281+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
282+
defer cancel()
283+
284+
workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{})
285+
require.NoError(t, err)
286+
actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
287+
require.NoError(t, err)
288+
require.Contains(t, actualParameters, codersdk.WorkspaceBuildParameter{
289+
Name: mutableParameterName,
290+
Value: newValue,
291+
})
292+
293+
})
242294
}

cli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ func (r *RootCmd) Core() []*clibase.Cmd {
9797
r.version(defaultVersionInfo),
9898

9999
// Workspace Commands
100+
r.autoupdate(),
100101
r.configSSH(),
101102
r.create(),
102103
r.deleteWorkspace(),

0 commit comments

Comments
 (0)