Content-Length: 443093 | pFad | http://github.com/coder/coder/pull/18912/files

FF feat(cli): add CLI support for creating a workspace with preset by ssncferreira · Pull Request #18912 · coder/coder · GitHub
Skip to content

feat(cli): add CLI support for creating a workspace with preset #18912

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Jul 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 123 additions & 2 deletions cli/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cli

import (
"context"
"errors"
"fmt"
"io"
"slices"
Expand All @@ -21,10 +22,18 @@ import (
"github.com/coder/serpent"
)

// PresetNone represents the special preset value "none".
// It is used when a user runs `create --preset none`,
// indicating that the CLI should not apply any preset.
const PresetNone = "none"

var ErrNoPresetFound = xerrors.New("no preset found")

func (r *RootCmd) create() *serpent.Command {
var (
templateName string
templateVersion string
presetName string
startAt string
stopAfter time.Duration
workspaceName string
Expand Down Expand Up @@ -263,11 +272,45 @@ func (r *RootCmd) create() *serpent.Command {
}
}

// Get presets for the template version
tvPresets, err := client.TemplateVersionPresets(inv.Context(), templateVersionID)
if err != nil {
return xerrors.Errorf("failed to get presets: %w", err)
}

var preset *codersdk.Preset
var presetParameters []codersdk.WorkspaceBuildParameter

// If the template has no presets, or the user explicitly used --preset none,
// skip applying a preset
if len(tvPresets) > 0 && strings.ToLower(presetName) != PresetNone {
// Attempt to resolve which preset to use
preset, err = resolvePreset(tvPresets, presetName)
if err != nil {
if !errors.Is(err, ErrNoPresetFound) {
return xerrors.Errorf("unable to resolve preset: %w", err)
}
// If no preset found, prompt the user to choose a preset
if preset, err = promptPresetSelection(inv, tvPresets); err != nil {
return xerrors.Errorf("unable to prompt user for preset: %w", err)
}
}

// Convert preset parameters into workspace build parameters
presetParameters = presetParameterAsWorkspaceBuildParameters(preset.Parameters)
// Inform the user which preset was applied and its parameters
displayAppliedPreset(inv, preset, presetParameters)
} else {
// Inform the user that no preset was applied
_, _ = fmt.Fprintf(inv.Stdout, "%s", cliui.Bold("No preset applied."))
}

richParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
Action: WorkspaceCreate,
TemplateVersionID: templateVersionID,
NewWorkspaceName: workspaceName,

PresetParameters: presetParameters,
RichParameterFile: parameterFlags.richParameterFile,
RichParameters: cliBuildParameters,
RichParameterDefaults: cliBuildParameterDefaults,
Expand All @@ -291,14 +334,21 @@ func (r *RootCmd) create() *serpent.Command {
ttlMillis = ptr.Ref(stopAfter.Milliseconds())
}

workspace, err := client.CreateUserWorkspace(inv.Context(), workspaceOwner, codersdk.CreateWorkspaceRequest{
req := codersdk.CreateWorkspaceRequest{
TemplateVersionID: templateVersionID,
Name: workspaceName,
AutostartSchedule: schedSpec,
TTLMillis: ttlMillis,
RichParameterValues: richParameters,
AutomaticUpdates: codersdk.AutomaticUpdates(autoUpdates),
})
}

// If a preset exists, update the create workspace request's preset ID
if preset != nil {
req.TemplateVersionPresetID = preset.ID
}

workspace, err := client.CreateUserWorkspace(inv.Context(), workspaceOwner, req)
if err != nil {
return xerrors.Errorf("create workspace: %w", err)
}
Expand Down Expand Up @@ -333,6 +383,12 @@ func (r *RootCmd) create() *serpent.Command {
Description: "Specify a template version name.",
Value: serpent.StringOf(&templateVersion),
},
serpent.Option{
Flag: "preset",
Env: "CODER_PRESET_NAME",
Description: "Specify the name of a template version preset. Use 'none' to explicitly indicate that no preset should be used.",
Value: serpent.StringOf(&presetName),
},
serpent.Option{
Flag: "start-at",
Env: "CODER_WORKSPACE_START_AT",
Expand Down Expand Up @@ -377,12 +433,76 @@ type prepWorkspaceBuildArgs struct {
PromptEphemeralParameters bool
EphemeralParameters []codersdk.WorkspaceBuildParameter

PresetParameters []codersdk.WorkspaceBuildParameter
PromptRichParameters bool
RichParameters []codersdk.WorkspaceBuildParameter
RichParameterFile string
RichParameterDefaults []codersdk.WorkspaceBuildParameter
}

// resolvePreset returns the preset matching the given presetName (if specified),
// or the default preset (if any).
// Returns ErrNoPresetFound if no matching or default preset is found.
func resolvePreset(presets []codersdk.Preset, presetName string) (*codersdk.Preset, error) {
// If preset name is specified, find it
if presetName != "" {
for _, p := range presets {
if p.Name == presetName {
return &p, nil
}
}
return nil, xerrors.Errorf("preset %q not found", presetName)
}

// No preset name specified, search for the default preset
for _, p := range presets {
if p.Default {
return &p, nil
}
}

// No preset found
return nil, ErrNoPresetFound
}

// promptPresetSelection shows a CLI selection menu of the presets defined in the template version.
// Returns the selected preset
func promptPresetSelection(inv *serpent.Invocation, presets []codersdk.Preset) (*codersdk.Preset, error) {
presetMap := make(map[string]*codersdk.Preset)
var presetOptions []string

for _, preset := range presets {
option := preset.Name
presetOptions = append(presetOptions, option)
presetMap[option] = &preset
}

// Show selection UI
_, _ = fmt.Fprintln(inv.Stdout, pretty.Sprint(cliui.DefaultStyles.Wrap, "Select a preset below:"))
selected, err := cliui.Select(inv, cliui.SelectOptions{
Options: presetOptions,
HideSearch: true,
})
if err != nil {
return nil, xerrors.Errorf("failed to select preset: %w", err)
}

return presetMap[selected], nil
}

// displayAppliedPreset shows the user which preset was applied and its parameters
func displayAppliedPreset(inv *serpent.Invocation, preset *codersdk.Preset, parameters []codersdk.WorkspaceBuildParameter) {
label := fmt.Sprintf("Preset '%s'", preset.Name)
if preset.Default {
label += " (default)"
}

_, _ = fmt.Fprintf(inv.Stdout, "%s applied:\n", cliui.Bold(label))
for _, param := range parameters {
_, _ = fmt.Fprintf(inv.Stdout, " %s: '%s'\n", cliui.Bold(param.Name), param.Value)
}
}

// 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) {
Expand Down Expand Up @@ -411,6 +531,7 @@ func prepWorkspaceBuild(inv *serpent.Invocation, client *codersdk.Client, args p
WithSourceWorkspaceParameters(args.SourceWorkspaceParameters).
WithPromptEphemeralParameters(args.PromptEphemeralParameters).
WithEphemeralParameters(args.EphemeralParameters).
WithPresetParameters(args.PresetParameters).
WithPromptRichParameters(args.PromptRichParameters).
WithRichParameters(args.RichParameters).
WithRichParametersFile(parameterFile).
Expand Down
Loading
Loading








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: http://github.com/coder/coder/pull/18912/files

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy