feat: add cli support for workspace automatic updates (#10438)

This commit is contained in:
Jon Ayers 2023-11-02 14:41:34 -05:00 committed by GitHub
parent e756baa0c4
commit 2dce4151ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 575 additions and 217 deletions

58
cli/autoupdate.go Normal file
View File

@ -0,0 +1,58 @@
package cli
import (
"fmt"
"strings"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) autoupdate() *clibase.Cmd {
client := new(codersdk.Client)
cmd := &clibase.Cmd{
Annotations: workspaceCommand,
Use: "autoupdate <workspace> <always|never>",
Short: "Toggle auto-update policy for a workspace",
Middleware: clibase.Chain(
clibase.RequireNArgs(2),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
policy := strings.ToLower(inv.Args[1])
err := validateAutoUpdatePolicy(policy)
if err != nil {
return xerrors.Errorf("validate policy: %w", err)
}
workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0])
if err != nil {
return xerrors.Errorf("get workspace: %w", err)
}
err = client.UpdateWorkspaceAutomaticUpdates(inv.Context(), workspace.ID, codersdk.UpdateWorkspaceAutomaticUpdatesRequest{
AutomaticUpdates: codersdk.AutomaticUpdates(policy),
})
if err != nil {
return xerrors.Errorf("update workspace automatic updates policy: %w", err)
}
_, _ = fmt.Fprintf(inv.Stdout, "Updated workspace %q auto-update policy to %q\n", workspace.Name, policy)
return nil
},
}
cmd.Options = append(cmd.Options, cliui.SkipPromptOption())
return cmd
}
func validateAutoUpdatePolicy(arg string) error {
switch codersdk.AutomaticUpdates(arg) {
case codersdk.AutomaticUpdatesAlways, codersdk.AutomaticUpdatesNever:
return nil
default:
return xerrors.Errorf("invalid option %q must be either of %q or %q", arg, codersdk.AutomaticUpdatesAlways, codersdk.AutomaticUpdatesNever)
}
}

79
cli/autoupdate_test.go Normal file
View File

@ -0,0 +1,79 @@
package cli_test
import (
"bytes"
"fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/codersdk"
)
func TestAutoUpdate(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, member, owner.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
require.Equal(t, codersdk.AutomaticUpdatesNever, workspace.AutomaticUpdates)
expectedPolicy := codersdk.AutomaticUpdatesAlways
inv, root := clitest.New(t, "autoupdate", workspace.Name, string(expectedPolicy))
clitest.SetupConfig(t, member, root)
var buf bytes.Buffer
inv.Stdout = &buf
err := inv.Run()
require.NoError(t, err)
require.Contains(t, buf.String(), fmt.Sprintf("Updated workspace %q auto-update policy to %q", workspace.Name, expectedPolicy))
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
require.Equal(t, expectedPolicy, workspace.AutomaticUpdates)
})
t.Run("InvalidArgs", func(t *testing.T) {
type testcase struct {
Name string
Args []string
ErrorContains string
}
cases := []testcase{
{
Name: "NoPolicy",
Args: []string{"autoupdate", "ws"},
ErrorContains: "wanted 2 args but got 1",
},
{
Name: "InvalidPolicy",
Args: []string{"autoupdate", "ws", "sometimes"},
ErrorContains: `invalid option "sometimes" must be either of`,
},
}
for _, c := range cases {
c := c
t.Run(c.Name, func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
inv, root := clitest.New(t, c.Args...)
clitest.SetupConfig(t, client, root)
err := inv.Run()
require.Error(t, err)
require.Contains(t, err.Error(), c.ErrorContains)
})
}
})
}

View File

@ -140,9 +140,9 @@ func (r *RootCmd) create() *clibase.Cmd {
}
richParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
Action: WorkspaceCreate,
Template: template,
NewWorkspaceName: workspaceName,
Action: WorkspaceCreate,
TemplateVersionID: template.ActiveVersionID,
NewWorkspaceName: workspaceName,
RichParameterFile: parameterFlags.richParameterFile,
RichParameters: cliRichParameters,
@ -224,10 +224,9 @@ func (r *RootCmd) create() *clibase.Cmd {
}
type prepWorkspaceBuildArgs struct {
Action WorkspaceCLIAction
Template codersdk.Template
NewWorkspaceName string
WorkspaceID uuid.UUID
Action WorkspaceCLIAction
TemplateVersionID uuid.UUID
NewWorkspaceName string
LastBuildParameters []codersdk.WorkspaceBuildParameter
@ -244,7 +243,7 @@ type prepWorkspaceBuildArgs struct {
func prepWorkspaceBuild(inv *clibase.Invocation, client *codersdk.Client, args prepWorkspaceBuildArgs) ([]codersdk.WorkspaceBuildParameter, error) {
ctx := inv.Context()
templateVersion, err := client.TemplateVersion(ctx, args.Template.ActiveVersionID)
templateVersion, err := client.TemplateVersion(ctx, args.TemplateVersionID)
if err != nil {
return nil, xerrors.Errorf("get template version: %w", err)
}

View File

@ -600,9 +600,9 @@ func (r *RootCmd) scaletestCreateWorkspaces() *clibase.Cmd {
}
richParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
Action: WorkspaceCreate,
Template: tpl,
NewWorkspaceName: "scaletest-N", // TODO: the scaletest runner will pass in a different name here. Does this matter?
Action: WorkspaceCreate,
TemplateVersionID: tpl.ActiveVersionID,
NewWorkspaceName: "scaletest-N", // TODO: the scaletest runner will pass in a different name here. Does this matter?
RichParameterFile: parameterFlags.richParameterFile,
RichParameters: cliRichParameters,

View File

@ -20,6 +20,13 @@ type workspaceParameterFlags struct {
richParameterFile string
richParameters []string
promptRichParameters bool
}
func (wpf *workspaceParameterFlags) allOptions() []clibase.Option {
options := append(wpf.cliBuildOptions(), wpf.cliParameters()...)
return append(options, wpf.alwaysPrompt())
}
func (wpf *workspaceParameterFlags) cliBuildOptions() []clibase.Option {
@ -55,6 +62,14 @@ func (wpf *workspaceParameterFlags) cliParameters() []clibase.Option {
}
}
func (wpf *workspaceParameterFlags) alwaysPrompt() clibase.Option {
return clibase.Option{
Flag: "always-prompt",
Description: "Always prompt all parameters. Does not pull parameter values from existing workspace.",
Value: clibase.BoolOf(&wpf.promptRichParameters),
}
}
func asWorkspaceBuildParameters(nameValuePairs []string) ([]codersdk.WorkspaceBuildParameter, error) {
var params []codersdk.WorkspaceBuildParameter
for _, nameValue := range nameValuePairs {

View File

@ -194,7 +194,7 @@ func (pr *ParameterResolver) resolveWithInput(resolved []codersdk.WorkspaceBuild
(action == WorkspaceUpdate && promptParameterOption) ||
(action == WorkspaceUpdate && tvp.Mutable && tvp.Required) ||
(action == WorkspaceUpdate && !tvp.Mutable && firstTimeUse) ||
(action == WorkspaceUpdate && tvp.Mutable && !tvp.Ephemeral && pr.promptRichParameters) {
(tvp.Mutable && !tvp.Ephemeral && pr.promptRichParameters) {
parameterValue, err := cliui.RichParameter(inv, tvp)
if err != nil {
return nil, err

View File

@ -25,7 +25,7 @@ func (r *RootCmd) restart() *clibase.Cmd {
clibase.RequireNArgs(1),
r.InitClient(client),
),
Options: append(parameterFlags.cliBuildOptions(), cliui.SkipPromptOption()),
Options: clibase.OptionSet{cliui.SkipPromptOption()},
Handler: func(inv *clibase.Invocation) error {
ctx := inv.Context()
out := inv.Stdout
@ -35,25 +35,7 @@ func (r *RootCmd) restart() *clibase.Cmd {
return err
}
lastBuildParameters, err := client.WorkspaceBuildParameters(inv.Context(), workspace.LatestBuild.ID)
if err != nil {
return err
}
buildOptions, err := asWorkspaceBuildParameters(parameterFlags.buildOptions)
if err != nil {
return xerrors.Errorf("can't parse build options: %w", err)
}
buildParameters, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{
Action: WorkspaceRestart,
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
LastBuildParameters: lastBuildParameters,
PromptBuildOptions: parameterFlags.promptBuildOptions,
BuildOptions: buildOptions,
})
startReq, err := buildWorkspaceStartRequest(inv, client, workspace, parameterFlags, WorkspaceRestart)
if err != nil {
return err
}
@ -72,27 +54,18 @@ func (r *RootCmd) restart() *clibase.Cmd {
if err != nil {
return err
}
err = cliui.WorkspaceBuild(ctx, out, client, build.ID)
if err != nil {
return err
}
req := codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionStart,
RichParameterValues: buildParameters,
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
}
build, err = client.CreateWorkspaceBuild(ctx, workspace.ID, req)
build, err = client.CreateWorkspaceBuild(ctx, workspace.ID, startReq)
// It's possible for a workspace build to fail due to the template requiring starting
// workspaces with the active version.
if cerr, ok := codersdk.AsError(err); ok && cerr.StatusCode() == http.StatusUnauthorized {
build, err = startWorkspaceActiveVersion(inv, client, startWorkspaceActiveVersionArgs{
BuildOptions: buildOptions,
LastBuildParameters: lastBuildParameters,
PromptBuildOptions: parameterFlags.promptBuildOptions,
Workspace: workspace,
})
_, _ = fmt.Fprintln(inv.Stdout, "Failed to restart with the template version from your last build. Policy may require you to restart with the current active template version.")
build, err = startWorkspace(inv, client, workspace, parameterFlags, WorkspaceUpdate)
if err != nil {
return xerrors.Errorf("start workspace with active template version: %w", err)
}
@ -112,5 +85,8 @@ func (r *RootCmd) restart() *clibase.Cmd {
return nil
},
}
cmd.Options = append(cmd.Options, parameterFlags.allOptions()...)
return cmd
}

View File

@ -239,4 +239,55 @@ func TestRestartWithParameters(t *testing.T) {
Value: immutableParameterValue,
})
})
t.Run("AlwaysPrompt", func(t *testing.T) {
t.Parallel()
// Create the workspace
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, mutableParamsResponse)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, member, owner.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.RichParameterValues = []codersdk.WorkspaceBuildParameter{
{
Name: mutableParameterName,
Value: mutableParameterValue,
},
}
})
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
inv, root := clitest.New(t, "restart", workspace.Name, "-y", "--always-prompt")
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
go func() {
defer close(doneChan)
err := inv.Run()
assert.NoError(t, err)
}()
// We should be prompted for the parameters again.
newValue := "xyz"
pty.ExpectMatch(mutableParameterName)
pty.WriteLine(newValue)
pty.ExpectMatch("workspace has been restarted")
<-doneChan
// Verify that the updated values are persisted.
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{})
require.NoError(t, err)
actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
require.NoError(t, err)
require.Contains(t, actualParameters, codersdk.WorkspaceBuildParameter{
Name: mutableParameterName,
Value: newValue,
})
})
}

View File

@ -97,6 +97,7 @@ func (r *RootCmd) Core() []*clibase.Cmd {
r.version(defaultVersionInfo),
// Workspace Commands
r.autoupdate(),
r.configSSH(),
r.create(),
r.deleteWorkspace(),

View File

@ -5,7 +5,6 @@ import (
"net/http"
"time"
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
@ -25,52 +24,19 @@ func (r *RootCmd) start() *clibase.Cmd {
clibase.RequireNArgs(1),
r.InitClient(client),
),
Options: append(parameterFlags.cliBuildOptions(), cliui.SkipPromptOption()),
Options: clibase.OptionSet{cliui.SkipPromptOption()},
Handler: func(inv *clibase.Invocation) error {
workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0])
if err != nil {
return err
}
lastBuildParameters, err := client.WorkspaceBuildParameters(inv.Context(), workspace.LatestBuild.ID)
if err != nil {
return err
}
buildOptions, err := asWorkspaceBuildParameters(parameterFlags.buildOptions)
if err != nil {
return xerrors.Errorf("unable to parse build options: %w", err)
}
buildParameters, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{
Action: WorkspaceStart,
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
LastBuildParameters: lastBuildParameters,
PromptBuildOptions: parameterFlags.promptBuildOptions,
BuildOptions: buildOptions,
})
if err != nil {
return err
}
req := codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionStart,
RichParameterValues: buildParameters,
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
}
build, err := client.CreateWorkspaceBuild(inv.Context(), workspace.ID, req)
build, err := startWorkspace(inv, client, workspace, parameterFlags, WorkspaceStart)
// It's possible for a workspace build to fail due to the template requiring starting
// workspaces with the active version.
if cerr, ok := codersdk.AsError(err); ok && cerr.StatusCode() == http.StatusUnauthorized {
build, err = startWorkspaceActiveVersion(inv, client, startWorkspaceActiveVersionArgs{
BuildOptions: buildOptions,
LastBuildParameters: lastBuildParameters,
PromptBuildOptions: parameterFlags.promptBuildOptions,
Workspace: workspace,
})
_, _ = fmt.Fprintln(inv.Stdout, "Failed to restart with the template version from your last build. Policy may require you to restart with the current active template version.")
build, err = startWorkspace(inv, client, workspace, parameterFlags, WorkspaceUpdate)
if err != nil {
return xerrors.Errorf("start workspace with active template version: %w", err)
}
@ -90,75 +56,69 @@ func (r *RootCmd) start() *clibase.Cmd {
return nil
},
}
cmd.Options = append(cmd.Options, parameterFlags.allOptions()...)
return cmd
}
type prepStartWorkspaceArgs struct {
Action WorkspaceCLIAction
TemplateVersionID uuid.UUID
LastBuildParameters []codersdk.WorkspaceBuildParameter
PromptBuildOptions bool
BuildOptions []codersdk.WorkspaceBuildParameter
}
func prepStartWorkspace(inv *clibase.Invocation, client *codersdk.Client, args prepStartWorkspaceArgs) ([]codersdk.WorkspaceBuildParameter, error) {
ctx := inv.Context()
templateVersion, err := client.TemplateVersion(ctx, args.TemplateVersionID)
if err != nil {
return nil, xerrors.Errorf("get template version: %w", err)
func buildWorkspaceStartRequest(inv *clibase.Invocation, client *codersdk.Client, workspace codersdk.Workspace, parameterFlags workspaceParameterFlags, action WorkspaceCLIAction) (codersdk.CreateWorkspaceBuildRequest, error) {
version := workspace.LatestBuild.TemplateVersionID
if workspace.AutomaticUpdates == codersdk.AutomaticUpdatesAlways || action == WorkspaceUpdate {
version = workspace.TemplateActiveVersionID
if version != workspace.LatestBuild.TemplateVersionID {
action = WorkspaceUpdate
}
}
templateVersionParameters, err := client.TemplateVersionRichParameters(inv.Context(), templateVersion.ID)
lastBuildParameters, err := client.WorkspaceBuildParameters(inv.Context(), workspace.LatestBuild.ID)
if err != nil {
return nil, xerrors.Errorf("get template version rich parameters: %w", err)
return codersdk.CreateWorkspaceBuildRequest{}, err
}
resolver := new(ParameterResolver).
WithLastBuildParameters(args.LastBuildParameters).
WithPromptBuildOptions(args.PromptBuildOptions).
WithBuildOptions(args.BuildOptions)
return resolver.Resolve(inv, args.Action, templateVersionParameters)
}
type startWorkspaceActiveVersionArgs struct {
BuildOptions []codersdk.WorkspaceBuildParameter
LastBuildParameters []codersdk.WorkspaceBuildParameter
PromptBuildOptions bool
Workspace codersdk.Workspace
}
func startWorkspaceActiveVersion(inv *clibase.Invocation, client *codersdk.Client, args startWorkspaceActiveVersionArgs) (codersdk.WorkspaceBuild, error) {
_, _ = fmt.Fprintln(inv.Stdout, "Failed to restart with the template version from your last build. Policy may require you to restart with the current active template version.")
template, err := client.Template(inv.Context(), args.Workspace.TemplateID)
buildOptions, err := asWorkspaceBuildParameters(parameterFlags.buildOptions)
if err != nil {
return codersdk.WorkspaceBuild{}, xerrors.Errorf("get template: %w", err)
return codersdk.CreateWorkspaceBuildRequest{}, xerrors.Errorf("unable to parse build options: %w", err)
}
buildParameters, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{
Action: WorkspaceStart,
TemplateVersionID: template.ActiveVersionID,
cliRichParameters, err := asWorkspaceBuildParameters(parameterFlags.richParameters)
if err != nil {
return codersdk.CreateWorkspaceBuildRequest{}, xerrors.Errorf("unable to parse build options: %w", err)
}
LastBuildParameters: args.LastBuildParameters,
buildParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
Action: action,
TemplateVersionID: version,
NewWorkspaceName: workspace.Name,
LastBuildParameters: lastBuildParameters,
PromptBuildOptions: args.PromptBuildOptions,
BuildOptions: args.BuildOptions,
PromptBuildOptions: parameterFlags.promptBuildOptions,
BuildOptions: buildOptions,
PromptRichParameters: parameterFlags.promptRichParameters,
RichParameters: cliRichParameters,
RichParameterFile: parameterFlags.richParameterFile,
})
if err != nil {
return codersdk.WorkspaceBuild{}, err
return codersdk.CreateWorkspaceBuildRequest{}, err
}
build, err := client.CreateWorkspaceBuild(inv.Context(), args.Workspace.ID, codersdk.CreateWorkspaceBuildRequest{
return codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionStart,
RichParameterValues: buildParameters,
TemplateVersionID: template.ActiveVersionID,
})
TemplateVersionID: version,
}, nil
}
func startWorkspace(inv *clibase.Invocation, client *codersdk.Client, workspace codersdk.Workspace, parameterFlags workspaceParameterFlags, action WorkspaceCLIAction) (codersdk.WorkspaceBuild, error) {
req, err := buildWorkspaceStartRequest(inv, client, workspace, parameterFlags, action)
if err != nil {
return codersdk.WorkspaceBuild{}, err
}
build, err := client.CreateWorkspaceBuild(inv.Context(), workspace.ID, req)
if err != nil {
return codersdk.WorkspaceBuild{}, xerrors.Errorf("create workspace build: %w", err)
}
return build, nil
}

View File

@ -26,6 +26,52 @@ const (
immutableParameterName = "immutable_parameter"
immutableParameterDescription = "This is immutable parameter"
immutableParameterValue = "abc"
mutableParameterName = "mutable_parameter"
mutableParameterValue = "hello"
)
var (
mutableParamsResponse = &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Parameters: []*proto.RichParameter{
{
Name: mutableParameterName,
Description: "This is a mutable parameter",
Required: true,
Mutable: true,
},
},
},
},
},
},
ProvisionApply: echo.ApplyComplete,
}
immutableParamsResponse = &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Parameters: []*proto.RichParameter{
{
Name: immutableParameterName,
Description: immutableParameterDescription,
Required: true,
},
},
},
},
},
},
ProvisionApply: echo.ApplyComplete,
}
)
func TestStart(t *testing.T) {
@ -147,26 +193,6 @@ func TestStart(t *testing.T) {
func TestStartWithParameters(t *testing.T) {
t.Parallel()
echoResponses := &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Parameters: []*proto.RichParameter{
{
Name: immutableParameterName,
Description: immutableParameterDescription,
Required: true,
},
},
},
},
},
},
ProvisionApply: echo.ApplyComplete,
}
t.Run("DoNotAskForImmutables", func(t *testing.T) {
t.Parallel()
@ -174,7 +200,7 @@ func TestStartWithParameters(t *testing.T) {
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, immutableParamsResponse)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, member, owner.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
@ -218,4 +244,133 @@ func TestStartWithParameters(t *testing.T) {
Value: immutableParameterValue,
})
})
t.Run("AlwaysPrompt", func(t *testing.T) {
t.Parallel()
// Create the workspace
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, mutableParamsResponse)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, member, owner.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.RichParameterValues = []codersdk.WorkspaceBuildParameter{
{
Name: mutableParameterName,
Value: mutableParameterValue,
},
}
})
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
// Stop the workspace
workspaceBuild := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceBuild.ID)
// Start the workspace again
inv, root := clitest.New(t, "start", workspace.Name, "--always-prompt")
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
go func() {
defer close(doneChan)
err := inv.Run()
assert.NoError(t, err)
}()
newValue := "xyz"
pty.ExpectMatch(mutableParameterName)
pty.WriteLine(newValue)
pty.ExpectMatch("workspace has been started")
<-doneChan
// Verify that the updated values are persisted.
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{})
require.NoError(t, err)
actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
require.NoError(t, err)
require.Contains(t, actualParameters, codersdk.WorkspaceBuildParameter{
Name: mutableParameterName,
Value: newValue,
})
})
}
// TestStartAutoUpdate also tests restart since the flows are virtually identical.
func TestStartAutoUpdate(t *testing.T) {
t.Parallel()
const (
stringParameterName = "myparam"
stringParameterValue = "abc"
)
stringRichParameters := []*proto.RichParameter{
{Name: stringParameterName, Type: "string", Mutable: true, Required: true},
}
type testcase struct {
Name string
Cmd string
}
cases := []testcase{
{
Name: "StartOK",
Cmd: "start",
},
{
Name: "RestartOK",
Cmd: "restart",
},
}
for _, c := range cases {
c := c
t.Run(c.Name, func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version1 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version1.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version1.ID)
workspace := coderdtest.CreateWorkspace(t, member, owner.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.AutomaticUpdates = codersdk.AutomaticUpdatesAlways
})
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
if c.Cmd == "start" {
coderdtest.MustTransitionWorkspace(t, member, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
}
version2 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, prepareEchoResponses(stringRichParameters), func(ctvr *codersdk.CreateTemplateVersionRequest) {
ctvr.TemplateID = template.ID
})
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version2.ID)
coderdtest.UpdateActiveTemplateVersion(t, client, template.ID, version2.ID)
inv, root := clitest.New(t, c.Cmd, "-y", workspace.Name)
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
go func() {
defer close(doneChan)
err := inv.Run()
assert.NoError(t, err)
}()
pty.ExpectMatch(stringParameterName)
pty.WriteLine(stringParameterValue)
<-doneChan
workspace = coderdtest.MustWorkspace(t, member, workspace.ID)
require.Equal(t, version2.ID, workspace.LatestBuild.TemplateVersionID)
})
}
}

View File

@ -14,6 +14,7 @@ USAGE:
$ coder templates init
SUBCOMMANDS:
autoupdate Toggle auto-update policy for a workspace
config-ssh Add an SSH Host entry for your workspaces "ssh
coder.workspace"
create Create a workspace

View File

@ -0,0 +1,13 @@
coder v0.0.0-devel
USAGE:
coder autoupdate [flags] <workspace> <always|never>
Toggle auto-update policy for a workspace
OPTIONS:
-y, --yes bool
Bypass prompts.
———
Run `coder --help` for a list of global options.

View File

@ -6,12 +6,23 @@ USAGE:
Restart a workspace
OPTIONS:
--always-prompt bool
Always prompt all parameters. Does not pull parameter values from
existing workspace.
--build-option string-array, $CODER_BUILD_OPTION
Build option value in the format "name=value".
--build-options bool
Prompt for one-time build options defined with ephemeral parameters.
--parameter string-array, $CODER_RICH_PARAMETER
Rich parameter value in the format "name=value".
--rich-parameter-file string, $CODER_RICH_PARAMETER_FILE
Specify a file path with values for rich parameters defined in the
template.
-y, --yes bool
Bypass prompts.

View File

@ -6,12 +6,23 @@ USAGE:
Start a workspace
OPTIONS:
--always-prompt bool
Always prompt all parameters. Does not pull parameter values from
existing workspace.
--build-option string-array, $CODER_BUILD_OPTION
Build option value in the format "name=value".
--build-options bool
Prompt for one-time build options defined with ephemeral parameters.
--parameter string-array, $CODER_RICH_PARAMETER
Rich parameter value in the format "name=value".
--rich-parameter-file string, $CODER_RICH_PARAMETER_FILE
Specify a file path with values for rich parameters defined in the
template.
-y, --yes bool
Bypass prompts.

View File

@ -10,11 +10,7 @@ import (
)
func (r *RootCmd) update() *clibase.Cmd {
var (
alwaysPrompt bool
parameterFlags workspaceParameterFlags
)
var parameterFlags workspaceParameterFlags
client := new(codersdk.Client)
cmd := &clibase.Cmd{
@ -31,58 +27,16 @@ func (r *RootCmd) update() *clibase.Cmd {
if err != nil {
return err
}
if !workspace.Outdated && !alwaysPrompt && !parameterFlags.promptBuildOptions && len(parameterFlags.buildOptions) == 0 {
if !workspace.Outdated && !parameterFlags.promptRichParameters && !parameterFlags.promptBuildOptions && len(parameterFlags.buildOptions) == 0 {
_, _ = fmt.Fprintf(inv.Stdout, "Workspace isn't outdated!\n")
return nil
}
buildOptions, err := asWorkspaceBuildParameters(parameterFlags.buildOptions)
build, err := startWorkspace(inv, client, workspace, parameterFlags, WorkspaceUpdate)
if err != nil {
return err
return xerrors.Errorf("start workspace: %w", err)
}
template, err := client.Template(inv.Context(), workspace.TemplateID)
if err != nil {
return err
}
lastBuildParameters, err := client.WorkspaceBuildParameters(inv.Context(), workspace.LatestBuild.ID)
if err != nil {
return err
}
cliRichParameters, err := asWorkspaceBuildParameters(parameterFlags.richParameters)
if err != nil {
return xerrors.Errorf("can't parse given parameter values: %w", err)
}
buildParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
Action: WorkspaceUpdate,
Template: template,
NewWorkspaceName: workspace.Name,
WorkspaceID: workspace.LatestBuild.ID,
LastBuildParameters: lastBuildParameters,
PromptBuildOptions: parameterFlags.promptBuildOptions,
BuildOptions: buildOptions,
PromptRichParameters: alwaysPrompt,
RichParameters: cliRichParameters,
RichParameterFile: parameterFlags.richParameterFile,
})
if err != nil {
return err
}
build, err := client.CreateWorkspaceBuild(inv.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
TemplateVersionID: template.ActiveVersionID,
Transition: codersdk.WorkspaceTransitionStart,
RichParameterValues: buildParameters,
})
if err != nil {
return err
}
logs, closer, err := client.WorkspaceBuildLogsAfter(inv.Context(), build.ID, 0)
if err != nil {
return err
@ -99,14 +53,6 @@ func (r *RootCmd) update() *clibase.Cmd {
},
}
cmd.Options = clibase.OptionSet{
{
Flag: "always-prompt",
Description: "Always prompt all parameters. Does not pull parameter values from existing workspace.",
Value: clibase.BoolOf(&alwaysPrompt),
},
}
cmd.Options = append(cmd.Options, parameterFlags.cliBuildOptions()...)
cmd.Options = append(cmd.Options, parameterFlags.cliParameters()...)
cmd.Options = append(cmd.Options, parameterFlags.allOptions()...)
return cmd
}

View File

@ -934,11 +934,8 @@ func MustTransitionWorkspace(t testing.TB, client *codersdk.Client, workspaceID
require.NoError(t, err, "unexpected error fetching workspace")
require.Equal(t, workspace.LatestBuild.Transition, codersdk.WorkspaceTransition(from), "expected workspace state: %s got: %s", from, workspace.LatestBuild.Transition)
template, err := client.Template(ctx, workspace.TemplateID)
require.NoError(t, err, "fetch workspace template")
req := codersdk.CreateWorkspaceBuildRequest{
TemplateVersionID: template.ActiveVersionID,
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
Transition: codersdk.WorkspaceTransition(to),
}

View File

@ -25,6 +25,7 @@ Coder — A tool for provisioning self-hosted development environments with Terr
| Name | Purpose |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
| [<code>autoupdate</code>](./cli/autoupdate.md) | Toggle auto-update policy for a workspace |
| [<code>config-ssh</code>](./cli/config-ssh.md) | Add an SSH Host entry for your workspaces "ssh coder.workspace" |
| [<code>create</code>](./cli/create.md) | Create a workspace |
| [<code>delete</code>](./cli/delete.md) | Delete a workspace |

21
docs/cli/autoupdate.md generated Normal file
View File

@ -0,0 +1,21 @@
<!-- DO NOT EDIT | GENERATED CONTENT -->
# autoupdate
Toggle auto-update policy for a workspace
## Usage
```console
coder autoupdate [flags] <workspace> <always|never>
```
## Options
### -y, --yes
| | |
| ---- | ----------------- |
| Type | <code>bool</code> |
Bypass prompts.

26
docs/cli/restart.md generated
View File

@ -12,6 +12,14 @@ coder restart [flags] <workspace>
## Options
### --always-prompt
| | |
| ---- | ----------------- |
| Type | <code>bool</code> |
Always prompt all parameters. Does not pull parameter values from existing workspace.
### --build-option
| | |
@ -29,6 +37,24 @@ Build option value in the format "name=value".
Prompt for one-time build options defined with ephemeral parameters.
### --parameter
| | |
| ----------- | ---------------------------------- |
| Type | <code>string-array</code> |
| Environment | <code>$CODER_RICH_PARAMETER</code> |
Rich parameter value in the format "name=value".
### --rich-parameter-file
| | |
| ----------- | --------------------------------------- |
| Type | <code>string</code> |
| Environment | <code>$CODER_RICH_PARAMETER_FILE</code> |
Specify a file path with values for rich parameters defined in the template.
### -y, --yes
| | |

26
docs/cli/start.md generated
View File

@ -12,6 +12,14 @@ coder start [flags] <workspace>
## Options
### --always-prompt
| | |
| ---- | ----------------- |
| Type | <code>bool</code> |
Always prompt all parameters. Does not pull parameter values from existing workspace.
### --build-option
| | |
@ -29,6 +37,24 @@ Build option value in the format "name=value".
Prompt for one-time build options defined with ephemeral parameters.
### --parameter
| | |
| ----------- | ---------------------------------- |
| Type | <code>string-array</code> |
| Environment | <code>$CODER_RICH_PARAMETER</code> |
Rich parameter value in the format "name=value".
### --rich-parameter-file
| | |
| ----------- | --------------------------------------- |
| Type | <code>string</code> |
| Environment | <code>$CODER_RICH_PARAMETER_FILE</code> |
Specify a file path with values for rich parameters defined in the template.
### -y, --yes
| | |

View File

@ -569,6 +569,11 @@
"path": "./cli.md",
"icon_path": "./images/icons/terminal.svg",
"children": [
{
"title": "autoupdate",
"description": "Toggle auto-update policy for a workspace",
"path": "cli/autoupdate.md"
},
{
"title": "coder",
"path": "cli.md"

View File

@ -1,6 +1,7 @@
package cli_test
import (
"bytes"
"testing"
"github.com/google/uuid"
@ -154,21 +155,26 @@ func TestStart(t *testing.T) {
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, c.Client, ws.LatestBuild.ID)
initialTemplateVersion := ws.LatestBuild.TemplateVersionID
if cmd == "start" {
// Stop the workspace so that we can start it.
coderdtest.MustTransitionWorkspace(t, c.Client, ws.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop, func(req *codersdk.CreateWorkspaceBuildRequest) {
req.TemplateVersionID = oldVersion.ID
})
coderdtest.MustTransitionWorkspace(t, c.Client, ws.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
}
// Start the workspace. Every test permutation should
// pass.
var buf bytes.Buffer
inv, conf := newCLI(t, cmd, ws.Name, "-y")
inv.Stdout = &buf
clitest.SetupConfig(t, c.Client, conf)
err = inv.Run()
require.NoError(t, err)
ws = coderdtest.MustWorkspace(t, c.Client, ws.ID)
require.Equal(t, c.ExpectedVersion, ws.LatestBuild.TemplateVersionID)
if initialTemplateVersion != ws.LatestBuild.TemplateVersionID {
require.Contains(t, buf.String(), "Failed to restart with the template version from your last build. Policy may require you to restart with the current active template version.")
}
})
}
})