Skip to content

Commit ce03bce

Browse files
authored
feat: terraform variables added to input (#165)
Input can override template vars, implementing the equivalent of '-var' in terraform.
1 parent 9a7fd06 commit ce03bce

File tree

8 files changed

+249
-49
lines changed

8 files changed

+249
-49
lines changed

internal/verify/exec.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,15 @@ func (e WorkingExecutable) Init(ctx context.Context) error {
5656
return e.TF.Init(ctx, tfexec.Upgrade(true))
5757
}
5858

59-
func (e WorkingExecutable) Plan(ctx context.Context, outPath string) (bool, error) {
60-
changes, err := e.TF.Plan(ctx, tfexec.Out(outPath))
59+
func (e WorkingExecutable) Plan(ctx context.Context, outPath string, opts ...tfexec.PlanOption) (bool, error) {
60+
opts = append(opts, tfexec.Out(outPath))
61+
changes, err := e.TF.Plan(ctx, opts...)
6162
return changes, err
6263
}
6364

64-
func (e WorkingExecutable) Apply(ctx context.Context) ([]byte, error) {
65+
func (e WorkingExecutable) Apply(ctx context.Context, opts ...tfexec.ApplyOption) ([]byte, error) {
6566
var out bytes.Buffer
66-
err := e.TF.ApplyJSON(ctx, &out)
67+
err := e.TF.ApplyJSON(ctx, &out, opts...)
6768
return out.Bytes(), err
6869
}
6970

preview.go

Lines changed: 24 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ import (
66
"fmt"
77
"io/fs"
88
"log/slog"
9-
"path/filepath"
109

1110
"github.com/aquasecurity/trivy/pkg/iac/scanners/terraform/parser"
1211
"github.com/hashicorp/hcl/v2"
1312
"github.com/zclconf/go-cty/cty"
1413
ctyjson "github.com/zclconf/go-cty/cty/json"
1514

1615
"github.com/coder/preview/hclext"
16+
"github.com/coder/preview/tfvars"
1717
"github.com/coder/preview/types"
1818
)
1919

@@ -26,6 +26,9 @@ type Input struct {
2626
ParameterValues map[string]string
2727
Owner types.WorkspaceOwner
2828
Logger *slog.Logger
29+
// TFVars will override any variables set in '.tfvars' files.
30+
// The value set must be a cty.Value, as the type can be anything.
31+
TFVars map[string]cty.Value
2932
}
3033

3134
type Output struct {
@@ -80,12 +83,7 @@ func Preview(ctx context.Context, input Input, dir fs.FS) (output *Output, diagn
8083
}
8184
}()
8285

83-
// TODO: Fix logging. There is no way to pass in an instanced logger to
84-
// the parser.
85-
// slog.SetLogLoggerLevel(slog.LevelDebug)
86-
// slog.SetDefault(slog.New(log.NewHandler(os.Stderr, nil)))
87-
88-
varFiles, err := tfVarFiles("", dir)
86+
varFiles, err := tfvars.TFVarFiles("", dir)
8987
if err != nil {
9088
return nil, hcl.Diagnostics{
9189
{
@@ -96,6 +94,17 @@ func Preview(ctx context.Context, input Input, dir fs.FS) (output *Output, diagn
9694
}
9795
}
9896

97+
variableValues, err := tfvars.LoadTFVars(dir, varFiles)
98+
if err != nil {
99+
return nil, hcl.Diagnostics{
100+
{
101+
Severity: hcl.DiagError,
102+
Summary: "Failed to load tfvars from files",
103+
Detail: err.Error(),
104+
},
105+
}
106+
}
107+
99108
planHook, err := planJSONHook(dir, input)
100109
if err != nil {
101110
return nil, hcl.Diagnostics{
@@ -123,17 +132,24 @@ func Preview(ctx context.Context, input Input, dir fs.FS) (output *Output, diagn
123132
logger = slog.New(slog.DiscardHandler)
124133
}
125134

135+
// Override with user-supplied variables
136+
for k, v := range input.TFVars {
137+
variableValues[k] = v
138+
}
139+
126140
// moduleSource is "" for a local module
127141
p := parser.New(dir, "",
128142
parser.OptionWithLogger(logger),
129143
parser.OptionStopOnHCLError(false),
130144
parser.OptionWithDownloads(false),
131145
parser.OptionWithSkipCachedModules(true),
132-
parser.OptionWithTFVarsPaths(varFiles...),
133146
parser.OptionWithEvalHook(planHook),
134147
parser.OptionWithEvalHook(ownerHook),
135148
parser.OptionWithWorkingDirectoryPath("/"),
136149
parser.OptionWithEvalHook(parameterContextsEvalHook(input)),
150+
// 'OptionsWithTfVars' cannot be set with 'OptionWithTFVarsPaths'. So load the
151+
// tfvars from the files ourselves and merge with the user-supplied tf vars.
152+
parser.OptionsWithTfVars(variableValues),
137153
)
138154

139155
err = p.ParseFS(ctx, ".")
@@ -179,33 +195,3 @@ func (i Input) RichParameterValue(key string) (string, bool) {
179195
p, ok := i.ParameterValues[key]
180196
return p, ok
181197
}
182-
183-
// tfVarFiles extracts any .tfvars files from the given directory.
184-
// TODO: Test nested directories and how that should behave.
185-
func tfVarFiles(path string, dir fs.FS) ([]string, error) {
186-
dp := "."
187-
entries, err := fs.ReadDir(dir, dp)
188-
if err != nil {
189-
return nil, fmt.Errorf("read dir %q: %w", dp, err)
190-
}
191-
192-
files := make([]string, 0)
193-
for _, entry := range entries {
194-
if entry.IsDir() {
195-
subD, err := fs.Sub(dir, entry.Name())
196-
if err != nil {
197-
return nil, fmt.Errorf("sub dir %q: %w", entry.Name(), err)
198-
}
199-
newFiles, err := tfVarFiles(filepath.Join(path, entry.Name()), subD)
200-
if err != nil {
201-
return nil, err
202-
}
203-
files = append(files, newFiles...)
204-
}
205-
206-
if filepath.Ext(entry.Name()) == ".tfvars" {
207-
files = append(files, filepath.Join(path, entry.Name()))
208-
}
209-
}
210-
return files, nil
211-
}

preview_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/hashicorp/hcl/v2"
1414
"github.com/stretchr/testify/assert"
1515
"github.com/stretchr/testify/require"
16+
"github.com/zclconf/go-cty/cty"
1617

1718
"github.com/coder/preview"
1819
"github.com/coder/preview/types"
@@ -471,6 +472,37 @@ func Test_Extract(t *testing.T) {
471472
optNames("GoLand 2024.3", "IntelliJ IDEA Ultimate 2024.3", "PyCharm Professional 2024.3"),
472473
},
473474
},
475+
{
476+
name: "tfvars_from_file",
477+
dir: "tfvars",
478+
expTags: map[string]string{},
479+
input: preview.Input{
480+
ParameterValues: map[string]string{},
481+
},
482+
unknownTags: []string{},
483+
params: map[string]assertParam{
484+
"variable_values": ap().
485+
def("alex").optVals("alex", "bob", "claire", "jason"),
486+
},
487+
},
488+
{
489+
name: "tfvars_from_input",
490+
dir: "tfvars",
491+
expTags: map[string]string{},
492+
input: preview.Input{
493+
ParameterValues: map[string]string{},
494+
TFVars: map[string]cty.Value{
495+
"one": cty.StringVal("andrew"),
496+
"two": cty.StringVal("bill"),
497+
"three": cty.StringVal("carter"),
498+
},
499+
},
500+
unknownTags: []string{},
501+
params: map[string]assertParam{
502+
"variable_values": ap().
503+
def("andrew").optVals("andrew", "bill", "carter", "jason"),
504+
},
505+
},
474506
{
475507
name: "unknownoption",
476508
dir: "unknownoption",

previewe2e_test.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import (
1010
"testing"
1111
"time"
1212

13+
"github.com/hashicorp/terraform-exec/tfexec"
1314
"github.com/stretchr/testify/require"
1415

1516
"github.com/coder/preview"
1617
"github.com/coder/preview/internal/verify"
18+
"github.com/coder/preview/tfvars"
1719
"github.com/coder/preview/types"
1820
)
1921

@@ -102,11 +104,11 @@ func Test_VerifyE2E(t *testing.T) {
102104

103105
entryWrkPath := t.TempDir()
104106

105-
for _, tfexec := range tfexecs {
106-
tfexec := tfexec
107+
for _, tfexecutable := range tfexecs {
108+
tfexecutable := tfexecutable
107109

108-
t.Run(tfexec.Version, func(t *testing.T) {
109-
wp := filepath.Join(entryWrkPath, tfexec.Version)
110+
t.Run(tfexecutable.Version, func(t *testing.T) {
111+
wp := filepath.Join(entryWrkPath, tfexecutable.Version)
110112
err := os.MkdirAll(wp, 0755)
111113
require.NoError(t, err, "creating working dir")
112114

@@ -118,17 +120,27 @@ func Test_VerifyE2E(t *testing.T) {
118120
err = verify.CopyTFFS(wp, subFS)
119121
require.NoError(t, err, "copying test data to working dir")
120122

121-
exe, err := tfexec.WorkingDir(wp)
123+
exe, err := tfexecutable.WorkingDir(wp)
122124
require.NoError(t, err, "creating working executable")
123125

124126
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*2)
125127
defer cancel()
126128
err = exe.Init(ctx)
127129
require.NoError(t, err, "terraform init")
128130

131+
tfVarFiles, err := tfvars.TFVarFiles("", subFS)
132+
require.NoError(t, err, "loading tfvars files")
133+
134+
planOpts := make([]tfexec.PlanOption, 0)
135+
applyOpts := make([]tfexec.ApplyOption, 0)
136+
for _, varFile := range tfVarFiles {
137+
planOpts = append(planOpts, tfexec.VarFile(varFile))
138+
applyOpts = append(applyOpts, tfexec.VarFile(varFile))
139+
}
140+
129141
planOutFile := "tfplan"
130142
planOutPath := filepath.Join(wp, planOutFile)
131-
_, err = exe.Plan(ctx, planOutPath)
143+
_, err = exe.Plan(ctx, planOutPath, planOpts...)
132144
require.NoError(t, err, "terraform plan")
133145

134146
plan, err := exe.ShowPlan(ctx, planOutPath)
@@ -141,7 +153,7 @@ func Test_VerifyE2E(t *testing.T) {
141153
err = os.WriteFile(filepath.Join(wp, "plan.json"), pd, 0644)
142154
require.NoError(t, err, "writing plan.json")
143155

144-
_, err = exe.Apply(ctx)
156+
_, err = exe.Apply(ctx, applyOpts...)
145157
require.NoError(t, err, "terraform apply")
146158

147159
state, err := exe.Show(ctx)

testdata/tfvars/.auto.tfvars.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"four":"jason"}

testdata/tfvars/main.tf

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Base case for workspace tags + parameters.
2+
terraform {
3+
required_providers {
4+
coder = {
5+
source = "coder/coder"
6+
}
7+
docker = {
8+
source = "kreuzwerker/docker"
9+
version = "3.0.2"
10+
}
11+
}
12+
}
13+
14+
variable "one" {
15+
default = "alice"
16+
type = string
17+
}
18+
19+
variable "two" {
20+
default = "bob"
21+
type = string
22+
}
23+
24+
variable "three" {
25+
default = "charlie"
26+
type = string
27+
}
28+
29+
variable "four" {
30+
default = "jack"
31+
type = string
32+
}
33+
34+
35+
data "coder_parameter" "variable_values" {
36+
name = "variable_values"
37+
description = "Just to show the variable values"
38+
type = "string"
39+
default = var.one
40+
41+
42+
option {
43+
name = "one"
44+
value = var.one
45+
}
46+
47+
option {
48+
name = "two"
49+
value = var.two
50+
}
51+
52+
option {
53+
name = "three"
54+
value = var.three
55+
}
56+
57+
option {
58+
name = "four"
59+
value = var.four
60+
}
61+
}

testdata/tfvars/values.tfvars

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
one="alex"
2+
three="claire"

0 commit comments

Comments
 (0)