feat(cli): create workspace using parameters from existing workspace (#10604)

This commit is contained in:
Marcin Tojek
2023-11-09 19:22:47 +01:00
committed by GitHub
parent 076db31486
commit 61fac2dcfc
5 changed files with 213 additions and 8 deletions
+51 -7
View File
@@ -26,8 +26,9 @@ func (r *RootCmd) create() *clibase.Cmd {
stopAfter time.Duration
workspaceName string
parameterFlags workspaceParameterFlags
autoUpdates string
parameterFlags workspaceParameterFlags
autoUpdates string
copyParametersFrom string
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
@@ -76,7 +77,24 @@ func (r *RootCmd) create() *clibase.Cmd {
return xerrors.Errorf("A workspace already exists named %q!", workspaceName)
}
var sourceWorkspace codersdk.Workspace
if copyParametersFrom != "" {
sourceWorkspaceOwner, sourceWorkspaceName, err := splitNamedWorkspace(copyParametersFrom)
if err != nil {
return err
}
sourceWorkspace, err = client.WorkspaceByOwnerAndName(inv.Context(), sourceWorkspaceOwner, sourceWorkspaceName, codersdk.WorkspaceOptions{})
if err != nil {
return xerrors.Errorf("get source workspace: %w", err)
}
_, _ = fmt.Fprintf(inv.Stdout, "Coder will use the same template %q as the source workspace.\n", sourceWorkspace.TemplateName)
templateName = sourceWorkspace.TemplateName
}
var template codersdk.Template
var templateVersionID uuid.UUID
if templateName == "" {
_, _ = fmt.Fprintln(inv.Stdout, pretty.Sprint(cliui.DefaultStyles.Wrap, "Select a template below to preview the provisioned infrastructure:"))
@@ -118,11 +136,19 @@ func (r *RootCmd) create() *clibase.Cmd {
}
template = templateByName[option]
templateVersionID = template.ActiveVersionID
} else if sourceWorkspace.LatestBuild.TemplateVersionID != uuid.Nil {
template, err = client.Template(inv.Context(), sourceWorkspace.TemplateID)
if err != nil {
return xerrors.Errorf("get template by name: %w", err)
}
templateVersionID = sourceWorkspace.LatestBuild.TemplateVersionID
} else {
template, err = client.TemplateByName(inv.Context(), organization.ID, templateName)
if err != nil {
return xerrors.Errorf("get template by name: %w", err)
}
templateVersionID = template.ActiveVersionID
}
var schedSpec *string
@@ -134,18 +160,28 @@ func (r *RootCmd) create() *clibase.Cmd {
schedSpec = ptr.Ref(sched.String())
}
cliRichParameters, err := asWorkspaceBuildParameters(parameterFlags.richParameters)
cliBuildParameters, err := asWorkspaceBuildParameters(parameterFlags.richParameters)
if err != nil {
return xerrors.Errorf("can't parse given parameter values: %w", err)
}
var sourceWorkspaceParameters []codersdk.WorkspaceBuildParameter
if copyParametersFrom != "" {
sourceWorkspaceParameters, err = client.WorkspaceBuildParameters(inv.Context(), sourceWorkspace.LatestBuild.ID)
if err != nil {
return xerrors.Errorf("get source workspace build parameters: %w", err)
}
}
richParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
Action: WorkspaceCreate,
TemplateVersionID: template.ActiveVersionID,
TemplateVersionID: templateVersionID,
NewWorkspaceName: workspaceName,
RichParameterFile: parameterFlags.richParameterFile,
RichParameters: cliRichParameters,
RichParameters: cliBuildParameters,
SourceWorkspaceParameters: sourceWorkspaceParameters,
})
if err != nil {
return xerrors.Errorf("prepare build: %w", err)
@@ -165,7 +201,7 @@ func (r *RootCmd) create() *clibase.Cmd {
}
workspace, err := client.CreateWorkspace(inv.Context(), organization.ID, workspaceOwner, codersdk.CreateWorkspaceRequest{
TemplateID: template.ID,
TemplateVersionID: templateVersionID,
Name: workspaceName,
AutostartSchedule: schedSpec,
TTLMillis: ttlMillis,
@@ -217,6 +253,12 @@ func (r *RootCmd) create() *clibase.Cmd {
Default: string(codersdk.AutomaticUpdatesNever),
Value: clibase.StringOf(&autoUpdates),
},
clibase.Option{
Flag: "copy-parameters-from",
Env: "CODER_WORKSPACE_COPY_PARAMETERS_FROM",
Description: "Specify the source workspace name to copy parameters from.",
Value: clibase.StringOf(&copyParametersFrom),
},
cliui.SkipPromptOption(),
)
cmd.Options = append(cmd.Options, parameterFlags.cliParameters()...)
@@ -228,7 +270,8 @@ type prepWorkspaceBuildArgs struct {
TemplateVersionID uuid.UUID
NewWorkspaceName string
LastBuildParameters []codersdk.WorkspaceBuildParameter
LastBuildParameters []codersdk.WorkspaceBuildParameter
SourceWorkspaceParameters []codersdk.WorkspaceBuildParameter
PromptBuildOptions bool
BuildOptions []codersdk.WorkspaceBuildParameter
@@ -263,6 +306,7 @@ func prepWorkspaceBuild(inv *clibase.Invocation, client *codersdk.Client, args p
resolver := new(ParameterResolver).
WithLastBuildParameters(args.LastBuildParameters).
WithSourceWorkspaceParameters(args.SourceWorkspaceParameters).
WithPromptBuildOptions(args.PromptBuildOptions).
WithBuildOptions(args.BuildOptions).
WithPromptRichParameters(args.PromptRichParameters).
+118
View File
@@ -416,6 +416,124 @@ func TestCreateWithRichParameters(t *testing.T) {
assert.ErrorContains(t, err, "parameter \""+wrongFirstParameterName+"\" is not present in the template")
assert.ErrorContains(t, err, "Did you mean: "+firstParameterName)
})
t.Run("CopyParameters", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
// Firstly, create a regular workspace using template with parameters.
inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name, "-y",
"--parameter", fmt.Sprintf("%s=%s", firstParameterName, firstParameterValue),
"--parameter", fmt.Sprintf("%s=%s", secondParameterName, secondParameterValue),
"--parameter", fmt.Sprintf("%s=%s", immutableParameterName, immutableParameterValue))
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t).Attach(inv)
inv.Stdout = pty.Output()
inv.Stderr = pty.Output()
err := inv.Run()
require.NoError(t, err, "can't create first workspace")
// Secondly, create a new workspace using parameters from the previous workspace.
const otherWorkspace = "other-workspace"
inv, root = clitest.New(t, "create", "--copy-parameters-from", "my-workspace", otherWorkspace, "-y")
clitest.SetupConfig(t, member, root)
pty = ptytest.New(t).Attach(inv)
inv.Stdout = pty.Output()
inv.Stderr = pty.Output()
err = inv.Run()
require.NoError(t, err, "can't create a workspace based on the source workspace")
// Verify if the new workspace uses expected parameters.
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
Name: otherWorkspace,
})
require.NoError(t, err, "can't list available workspaces")
require.Len(t, workspaces.Workspaces, 1)
otherWorkspaceLatestBuild := workspaces.Workspaces[0].LatestBuild
buildParameters, err := client.WorkspaceBuildParameters(ctx, otherWorkspaceLatestBuild.ID)
require.NoError(t, err)
require.Len(t, buildParameters, 3)
require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: firstParameterName, Value: firstParameterValue})
require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: secondParameterName, Value: secondParameterValue})
require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: immutableParameterName, Value: immutableParameterValue})
})
t.Run("CopyParametersFromNotUpdatedWorkspace", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
// Firstly, create a regular workspace using template with parameters.
inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name, "-y",
"--parameter", fmt.Sprintf("%s=%s", firstParameterName, firstParameterValue),
"--parameter", fmt.Sprintf("%s=%s", secondParameterName, secondParameterValue),
"--parameter", fmt.Sprintf("%s=%s", immutableParameterName, immutableParameterValue))
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t).Attach(inv)
inv.Stdout = pty.Output()
inv.Stderr = pty.Output()
err := inv.Run()
require.NoError(t, err, "can't create first workspace")
// Secondly, update the template to the newer version.
version2 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, prepareEchoResponses([]*proto.RichParameter{
{Name: "third_parameter", Type: "string", DefaultValue: "not-relevant"},
}), func(ctvr *codersdk.CreateTemplateVersionRequest) {
ctvr.TemplateID = template.ID
})
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version2.ID)
coderdtest.UpdateActiveTemplateVersion(t, client, template.ID, version2.ID)
// Thirdly, create a new workspace using parameters from the previous workspace.
const otherWorkspace = "other-workspace"
inv, root = clitest.New(t, "create", "--copy-parameters-from", "my-workspace", otherWorkspace, "-y")
clitest.SetupConfig(t, member, root)
pty = ptytest.New(t).Attach(inv)
inv.Stdout = pty.Output()
inv.Stderr = pty.Output()
err = inv.Run()
require.NoError(t, err, "can't create a workspace based on the source workspace")
// Verify if the new workspace uses expected parameters.
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
Name: otherWorkspace,
})
require.NoError(t, err, "can't list available workspaces")
require.Len(t, workspaces.Workspaces, 1)
otherWorkspaceLatestBuild := workspaces.Workspaces[0].LatestBuild
require.Equal(t, version.ID, otherWorkspaceLatestBuild.TemplateVersionID)
buildParameters, err := client.WorkspaceBuildParameters(ctx, otherWorkspaceLatestBuild.ID)
require.NoError(t, err)
require.Len(t, buildParameters, 3)
require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: firstParameterName, Value: firstParameterValue})
require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: secondParameterName, Value: secondParameterValue})
require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: immutableParameterName, Value: immutableParameterValue})
})
}
func TestCreateValidateRichParameters(t *testing.T) {
+32 -1
View File
@@ -23,7 +23,8 @@ const (
)
type ParameterResolver struct {
lastBuildParameters []codersdk.WorkspaceBuildParameter
lastBuildParameters []codersdk.WorkspaceBuildParameter
sourceWorkspaceParameters []codersdk.WorkspaceBuildParameter
richParameters []codersdk.WorkspaceBuildParameter
richParametersFile map[string]string
@@ -38,6 +39,11 @@ func (pr *ParameterResolver) WithLastBuildParameters(params []codersdk.Workspace
return pr
}
func (pr *ParameterResolver) WithSourceWorkspaceParameters(params []codersdk.WorkspaceBuildParameter) *ParameterResolver {
pr.sourceWorkspaceParameters = params
return pr
}
func (pr *ParameterResolver) WithRichParameters(params []codersdk.WorkspaceBuildParameter) *ParameterResolver {
pr.richParameters = params
return pr
@@ -69,6 +75,7 @@ func (pr *ParameterResolver) Resolve(inv *clibase.Invocation, action WorkspaceCL
staged = pr.resolveWithParametersMapFile(staged)
staged = pr.resolveWithCommandLineOrEnv(staged)
staged = pr.resolveWithSourceBuildParameters(staged, templateVersionParameters)
staged = pr.resolveWithLastBuildParameters(staged, templateVersionParameters)
if err = pr.verifyConstraints(staged, action, templateVersionParameters); err != nil {
return nil, err
@@ -160,6 +167,30 @@ next:
return resolved
}
func (pr *ParameterResolver) resolveWithSourceBuildParameters(resolved []codersdk.WorkspaceBuildParameter, templateVersionParameters []codersdk.TemplateVersionParameter) []codersdk.WorkspaceBuildParameter {
next:
for _, buildParameter := range pr.sourceWorkspaceParameters {
tvp := findTemplateVersionParameter(buildParameter, templateVersionParameters)
if tvp == nil {
continue // it looks like this parameter is not present anymore
}
if tvp.Ephemeral {
continue // ephemeral parameters should not be passed to consecutive builds
}
for i, r := range resolved {
if r.Name == buildParameter.Name {
resolved[i].Value = buildParameter.Value
continue next
}
}
resolved = append(resolved, buildParameter)
}
return resolved
}
func (pr *ParameterResolver) verifyConstraints(resolved []codersdk.WorkspaceBuildParameter, action WorkspaceCLIAction, templateVersionParameters []codersdk.TemplateVersionParameter) error {
for _, r := range resolved {
tvp := findTemplateVersionParameter(r, templateVersionParameters)
+3
View File
@@ -14,6 +14,9 @@ OPTIONS:
Specify automatic updates setting for the workspace (accepts 'always'
or 'never').
--copy-parameters-from string, $CODER_WORKSPACE_COPY_PARAMETERS_FROM
Specify the source workspace name to copy parameters from.
--parameter string-array, $CODER_RICH_PARAMETER
Rich parameter value in the format "name=value".
+9
View File
@@ -30,6 +30,15 @@ coder create [flags] [name]
Specify automatic updates setting for the workspace (accepts 'always' or 'never').
### --copy-parameters-from
| | |
| ----------- | -------------------------------------------------- |
| Type | <code>string</code> |
| Environment | <code>$CODER_WORKSPACE_COPY_PARAMETERS_FROM</code> |
Specify the source workspace name to copy parameters from.
### --parameter
| | |