diff --git a/cli/cliui/parameter.go b/cli/cliui/parameter.go index 1d43b7fc4d..f0f4fd99e4 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 b7ffbec581..bb2b9647bf 100644 --- a/cli/create.go +++ b/cli/create.go @@ -23,6 +23,8 @@ func (r *RootCmd) create() *clibase.Cmd { startAt string stopAfter time.Duration workspaceName string + + parameterFlags workspaceParameterFlags ) client := new(codersdk.Client) cmd := &clibase.Cmd{ @@ -123,6 +125,7 @@ func (r *RootCmd) create() *clibase.Cmd { Template: template, RichParameterFile: richParameterFile, NewWorkspaceName: workspaceName, + BuildOptions: parameterFlags.buildOptions, }) if err != nil { return xerrors.Errorf("prepare build: %w", err) @@ -191,6 +194,7 @@ func (r *RootCmd) create() *clibase.Cmd { }, cliui.SkipPromptOption(), ) + cmd.Options = append(cmd.Options, parameterFlags.options()...) return cmd } @@ -202,6 +206,7 @@ type prepWorkspaceBuildArgs struct { NewWorkspaceName string UpdateWorkspace bool + BuildOptions bool WorkspaceID uuid.UUID } @@ -240,13 +245,17 @@ func prepWorkspaceBuild(inv *clibase.Invocation, client *codersdk.Client, args p richParameters := make([]codersdk.WorkspaceBuildParameter, 0) PromptRichParamLoop: for _, templateVersionParameter := range templateVersionParameters { + if !args.BuildOptions && 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 } // 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 176dce8265..753c8b9e2d 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,60 @@ 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) + + 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) + 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 + + // 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, + }) + }) } func TestCreateValidateRichParameters(t *testing.T) { diff --git a/cli/restart.go b/cli/restart.go index 7a139d8206..4cff7ac757 100644 --- a/cli/restart.go +++ b/cli/restart.go @@ -10,6 +10,8 @@ import ( ) func (r *RootCmd) restart() *clibase.Cmd { + var parameterFlags workspaceParameterFlags + client := new(codersdk.Client) cmd := &clibase.Cmd{ Annotations: workspaceCommand, @@ -19,22 +21,33 @@ func (r *RootCmd) restart() *clibase.Cmd { clibase.RequireNArgs(1), r.InitClient(client), ), - Options: clibase.OptionSet{ - cliui.SkipPromptOption(), - }, + Options: append(parameterFlags.options(), 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 + } + + template, err := client.Template(inv.Context(), workspace.TemplateID) + if err != nil { + return err + } + + buildParams, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{ + Template: template, + BuildOptions: parameterFlags.buildOptions, }) if err != nil { return err } - workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0]) + _, err = cliui.Prompt(inv, cliui.PromptOptions{ + Text: "Confirm restart workspace?", + IsConfirm: true, + }) if err != nil { return err } @@ -51,7 +64,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/restart_test.go b/cli/restart_test.go index d1dfa6bd3b..83b066e4de 100644 --- a/cli/restart_test.go +++ b/cli/restart_test.go @@ -1,12 +1,17 @@ package cli_test import ( + "context" "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/codersdk" + "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 +19,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 +73,57 @@ 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 + + // 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.go b/cli/start.go index 1fb619abf4..b576921c28 100644 --- a/cli/start.go +++ b/cli/start.go @@ -4,12 +4,31 @@ import ( "fmt" "time" + "golang.org/x/xerrors" + "github.com/coder/coder/cli/clibase" "github.com/coder/coder/cli/cliui" "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 parameterFlags workspaceParameterFlags + client := new(codersdk.Client) cmd := &clibase.Cmd{ Annotations: workspaceCommand, @@ -19,16 +38,29 @@ func (r *RootCmd) start() *clibase.Cmd { clibase.RequireNArgs(1), r.InitClient(client), ), - Options: clibase.OptionSet{ - 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 { return err } + + template, err := client.Template(inv.Context(), workspace.TemplateID) + if err != nil { + return err + } + + buildParams, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{ + Template: template, + BuildOptions: parameterFlags.buildOptions, + }) + if err != nil { + return err + } + 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 +77,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 { + 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/start_test.go b/cli/start_test.go new file mode 100644 index 0000000000..a302fe2ac1 --- /dev/null +++ b/cli/start_test.go @@ -0,0 +1,102 @@ +package cli_test + +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 ( + 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 + + // 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/testdata/coder_create_--help.golden b/cli/testdata/coder_create_--help.golden index 8d7c4ce965..bf0b40660b 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_restart_--help.golden b/cli/testdata/coder_restart_--help.golden index 828230987f..c2079b9065 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/cli/testdata/coder_start_--help.golden b/cli/testdata/coder_start_--help.golden index 5b1083431a..aa447240e9 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/cli/testdata/coder_update_--help.golden b/cli/testdata/coder_update_--help.golden index 4552933cf5..40e899cd37 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 4edb228925..64710217bb 100644 --- a/cli/update.go +++ b/cli/update.go @@ -11,6 +11,8 @@ func (r *RootCmd) update() *clibase.Cmd { var ( richParameterFile string alwaysPrompt bool + + parameterFlags workspaceParameterFlags ) client := new(codersdk.Client) @@ -28,20 +30,20 @@ func (r *RootCmd) update() *clibase.Cmd { if err != nil { return err } - if !workspace.Outdated && !alwaysPrompt { + if !workspace.Outdated && !alwaysPrompt && !parameterFlags.buildOptions { _, _ = fmt.Fprintf(inv.Stdout, "Workspace isn't outdated!\n") return nil } 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 } } @@ -53,9 +55,11 @@ func (r *RootCmd) update() *clibase.Cmd { UpdateWorkspace: true, WorkspaceID: workspace.LatestBuild.ID, + + BuildOptions: parameterFlags.buildOptions, }) if err != nil { - return nil + return err } build, err := client.CreateWorkspaceBuild(inv.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{ @@ -86,8 +90,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", @@ -96,5 +99,6 @@ func (r *RootCmd) update() *clibase.Cmd { Value: clibase.StringOf(&richParameterFile), }, } + cmd.Options = append(cmd.Options, parameterFlags.options()...) return cmd } diff --git a/cli/update_test.go b/cli/update_test.go index 14a44db4ca..886adf9bea 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) { @@ -90,9 +91,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 +110,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 +172,70 @@ 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) + + 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", workspaceName, "--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 + + // 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, + }) + }) } func TestUpdateValidateRichParameters(t *testing.T) { diff --git a/docs/cli/create.md b/docs/cli/create.md index 1a76673c86..d959908075 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/restart.md b/docs/cli/restart.md index 9966d61c10..72daa5dec4 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 | | | diff --git a/docs/cli/start.md b/docs/cli/start.md index ed847135fe..8c3fd7f712 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 | | | diff --git a/docs/cli/update.md b/docs/cli/update.md index e43502a41b..0b3d31a975 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 | | | diff --git a/docs/templates/parameters.md b/docs/templates/parameters.md index 321ee5f79f..7304b69ccf 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.