mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat(cli): prompt for missing required template variables on template creation (#19973)
## Summary In this pull request we're adding support in the CLI for prompting the user for any missing required template variables in the `coder templates push` command and automatically retrying the template build once a user has provided any missing variable values. Closes: https://github.com/coder/coder/issues/19782 ### Demo In the following recording I created a simple template terraform file that used different variable types (string, number, boolean, and sensitive) and prompted the user to enter a value for each variable. <details> <summary>See example template terraform file</summary> ```tf ... # Required variables for testing interactive prompting variable "docker_image" { description = "Docker image to use for the workspace" type = string } variable "workspace_name" { description = "Name of the workspace" type = string } variable "cpu_limit" { description = "CPU limit for the container (number of cores)" type = number } variable "enable_gpu" { description = "Enable GPU access for the container" type = bool } variable "api_key" { description = "API key for external services (sensitive)" type = string sensitive = true } # Optional variable with default variable "docker_socket" { default = "/var/run/docker.sock" description = "Docker socket path" type = string } ... ``` </details> Once the user entered a valid value for each variable, the template build would be retried. https://github.com/user-attachments/assets/770cf954-3cbc-4464-925e-2be4e32a97de <details> <summary>See output from recording</summary> ```shell $ ./scripts/coder-dev.sh templates push test-required-params -d examples/templates/test-required-params/ INFO : Overriding codersdk.SessionTokenCookie as we are developing inside a Coder workspace. /home/coder/coder/build/coder-slim_2.26.0-devel+a68122ca3_linux_amd64 Provisioner tags: <none> WARN: No .terraform.lock.hcl file found | When provisioning, Coder will be unable to cache providers without a lockfile and must download them from the internet each time. | Create one by running terraform init in your template directory. > Upload "examples/templates/test-required-params"? (yes/no) yes === ✔ Queued [0ms] ==> ⧗ Running ==> ⧗ Running === ✔ Running [4ms] ==> ⧗ Setting up === ✔ Setting up [0ms] ==> ⧗ Parsing template parameters === ✔ Parsing template parameters [8ms] ==> ⧗ Cleaning Up === ✘ Cleaning Up [4ms] === ✘ Cleaning Up [8ms] Found 5 missing required variables: - docker_image (string): Docker image to use for the workspace - workspace_name (string): Name of the workspace - cpu_limit (number): CPU limit for the container (number of cores) - enable_gpu (bool): Enable GPU access for the container - api_key (string): API key for external services (sensitive) The template requires values for the following variables: var.docker_image (required) Description: Docker image to use for the workspace Type: string Current value: <empty> > Enter value: image-name var.workspace_name (required) Description: Name of the workspace Type: string Current value: <empty> > Enter value: workspace-name var.cpu_limit (required) Description: CPU limit for the container (number of cores) Type: number Current value: <empty> > Enter value: 1 var.enable_gpu (required) Description: Enable GPU access for the container Type: bool Current value: <empty> ? Select value: false var.api_key (required), sensitive Description: API key for external services (sensitive) Type: string Current value: <empty> > Enter value: (*redacted*) ****** Retrying template build with provided variables... === ✔ Queued [0ms] ==> ⧗ Running ==> ⧗ Running === ✔ Running [2ms] ==> ⧗ Setting up === ✔ Setting up [0ms] ==> ⧗ Parsing template parameters === ✔ Parsing template parameters [7ms] ==> ⧗ Detecting persistent resources 2025-09-25 22:34:14.731Z Terraform 1.13.0 2025-09-25 22:34:15.140Z data.coder_provisioner.me: Refreshing... 2025-09-25 22:34:15.140Z data.coder_workspace.me: Refreshing... 2025-09-25 22:34:15.140Z data.coder_workspace_owner.me: Refreshing... 2025-09-25 22:34:15.141Z data.coder_provisioner.me: Refresh complete after 0s [id=2bd73098-d127-4362-b3a5-628e5bce6998] 2025-09-25 22:34:15.141Z data.coder_workspace_owner.me: Refresh complete after 0s [id=c2006933-4f3e-4c45-9e04-79612c3a5eca] 2025-09-25 22:34:15.141Z data.coder_workspace.me: Refresh complete after 0s [id=36f2dc6f-0bf2-43bd-bc4d-b29768334e02] 2025-09-25 22:34:15.186Z coder_agent.main: Plan to create 2025-09-25 22:34:15.186Z module.code-server[0].coder_app.code-server: Plan to create 2025-09-25 22:34:15.186Z docker_volume.home_volume: Plan to create 2025-09-25 22:34:15.186Z module.code-server[0].coder_script.code-server: Plan to create 2025-09-25 22:34:15.187Z docker_container.workspace[0]: Plan to create 2025-09-25 22:34:15.187Z Plan: 5 to add, 0 to change, 0 to destroy. === ✔ Detecting persistent resources [3104ms] ==> ⧗ Detecting ephemeral resources 2025-09-25 22:34:16.033Z Terraform 1.13.0 2025-09-25 22:34:16.428Z data.coder_workspace.me: Refreshing... 2025-09-25 22:34:16.428Z data.coder_provisioner.me: Refreshing... 2025-09-25 22:34:16.429Z data.coder_workspace_owner.me: Refreshing... 2025-09-25 22:34:16.429Z data.coder_provisioner.me: Refresh complete after 0s [id=2d2f7083-88e6-425c-9df3-856a3bf4cc73] 2025-09-25 22:34:16.429Z data.coder_workspace.me: Refresh complete after 0s [id=c723575e-c7d3-43d7-bf54-0e34d0959dc3] 2025-09-25 22:34:16.431Z data.coder_workspace_owner.me: Refresh complete after 0s [id=d43470c2-236e-4ae9-a977-6b53688c2cb1] 2025-09-25 22:34:16.453Z coder_agent.main: Plan to create 2025-09-25 22:34:16.453Z docker_volume.home_volume: Plan to create 2025-09-25 22:34:16.454Z Plan: 2 to add, 0 to change, 0 to destroy. === ✔ Detecting ephemeral resources [1278ms] ==> ⧗ Cleaning Up === ✔ Cleaning Up [6ms] ┌──────────────────────────────────┐ │ Template Preview │ ├──────────────────────────────────┤ │ RESOURCE │ ├──────────────────────────────────┤ │ docker_container.workspace │ │ └─ main (linux, amd64) │ ├──────────────────────────────────┤ │ docker_volume.home_volume │ └──────────────────────────────────┘ The test-required-params template has been created at Sep 25 22:34:16! Developers can provision a workspace with this template using: Updated version at Sep 25 22:34:16! ``` </details> ### Changes - Added a new function to check if the provisioner failed due to a template missing required variables - Added a handler function that is called when a provisioner fails due to the "missing required variables" error. The handler function will: - Check for provided template variables and identify any missing variables - Prompt the user for any missing variables (prompt is adapted based on the variable type) - Validate user input for missing variables - Retry the template build when all variables have been provided by the user ### Testing Added tests for the following scenarios: - Ensure validation based on variable type - Ensure users are not prompted for variables with a default value - Ensure variables provided via a variables files (`--variables-file`) or a variable flag (`--variable`) take precedence over a template
This commit is contained in:
+158
-3
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -461,10 +462,14 @@ func createValidTemplateVersion(inv *serpent.Invocation, args createValidTemplat
|
||||
})
|
||||
if err != nil {
|
||||
var jobErr *cliui.ProvisionerJobError
|
||||
if errors.As(err, &jobErr) && !codersdk.JobIsMissingParameterErrorCode(jobErr.Code) {
|
||||
return nil, err
|
||||
if errors.As(err, &jobErr) {
|
||||
if codersdk.JobIsMissingRequiredTemplateVariableErrorCode(jobErr.Code) {
|
||||
return handleMissingTemplateVariables(inv, args, version.ID)
|
||||
}
|
||||
if !codersdk.JobIsMissingParameterErrorCode(jobErr.Code) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
version, err = client.TemplateVersion(inv.Context(), version.ID)
|
||||
@@ -528,3 +533,153 @@ func prettyDirectoryPath(dir string) string {
|
||||
}
|
||||
return prettyDir
|
||||
}
|
||||
|
||||
func handleMissingTemplateVariables(inv *serpent.Invocation, args createValidTemplateVersionArgs, failedVersionID uuid.UUID) (*codersdk.TemplateVersion, error) {
|
||||
client := args.Client
|
||||
|
||||
templateVariables, err := client.TemplateVersionVariables(inv.Context(), failedVersionID)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("fetch template variables: %w", err)
|
||||
}
|
||||
|
||||
existingValues := make(map[string]string)
|
||||
for _, v := range args.UserVariableValues {
|
||||
existingValues[v.Name] = v.Value
|
||||
}
|
||||
|
||||
var missingVariables []codersdk.TemplateVersionVariable
|
||||
for _, variable := range templateVariables {
|
||||
if !variable.Required {
|
||||
continue
|
||||
}
|
||||
|
||||
if existingValue, exists := existingValues[variable.Name]; exists && existingValue != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Only prompt for variables that don't have a default value or have a redacted default
|
||||
// Sensitive variables have a default value of "*redacted*"
|
||||
// See: https://github.com/coder/coder/blob/a78790c632974e04babfef6de0e2ddf044787a7a/coderd/provisionerdserver/provisionerdserver.go#L3206
|
||||
if variable.DefaultValue == "" || (variable.Sensitive && variable.DefaultValue == "*redacted*") {
|
||||
missingVariables = append(missingVariables, variable)
|
||||
}
|
||||
}
|
||||
|
||||
if len(missingVariables) == 0 {
|
||||
return nil, xerrors.New("no missing required variables found")
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(inv.Stderr, "Found %d missing required variables:\n", len(missingVariables))
|
||||
for _, v := range missingVariables {
|
||||
_, _ = fmt.Fprintf(inv.Stderr, " - %s (%s): %s\n", v.Name, v.Type, v.Description)
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(inv.Stderr, "\nThe template requires values for the following variables:")
|
||||
|
||||
var promptedValues []codersdk.VariableValue
|
||||
for _, variable := range missingVariables {
|
||||
value, err := promptForTemplateVariable(inv, variable)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("prompt for variable %q: %w", variable.Name, err)
|
||||
}
|
||||
promptedValues = append(promptedValues, codersdk.VariableValue{
|
||||
Name: variable.Name,
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
|
||||
combinedValues := codersdk.CombineVariableValues(args.UserVariableValues, promptedValues)
|
||||
|
||||
_, _ = fmt.Fprintln(inv.Stderr, "\nRetrying template build with provided variables...")
|
||||
|
||||
retryArgs := args
|
||||
retryArgs.UserVariableValues = combinedValues
|
||||
|
||||
return createValidTemplateVersion(inv, retryArgs)
|
||||
}
|
||||
|
||||
func promptForTemplateVariable(inv *serpent.Invocation, variable codersdk.TemplateVersionVariable) (string, error) {
|
||||
displayVariableInfo(inv, variable)
|
||||
|
||||
switch variable.Type {
|
||||
case "bool":
|
||||
return promptForBoolVariable(inv, variable)
|
||||
case "number":
|
||||
return promptForNumberVariable(inv, variable)
|
||||
default:
|
||||
return promptForStringVariable(inv, variable)
|
||||
}
|
||||
}
|
||||
|
||||
func displayVariableInfo(inv *serpent.Invocation, variable codersdk.TemplateVersionVariable) {
|
||||
_, _ = fmt.Fprintf(inv.Stderr, "var.%s", cliui.Bold(variable.Name))
|
||||
if variable.Required {
|
||||
_, _ = fmt.Fprint(inv.Stderr, pretty.Sprint(cliui.DefaultStyles.Error, " (required)"))
|
||||
}
|
||||
if variable.Sensitive {
|
||||
_, _ = fmt.Fprint(inv.Stderr, pretty.Sprint(cliui.DefaultStyles.Warn, ", sensitive"))
|
||||
}
|
||||
_, _ = fmt.Fprintln(inv.Stderr, "")
|
||||
|
||||
if variable.Description != "" {
|
||||
_, _ = fmt.Fprintf(inv.Stderr, " Description: %s\n", variable.Description)
|
||||
}
|
||||
_, _ = fmt.Fprintf(inv.Stderr, " Type: %s\n", variable.Type)
|
||||
_, _ = fmt.Fprintf(inv.Stderr, " Current value: %s\n", pretty.Sprint(cliui.DefaultStyles.Placeholder, "<empty>"))
|
||||
}
|
||||
|
||||
func promptForBoolVariable(inv *serpent.Invocation, variable codersdk.TemplateVersionVariable) (string, error) {
|
||||
defaultValue := variable.DefaultValue
|
||||
if defaultValue == "" {
|
||||
defaultValue = "false"
|
||||
}
|
||||
|
||||
return cliui.Select(inv, cliui.SelectOptions{
|
||||
Options: []string{"true", "false"},
|
||||
Default: defaultValue,
|
||||
Message: "Select value:",
|
||||
})
|
||||
}
|
||||
|
||||
func promptForNumberVariable(inv *serpent.Invocation, variable codersdk.TemplateVersionVariable) (string, error) {
|
||||
prompt := "Enter value:"
|
||||
if !variable.Required && variable.DefaultValue != "" {
|
||||
prompt = fmt.Sprintf("Enter value (default: %q):", variable.DefaultValue)
|
||||
}
|
||||
|
||||
return cliui.Prompt(inv, cliui.PromptOptions{
|
||||
Text: prompt,
|
||||
Default: variable.DefaultValue,
|
||||
Validate: createVariableValidator(variable),
|
||||
})
|
||||
}
|
||||
|
||||
func promptForStringVariable(inv *serpent.Invocation, variable codersdk.TemplateVersionVariable) (string, error) {
|
||||
prompt := "Enter value:"
|
||||
if !variable.Sensitive {
|
||||
if !variable.Required && variable.DefaultValue != "" {
|
||||
prompt = fmt.Sprintf("Enter value (default: %q):", variable.DefaultValue)
|
||||
}
|
||||
}
|
||||
|
||||
return cliui.Prompt(inv, cliui.PromptOptions{
|
||||
Text: prompt,
|
||||
Default: variable.DefaultValue,
|
||||
Secret: variable.Sensitive,
|
||||
Validate: createVariableValidator(variable),
|
||||
})
|
||||
}
|
||||
|
||||
func createVariableValidator(variable codersdk.TemplateVersionVariable) func(string) error {
|
||||
return func(s string) error {
|
||||
if variable.Required && s == "" && variable.DefaultValue == "" {
|
||||
return xerrors.New("value is required")
|
||||
}
|
||||
if variable.Type == "number" && s != "" {
|
||||
if _, err := strconv.ParseFloat(s, 64); err != nil {
|
||||
return xerrors.Errorf("must be a valid number, got: %q", s)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
+234
-48
@@ -852,54 +852,6 @@ func TestTemplatePush(t *testing.T) {
|
||||
require.Equal(t, "foobar", templateVariables[1].Value)
|
||||
})
|
||||
|
||||
t.Run("VariableIsRequiredButNotProvided", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
|
||||
templateVersion := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, createEchoResponsesWithTemplateVariables(initialTemplateVariables))
|
||||
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, templateVersion.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, templateVersion.ID)
|
||||
|
||||
// Test the cli command.
|
||||
//nolint:gocritic
|
||||
modifiedTemplateVariables := append(initialTemplateVariables,
|
||||
&proto.TemplateVariable{
|
||||
Name: "second_variable",
|
||||
Description: "This is the second variable.",
|
||||
Type: "string",
|
||||
Required: true,
|
||||
},
|
||||
)
|
||||
source := clitest.CreateTemplateVersionSource(t, createEchoResponsesWithTemplateVariables(modifiedTemplateVariables))
|
||||
inv, root := clitest.New(t, "templates", "push", template.Name, "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho), "--name", "example")
|
||||
clitest.SetupConfig(t, templateAdmin, root)
|
||||
pty := ptytest.New(t)
|
||||
inv.Stdin = pty.Input()
|
||||
inv.Stdout = pty.Output()
|
||||
|
||||
execDone := make(chan error)
|
||||
go func() {
|
||||
execDone <- inv.Run()
|
||||
}()
|
||||
|
||||
matches := []struct {
|
||||
match string
|
||||
write string
|
||||
}{
|
||||
{match: "Upload", write: "yes"},
|
||||
}
|
||||
for _, m := range matches {
|
||||
pty.ExpectMatch(m.match)
|
||||
pty.WriteLine(m.write)
|
||||
}
|
||||
|
||||
wantErr := <-execDone
|
||||
require.Error(t, wantErr)
|
||||
require.Contains(t, wantErr.Error(), "required template variables need values")
|
||||
})
|
||||
|
||||
t.Run("VariableIsOptionalButNotProvided", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
@@ -1115,6 +1067,240 @@ func TestTemplatePush(t *testing.T) {
|
||||
require.Len(t, templateVersions, 2)
|
||||
require.Equal(t, "example", templateVersions[1].Name)
|
||||
})
|
||||
|
||||
t.Run("PromptForDifferentRequiredTypes", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
|
||||
templateVariables := []*proto.TemplateVariable{
|
||||
{
|
||||
Name: "string_var",
|
||||
Description: "A string variable",
|
||||
Type: "string",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "number_var",
|
||||
Description: "A number variable",
|
||||
Type: "number",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "bool_var",
|
||||
Description: "A boolean variable",
|
||||
Type: "bool",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "sensitive_var",
|
||||
Description: "A sensitive variable",
|
||||
Type: "string",
|
||||
Required: true,
|
||||
Sensitive: true,
|
||||
},
|
||||
}
|
||||
|
||||
source := clitest.CreateTemplateVersionSource(t, createEchoResponsesWithTemplateVariables(templateVariables))
|
||||
inv, root := clitest.New(t, "templates", "push", "test-template", "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho))
|
||||
clitest.SetupConfig(t, templateAdmin, root)
|
||||
pty := ptytest.New(t).Attach(inv)
|
||||
|
||||
execDone := make(chan error)
|
||||
go func() {
|
||||
execDone <- inv.Run()
|
||||
}()
|
||||
|
||||
// Select "Yes" for the "Upload <template_path>" prompt
|
||||
pty.ExpectMatch("Upload")
|
||||
pty.WriteLine("yes")
|
||||
|
||||
pty.ExpectMatch("var.string_var")
|
||||
pty.ExpectMatch("Enter value:")
|
||||
pty.WriteLine("test-string")
|
||||
|
||||
pty.ExpectMatch("var.number_var")
|
||||
pty.ExpectMatch("Enter value:")
|
||||
pty.WriteLine("42")
|
||||
|
||||
// Boolean variable automatically selects the first option ("true")
|
||||
pty.ExpectMatch("var.bool_var")
|
||||
|
||||
pty.ExpectMatch("var.sensitive_var")
|
||||
pty.ExpectMatch("Enter value:")
|
||||
pty.WriteLine("secret-value")
|
||||
|
||||
require.NoError(t, <-execDone)
|
||||
})
|
||||
|
||||
t.Run("ValidateNumberInput", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
|
||||
templateVariables := []*proto.TemplateVariable{
|
||||
{
|
||||
Name: "number_var",
|
||||
Description: "A number that requires validation",
|
||||
Type: "number",
|
||||
Required: true,
|
||||
},
|
||||
}
|
||||
|
||||
source := clitest.CreateTemplateVersionSource(t, createEchoResponsesWithTemplateVariables(templateVariables))
|
||||
inv, root := clitest.New(t, "templates", "push", "test-template", "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho))
|
||||
clitest.SetupConfig(t, templateAdmin, root)
|
||||
pty := ptytest.New(t).Attach(inv)
|
||||
|
||||
execDone := make(chan error)
|
||||
go func() {
|
||||
execDone <- inv.Run()
|
||||
}()
|
||||
|
||||
// Select "Yes" for the "Upload <template_path>" prompt
|
||||
pty.ExpectMatch("Upload")
|
||||
pty.WriteLine("yes")
|
||||
|
||||
pty.ExpectMatch("var.number_var")
|
||||
|
||||
pty.WriteLine("not-a-number")
|
||||
pty.ExpectMatch("must be a valid number")
|
||||
|
||||
pty.WriteLine("123.45")
|
||||
|
||||
require.NoError(t, <-execDone)
|
||||
})
|
||||
|
||||
t.Run("DontPromptForDefaultValues", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
|
||||
templateVariables := []*proto.TemplateVariable{
|
||||
{
|
||||
Name: "with_default",
|
||||
Type: "string",
|
||||
Required: true,
|
||||
DefaultValue: "default-value",
|
||||
},
|
||||
{
|
||||
Name: "without_default",
|
||||
Type: "string",
|
||||
Required: true,
|
||||
},
|
||||
}
|
||||
|
||||
source := clitest.CreateTemplateVersionSource(t, createEchoResponsesWithTemplateVariables(templateVariables))
|
||||
inv, root := clitest.New(t, "templates", "push", "test-template", "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho))
|
||||
clitest.SetupConfig(t, templateAdmin, root)
|
||||
pty := ptytest.New(t).Attach(inv)
|
||||
|
||||
execDone := make(chan error)
|
||||
go func() {
|
||||
execDone <- inv.Run()
|
||||
}()
|
||||
|
||||
// Select "Yes" for the "Upload <template_path>" prompt
|
||||
pty.ExpectMatch("Upload")
|
||||
pty.WriteLine("yes")
|
||||
|
||||
pty.ExpectMatch("var.without_default")
|
||||
pty.WriteLine("test-value")
|
||||
|
||||
require.NoError(t, <-execDone)
|
||||
})
|
||||
|
||||
t.Run("VariableSourcesPriority", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
|
||||
templateVariables := []*proto.TemplateVariable{
|
||||
{
|
||||
Name: "cli_flag_var",
|
||||
Description: "Variable provided via CLI flag",
|
||||
Type: "string",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "file_var",
|
||||
Description: "Variable provided via file",
|
||||
Type: "string",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "prompt_var",
|
||||
Description: "Variable provided via prompt",
|
||||
Type: "string",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "cli_overrides_file_var",
|
||||
Description: "Variable in both CLI and file",
|
||||
Type: "string",
|
||||
Required: true,
|
||||
},
|
||||
}
|
||||
|
||||
source := clitest.CreateTemplateVersionSource(t, createEchoResponsesWithTemplateVariables(templateVariables))
|
||||
|
||||
// Create a temporary variables file.
|
||||
tempDir := t.TempDir()
|
||||
removeTmpDirUntilSuccessAfterTest(t, tempDir)
|
||||
variablesFile, err := os.CreateTemp(tempDir, "variables*.yaml")
|
||||
require.NoError(t, err)
|
||||
_, err = variablesFile.WriteString(`file_var: from-file
|
||||
cli_overrides_file_var: from-file`)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, variablesFile.Close())
|
||||
|
||||
inv, root := clitest.New(t, "templates", "push", "test-template",
|
||||
"--directory", source,
|
||||
"--test.provisioner", string(database.ProvisionerTypeEcho),
|
||||
"--variables-file", variablesFile.Name(),
|
||||
"--variable", "cli_flag_var=from-cli-flag",
|
||||
"--variable", "cli_overrides_file_var=from-cli-override",
|
||||
)
|
||||
clitest.SetupConfig(t, templateAdmin, root)
|
||||
pty := ptytest.New(t).Attach(inv)
|
||||
|
||||
execDone := make(chan error)
|
||||
go func() {
|
||||
execDone <- inv.Run()
|
||||
}()
|
||||
|
||||
// Select "Yes" for the "Upload <template_path>" prompt
|
||||
pty.ExpectMatch("Upload")
|
||||
pty.WriteLine("yes")
|
||||
|
||||
// Only check for prompt_var, other variables should not prompt
|
||||
pty.ExpectMatch("var.prompt_var")
|
||||
pty.ExpectMatch("Enter value:")
|
||||
pty.WriteLine("from-prompt")
|
||||
|
||||
require.NoError(t, <-execDone)
|
||||
|
||||
template, err := client.TemplateByName(context.Background(), owner.OrganizationID, "test-template")
|
||||
require.NoError(t, err)
|
||||
|
||||
templateVersionVars, err := client.TemplateVersionVariables(context.Background(), template.ActiveVersionID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, templateVersionVars, 4)
|
||||
|
||||
varMap := make(map[string]string)
|
||||
for _, tv := range templateVersionVars {
|
||||
varMap[tv.Name] = tv.Value
|
||||
}
|
||||
|
||||
require.Equal(t, "from-cli-flag", varMap["cli_flag_var"])
|
||||
require.Equal(t, "from-file", varMap["file_var"])
|
||||
require.Equal(t, "from-prompt", varMap["prompt_var"])
|
||||
require.Equal(t, "from-cli-override", varMap["cli_overrides_file_var"])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -175,6 +175,12 @@ func JobIsMissingParameterErrorCode(code JobErrorCode) bool {
|
||||
return string(code) == runner.MissingParameterErrorCode
|
||||
}
|
||||
|
||||
// JobIsMissingRequiredTemplateVariableErrorCode returns whether the error is a missing a required template
|
||||
// variable error. This can indicate to consumers that they need to provide required template variables.
|
||||
func JobIsMissingRequiredTemplateVariableErrorCode(code JobErrorCode) bool {
|
||||
return string(code) == runner.RequiredTemplateVariablesErrorCode
|
||||
}
|
||||
|
||||
// ProvisionerJob describes the job executed by the provisioning daemon.
|
||||
type ProvisionerJob struct {
|
||||
ID uuid.UUID `json:"id" format:"uuid" table:"id"`
|
||||
|
||||
@@ -68,7 +68,7 @@ func ParseUserVariableValues(varsFiles []string, variablesFile string, commandLi
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return combineVariableValues(fromVars, fromFile, fromCommandLine), nil
|
||||
return CombineVariableValues(fromVars, fromFile, fromCommandLine), nil
|
||||
}
|
||||
|
||||
func parseVariableValuesFromVarsFiles(varsFiles []string) ([]VariableValue, error) {
|
||||
@@ -252,7 +252,7 @@ func parseVariableValuesFromCommandLine(variables []string) ([]VariableValue, er
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func combineVariableValues(valuesSets ...[]VariableValue) []VariableValue {
|
||||
func CombineVariableValues(valuesSets ...[]VariableValue) []VariableValue {
|
||||
combinedValues := make(map[string]string)
|
||||
|
||||
for _, values := range valuesSets {
|
||||
|
||||
Reference in New Issue
Block a user