mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
feat(cli): create workspace using parameters from existing workspace (#10604)
This commit is contained in:
+51
-7
@@ -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).
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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".
|
||||
|
||||
|
||||
Generated
+9
@@ -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
|
||||
|
||||
| | |
|
||||
|
||||
Reference in New Issue
Block a user