Skip to content

Commit 77d04a2

Browse files
committed
Merge remote-tracking branch 'origin/main' into ssncferreira/feat-preset-icon-description
2 parents cd273c9 + b975d6d commit 77d04a2

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)