mirror of https://github.com/coder/coder.git
feat: add cli support for workspace automatic updates (#10438)
This commit is contained in:
parent
e756baa0c4
commit
2dce4151ba
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -140,9 +140,9 @@ func (r *RootCmd) create() *clibase.Cmd {
|
||||||
}
|
}
|
||||||
|
|
||||||
richParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
|
richParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
|
||||||
Action: WorkspaceCreate,
|
Action: WorkspaceCreate,
|
||||||
Template: template,
|
TemplateVersionID: template.ActiveVersionID,
|
||||||
NewWorkspaceName: workspaceName,
|
NewWorkspaceName: workspaceName,
|
||||||
|
|
||||||
RichParameterFile: parameterFlags.richParameterFile,
|
RichParameterFile: parameterFlags.richParameterFile,
|
||||||
RichParameters: cliRichParameters,
|
RichParameters: cliRichParameters,
|
||||||
|
@ -224,10 +224,9 @@ func (r *RootCmd) create() *clibase.Cmd {
|
||||||
}
|
}
|
||||||
|
|
||||||
type prepWorkspaceBuildArgs struct {
|
type prepWorkspaceBuildArgs struct {
|
||||||
Action WorkspaceCLIAction
|
Action WorkspaceCLIAction
|
||||||
Template codersdk.Template
|
TemplateVersionID uuid.UUID
|
||||||
NewWorkspaceName string
|
NewWorkspaceName string
|
||||||
WorkspaceID uuid.UUID
|
|
||||||
|
|
||||||
LastBuildParameters []codersdk.WorkspaceBuildParameter
|
LastBuildParameters []codersdk.WorkspaceBuildParameter
|
||||||
|
|
||||||
|
@ -244,7 +243,7 @@ type prepWorkspaceBuildArgs struct {
|
||||||
func prepWorkspaceBuild(inv *clibase.Invocation, client *codersdk.Client, args prepWorkspaceBuildArgs) ([]codersdk.WorkspaceBuildParameter, error) {
|
func prepWorkspaceBuild(inv *clibase.Invocation, client *codersdk.Client, args prepWorkspaceBuildArgs) ([]codersdk.WorkspaceBuildParameter, error) {
|
||||||
ctx := inv.Context()
|
ctx := inv.Context()
|
||||||
|
|
||||||
templateVersion, err := client.TemplateVersion(ctx, args.Template.ActiveVersionID)
|
templateVersion, err := client.TemplateVersion(ctx, args.TemplateVersionID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, xerrors.Errorf("get template version: %w", err)
|
return nil, xerrors.Errorf("get template version: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -600,9 +600,9 @@ func (r *RootCmd) scaletestCreateWorkspaces() *clibase.Cmd {
|
||||||
}
|
}
|
||||||
|
|
||||||
richParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
|
richParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
|
||||||
Action: WorkspaceCreate,
|
Action: WorkspaceCreate,
|
||||||
Template: tpl,
|
TemplateVersionID: tpl.ActiveVersionID,
|
||||||
NewWorkspaceName: "scaletest-N", // TODO: the scaletest runner will pass in a different name here. Does this matter?
|
NewWorkspaceName: "scaletest-N", // TODO: the scaletest runner will pass in a different name here. Does this matter?
|
||||||
|
|
||||||
RichParameterFile: parameterFlags.richParameterFile,
|
RichParameterFile: parameterFlags.richParameterFile,
|
||||||
RichParameters: cliRichParameters,
|
RichParameters: cliRichParameters,
|
||||||
|
|
|
@ -20,6 +20,13 @@ type workspaceParameterFlags struct {
|
||||||
|
|
||||||
richParameterFile string
|
richParameterFile string
|
||||||
richParameters []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 {
|
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) {
|
func asWorkspaceBuildParameters(nameValuePairs []string) ([]codersdk.WorkspaceBuildParameter, error) {
|
||||||
var params []codersdk.WorkspaceBuildParameter
|
var params []codersdk.WorkspaceBuildParameter
|
||||||
for _, nameValue := range nameValuePairs {
|
for _, nameValue := range nameValuePairs {
|
||||||
|
|
|
@ -194,7 +194,7 @@ func (pr *ParameterResolver) resolveWithInput(resolved []codersdk.WorkspaceBuild
|
||||||
(action == WorkspaceUpdate && promptParameterOption) ||
|
(action == WorkspaceUpdate && promptParameterOption) ||
|
||||||
(action == WorkspaceUpdate && tvp.Mutable && tvp.Required) ||
|
(action == WorkspaceUpdate && tvp.Mutable && tvp.Required) ||
|
||||||
(action == WorkspaceUpdate && !tvp.Mutable && firstTimeUse) ||
|
(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)
|
parameterValue, err := cliui.RichParameter(inv, tvp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -25,7 +25,7 @@ func (r *RootCmd) restart() *clibase.Cmd {
|
||||||
clibase.RequireNArgs(1),
|
clibase.RequireNArgs(1),
|
||||||
r.InitClient(client),
|
r.InitClient(client),
|
||||||
),
|
),
|
||||||
Options: append(parameterFlags.cliBuildOptions(), cliui.SkipPromptOption()),
|
Options: clibase.OptionSet{cliui.SkipPromptOption()},
|
||||||
Handler: func(inv *clibase.Invocation) error {
|
Handler: func(inv *clibase.Invocation) error {
|
||||||
ctx := inv.Context()
|
ctx := inv.Context()
|
||||||
out := inv.Stdout
|
out := inv.Stdout
|
||||||
|
@ -35,25 +35,7 @@ func (r *RootCmd) restart() *clibase.Cmd {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
lastBuildParameters, err := client.WorkspaceBuildParameters(inv.Context(), workspace.LatestBuild.ID)
|
startReq, err := buildWorkspaceStartRequest(inv, client, workspace, parameterFlags, WorkspaceRestart)
|
||||||
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,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -72,27 +54,18 @@ func (r *RootCmd) restart() *clibase.Cmd {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cliui.WorkspaceBuild(ctx, out, client, build.ID)
|
err = cliui.WorkspaceBuild(ctx, out, client, build.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
req := codersdk.CreateWorkspaceBuildRequest{
|
build, err = client.CreateWorkspaceBuild(ctx, workspace.ID, startReq)
|
||||||
Transition: codersdk.WorkspaceTransitionStart,
|
|
||||||
RichParameterValues: buildParameters,
|
|
||||||
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
|
|
||||||
}
|
|
||||||
|
|
||||||
build, err = client.CreateWorkspaceBuild(ctx, workspace.ID, req)
|
|
||||||
// It's possible for a workspace build to fail due to the template requiring starting
|
// It's possible for a workspace build to fail due to the template requiring starting
|
||||||
// workspaces with the active version.
|
// workspaces with the active version.
|
||||||
if cerr, ok := codersdk.AsError(err); ok && cerr.StatusCode() == http.StatusUnauthorized {
|
if cerr, ok := codersdk.AsError(err); ok && cerr.StatusCode() == http.StatusUnauthorized {
|
||||||
build, err = startWorkspaceActiveVersion(inv, client, startWorkspaceActiveVersionArgs{
|
_, _ = 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.")
|
||||||
BuildOptions: buildOptions,
|
build, err = startWorkspace(inv, client, workspace, parameterFlags, WorkspaceUpdate)
|
||||||
LastBuildParameters: lastBuildParameters,
|
|
||||||
PromptBuildOptions: parameterFlags.promptBuildOptions,
|
|
||||||
Workspace: workspace,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("start workspace with active template version: %w", err)
|
return xerrors.Errorf("start workspace with active template version: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -112,5 +85,8 @@ func (r *RootCmd) restart() *clibase.Cmd {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd.Options = append(cmd.Options, parameterFlags.allOptions()...)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
|
@ -239,4 +239,55 @@ func TestRestartWithParameters(t *testing.T) {
|
||||||
Value: immutableParameterValue,
|
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,
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,6 +97,7 @@ func (r *RootCmd) Core() []*clibase.Cmd {
|
||||||
r.version(defaultVersionInfo),
|
r.version(defaultVersionInfo),
|
||||||
|
|
||||||
// Workspace Commands
|
// Workspace Commands
|
||||||
|
r.autoupdate(),
|
||||||
r.configSSH(),
|
r.configSSH(),
|
||||||
r.create(),
|
r.create(),
|
||||||
r.deleteWorkspace(),
|
r.deleteWorkspace(),
|
||||||
|
|
130
cli/start.go
130
cli/start.go
|
@ -5,7 +5,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
"github.com/coder/coder/v2/cli/clibase"
|
"github.com/coder/coder/v2/cli/clibase"
|
||||||
|
@ -25,52 +24,19 @@ func (r *RootCmd) start() *clibase.Cmd {
|
||||||
clibase.RequireNArgs(1),
|
clibase.RequireNArgs(1),
|
||||||
r.InitClient(client),
|
r.InitClient(client),
|
||||||
),
|
),
|
||||||
Options: append(parameterFlags.cliBuildOptions(), cliui.SkipPromptOption()),
|
Options: clibase.OptionSet{cliui.SkipPromptOption()},
|
||||||
Handler: func(inv *clibase.Invocation) error {
|
Handler: func(inv *clibase.Invocation) error {
|
||||||
workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0])
|
workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
lastBuildParameters, err := client.WorkspaceBuildParameters(inv.Context(), workspace.LatestBuild.ID)
|
build, err := startWorkspace(inv, client, workspace, parameterFlags, WorkspaceStart)
|
||||||
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)
|
|
||||||
// It's possible for a workspace build to fail due to the template requiring starting
|
// It's possible for a workspace build to fail due to the template requiring starting
|
||||||
// workspaces with the active version.
|
// workspaces with the active version.
|
||||||
if cerr, ok := codersdk.AsError(err); ok && cerr.StatusCode() == http.StatusUnauthorized {
|
if cerr, ok := codersdk.AsError(err); ok && cerr.StatusCode() == http.StatusUnauthorized {
|
||||||
build, err = startWorkspaceActiveVersion(inv, client, startWorkspaceActiveVersionArgs{
|
_, _ = 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.")
|
||||||
BuildOptions: buildOptions,
|
build, err = startWorkspace(inv, client, workspace, parameterFlags, WorkspaceUpdate)
|
||||||
LastBuildParameters: lastBuildParameters,
|
|
||||||
PromptBuildOptions: parameterFlags.promptBuildOptions,
|
|
||||||
Workspace: workspace,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("start workspace with active template version: %w", err)
|
return xerrors.Errorf("start workspace with active template version: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -90,75 +56,69 @@ func (r *RootCmd) start() *clibase.Cmd {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd.Options = append(cmd.Options, parameterFlags.allOptions()...)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
type prepStartWorkspaceArgs struct {
|
func buildWorkspaceStartRequest(inv *clibase.Invocation, client *codersdk.Client, workspace codersdk.Workspace, parameterFlags workspaceParameterFlags, action WorkspaceCLIAction) (codersdk.CreateWorkspaceBuildRequest, error) {
|
||||||
Action WorkspaceCLIAction
|
version := workspace.LatestBuild.TemplateVersionID
|
||||||
TemplateVersionID uuid.UUID
|
if workspace.AutomaticUpdates == codersdk.AutomaticUpdatesAlways || action == WorkspaceUpdate {
|
||||||
|
version = workspace.TemplateActiveVersionID
|
||||||
LastBuildParameters []codersdk.WorkspaceBuildParameter
|
if version != workspace.LatestBuild.TemplateVersionID {
|
||||||
|
action = WorkspaceUpdate
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
templateVersionParameters, err := client.TemplateVersionRichParameters(inv.Context(), templateVersion.ID)
|
lastBuildParameters, err := client.WorkspaceBuildParameters(inv.Context(), workspace.LatestBuild.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, xerrors.Errorf("get template version rich parameters: %w", err)
|
return codersdk.CreateWorkspaceBuildRequest{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resolver := new(ParameterResolver).
|
buildOptions, err := asWorkspaceBuildParameters(parameterFlags.buildOptions)
|
||||||
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)
|
|
||||||
if err != nil {
|
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{
|
cliRichParameters, err := asWorkspaceBuildParameters(parameterFlags.richParameters)
|
||||||
Action: WorkspaceStart,
|
if err != nil {
|
||||||
TemplateVersionID: template.ActiveVersionID,
|
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,
|
PromptBuildOptions: parameterFlags.promptBuildOptions,
|
||||||
BuildOptions: args.BuildOptions,
|
BuildOptions: buildOptions,
|
||||||
|
PromptRichParameters: parameterFlags.promptRichParameters,
|
||||||
|
RichParameters: cliRichParameters,
|
||||||
|
RichParameterFile: parameterFlags.richParameterFile,
|
||||||
})
|
})
|
||||||
if err != nil {
|
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,
|
Transition: codersdk.WorkspaceTransitionStart,
|
||||||
RichParameterValues: buildParameters,
|
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 {
|
if err != nil {
|
||||||
return codersdk.WorkspaceBuild{}, err
|
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
|
return build, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,52 @@ const (
|
||||||
immutableParameterName = "immutable_parameter"
|
immutableParameterName = "immutable_parameter"
|
||||||
immutableParameterDescription = "This is immutable parameter"
|
immutableParameterDescription = "This is immutable parameter"
|
||||||
immutableParameterValue = "abc"
|
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) {
|
func TestStart(t *testing.T) {
|
||||||
|
@ -147,26 +193,6 @@ func TestStart(t *testing.T) {
|
||||||
func TestStartWithParameters(t *testing.T) {
|
func TestStartWithParameters(t *testing.T) {
|
||||||
t.Parallel()
|
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.Run("DoNotAskForImmutables", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
@ -174,7 +200,7 @@ func TestStartWithParameters(t *testing.T) {
|
||||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||||
owner := coderdtest.CreateFirstUser(t, client)
|
owner := coderdtest.CreateFirstUser(t, client)
|
||||||
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
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)
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||||
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
|
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
|
||||||
workspace := coderdtest.CreateWorkspace(t, member, owner.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
workspace := coderdtest.CreateWorkspace(t, member, owner.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||||||
|
@ -218,4 +244,133 @@ func TestStartWithParameters(t *testing.T) {
|
||||||
Value: immutableParameterValue,
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ USAGE:
|
||||||
$ coder templates init
|
$ coder templates init
|
||||||
|
|
||||||
SUBCOMMANDS:
|
SUBCOMMANDS:
|
||||||
|
autoupdate Toggle auto-update policy for a workspace
|
||||||
config-ssh Add an SSH Host entry for your workspaces "ssh
|
config-ssh Add an SSH Host entry for your workspaces "ssh
|
||||||
coder.workspace"
|
coder.workspace"
|
||||||
create Create a workspace
|
create Create a workspace
|
||||||
|
|
|
@ -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.
|
|
@ -6,12 +6,23 @@ USAGE:
|
||||||
Restart a workspace
|
Restart a workspace
|
||||||
|
|
||||||
OPTIONS:
|
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 string-array, $CODER_BUILD_OPTION
|
||||||
Build option value in the format "name=value".
|
Build option value in the format "name=value".
|
||||||
|
|
||||||
--build-options bool
|
--build-options bool
|
||||||
Prompt for one-time build options defined with ephemeral parameters.
|
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
|
-y, --yes bool
|
||||||
Bypass prompts.
|
Bypass prompts.
|
||||||
|
|
||||||
|
|
|
@ -6,12 +6,23 @@ USAGE:
|
||||||
Start a workspace
|
Start a workspace
|
||||||
|
|
||||||
OPTIONS:
|
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 string-array, $CODER_BUILD_OPTION
|
||||||
Build option value in the format "name=value".
|
Build option value in the format "name=value".
|
||||||
|
|
||||||
--build-options bool
|
--build-options bool
|
||||||
Prompt for one-time build options defined with ephemeral parameters.
|
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
|
-y, --yes bool
|
||||||
Bypass prompts.
|
Bypass prompts.
|
||||||
|
|
||||||
|
|
|
@ -10,11 +10,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *RootCmd) update() *clibase.Cmd {
|
func (r *RootCmd) update() *clibase.Cmd {
|
||||||
var (
|
var parameterFlags workspaceParameterFlags
|
||||||
alwaysPrompt bool
|
|
||||||
|
|
||||||
parameterFlags workspaceParameterFlags
|
|
||||||
)
|
|
||||||
|
|
||||||
client := new(codersdk.Client)
|
client := new(codersdk.Client)
|
||||||
cmd := &clibase.Cmd{
|
cmd := &clibase.Cmd{
|
||||||
|
@ -31,58 +27,16 @@ func (r *RootCmd) update() *clibase.Cmd {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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")
|
_, _ = fmt.Fprintf(inv.Stdout, "Workspace isn't outdated!\n")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
buildOptions, err := asWorkspaceBuildParameters(parameterFlags.buildOptions)
|
build, err := startWorkspace(inv, client, workspace, parameterFlags, WorkspaceUpdate)
|
||||||
if err != nil {
|
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)
|
logs, closer, err := client.WorkspaceBuildLogsAfter(inv.Context(), build.ID, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -99,14 +53,6 @@ func (r *RootCmd) update() *clibase.Cmd {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Options = clibase.OptionSet{
|
cmd.Options = append(cmd.Options, parameterFlags.allOptions()...)
|
||||||
{
|
|
||||||
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()...)
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
|
@ -934,11 +934,8 @@ func MustTransitionWorkspace(t testing.TB, client *codersdk.Client, workspaceID
|
||||||
require.NoError(t, err, "unexpected error fetching workspace")
|
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)
|
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{
|
req := codersdk.CreateWorkspaceBuildRequest{
|
||||||
TemplateVersionID: template.ActiveVersionID,
|
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
|
||||||
Transition: codersdk.WorkspaceTransition(to),
|
Transition: codersdk.WorkspaceTransition(to),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ Coder — A tool for provisioning self-hosted development environments with Terr
|
||||||
|
|
||||||
| Name | Purpose |
|
| 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>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>create</code>](./cli/create.md) | Create a workspace |
|
||||||
| [<code>delete</code>](./cli/delete.md) | Delete a workspace |
|
| [<code>delete</code>](./cli/delete.md) | Delete a workspace |
|
||||||
|
|
|
@ -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.
|
|
@ -12,6 +12,14 @@ coder restart [flags] <workspace>
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
|
### --always-prompt
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| ---- | ----------------- |
|
||||||
|
| Type | <code>bool</code> |
|
||||||
|
|
||||||
|
Always prompt all parameters. Does not pull parameter values from existing workspace.
|
||||||
|
|
||||||
### --build-option
|
### --build-option
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|
@ -29,6 +37,24 @@ Build option value in the format "name=value".
|
||||||
|
|
||||||
Prompt for one-time build options defined with ephemeral parameters.
|
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
|
### -y, --yes
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|
|
|
@ -12,6 +12,14 @@ coder start [flags] <workspace>
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
|
### --always-prompt
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| ---- | ----------------- |
|
||||||
|
| Type | <code>bool</code> |
|
||||||
|
|
||||||
|
Always prompt all parameters. Does not pull parameter values from existing workspace.
|
||||||
|
|
||||||
### --build-option
|
### --build-option
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|
@ -29,6 +37,24 @@ Build option value in the format "name=value".
|
||||||
|
|
||||||
Prompt for one-time build options defined with ephemeral parameters.
|
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
|
### -y, --yes
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|
|
|
@ -569,6 +569,11 @@
|
||||||
"path": "./cli.md",
|
"path": "./cli.md",
|
||||||
"icon_path": "./images/icons/terminal.svg",
|
"icon_path": "./images/icons/terminal.svg",
|
||||||
"children": [
|
"children": [
|
||||||
|
{
|
||||||
|
"title": "autoupdate",
|
||||||
|
"description": "Toggle auto-update policy for a workspace",
|
||||||
|
"path": "cli/autoupdate.md"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "coder",
|
"title": "coder",
|
||||||
"path": "cli.md"
|
"path": "cli.md"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package cli_test
|
package cli_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
@ -154,21 +155,26 @@ func TestStart(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, c.Client, ws.LatestBuild.ID)
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, c.Client, ws.LatestBuild.ID)
|
||||||
|
|
||||||
|
initialTemplateVersion := ws.LatestBuild.TemplateVersionID
|
||||||
|
|
||||||
if cmd == "start" {
|
if cmd == "start" {
|
||||||
// Stop the workspace so that we can start it.
|
// Stop the workspace so that we can start it.
|
||||||
coderdtest.MustTransitionWorkspace(t, c.Client, ws.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop, func(req *codersdk.CreateWorkspaceBuildRequest) {
|
coderdtest.MustTransitionWorkspace(t, c.Client, ws.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||||
req.TemplateVersionID = oldVersion.ID
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
// Start the workspace. Every test permutation should
|
// Start the workspace. Every test permutation should
|
||||||
// pass.
|
// pass.
|
||||||
|
var buf bytes.Buffer
|
||||||
inv, conf := newCLI(t, cmd, ws.Name, "-y")
|
inv, conf := newCLI(t, cmd, ws.Name, "-y")
|
||||||
|
inv.Stdout = &buf
|
||||||
clitest.SetupConfig(t, c.Client, conf)
|
clitest.SetupConfig(t, c.Client, conf)
|
||||||
err = inv.Run()
|
err = inv.Run()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ws = coderdtest.MustWorkspace(t, c.Client, ws.ID)
|
ws = coderdtest.MustWorkspace(t, c.Client, ws.ID)
|
||||||
require.Equal(t, c.ExpectedVersion, ws.LatestBuild.TemplateVersionID)
|
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.")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue