mirror of
https://github.com/coder/coder.git
synced 2025-07-06 15:41:45 +00:00
This change will improve over CLI performance and "snappiness" as well as substantially reduce our test times. Preliminary benchmarks show `coder server --help` times cut from 300ms to 120ms on my dogfood instance. The inefficiency of lipgloss disproportionately impacts our system, as all help text for every command is generated whenever any command is invoked. The `pretty` API could clean up a lot of the code (e.g., by replacing complex string concatenations with Printf), but this commit is too expansive as is so that work will be done in a follow up.
257 lines
8.0 KiB
Go
257 lines
8.0 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/pretty"
|
|
|
|
"github.com/coder/coder/v2/cli/clibase"
|
|
"github.com/coder/coder/v2/cli/cliui"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
)
|
|
|
|
type WorkspaceCLIAction int
|
|
|
|
const (
|
|
WorkspaceCreate WorkspaceCLIAction = iota
|
|
WorkspaceStart
|
|
WorkspaceUpdate
|
|
WorkspaceRestart
|
|
)
|
|
|
|
type ParameterResolver struct {
|
|
lastBuildParameters []codersdk.WorkspaceBuildParameter
|
|
|
|
richParameters []codersdk.WorkspaceBuildParameter
|
|
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) 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) 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 *clibase.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.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) verifyConstraints(resolved []codersdk.WorkspaceBuildParameter, action WorkspaceCLIAction, templateVersionParameters []codersdk.TemplateVersionParameter) error {
|
|
for _, r := range resolved {
|
|
tvp := findTemplateVersionParameter(r, templateVersionParameters)
|
|
if tvp == nil {
|
|
return xerrors.Errorf("parameter %q is not present in the template", r.Name)
|
|
}
|
|
|
|
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 *clibase.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) ||
|
|
(action == WorkspaceUpdate && tvp.Mutable && !tvp.Ephemeral && pr.promptRichParameters) {
|
|
parameterValue, err := cliui.RichParameter(inv, tvp)
|
|
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
|
|
}
|