Files
coder/provisioner/terraform/tfparse/tfparse_test.go
T
Spike Curtis bddb808b25 chore: arrange imports in a standard way (#21452)
Fixes all our Go file imports to match the preferred spec that we've _mostly_ been using. For example:

```
import (
	"context"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"golang.org/x/xerrors"
	"gopkg.in/natefinch/lumberjack.v2"

	"cdr.dev/slog/v3"
	"github.com/coder/coder/v2/codersdk/agentsdk"
	"github.com/coder/serpent"
)
```

3 groups: standard library, 3rd partly libs, Coder libs.

This PR makes the change across the codebase. The PR in the stack above modifies our formatting to maintain this state of affairs, and is a separate PR so it's possible to review that one in detail.
2026-01-08 15:24:11 +04:00

697 lines
18 KiB
Go

package tfparse_test
import (
"context"
"io"
"log"
"testing"
"github.com/stretchr/testify/require"
"cdr.dev/slog/v3"
"cdr.dev/slog/v3/sloggers/sloghuman"
"github.com/coder/coder/v2/provisioner/terraform/tfparse"
"github.com/coder/coder/v2/testutil"
)
func Test_WorkspaceTagDefaultsFromFile(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
files map[string]string
expectTags map[string]string
expectError string
}{
{
name: "empty",
files: map[string]string{},
expectTags: map[string]string{},
expectError: "",
},
{
name: "single text file",
files: map[string]string{
"file.txt": `
hello world`,
},
expectTags: map[string]string{},
expectError: "",
},
{
name: "main.tf with no workspace_tags",
files: map[string]string{
"main.tf": `
provider "foo" {}
resource "foo_bar" "baz" {}
variable "region" {
type = string
default = "us"
}
data "coder_parameter" "unrelated" {
name = "unrelated"
type = "list(string)"
default = jsonencode(["a", "b"])
}
data "coder_parameter" "az" {
name = "az"
type = "string"
default = "a"
}`,
},
expectTags: map[string]string{},
expectError: "",
},
{
name: "main.tf with empty workspace tags",
files: map[string]string{
"main.tf": `
provider "foo" {}
resource "foo_bar" "baz" {}
variable "region" {
type = string
default = "us"
}
data "coder_parameter" "unrelated" {
name = "unrelated"
type = "list(string)"
default = jsonencode(["a", "b"])
}
data "coder_parameter" "az" {
name = "az"
type = "string"
default = "a"
}
data "coder_workspace_tags" "tags" {}`,
},
expectTags: map[string]string{},
expectError: `"tags" attribute is required by coder_workspace_tags`,
},
{
name: "main.tf with valid workspace tags",
files: map[string]string{
"main.tf": `
provider "foo" {}
resource "foo_bar" "baz" {}
variable "region" {
type = string
default = "us"
}
variable "unrelated" {
type = bool
}
data "coder_parameter" "unrelated" {
name = "unrelated"
type = "list(string)"
default = jsonencode(["a", "b"])
}
data "coder_parameter" "az" {
name = "az"
type = "string"
default = "a"
}
data "coder_workspace_tags" "tags" {
tags = {
"platform" = "kubernetes",
"cluster" = "${"devel"}${"opers"}"
"region" = var.region
"az" = data.coder_parameter.az.value
}
}`,
},
expectTags: map[string]string{"platform": "kubernetes", "cluster": "developers", "region": "us", "az": "a"},
expectError: "",
},
{
name: "main.tf with parameter that has default value from dynamic value",
files: map[string]string{
"main.tf": `
provider "foo" {}
resource "foo_bar" "baz" {}
variable "region" {
type = string
default = "us"
}
variable "az" {
type = string
default = "${""}${"a"}"
}
data "coder_parameter" "unrelated" {
name = "unrelated"
type = "list(string)"
default = jsonencode(["a", "b"])
}
data "coder_parameter" "az" {
name = "az"
type = "string"
default = var.az
}
data "coder_workspace_tags" "tags" {
tags = {
"platform" = "kubernetes",
"cluster" = "${"devel"}${"opers"}"
"region" = var.region
"az" = data.coder_parameter.az.value
}
}`,
},
expectTags: map[string]string{"platform": "kubernetes", "cluster": "developers", "region": "us", "az": "a"},
expectError: "",
},
{
name: "main.tf with parameter that has default value from another parameter",
files: map[string]string{
"main.tf": `
provider "foo" {}
resource "foo_bar" "baz" {}
variable "region" {
type = string
default = "us"
}
data "coder_parameter" "unrelated" {
name = "unrelated"
type = "list(string)"
default = jsonencode(["a", "b"])
}
data "coder_parameter" "az" {
type = string
default = "${""}${"a"}"
}
data "coder_parameter" "az2" {
name = "az"
type = "string"
default = data.coder_parameter.az.value
}
data "coder_workspace_tags" "tags" {
tags = {
"platform" = "kubernetes",
"cluster" = "${"devel"}${"opers"}"
"region" = var.region
"az" = data.coder_parameter.az2.value
}
}`,
},
expectError: "Unknown variable; There is no variable named \"data\".",
},
{
name: "main.tf with multiple valid workspace tags",
files: map[string]string{
"main.tf": `
provider "foo" {}
resource "foo_bar" "baz" {}
variable "region" {
type = string
default = "us"
}
variable "region2" {
type = string
default = "eu"
}
data "coder_parameter" "unrelated" {
name = "unrelated"
type = "list(string)"
default = jsonencode(["a", "b"])
}
data "coder_parameter" "az" {
name = "az"
type = "string"
default = "a"
}
data "coder_parameter" "az2" {
name = "az2"
type = "string"
default = "b"
}
data "coder_workspace_tags" "tags" {
tags = {
"platform" = "kubernetes",
"cluster" = "${"devel"}${"opers"}"
"region" = var.region
"az" = data.coder_parameter.az.value
}
}
data "coder_workspace_tags" "more_tags" {
tags = {
"foo" = "bar"
}
}`,
},
expectTags: map[string]string{"platform": "kubernetes", "cluster": "developers", "region": "us", "az": "a", "foo": "bar"},
expectError: "",
},
{
name: "main.tf with missing parameter default value for workspace tags",
files: map[string]string{
"main.tf": `
provider "foo" {}
resource "foo_bar" "baz" {}
variable "region" {
type = string
default = "us"
}
data "coder_parameter" "unrelated" {
name = "unrelated"
type = "list(string)"
default = jsonencode(["a", "b"])
}
data "coder_parameter" "az" {
name = "az"
type = "string"
}
data "coder_workspace_tags" "tags" {
tags = {
"platform" = "kubernetes",
"cluster" = "${"devel"}${"opers"}"
"region" = var.region
"az" = data.coder_parameter.az.value
}
}`,
},
expectTags: map[string]string{"cluster": "developers", "az": "", "platform": "kubernetes", "region": "us"},
},
{
name: "main.tf with missing parameter default value outside workspace tags",
files: map[string]string{
"main.tf": `
provider "foo" {}
resource "foo_bar" "baz" {}
variable "region" {
type = string
default = "us"
}
data "coder_parameter" "unrelated" {
name = "unrelated"
type = "list(string)"
default = jsonencode(["a", "b"])
}
data "coder_parameter" "az" {
name = "az"
type = "string"
default = "a"
}
data "coder_parameter" "notaz" {
name = "notaz"
type = "string"
}
data "coder_workspace_tags" "tags" {
tags = {
"platform" = "kubernetes",
"cluster" = "${"devel"}${"opers"}"
"region" = var.region
"az" = data.coder_parameter.az.value
}
}`,
},
expectTags: map[string]string{"platform": "kubernetes", "cluster": "developers", "region": "us", "az": "a"},
expectError: ``,
},
{
name: "main.tf with missing variable default value outside workspace tags",
files: map[string]string{
"main.tf": `
provider "foo" {}
resource "foo_bar" "baz" {}
variable "region" {
type = string
default = "us"
}
variable "notregion" {
type = string
}
data "coder_parameter" "unrelated" {
name = "unrelated"
type = "list(string)"
default = jsonencode(["a", "b"])
}
data "coder_parameter" "az" {
name = "az"
type = "string"
default = "a"
}
data "coder_workspace_tags" "tags" {
tags = {
"platform" = "kubernetes",
"cluster" = "${"devel"}${"opers"}"
"region" = var.region
"az" = data.coder_parameter.az.value
}
}`,
},
expectTags: map[string]string{"platform": "kubernetes", "cluster": "developers", "region": "us", "az": "a"},
expectError: ``,
},
{
name: "main.tf with disallowed data source for workspace tags",
files: map[string]string{
"main.tf": `
provider "foo" {}
resource "foo_bar" "baz" {
name = "foobar"
}
variable "region" {
type = string
default = "us"
}
data "coder_parameter" "unrelated" {
name = "unrelated"
type = "list(string)"
default = jsonencode(["a", "b"])
}
data "coder_parameter" "az" {
name = "az"
type = "string"
default = "a"
}
data "local_file" "hostname" {
filename = "/etc/hostname"
}
data "coder_workspace_tags" "tags" {
tags = {
"platform" = "kubernetes",
"cluster" = "${"devel"}${"opers"}"
"region" = var.region
"az" = data.coder_parameter.az.value
"hostname" = data.local_file.hostname.content
}
}`,
},
expectTags: nil,
expectError: `invalid workspace tag value "data.local_file.hostname.content": only the "coder_parameter" data source is supported here`,
},
{
name: "main.tf with disallowed resource for workspace tags",
files: map[string]string{
"main.tf": `
provider "foo" {}
resource "foo_bar" "baz" {
name = "foobar"
}
variable "region" {
type = string
default = "us"
}
data "coder_parameter" "unrelated" {
name = "unrelated"
type = "list(string)"
default = jsonencode(["a", "b"])
}
data "coder_parameter" "az" {
name = "az"
type = "string"
default = "a"
}
data "coder_workspace_tags" "tags" {
tags = {
"platform" = "kubernetes",
"cluster" = "${"devel"}${"opers"}"
"region" = var.region
"az" = data.coder_parameter.az.value
"foobarbaz" = foo_bar.baz.name
}
}`,
},
expectTags: nil,
// TODO: this error isn't great, but it has the desired effect.
expectError: `There is no variable named "foo_bar"`,
},
{
name: "main.tf with allowed functions in workspace tags",
files: map[string]string{
"main.tf": `
provider "foo" {}
resource "foo_bar" "baz" {
name = "foobar"
}
locals {
some_path = pathexpand("file.txt")
}
variable "region" {
type = string
default = "us"
}
data "coder_parameter" "unrelated" {
name = "unrelated"
type = "list(string)"
default = jsonencode(["a", "b"])
}
data "coder_parameter" "az" {
name = "az"
type = "string"
default = "a"
}
data "coder_workspace_tags" "tags" {
tags = {
"platform" = "kubernetes",
"cluster" = "${"devel"}${"opers"}"
"region" = try(split(".", var.region)[1], "placeholder")
"az" = try(split(".", data.coder_parameter.az.value)[1], "placeholder")
}
}`,
},
expectTags: map[string]string{"platform": "kubernetes", "cluster": "developers", "region": "placeholder", "az": "placeholder"},
},
{
name: "main.tf with disallowed functions in workspace tags",
files: map[string]string{
"main.tf": `
provider "foo" {}
resource "foo_bar" "baz" {
name = "foobar"
}
locals {
some_path = pathexpand("file.txt")
}
variable "region" {
type = string
default = "region.us"
}
data "coder_parameter" "unrelated" {
name = "unrelated"
type = "list(string)"
default = jsonencode(["a", "b"])
}
data "coder_parameter" "az" {
name = "az"
type = "string"
default = "az.a"
}
data "coder_workspace_tags" "tags" {
tags = {
"platform" = "kubernetes",
"cluster" = "${"devel"}${"opers"}"
"region" = try(split(".", var.region)[1], "placeholder")
"az" = try(split(".", data.coder_parameter.az.value)[1], "placeholder")
"some_path" = pathexpand("~/file.txt")
}
}`,
},
expectTags: nil,
expectError: `function "pathexpand" may not be used here`,
},
{
name: "supported types",
files: map[string]string{
"main.tf": `
variable "stringvar" {
type = string
default = "a"
}
variable "numvar" {
type = number
default = 1
}
variable "boolvar" {
type = bool
default = true
}
variable "listvar" {
type = list(string)
default = ["a"]
}
variable "mapvar" {
type = map(string)
default = {"a": "b"}
}
data "coder_parameter" "stringparam" {
name = "stringparam"
type = "string"
default = "a"
}
data "coder_parameter" "numparam" {
name = "numparam"
type = "number"
default = 1
}
data "coder_parameter" "boolparam" {
name = "boolparam"
type = "bool"
default = true
}
data "coder_parameter" "listparam" {
name = "listparam"
type = "list(string)"
default = "[\"a\", \"b\"]"
}
data "coder_workspace_tags" "tags" {
tags = {
"stringvar" = var.stringvar
"numvar" = var.numvar
"boolvar" = var.boolvar
"listvar" = var.listvar
"mapvar" = var.mapvar
"stringparam" = data.coder_parameter.stringparam.value
"numparam" = data.coder_parameter.numparam.value
"boolparam" = data.coder_parameter.boolparam.value
"listparam" = data.coder_parameter.listparam.value
}
}`,
},
expectTags: map[string]string{
"stringvar": "a",
"numvar": "1",
"boolvar": "true",
"listvar": `["a"]`,
"mapvar": `{"a":"b"}`,
"stringparam": "a",
"numparam": "1",
"boolparam": "true",
"listparam": `["a", "b"]`,
},
expectError: ``,
},
{
name: "overlapping var name",
files: map[string]string{
`main.tf`: `
variable "a" {
type = string
default = "1"
}
variable "unused" {
type = map(string)
default = {"a" : "b"}
}
variable "ab" {
description = "This is a variable of type string"
type = string
default = "ab"
}
data "coder_workspace_tags" "tags" {
tags = {
"foo": "bar",
"a": var.a,
}
}`,
},
expectTags: map[string]string{"foo": "bar", "a": "1"},
},
} {
t.Run(tc.name+"/tar", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
tar := testutil.CreateTar(t, tc.files)
logger := testutil.Logger(t)
tmpDir := t.TempDir()
tfparse.WriteArchive(tar, "application/x-tar", tmpDir)
parser, diags := tfparse.New(tmpDir, tfparse.WithLogger(logger))
require.NoError(t, diags.Err())
tags, err := parser.WorkspaceTagDefaults(ctx)
if tc.expectError != "" {
require.NotNil(t, err)
require.Contains(t, err.Error(), tc.expectError)
} else {
require.NoError(t, err)
require.Equal(t, tc.expectTags, tags)
}
})
t.Run(tc.name+"/zip", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
zip := testutil.CreateZip(t, tc.files)
logger := testutil.Logger(t)
tmpDir := t.TempDir()
tfparse.WriteArchive(zip, "application/zip", tmpDir)
parser, diags := tfparse.New(tmpDir, tfparse.WithLogger(logger))
require.NoError(t, diags.Err())
tags, err := parser.WorkspaceTagDefaults(ctx)
if tc.expectError != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tc.expectError)
} else {
require.NoError(t, err)
require.Equal(t, tc.expectTags, tags)
}
})
}
}
// Last run results:
// goos: linux
// goarch: amd64
// pkg: github.com/coder/coder/v2/provisioner/terraform/tfparse
// cpu: AMD EPYC 7502P 32-Core Processor
// BenchmarkWorkspaceTagDefaultsFromFile/Tar-16 1922 847236 ns/op 176257 B/op 1073 allocs/op
// BenchmarkWorkspaceTagDefaultsFromFile/Zip-16 1273 946910 ns/op 225293 B/op 1130 allocs/op
// PASS
func BenchmarkWorkspaceTagDefaultsFromFile(b *testing.B) {
files := map[string]string{
"main.tf": `
provider "foo" {}
resource "foo_bar" "baz" {}
variable "region" {
type = string
default = "us"
}
data "coder_parameter" "az" {
name = "az"
type = "string"
default = "a"
}
data "coder_workspace_tags" "tags" {
tags = {
"platform" = "kubernetes",
"cluster" = "${"devel"}${"opers"}"
"region" = var.region
"az" = data.coder_parameter.az.value
}
}`,
}
tarFile := testutil.CreateTar(b, files)
zipFile := testutil.CreateZip(b, files)
logger := discardLogger(b)
b.ResetTimer()
b.Run("Tar", func(b *testing.B) {
ctx := context.Background()
for i := 0; i < b.N; i++ {
tmpDir := b.TempDir()
tfparse.WriteArchive(tarFile, "application/x-tar", tmpDir)
parser, diags := tfparse.New(tmpDir, tfparse.WithLogger(logger))
require.NoError(b, diags.Err())
_, _, err := parser.WorkspaceTags(ctx)
if err != nil {
b.Fatal(err)
}
}
})
b.Run("Zip", func(b *testing.B) {
ctx := context.Background()
for i := 0; i < b.N; i++ {
tmpDir := b.TempDir()
tfparse.WriteArchive(zipFile, "application/zip", tmpDir)
parser, diags := tfparse.New(tmpDir, tfparse.WithLogger(logger))
require.NoError(b, diags.Err())
_, _, err := parser.WorkspaceTags(ctx)
if err != nil {
b.Fatal(err)
}
}
})
}
func discardLogger(_ testing.TB) slog.Logger {
l := slog.Make(sloghuman.Sink(io.Discard))
log.SetOutput(slog.Stdlib(context.Background(), l, slog.LevelInfo).Writer())
return l
}