Content-Length: 622873 | pFad | http://github.com/coder/coder/commit/b975d6d9b34e5349990dd5923722f17337f88b7d

8F feat(cli): add CLI support for creating a workspace with preset (#18912) · coder/coder@b975d6d · GitHub
Skip to content

Commit b975d6d

Browse files
authored
feat(cli): add CLI support for creating a workspace with preset (#18912)
## Description This PR introduces a `--preset` flag for the `create` command to allow users to apply a predefined preset to their workspace build. ## Changes - The `--preset` flag on the `create` command integrates with the parameter resolution logic and takes precedence over other sources (e.g., CLI/env vars, last build, etc.). - Added internal logic to ensure that preset parameters override parameters values during resolution. - Updated tests and added new ones to cover these flows. ## Implementation logic * If a template has presets and includes a default, the CLI will automatically use the default when `--preset` is not specified. * If a template has presets but no default, the CLI will prompt the user to select one when `--preset` is not specified. * If a template does not have presets, the CLI will not prompt the user for a preset. * If the user specifies a preset using the `--preset` flag, that preset will be used. * If the user passes `--preset None`, no preset will be applied. This logic aligns with the behavior in the UI for consistency. ``` > coder create --help USAGE: coder create [flags] [workspace] Create a workspace - Create a workspace for another user (if you have permission): $ coder create <username>/<workspace_name> OPTIONS: (...) --preset string, $CODER_PRESET_NAME Specify the name of a template version preset. Use 'none' to explicitly indicate that no preset should be used. (...) -y, --yes bool Bypass prompts. ``` ## Breaking change **Note:** This is a breaking change to the create CLI command. If a template includes presets and the user does not provide a `--preset` flag, the CLI will now prompt the user to select one. This behavior may break non-interactive scripts or automated workflows. Relates to PR: #18910 - please consider both PRs together as they’re part of the same workflow Relates to issue: #16594
1 parent 66cf90c commit b975d6d

File tree

7 files changed

+1197
-3
lines changed

7 files changed

+1197
-3
lines changed

cli/create.go

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cli
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"io"
78
"slices"
@@ -21,10 +22,18 @@ import (
2122
"github.com/coder/serpent"
2223
)
2324

25+
// PresetNone represents the special preset value "none".
26+
// It is used when a user runs `create --preset none`,
27+
// indicating that the CLI should not apply any preset.
28+
const PresetNone = "none"
29+
30+
var ErrNoPresetFound = xerrors.New("no preset found")
31+
2432
func (r *RootCmd) create() *serpent.Command {
2533
var (
2634
templateName string
2735
templateVersion string
36+
presetName string
2837
startAt string
2938
stopAfter time.Duration
3039
workspaceName string
@@ -263,11 +272,45 @@ func (r *RootCmd) create() *serpent.Command {
263272
}
264273
}
265274

275+
// Get presets for the template version
276+
tvPresets, err := client.TemplateVersionPresets(inv.Context(), templateVersionID)
277+
if err != nil {
278+
return xerrors.Errorf("failed to get presets: %w", err)
279+
}
280+
281+
var preset *codersdk.Preset
282+
var presetParameters []codersdk.WorkspaceBuildParameter
283+
284+
// If the template has no presets, or the user explicitly used --preset none,
285+
// skip applying a preset
286+
if len(tvPresets) > 0 && strings.ToLower(presetName) != PresetNone {
287+
// Attempt to resolve which preset to use
288+
preset, err = resolvePreset(tvPresets, presetName)
289+
if err != nil {
290+
if !errors.Is(err, ErrNoPresetFound) {
291+
return xerrors.Errorf("unable to resolve preset: %w", err)
292+
}
293+
// If no preset found, prompt the user to choose a preset
294+
if preset, err = promptPresetSelection(inv, tvPresets); err != nil {
295+
return xerrors.Errorf("unable to prompt user for preset: %w", err)
296+
}
297+
}
298+
299+
// Convert preset parameters into workspace build parameters
300+
presetParameters = presetParameterAsWorkspaceBuildParameters(preset.Parameters)
301+
// Inform the user which preset was applied and its parameters
302+
displayAppliedPreset(inv, preset, presetParameters)
303+
} else {
304+
// Inform the user that no preset was applied
305+
_, _ = fmt.Fprintf(inv.Stdout, "%s", cliui.Bold("No preset applied."))
306+
}
307+
266308
richParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
267309
Action: WorkspaceCreate,
268310
TemplateVersionID: templateVersionID,
269311
NewWorkspaceName: workspaceName,
270312

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

294-
workspace, err := client.CreateUserWorkspace(inv.Context(), workspaceOwner, codersdk.CreateWorkspaceRequest{
337+
req := codersdk.CreateWorkspaceRequest{
295338
TemplateVersionID: templateVersionID,
296339
Name: workspaceName,
297340
AutostartSchedule: schedSpec,
298341
TTLMillis: ttlMillis,
299342
RichParameterValues: richParameters,
300343
AutomaticUpdates: codersdk.AutomaticUpdates(autoUpdates),
301-
})
344+
}
345+
346+
// If a preset exists, update the create workspace request's preset ID
347+
if preset != nil {
348+
req.TemplateVersionPresetID = preset.ID
349+
}
350+
351+
workspace, err := client.CreateUserWorkspace(inv.Context(), workspaceOwner, req)
302352
if err != nil {
303353
return xerrors.Errorf("create workspace: %w", err)
304354
}
@@ -333,6 +383,12 @@ func (r *RootCmd) create() *serpent.Command {
333383
Description: "Specify a template version name.",
334384
Value: serpent.StringOf(&templateVersion),
335385
},
386+
serpent.Option{
387+
Flag: "preset",
388+
Env: "CODER_PRESET_NAME",
389+
Description: "Specify the name of a template version preset. Use 'none' to explicitly indicate that no preset should be used.",
390+
Value: serpent.StringOf(&presetName),
391+
},
336392
serpent.Option{
337393
Flag: "start-at",
338394
Env: "CODER_WORKSPACE_START_AT",
@@ -377,12 +433,76 @@ type prepWorkspaceBuildArgs struct {
377433
PromptEphemeralParameters bool
378434
EphemeralParameters []codersdk.WorkspaceBuildParameter
379435

436+
PresetParameters []codersdk.WorkspaceBuildParameter
380437
PromptRichParameters bool
381438
RichParameters []codersdk.WorkspaceBuildParameter
382439
RichParameterFile string
383440
RichParameterDefaults []codersdk.WorkspaceBuildParameter
384441
}
385442

443+
// resolvePreset returns the preset matching the given presetName (if specified),
444+
// or the default preset (if any).
445+
// Returns ErrNoPresetFound if no matching or default preset is found.
446+
func resolvePreset(presets []codersdk.Preset, presetName string) (*codersdk.Preset, error) {
447+
// If preset name is specified, find it
448+
if presetName != "" {
449+
for _, p := range presets {
450+
if p.Name == presetName {
451+
return &p, nil
452+
}
453+
}
454+
return nil, xerrors.Errorf("preset %q not found", presetName)
455+
}
456+
457+
// No preset name specified, search for the default preset
458+
for _, p := range presets {
459+
if p.Default {
460+
return &p, nil
461+
}
462+
}
463+
464+
// No preset found
465+
return nil, ErrNoPresetFound
466+
}
467+
468+
// promptPresetSelection shows a CLI selection menu of the presets defined in the template version.
469+
// Returns the selected preset
470+
func promptPresetSelection(inv *serpent.Invocation, presets []codersdk.Preset) (*codersdk.Preset, error) {
471+
presetMap := make(map[string]*codersdk.Preset)
472+
var presetOptions []string
473+
474+
for _, preset := range presets {
475+
option := preset.Name
476+
presetOptions = append(presetOptions, option)
477+
presetMap[option] = &preset
478+
}
479+
480+
// Show selection UI
481+
_, _ = fmt.Fprintln(inv.Stdout, pretty.Sprint(cliui.DefaultStyles.Wrap, "Select a preset below:"))
482+
selected, err := cliui.Select(inv, cliui.SelectOptions{
483+
Options: presetOptions,
484+
HideSearch: true,
485+
})
486+
if err != nil {
487+
return nil, xerrors.Errorf("failed to select preset: %w", err)
488+
}
489+
490+
return presetMap[selected], nil
491+
}
492+
493+
// displayAppliedPreset shows the user which preset was applied and its parameters
494+
func displayAppliedPreset(inv *serpent.Invocation, preset *codersdk.Preset, parameters []codersdk.WorkspaceBuildParameter) {
495+
label := fmt.Sprintf("Preset '%s'", preset.Name)
496+
if preset.Default {
497+
label += " (default)"
498+
}
499+
500+
_, _ = fmt.Fprintf(inv.Stdout, "%s applied:\n", cliui.Bold(label))
501+
for _, param := range parameters {
502+
_, _ = fmt.Fprintf(inv.Stdout, " %s: '%s'\n", cliui.Bold(param.Name), param.Value)
503+
}
504+
}
505+
386506
// prepWorkspaceBuild will ensure a workspace build will succeed on the latest template version.
387507
// Any missing params will be prompted to the user. It supports rich parameters.
388508
func prepWorkspaceBuild(inv *serpent.Invocation, client *codersdk.Client, args prepWorkspaceBuildArgs) ([]codersdk.WorkspaceBuildParameter, error) {
@@ -411,6 +531,7 @@ func prepWorkspaceBuild(inv *serpent.Invocation, client *codersdk.Client, args p
411531
WithSourceWorkspaceParameters(args.SourceWorkspaceParameters).
412532
WithPromptEphemeralParameters(args.PromptEphemeralParameters).
413533
WithEphemeralParameters(args.EphemeralParameters).
534+
WithPresetParameters(args.PresetParameters).
414535
WithPromptRichParameters(args.PromptRichParameters).
415536
WithRichParameters(args.RichParameters).
416537
WithRichParametersFile(parameterFile).

0 commit comments

Comments
 (0)








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/commit/b975d6d9b34e5349990dd5923722f17337f88b7d

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy