Files
coder/cli/templatepush_test.go
T
Rafael Rodriguez bb5884467d 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
2025-10-03 10:20:06 -05:00

1382 lines
46 KiB
Go

package cli_test
import (
"bytes"
"context"
"database/sql"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisioner/echo"
"github.com/coder/coder/v2/provisioner/terraform/tfparse"
"github.com/coder/coder/v2/provisionersdk"
"github.com/coder/coder/v2/provisionersdk/proto"
"github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/coder/v2/testutil"
)
func TestTemplatePush(t *testing.T) {
t.Parallel()
t.Run("OK", 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())
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
// Test the cli command.
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
})
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).Attach(inv)
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)
}
require.NoError(t, <-execDone)
// Assert that the template version changed.
templateVersions, err := client.TemplateVersionsByTemplate(context.Background(), codersdk.TemplateVersionsByTemplateRequest{
TemplateID: template.ID,
})
require.NoError(t, err)
assert.Len(t, templateVersions, 2)
assert.NotEqual(t, template.ActiveVersionID, templateVersions[1].ID)
require.Equal(t, "example", templateVersions[1].Name)
})
t.Run("Message less than or equal to 72 chars", 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())
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
})
wantMessage := strings.Repeat("a", 72)
inv, root := clitest.New(t, "templates", "push", template.Name, "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho), "--name", "example", "--message", wantMessage, "--yes")
clitest.SetupConfig(t, templateAdmin, root)
pty := ptytest.New(t).Attach(inv)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
defer cancel()
inv = inv.WithContext(ctx)
w := clitest.StartWithWaiter(t, inv)
pty.ExpectNoMatchBefore(ctx, "Template message is longer than 72 characters", "Updated version at")
w.RequireSuccess()
// Assert that the template version changed.
templateVersions, err := client.TemplateVersionsByTemplate(ctx, codersdk.TemplateVersionsByTemplateRequest{
TemplateID: template.ID,
})
require.NoError(t, err)
assert.Len(t, templateVersions, 2)
assert.NotEqual(t, template.ActiveVersionID, templateVersions[1].ID)
require.Equal(t, wantMessage, templateVersions[1].Message)
})
t.Run("Message too long, warn but continue", 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())
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
})
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
for i, tt := range []struct {
wantMessage string
wantMatch string
}{
{wantMessage: strings.Repeat("a", 73), wantMatch: "Template message is longer than 72 characters"},
{wantMessage: "This is my title\n\nAnd this is my body.", wantMatch: "Template message contains newlines"},
} {
inv, root := clitest.New(t, "templates", "push", template.Name,
"--directory", source,
"--test.provisioner", string(database.ProvisionerTypeEcho),
"--message", tt.wantMessage,
"--yes",
)
clitest.SetupConfig(t, templateAdmin, root)
pty := ptytest.New(t).Attach(inv)
inv = inv.WithContext(ctx)
w := clitest.StartWithWaiter(t, inv)
pty.ExpectMatchContext(ctx, tt.wantMatch)
w.RequireSuccess()
// Assert that the template version changed.
templateVersions, err := client.TemplateVersionsByTemplate(ctx, codersdk.TemplateVersionsByTemplateRequest{
TemplateID: template.ID,
})
require.NoError(t, err)
assert.Len(t, templateVersions, 2+i)
assert.NotEqual(t, template.ActiveVersionID, templateVersions[1+i].ID)
require.Equal(t, tt.wantMessage, templateVersions[1+i].Message)
}
})
t.Run("NoLockfile", 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())
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
// Test the cli command.
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
})
require.NoError(t, os.Remove(filepath.Join(source, ".terraform.lock.hcl")))
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).Attach(inv)
execDone := make(chan error)
go func() {
execDone <- inv.Run()
}()
matches := []struct {
match string
write string
}{
{match: "No .terraform.lock.hcl file found"},
{match: "Upload", write: "no"},
}
for _, m := range matches {
pty.ExpectMatch(m.match)
if m.write != "" {
pty.WriteLine(m.write)
}
}
// cmd should error once we say no.
require.Error(t, <-execDone)
})
t.Run("NoLockfileIgnored", 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())
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
// Test the cli command.
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
})
require.NoError(t, os.Remove(filepath.Join(source, ".terraform.lock.hcl")))
inv, root := clitest.New(t, "templates", "push", template.Name,
"--directory", source,
"--test.provisioner", string(database.ProvisionerTypeEcho),
"--name", "example",
"--ignore-lockfile",
)
clitest.SetupConfig(t, templateAdmin, root)
pty := ptytest.New(t).Attach(inv)
execDone := make(chan error)
go func() {
execDone <- inv.Run()
}()
{
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
defer cancel()
pty.ExpectNoMatchBefore(ctx, "No .terraform.lock.hcl file found", "Upload")
pty.WriteLine("no")
}
// cmd should error once we say no.
require.Error(t, <-execDone)
})
t.Run("PushInactiveTemplateVersion", 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())
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
// Test the cli command.
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
})
inv, root := clitest.New(t, "templates", "push", template.Name,
"--activate=false",
"--directory", source,
"--test.provisioner", string(database.ProvisionerTypeEcho),
"--name", "example",
)
clitest.SetupConfig(t, templateAdmin, root)
pty := ptytest.New(t).Attach(inv)
w := clitest.StartWithWaiter(t, inv)
matches := []struct {
match string
write string
}{
{match: "Upload", write: "yes"},
}
for _, m := range matches {
pty.ExpectMatch(m.match)
pty.WriteLine(m.write)
}
w.RequireSuccess()
// Assert that the template version didn't change.
templateVersions, err := client.TemplateVersionsByTemplate(context.Background(), codersdk.TemplateVersionsByTemplateRequest{
TemplateID: template.ID,
})
require.NoError(t, err)
assert.Len(t, templateVersions, 2)
assert.Equal(t, template.ActiveVersionID, templateVersions[0].ID)
require.NotEqual(t, "example", templateVersions[0].Name)
})
t.Run("UseWorkingDir", func(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip(`On Windows this test flakes with: "The process cannot access the file because it is being used by another process"`)
}
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin())
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
// Test the cli command.
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
})
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID,
func(r *codersdk.CreateTemplateRequest) {
r.Name = filepath.Base(source)
})
// Don't pass the name of the template, it should use the
// directory of the source.
inv, root := clitest.New(t, "templates", "push",
"--test.provisioner", string(database.ProvisionerTypeEcho),
"--test.workdir", source,
"--force-tty",
)
clitest.SetupConfig(t, templateAdmin, root)
pty := ptytest.New(t).Attach(inv)
waiter := clitest.StartWithWaiter(t, inv)
matches := []struct {
match string
write string
}{
{match: "Upload", write: "yes"},
}
for _, m := range matches {
pty.ExpectMatch(m.match)
pty.WriteLine(m.write)
}
waiter.RequireSuccess()
// Assert that the template version changed.
templateVersions, err := client.TemplateVersionsByTemplate(context.Background(), codersdk.TemplateVersionsByTemplateRequest{
TemplateID: template.ID,
})
require.NoError(t, err)
assert.Len(t, templateVersions, 2)
assert.NotEqual(t, template.ActiveVersionID, templateVersions[1].ID)
})
t.Run("Stdin", 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())
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
source, err := echo.Tar(&echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
})
require.NoError(t, err)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
inv, root := clitest.New(
t, "templates", "push", "--directory", "-",
"--test.provisioner", string(database.ProvisionerTypeEcho),
template.Name,
)
clitest.SetupConfig(t, templateAdmin, root)
pty := ptytest.New(t)
inv.Stdin = bytes.NewReader(source)
inv.Stdout = pty.Output()
execDone := make(chan error)
go func() {
execDone <- inv.Run()
}()
require.NoError(t, <-execDone)
// Assert that the template version changed.
templateVersions, err := client.TemplateVersionsByTemplate(context.Background(), codersdk.TemplateVersionsByTemplateRequest{
TemplateID: template.ID,
})
require.NoError(t, err)
assert.Len(t, templateVersions, 2)
assert.NotEqual(t, template.ActiveVersionID, templateVersions[1].ID)
})
t.Run("ProvisionerTags", func(t *testing.T) {
t.Parallel()
t.Run("WorkspaceTagsTerraform", func(t *testing.T) {
t.Parallel()
tests := []struct {
name string
setupDaemon func(ctx context.Context, store database.Store, owner codersdk.CreateFirstUserResponse, tags database.StringMap, now time.Time) error
expectOutput string
}{
{
name: "no provisioners available",
setupDaemon: func(_ context.Context, _ database.Store, _ codersdk.CreateFirstUserResponse, _ database.StringMap, _ time.Time) error {
return nil
},
expectOutput: "there are no provisioners that accept the required tags",
},
{
name: "provisioner stale",
setupDaemon: func(ctx context.Context, store database.Store, owner codersdk.CreateFirstUserResponse, tags database.StringMap, now time.Time) error {
pk, err := store.InsertProvisionerKey(ctx, database.InsertProvisionerKeyParams{
ID: uuid.New(),
CreatedAt: now,
OrganizationID: owner.OrganizationID,
Name: "test",
Tags: tags,
HashedSecret: []byte("secret"),
})
if err != nil {
return err
}
oneHourAgo := now.Add(-time.Hour)
_, err = store.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{
Provisioners: []database.ProvisionerType{database.ProvisionerTypeTerraform},
LastSeenAt: sql.NullTime{Time: oneHourAgo, Valid: true},
CreatedAt: oneHourAgo,
Name: "test",
Tags: tags,
OrganizationID: owner.OrganizationID,
KeyID: pk.ID,
})
return err
},
expectOutput: "Provisioners that accept the required tags have not responded for longer than expected",
},
{
name: "active provisioner",
setupDaemon: func(ctx context.Context, store database.Store, owner codersdk.CreateFirstUserResponse, tags database.StringMap, now time.Time) error {
pk, err := store.InsertProvisionerKey(ctx, database.InsertProvisionerKeyParams{
ID: uuid.New(),
CreatedAt: now,
OrganizationID: owner.OrganizationID,
Name: "test",
Tags: tags,
HashedSecret: []byte("secret"),
})
if err != nil {
return err
}
_, err = store.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{
Provisioners: []database.ProvisionerType{database.ProvisionerTypeTerraform},
LastSeenAt: sql.NullTime{Time: now, Valid: true},
CreatedAt: now,
Name: "test-active",
Tags: tags,
OrganizationID: owner.OrganizationID,
KeyID: pk.ID,
})
return err
},
expectOutput: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Start an instance **without** a built-in provisioner.
// We're not actually testing that the Terraform applies.
// What we test is that a provisioner job is created with the expected
// tags based on the __content__ of the Terraform.
store, ps := dbtestutil.NewDB(t)
client := coderdtest.New(t, &coderdtest.Options{
Database: store,
Pubsub: ps,
})
owner := coderdtest.CreateFirstUser(t, client)
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin())
// Create a tar file with some pre-defined content
tarFile := testutil.CreateTar(t, map[string]string{
"main.tf": `
variable "a" {
type = string
default = "1"
}
data "coder_parameter" "b" {
name = "b"
type = string
default = "2"
}
resource "null_resource" "test" {}
data "coder_workspace_tags" "tags" {
tags = {
"a": var.a,
"b": data.coder_parameter.b.value,
"test_name": "` + tt.name + `"
}
}`,
})
// Write the tar file to disk.
tempDir := t.TempDir()
err := tfparse.WriteArchive(tarFile, "application/x-tar", tempDir)
require.NoError(t, err)
wantTags := database.StringMap(provisionersdk.MutateTags(uuid.Nil, map[string]string{
"a": "1",
"b": "2",
"test_name": tt.name,
}))
templateName := testutil.GetRandomNameHyphenated(t)
inv, root := clitest.New(t, "templates", "push", templateName, "-d", tempDir, "--yes")
clitest.SetupConfig(t, templateAdmin, root)
pty := ptytest.New(t).Attach(inv)
ctx := testutil.Context(t, testutil.WaitShort)
now := dbtime.Now()
require.NoError(t, tt.setupDaemon(ctx, store, owner, wantTags, now))
cancelCtx, cancel := context.WithCancel(ctx)
t.Cleanup(cancel)
done := make(chan error)
go func() {
done <- inv.WithContext(cancelCtx).Run()
}()
require.Eventually(t, func() bool {
jobs, err := store.GetProvisionerJobsCreatedAfter(ctx, time.Time{})
if !assert.NoError(t, err) {
return false
}
if len(jobs) == 0 {
return false
}
return assert.EqualValues(t, wantTags, jobs[0].Tags)
}, testutil.WaitShort, testutil.IntervalFast)
if tt.expectOutput != "" {
pty.ExpectMatch(tt.expectOutput)
}
cancel()
<-done
})
}
})
t.Run("ChangeTags", func(t *testing.T) {
t.Parallel()
// Start the first provisioner
client, provisionerDocker, api := coderdtest.NewWithAPI(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
ProvisionerDaemonTags: map[string]string{
"docker": "true",
},
})
defer provisionerDocker.Close()
// Start the second provisioner
provisionerFoobar := coderdtest.NewTaggedProvisionerDaemon(t, api, "provisioner-foobar", map[string]string{
"foobar": "foobaz",
})
defer provisionerFoobar.Close()
owner := coderdtest.CreateFirstUser(t, client)
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin())
// Create the template with initial tagged template version.
templateVersion := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil, func(ctvr *codersdk.CreateTemplateVersionRequest) {
ctvr.ProvisionerTags = map[string]string{
"docker": "true",
}
})
templateVersion = coderdtest.AwaitTemplateVersionJobCompleted(t, client, templateVersion.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, templateVersion.ID)
// Push new template version with different provisioner tags.
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
})
inv, root := clitest.New(t, "templates", "push", template.Name, "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho), "--name", template.Name,
"--provisioner-tag", "foobar=foobaz")
clitest.SetupConfig(t, templateAdmin, root)
pty := ptytest.New(t).Attach(inv)
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)
}
require.NoError(t, <-execDone)
// Verify template version tags
template, err := client.Template(context.Background(), template.ID)
require.NoError(t, err)
templateVersion, err = client.TemplateVersion(context.Background(), template.ActiveVersionID)
require.NoError(t, err)
require.EqualValues(t, map[string]string{"foobar": "foobaz", "owner": "", "scope": "organization"}, templateVersion.Job.Tags)
})
t.Run("DeleteTags", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
// Start the first provisioner with no tags.
client, provisionerDocker, api := coderdtest.NewWithAPI(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
ProvisionerDaemonTags: map[string]string{},
})
defer provisionerDocker.Close()
// Start the second provisioner with a tag set.
provisionerFoobar := coderdtest.NewTaggedProvisionerDaemon(t, api, "provisioner-foobar", map[string]string{
"foobar": "foobaz",
})
defer provisionerFoobar.Close()
owner := coderdtest.CreateFirstUser(t, client)
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin())
// Create the template with initial tagged template version.
templateVersion := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil, func(ctvr *codersdk.CreateTemplateVersionRequest) {
ctvr.ProvisionerTags = map[string]string{
"foobar": "foobaz",
}
})
templateVersion = coderdtest.AwaitTemplateVersionJobCompleted(t, client, templateVersion.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, templateVersion.ID)
// Stop the tagged provisioner daemon.
provisionerFoobar.Close()
// Push new template version with no provisioner tags.
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
})
inv, root := clitest.New(t, "templates", "push", template.Name, "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho), "--name", template.Name, "--provisioner-tag=\"-\"")
clitest.SetupConfig(t, templateAdmin, root)
pty := ptytest.New(t).Attach(inv)
execDone := make(chan error)
go func() {
execDone <- inv.WithContext(ctx).Run()
}()
matches := []struct {
match string
write string
}{
{match: "Upload", write: "yes"},
}
for _, m := range matches {
pty.ExpectMatch(m.match)
pty.WriteLine(m.write)
}
require.NoError(t, <-execDone)
// Verify template version tags
template, err := client.Template(ctx, template.ID)
require.NoError(t, err)
templateVersion, err = client.TemplateVersion(ctx, template.ActiveVersionID)
require.NoError(t, err)
require.EqualValues(t, map[string]string{"owner": "", "scope": "organization"}, templateVersion.Job.Tags)
})
t.Run("DoNotChangeTags", func(t *testing.T) {
t.Parallel()
// Start the tagged provisioner
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
ProvisionerDaemonTags: map[string]string{
"docker": "true",
},
})
owner := coderdtest.CreateFirstUser(t, client)
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin())
// Create the template with initial tagged template version.
templateVersion := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil, func(ctvr *codersdk.CreateTemplateVersionRequest) {
ctvr.ProvisionerTags = map[string]string{
"docker": "true",
}
})
templateVersion = coderdtest.AwaitTemplateVersionJobCompleted(t, client, templateVersion.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, templateVersion.ID)
// Push new template version without provisioner tags. CLI should reuse tags from the previous version.
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
})
inv, root := clitest.New(t, "templates", "push", template.Name, "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho), "--name", template.Name)
clitest.SetupConfig(t, templateAdmin, root)
pty := ptytest.New(t).Attach(inv)
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)
}
require.NoError(t, <-execDone)
// Verify template version tags
template, err := client.Template(context.Background(), template.ID)
require.NoError(t, err)
templateVersion, err = client.TemplateVersion(context.Background(), template.ActiveVersionID)
require.NoError(t, err)
require.EqualValues(t, map[string]string{"docker": "true", "owner": "", "scope": "organization"}, templateVersion.Job.Tags)
})
})
t.Run("Variables", func(t *testing.T) {
t.Parallel()
initialTemplateVariables := []*proto.TemplateVariable{
{
Name: "first_variable",
Description: "This is the first variable",
Type: "string",
DefaultValue: "abc",
Required: false,
Sensitive: true,
},
}
t.Run("VariableIsRequired", 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))
tempDir := t.TempDir()
removeTmpDirUntilSuccessAfterTest(t, tempDir)
variablesFile, _ := os.CreateTemp(tempDir, "variables*.yaml")
_, _ = variablesFile.WriteString(`second_variable: foobar`)
inv, root := clitest.New(t, "templates", "push", template.Name,
"--directory", source,
"--test.provisioner", string(database.ProvisionerTypeEcho),
"--name", "example",
"--variables-file", variablesFile.Name(),
)
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)
}
require.NoError(t, <-execDone)
// Assert that the template version changed.
templateVersions, err := client.TemplateVersionsByTemplate(context.Background(), codersdk.TemplateVersionsByTemplateRequest{
TemplateID: template.ID,
})
require.NoError(t, err)
assert.Len(t, templateVersions, 2)
assert.NotEqual(t, template.ActiveVersionID, templateVersions[1].ID)
require.Equal(t, "example", templateVersions[1].Name)
templateVariables, err := client.TemplateVersionVariables(context.Background(), templateVersions[1].ID)
require.NoError(t, err)
assert.Len(t, templateVariables, 2)
require.Equal(t, "second_variable", templateVariables[1].Name)
require.Equal(t, "foobar", templateVariables[1].Value)
})
t.Run("VariableIsOptionalButNotProvided", 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",
DefaultValue: "abc",
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)
}
require.NoError(t, <-execDone)
// Assert that the template version changed.
templateVersions, err := client.TemplateVersionsByTemplate(context.Background(), codersdk.TemplateVersionsByTemplateRequest{
TemplateID: template.ID,
})
require.NoError(t, err)
assert.Len(t, templateVersions, 2)
assert.NotEqual(t, template.ActiveVersionID, templateVersions[1].ID)
require.Equal(t, "example", templateVersions[1].Name)
templateVariables, err := client.TemplateVersionVariables(context.Background(), templateVersions[1].ID)
require.NoError(t, err)
assert.Len(t, templateVariables, 2)
require.Equal(t, "second_variable", templateVariables[1].Name)
require.Equal(t, "abc", templateVariables[1].Value)
require.Equal(t, templateVariables[1].DefaultValue, templateVariables[1].Value)
})
t.Run("WithVariableOption", 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",
"--variable", "second_variable=foobar",
)
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)
}
require.NoError(t, <-execDone)
// Assert that the template version changed.
templateVersions, err := client.TemplateVersionsByTemplate(context.Background(), codersdk.TemplateVersionsByTemplateRequest{
TemplateID: template.ID,
})
require.NoError(t, err)
assert.Len(t, templateVersions, 2)
assert.NotEqual(t, template.ActiveVersionID, templateVersions[1].ID)
require.Equal(t, "example", templateVersions[1].Name)
templateVariables, err := client.TemplateVersionVariables(context.Background(), templateVersions[1].ID)
require.NoError(t, err)
assert.Len(t, templateVariables, 2)
require.Equal(t, "second_variable", templateVariables[1].Name)
require.Equal(t, "foobar", templateVariables[1].Value)
})
t.Run("CreateTemplate", 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())
source := clitest.CreateTemplateVersionSource(t, completeWithAgent())
const templateName = "my-template"
args := []string{
"templates",
"push",
templateName,
"--directory", source,
"--test.provisioner", string(database.ProvisionerTypeEcho),
}
inv, root := clitest.New(t, args...)
clitest.SetupConfig(t, templateAdmin, root)
pty := ptytest.New(t).Attach(inv)
waiter := clitest.StartWithWaiter(t, inv)
matches := []struct {
match string
write string
}{
{match: "Upload", write: "yes"},
{match: "template has been created"},
}
for _, m := range matches {
pty.ExpectMatch(m.match)
if m.write != "" {
pty.WriteLine(m.write)
}
}
waiter.RequireSuccess()
template, err := client.TemplateByName(context.Background(), owner.OrganizationID, templateName)
require.NoError(t, err)
require.Equal(t, templateName, template.Name)
require.NotEqual(t, uuid.Nil, template.ActiveVersionID)
})
t.Run("NoStdinWithCurrentDirectory", 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())
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
})
inv, root := clitest.New(t, "templates", "push", template.Name,
"--directory", ".",
"--test.provisioner", string(database.ProvisionerTypeEcho),
"--test.workdir", source,
"--name", "example",
"--yes")
clitest.SetupConfig(t, templateAdmin, root)
inv.Stdin = strings.NewReader("invalid tar content that would cause failure")
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
defer cancel()
err := inv.WithContext(ctx).Run()
require.NoError(t, err, "Should succeed without reading from stdin")
templateVersions, err := client.TemplateVersionsByTemplate(ctx, codersdk.TemplateVersionsByTemplateRequest{
TemplateID: template.ID,
})
require.NoError(t, err)
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"])
})
})
}
func createEchoResponsesWithTemplateVariables(templateVariables []*proto.TemplateVariable) *echo.Responses {
return &echo.Responses{
Parse: []*proto.Response{
{
Type: &proto.Response_Parse{
Parse: &proto.ParseComplete{
TemplateVariables: templateVariables,
},
},
},
},
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ApplyComplete,
}
}
func completeWithAgent() *echo.Responses {
return &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Resources: []*proto.Resource{
{
Type: "compute",
Name: "main",
Agents: []*proto.Agent{
{
Name: "smith",
OperatingSystem: "linux",
Architecture: "i386",
},
},
},
},
},
},
},
},
ProvisionApply: []*proto.Response{
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{
{
Type: "compute",
Name: "main",
Agents: []*proto.Agent{
{
Name: "smith",
OperatingSystem: "linux",
Architecture: "i386",
},
},
},
},
},
},
},
},
}
}
// Need this for Windows because of a known issue with Go:
// https://github.com/golang/go/issues/52986
func removeTmpDirUntilSuccessAfterTest(t *testing.T, tempDir string) {
t.Helper()
t.Cleanup(func() {
err := os.RemoveAll(tempDir)
for err != nil {
err = os.RemoveAll(tempDir)
}
})
}