@@ -2,6 +2,7 @@ package cli
2
2
3
3
import (
4
4
"context"
5
+ "errors"
5
6
"fmt"
6
7
"io"
7
8
"slices"
@@ -21,10 +22,18 @@ import (
21
22
"github.com/coder/serpent"
22
23
)
23
24
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
+
24
32
func (r * RootCmd ) create () * serpent.Command {
25
33
var (
26
34
templateName string
27
35
templateVersion string
36
+ presetName string
28
37
startAt string
29
38
stopAfter time.Duration
30
39
workspaceName string
@@ -263,11 +272,45 @@ func (r *RootCmd) create() *serpent.Command {
263
272
}
264
273
}
265
274
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
+
266
308
richParameters , err := prepWorkspaceBuild (inv , client , prepWorkspaceBuildArgs {
267
309
Action : WorkspaceCreate ,
268
310
TemplateVersionID : templateVersionID ,
269
311
NewWorkspaceName : workspaceName ,
270
312
313
+ PresetParameters : presetParameters ,
271
314
RichParameterFile : parameterFlags .richParameterFile ,
272
315
RichParameters : cliBuildParameters ,
273
316
RichParameterDefaults : cliBuildParameterDefaults ,
@@ -291,14 +334,21 @@ func (r *RootCmd) create() *serpent.Command {
291
334
ttlMillis = ptr .Ref (stopAfter .Milliseconds ())
292
335
}
293
336
294
- workspace , err := client . CreateUserWorkspace ( inv . Context (), workspaceOwner , codersdk.CreateWorkspaceRequest {
337
+ req := codersdk.CreateWorkspaceRequest {
295
338
TemplateVersionID : templateVersionID ,
296
339
Name : workspaceName ,
297
340
AutostartSchedule : schedSpec ,
298
341
TTLMillis : ttlMillis ,
299
342
RichParameterValues : richParameters ,
300
343
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 )
302
352
if err != nil {
303
353
return xerrors .Errorf ("create workspace: %w" , err )
304
354
}
@@ -333,6 +383,12 @@ func (r *RootCmd) create() *serpent.Command {
333
383
Description : "Specify a template version name." ,
334
384
Value : serpent .StringOf (& templateVersion ),
335
385
},
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
+ },
336
392
serpent.Option {
337
393
Flag : "start-at" ,
338
394
Env : "CODER_WORKSPACE_START_AT" ,
@@ -377,12 +433,76 @@ type prepWorkspaceBuildArgs struct {
377
433
PromptEphemeralParameters bool
378
434
EphemeralParameters []codersdk.WorkspaceBuildParameter
379
435
436
+ PresetParameters []codersdk.WorkspaceBuildParameter
380
437
PromptRichParameters bool
381
438
RichParameters []codersdk.WorkspaceBuildParameter
382
439
RichParameterFile string
383
440
RichParameterDefaults []codersdk.WorkspaceBuildParameter
384
441
}
385
442
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
+
386
506
// prepWorkspaceBuild will ensure a workspace build will succeed on the latest template version.
387
507
// Any missing params will be prompted to the user. It supports rich parameters.
388
508
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
411
531
WithSourceWorkspaceParameters (args .SourceWorkspaceParameters ).
412
532
WithPromptEphemeralParameters (args .PromptEphemeralParameters ).
413
533
WithEphemeralParameters (args .EphemeralParameters ).
534
+ WithPresetParameters (args .PresetParameters ).
414
535
WithPromptRichParameters (args .PromptRichParameters ).
415
536
WithRichParameters (args .RichParameters ).
416
537
WithRichParametersFile (parameterFile ).
0 commit comments