2023-08-09 11:00:25 +00:00
package cli
import (
"fmt"
2023-11-06 13:44:39 +00:00
"strings"
2023-08-09 11:00:25 +00:00
"golang.org/x/xerrors"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/cli/cliui"
2023-11-06 13:44:39 +00:00
"github.com/coder/coder/v2/cli/cliutil/levenshtein"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/codersdk"
2023-11-06 13:44:39 +00:00
"github.com/coder/pretty"
2024-03-15 16:24:38 +00:00
"github.com/coder/serpent"
2023-08-09 11:00:25 +00:00
)
type WorkspaceCLIAction int
const (
WorkspaceCreate WorkspaceCLIAction = iota
WorkspaceStart
WorkspaceUpdate
WorkspaceRestart
)
type ParameterResolver struct {
2023-11-09 18:22:47 +00:00
lastBuildParameters [ ] codersdk . WorkspaceBuildParameter
sourceWorkspaceParameters [ ] codersdk . WorkspaceBuildParameter
2023-08-09 11:00:25 +00:00
2024-04-29 18:23:54 +00:00
richParameters [ ] codersdk . WorkspaceBuildParameter
richParametersDefaults map [ string ] string
richParametersFile map [ string ] string
buildOptions [ ] codersdk . WorkspaceBuildParameter
2023-08-09 11:00:25 +00:00
promptRichParameters bool
promptBuildOptions bool
}
func ( pr * ParameterResolver ) WithLastBuildParameters ( params [ ] codersdk . WorkspaceBuildParameter ) * ParameterResolver {
pr . lastBuildParameters = params
return pr
}
2023-11-09 18:22:47 +00:00
func ( pr * ParameterResolver ) WithSourceWorkspaceParameters ( params [ ] codersdk . WorkspaceBuildParameter ) * ParameterResolver {
pr . sourceWorkspaceParameters = params
return pr
}
2023-08-09 11:00:25 +00:00
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
}
2024-04-29 18:23:54 +00:00
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
}
2023-08-09 11:00:25 +00:00
func ( pr * ParameterResolver ) WithPromptRichParameters ( promptRichParameters bool ) * ParameterResolver {
pr . promptRichParameters = promptRichParameters
return pr
}
func ( pr * ParameterResolver ) WithPromptBuildOptions ( promptBuildOptions bool ) * ParameterResolver {
pr . promptBuildOptions = promptBuildOptions
return pr
}
2024-03-15 16:24:38 +00:00
func ( pr * ParameterResolver ) Resolve ( inv * serpent . Invocation , action WorkspaceCLIAction , templateVersionParameters [ ] codersdk . TemplateVersionParameter ) ( [ ] codersdk . WorkspaceBuildParameter , error ) {
2023-08-09 11:00:25 +00:00
var staged [ ] codersdk . WorkspaceBuildParameter
var err error
staged = pr . resolveWithParametersMapFile ( staged )
staged = pr . resolveWithCommandLineOrEnv ( staged )
2023-11-09 18:22:47 +00:00
staged = pr . resolveWithSourceBuildParameters ( staged , templateVersionParameters )
2023-08-09 11:00:25 +00:00
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 {
2023-08-10 10:08:00 +00:00
next :
2023-08-09 11:00:25 +00:00
for name , value := range pr . richParametersFile {
for i , r := range resolved {
if r . Name == name {
resolved [ i ] . Value = value
2023-08-10 10:08:00 +00:00
continue next
2023-08-09 11:00:25 +00:00
}
}
resolved = append ( resolved , codersdk . WorkspaceBuildParameter {
Name : name ,
Value : value ,
} )
}
return resolved
}
func ( pr * ParameterResolver ) resolveWithCommandLineOrEnv ( resolved [ ] codersdk . WorkspaceBuildParameter ) [ ] codersdk . WorkspaceBuildParameter {
2023-08-10 10:08:00 +00:00
nextRichParameter :
2023-08-09 11:00:25 +00:00
for _ , richParameter := range pr . richParameters {
for i , r := range resolved {
if r . Name == richParameter . Name {
resolved [ i ] . Value = richParameter . Value
2023-08-10 10:08:00 +00:00
continue nextRichParameter
2023-08-09 11:00:25 +00:00
}
}
resolved = append ( resolved , richParameter )
}
2023-08-10 10:08:00 +00:00
nextBuildOption :
2023-08-09 11:00:25 +00:00
for _ , buildOption := range pr . buildOptions {
for i , r := range resolved {
if r . Name == buildOption . Name {
resolved [ i ] . Value = buildOption . Value
2023-08-10 10:08:00 +00:00
continue nextBuildOption
2023-08-09 11:00:25 +00:00
}
}
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
}
2023-08-10 10:08:00 +00:00
next :
2023-08-09 11:00:25 +00:00
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
}
2023-08-23 16:18:38 +00:00
if len ( tvp . Options ) > 0 && ! isValidTemplateParameterOption ( buildParameter , tvp . Options ) {
continue // do not propagate invalid options
}
2023-08-09 11:00:25 +00:00
for i , r := range resolved {
if r . Name == buildParameter . Name {
resolved [ i ] . Value = buildParameter . Value
2023-08-10 10:08:00 +00:00
continue next
2023-08-09 11:00:25 +00:00
}
}
resolved = append ( resolved , buildParameter )
}
return resolved
}
2023-11-09 18:22:47 +00:00
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
}
2023-08-09 11:00:25 +00:00
func ( pr * ParameterResolver ) verifyConstraints ( resolved [ ] codersdk . WorkspaceBuildParameter , action WorkspaceCLIAction , templateVersionParameters [ ] codersdk . TemplateVersionParameter ) error {
for _ , r := range resolved {
tvp := findTemplateVersionParameter ( r , templateVersionParameters )
if tvp == nil {
2023-11-06 13:44:39 +00:00
return templateVersionParametersNotFound ( r . Name , templateVersionParameters )
2023-08-09 11:00:25 +00:00
}
2023-08-10 10:08:00 +00:00
if tvp . Ephemeral && ! pr . promptBuildOptions && findWorkspaceBuildParameter ( tvp . Name , pr . buildOptions ) == nil {
2023-08-09 11:00:25 +00:00
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
}
2024-03-15 16:24:38 +00:00
func ( pr * ParameterResolver ) resolveWithInput ( resolved [ ] codersdk . WorkspaceBuildParameter , inv * serpent . Invocation , action WorkspaceCLIAction , templateVersionParameters [ ] codersdk . TemplateVersionParameter ) ( [ ] codersdk . WorkspaceBuildParameter , error ) {
2023-08-09 11:00:25 +00:00
for _ , tvp := range templateVersionParameters {
2023-08-10 10:08:00 +00:00
p := findWorkspaceBuildParameter ( tvp . Name , resolved )
2023-08-09 11:00:25 +00:00
if p != nil {
continue
}
2023-08-18 12:06:46 +00:00
// Parameter has not been resolved yet, so CLI needs to determine if user should input it.
2023-08-09 11:00:25 +00:00
2023-08-10 10:08:00 +00:00
firstTimeUse := pr . isFirstTimeUse ( tvp . Name )
2023-08-23 16:18:38 +00:00
promptParameterOption := pr . isLastBuildParameterInvalidOption ( tvp )
2023-08-09 11:00:25 +00:00
if ( tvp . Ephemeral && pr . promptBuildOptions ) ||
2023-08-18 12:06:46 +00:00
( action == WorkspaceCreate && tvp . Required ) ||
( action == WorkspaceCreate && ! tvp . Ephemeral ) ||
2023-08-23 16:18:38 +00:00
( action == WorkspaceUpdate && promptParameterOption ) ||
2023-08-23 10:46:52 +00:00
( action == WorkspaceUpdate && tvp . Mutable && tvp . Required ) ||
2023-08-09 11:00:25 +00:00
( action == WorkspaceUpdate && ! tvp . Mutable && firstTimeUse ) ||
2023-11-02 19:41:34 +00:00
( tvp . Mutable && ! tvp . Ephemeral && pr . promptRichParameters ) {
2024-04-29 18:23:54 +00:00
parameterValue , err := cliui . RichParameter ( inv , tvp , pr . richParametersDefaults )
2023-08-09 11:00:25 +00:00
if err != nil {
return nil , err
}
resolved = append ( resolved , codersdk . WorkspaceBuildParameter {
Name : tvp . Name ,
Value : parameterValue ,
} )
} else if action == WorkspaceUpdate && ! tvp . Mutable && ! firstTimeUse {
2023-09-07 21:28:22 +00:00
_ , _ = 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 ) ) )
2023-08-09 11:00:25 +00:00
}
}
return resolved , nil
}
2023-08-10 10:08:00 +00:00
func ( pr * ParameterResolver ) isFirstTimeUse ( parameterName string ) bool {
return findWorkspaceBuildParameter ( parameterName , pr . lastBuildParameters ) == nil
2023-08-09 11:00:25 +00:00
}
2023-08-23 16:18:38 +00:00
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
}
2023-08-09 11:00:25 +00:00
func findTemplateVersionParameter ( workspaceBuildParameter codersdk . WorkspaceBuildParameter , templateVersionParameters [ ] codersdk . TemplateVersionParameter ) * codersdk . TemplateVersionParameter {
for _ , tvp := range templateVersionParameters {
if tvp . Name == workspaceBuildParameter . Name {
return & tvp
}
}
return nil
}
2023-08-10 10:08:00 +00:00
func findWorkspaceBuildParameter ( parameterName string , params [ ] codersdk . WorkspaceBuildParameter ) * codersdk . WorkspaceBuildParameter {
2023-08-09 11:00:25 +00:00
for _ , p := range params {
2023-08-10 10:08:00 +00:00
if p . Name == parameterName {
2023-08-09 11:00:25 +00:00
return & p
}
}
return nil
}
2023-08-23 16:18:38 +00:00
func isValidTemplateParameterOption ( buildParameter codersdk . WorkspaceBuildParameter , options [ ] codersdk . TemplateVersionParameterOption ) bool {
for _ , opt := range options {
if opt . Value == buildParameter . Value {
return true
}
}
return false
}
2023-11-06 13:44:39 +00:00
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 ( ) )
}