1
1
package cli
2
2
3
3
import (
4
+ "encoding/json"
5
+ "fmt"
4
6
"os"
7
+ "path/filepath"
8
+ "sort"
5
9
"strings"
6
10
7
11
"golang.org/x/xerrors"
8
12
"gopkg.in/yaml.v3"
9
13
14
+ "github.com/hashicorp/hcl/v2/hclparse"
15
+ "github.com/zclconf/go-cty/cty"
16
+
10
17
"github.com/coder/coder/v2/codersdk"
11
18
)
12
19
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
+
14
63
fromFile , err := parseVariableValuesFromFile (variablesFile )
15
64
if err != nil {
16
65
return nil , err
@@ -21,7 +70,131 @@ func ParseUserVariableValues(variablesFile string, commandLineVariables []string
21
70
return nil , err
22
71
}
23
72
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
25
198
}
26
199
27
200
func parseVariableValuesFromFile (variablesFile string ) ([]codersdk.VariableValue , error ) {
@@ -94,5 +267,8 @@ func combineVariableValues(valuesSets ...[]codersdk.VariableValue) []codersdk.Va
94
267
result = append (result , codersdk.VariableValue {Name : name , Value : value })
95
268
}
96
269
270
+ sort .Slice (result , func (i , j int ) bool {
271
+ return result [i ].Name < result [j ].Name
272
+ })
97
273
return result
98
274
}
0 commit comments