mirror of
https://github.com/coder/coder.git
synced 2025-07-06 15:41:45 +00:00
* feat: influence parameter defaults through cli flag/env Add a --parameter-default flag / CODER_RICH_PARAMETER_DEFAULT environment variable which overrides default values suggested for parameters. This allows scripts or middleware wrapping the CLI to substitute defaults for parameter values beyond those defined at the template level. For example, Git repository/branch parameters can be given defaults based on the current checkout, or default parameter values can be parsed out of files inside the repo. * Rename defaults arg to defaultOverrides
316 lines
10 KiB
Go
316 lines
10 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/v2/cli/cliui"
|
|
"github.com/coder/coder/v2/cli/cliutil/levenshtein"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/pretty"
|
|
"github.com/coder/serpent"
|
|
)
|
|
|
|
type WorkspaceCLIAction int
|
|
|
|
const (
|
|
WorkspaceCreate WorkspaceCLIAction = iota
|
|
WorkspaceStart
|
|
WorkspaceUpdate
|
|
WorkspaceRestart
|
|
)
|
|
|
|
type ParameterResolver struct {
|
|
lastBuildParameters []codersdk.WorkspaceBuildParameter
|
|
sourceWorkspaceParameters []codersdk.WorkspaceBuildParameter
|
|
|
|
richParameters []codersdk.WorkspaceBuildParameter
|
|
richParametersDefaults map[string]string
|
|
richParametersFile map[string]string
|
|
buildOptions []codersdk.WorkspaceBuildParameter
|
|
|
|
promptRichParameters bool
|
|
promptBuildOptions bool
|
|
}
|
|
|
|
func (pr *ParameterResolver) WithLastBuildParameters(params []codersdk.WorkspaceBuildParameter) *ParameterResolver {
|
|
pr.lastBuildParameters = params
|
|
return pr
|
|
}
|
|
|
|
func (pr *ParameterResolver) WithSourceWorkspaceParameters(params []codersdk.WorkspaceBuildParameter) *ParameterResolver {
|
|
pr.sourceWorkspaceParameters = params
|
|
return pr
|
|
}
|
|
|
|
func (pr *ParameterResolver) WithRichParameters(params []codersdk.WorkspaceBuildParameter) *ParameterResolver {
|
|
pr.richParameters = params
|
|
return pr
|
|
}
|
|
|
|
func (pr *ParameterResolver) WithBuildOptions(params []codersdk.WorkspaceBuildParameter) *ParameterResolver {
|
|
pr.buildOptions = params
|
|
return pr
|
|
}
|
|
|
|
func (pr *ParameterResolver) WithRichParametersFile(fileMap map[string]string) *ParameterResolver {
|
|
pr.richParametersFile = fileMap
|
|
return pr
|
|
}
|
|
|
|
func (pr *ParameterResolver) WithRichParametersDefaults(params []codersdk.WorkspaceBuildParameter) *ParameterResolver {
|
|
if pr.richParametersDefaults == nil {
|
|
pr.richParametersDefaults = make(map[string]string)
|
|
}
|
|
for _, p := range params {
|
|
pr.richParametersDefaults[p.Name] = p.Value
|
|
}
|
|
return pr
|
|
}
|
|
|
|
func (pr *ParameterResolver) WithPromptRichParameters(promptRichParameters bool) *ParameterResolver {
|
|
pr.promptRichParameters = promptRichParameters
|
|
return pr
|
|
}
|
|
|
|
func (pr *ParameterResolver) WithPromptBuildOptions(promptBuildOptions bool) *ParameterResolver {
|
|
pr.promptBuildOptions = promptBuildOptions
|
|
return pr
|
|
}
|
|
|
|
func (pr *ParameterResolver) Resolve(inv *serpent.Invocation, action WorkspaceCLIAction, templateVersionParameters []codersdk.TemplateVersionParameter) ([]codersdk.WorkspaceBuildParameter, error) {
|
|
var staged []codersdk.WorkspaceBuildParameter
|
|
var err error
|
|
|
|
staged = pr.resolveWithParametersMapFile(staged)
|
|
staged = pr.resolveWithCommandLineOrEnv(staged)
|
|
staged = pr.resolveWithSourceBuildParameters(staged, templateVersionParameters)
|
|
staged = pr.resolveWithLastBuildParameters(staged, templateVersionParameters)
|
|
if err = pr.verifyConstraints(staged, action, templateVersionParameters); err != nil {
|
|
return nil, err
|
|
}
|
|
if staged, err = pr.resolveWithInput(staged, inv, action, templateVersionParameters); err != nil {
|
|
return nil, err
|
|
}
|
|
return staged, nil
|
|
}
|
|
|
|
func (pr *ParameterResolver) resolveWithParametersMapFile(resolved []codersdk.WorkspaceBuildParameter) []codersdk.WorkspaceBuildParameter {
|
|
next:
|
|
for name, value := range pr.richParametersFile {
|
|
for i, r := range resolved {
|
|
if r.Name == name {
|
|
resolved[i].Value = value
|
|
continue next
|
|
}
|
|
}
|
|
|
|
resolved = append(resolved, codersdk.WorkspaceBuildParameter{
|
|
Name: name,
|
|
Value: value,
|
|
})
|
|
}
|
|
return resolved
|
|
}
|
|
|
|
func (pr *ParameterResolver) resolveWithCommandLineOrEnv(resolved []codersdk.WorkspaceBuildParameter) []codersdk.WorkspaceBuildParameter {
|
|
nextRichParameter:
|
|
for _, richParameter := range pr.richParameters {
|
|
for i, r := range resolved {
|
|
if r.Name == richParameter.Name {
|
|
resolved[i].Value = richParameter.Value
|
|
continue nextRichParameter
|
|
}
|
|
}
|
|
|
|
resolved = append(resolved, richParameter)
|
|
}
|
|
|
|
nextBuildOption:
|
|
for _, buildOption := range pr.buildOptions {
|
|
for i, r := range resolved {
|
|
if r.Name == buildOption.Name {
|
|
resolved[i].Value = buildOption.Value
|
|
continue nextBuildOption
|
|
}
|
|
}
|
|
|
|
resolved = append(resolved, buildOption)
|
|
}
|
|
return resolved
|
|
}
|
|
|
|
func (pr *ParameterResolver) resolveWithLastBuildParameters(resolved []codersdk.WorkspaceBuildParameter, templateVersionParameters []codersdk.TemplateVersionParameter) []codersdk.WorkspaceBuildParameter {
|
|
if pr.promptRichParameters {
|
|
return resolved // don't pull parameters from last build
|
|
}
|
|
|
|
next:
|
|
for _, buildParameter := range pr.lastBuildParameters {
|
|
tvp := findTemplateVersionParameter(buildParameter, templateVersionParameters)
|
|
if tvp == nil {
|
|
continue // it looks like this parameter is not present anymore
|
|
}
|
|
|
|
if tvp.Ephemeral {
|
|
continue // ephemeral parameters should not be passed to consecutive builds
|
|
}
|
|
|
|
if !tvp.Mutable {
|
|
continue // immutables should not be passed to consecutive builds
|
|
}
|
|
|
|
if len(tvp.Options) > 0 && !isValidTemplateParameterOption(buildParameter, tvp.Options) {
|
|
continue // do not propagate invalid options
|
|
}
|
|
|
|
for i, r := range resolved {
|
|
if r.Name == buildParameter.Name {
|
|
resolved[i].Value = buildParameter.Value
|
|
continue next
|
|
}
|
|
}
|
|
|
|
resolved = append(resolved, buildParameter)
|
|
}
|
|
return resolved
|
|
}
|
|
|
|
func (pr *ParameterResolver) resolveWithSourceBuildParameters(resolved []codersdk.WorkspaceBuildParameter, templateVersionParameters []codersdk.TemplateVersionParameter) []codersdk.WorkspaceBuildParameter {
|
|
next:
|
|
for _, buildParameter := range pr.sourceWorkspaceParameters {
|
|
tvp := findTemplateVersionParameter(buildParameter, templateVersionParameters)
|
|
if tvp == nil {
|
|
continue // it looks like this parameter is not present anymore
|
|
}
|
|
|
|
if tvp.Ephemeral {
|
|
continue // ephemeral parameters should not be passed to consecutive builds
|
|
}
|
|
|
|
for i, r := range resolved {
|
|
if r.Name == buildParameter.Name {
|
|
resolved[i].Value = buildParameter.Value
|
|
continue next
|
|
}
|
|
}
|
|
|
|
resolved = append(resolved, buildParameter)
|
|
}
|
|
return resolved
|
|
}
|
|
|
|
func (pr *ParameterResolver) verifyConstraints(resolved []codersdk.WorkspaceBuildParameter, action WorkspaceCLIAction, templateVersionParameters []codersdk.TemplateVersionParameter) error {
|
|
for _, r := range resolved {
|
|
tvp := findTemplateVersionParameter(r, templateVersionParameters)
|
|
if tvp == nil {
|
|
return templateVersionParametersNotFound(r.Name, templateVersionParameters)
|
|
}
|
|
|
|
if tvp.Ephemeral && !pr.promptBuildOptions && findWorkspaceBuildParameter(tvp.Name, pr.buildOptions) == nil {
|
|
return xerrors.Errorf("ephemeral parameter %q can be used only with --build-options or --build-option flag", r.Name)
|
|
}
|
|
|
|
if !tvp.Mutable && action != WorkspaceCreate {
|
|
return xerrors.Errorf("parameter %q is immutable and cannot be updated", r.Name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (pr *ParameterResolver) resolveWithInput(resolved []codersdk.WorkspaceBuildParameter, inv *serpent.Invocation, action WorkspaceCLIAction, templateVersionParameters []codersdk.TemplateVersionParameter) ([]codersdk.WorkspaceBuildParameter, error) {
|
|
for _, tvp := range templateVersionParameters {
|
|
p := findWorkspaceBuildParameter(tvp.Name, resolved)
|
|
if p != nil {
|
|
continue
|
|
}
|
|
// Parameter has not been resolved yet, so CLI needs to determine if user should input it.
|
|
|
|
firstTimeUse := pr.isFirstTimeUse(tvp.Name)
|
|
promptParameterOption := pr.isLastBuildParameterInvalidOption(tvp)
|
|
|
|
if (tvp.Ephemeral && pr.promptBuildOptions) ||
|
|
(action == WorkspaceCreate && tvp.Required) ||
|
|
(action == WorkspaceCreate && !tvp.Ephemeral) ||
|
|
(action == WorkspaceUpdate && promptParameterOption) ||
|
|
(action == WorkspaceUpdate && tvp.Mutable && tvp.Required) ||
|
|
(action == WorkspaceUpdate && !tvp.Mutable && firstTimeUse) ||
|
|
(tvp.Mutable && !tvp.Ephemeral && pr.promptRichParameters) {
|
|
parameterValue, err := cliui.RichParameter(inv, tvp, pr.richParametersDefaults)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resolved = append(resolved, codersdk.WorkspaceBuildParameter{
|
|
Name: tvp.Name,
|
|
Value: parameterValue,
|
|
})
|
|
} else if action == WorkspaceUpdate && !tvp.Mutable && !firstTimeUse {
|
|
_, _ = fmt.Fprintln(inv.Stdout, pretty.Sprint(cliui.DefaultStyles.Warn, fmt.Sprintf("Parameter %q is not mutable, and cannot be customized after workspace creation.", tvp.Name)))
|
|
}
|
|
}
|
|
return resolved, nil
|
|
}
|
|
|
|
func (pr *ParameterResolver) isFirstTimeUse(parameterName string) bool {
|
|
return findWorkspaceBuildParameter(parameterName, pr.lastBuildParameters) == nil
|
|
}
|
|
|
|
func (pr *ParameterResolver) isLastBuildParameterInvalidOption(templateVersionParameter codersdk.TemplateVersionParameter) bool {
|
|
if len(templateVersionParameter.Options) == 0 {
|
|
return false
|
|
}
|
|
|
|
for _, buildParameter := range pr.lastBuildParameters {
|
|
if buildParameter.Name == templateVersionParameter.Name {
|
|
return !isValidTemplateParameterOption(buildParameter, templateVersionParameter.Options)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func findTemplateVersionParameter(workspaceBuildParameter codersdk.WorkspaceBuildParameter, templateVersionParameters []codersdk.TemplateVersionParameter) *codersdk.TemplateVersionParameter {
|
|
for _, tvp := range templateVersionParameters {
|
|
if tvp.Name == workspaceBuildParameter.Name {
|
|
return &tvp
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func findWorkspaceBuildParameter(parameterName string, params []codersdk.WorkspaceBuildParameter) *codersdk.WorkspaceBuildParameter {
|
|
for _, p := range params {
|
|
if p.Name == parameterName {
|
|
return &p
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isValidTemplateParameterOption(buildParameter codersdk.WorkspaceBuildParameter, options []codersdk.TemplateVersionParameterOption) bool {
|
|
for _, opt := range options {
|
|
if opt.Value == buildParameter.Value {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func templateVersionParametersNotFound(unknown string, params []codersdk.TemplateVersionParameter) error {
|
|
var sb strings.Builder
|
|
_, _ = sb.WriteString(fmt.Sprintf("parameter %q is not present in the template.", unknown))
|
|
// Going with a fairly generous edit distance
|
|
maxDist := len(unknown) / 2
|
|
var paramNames []string
|
|
for _, p := range params {
|
|
paramNames = append(paramNames, p.Name)
|
|
}
|
|
matches := levenshtein.Matches(unknown, maxDist, paramNames...)
|
|
if len(matches) > 0 {
|
|
_, _ = sb.WriteString(fmt.Sprintf("\nDid you mean: %s", strings.Join(matches, ", ")))
|
|
}
|
|
return xerrors.Errorf(sb.String())
|
|
}
|