mirror of
https://github.com/coder/coder.git
synced 2026-06-03 13:08:25 +00:00
02b123518c
## Problem
The CLI does not honor `default` values on template parameters in two
ways:
1. **`--use-parameter-defaults` rejects empty-string defaults.** The
check `parameterValue != ""` means `default = ""` in Terraform falls
through to an interactive prompt. In CI this causes an EOF error.
2. **`--use-parameter-defaults` only exists on `coder create`.** The
`start`, `update`, and `restart` commands never wire it through. SSH
auto-start passes empty `workspaceParameterFlags{}`, so users SSH-ing
into a stopped workspace with new template parameters get stuck in an
interactive prompt they cannot complete.
## Fix
### 1. Fix empty-string default detection and expose flag on all
commands
Replace `parameterValue != ""` with a check based on `!tvp.Required`. A
parameter with `Required==false` always has a valid default in
Terraform, even if that default is `""`. Also respect CLI defaults
provided via `--parameter-default`.
Move `--use-parameter-defaults` from a standalone option on `create`
into the shared `workspaceParameterFlags` struct. This exposes the flag
(and `CODER_WORKSPACE_USE_PARAMETER_DEFAULTS`) on `start`, `update`, and
`restart` via `allOptions()`. Wire it through
`buildWorkspaceStartRequest` so the resolver receives it.
### 2. SSH auto-start always uses defaults
Set `useParameterDefaults: true` on both `startWorkspace` calls in the
SSH auto-start path (initial start and the forbidden/upgrade fallback).
SSH is non-interactive and should never prompt.
Fixes https://linear.app/codercom/issue/DEVEX-180
Fixes https://github.com/coder/coder/issues/22272
<details><summary>Implementation notes</summary>
### Scoping decisions
- **`--yes` does not imply `--use-parameter-defaults`**: Making `--yes`
auto-accept defaults exposes a validation gap in the dynamic parameter
path (client-side validation happens during prompting, and skipping
prompts bypasses it). This is deferred to a follow-up that also
addresses `codersdk.ValidateWorkspaceBuildParameter` integration in the
resolver. Tracked in PLAT-114.
- **Explicit overrides always win**: `--parameter`,
`--rich-parameter-file`, and `--preset` are resolved in stages 1-5 of
the resolver, before `resolveWithInput` runs. No change needed for
precedence.
- **`!tvp.Required` vs `parameterValue != ""`**: The `Required` field is
set by the Terraform provider based on whether a `default` is present.
This is the canonical signal for "has a default," not the string value
itself.
</details>
> Generated with [Coder Agents](https://coder.com/agents)
198 lines
6.2 KiB
Go
198 lines
6.2 KiB
Go
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"golang.org/x/xerrors"
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/serpent"
|
|
)
|
|
|
|
// workspaceParameterFlags are used by commands processing rich parameters and/or build options.
|
|
type workspaceParameterFlags struct {
|
|
promptEphemeralParameters bool
|
|
|
|
ephemeralParameters []string
|
|
|
|
richParameterFile string
|
|
richParameters []string
|
|
richParameterDefaults []string
|
|
|
|
promptRichParameters bool
|
|
useParameterDefaults bool
|
|
}
|
|
|
|
func (wpf *workspaceParameterFlags) allOptions() []serpent.Option {
|
|
options := append(wpf.cliEphemeralParameters(), wpf.cliParameters()...)
|
|
options = append(options, wpf.cliParameterDefaults()...)
|
|
options = append(options, wpf.useParameterDefaultsOption())
|
|
return append(options, wpf.alwaysPrompt())
|
|
}
|
|
|
|
func (wpf *workspaceParameterFlags) cliEphemeralParameters() []serpent.Option {
|
|
return serpent.OptionSet{
|
|
// Deprecated - replaced with ephemeral-parameter
|
|
{
|
|
Flag: "build-option",
|
|
Env: "CODER_BUILD_OPTION",
|
|
Description: `Build option value in the format "name=value".`,
|
|
UseInstead: []serpent.Option{{Flag: "ephemeral-parameter"}},
|
|
Value: serpent.StringArrayOf(&wpf.ephemeralParameters),
|
|
},
|
|
// Deprecated - replaced with prompt-ephemeral-parameters
|
|
{
|
|
Flag: "build-options",
|
|
Description: "Prompt for one-time build options defined with ephemeral parameters.",
|
|
UseInstead: []serpent.Option{{Flag: "prompt-ephemeral-parameters"}},
|
|
Value: serpent.BoolOf(&wpf.promptEphemeralParameters),
|
|
},
|
|
{
|
|
Flag: "ephemeral-parameter",
|
|
Env: "CODER_EPHEMERAL_PARAMETER",
|
|
Description: `Set the value of ephemeral parameters defined in the template. The format is "name=value".`,
|
|
Value: serpent.StringArrayOf(&wpf.ephemeralParameters),
|
|
},
|
|
{
|
|
Flag: "prompt-ephemeral-parameters",
|
|
Env: "CODER_PROMPT_EPHEMERAL_PARAMETERS",
|
|
Description: "Prompt to set values of ephemeral parameters defined in the template. If a value has been set via --ephemeral-parameter, it will not be prompted for.",
|
|
Value: serpent.BoolOf(&wpf.promptEphemeralParameters),
|
|
},
|
|
}
|
|
}
|
|
|
|
func (wpf *workspaceParameterFlags) cliParameters() []serpent.Option {
|
|
return serpent.OptionSet{
|
|
serpent.Option{
|
|
Flag: "parameter",
|
|
Env: "CODER_RICH_PARAMETER",
|
|
Description: `Rich parameter value in the format "name=value".`,
|
|
Value: serpent.StringArrayOf(&wpf.richParameters),
|
|
},
|
|
serpent.Option{
|
|
Flag: "rich-parameter-file",
|
|
Env: "CODER_RICH_PARAMETER_FILE",
|
|
Description: "Specify a file path with values for rich parameters defined in the template. The file should be in YAML format, containing key-value pairs for the parameters.",
|
|
Value: serpent.StringOf(&wpf.richParameterFile),
|
|
},
|
|
}
|
|
}
|
|
|
|
func (wpf *workspaceParameterFlags) cliParameterDefaults() []serpent.Option {
|
|
return serpent.OptionSet{
|
|
serpent.Option{
|
|
Flag: "parameter-default",
|
|
Env: "CODER_RICH_PARAMETER_DEFAULT",
|
|
Description: `Rich parameter default values in the format "name=value".`,
|
|
Value: serpent.StringArrayOf(&wpf.richParameterDefaults),
|
|
},
|
|
}
|
|
}
|
|
|
|
func (wpf *workspaceParameterFlags) useParameterDefaultsOption() serpent.Option {
|
|
return serpent.Option{
|
|
Flag: "use-parameter-defaults",
|
|
Env: "CODER_WORKSPACE_USE_PARAMETER_DEFAULTS",
|
|
Description: "Automatically accept parameter defaults when no value is provided.",
|
|
Value: serpent.BoolOf(&wpf.useParameterDefaults),
|
|
}
|
|
}
|
|
|
|
func (wpf *workspaceParameterFlags) alwaysPrompt() serpent.Option {
|
|
return serpent.Option{
|
|
Flag: "always-prompt",
|
|
Description: "Always prompt all parameters. Does not pull parameter values from existing workspace.",
|
|
Value: serpent.BoolOf(&wpf.promptRichParameters),
|
|
}
|
|
}
|
|
|
|
func presetParameterAsWorkspaceBuildParameters(presetParameters []codersdk.PresetParameter) []codersdk.WorkspaceBuildParameter {
|
|
var params []codersdk.WorkspaceBuildParameter
|
|
for _, parameter := range presetParameters {
|
|
params = append(params, codersdk.WorkspaceBuildParameter(parameter))
|
|
}
|
|
return params
|
|
}
|
|
|
|
func asWorkspaceBuildParameters(nameValuePairs []string) ([]codersdk.WorkspaceBuildParameter, error) {
|
|
var params []codersdk.WorkspaceBuildParameter
|
|
for _, nameValue := range nameValuePairs {
|
|
split := strings.SplitN(nameValue, "=", 2)
|
|
if len(split) < 2 {
|
|
return nil, xerrors.Errorf("format key=value expected, but got %s", nameValue)
|
|
}
|
|
params = append(params, codersdk.WorkspaceBuildParameter{
|
|
Name: split[0],
|
|
Value: split[1],
|
|
})
|
|
}
|
|
return params, nil
|
|
}
|
|
|
|
func parseParameterMapFile(parameterFile string) (map[string]string, error) {
|
|
parameterFileContents, err := os.ReadFile(parameterFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
mapStringInterface := make(map[string]interface{})
|
|
err = yaml.Unmarshal(parameterFileContents, &mapStringInterface)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
parameterMap := map[string]string{}
|
|
for k, v := range mapStringInterface {
|
|
switch val := v.(type) {
|
|
case string, bool, int:
|
|
parameterMap[k] = fmt.Sprintf("%v", val)
|
|
case []interface{}:
|
|
b, err := json.Marshal(&val)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
parameterMap[k] = string(b)
|
|
default:
|
|
return nil, xerrors.Errorf("invalid parameter type: %T", v)
|
|
}
|
|
}
|
|
return parameterMap, nil
|
|
}
|
|
|
|
// buildFlags contains options relating to troubleshooting provisioner jobs
|
|
// and setting the reason for the workspace build.
|
|
type buildFlags struct {
|
|
provisionerLogDebug bool
|
|
reason string
|
|
}
|
|
|
|
func (bf *buildFlags) cliOptions() []serpent.Option {
|
|
return []serpent.Option{
|
|
{
|
|
Flag: "provisioner-log-debug",
|
|
Description: `Sets the provisioner log level to debug.
|
|
This will print additional information about the build process.
|
|
This is useful for troubleshooting build issues.`,
|
|
Value: serpent.BoolOf(&bf.provisionerLogDebug),
|
|
Hidden: true,
|
|
},
|
|
{
|
|
Flag: "reason",
|
|
Description: `Sets the reason for the workspace build (cli, vscode_connection, jetbrains_connection).`,
|
|
Value: serpent.EnumOf(
|
|
&bf.reason,
|
|
string(codersdk.BuildReasonCLI),
|
|
string(codersdk.BuildReasonVSCodeConnection),
|
|
string(codersdk.BuildReasonJetbrainsConnection),
|
|
),
|
|
Default: string(codersdk.BuildReasonCLI),
|
|
Hidden: true,
|
|
},
|
|
}
|
|
}
|