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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

ssncferreira
Copy link
Contributor

@ssncferreira ssncferreira commented Jul 17, 2025

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.
> 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 a template version preset name. Use 'default' to apply the default preset defined in the template version, if available.

      (...)

  -y, --yes bool
          Bypass prompts.

Relates to PR: #18910 - please consider both PRs together as they’re part of the same workflow
Relates to issue: #16594

@@ -21,10 +21,15 @@ import (
"github.com/coder/serpent"
)

// DefaultPresetName is used when a user runs `create --preset default`.
// It instructs the CLI to use the default preset defined for the template version, if one exists.
const DefaultPresetName = "default"
Copy link
Contributor Author

@ssncferreira ssncferreira Jul 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To preserve current behavior, if no --preset is provided, the CLI does not apply any preset. I added support for a special default value to allow users to explicitly use the default preset (if one exists). However, this approach might not be ideal, as it introduces ambiguity, e.g., what if a preset is literally named "default"?

Another approach I considered was assigning a default value (like default) to the --preset flag itself. But that could lead to unexpected behavior: the user may not realize a preset is being applied at all.

A potentially better UX would be:

  • If the template has presets and none is specified via --preset, prompt the user to select one.
  • If there’s a default preset, it should appear as the first option (as in the UI).
  • Add a none option to explicitly skip using a preset (as in the UI). This means that instead of having a default value for the preset, we can have a none value that explicitly indicates no preset should be used.
  • This would bring the CLI experience closer to the web interface and make it more intuitive.

However, the downside is that this would break existing automation/scripts that rely on creating workspaces silently when presets exist. Those scripts would now hang, waiting for input.

Let me know what you think

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've summarised the trade-offs well. My assessment of them is that without a --preset flag specified, it should use the default as defined in the template. This use case is what we allow a default to be defined for.

I see you've already added a line to the command output to inform the user which preset was chosen, so that should help make it more visible.

Like with the frontend, we can allow for a "None" preset. In fact it might be a good idea to move that from the frontend to the API so that we have it here without any additional trouble.

@ssncferreira ssncferreira marked this pull request as ready for review July 17, 2025 09:29
@ssncferreira ssncferreira requested review from Emyrk and SasSwart July 17, 2025 09:30
Copy link
Contributor

@SasSwart SasSwart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid work @ssncferreira. Thorough tests! Let me know where you land on the default preset matter? :)

@@ -21,10 +21,15 @@ import (
"github.com/coder/serpent"
)

// DefaultPresetName is used when a user runs `create --preset default`.
// It instructs the CLI to use the default preset defined for the template version, if one exists.
const DefaultPresetName = "default"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've summarised the trade-offs well. My assessment of them is that without a --preset flag specified, it should use the default as defined in the template. This use case is what we allow a default to be defined for.

I see you've already added a line to the command output to inform the user which preset was chosen, so that should help make it more visible.

Like with the frontend, we can allow for a "None" preset. In fact it might be a good idea to move that from the frontend to the API so that we have it here without any additional trouble.

@@ -100,6 +100,14 @@ func (wpf *workspaceParameterFlags) alwaysPrompt() serpent.Option {
}
}

func presetParameterAsWorkspaceBuildParameters(presetParameters []codersdk.PresetParameter) ([]codersdk.WorkspaceBuildParameter, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does this return an error? I don't see the return used in the function.

@@ -411,6 +477,7 @@ func prepWorkspaceBuild(inv *serpent.Invocation, client *codersdk.Client, args p
WithSourceWorkspaceParameters(args.SourceWorkspaceParameters).
WithPromptEphemeralParameters(args.PromptEphemeralParameters).
WithEphemeralParameters(args.EphemeralParameters).
WithPresetParameters(args.PresetParameters).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this not be last in the list? Otherwise we allow preset parameters to be overwritten.

DefaultValue: firstParameterValue,
Options: []*proto.RichParameterOption{
{
Name: firstOptionalParameterName,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nonblocking: the naming here made me have to think twice, but I'm not sure what a better name would have been. The current name implies that its a parameter like the others instead of an option on an actual parameter. Feel free to leave it as is if no better name comes to mind.

) {
t.Helper()

state, err := reconciler.SnapshotState(ctx, db)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This duplicates business logic that we already have in the reconciliation loop. Perhaps look for some of the prebuild claim tests. Yevhenii wrote a nice test that spins up an actual reconciliation loop with minimal LoC and I think it might work nicely here. Nevermind. I see that you are using the reconciler to a reasonable extent. Nice tests!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants