Skip to content

Commit 61fac2d

Browse files
authored
feat(cli): create workspace using parameters from existing workspace (#10604)
1 parent 076db31 commit 61fac2d

File tree

5 files changed

+213
-8
lines changed

5 files changed

+213
-8
lines changed

cli/create.go

+51-7
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ func (r *RootCmd) create() *clibase.Cmd {
2626
stopAfter time.Duration
2727
workspaceName string
2828

29-
parameterFlags workspaceParameterFlags
30-
autoUpdates string
29+
parameterFlags workspaceParameterFlags
30+
autoUpdates string
31+
copyParametersFrom string
3132
)
3233
client := new(codersdk.Client)
3334
cmd := &clibase.Cmd{
@@ -76,7 +77,24 @@ func (r *RootCmd) create() *clibase.Cmd {
7677
return xerrors.Errorf("A workspace already exists named %q!", workspaceName)
7778
}
7879

80+
var sourceWorkspace codersdk.Workspace
81+
if copyParametersFrom != "" {
82+
sourceWorkspaceOwner, sourceWorkspaceName, err := splitNamedWorkspace(copyParametersFrom)
83+
if err != nil {
84+
return err
85+
}
86+
87+
sourceWorkspace, err = client.WorkspaceByOwnerAndName(inv.Context(), sourceWorkspaceOwner, sourceWorkspaceName, codersdk.WorkspaceOptions{})
88+
if err != nil {
89+
return xerrors.Errorf("get source workspace: %w", err)
90+
}
91+
92+
_, _ = fmt.Fprintf(inv.Stdout, "Coder will use the same template %q as the source workspace.\n", sourceWorkspace.TemplateName)
93+
templateName = sourceWorkspace.TemplateName
94+
}
95+
7996
var template codersdk.Template
97+
var templateVersionID uuid.UUID
8098
if templateName == "" {
8199
_, _ = fmt.Fprintln(inv.Stdout, pretty.Sprint(cliui.DefaultStyles.Wrap, "Select a template below to preview the provisioned infrastructure:"))
82100

@@ -118,11 +136,19 @@ func (r *RootCmd) create() *clibase.Cmd {
118136
}
119137

120138
template = templateByName[option]
139+
templateVersionID = template.ActiveVersionID
140+
} else if sourceWorkspace.LatestBuild.TemplateVersionID != uuid.Nil {
141+
template, err = client.Template(inv.Context(), sourceWorkspace.TemplateID)
142+
if err != nil {
143+
return xerrors.Errorf("get template by name: %w", err)
144+
}
145+
templateVersionID = sourceWorkspace.LatestBuild.TemplateVersionID
121146
} else {
122147
template, err = client.TemplateByName(inv.Context(), organization.ID, templateName)
123148
if err != nil {
124149
return xerrors.Errorf("get template by name: %w", err)
125150
}
151+
templateVersionID = template.ActiveVersionID
126152
}
127153

128154
var schedSpec *string
@@ -134,18 +160,28 @@ func (r *RootCmd) create() *clibase.Cmd {
134160
schedSpec = ptr.Ref(sched.String())
135161
}
136162

137-
cliRichParameters, err := asWorkspaceBuildParameters(parameterFlags.richParameters)
163+
cliBuildParameters, err := asWorkspaceBuildParameters(parameterFlags.richParameters)
138164
if err != nil {
139165
return xerrors.Errorf("can't parse given parameter values: %w", err)
140166
}
141167

168+
var sourceWorkspaceParameters []codersdk.WorkspaceBuildParameter
169+
if copyParametersFrom != "" {
170+
sourceWorkspaceParameters, err = client.WorkspaceBuildParameters(inv.Context(), sourceWorkspace.LatestBuild.ID)
171+
if err != nil {
172+
return xerrors.Errorf("get source workspace build parameters: %w", err)
173+
}
174+
}
175+
142176
richParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
143177
Action: WorkspaceCreate,
144-
TemplateVersionID: template.ActiveVersionID,
178+
TemplateVersionID: templateVersionID,
145179
NewWorkspaceName: workspaceName,
146180

147181
RichParameterFile: parameterFlags.richParameterFile,
148-
RichParameters: cliRichParameters,
182+
RichParameters: cliBuildParameters,
183+
184+
SourceWorkspaceParameters: sourceWorkspaceParameters,
149185
})
150186
if err != nil {
151187
return xerrors.Errorf("prepare build: %w", err)
@@ -165,7 +201,7 @@ func (r *RootCmd) create() *clibase.Cmd {
165201
}
166202

167203
workspace, err := client.CreateWorkspace(inv.Context(), organization.ID, workspaceOwner, codersdk.CreateWorkspaceRequest{
168-
TemplateID: template.ID,
204+
TemplateVersionID: templateVersionID,
169205
Name: workspaceName,
170206
AutostartSchedule: schedSpec,
171207
TTLMillis: ttlMillis,
@@ -217,6 +253,12 @@ func (r *RootCmd) create() *clibase.Cmd {
217253
Default: string(codersdk.AutomaticUpdatesNever),
218254
Value: clibase.StringOf(&autoUpdates),
219255
},
256+
clibase.Option{
257+
Flag: "copy-parameters-from",
258+
Env: "CODER_WORKSPACE_COPY_PARAMETERS_FROM",
259+
Description: "Specify the source workspace name to copy parameters from.",
260+
Value: clibase.StringOf(&copyParametersFrom),
261+
},
220262
cliui.SkipPromptOption(),
221263
)
222264
cmd.Options = append(cmd.Options, parameterFlags.cliParameters()...)
@@ -228,7 +270,8 @@ type prepWorkspaceBuildArgs struct {
228270
TemplateVersionID uuid.UUID
229271
NewWorkspaceName string
230272

231-
LastBuildParameters []codersdk.WorkspaceBuildParameter
273+
LastBuildParameters []codersdk.WorkspaceBuildParameter
274+
SourceWorkspaceParameters []codersdk.WorkspaceBuildParameter
232275

233276
PromptBuildOptions bool
234277
BuildOptions []codersdk.WorkspaceBuildParameter
@@ -263,6 +306,7 @@ func prepWorkspaceBuild(inv *clibase.Invocation, client *codersdk.Client, args p
263306

264307
resolver := new(ParameterResolver).
265308
WithLastBuildParameters(args.LastBuildParameters).
309+
WithSourceWorkspaceParameters(args.SourceWorkspaceParameters).
266310
WithPromptBuildOptions(args.PromptBuildOptions).
267311
WithBuildOptions(args.BuildOptions).
268312
WithPromptRichParameters(args.PromptRichParameters).

cli/create_test.go

+118
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,124 @@ func TestCreateWithRichParameters(t *testing.T) {
416416
assert.ErrorContains(t, err, "parameter \""+wrongFirstParameterName+"\" is not present in the template")
417417
assert.ErrorContains(t, err, "Did you mean: "+firstParameterName)
418418
})
419+
420+
t.Run("CopyParameters", func(t *testing.T) {
421+
t.Parallel()
422+
423+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
424+
owner := coderdtest.CreateFirstUser(t, client)
425+
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
426+
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses)
427+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
428+
429+
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
430+
431+
// Firstly, create a regular workspace using template with parameters.
432+
inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name, "-y",
433+
"--parameter", fmt.Sprintf("%s=%s", firstParameterName, firstParameterValue),
434+
"--parameter", fmt.Sprintf("%s=%s", secondParameterName, secondParameterValue),
435+
"--parameter", fmt.Sprintf("%s=%s", immutableParameterName, immutableParameterValue))
436+
clitest.SetupConfig(t, member, root)
437+
pty := ptytest.New(t).Attach(inv)
438+
inv.Stdout = pty.Output()
439+
inv.Stderr = pty.Output()
440+
err := inv.Run()
441+
require.NoError(t, err, "can't create first workspace")
442+
443+
// Secondly, create a new workspace using parameters from the previous workspace.
444+
const otherWorkspace = "other-workspace"
445+
446+
inv, root = clitest.New(t, "create", "--copy-parameters-from", "my-workspace", otherWorkspace, "-y")
447+
clitest.SetupConfig(t, member, root)
448+
pty = ptytest.New(t).Attach(inv)
449+
inv.Stdout = pty.Output()
450+
inv.Stderr = pty.Output()
451+
err = inv.Run()
452+
require.NoError(t, err, "can't create a workspace based on the source workspace")
453+
454+
// Verify if the new workspace uses expected parameters.
455+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
456+
defer cancel()
457+
458+
workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
459+
Name: otherWorkspace,
460+
})
461+
require.NoError(t, err, "can't list available workspaces")
462+
require.Len(t, workspaces.Workspaces, 1)
463+
464+
otherWorkspaceLatestBuild := workspaces.Workspaces[0].LatestBuild
465+
466+
buildParameters, err := client.WorkspaceBuildParameters(ctx, otherWorkspaceLatestBuild.ID)
467+
require.NoError(t, err)
468+
require.Len(t, buildParameters, 3)
469+
require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: firstParameterName, Value: firstParameterValue})
470+
require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: secondParameterName, Value: secondParameterValue})
471+
require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: immutableParameterName, Value: immutableParameterValue})
472+
})
473+
474+
t.Run("CopyParametersFromNotUpdatedWorkspace", func(t *testing.T) {
475+
t.Parallel()
476+
477+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
478+
owner := coderdtest.CreateFirstUser(t, client)
479+
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
480+
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses)
481+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
482+
483+
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
484+
485+
// Firstly, create a regular workspace using template with parameters.
486+
inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name, "-y",
487+
"--parameter", fmt.Sprintf("%s=%s", firstParameterName, firstParameterValue),
488+
"--parameter", fmt.Sprintf("%s=%s", secondParameterName, secondParameterValue),
489+
"--parameter", fmt.Sprintf("%s=%s", immutableParameterName, immutableParameterValue))
490+
clitest.SetupConfig(t, member, root)
491+
pty := ptytest.New(t).Attach(inv)
492+
inv.Stdout = pty.Output()
493+
inv.Stderr = pty.Output()
494+
err := inv.Run()
495+
require.NoError(t, err, "can't create first workspace")
496+
497+
// Secondly, update the template to the newer version.
498+
version2 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, prepareEchoResponses([]*proto.RichParameter{
499+
{Name: "third_parameter", Type: "string", DefaultValue: "not-relevant"},
500+
}), func(ctvr *codersdk.CreateTemplateVersionRequest) {
501+
ctvr.TemplateID = template.ID
502+
})
503+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version2.ID)
504+
coderdtest.UpdateActiveTemplateVersion(t, client, template.ID, version2.ID)
505+
506+
// Thirdly, create a new workspace using parameters from the previous workspace.
507+
const otherWorkspace = "other-workspace"
508+
509+
inv, root = clitest.New(t, "create", "--copy-parameters-from", "my-workspace", otherWorkspace, "-y")
510+
clitest.SetupConfig(t, member, root)
511+
pty = ptytest.New(t).Attach(inv)
512+
inv.Stdout = pty.Output()
513+
inv.Stderr = pty.Output()
514+
err = inv.Run()
515+
require.NoError(t, err, "can't create a workspace based on the source workspace")
516+
517+
// Verify if the new workspace uses expected parameters.
518+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
519+
defer cancel()
520+
521+
workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
522+
Name: otherWorkspace,
523+
})
524+
require.NoError(t, err, "can't list available workspaces")
525+
require.Len(t, workspaces.Workspaces, 1)
526+
527+
otherWorkspaceLatestBuild := workspaces.Workspaces[0].LatestBuild
528+
require.Equal(t, version.ID, otherWorkspaceLatestBuild.TemplateVersionID)
529+
530+
buildParameters, err := client.WorkspaceBuildParameters(ctx, otherWorkspaceLatestBuild.ID)
531+
require.NoError(t, err)
532+
require.Len(t, buildParameters, 3)
533+
require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: firstParameterName, Value: firstParameterValue})
534+
require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: secondParameterName, Value: secondParameterValue})
535+
require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: immutableParameterName, Value: immutableParameterValue})
536+
})
419537
}
420538

421539
func TestCreateValidateRichParameters(t *testing.T) {

cli/parameterresolver.go

+32-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ const (
2323
)
2424

2525
type ParameterResolver struct {
26-
lastBuildParameters []codersdk.WorkspaceBuildParameter
26+
lastBuildParameters []codersdk.WorkspaceBuildParameter
27+
sourceWorkspaceParameters []codersdk.WorkspaceBuildParameter
2728

2829
richParameters []codersdk.WorkspaceBuildParameter
2930
richParametersFile map[string]string
@@ -38,6 +39,11 @@ func (pr *ParameterResolver) WithLastBuildParameters(params []codersdk.Workspace
3839
return pr
3940
}
4041

42+
func (pr *ParameterResolver) WithSourceWorkspaceParameters(params []codersdk.WorkspaceBuildParameter) *ParameterResolver {
43+
pr.sourceWorkspaceParameters = params
44+
return pr
45+
}
46+
4147
func (pr *ParameterResolver) WithRichParameters(params []codersdk.WorkspaceBuildParameter) *ParameterResolver {
4248
pr.richParameters = params
4349
return pr
@@ -69,6 +75,7 @@ func (pr *ParameterResolver) Resolve(inv *clibase.Invocation, action WorkspaceCL
6975

7076
staged = pr.resolveWithParametersMapFile(staged)
7177
staged = pr.resolveWithCommandLineOrEnv(staged)
78+
staged = pr.resolveWithSourceBuildParameters(staged, templateVersionParameters)
7279
staged = pr.resolveWithLastBuildParameters(staged, templateVersionParameters)
7380
if err = pr.verifyConstraints(staged, action, templateVersionParameters); err != nil {
7481
return nil, err
@@ -160,6 +167,30 @@ next:
160167
return resolved
161168
}
162169

170+
func (pr *ParameterResolver) resolveWithSourceBuildParameters(resolved []codersdk.WorkspaceBuildParameter, templateVersionParameters []codersdk.TemplateVersionParameter) []codersdk.WorkspaceBuildParameter {
171+
next:
172+
for _, buildParameter := range pr.sourceWorkspaceParameters {
173+
tvp := findTemplateVersionParameter(buildParameter, templateVersionParameters)
174+
if tvp == nil {
175+
continue // it looks like this parameter is not present anymore
176+
}
177+
178+
if tvp.Ephemeral {
179+
continue // ephemeral parameters should not be passed to consecutive builds
180+
}
181+
182+
for i, r := range resolved {
183+
if r.Name == buildParameter.Name {
184+
resolved[i].Value = buildParameter.Value
185+
continue next
186+
}
187+
}
188+
189+
resolved = append(resolved, buildParameter)
190+
}
191+
return resolved
192+
}
193+
163194
func (pr *ParameterResolver) verifyConstraints(resolved []codersdk.WorkspaceBuildParameter, action WorkspaceCLIAction, templateVersionParameters []codersdk.TemplateVersionParameter) error {
164195
for _, r := range resolved {
165196
tvp := findTemplateVersionParameter(r, templateVersionParameters)

cli/testdata/coder_create_--help.golden

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ OPTIONS:
1414
Specify automatic updates setting for the workspace (accepts 'always'
1515
or 'never').
1616

17+
--copy-parameters-from string, $CODER_WORKSPACE_COPY_PARAMETERS_FROM
18+
Specify the source workspace name to copy parameters from.
19+
1720
--parameter string-array, $CODER_RICH_PARAMETER
1821
Rich parameter value in the format "name=value".
1922

docs/cli/create.md

+9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)