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 b7ffbec5819cb..bb2b9647bfbde 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 176dce82656e6..753c8b9e2d900 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 7a139d8206bb9..4cff7ac7571d7 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 d1dfa6bd3b497..83b066e4defc5 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 1fb619abf4b77..b576921c28068 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 0000000000000..a302fe2ac1c40 --- /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 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_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/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/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..64710217bb996 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 14a44db4cabbb..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) { @@ -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 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/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 | | | 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 | | | 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 | | | 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.