Skip to content

Commit cb77f04

Browse files
authored
feat: load variables from tfvars files (#11549)
1 parent aeb1ab8 commit cb77f04

File tree

4 files changed

+382
-2
lines changed

4 files changed

+382
-2
lines changed

cli/templatecreate.go

+13
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,18 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
9595

9696
message := uploadFlags.templateMessage(inv)
9797

98+
var varsFiles []string
99+
if !uploadFlags.stdin() {
100+
varsFiles, err = DiscoverVarsFiles(uploadFlags.directory)
101+
if err != nil {
102+
return err
103+
}
104+
105+
if len(varsFiles) > 0 {
106+
_, _ = fmt.Fprintln(inv.Stdout, "Auto-discovered Terraform tfvars files. Make sure to review and clean up any unused files.")
107+
}
108+
}
109+
98110
// Confirm upload of the directory.
99111
resp, err := uploadFlags.upload(inv, client)
100112
if err != nil {
@@ -107,6 +119,7 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
107119
}
108120

109121
userVariableValues, err := ParseUserVariableValues(
122+
varsFiles,
110123
variablesFile,
111124
commandLineVariables)
112125
if err != nil {

cli/templatepush.go

+13
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,18 @@ func (r *RootCmd) templatePush() *clibase.Cmd {
7878

7979
message := uploadFlags.templateMessage(inv)
8080

81+
var varsFiles []string
82+
if !uploadFlags.stdin() {
83+
varsFiles, err = DiscoverVarsFiles(uploadFlags.directory)
84+
if err != nil {
85+
return err
86+
}
87+
88+
if len(varsFiles) > 0 {
89+
_, _ = fmt.Fprintln(inv.Stdout, "Auto-discovered Terraform tfvars files. Make sure to review and clean up any unused files.")
90+
}
91+
}
92+
8193
resp, err := uploadFlags.upload(inv, client)
8294
if err != nil {
8395
return err
@@ -89,6 +101,7 @@ func (r *RootCmd) templatePush() *clibase.Cmd {
89101
}
90102

91103
userVariableValues, err := ParseUserVariableValues(
104+
varsFiles,
92105
variablesFile,
93106
commandLineVariables)
94107
if err != nil {

cli/templatevariables.go

+178-2
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,65 @@
11
package cli
22

33
import (
4+
"encoding/json"
5+
"fmt"
46
"os"
7+
"path/filepath"
8+
"sort"
59
"strings"
610

711
"golang.org/x/xerrors"
812
"gopkg.in/yaml.v3"
913

14+
"github.com/hashicorp/hcl/v2/hclparse"
15+
"github.com/zclconf/go-cty/cty"
16+
1017
"github.com/coder/coder/v2/codersdk"
1118
)
1219

13-
func ParseUserVariableValues(variablesFile string, commandLineVariables []string) ([]codersdk.VariableValue, error) {
20+
/**
21+
* DiscoverVarsFiles function loads vars files in a predefined order:
22+
* 1. terraform.tfvars
23+
* 2. terraform.tfvars.json
24+
* 3. *.auto.tfvars
25+
* 4. *.auto.tfvars.json
26+
*/
27+
func DiscoverVarsFiles(workDir string) ([]string, error) {
28+
var found []string
29+
30+
fi, err := os.Stat(filepath.Join(workDir, "terraform.tfvars"))
31+
if err == nil {
32+
found = append(found, filepath.Join(workDir, fi.Name()))
33+
} else if !os.IsNotExist(err) {
34+
return nil, err
35+
}
36+
37+
fi, err = os.Stat(filepath.Join(workDir, "terraform.tfvars.json"))
38+
if err == nil {
39+
found = append(found, filepath.Join(workDir, fi.Name()))
40+
} else if !os.IsNotExist(err) {
41+
return nil, err
42+
}
43+
44+
dirEntries, err := os.ReadDir(workDir)
45+
if err != nil {
46+
return nil, err
47+
}
48+
49+
for _, dirEntry := range dirEntries {
50+
if strings.HasSuffix(dirEntry.Name(), ".auto.tfvars") || strings.HasSuffix(dirEntry.Name(), ".auto.tfvars.json") {
51+
found = append(found, filepath.Join(workDir, dirEntry.Name()))
52+
}
53+
}
54+
return found, nil
55+
}
56+
57+
func ParseUserVariableValues(varsFiles []string, variablesFile string, commandLineVariables []string) ([]codersdk.VariableValue, error) {
58+
fromVars, err := parseVariableValuesFromVarsFiles(varsFiles)
59+
if err != nil {
60+
return nil, err
61+
}
62+
1463
fromFile, err := parseVariableValuesFromFile(variablesFile)
1564
if err != nil {
1665
return nil, err
@@ -21,7 +70,131 @@ func ParseUserVariableValues(variablesFile string, commandLineVariables []string
2170
return nil, err
2271
}
2372

24-
return combineVariableValues(fromFile, fromCommandLine), nil
73+
return combineVariableValues(fromVars, fromFile, fromCommandLine), nil
74+
}
75+
76+
func parseVariableValuesFromVarsFiles(varsFiles []string) ([]codersdk.VariableValue, error) {
77+
var parsed []codersdk.VariableValue
78+
for _, varsFile := range varsFiles {
79+
content, err := os.ReadFile(varsFile)
80+
if err != nil {
81+
return nil, err
82+
}
83+
84+
var t []codersdk.VariableValue
85+
ext := filepath.Ext(varsFile)
86+
switch ext {
87+
case ".tfvars":
88+
t, err = parseVariableValuesFromHCL(content)
89+
if err != nil {
90+
return nil, xerrors.Errorf("unable to parse HCL content: %w", err)
91+
}
92+
case ".json":
93+
t, err = parseVariableValuesFromJSON(content)
94+
if err != nil {
95+
return nil, xerrors.Errorf("unable to parse JSON content: %w", err)
96+
}
97+
default:
98+
return nil, xerrors.Errorf("unexpected tfvars format: %s", ext)
99+
}
100+
101+
parsed = append(parsed, t...)
102+
}
103+
return parsed, nil
104+
}
105+
106+
func parseVariableValuesFromHCL(content []byte) ([]codersdk.VariableValue, error) {
107+
parser := hclparse.NewParser()
108+
hclFile, diags := parser.ParseHCL(content, "file.hcl")
109+
if diags.HasErrors() {
110+
return nil, diags
111+
}
112+
113+
attrs, diags := hclFile.Body.JustAttributes()
114+
if diags.HasErrors() {
115+
return nil, diags
116+
}
117+
118+
stringData := map[string]string{}
119+
for _, attribute := range attrs {
120+
ctyValue, diags := attribute.Expr.Value(nil)
121+
if diags.HasErrors() {
122+
return nil, diags
123+
}
124+
125+
ctyType := ctyValue.Type()
126+
if ctyType.Equals(cty.String) {
127+
stringData[attribute.Name] = ctyValue.AsString()
128+
} else if ctyType.Equals(cty.Number) {
129+
stringData[attribute.Name] = ctyValue.AsBigFloat().String()
130+
} else if ctyType.IsTupleType() {
131+
// In case of tuples, Coder only supports the list(string) type.
132+
var items []string
133+
var err error
134+
_ = ctyValue.ForEachElement(func(key, val cty.Value) (stop bool) {
135+
if !val.Type().Equals(cty.String) {
136+
err = xerrors.Errorf("unsupported tuple item type: %s ", val.GoString())
137+
return true
138+
}
139+
items = append(items, val.AsString())
140+
return false
141+
})
142+
if err != nil {
143+
return nil, err
144+
}
145+
146+
m, err := json.Marshal(items)
147+
if err != nil {
148+
return nil, err
149+
}
150+
stringData[attribute.Name] = string(m)
151+
} else {
152+
return nil, xerrors.Errorf("unsupported value type (name: %s): %s", attribute.Name, ctyType.GoString())
153+
}
154+
}
155+
156+
return convertMapIntoVariableValues(stringData), nil
157+
}
158+
159+
// parseVariableValuesFromJSON converts the .tfvars.json content into template variables.
160+
// The function visits only root-level properties as template variables do not support nested
161+
// structures.
162+
func parseVariableValuesFromJSON(content []byte) ([]codersdk.VariableValue, error) {
163+
var data map[string]interface{}
164+
err := json.Unmarshal(content, &data)
165+
if err != nil {
166+
return nil, err
167+
}
168+
169+
stringData := map[string]string{}
170+
for key, value := range data {
171+
switch value.(type) {
172+
case string, int, bool:
173+
stringData[key] = fmt.Sprintf("%v", value)
174+
default:
175+
m, err := json.Marshal(value)
176+
if err != nil {
177+
return nil, err
178+
}
179+
stringData[key] = string(m)
180+
}
181+
}
182+
183+
return convertMapIntoVariableValues(stringData), nil
184+
}
185+
186+
func convertMapIntoVariableValues(m map[string]string) []codersdk.VariableValue {
187+
var parsed []codersdk.VariableValue
188+
for key, value := range m {
189+
parsed = append(parsed, codersdk.VariableValue{
190+
Name: key,
191+
Value: value,
192+
})
193+
}
194+
sort.Slice(parsed, func(i, j int) bool {
195+
return parsed[i].Name < parsed[j].Name
196+
})
197+
return parsed
25198
}
26199

27200
func parseVariableValuesFromFile(variablesFile string) ([]codersdk.VariableValue, error) {
@@ -94,5 +267,8 @@ func combineVariableValues(valuesSets ...[]codersdk.VariableValue) []codersdk.Va
94267
result = append(result, codersdk.VariableValue{Name: name, Value: value})
95268
}
96269

270+
sort.Slice(result, func(i, j int) bool {
271+
return result[i].Name < result[j].Name
272+
})
97273
return result
98274
}

0 commit comments

Comments
 (0)