mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
0e3dc2a80f
* feat: influence parameter defaults through cli flag/env Add a --parameter-default flag / CODER_RICH_PARAMETER_DEFAULT environment variable which overrides default values suggested for parameters. This allows scripts or middleware wrapping the CLI to substitute defaults for parameter values beyond those defined at the template level. For example, Git repository/branch parameters can be given defaults based on the current checkout, or default parameter values can be parsed out of files inside the repo. * Rename defaults arg to defaultOverrides
383 lines
12 KiB
Go
383 lines
12 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"golang.org/x/exp/slices"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/pretty"
|
|
|
|
"github.com/coder/coder/v2/cli/cliui"
|
|
"github.com/coder/coder/v2/coderd/util/ptr"
|
|
"github.com/coder/coder/v2/coderd/util/slice"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/serpent"
|
|
)
|
|
|
|
func (r *RootCmd) create() *serpent.Command {
|
|
var (
|
|
templateName string
|
|
startAt string
|
|
stopAfter time.Duration
|
|
workspaceName string
|
|
|
|
parameterFlags workspaceParameterFlags
|
|
autoUpdates string
|
|
copyParametersFrom string
|
|
)
|
|
client := new(codersdk.Client)
|
|
cmd := &serpent.Command{
|
|
Annotations: workspaceCommand,
|
|
Use: "create [name]",
|
|
Short: "Create a workspace",
|
|
Long: formatExamples(
|
|
example{
|
|
Description: "Create a workspace for another user (if you have permission)",
|
|
Command: "coder create <username>/<workspace_name>",
|
|
},
|
|
),
|
|
Middleware: serpent.Chain(r.InitClient(client)),
|
|
Handler: func(inv *serpent.Invocation) error {
|
|
organization, err := CurrentOrganization(r, inv, client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
workspaceOwner := codersdk.Me
|
|
if len(inv.Args) >= 1 {
|
|
workspaceOwner, workspaceName, err = splitNamedWorkspace(inv.Args[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if workspaceName == "" {
|
|
workspaceName, err = cliui.Prompt(inv, cliui.PromptOptions{
|
|
Text: "Specify a name for your workspace:",
|
|
Validate: func(workspaceName string) error {
|
|
_, err = client.WorkspaceByOwnerAndName(inv.Context(), codersdk.Me, workspaceName, codersdk.WorkspaceOptions{})
|
|
if err == nil {
|
|
return xerrors.Errorf("A workspace already exists named %q!", workspaceName)
|
|
}
|
|
return nil
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
_, err = client.WorkspaceByOwnerAndName(inv.Context(), workspaceOwner, workspaceName, codersdk.WorkspaceOptions{})
|
|
if err == nil {
|
|
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:"))
|
|
|
|
templates, err := client.TemplatesByOrganization(inv.Context(), organization.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
slices.SortFunc(templates, func(a, b codersdk.Template) int {
|
|
return slice.Descending(a.ActiveUserCount, b.ActiveUserCount)
|
|
})
|
|
|
|
templateNames := make([]string, 0, len(templates))
|
|
templateByName := make(map[string]codersdk.Template, len(templates))
|
|
|
|
for _, template := range templates {
|
|
templateName := template.Name
|
|
|
|
if template.ActiveUserCount > 0 {
|
|
templateName += cliui.Placeholder(
|
|
fmt.Sprintf(
|
|
" (used by %s)",
|
|
formatActiveDevelopers(template.ActiveUserCount),
|
|
),
|
|
)
|
|
}
|
|
|
|
templateNames = append(templateNames, templateName)
|
|
templateByName[templateName] = template
|
|
}
|
|
|
|
// Move the cursor up a single line for nicer display!
|
|
option, err := cliui.Select(inv, cliui.SelectOptions{
|
|
Options: templateNames,
|
|
HideSearch: true,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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
|
|
if startAt != "" {
|
|
sched, err := parseCLISchedule(startAt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
schedSpec = ptr.Ref(sched.String())
|
|
}
|
|
|
|
cliBuildParameters, err := asWorkspaceBuildParameters(parameterFlags.richParameters)
|
|
if err != nil {
|
|
return xerrors.Errorf("can't parse given parameter values: %w", err)
|
|
}
|
|
|
|
cliBuildParameterDefaults, err := asWorkspaceBuildParameters(parameterFlags.richParameterDefaults)
|
|
if err != nil {
|
|
return xerrors.Errorf("can't parse given parameter defaults: %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: templateVersionID,
|
|
NewWorkspaceName: workspaceName,
|
|
|
|
RichParameterFile: parameterFlags.richParameterFile,
|
|
RichParameters: cliBuildParameters,
|
|
RichParameterDefaults: cliBuildParameterDefaults,
|
|
|
|
SourceWorkspaceParameters: sourceWorkspaceParameters,
|
|
})
|
|
if err != nil {
|
|
return xerrors.Errorf("prepare build: %w", err)
|
|
}
|
|
|
|
_, err = cliui.Prompt(inv, cliui.PromptOptions{
|
|
Text: "Confirm create?",
|
|
IsConfirm: true,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var ttlMillis *int64
|
|
if stopAfter > 0 {
|
|
ttlMillis = ptr.Ref(stopAfter.Milliseconds())
|
|
}
|
|
|
|
workspace, err := client.CreateWorkspace(inv.Context(), organization.ID, workspaceOwner, codersdk.CreateWorkspaceRequest{
|
|
TemplateVersionID: templateVersionID,
|
|
Name: workspaceName,
|
|
AutostartSchedule: schedSpec,
|
|
TTLMillis: ttlMillis,
|
|
RichParameterValues: richParameters,
|
|
AutomaticUpdates: codersdk.AutomaticUpdates(autoUpdates),
|
|
})
|
|
if err != nil {
|
|
return xerrors.Errorf("create workspace: %w", err)
|
|
}
|
|
|
|
err = cliui.WorkspaceBuild(inv.Context(), inv.Stdout, client, workspace.LatestBuild.ID)
|
|
if err != nil {
|
|
return xerrors.Errorf("watch build: %w", err)
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(
|
|
inv.Stdout,
|
|
"\nThe %s workspace has been created at %s!\n",
|
|
cliui.Keyword(workspace.Name),
|
|
cliui.Timestamp(time.Now()),
|
|
)
|
|
return nil
|
|
},
|
|
}
|
|
cmd.Options = append(cmd.Options,
|
|
serpent.Option{
|
|
Flag: "template",
|
|
FlagShorthand: "t",
|
|
Env: "CODER_TEMPLATE_NAME",
|
|
Description: "Specify a template name.",
|
|
Value: serpent.StringOf(&templateName),
|
|
},
|
|
serpent.Option{
|
|
Flag: "start-at",
|
|
Env: "CODER_WORKSPACE_START_AT",
|
|
Description: "Specify the workspace autostart schedule. Check coder schedule start --help for the syntax.",
|
|
Value: serpent.StringOf(&startAt),
|
|
},
|
|
serpent.Option{
|
|
Flag: "stop-after",
|
|
Env: "CODER_WORKSPACE_STOP_AFTER",
|
|
Description: "Specify a duration after which the workspace should shut down (e.g. 8h).",
|
|
Value: serpent.DurationOf(&stopAfter),
|
|
},
|
|
serpent.Option{
|
|
Flag: "automatic-updates",
|
|
Env: "CODER_WORKSPACE_AUTOMATIC_UPDATES",
|
|
Description: "Specify automatic updates setting for the workspace (accepts 'always' or 'never').",
|
|
Default: string(codersdk.AutomaticUpdatesNever),
|
|
Value: serpent.StringOf(&autoUpdates),
|
|
},
|
|
serpent.Option{
|
|
Flag: "copy-parameters-from",
|
|
Env: "CODER_WORKSPACE_COPY_PARAMETERS_FROM",
|
|
Description: "Specify the source workspace name to copy parameters from.",
|
|
Value: serpent.StringOf(©ParametersFrom),
|
|
},
|
|
cliui.SkipPromptOption(),
|
|
)
|
|
cmd.Options = append(cmd.Options, parameterFlags.cliParameters()...)
|
|
cmd.Options = append(cmd.Options, parameterFlags.cliParameterDefaults()...)
|
|
return cmd
|
|
}
|
|
|
|
type prepWorkspaceBuildArgs struct {
|
|
Action WorkspaceCLIAction
|
|
TemplateVersionID uuid.UUID
|
|
NewWorkspaceName string
|
|
|
|
LastBuildParameters []codersdk.WorkspaceBuildParameter
|
|
SourceWorkspaceParameters []codersdk.WorkspaceBuildParameter
|
|
|
|
PromptBuildOptions bool
|
|
BuildOptions []codersdk.WorkspaceBuildParameter
|
|
|
|
PromptRichParameters bool
|
|
RichParameters []codersdk.WorkspaceBuildParameter
|
|
RichParameterFile string
|
|
RichParameterDefaults []codersdk.WorkspaceBuildParameter
|
|
}
|
|
|
|
// prepWorkspaceBuild will ensure a workspace build will succeed on the latest template version.
|
|
// Any missing params will be prompted to the user. It supports rich parameters.
|
|
func prepWorkspaceBuild(inv *serpent.Invocation, client *codersdk.Client, args prepWorkspaceBuildArgs) ([]codersdk.WorkspaceBuildParameter, error) {
|
|
ctx := inv.Context()
|
|
|
|
templateVersion, err := client.TemplateVersion(ctx, args.TemplateVersionID)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("get template version: %w", err)
|
|
}
|
|
|
|
templateVersionParameters, err := client.TemplateVersionRichParameters(inv.Context(), templateVersion.ID)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("get template version rich parameters: %w", err)
|
|
}
|
|
|
|
parameterFile := map[string]string{}
|
|
if args.RichParameterFile != "" {
|
|
parameterFile, err = parseParameterMapFile(args.RichParameterFile)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("can't parse parameter map file: %w", err)
|
|
}
|
|
}
|
|
|
|
resolver := new(ParameterResolver).
|
|
WithLastBuildParameters(args.LastBuildParameters).
|
|
WithSourceWorkspaceParameters(args.SourceWorkspaceParameters).
|
|
WithPromptBuildOptions(args.PromptBuildOptions).
|
|
WithBuildOptions(args.BuildOptions).
|
|
WithPromptRichParameters(args.PromptRichParameters).
|
|
WithRichParameters(args.RichParameters).
|
|
WithRichParametersFile(parameterFile).
|
|
WithRichParametersDefaults(args.RichParameterDefaults)
|
|
buildParameters, err := resolver.Resolve(inv, args.Action, templateVersionParameters)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = cliui.ExternalAuth(ctx, inv.Stdout, cliui.ExternalAuthOptions{
|
|
Fetch: func(ctx context.Context) ([]codersdk.TemplateVersionExternalAuth, error) {
|
|
return client.TemplateVersionExternalAuth(ctx, templateVersion.ID)
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("template version git auth: %w", err)
|
|
}
|
|
|
|
// Run a dry-run with the given parameters to check correctness
|
|
dryRun, err := client.CreateTemplateVersionDryRun(inv.Context(), templateVersion.ID, codersdk.CreateTemplateVersionDryRunRequest{
|
|
WorkspaceName: args.NewWorkspaceName,
|
|
RichParameterValues: buildParameters,
|
|
})
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("begin workspace dry-run: %w", err)
|
|
}
|
|
_, _ = fmt.Fprintln(inv.Stdout, "Planning workspace...")
|
|
err = cliui.ProvisionerJob(inv.Context(), inv.Stdout, cliui.ProvisionerJobOptions{
|
|
Fetch: func() (codersdk.ProvisionerJob, error) {
|
|
return client.TemplateVersionDryRun(inv.Context(), templateVersion.ID, dryRun.ID)
|
|
},
|
|
Cancel: func() error {
|
|
return client.CancelTemplateVersionDryRun(inv.Context(), templateVersion.ID, dryRun.ID)
|
|
},
|
|
Logs: func() (<-chan codersdk.ProvisionerJobLog, io.Closer, error) {
|
|
return client.TemplateVersionDryRunLogsAfter(inv.Context(), templateVersion.ID, dryRun.ID, 0)
|
|
},
|
|
// Don't show log output for the dry-run unless there's an error.
|
|
Silent: true,
|
|
})
|
|
if err != nil {
|
|
// TODO (Dean): reprompt for parameter values if we deem it to
|
|
// be a validation error
|
|
return nil, xerrors.Errorf("dry-run workspace: %w", err)
|
|
}
|
|
|
|
resources, err := client.TemplateVersionDryRunResources(inv.Context(), templateVersion.ID, dryRun.ID)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("get workspace dry-run resources: %w", err)
|
|
}
|
|
|
|
err = cliui.WorkspaceResources(inv.Stdout, resources, cliui.WorkspaceResourcesOptions{
|
|
WorkspaceName: args.NewWorkspaceName,
|
|
// Since agents haven't connected yet, hiding this makes more sense.
|
|
HideAgentState: true,
|
|
Title: "Workspace Preview",
|
|
})
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("get resources: %w", err)
|
|
}
|
|
|
|
return buildParameters, nil
|
|
}
|