- Notifications
You must be signed in to change notification settings - Fork1k
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Merged
Uh oh!
There was an error while loading.Please reload this page.
Merged
Changes fromall commits
Commits
Show all changes
15 commits Select commitHold shift + click to select a range
982031a
feat(cli): add CLI support for creating a workspace with preset
ssncferreiraae16155
chore: fix presetParameterAsWorkspaceBuildParameters function
ssncferreira3d7b40e
chore: minor fixes
ssncferreira7159de2
Merge remote-tracking branch 'origin/main' into ssncferreira/cli-crea…
ssncferreirab8a1c14
chore: fix merge orgin/main
ssncferreira77a72b1
chore: prompt user to select a preset when no --preset flag is provided
ssncferreiracffe049
chore: update preset flag description
ssncferreiraac12e0a
Merge remote-tracking branch 'origin/main' into ssncferreira/cli-crea…
ssncferreiradbe0685
chore: default to preset if available and none specified
ssncferreiraa1c8b5e
Merge remote-tracking branch 'origin/main' into ssncferreira/cli-crea…
ssncferreira645ec89
fix: make gen
ssncferreira197d387
Merge remote-tracking branch 'origin/main' into ssncferreira/cli-crea…
ssncferreira0b7cede
chore: minor fixes
ssncferreira6210f22
chore: run make gen
ssncferreira8256700
Merge remote-tracking branch 'origin/main' into ssncferreira/cli-crea…
ssncferreiraFile filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
125 changes: 123 additions & 2 deletionscli/create.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -2,6 +2,7 @@ package cli | ||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"slices" | ||
@@ -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 | ||
@@ -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, | ||
@@ -291,14 +334,21 @@ func (r *RootCmd) create() *serpent.Command { | ||
ttlMillis = ptr.Ref(stopAfter.Milliseconds()) | ||
} | ||
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) | ||
} | ||
@@ -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", | ||
@@ -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) { | ||
@@ -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). | ||
SasSwart marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
WithPromptRichParameters(args.PromptRichParameters). | ||
WithRichParameters(args.RichParameters). | ||
WithRichParametersFile(parameterFile). | ||
Oops, something went wrong.
Uh oh!
There was an error while loading.Please reload this page.
Oops, something went wrong.
Uh oh!
There was an error while loading.Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.