From e8424b21c35385b40f10d495c78ed791f1cdb5d8 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 11 Jul 2023 12:39:55 +0200 Subject: [PATCH 01/14] Skip ephemeral parameters while creating/updating the workspace --- cli/create.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cli/create.go b/cli/create.go index b7ffbec5819cb..8cc919d04decf 100644 --- a/cli/create.go +++ b/cli/create.go @@ -240,6 +240,10 @@ func prepWorkspaceBuild(inv *clibase.Invocation, client *codersdk.Client, args p richParameters := make([]codersdk.WorkspaceBuildParameter, 0) PromptRichParamLoop: for _, templateVersionParameter := range templateVersionParameters { + if templateVersionParameter.Ephemeral { + continue + } + if !disclaimerPrinted { _, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Paragraph.Render("This template has customizable parameters. Values can be changed after create, but may have unintended side effects (like data loss).")+"\r\n") disclaimerPrinted = true From 8980fcefed8febeaae3b47e9b8857625739c219b Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 11 Jul 2023 12:59:45 +0200 Subject: [PATCH 02/14] golden updates --- cli/create.go | 8 ++++++++ cli/testdata/coder_create_--help.golden | 3 +++ cli/testdata/coder_update_--help.golden | 3 +++ cli/update.go | 11 +++++++++-- 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/cli/create.go b/cli/create.go index 8cc919d04decf..ac0a4458fea3c 100644 --- a/cli/create.go +++ b/cli/create.go @@ -23,6 +23,7 @@ func (r *RootCmd) create() *clibase.Cmd { startAt string stopAfter time.Duration workspaceName string + buildOptions bool ) client := new(codersdk.Client) cmd := &clibase.Cmd{ @@ -123,6 +124,7 @@ func (r *RootCmd) create() *clibase.Cmd { Template: template, RichParameterFile: richParameterFile, NewWorkspaceName: workspaceName, + BuildOptions: buildOptions, }) if err != nil { return xerrors.Errorf("prepare build: %w", err) @@ -189,6 +191,11 @@ func (r *RootCmd) create() *clibase.Cmd { Description: "Specify a duration after which the workspace should shut down (e.g. 8h).", Value: clibase.DurationOf(&stopAfter), }, + clibase.Option{ + Flag: "build-options", + Description: "Prompt for one-time build options defined with ephemeral parameters.", + Value: clibase.BoolOf(&buildOptions), + }, cliui.SkipPromptOption(), ) @@ -202,6 +209,7 @@ type prepWorkspaceBuildArgs struct { NewWorkspaceName string UpdateWorkspace bool + BuildOptions bool WorkspaceID uuid.UUID } diff --git a/cli/testdata/coder_create_--help.golden b/cli/testdata/coder_create_--help.golden index 8d7c4ce9653e5..bf0b40660b2b2 100644 --- a/cli/testdata/coder_create_--help.golden +++ b/cli/testdata/coder_create_--help.golden @@ -3,6 +3,9 @@ Usage: coder create [flags] [name] Create a workspace Options + --build-options bool + Prompt for one-time build options defined with ephemeral parameters. + --rich-parameter-file string, $CODER_RICH_PARAMETER_FILE Specify a file path with values for rich parameters defined in the template. diff --git a/cli/testdata/coder_update_--help.golden b/cli/testdata/coder_update_--help.golden index 4552933cf507e..40e899cd37348 100644 --- a/cli/testdata/coder_update_--help.golden +++ b/cli/testdata/coder_update_--help.golden @@ -9,6 +9,9 @@ Use --always-prompt to change the parameter values of the workspace. Always prompt all parameters. Does not pull parameter values from existing workspace. + --build-options bool + Prompt for one-time build options defined with ephemeral parameters. + --rich-parameter-file string, $CODER_RICH_PARAMETER_FILE Specify a file path with values for rich parameters defined in the template. diff --git a/cli/update.go b/cli/update.go index 4edb22892526d..3bc616f6a94bc 100644 --- a/cli/update.go +++ b/cli/update.go @@ -11,6 +11,7 @@ func (r *RootCmd) update() *clibase.Cmd { var ( richParameterFile string alwaysPrompt bool + buildOptions bool ) client := new(codersdk.Client) @@ -53,6 +54,8 @@ func (r *RootCmd) update() *clibase.Cmd { UpdateWorkspace: true, WorkspaceID: workspace.LatestBuild.ID, + + BuildOptions: buildOptions, }) if err != nil { return nil @@ -86,8 +89,7 @@ func (r *RootCmd) update() *clibase.Cmd { { Flag: "always-prompt", Description: "Always prompt all parameters. Does not pull parameter values from existing workspace.", - - Value: clibase.BoolOf(&alwaysPrompt), + Value: clibase.BoolOf(&alwaysPrompt), }, { Flag: "rich-parameter-file", @@ -95,6 +97,11 @@ func (r *RootCmd) update() *clibase.Cmd { Env: "CODER_RICH_PARAMETER_FILE", Value: clibase.StringOf(&richParameterFile), }, + { + Flag: "build-options", + Description: "Prompt for one-time build options defined with ephemeral parameters.", + Value: clibase.BoolOf(&buildOptions), + }, } return cmd } From 4e58e17229fdabefcd2cf2062d2b388f67ac3efb Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 11 Jul 2023 13:46:22 +0200 Subject: [PATCH 03/14] build option is yellow --- cli/cliui/parameter.go | 5 +++++ cli/create.go | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/cli/cliui/parameter.go b/cli/cliui/parameter.go index 1d43b7fc4dde8..f0f4fd99e45d0 100644 --- a/cli/cliui/parameter.go +++ b/cli/cliui/parameter.go @@ -15,7 +15,12 @@ func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.Te label = templateVersionParameter.DisplayName } + if templateVersionParameter.Ephemeral { + label += DefaultStyles.Warn.Render(" (build option)") + } + _, _ = fmt.Fprintln(inv.Stdout, DefaultStyles.Bold.Render(label)) + if templateVersionParameter.DescriptionPlaintext != "" { _, _ = fmt.Fprintln(inv.Stdout, " "+strings.TrimSpace(strings.Join(strings.Split(templateVersionParameter.DescriptionPlaintext, "\n"), "\n "))+"\n") } diff --git a/cli/create.go b/cli/create.go index ac0a4458fea3c..3b3cea1fec63b 100644 --- a/cli/create.go +++ b/cli/create.go @@ -248,7 +248,7 @@ func prepWorkspaceBuild(inv *clibase.Invocation, client *codersdk.Client, args p richParameters := make([]codersdk.WorkspaceBuildParameter, 0) PromptRichParamLoop: for _, templateVersionParameter := range templateVersionParameters { - if templateVersionParameter.Ephemeral { + if !args.BuildOptions && templateVersionParameter.Ephemeral { continue } From 3baef188786259a70d4985e58a329e361484ff0f Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 11 Jul 2023 13:56:38 +0200 Subject: [PATCH 04/14] make gen --- docs/cli/create.md | 8 ++++++++ docs/cli/update.md | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/docs/cli/create.md b/docs/cli/create.md index 1a76673c86945..d959908075781 100644 --- a/docs/cli/create.md +++ b/docs/cli/create.md @@ -12,6 +12,14 @@ coder create [flags] [name] ## Options +### --build-options + +| | | +| ---- | ----------------- | +| Type | bool | + +Prompt for one-time build options defined with ephemeral parameters. + ### --rich-parameter-file | | | diff --git a/docs/cli/update.md b/docs/cli/update.md index e43502a41bf47..0b3d31a9755b8 100644 --- a/docs/cli/update.md +++ b/docs/cli/update.md @@ -26,6 +26,14 @@ Use --always-prompt to change the parameter values of the workspace. Always prompt all parameters. Does not pull parameter values from existing workspace. +### --build-options + +| | | +| ---- | ----------------- | +| Type | bool | + +Prompt for one-time build options defined with ephemeral parameters. + ### --rich-parameter-file | | | From 0a7578c9f8910945d4550b8b6dbbb3d9d3e2910a Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 11 Jul 2023 14:43:02 +0200 Subject: [PATCH 05/14] Unit tests for build options --- cli/create.go | 2 +- cli/create_test.go | 47 +++++++++++++++++++++++++++++++++++++- cli/update.go | 2 +- cli/update_test.go | 56 +++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 103 insertions(+), 4 deletions(-) diff --git a/cli/create.go b/cli/create.go index 3b3cea1fec63b..18d6a03a5d5c9 100644 --- a/cli/create.go +++ b/cli/create.go @@ -258,7 +258,7 @@ PromptRichParamLoop: } // Param file is all or nothing - if !useParamFile { + if !useParamFile && !templateVersionParameter.Ephemeral { for _, e := range args.ExistingRichParams { if e.Name == templateVersionParameter.Name { // If the param already exists, we do not need to prompt it again. diff --git a/cli/create_test.go b/cli/create_test.go index 176dce82656e6..6206e98a5846c 100644 --- a/cli/create_test.go +++ b/cli/create_test.go @@ -195,9 +195,13 @@ func TestCreateWithRichParameters(t *testing.T) { secondParameterDescription = "This is second parameter" secondParameterValue = "2" + ephemeralParameterName = "ephemeral_parameter" + ephemeralParameterDescription = "This is ephemeral parameter" + ephemeralParameterValue = "3" + immutableParameterName = "third_parameter" immutableParameterDescription = "This is not mutable parameter" - immutableParameterValue = "3" + immutableParameterValue = "4" ) echoResponses := &echo.Responses{ @@ -209,6 +213,7 @@ func TestCreateWithRichParameters(t *testing.T) { Parameters: []*proto.RichParameter{ {Name: firstParameterName, Description: firstParameterDescription, Mutable: true}, {Name: secondParameterName, DisplayName: secondParameterDisplayName, Description: secondParameterDescription, Mutable: true}, + {Name: ephemeralParameterName, Description: ephemeralParameterDescription, Mutable: true, Ephemeral: true}, {Name: immutableParameterName, Description: immutableParameterDescription, Mutable: false}, }, }, @@ -300,6 +305,46 @@ func TestCreateWithRichParameters(t *testing.T) { } <-doneChan }) + + t.Run("BuildOptions", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + + inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name, "--build-options") + clitest.SetupConfig(t, client, root) + doneChan := make(chan struct{}) + pty := ptytest.New(t).Attach(inv) + go func() { + defer close(doneChan) + err := inv.Run() + assert.NoError(t, err) + }() + + matches := []string{ + ephemeralParameterDescription, ephemeralParameterValue, + firstParameterDescription, firstParameterValue, + secondParameterDisplayName, "", + secondParameterDescription, secondParameterValue, + immutableParameterDescription, immutableParameterValue, + "Confirm create?", "yes", + } + for i := 0; i < len(matches); i += 2 { + match := matches[i] + value := matches[i+1] + pty.ExpectMatch(match) + + if value != "" { + pty.WriteLine(value) + } + } + <-doneChan + }) } func TestCreateValidateRichParameters(t *testing.T) { diff --git a/cli/update.go b/cli/update.go index 3bc616f6a94bc..2e1e5f0351bb8 100644 --- a/cli/update.go +++ b/cli/update.go @@ -29,7 +29,7 @@ func (r *RootCmd) update() *clibase.Cmd { if err != nil { return err } - if !workspace.Outdated && !alwaysPrompt { + if !workspace.Outdated && !alwaysPrompt && !buildOptions { _, _ = fmt.Fprintf(inv.Stdout, "Workspace isn't outdated!\n") return nil } diff --git a/cli/update_test.go b/cli/update_test.go index 14a44db4cabbb..a88c8d3430d9d 100644 --- a/cli/update_test.go +++ b/cli/update_test.go @@ -90,9 +90,13 @@ func TestUpdateWithRichParameters(t *testing.T) { secondParameterDescription = "This is second parameter" secondParameterValue = "2" + ephemeralParameterName = "ephemeral_parameter" + ephemeralParameterDescription = "This is ephemeral parameter" + ephemeralParameterValue = "3" + immutableParameterName = "immutable_parameter" immutableParameterDescription = "This is not mutable parameter" - immutableParameterValue = "3" + immutableParameterValue = "4" ) echoResponses := &echo.Responses{ @@ -105,6 +109,7 @@ func TestUpdateWithRichParameters(t *testing.T) { {Name: firstParameterName, Description: firstParameterDescription, Mutable: true}, {Name: immutableParameterName, Description: immutableParameterDescription, Mutable: false}, {Name: secondParameterName, Description: secondParameterDescription, Mutable: true}, + {Name: ephemeralParameterName, Description: ephemeralParameterDescription, Mutable: true, Ephemeral: true}, }, }, }, @@ -166,6 +171,55 @@ func TestUpdateWithRichParameters(t *testing.T) { } <-doneChan }) + + t.Run("BuildOptions", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + + tempDir := t.TempDir() + removeTmpDirUntilSuccessAfterTest(t, tempDir) + parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml") + _, _ = parameterFile.WriteString( + firstParameterName + ": " + firstParameterValue + "\n" + + immutableParameterName + ": " + immutableParameterValue + "\n" + + secondParameterName + ": " + secondParameterValue) + + inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name, "--rich-parameter-file", parameterFile.Name(), "-y") + clitest.SetupConfig(t, client, root) + err := inv.Run() + assert.NoError(t, err) + + inv, root = clitest.New(t, "update", "my-workspace", "--build-options") + clitest.SetupConfig(t, client, root) + + doneChan := make(chan struct{}) + pty := ptytest.New(t).Attach(inv) + go func() { + defer close(doneChan) + err := inv.Run() + assert.NoError(t, err) + }() + + matches := []string{ + ephemeralParameterDescription, ephemeralParameterValue, + "Planning workspace", "", + } + for i := 0; i < len(matches); i += 2 { + match := matches[i] + value := matches[i+1] + pty.ExpectMatch(match) + if value != "" { + pty.WriteLine(value) + } + } + <-doneChan + }) } func TestUpdateValidateRichParameters(t *testing.T) { From fdea6b2c4ecc27a9f5cbfac08de593e1748e08cb Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 11 Jul 2023 15:16:55 +0200 Subject: [PATCH 06/14] Start with options --- cli/start.go | 72 +++++++++++++++++++++++++- cli/testdata/coder_start_--help.golden | 3 ++ docs/cli/start.md | 8 +++ 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/cli/start.go b/cli/start.go index 1fb619abf4b77..a0d1d612ef91a 100644 --- a/cli/start.go +++ b/cli/start.go @@ -4,12 +4,16 @@ import ( "fmt" "time" + "golang.org/x/xerrors" + "github.com/coder/coder/cli/clibase" "github.com/coder/coder/cli/cliui" "github.com/coder/coder/codersdk" ) func (r *RootCmd) start() *clibase.Cmd { + var buildOptions bool + client := new(codersdk.Client) cmd := &clibase.Cmd{ Annotations: workspaceCommand, @@ -20,6 +24,11 @@ func (r *RootCmd) start() *clibase.Cmd { r.InitClient(client), ), Options: clibase.OptionSet{ + { + Flag: "build-options", + Description: "Prompt for one-time build options defined with ephemeral parameters.", + Value: clibase.BoolOf(&buildOptions), + }, cliui.SkipPromptOption(), }, Handler: func(inv *clibase.Invocation) error { @@ -27,8 +36,23 @@ func (r *RootCmd) start() *clibase.Cmd { if err != nil { return err } + + template, err := client.Template(inv.Context(), workspace.TemplateID) + if err != nil { + return nil + } + + buildParams, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{ + Template: template, + BuildOptions: buildOptions, + }) + if err != nil { + return nil + } + build, err := client.CreateWorkspaceBuild(inv.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{ - Transition: codersdk.WorkspaceTransitionStart, + Transition: codersdk.WorkspaceTransitionStart, + RichParameterValues: buildParams.richParameters, }) if err != nil { return err @@ -45,3 +69,49 @@ func (r *RootCmd) start() *clibase.Cmd { } return cmd } + +type prepStartWorkspaceArgs struct { + Template codersdk.Template + BuildOptions bool +} + +func prepStartWorkspace(inv *clibase.Invocation, client *codersdk.Client, args prepStartWorkspaceArgs) (*buildParameters, error) { + ctx := inv.Context() + + templateVersion, err := client.TemplateVersion(ctx, args.Template.ActiveVersionID) + if err != nil { + return nil, err + } + + templateVersionParameters, err := client.TemplateVersionRichParameters(inv.Context(), templateVersion.ID) + if err != nil { + return nil, xerrors.Errorf("get template version rich parameters: %w", err) + } + + richParameters := make([]codersdk.WorkspaceBuildParameter, 0) + if !args.BuildOptions { + return &buildParameters{ + richParameters: richParameters, + }, nil + } + + for _, templateVersionParameter := range templateVersionParameters { + if !templateVersionParameter.Ephemeral || !templateVersionParameter.Mutable { + continue + } + + parameterValue, err := cliui.RichParameter(inv, templateVersionParameter) + if err != nil { + return nil, err + } + + richParameters = append(richParameters, codersdk.WorkspaceBuildParameter{ + Name: templateVersionParameter.Name, + Value: parameterValue, + }) + } + + return &buildParameters{ + richParameters: richParameters, + }, nil +} diff --git a/cli/testdata/coder_start_--help.golden b/cli/testdata/coder_start_--help.golden index 5b1083431a923..aa447240e9bbb 100644 --- a/cli/testdata/coder_start_--help.golden +++ b/cli/testdata/coder_start_--help.golden @@ -3,6 +3,9 @@ Usage: coder start [flags] Start a workspace Options + --build-options bool + Prompt for one-time build options defined with ephemeral parameters. + -y, --yes bool Bypass prompts. diff --git a/docs/cli/start.md b/docs/cli/start.md index ed847135fe20a..8c3fd7f71276e 100644 --- a/docs/cli/start.md +++ b/docs/cli/start.md @@ -12,6 +12,14 @@ coder start [flags] ## Options +### --build-options + +| | | +| ---- | ----------------- | +| Type | bool | + +Prompt for one-time build options defined with ephemeral parameters. + ### -y, --yes | | | From 009a534c5f521e2149756fe937dd59631bc1c653 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 11 Jul 2023 16:15:17 +0200 Subject: [PATCH 07/14] unit tests for start --- cli/start_test.go | 85 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 cli/start_test.go diff --git a/cli/start_test.go b/cli/start_test.go new file mode 100644 index 0000000000000..7f6d3911ef8bb --- /dev/null +++ b/cli/start_test.go @@ -0,0 +1,85 @@ +package cli_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/coder/coder/cli/clitest" + "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/provisioner/echo" + "github.com/coder/coder/provisionersdk/proto" + "github.com/coder/coder/pty/ptytest" +) + +const ( + ephemeralParameterName = "ephemeral_parameter" + ephemeralParameterDescription = "This is ephemeral parameter" + ephemeralParameterValue = "3" +) + +func TestStart(t *testing.T) { + t.Parallel() + + echoResponses := &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: []*proto.Provision_Response{ + { + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Parameters: []*proto.RichParameter{ + { + Name: ephemeralParameterName, + Description: ephemeralParameterDescription, + Mutable: true, + Ephemeral: true, + }, + }, + }, + }, + }, + }, + ProvisionApply: []*proto.Provision_Response{{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{}, + }, + }}, + } + + t.Run("BuildOptions", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + + inv, root := clitest.New(t, "start", workspace.Name, "--build-options") + clitest.SetupConfig(t, client, root) + doneChan := make(chan struct{}) + pty := ptytest.New(t).Attach(inv) + go func() { + defer close(doneChan) + err := inv.Run() + assert.NoError(t, err) + }() + + matches := []string{ + ephemeralParameterDescription, ephemeralParameterValue, + "workspace has been started", "", + } + for i := 0; i < len(matches); i += 2 { + match := matches[i] + value := matches[i+1] + pty.ExpectMatch(match) + + if value != "" { + pty.WriteLine(value) + } + } + <-doneChan + }) +} From 2bdcf27af67f47b6c14da18cbe761d7bdd49a2fa Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 11 Jul 2023 16:32:00 +0200 Subject: [PATCH 08/14] Restart workspace --- cli/restart.go | 33 +++++++++++++++++++----- cli/testdata/coder_restart_--help.golden | 3 +++ docs/cli/restart.md | 8 ++++++ 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/cli/restart.go b/cli/restart.go index 7a139d8206bb9..a1f12cc75c8e0 100644 --- a/cli/restart.go +++ b/cli/restart.go @@ -10,6 +10,8 @@ import ( ) func (r *RootCmd) restart() *clibase.Cmd { + var buildOptions bool + client := new(codersdk.Client) cmd := &clibase.Cmd{ Annotations: workspaceCommand, @@ -20,21 +22,39 @@ func (r *RootCmd) restart() *clibase.Cmd { r.InitClient(client), ), Options: clibase.OptionSet{ + { + Flag: "build-options", + Description: "Prompt for one-time build options defined with ephemeral parameters.", + Value: clibase.BoolOf(&buildOptions), + }, cliui.SkipPromptOption(), }, Handler: func(inv *clibase.Invocation) error { ctx := inv.Context() out := inv.Stdout - _, err := cliui.Prompt(inv, cliui.PromptOptions{ - Text: "Confirm restart workspace?", - IsConfirm: true, - }) + workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0]) if err != nil { return err } - workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0]) + template, err := client.Template(inv.Context(), workspace.TemplateID) + if err != nil { + return nil + } + + buildParams, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{ + Template: template, + BuildOptions: buildOptions, + }) + if err != nil { + return nil + } + + _, err = cliui.Prompt(inv, cliui.PromptOptions{ + Text: "Confirm restart workspace?", + IsConfirm: true, + }) if err != nil { return err } @@ -51,7 +71,8 @@ func (r *RootCmd) restart() *clibase.Cmd { } build, err = client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ - Transition: codersdk.WorkspaceTransitionStart, + Transition: codersdk.WorkspaceTransitionStart, + RichParameterValues: buildParams.richParameters, }) if err != nil { return err diff --git a/cli/testdata/coder_restart_--help.golden b/cli/testdata/coder_restart_--help.golden index 828230987f114..c2079b9065dca 100644 --- a/cli/testdata/coder_restart_--help.golden +++ b/cli/testdata/coder_restart_--help.golden @@ -3,6 +3,9 @@ Usage: coder restart [flags] Restart a workspace Options + --build-options bool + Prompt for one-time build options defined with ephemeral parameters. + -y, --yes bool Bypass prompts. diff --git a/docs/cli/restart.md b/docs/cli/restart.md index 9966d61c10257..72daa5dec405d 100644 --- a/docs/cli/restart.md +++ b/docs/cli/restart.md @@ -12,6 +12,14 @@ coder restart [flags] ## Options +### --build-options + +| | | +| ---- | ----------------- | +| Type | bool | + +Prompt for one-time build options defined with ephemeral parameters. + ### -y, --yes | | | From dbd29db96e7a4104fb77bdbf1eff5b76843cb5cf Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 11 Jul 2023 16:34:12 +0200 Subject: [PATCH 09/14] unit tests for restart --- cli/restart_test.go | 68 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/cli/restart_test.go b/cli/restart_test.go index d1dfa6bd3b497..c16a5f30b6038 100644 --- a/cli/restart_test.go +++ b/cli/restart_test.go @@ -3,10 +3,13 @@ package cli_test import ( "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/coder/coder/cli/clitest" "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/provisioner/echo" + "github.com/coder/coder/provisionersdk/proto" "github.com/coder/coder/pty/ptytest" "github.com/coder/coder/testutil" ) @@ -14,6 +17,31 @@ import ( func TestRestart(t *testing.T) { t.Parallel() + echoResponses := &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: []*proto.Provision_Response{ + { + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Parameters: []*proto.RichParameter{ + { + Name: ephemeralParameterName, + Description: ephemeralParameterDescription, + Mutable: true, + Ephemeral: true, + }, + }, + }, + }, + }, + }, + ProvisionApply: []*proto.Provision_Response{{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{}, + }, + }}, + } + t.Run("OK", func(t *testing.T) { t.Parallel() @@ -43,4 +71,44 @@ func TestRestart(t *testing.T) { err := <-done require.NoError(t, err, "execute failed") }) + + t.Run("BuildOptions", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + + inv, root := clitest.New(t, "restart", workspace.Name, "--build-options") + clitest.SetupConfig(t, client, root) + doneChan := make(chan struct{}) + pty := ptytest.New(t).Attach(inv) + go func() { + defer close(doneChan) + err := inv.Run() + assert.NoError(t, err) + }() + + matches := []string{ + ephemeralParameterDescription, ephemeralParameterValue, + "Confirm restart workspace?", "yes", + "Stopping workspace", "", + "Starting workspace", "", + "workspace has been restarted", "", + } + for i := 0; i < len(matches); i += 2 { + match := matches[i] + value := matches[i+1] + pty.ExpectMatch(match) + + if value != "" { + pty.WriteLine(value) + } + } + <-doneChan + }) } From 9e5a0745ff5a2fb541db7261481f0827d0a18d94 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 13 Jul 2023 11:04:41 +0200 Subject: [PATCH 10/14] Use workspaceBuildFlags --- cli/create.go | 11 ++++------- cli/restart.go | 13 +++---------- cli/start.go | 28 ++++++++++++++++++---------- cli/update.go | 13 +++++-------- 4 files changed, 30 insertions(+), 35 deletions(-) diff --git a/cli/create.go b/cli/create.go index 18d6a03a5d5c9..bb2b9647bfbde 100644 --- a/cli/create.go +++ b/cli/create.go @@ -23,7 +23,8 @@ func (r *RootCmd) create() *clibase.Cmd { startAt string stopAfter time.Duration workspaceName string - buildOptions bool + + parameterFlags workspaceParameterFlags ) client := new(codersdk.Client) cmd := &clibase.Cmd{ @@ -124,7 +125,7 @@ func (r *RootCmd) create() *clibase.Cmd { Template: template, RichParameterFile: richParameterFile, NewWorkspaceName: workspaceName, - BuildOptions: buildOptions, + BuildOptions: parameterFlags.buildOptions, }) if err != nil { return xerrors.Errorf("prepare build: %w", err) @@ -191,13 +192,9 @@ func (r *RootCmd) create() *clibase.Cmd { Description: "Specify a duration after which the workspace should shut down (e.g. 8h).", Value: clibase.DurationOf(&stopAfter), }, - clibase.Option{ - Flag: "build-options", - Description: "Prompt for one-time build options defined with ephemeral parameters.", - Value: clibase.BoolOf(&buildOptions), - }, cliui.SkipPromptOption(), ) + cmd.Options = append(cmd.Options, parameterFlags.options()...) return cmd } diff --git a/cli/restart.go b/cli/restart.go index a1f12cc75c8e0..e89746bf1e130 100644 --- a/cli/restart.go +++ b/cli/restart.go @@ -10,7 +10,7 @@ import ( ) func (r *RootCmd) restart() *clibase.Cmd { - var buildOptions bool + var parameterFlags workspaceParameterFlags client := new(codersdk.Client) cmd := &clibase.Cmd{ @@ -21,14 +21,7 @@ func (r *RootCmd) restart() *clibase.Cmd { clibase.RequireNArgs(1), r.InitClient(client), ), - Options: clibase.OptionSet{ - { - Flag: "build-options", - Description: "Prompt for one-time build options defined with ephemeral parameters.", - Value: clibase.BoolOf(&buildOptions), - }, - cliui.SkipPromptOption(), - }, + Options: append(parameterFlags.options(), cliui.SkipPromptOption()), Handler: func(inv *clibase.Invocation) error { ctx := inv.Context() out := inv.Stdout @@ -45,7 +38,7 @@ func (r *RootCmd) restart() *clibase.Cmd { buildParams, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{ Template: template, - BuildOptions: buildOptions, + BuildOptions: parameterFlags.buildOptions, }) if err != nil { return nil diff --git a/cli/start.go b/cli/start.go index a0d1d612ef91a..3c01c22d92502 100644 --- a/cli/start.go +++ b/cli/start.go @@ -11,8 +11,23 @@ import ( "github.com/coder/coder/codersdk" ) +// workspaceParameterFlags are used by "start", "restart", "create", and "update". +type workspaceParameterFlags struct { + buildOptions bool +} + +func (wpf *workspaceParameterFlags) options() []clibase.Option { + return clibase.OptionSet{ + { + Flag: "build-options", + Description: "Prompt for one-time build options defined with ephemeral parameters.", + Value: clibase.BoolOf(&wpf.buildOptions), + }, + } +} + func (r *RootCmd) start() *clibase.Cmd { - var buildOptions bool + var parameterFlags workspaceParameterFlags client := new(codersdk.Client) cmd := &clibase.Cmd{ @@ -23,14 +38,7 @@ func (r *RootCmd) start() *clibase.Cmd { clibase.RequireNArgs(1), r.InitClient(client), ), - Options: clibase.OptionSet{ - { - Flag: "build-options", - Description: "Prompt for one-time build options defined with ephemeral parameters.", - Value: clibase.BoolOf(&buildOptions), - }, - cliui.SkipPromptOption(), - }, + Options: append(parameterFlags.options(), cliui.SkipPromptOption()), Handler: func(inv *clibase.Invocation) error { workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0]) if err != nil { @@ -44,7 +52,7 @@ func (r *RootCmd) start() *clibase.Cmd { buildParams, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{ Template: template, - BuildOptions: buildOptions, + BuildOptions: parameterFlags.buildOptions, }) if err != nil { return nil diff --git a/cli/update.go b/cli/update.go index 2e1e5f0351bb8..f65577b7ae26d 100644 --- a/cli/update.go +++ b/cli/update.go @@ -11,7 +11,8 @@ func (r *RootCmd) update() *clibase.Cmd { var ( richParameterFile string alwaysPrompt bool - buildOptions bool + + parameterFlags workspaceParameterFlags ) client := new(codersdk.Client) @@ -29,7 +30,7 @@ func (r *RootCmd) update() *clibase.Cmd { if err != nil { return err } - if !workspace.Outdated && !alwaysPrompt && !buildOptions { + if !workspace.Outdated && !alwaysPrompt && !parameterFlags.buildOptions { _, _ = fmt.Fprintf(inv.Stdout, "Workspace isn't outdated!\n") return nil } @@ -55,7 +56,7 @@ func (r *RootCmd) update() *clibase.Cmd { UpdateWorkspace: true, WorkspaceID: workspace.LatestBuild.ID, - BuildOptions: buildOptions, + BuildOptions: parameterFlags.buildOptions, }) if err != nil { return nil @@ -97,11 +98,7 @@ func (r *RootCmd) update() *clibase.Cmd { Env: "CODER_RICH_PARAMETER_FILE", Value: clibase.StringOf(&richParameterFile), }, - { - Flag: "build-options", - Description: "Prompt for one-time build options defined with ephemeral parameters.", - Value: clibase.BoolOf(&buildOptions), - }, } + cmd.Options = append(cmd.Options, parameterFlags.options()...) return cmd } From 1d28bb461c6ba2246c0b3d7dcc9c101592da9b0f Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 13 Jul 2023 11:25:49 +0200 Subject: [PATCH 11/14] templateVersionParameter.Mutable --- cli/start.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/start.go b/cli/start.go index 3c01c22d92502..a941d0fbe32ff 100644 --- a/cli/start.go +++ b/cli/start.go @@ -104,7 +104,7 @@ func prepStartWorkspace(inv *clibase.Invocation, client *codersdk.Client, args p } for _, templateVersionParameter := range templateVersionParameters { - if !templateVersionParameter.Ephemeral || !templateVersionParameter.Mutable { + if !templateVersionParameter.Ephemeral { continue } From 159161527ab96ba060878674805a78a4bd7afa98 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 13 Jul 2023 11:30:53 +0200 Subject: [PATCH 12/14] return nil --- cli/restart.go | 4 ++-- cli/start.go | 4 ++-- cli/update.go | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cli/restart.go b/cli/restart.go index e89746bf1e130..4cff7ac7571d7 100644 --- a/cli/restart.go +++ b/cli/restart.go @@ -33,7 +33,7 @@ func (r *RootCmd) restart() *clibase.Cmd { template, err := client.Template(inv.Context(), workspace.TemplateID) if err != nil { - return nil + return err } buildParams, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{ @@ -41,7 +41,7 @@ func (r *RootCmd) restart() *clibase.Cmd { BuildOptions: parameterFlags.buildOptions, }) if err != nil { - return nil + return err } _, err = cliui.Prompt(inv, cliui.PromptOptions{ diff --git a/cli/start.go b/cli/start.go index a941d0fbe32ff..b576921c28068 100644 --- a/cli/start.go +++ b/cli/start.go @@ -47,7 +47,7 @@ func (r *RootCmd) start() *clibase.Cmd { template, err := client.Template(inv.Context(), workspace.TemplateID) if err != nil { - return nil + return err } buildParams, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{ @@ -55,7 +55,7 @@ func (r *RootCmd) start() *clibase.Cmd { BuildOptions: parameterFlags.buildOptions, }) if err != nil { - return nil + return err } build, err := client.CreateWorkspaceBuild(inv.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{ diff --git a/cli/update.go b/cli/update.go index f65577b7ae26d..64710217bb996 100644 --- a/cli/update.go +++ b/cli/update.go @@ -36,14 +36,14 @@ func (r *RootCmd) update() *clibase.Cmd { } template, err := client.Template(inv.Context(), workspace.TemplateID) if err != nil { - return nil + return err } var existingRichParams []codersdk.WorkspaceBuildParameter if !alwaysPrompt { existingRichParams, err = client.WorkspaceBuildParameters(inv.Context(), workspace.LatestBuild.ID) if err != nil { - return nil + return err } } @@ -59,7 +59,7 @@ func (r *RootCmd) update() *clibase.Cmd { BuildOptions: parameterFlags.buildOptions, }) if err != nil { - return nil + return err } build, err := client.CreateWorkspaceBuild(inv.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{ From 6fd3da54ee8ac29d7f13826cda6337d4f609ef10 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 13 Jul 2023 12:13:50 +0200 Subject: [PATCH 13/14] Verify if build option is set --- cli/create_test.go | 16 +++++++++++++++- cli/restart_test.go | 15 +++++++++++++++ cli/start_test.go | 17 +++++++++++++++++ cli/update_test.go | 20 ++++++++++++++++++-- 4 files changed, 65 insertions(+), 3 deletions(-) diff --git a/cli/create_test.go b/cli/create_test.go index 6206e98a5846c..753c8b9e2d900 100644 --- a/cli/create_test.go +++ b/cli/create_test.go @@ -316,7 +316,8 @@ func TestCreateWithRichParameters(t *testing.T) { template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name, "--build-options") + const workspaceName = "my-workspace" + inv, root := clitest.New(t, "create", workspaceName, "--template", template.Name, "--build-options") clitest.SetupConfig(t, client, root) doneChan := make(chan struct{}) pty := ptytest.New(t).Attach(inv) @@ -344,6 +345,19 @@ func TestCreateWithRichParameters(t *testing.T) { } } <-doneChan + + // Verify if build option is set + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() + + workspace, err := client.WorkspaceByOwnerAndName(ctx, user.UserID.String(), workspaceName, 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: ephemeralParameterName, + Value: ephemeralParameterValue, + }) }) } diff --git a/cli/restart_test.go b/cli/restart_test.go index c16a5f30b6038..83b066e4defc5 100644 --- a/cli/restart_test.go +++ b/cli/restart_test.go @@ -1,6 +1,7 @@ package cli_test import ( + "context" "testing" "github.com/stretchr/testify/assert" @@ -8,6 +9,7 @@ import ( "github.com/coder/coder/cli/clitest" "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/codersdk" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" "github.com/coder/coder/pty/ptytest" @@ -110,5 +112,18 @@ func TestRestart(t *testing.T) { } } <-doneChan + + // Verify if build option is set + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() + + workspace, err := client.WorkspaceByOwnerAndName(ctx, user.UserID.String(), 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: ephemeralParameterName, + Value: ephemeralParameterValue, + }) }) } diff --git a/cli/start_test.go b/cli/start_test.go index 7f6d3911ef8bb..a302fe2ac1c40 100644 --- a/cli/start_test.go +++ b/cli/start_test.go @@ -4,12 +4,16 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/context" "github.com/coder/coder/cli/clitest" "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/codersdk" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" "github.com/coder/coder/pty/ptytest" + "github.com/coder/coder/testutil" ) const ( @@ -81,5 +85,18 @@ func TestStart(t *testing.T) { } } <-doneChan + + // Verify if build option is set + 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: ephemeralParameterName, + Value: ephemeralParameterValue, + }) }) } diff --git a/cli/update_test.go b/cli/update_test.go index a88c8d3430d9d..886adf9bea264 100644 --- a/cli/update_test.go +++ b/cli/update_test.go @@ -16,6 +16,7 @@ import ( "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" "github.com/coder/coder/pty/ptytest" + "github.com/coder/coder/testutil" ) func TestUpdate(t *testing.T) { @@ -190,12 +191,14 @@ func TestUpdateWithRichParameters(t *testing.T) { immutableParameterName + ": " + immutableParameterValue + "\n" + secondParameterName + ": " + secondParameterValue) - inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name, "--rich-parameter-file", parameterFile.Name(), "-y") + const workspaceName = "my-workspace" + + inv, root := clitest.New(t, "create", workspaceName, "--template", template.Name, "--rich-parameter-file", parameterFile.Name(), "-y") clitest.SetupConfig(t, client, root) err := inv.Run() assert.NoError(t, err) - inv, root = clitest.New(t, "update", "my-workspace", "--build-options") + inv, root = clitest.New(t, "update", workspaceName, "--build-options") clitest.SetupConfig(t, client, root) doneChan := make(chan struct{}) @@ -219,6 +222,19 @@ func TestUpdateWithRichParameters(t *testing.T) { } } <-doneChan + + // Verify if build option is set + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() + + workspace, err := client.WorkspaceByOwnerAndName(ctx, user.UserID.String(), workspaceName, 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: ephemeralParameterName, + Value: ephemeralParameterValue, + }) }) } From fb6d5a1ce2b470817f653fadf64078dd91c4bf9c Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 13 Jul 2023 12:33:11 +0200 Subject: [PATCH 14/14] docs --- docs/templates/parameters.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/templates/parameters.md b/docs/templates/parameters.md index 321ee5f79ff9f..7304b69ccf591 100644 --- a/docs/templates/parameters.md +++ b/docs/templates/parameters.md @@ -149,6 +149,24 @@ data "coder_parameter" "region" { It is allowed to modify the mutability state anytime. In case of emergency, template authors can temporarily allow for changing immutable parameters to fix an operational issue, but it is not advised to overuse this opportunity. +## Ephemeral parameters + +Ephemeral parameters are introduced to users in the form of "build options." This functionality can be used to model +specific behaviors within a Coder workspace, such as reverting to a previous image, restoring from a volume snapshot, or +building a project without utilizing cache. + +As these parameters are ephemeral in nature, subsequent builds will proceed in the standard manner. + +```hcl +data "coder_parameter" "force_rebuild" { + name = "force_rebuild" + type = "bool" + description = "Rebuild the Docker image rather than use the cached one." + mutable = true + ephemeral = true +} +``` + ## Validation Rich parameters support multiple validation modes - min, max, monotonic numbers, and regular expressions.