diff --git a/cli/create.go b/cli/create.go
index 79f569d4a02d6..1a2492374a186 100644
--- a/cli/create.go
+++ b/cli/create.go
@@ -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(©ParametersFrom),
+ },
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).
diff --git a/cli/create_test.go b/cli/create_test.go
index 0f3f06eb1db1d..42b526d404cfc 100644
--- a/cli/create_test.go
+++ b/cli/create_test.go
@@ -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) {
diff --git a/cli/parameterresolver.go b/cli/parameterresolver.go
index 21a31825bd0cf..c97d9a3e1bfd2 100644
--- a/cli/parameterresolver.go
+++ b/cli/parameterresolver.go
@@ -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)
diff --git a/cli/testdata/coder_create_--help.golden b/cli/testdata/coder_create_--help.golden
index 2d4031999c3d6..7662ecd7ed7ee 100644
--- a/cli/testdata/coder_create_--help.golden
+++ b/cli/testdata/coder_create_--help.golden
@@ -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".
diff --git a/docs/cli/create.md b/docs/cli/create.md
index f7036ac84d9a3..0b3f63b32b3fd 100644
--- a/docs/cli/create.md
+++ b/docs/cli/create.md
@@ -30,6 +30,15 @@ coder create [flags] [name]
Specify automatic updates setting for the workspace (accepts 'always' or 'never').
+### --copy-parameters-from
+
+| | |
+| ----------- | -------------------------------------------------- |
+| Type | string
|
+| Environment | $CODER_WORKSPACE_COPY_PARAMETERS_FROM
|
+
+Specify the source workspace name to copy parameters from.
+
### --parameter
| | |