feat(cli): support ephemeral parameters (#8415)

This commit is contained in:
Marcin Tojek
2023-07-13 12:56:02 +02:00
committed by GitHub
parent cdf9b9045f
commit 5432c3f5ea
18 changed files with 507 additions and 21 deletions
+5
View File
@@ -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")
}
+10 -1
View File
@@ -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.
+60 -1
View File
@@ -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) {
+22 -8
View File
@@ -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
+83
View File
@@ -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,
})
})
}
+82 -4
View File
@@ -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
}
+102
View File
@@ -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,
})
})
}
+3
View File
@@ -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.
+3
View File
@@ -3,6 +3,9 @@ Usage: coder restart [flags] <workspace>
Restart a workspace
Options
--build-options bool
Prompt for one-time build options defined with ephemeral parameters.
-y, --yes bool
Bypass prompts.
+3
View File
@@ -3,6 +3,9 @@ Usage: coder start [flags] <workspace>
Start a workspace
Options
--build-options bool
Prompt for one-time build options defined with ephemeral parameters.
-y, --yes bool
Bypass prompts.
+3
View File
@@ -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.
+10 -6
View File
@@ -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
}
+71 -1
View File
@@ -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) {