Skip to content

feat: terraform variables added to input #165

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions internal/verify/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,15 @@ func (e WorkingExecutable) Init(ctx context.Context) error {
return e.TF.Init(ctx, tfexec.Upgrade(true))
}

func (e WorkingExecutable) Plan(ctx context.Context, outPath string) (bool, error) {
changes, err := e.TF.Plan(ctx, tfexec.Out(outPath))
func (e WorkingExecutable) Plan(ctx context.Context, outPath string, opts ...tfexec.PlanOption) (bool, error) {
opts = append(opts, tfexec.Out(outPath))
changes, err := e.TF.Plan(ctx, opts...)
return changes, err
}

func (e WorkingExecutable) Apply(ctx context.Context) ([]byte, error) {
func (e WorkingExecutable) Apply(ctx context.Context, opts ...tfexec.ApplyOption) ([]byte, error) {
var out bytes.Buffer
err := e.TF.ApplyJSON(ctx, &out)
err := e.TF.ApplyJSON(ctx, &out, opts...)
return out.Bytes(), err
}

Expand Down
62 changes: 24 additions & 38 deletions preview.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import (
"fmt"
"io/fs"
"log/slog"
"path/filepath"

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

"github.com/coder/preview/hclext"
"github.com/coder/preview/tfvars"
"github.com/coder/preview/types"
)

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

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

// TODO: Fix logging. There is no way to pass in an instanced logger to
// the parser.
// slog.SetLogLoggerLevel(slog.LevelDebug)
// slog.SetDefault(slog.New(log.NewHandler(os.Stderr, nil)))

varFiles, err := tfVarFiles("", dir)
varFiles, err := tfvars.TFVarFiles("", dir)
if err != nil {
return nil, hcl.Diagnostics{
{
Expand All @@ -96,6 +94,17 @@ func Preview(ctx context.Context, input Input, dir fs.FS) (output *Output, diagn
}
}

variableValues, err := tfvars.LoadTFVars(dir, varFiles)
if err != nil {
return nil, hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "Failed to load tfvars from files",
Detail: err.Error(),
},
}
}

planHook, err := planJSONHook(dir, input)
if err != nil {
return nil, hcl.Diagnostics{
Expand Down Expand Up @@ -123,17 +132,24 @@ func Preview(ctx context.Context, input Input, dir fs.FS) (output *Output, diagn
logger = slog.New(slog.DiscardHandler)
}

// Override with user-supplied variables
for k, v := range input.TFVars {
variableValues[k] = v
}

// moduleSource is "" for a local module
p := parser.New(dir, "",
parser.OptionWithLogger(logger),
parser.OptionStopOnHCLError(false),
parser.OptionWithDownloads(false),
parser.OptionWithSkipCachedModules(true),
parser.OptionWithTFVarsPaths(varFiles...),
parser.OptionWithEvalHook(planHook),
parser.OptionWithEvalHook(ownerHook),
parser.OptionWithWorkingDirectoryPath("/"),
parser.OptionWithEvalHook(parameterContextsEvalHook(input)),
// 'OptionsWithTfVars' cannot be set with 'OptionWithTFVarsPaths'. So load the
// tfvars from the files ourselves and merge with the user-supplied tf vars.
parser.OptionsWithTfVars(variableValues),
)

err = p.ParseFS(ctx, ".")
Expand Down Expand Up @@ -179,33 +195,3 @@ func (i Input) RichParameterValue(key string) (string, bool) {
p, ok := i.ParameterValues[key]
return p, ok
}

// tfVarFiles extracts any .tfvars files from the given directory.
// TODO: Test nested directories and how that should behave.
func tfVarFiles(path string, dir fs.FS) ([]string, error) {
dp := "."
entries, err := fs.ReadDir(dir, dp)
if err != nil {
return nil, fmt.Errorf("read dir %q: %w", dp, err)
}

files := make([]string, 0)
for _, entry := range entries {
if entry.IsDir() {
subD, err := fs.Sub(dir, entry.Name())
if err != nil {
return nil, fmt.Errorf("sub dir %q: %w", entry.Name(), err)
}
newFiles, err := tfVarFiles(filepath.Join(path, entry.Name()), subD)
if err != nil {
return nil, err
}
files = append(files, newFiles...)
}

if filepath.Ext(entry.Name()) == ".tfvars" {
files = append(files, filepath.Join(path, entry.Name()))
}
}
return files, nil
}
32 changes: 32 additions & 0 deletions preview_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/hashicorp/hcl/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zclconf/go-cty/cty"

"github.com/coder/preview"
"github.com/coder/preview/types"
Expand Down Expand Up @@ -471,6 +472,37 @@ func Test_Extract(t *testing.T) {
optNames("GoLand 2024.3", "IntelliJ IDEA Ultimate 2024.3", "PyCharm Professional 2024.3"),
},
},
{
name: "tfvars_from_file",
dir: "tfvars",
expTags: map[string]string{},
input: preview.Input{
ParameterValues: map[string]string{},
},
unknownTags: []string{},
params: map[string]assertParam{
"variable_values": ap().
def("alex").optVals("alex", "bob", "claire", "jason"),
},
},
{
name: "tfvars_from_input",
dir: "tfvars",
expTags: map[string]string{},
input: preview.Input{
ParameterValues: map[string]string{},
TFVars: map[string]cty.Value{
"one": cty.StringVal("andrew"),
"two": cty.StringVal("bill"),
"three": cty.StringVal("carter"),
},
},
unknownTags: []string{},
params: map[string]assertParam{
"variable_values": ap().
def("andrew").optVals("andrew", "bill", "carter", "jason"),
},
},
{
name: "unknownoption",
dir: "unknownoption",
Expand Down
26 changes: 19 additions & 7 deletions previewe2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import (
"testing"
"time"

"github.com/hashicorp/terraform-exec/tfexec"
"github.com/stretchr/testify/require"

"github.com/coder/preview"
"github.com/coder/preview/internal/verify"
"github.com/coder/preview/tfvars"
"github.com/coder/preview/types"
)

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

entryWrkPath := t.TempDir()

for _, tfexec := range tfexecs {
tfexec := tfexec
for _, tfexecutable := range tfexecs {
tfexecutable := tfexecutable

t.Run(tfexec.Version, func(t *testing.T) {
wp := filepath.Join(entryWrkPath, tfexec.Version)
t.Run(tfexecutable.Version, func(t *testing.T) {
wp := filepath.Join(entryWrkPath, tfexecutable.Version)
err := os.MkdirAll(wp, 0755)
require.NoError(t, err, "creating working dir")

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

exe, err := tfexec.WorkingDir(wp)
exe, err := tfexecutable.WorkingDir(wp)
require.NoError(t, err, "creating working executable")

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

tfVarFiles, err := tfvars.TFVarFiles("", subFS)
require.NoError(t, err, "loading tfvars files")

planOpts := make([]tfexec.PlanOption, 0)
applyOpts := make([]tfexec.ApplyOption, 0)
for _, varFile := range tfVarFiles {
planOpts = append(planOpts, tfexec.VarFile(varFile))
applyOpts = append(applyOpts, tfexec.VarFile(varFile))
}

planOutFile := "tfplan"
planOutPath := filepath.Join(wp, planOutFile)
_, err = exe.Plan(ctx, planOutPath)
_, err = exe.Plan(ctx, planOutPath, planOpts...)
require.NoError(t, err, "terraform plan")

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

_, err = exe.Apply(ctx)
_, err = exe.Apply(ctx, applyOpts...)
require.NoError(t, err, "terraform apply")

state, err := exe.Show(ctx)
Expand Down
1 change: 1 addition & 0 deletions testdata/tfvars/.auto.tfvars.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"four":"jason"}
61 changes: 61 additions & 0 deletions testdata/tfvars/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Base case for workspace tags + parameters.
terraform {
required_providers {
coder = {
source = "coder/coder"
}
docker = {
source = "kreuzwerker/docker"
version = "3.0.2"
}
}
}

variable "one" {
default = "alice"
type = string
}

variable "two" {
default = "bob"
type = string
}

variable "three" {
default = "charlie"
type = string
}

variable "four" {
default = "jack"
type = string
}


data "coder_parameter" "variable_values" {
name = "variable_values"
description = "Just to show the variable values"
type = "string"
default = var.one


option {
name = "one"
value = var.one
}

option {
name = "two"
value = var.two
}

option {
name = "three"
value = var.three
}

option {
name = "four"
value = var.four
}
}
2 changes: 2 additions & 0 deletions testdata/tfvars/values.tfvars
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
one="alex"
three="claire"
Loading
Loading