mirror of https://github.com/coder/coder.git
feat(cli): provide parameter values via command line (#8898)
This commit is contained in:
parent
1730d35467
commit
0d382d1e05
151
cli/create.go
151
cli/create.go
|
@ -18,11 +18,12 @@ import (
|
||||||
|
|
||||||
func (r *RootCmd) create() *clibase.Cmd {
|
func (r *RootCmd) create() *clibase.Cmd {
|
||||||
var (
|
var (
|
||||||
richParameterFile string
|
templateName string
|
||||||
templateName string
|
startAt string
|
||||||
startAt string
|
stopAfter time.Duration
|
||||||
stopAfter time.Duration
|
workspaceName string
|
||||||
workspaceName string
|
|
||||||
|
parameterFlags workspaceParameterFlags
|
||||||
)
|
)
|
||||||
client := new(codersdk.Client)
|
client := new(codersdk.Client)
|
||||||
cmd := &clibase.Cmd{
|
cmd := &clibase.Cmd{
|
||||||
|
@ -129,10 +130,18 @@ func (r *RootCmd) create() *clibase.Cmd {
|
||||||
schedSpec = ptr.Ref(sched.String())
|
schedSpec = ptr.Ref(sched.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
buildParams, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
|
cliRichParameters, err := asWorkspaceBuildParameters(parameterFlags.richParameters)
|
||||||
Template: template,
|
if err != nil {
|
||||||
RichParameterFile: richParameterFile,
|
return xerrors.Errorf("can't parse given parameter values: %w", err)
|
||||||
NewWorkspaceName: workspaceName,
|
}
|
||||||
|
|
||||||
|
richParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
|
||||||
|
Action: WorkspaceCreate,
|
||||||
|
Template: template,
|
||||||
|
NewWorkspaceName: workspaceName,
|
||||||
|
|
||||||
|
RichParameterFile: parameterFlags.richParameterFile,
|
||||||
|
RichParameters: cliRichParameters,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("prepare build: %w", err)
|
return xerrors.Errorf("prepare build: %w", err)
|
||||||
|
@ -156,7 +165,7 @@ func (r *RootCmd) create() *clibase.Cmd {
|
||||||
Name: workspaceName,
|
Name: workspaceName,
|
||||||
AutostartSchedule: schedSpec,
|
AutostartSchedule: schedSpec,
|
||||||
TTLMillis: ttlMillis,
|
TTLMillis: ttlMillis,
|
||||||
RichParameterValues: buildParams.richParameters,
|
RichParameterValues: richParameters,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("create workspace: %w", err)
|
return xerrors.Errorf("create workspace: %w", err)
|
||||||
|
@ -179,12 +188,6 @@ func (r *RootCmd) create() *clibase.Cmd {
|
||||||
Description: "Specify a template name.",
|
Description: "Specify a template name.",
|
||||||
Value: clibase.StringOf(&templateName),
|
Value: clibase.StringOf(&templateName),
|
||||||
},
|
},
|
||||||
clibase.Option{
|
|
||||||
Flag: "rich-parameter-file",
|
|
||||||
Env: "CODER_RICH_PARAMETER_FILE",
|
|
||||||
Description: "Specify a file path with values for rich parameters defined in the template.",
|
|
||||||
Value: clibase.StringOf(&richParameterFile),
|
|
||||||
},
|
|
||||||
clibase.Option{
|
clibase.Option{
|
||||||
Flag: "start-at",
|
Flag: "start-at",
|
||||||
Env: "CODER_WORKSPACE_START_AT",
|
Env: "CODER_WORKSPACE_START_AT",
|
||||||
|
@ -199,99 +202,59 @@ func (r *RootCmd) create() *clibase.Cmd {
|
||||||
},
|
},
|
||||||
cliui.SkipPromptOption(),
|
cliui.SkipPromptOption(),
|
||||||
)
|
)
|
||||||
|
cmd.Options = append(cmd.Options, parameterFlags.cliParameters()...)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
type prepWorkspaceBuildArgs struct {
|
type prepWorkspaceBuildArgs struct {
|
||||||
Template codersdk.Template
|
Action WorkspaceCLIAction
|
||||||
ExistingRichParams []codersdk.WorkspaceBuildParameter
|
Template codersdk.Template
|
||||||
RichParameterFile string
|
NewWorkspaceName string
|
||||||
NewWorkspaceName string
|
WorkspaceID uuid.UUID
|
||||||
|
|
||||||
UpdateWorkspace bool
|
LastBuildParameters []codersdk.WorkspaceBuildParameter
|
||||||
BuildOptions bool
|
|
||||||
WorkspaceID uuid.UUID
|
|
||||||
}
|
|
||||||
|
|
||||||
type buildParameters struct {
|
PromptBuildOptions bool
|
||||||
// Rich parameters stores values for build parameters annotated with description, icon, type, etc.
|
BuildOptions []codersdk.WorkspaceBuildParameter
|
||||||
richParameters []codersdk.WorkspaceBuildParameter
|
|
||||||
|
PromptRichParameters bool
|
||||||
|
RichParameters []codersdk.WorkspaceBuildParameter
|
||||||
|
RichParameterFile string
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepWorkspaceBuild will ensure a workspace build will succeed on the latest template version.
|
// prepWorkspaceBuild will ensure a workspace build will succeed on the latest template version.
|
||||||
// Any missing params will be prompted to the user. It supports legacy and rich parameters.
|
// Any missing params will be prompted to the user. It supports rich parameters.
|
||||||
func prepWorkspaceBuild(inv *clibase.Invocation, client *codersdk.Client, args prepWorkspaceBuildArgs) (*buildParameters, error) {
|
func prepWorkspaceBuild(inv *clibase.Invocation, client *codersdk.Client, args prepWorkspaceBuildArgs) ([]codersdk.WorkspaceBuildParameter, error) {
|
||||||
ctx := inv.Context()
|
ctx := inv.Context()
|
||||||
|
|
||||||
templateVersion, err := client.TemplateVersion(ctx, args.Template.ActiveVersionID)
|
templateVersion, err := client.TemplateVersion(ctx, args.Template.ActiveVersionID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, xerrors.Errorf("get template version: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rich parameters
|
|
||||||
templateVersionParameters, err := client.TemplateVersionRichParameters(inv.Context(), templateVersion.ID)
|
templateVersionParameters, err := client.TemplateVersionRichParameters(inv.Context(), templateVersion.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, xerrors.Errorf("get template version rich parameters: %w", err)
|
return nil, xerrors.Errorf("get template version rich parameters: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
parameterMapFromFile := map[string]string{}
|
parameterFile := map[string]string{}
|
||||||
useParamFile := false
|
|
||||||
if args.RichParameterFile != "" {
|
if args.RichParameterFile != "" {
|
||||||
useParamFile = true
|
parameterFile, err = parseParameterMapFile(args.RichParameterFile)
|
||||||
_, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Paragraph.Render("Attempting to read the variables from the rich parameter file.")+"\r\n")
|
|
||||||
parameterMapFromFile, err = createParameterMapFromFile(args.RichParameterFile)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, xerrors.Errorf("can't parse parameter map file: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
disclaimerPrinted := false
|
|
||||||
richParameters := make([]codersdk.WorkspaceBuildParameter, 0)
|
|
||||||
PromptRichParamLoop:
|
|
||||||
for _, templateVersionParameter := range templateVersionParameters {
|
|
||||||
if !args.BuildOptions && templateVersionParameter.Ephemeral {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !disclaimerPrinted {
|
resolver := new(ParameterResolver).
|
||||||
_, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Paragraph.Render("This template has customizable parameters. Values can be changed after create, but may have unintended side effects (like data loss).")+"\r\n")
|
WithLastBuildParameters(args.LastBuildParameters).
|
||||||
disclaimerPrinted = true
|
WithPromptBuildOptions(args.PromptBuildOptions).
|
||||||
}
|
WithBuildOptions(args.BuildOptions).
|
||||||
|
WithPromptRichParameters(args.PromptRichParameters).
|
||||||
// Param file is all or nothing
|
WithRichParameters(args.RichParameters).
|
||||||
if !useParamFile && !templateVersionParameter.Ephemeral {
|
WithRichParametersFile(parameterFile)
|
||||||
for _, e := range args.ExistingRichParams {
|
buildParameters, err := resolver.Resolve(inv, args.Action, templateVersionParameters)
|
||||||
if e.Name == templateVersionParameter.Name {
|
if err != nil {
|
||||||
// If the param already exists, we do not need to prompt it again.
|
return nil, err
|
||||||
// The workspace scope will reuse params for each build.
|
|
||||||
continue PromptRichParamLoop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.UpdateWorkspace && !templateVersionParameter.Mutable {
|
|
||||||
// Check if the immutable parameter was used in the previous build. If so, then it isn't a fresh one
|
|
||||||
// and the user should be warned.
|
|
||||||
exists, err := workspaceBuildParameterExists(ctx, client, args.WorkspaceID, templateVersionParameter)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if exists {
|
|
||||||
_, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Warn.Render(fmt.Sprintf(`Parameter %q is not mutable, so can't be customized after workspace creation.`, templateVersionParameter.Name)))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parameterValue, err := getWorkspaceBuildParameterValueFromMapOrInput(inv, parameterMapFromFile, templateVersionParameter)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
richParameters = append(richParameters, *parameterValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
if disclaimerPrinted {
|
|
||||||
_, _ = fmt.Fprintln(inv.Stdout)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cliui.GitAuth(ctx, inv.Stdout, cliui.GitAuthOptions{
|
err = cliui.GitAuth(ctx, inv.Stdout, cliui.GitAuthOptions{
|
||||||
|
@ -306,7 +269,7 @@ PromptRichParamLoop:
|
||||||
// Run a dry-run with the given parameters to check correctness
|
// Run a dry-run with the given parameters to check correctness
|
||||||
dryRun, err := client.CreateTemplateVersionDryRun(inv.Context(), templateVersion.ID, codersdk.CreateTemplateVersionDryRunRequest{
|
dryRun, err := client.CreateTemplateVersionDryRun(inv.Context(), templateVersion.ID, codersdk.CreateTemplateVersionDryRunRequest{
|
||||||
WorkspaceName: args.NewWorkspaceName,
|
WorkspaceName: args.NewWorkspaceName,
|
||||||
RichParameterValues: richParameters,
|
RichParameterValues: buildParameters,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, xerrors.Errorf("begin workspace dry-run: %w", err)
|
return nil, xerrors.Errorf("begin workspace dry-run: %w", err)
|
||||||
|
@ -346,21 +309,5 @@ PromptRichParamLoop:
|
||||||
return nil, xerrors.Errorf("get resources: %w", err)
|
return nil, xerrors.Errorf("get resources: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &buildParameters{
|
return buildParameters, nil
|
||||||
richParameters: richParameters,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func workspaceBuildParameterExists(ctx context.Context, client *codersdk.Client, workspaceID uuid.UUID, templateVersionParameter codersdk.TemplateVersionParameter) (bool, error) {
|
|
||||||
lastBuildParameters, err := client.WorkspaceBuildParameters(ctx, workspaceID)
|
|
||||||
if err != nil {
|
|
||||||
return false, xerrors.Errorf("can't fetch last workspace build parameters: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range lastBuildParameters {
|
|
||||||
if p.Name == templateVersionParameter.Name {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package cli_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
@ -357,6 +358,41 @@ func TestCreateWithRichParameters(t *testing.T) {
|
||||||
}
|
}
|
||||||
<-doneChan
|
<-doneChan
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("ParameterFlags", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||||
|
user := coderdtest.CreateFirstUser(t, client)
|
||||||
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
|
||||||
|
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||||
|
|
||||||
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||||
|
|
||||||
|
inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name,
|
||||||
|
"--parameter", fmt.Sprintf("%s=%s", firstParameterName, firstParameterValue),
|
||||||
|
"--parameter", fmt.Sprintf("%s=%s", secondParameterName, secondParameterValue),
|
||||||
|
"--parameter", fmt.Sprintf("%s=%s", immutableParameterName, immutableParameterValue))
|
||||||
|
clitest.SetupConfig(t, client, root)
|
||||||
|
doneChan := make(chan struct{})
|
||||||
|
pty := ptytest.New(t).Attach(inv)
|
||||||
|
go func() {
|
||||||
|
defer close(doneChan)
|
||||||
|
err := inv.Run()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
matches := []string{
|
||||||
|
"Confirm create?", "yes",
|
||||||
|
}
|
||||||
|
for i := 0; i < len(matches); i += 2 {
|
||||||
|
match := matches[i]
|
||||||
|
value := matches[i+1]
|
||||||
|
pty.ExpectMatch(match)
|
||||||
|
pty.WriteLine(value)
|
||||||
|
}
|
||||||
|
<-doneChan
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateValidateRichParameters(t *testing.T) {
|
func TestCreateValidateRichParameters(t *testing.T) {
|
||||||
|
|
129
cli/parameter.go
129
cli/parameter.go
|
@ -4,71 +4,98 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
"github.com/coder/coder/cli/clibase"
|
"github.com/coder/coder/cli/clibase"
|
||||||
"github.com/coder/coder/cli/cliui"
|
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Reads a YAML file and populates a string -> string map.
|
// workspaceParameterFlags are used by commands processing rich parameters and/or build options.
|
||||||
// Throws an error if the file name is empty.
|
type workspaceParameterFlags struct {
|
||||||
func createParameterMapFromFile(parameterFile string) (map[string]string, error) {
|
promptBuildOptions bool
|
||||||
if parameterFile != "" {
|
buildOptions []string
|
||||||
parameterFileContents, err := os.ReadFile(parameterFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
mapStringInterface := make(map[string]interface{})
|
richParameterFile string
|
||||||
err = yaml.Unmarshal(parameterFileContents, &mapStringInterface)
|
richParameters []string
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, xerrors.Errorf("Parameter file name is not specified")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getWorkspaceBuildParameterValueFromMapOrInput(inv *clibase.Invocation, parameterMap map[string]string, templateVersionParameter codersdk.TemplateVersionParameter) (*codersdk.WorkspaceBuildParameter, error) {
|
func (wpf *workspaceParameterFlags) cliBuildOptions() []clibase.Option {
|
||||||
var parameterValue string
|
return clibase.OptionSet{
|
||||||
var err error
|
{
|
||||||
if parameterMap != nil {
|
Flag: "build-option",
|
||||||
var ok bool
|
Env: "CODER_BUILD_OPTION",
|
||||||
parameterValue, ok = parameterMap[templateVersionParameter.Name]
|
Description: `Build option value in the format "name=value".`,
|
||||||
if !ok {
|
Value: clibase.StringArrayOf(&wpf.buildOptions),
|
||||||
parameterValue, err = cliui.RichParameter(inv, templateVersionParameter)
|
},
|
||||||
|
{
|
||||||
|
Flag: "build-options",
|
||||||
|
Description: "Prompt for one-time build options defined with ephemeral parameters.",
|
||||||
|
Value: clibase.BoolOf(&wpf.promptBuildOptions),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wpf *workspaceParameterFlags) cliParameters() []clibase.Option {
|
||||||
|
return clibase.OptionSet{
|
||||||
|
clibase.Option{
|
||||||
|
Flag: "parameter",
|
||||||
|
Env: "CODER_RICH_PARAMETER",
|
||||||
|
Description: `Rich parameter value in the format "name=value".`,
|
||||||
|
Value: clibase.StringArrayOf(&wpf.richParameters),
|
||||||
|
},
|
||||||
|
clibase.Option{
|
||||||
|
Flag: "rich-parameter-file",
|
||||||
|
Env: "CODER_RICH_PARAMETER_FILE",
|
||||||
|
Description: "Specify a file path with values for rich parameters defined in the template.",
|
||||||
|
Value: clibase.StringOf(&wpf.richParameterFile),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
parameterMap[k] = string(b)
|
||||||
} else {
|
default:
|
||||||
parameterValue, err = cliui.RichParameter(inv, templateVersionParameter)
|
return nil, xerrors.Errorf("invalid parameter type: %T", v)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &codersdk.WorkspaceBuildParameter{
|
return parameterMap, nil
|
||||||
Name: templateVersionParameter.Name,
|
|
||||||
Value: parameterValue,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ func TestCreateParameterMapFromFile(t *testing.T) {
|
||||||
parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml")
|
parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml")
|
||||||
_, _ = parameterFile.WriteString("region: \"bananas\"\ndisk: \"20\"\n")
|
_, _ = parameterFile.WriteString("region: \"bananas\"\ndisk: \"20\"\n")
|
||||||
|
|
||||||
parameterMapFromFile, err := createParameterMapFromFile(parameterFile.Name())
|
parameterMapFromFile, err := parseParameterMapFile(parameterFile.Name())
|
||||||
|
|
||||||
expectedMap := map[string]string{
|
expectedMap := map[string]string{
|
||||||
"region": "bananas",
|
"region": "bananas",
|
||||||
|
@ -28,18 +28,10 @@ func TestCreateParameterMapFromFile(t *testing.T) {
|
||||||
|
|
||||||
removeTmpDirUntilSuccess(t, tempDir)
|
removeTmpDirUntilSuccess(t, tempDir)
|
||||||
})
|
})
|
||||||
t.Run("WithEmptyFilename", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
parameterMapFromFile, err := createParameterMapFromFile("")
|
|
||||||
|
|
||||||
assert.Nil(t, parameterMapFromFile)
|
|
||||||
assert.EqualError(t, err, "Parameter file name is not specified")
|
|
||||||
})
|
|
||||||
t.Run("WithInvalidFilename", func(t *testing.T) {
|
t.Run("WithInvalidFilename", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
parameterMapFromFile, err := createParameterMapFromFile("invalidFile.yaml")
|
parameterMapFromFile, err := parseParameterMapFile("invalidFile.yaml")
|
||||||
|
|
||||||
assert.Nil(t, parameterMapFromFile)
|
assert.Nil(t, parameterMapFromFile)
|
||||||
|
|
||||||
|
@ -57,7 +49,7 @@ func TestCreateParameterMapFromFile(t *testing.T) {
|
||||||
parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml")
|
parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml")
|
||||||
_, _ = parameterFile.WriteString("region = \"bananas\"\ndisk = \"20\"\n")
|
_, _ = parameterFile.WriteString("region = \"bananas\"\ndisk = \"20\"\n")
|
||||||
|
|
||||||
parameterMapFromFile, err := createParameterMapFromFile(parameterFile.Name())
|
parameterMapFromFile, err := parseParameterMapFile(parameterFile.Name())
|
||||||
|
|
||||||
assert.Nil(t, parameterMapFromFile)
|
assert.Nil(t, parameterMapFromFile)
|
||||||
assert.EqualError(t, err, "yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `region ...` into map[string]interface {}")
|
assert.EqualError(t, err, "yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `region ...` into map[string]interface {}")
|
||||||
|
|
|
@ -0,0 +1,224 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
|
"github.com/coder/coder/cli/clibase"
|
||||||
|
"github.com/coder/coder/cli/cliui"
|
||||||
|
"github.com/coder/coder/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 {
|
||||||
|
for name, value := range pr.richParametersFile {
|
||||||
|
for i, r := range resolved {
|
||||||
|
if r.Name == name {
|
||||||
|
resolved[i].Value = value
|
||||||
|
goto done
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved = append(resolved, codersdk.WorkspaceBuildParameter{
|
||||||
|
Name: name,
|
||||||
|
Value: value,
|
||||||
|
})
|
||||||
|
done:
|
||||||
|
}
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pr *ParameterResolver) resolveWithCommandLineOrEnv(resolved []codersdk.WorkspaceBuildParameter) []codersdk.WorkspaceBuildParameter {
|
||||||
|
for _, richParameter := range pr.richParameters {
|
||||||
|
for i, r := range resolved {
|
||||||
|
if r.Name == richParameter.Name {
|
||||||
|
resolved[i].Value = richParameter.Value
|
||||||
|
goto richParameterDone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved = append(resolved, richParameter)
|
||||||
|
richParameterDone:
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, buildOption := range pr.buildOptions {
|
||||||
|
for i, r := range resolved {
|
||||||
|
if r.Name == buildOption.Name {
|
||||||
|
resolved[i].Value = buildOption.Value
|
||||||
|
goto buildOptionDone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved = append(resolved, buildOption)
|
||||||
|
buildOptionDone:
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, r := range resolved {
|
||||||
|
if r.Name == buildParameter.Name {
|
||||||
|
resolved[i].Value = buildParameter.Value
|
||||||
|
goto done
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved = append(resolved, buildParameter)
|
||||||
|
done:
|
||||||
|
}
|
||||||
|
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 && len(pr.buildOptions) == 0 {
|
||||||
|
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, resolved)
|
||||||
|
if p != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
firstTimeUse := pr.isFirstTimeUse(tvp)
|
||||||
|
|
||||||
|
if (tvp.Ephemeral && pr.promptBuildOptions) ||
|
||||||
|
tvp.Required ||
|
||||||
|
(action == WorkspaceUpdate && !tvp.Mutable && firstTimeUse) ||
|
||||||
|
(action == WorkspaceUpdate && tvp.Mutable && !tvp.Ephemeral && pr.promptRichParameters) ||
|
||||||
|
(action == WorkspaceCreate && !tvp.Ephemeral) {
|
||||||
|
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, cliui.DefaultStyles.Warn.Render(fmt.Sprintf("Parameter %q is not mutable, and cannot be customized after workspace creation.", tvp.Name)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolved, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pr *ParameterResolver) isFirstTimeUse(tvp codersdk.TemplateVersionParameter) bool {
|
||||||
|
return findWorkspaceBuildParameter(tvp, pr.lastBuildParameters) == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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(tvp codersdk.TemplateVersionParameter, params []codersdk.WorkspaceBuildParameter) *codersdk.WorkspaceBuildParameter {
|
||||||
|
for _, p := range params {
|
||||||
|
if p.Name == tvp.Name {
|
||||||
|
return &p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
"github.com/coder/coder/cli/clibase"
|
"github.com/coder/coder/cli/clibase"
|
||||||
"github.com/coder/coder/cli/cliui"
|
"github.com/coder/coder/cli/cliui"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
|
@ -21,7 +23,7 @@ func (r *RootCmd) restart() *clibase.Cmd {
|
||||||
clibase.RequireNArgs(1),
|
clibase.RequireNArgs(1),
|
||||||
r.InitClient(client),
|
r.InitClient(client),
|
||||||
),
|
),
|
||||||
Options: append(parameterFlags.options(), cliui.SkipPromptOption()),
|
Options: append(parameterFlags.cliBuildOptions(), cliui.SkipPromptOption()),
|
||||||
Handler: func(inv *clibase.Invocation) error {
|
Handler: func(inv *clibase.Invocation) error {
|
||||||
ctx := inv.Context()
|
ctx := inv.Context()
|
||||||
out := inv.Stdout
|
out := inv.Stdout
|
||||||
|
@ -31,14 +33,29 @@ func (r *RootCmd) restart() *clibase.Cmd {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lastBuildParameters, err := client.WorkspaceBuildParameters(inv.Context(), workspace.LatestBuild.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
template, err := client.Template(inv.Context(), workspace.TemplateID)
|
template, err := client.Template(inv.Context(), workspace.TemplateID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
buildParams, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{
|
buildOptions, err := asWorkspaceBuildParameters(parameterFlags.buildOptions)
|
||||||
Template: template,
|
if err != nil {
|
||||||
BuildOptions: parameterFlags.buildOptions,
|
return xerrors.Errorf("can't parse build options: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildParameters, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{
|
||||||
|
Action: WorkspaceRestart,
|
||||||
|
Template: template,
|
||||||
|
|
||||||
|
LastBuildParameters: lastBuildParameters,
|
||||||
|
|
||||||
|
PromptBuildOptions: parameterFlags.promptBuildOptions,
|
||||||
|
BuildOptions: buildOptions,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -65,7 +82,7 @@ func (r *RootCmd) restart() *clibase.Cmd {
|
||||||
|
|
||||||
build, err = client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
build, err = client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||||
Transition: codersdk.WorkspaceTransitionStart,
|
Transition: codersdk.WorkspaceTransitionStart,
|
||||||
RichParameterValues: buildParams.richParameters,
|
RichParameterValues: buildParameters,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -2,6 +2,7 @@ package cli_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -126,4 +127,57 @@ func TestRestart(t *testing.T) {
|
||||||
Value: ephemeralParameterValue,
|
Value: ephemeralParameterValue,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("BuildOptionFlags", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||||
|
user := coderdtest.CreateFirstUser(t, client)
|
||||||
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
|
||||||
|
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||||
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||||
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||||
|
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||||
|
|
||||||
|
inv, root := clitest.New(t, "restart", workspace.Name,
|
||||||
|
"--build-option", fmt.Sprintf("%s=%s", ephemeralParameterName, ephemeralParameterValue))
|
||||||
|
clitest.SetupConfig(t, client, root)
|
||||||
|
doneChan := make(chan struct{})
|
||||||
|
pty := ptytest.New(t).Attach(inv)
|
||||||
|
go func() {
|
||||||
|
defer close(doneChan)
|
||||||
|
err := inv.Run()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
matches := []string{
|
||||||
|
"Confirm restart workspace?", "yes",
|
||||||
|
"Stopping workspace", "",
|
||||||
|
"Starting workspace", "",
|
||||||
|
"workspace has been restarted", "",
|
||||||
|
}
|
||||||
|
for i := 0; i < len(matches); i += 2 {
|
||||||
|
match := matches[i]
|
||||||
|
value := matches[i+1]
|
||||||
|
pty.ExpectMatch(match)
|
||||||
|
|
||||||
|
if value != "" {
|
||||||
|
pty.WriteLine(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<-doneChan
|
||||||
|
|
||||||
|
// Verify if build option is set
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
workspace, err := client.WorkspaceByOwnerAndName(ctx, user.UserID.String(), workspace.Name, codersdk.WorkspaceOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, actualParameters, codersdk.WorkspaceBuildParameter{
|
||||||
|
Name: ephemeralParameterName,
|
||||||
|
Value: ephemeralParameterValue,
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
84
cli/start.go
84
cli/start.go
|
@ -11,21 +11,6 @@ import (
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
// workspaceParameterFlags are used by "start", "restart", and "update".
|
|
||||||
type workspaceParameterFlags struct {
|
|
||||||
buildOptions bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wpf *workspaceParameterFlags) options() []clibase.Option {
|
|
||||||
return clibase.OptionSet{
|
|
||||||
{
|
|
||||||
Flag: "build-options",
|
|
||||||
Description: "Prompt for one-time build options defined with ephemeral parameters.",
|
|
||||||
Value: clibase.BoolOf(&wpf.buildOptions),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RootCmd) start() *clibase.Cmd {
|
func (r *RootCmd) start() *clibase.Cmd {
|
||||||
var parameterFlags workspaceParameterFlags
|
var parameterFlags workspaceParameterFlags
|
||||||
|
|
||||||
|
@ -38,21 +23,36 @@ func (r *RootCmd) start() *clibase.Cmd {
|
||||||
clibase.RequireNArgs(1),
|
clibase.RequireNArgs(1),
|
||||||
r.InitClient(client),
|
r.InitClient(client),
|
||||||
),
|
),
|
||||||
Options: append(parameterFlags.options(), cliui.SkipPromptOption()),
|
Options: append(parameterFlags.cliBuildOptions(), cliui.SkipPromptOption()),
|
||||||
Handler: func(inv *clibase.Invocation) error {
|
Handler: func(inv *clibase.Invocation) error {
|
||||||
workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0])
|
workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lastBuildParameters, err := client.WorkspaceBuildParameters(inv.Context(), workspace.LatestBuild.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
template, err := client.Template(inv.Context(), workspace.TemplateID)
|
template, err := client.Template(inv.Context(), workspace.TemplateID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
buildParams, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{
|
buildOptions, err := asWorkspaceBuildParameters(parameterFlags.buildOptions)
|
||||||
Template: template,
|
if err != nil {
|
||||||
BuildOptions: parameterFlags.buildOptions,
|
return xerrors.Errorf("unable to parse build options: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildParameters, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{
|
||||||
|
Action: WorkspaceStart,
|
||||||
|
Template: template,
|
||||||
|
|
||||||
|
LastBuildParameters: lastBuildParameters,
|
||||||
|
|
||||||
|
PromptBuildOptions: parameterFlags.promptBuildOptions,
|
||||||
|
BuildOptions: buildOptions,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -60,7 +60,7 @@ func (r *RootCmd) start() *clibase.Cmd {
|
||||||
|
|
||||||
build, err := client.CreateWorkspaceBuild(inv.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
build, err := client.CreateWorkspaceBuild(inv.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||||
Transition: codersdk.WorkspaceTransitionStart,
|
Transition: codersdk.WorkspaceTransitionStart,
|
||||||
RichParameterValues: buildParams.richParameters,
|
RichParameterValues: buildParameters,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -79,16 +79,21 @@ func (r *RootCmd) start() *clibase.Cmd {
|
||||||
}
|
}
|
||||||
|
|
||||||
type prepStartWorkspaceArgs struct {
|
type prepStartWorkspaceArgs struct {
|
||||||
Template codersdk.Template
|
Action WorkspaceCLIAction
|
||||||
BuildOptions bool
|
Template codersdk.Template
|
||||||
|
|
||||||
|
LastBuildParameters []codersdk.WorkspaceBuildParameter
|
||||||
|
|
||||||
|
PromptBuildOptions bool
|
||||||
|
BuildOptions []codersdk.WorkspaceBuildParameter
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepStartWorkspace(inv *clibase.Invocation, client *codersdk.Client, args prepStartWorkspaceArgs) (*buildParameters, error) {
|
func prepStartWorkspace(inv *clibase.Invocation, client *codersdk.Client, args prepStartWorkspaceArgs) ([]codersdk.WorkspaceBuildParameter, error) {
|
||||||
ctx := inv.Context()
|
ctx := inv.Context()
|
||||||
|
|
||||||
templateVersion, err := client.TemplateVersion(ctx, args.Template.ActiveVersionID)
|
templateVersion, err := client.TemplateVersion(ctx, args.Template.ActiveVersionID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, xerrors.Errorf("get template version: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
templateVersionParameters, err := client.TemplateVersionRichParameters(inv.Context(), templateVersion.ID)
|
templateVersionParameters, err := client.TemplateVersionRichParameters(inv.Context(), templateVersion.ID)
|
||||||
|
@ -96,30 +101,9 @@ func prepStartWorkspace(inv *clibase.Invocation, client *codersdk.Client, args p
|
||||||
return nil, xerrors.Errorf("get template version rich parameters: %w", err)
|
return nil, xerrors.Errorf("get template version rich parameters: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
richParameters := make([]codersdk.WorkspaceBuildParameter, 0)
|
resolver := new(ParameterResolver).
|
||||||
if !args.BuildOptions {
|
WithLastBuildParameters(args.LastBuildParameters).
|
||||||
return &buildParameters{
|
WithPromptBuildOptions(args.PromptBuildOptions).
|
||||||
richParameters: richParameters,
|
WithBuildOptions(args.BuildOptions)
|
||||||
}, nil
|
return resolver.Resolve(inv, args.Action, templateVersionParameters)
|
||||||
}
|
|
||||||
|
|
||||||
for _, templateVersionParameter := range templateVersionParameters {
|
|
||||||
if !templateVersionParameter.Ephemeral {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
parameterValue, err := cliui.RichParameter(inv, templateVersionParameter)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
richParameters = append(richParameters, codersdk.WorkspaceBuildParameter{
|
|
||||||
Name: templateVersionParameter.Name,
|
|
||||||
Value: parameterValue,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return &buildParameters{
|
|
||||||
richParameters: richParameters,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package cli_test
|
package cli_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -99,4 +100,43 @@ func TestStart(t *testing.T) {
|
||||||
Value: ephemeralParameterValue,
|
Value: ephemeralParameterValue,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("BuildOptionFlags", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||||
|
user := coderdtest.CreateFirstUser(t, client)
|
||||||
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
|
||||||
|
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||||
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||||
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||||
|
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||||
|
|
||||||
|
inv, root := clitest.New(t, "start", workspace.Name,
|
||||||
|
"--build-option", fmt.Sprintf("%s=%s", ephemeralParameterName, ephemeralParameterValue))
|
||||||
|
clitest.SetupConfig(t, client, root)
|
||||||
|
doneChan := make(chan struct{})
|
||||||
|
pty := ptytest.New(t).Attach(inv)
|
||||||
|
go func() {
|
||||||
|
defer close(doneChan)
|
||||||
|
err := inv.Run()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
pty.ExpectMatch("workspace has been started")
|
||||||
|
<-doneChan
|
||||||
|
|
||||||
|
// Verify if build option is set
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, actualParameters, codersdk.WorkspaceBuildParameter{
|
||||||
|
Name: ephemeralParameterName,
|
||||||
|
Value: ephemeralParameterValue,
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,9 @@ Create a workspace
|
||||||
[40m [0m[91;40m$ coder create <username>/<workspace_name>[0m[40m [0m
|
[40m [0m[91;40m$ coder create <username>/<workspace_name>[0m[40m [0m
|
||||||
|
|
||||||
[1mOptions[0m
|
[1mOptions[0m
|
||||||
|
--parameter string-array, $CODER_RICH_PARAMETER
|
||||||
|
Rich parameter value in the format "name=value".
|
||||||
|
|
||||||
--rich-parameter-file string, $CODER_RICH_PARAMETER_FILE
|
--rich-parameter-file string, $CODER_RICH_PARAMETER_FILE
|
||||||
Specify a file path with values for rich parameters defined in the
|
Specify a file path with values for rich parameters defined in the
|
||||||
template.
|
template.
|
||||||
|
|
|
@ -3,6 +3,9 @@ Usage: coder restart [flags] <workspace>
|
||||||
Restart a workspace
|
Restart a workspace
|
||||||
|
|
||||||
[1mOptions[0m
|
[1mOptions[0m
|
||||||
|
--build-option string-array, $CODER_BUILD_OPTION
|
||||||
|
Build option value in the format "name=value".
|
||||||
|
|
||||||
--build-options bool
|
--build-options bool
|
||||||
Prompt for one-time build options defined with ephemeral parameters.
|
Prompt for one-time build options defined with ephemeral parameters.
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,9 @@ Usage: coder start [flags] <workspace>
|
||||||
Start a workspace
|
Start a workspace
|
||||||
|
|
||||||
[1mOptions[0m
|
[1mOptions[0m
|
||||||
|
--build-option string-array, $CODER_BUILD_OPTION
|
||||||
|
Build option value in the format "name=value".
|
||||||
|
|
||||||
--build-options bool
|
--build-options bool
|
||||||
Prompt for one-time build options defined with ephemeral parameters.
|
Prompt for one-time build options defined with ephemeral parameters.
|
||||||
|
|
||||||
|
|
|
@ -9,9 +9,15 @@ Use --always-prompt to change the parameter values of the workspace.
|
||||||
Always prompt all parameters. Does not pull parameter values from
|
Always prompt all parameters. Does not pull parameter values from
|
||||||
existing workspace.
|
existing workspace.
|
||||||
|
|
||||||
|
--build-option string-array, $CODER_BUILD_OPTION
|
||||||
|
Build option value in the format "name=value".
|
||||||
|
|
||||||
--build-options bool
|
--build-options bool
|
||||||
Prompt for one-time build options defined with ephemeral parameters.
|
Prompt for one-time build options defined with ephemeral parameters.
|
||||||
|
|
||||||
|
--parameter string-array, $CODER_RICH_PARAMETER
|
||||||
|
Rich parameter value in the format "name=value".
|
||||||
|
|
||||||
--rich-parameter-file string, $CODER_RICH_PARAMETER_FILE
|
--rich-parameter-file string, $CODER_RICH_PARAMETER_FILE
|
||||||
Specify a file path with values for rich parameters defined in the
|
Specify a file path with values for rich parameters defined in the
|
||||||
template.
|
template.
|
||||||
|
|
|
@ -3,14 +3,15 @@ package cli
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
"github.com/coder/coder/cli/clibase"
|
"github.com/coder/coder/cli/clibase"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *RootCmd) update() *clibase.Cmd {
|
func (r *RootCmd) update() *clibase.Cmd {
|
||||||
var (
|
var (
|
||||||
richParameterFile string
|
alwaysPrompt bool
|
||||||
alwaysPrompt bool
|
|
||||||
|
|
||||||
parameterFlags workspaceParameterFlags
|
parameterFlags workspaceParameterFlags
|
||||||
)
|
)
|
||||||
|
@ -30,33 +31,45 @@ func (r *RootCmd) update() *clibase.Cmd {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !workspace.Outdated && !alwaysPrompt && !parameterFlags.buildOptions {
|
if !workspace.Outdated && !alwaysPrompt && !parameterFlags.promptBuildOptions && len(parameterFlags.buildOptions) == 0 {
|
||||||
_, _ = fmt.Fprintf(inv.Stdout, "Workspace isn't outdated!\n")
|
_, _ = fmt.Fprintf(inv.Stdout, "Workspace isn't outdated!\n")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildOptions, err := asWorkspaceBuildParameters(parameterFlags.buildOptions)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
template, err := client.Template(inv.Context(), workspace.TemplateID)
|
template, err := client.Template(inv.Context(), workspace.TemplateID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var existingRichParams []codersdk.WorkspaceBuildParameter
|
lastBuildParameters, err := client.WorkspaceBuildParameters(inv.Context(), workspace.LatestBuild.ID)
|
||||||
if !alwaysPrompt {
|
if err != nil {
|
||||||
existingRichParams, err = client.WorkspaceBuildParameters(inv.Context(), workspace.LatestBuild.ID)
|
return err
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildParams, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
|
cliRichParameters, err := asWorkspaceBuildParameters(parameterFlags.richParameters)
|
||||||
Template: template,
|
if err != nil {
|
||||||
ExistingRichParams: existingRichParams,
|
return xerrors.Errorf("can't parse given parameter values: %w", err)
|
||||||
RichParameterFile: richParameterFile,
|
}
|
||||||
NewWorkspaceName: workspace.Name,
|
|
||||||
|
|
||||||
UpdateWorkspace: true,
|
buildParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
|
||||||
WorkspaceID: workspace.LatestBuild.ID,
|
Action: WorkspaceUpdate,
|
||||||
|
Template: template,
|
||||||
|
NewWorkspaceName: workspace.Name,
|
||||||
|
WorkspaceID: workspace.LatestBuild.ID,
|
||||||
|
|
||||||
BuildOptions: parameterFlags.buildOptions,
|
LastBuildParameters: lastBuildParameters,
|
||||||
|
|
||||||
|
PromptBuildOptions: parameterFlags.promptBuildOptions,
|
||||||
|
BuildOptions: buildOptions,
|
||||||
|
|
||||||
|
PromptRichParameters: alwaysPrompt,
|
||||||
|
RichParameters: cliRichParameters,
|
||||||
|
RichParameterFile: parameterFlags.richParameterFile,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -65,7 +78,7 @@ func (r *RootCmd) update() *clibase.Cmd {
|
||||||
build, err := client.CreateWorkspaceBuild(inv.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
build, err := client.CreateWorkspaceBuild(inv.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||||
TemplateVersionID: template.ActiveVersionID,
|
TemplateVersionID: template.ActiveVersionID,
|
||||||
Transition: codersdk.WorkspaceTransitionStart,
|
Transition: codersdk.WorkspaceTransitionStart,
|
||||||
RichParameterValues: buildParams.richParameters,
|
RichParameterValues: buildParameters,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -92,13 +105,8 @@ func (r *RootCmd) update() *clibase.Cmd {
|
||||||
Description: "Always prompt all parameters. Does not pull parameter values from existing workspace.",
|
Description: "Always prompt all parameters. Does not pull parameter values from existing workspace.",
|
||||||
Value: clibase.BoolOf(&alwaysPrompt),
|
Value: clibase.BoolOf(&alwaysPrompt),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Flag: "rich-parameter-file",
|
|
||||||
Description: "Specify a file path with values for rich parameters defined in the template.",
|
|
||||||
Env: "CODER_RICH_PARAMETER_FILE",
|
|
||||||
Value: clibase.StringOf(&richParameterFile),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
cmd.Options = append(cmd.Options, parameterFlags.options()...)
|
cmd.Options = append(cmd.Options, parameterFlags.cliBuildOptions()...)
|
||||||
|
cmd.Options = append(cmd.Options, parameterFlags.cliParameters()...)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
|
@ -159,7 +159,7 @@ func TestUpdateWithRichParameters(t *testing.T) {
|
||||||
|
|
||||||
matches := []string{
|
matches := []string{
|
||||||
firstParameterDescription, firstParameterValue,
|
firstParameterDescription, firstParameterValue,
|
||||||
fmt.Sprintf("Parameter %q is not mutable, so can't be customized after workspace creation.", immutableParameterName), "",
|
fmt.Sprintf("Parameter %q is not mutable, and cannot be customized after workspace creation.", immutableParameterName), "",
|
||||||
secondParameterDescription, secondParameterValue,
|
secondParameterDescription, secondParameterValue,
|
||||||
}
|
}
|
||||||
for i := 0; i < len(matches); i += 2 {
|
for i := 0; i < len(matches); i += 2 {
|
||||||
|
@ -236,6 +236,55 @@ func TestUpdateWithRichParameters(t *testing.T) {
|
||||||
Value: ephemeralParameterValue,
|
Value: ephemeralParameterValue,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("BuildOptionFlags", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||||
|
user := coderdtest.CreateFirstUser(t, client)
|
||||||
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
|
||||||
|
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||||
|
|
||||||
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||||
|
|
||||||
|
const workspaceName = "my-workspace"
|
||||||
|
|
||||||
|
inv, root := clitest.New(t, "create", workspaceName, "--template", template.Name, "-y",
|
||||||
|
"--parameter", fmt.Sprintf("%s=%s", firstParameterName, firstParameterValue),
|
||||||
|
"--parameter", fmt.Sprintf("%s=%s", immutableParameterName, immutableParameterValue),
|
||||||
|
"--parameter", fmt.Sprintf("%s=%s", secondParameterName, secondParameterValue))
|
||||||
|
clitest.SetupConfig(t, client, root)
|
||||||
|
err := inv.Run()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
inv, root = clitest.New(t, "update", workspaceName,
|
||||||
|
"--build-option", fmt.Sprintf("%s=%s", ephemeralParameterName, ephemeralParameterValue))
|
||||||
|
clitest.SetupConfig(t, client, root)
|
||||||
|
|
||||||
|
doneChan := make(chan struct{})
|
||||||
|
pty := ptytest.New(t).Attach(inv)
|
||||||
|
go func() {
|
||||||
|
defer close(doneChan)
|
||||||
|
err := inv.Run()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
pty.ExpectMatch("Planning workspace")
|
||||||
|
<-doneChan
|
||||||
|
|
||||||
|
// Verify if build option is set
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
workspace, err := client.WorkspaceByOwnerAndName(ctx, user.UserID.String(), workspaceName, codersdk.WorkspaceOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, actualParameters, codersdk.WorkspaceBuildParameter{
|
||||||
|
Name: ephemeralParameterName,
|
||||||
|
Value: ephemeralParameterValue,
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdateValidateRichParameters(t *testing.T) {
|
func TestUpdateValidateRichParameters(t *testing.T) {
|
||||||
|
@ -545,14 +594,11 @@ func TestUpdateValidateRichParameters(t *testing.T) {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
matches := []string{
|
matches := []string{
|
||||||
"added_parameter", "",
|
"Planning workspace...", "",
|
||||||
`Enter a value (default: "foobar")`, "abc",
|
|
||||||
}
|
}
|
||||||
for i := 0; i < len(matches); i += 2 {
|
for i := 0; i < len(matches); i += 2 {
|
||||||
match := matches[i]
|
match := matches[i]
|
||||||
value := matches[i+1]
|
|
||||||
pty.ExpectMatch(match)
|
pty.ExpectMatch(match)
|
||||||
pty.WriteLine(value)
|
|
||||||
}
|
}
|
||||||
<-doneChan
|
<-doneChan
|
||||||
})
|
})
|
||||||
|
|
|
@ -20,6 +20,15 @@ coder create [flags] [name]
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
|
### --parameter
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| ----------- | ---------------------------------- |
|
||||||
|
| Type | <code>string-array</code> |
|
||||||
|
| Environment | <code>$CODER_RICH_PARAMETER</code> |
|
||||||
|
|
||||||
|
Rich parameter value in the format "name=value".
|
||||||
|
|
||||||
### --rich-parameter-file
|
### --rich-parameter-file
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|
|
|
@ -12,6 +12,15 @@ coder restart [flags] <workspace>
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
|
### --build-option
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| ----------- | -------------------------------- |
|
||||||
|
| Type | <code>string-array</code> |
|
||||||
|
| Environment | <code>$CODER_BUILD_OPTION</code> |
|
||||||
|
|
||||||
|
Build option value in the format "name=value".
|
||||||
|
|
||||||
### --build-options
|
### --build-options
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|
|
|
@ -12,6 +12,15 @@ coder start [flags] <workspace>
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
|
### --build-option
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| ----------- | -------------------------------- |
|
||||||
|
| Type | <code>string-array</code> |
|
||||||
|
| Environment | <code>$CODER_BUILD_OPTION</code> |
|
||||||
|
|
||||||
|
Build option value in the format "name=value".
|
||||||
|
|
||||||
### --build-options
|
### --build-options
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|
|
|
@ -26,6 +26,15 @@ Use --always-prompt to change the parameter values of the workspace.
|
||||||
|
|
||||||
Always prompt all parameters. Does not pull parameter values from existing workspace.
|
Always prompt all parameters. Does not pull parameter values from existing workspace.
|
||||||
|
|
||||||
|
### --build-option
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| ----------- | -------------------------------- |
|
||||||
|
| Type | <code>string-array</code> |
|
||||||
|
| Environment | <code>$CODER_BUILD_OPTION</code> |
|
||||||
|
|
||||||
|
Build option value in the format "name=value".
|
||||||
|
|
||||||
### --build-options
|
### --build-options
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|
@ -34,6 +43,15 @@ Always prompt all parameters. Does not pull parameter values from existing works
|
||||||
|
|
||||||
Prompt for one-time build options defined with ephemeral parameters.
|
Prompt for one-time build options defined with ephemeral parameters.
|
||||||
|
|
||||||
|
### --parameter
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| ----------- | ---------------------------------- |
|
||||||
|
| Type | <code>string-array</code> |
|
||||||
|
| Environment | <code>$CODER_RICH_PARAMETER</code> |
|
||||||
|
|
||||||
|
Rich parameter value in the format "name=value".
|
||||||
|
|
||||||
### --rich-parameter-file
|
### --rich-parameter-file
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|
|
Loading…
Reference in New Issue