@@ -31,25 +31,60 @@ import (
31
31
// introducing a circular dependency
32
32
const maxFileSizeBytes = 10 * (10 << 20 ) // 10 MB
33
33
34
- // ParseHCLFile() is only method of hclparse.Parser we use in this package.
35
- // hclparse.Parser will by default cache all previous files it has ever
36
- // parsed. While leveraging this caching behavior is nice, we _never_ want
37
- // to end up in a situation where we end up returning stale values.
38
- type Parser interface {
34
+ // parseHCLFiler is the actual interface of *hclparse.Parser we use
35
+ // to parse HCL. This is extracted to an interface so we can more
36
+ // easily swap this out for an alternative implementation later on.
37
+ type parseHCLFiler interface {
39
38
ParseHCLFile (filename string ) (* hcl.File , hcl.Diagnostics )
40
39
}
41
40
42
- // WorkspaceTags extracts tags from coder_workspace_tags data sources defined in module.
43
- // Note that this only returns the lexical values of the data source, and does not
44
- // evaluate variables and such. To do this, see evalProvisionerTags below.
45
- // If the provided Parser is nil, a new instance of hclparse.Parser will be used instead.
46
- func WorkspaceTags (ctx context.Context , logger slog.Logger , parser Parser , module * tfconfig.Module ) (map [string ]string , error ) {
47
- if parser == nil {
48
- parser = hclparse .NewParser ()
41
+ // Parser parses a Terraform module on disk.
42
+ type Parser struct {
43
+ logger slog.Logger
44
+ underlying parseHCLFiler
45
+ module * tfconfig.Module
46
+ workdir string
47
+ }
48
+
49
+ // Option is an option for a new instance of Parser.
50
+ type Option func (* Parser )
51
+
52
+ // WithLogger sets the logger to be used by Parser
53
+ func WithLogger (logger slog.Logger ) Option {
54
+ return func (p * Parser ) {
55
+ p .logger = logger
56
+ }
57
+ }
58
+
59
+ // New returns a new instance of Parser, as well as any diagnostics
60
+ // encountered while parsing the module.
61
+ func New (workdir string , opts ... Option ) (* Parser , tfconfig.Diagnostics ) {
62
+ p := Parser {
63
+ logger : slog .Make (),
64
+ underlying : hclparse .NewParser (),
65
+ workdir : workdir ,
66
+ module : nil ,
67
+ }
68
+ for _ , o := range opts {
69
+ o (& p )
70
+ }
71
+
72
+ var diags tfconfig.Diagnostics
73
+ if p .module == nil {
74
+ m , ds := tfconfig .LoadModule (workdir )
75
+ diags = ds
76
+ p .module = m
49
77
}
78
+
79
+ return & p , diags
80
+ }
81
+
82
+ // WorkspaceTags looks for all coder_workspace_tags datasource in the module
83
+ // and returns the raw values for the tags. Use
84
+ func (p * Parser ) WorkspaceTags (ctx context.Context ) (map [string ]string , error ) {
50
85
tags := map [string ]string {}
51
86
var skipped []string
52
- for _ , dataResource := range module .DataResources {
87
+ for _ , dataResource := range p . module .DataResources {
53
88
if dataResource .Type != "coder_workspace_tags" {
54
89
skipped = append (skipped , strings .Join ([]string {"data" , dataResource .Type , dataResource .Name }, "." ))
55
90
continue
@@ -62,7 +97,7 @@ func WorkspaceTags(ctx context.Context, logger slog.Logger, parser Parser, modul
62
97
continue
63
98
}
64
99
// We know in which HCL file is the data resource defined.
65
- file , diags = parser .ParseHCLFile (dataResource .Pos .Filename )
100
+ file , diags = p . underlying .ParseHCLFile (dataResource .Pos .Filename )
66
101
if diags .HasErrors () {
67
102
return nil , xerrors .Errorf ("can't parse the resource file: %s" , diags .Error ())
68
103
}
@@ -119,54 +154,33 @@ func WorkspaceTags(ctx context.Context, logger slog.Logger, parser Parser, modul
119
154
}
120
155
}
121
156
}
122
- logger .Debug (ctx , "found workspace tags" , slog .F ("tags" , maps .Keys (tags )), slog .F ("skipped" , skipped ))
157
+ p . logger .Debug (ctx , "found workspace tags" , slog .F ("tags" , maps .Keys (tags )), slog .F ("skipped" , skipped ))
123
158
return tags , nil
124
159
}
125
160
126
- // WorkspaceTagDefaultsFromFile extracts the default values for a `coder_workspace_tags` resource from the given
127
- //
128
- // file. It also ensures that any uses of the `coder_workspace_tags` data source only
129
- // reference the following data types:
130
- // 1. Static variables
131
- // 2. Template variables
132
- // 3. Coder parameters
133
- // Any other data types are not allowed, as their values cannot be known at
134
- // the time of template import.
135
- func WorkspaceTagDefaultsFromFile (ctx context.Context , logger slog.Logger , file []byte , mimetype string ) (tags map [string ]string , err error ) {
136
- module , cleanup , err := loadModuleFromFile (file , mimetype )
137
- if err != nil {
138
- return nil , xerrors .Errorf ("load module from file: %w" , err )
139
- }
140
- defer func () {
141
- if err := cleanup (); err != nil {
142
- logger .Error (ctx , "failed to clean up" , slog .Error (err ))
143
- }
144
- }()
145
-
146
- parser := hclparse .NewParser ()
147
-
161
+ func (p * Parser ) WorkspaceTagDefaults (ctx context.Context ) (map [string ]string , error ) {
148
162
// This only gets us the expressions. We need to evaluate them.
149
163
// Example: var.region -> "us"
150
- tags , err = WorkspaceTags (ctx , logger , parser , module )
164
+ tags , err := p . WorkspaceTags (ctx )
151
165
if err != nil {
152
166
return nil , xerrors .Errorf ("extract workspace tags: %w" , err )
153
167
}
154
168
155
169
// To evaluate the expressions, we need to load the default values for
156
170
// variables and parameters.
157
- varsDefaults , err := loadVarsDefaults ( ctx , logger , maps . Values ( module . Variables ) )
171
+ varsDefaults , err := p . VariableDefaults ( ctx )
158
172
if err != nil {
159
173
return nil , xerrors .Errorf ("load variable defaults: %w" , err )
160
174
}
161
- paramsDefaults , err := loadParamsDefaults ( ctx , logger , parser , maps . Values ( module . DataResources ) )
175
+ paramsDefaults , err := p . CoderParameterDefaults ( ctx )
162
176
if err != nil {
163
177
return nil , xerrors .Errorf ("load parameter defaults: %w" , err )
164
178
}
165
179
166
180
// Evaluate the tags expressions given the inputs.
167
181
// This will resolve any variables or parameters to their default
168
182
// values.
169
- evalTags , err := EvalProvisionerTags (varsDefaults , paramsDefaults , tags )
183
+ evalTags , err := evaluateWorkspaceTags (varsDefaults , paramsDefaults , tags )
170
184
if err != nil {
171
185
return nil , xerrors .Errorf ("eval provisioner tags: %w" , err )
172
186
}
@@ -181,54 +195,64 @@ func WorkspaceTagDefaultsFromFile(ctx context.Context, logger slog.Logger, file
181
195
return evalTags , nil
182
196
}
183
197
184
- func loadModuleFromFile (file []byte , mimetype string ) (module * tfconfig.Module , cleanup func () error , err error ) {
185
- // Create a temporary directory
186
- cleanup = func () error { return nil } // no-op cleanup
187
- tmpDir , err := os .MkdirTemp ("" , "tfparse-*" )
188
- if err != nil {
189
- return nil , cleanup , xerrors .Errorf ("create temp dir: %w" , err )
198
+ // TemplateVariables returns all of the Terraform variables in the module
199
+ // as TemplateVariables.
200
+ func (p * Parser ) TemplateVariables () ([]* proto.TemplateVariable , error ) {
201
+ // Sort variables by (filename, line) to make the ordering consistent
202
+ variables := make ([]* tfconfig.Variable , 0 , len (p .module .Variables ))
203
+ for _ , v := range p .module .Variables {
204
+ variables = append (variables , v )
190
205
}
191
- cleanup = func () error { // real cleanup
192
- return os .RemoveAll (tmpDir )
206
+ sort .Slice (variables , func (i , j int ) bool {
207
+ return compareSourcePos (variables [i ].Pos , variables [j ].Pos )
208
+ })
209
+
210
+ var templateVariables []* proto.TemplateVariable
211
+ for _ , v := range variables {
212
+ mv , err := convertTerraformVariable (v )
213
+ if err != nil {
214
+ return nil , err
215
+ }
216
+ templateVariables = append (templateVariables , mv )
193
217
}
218
+ return templateVariables , nil
219
+ }
194
220
195
- // Untar the file into the temporary directory
221
+ // WriteArchive is a helper function to write a in-memory archive
222
+ // with the given mimetype to disk. Only zip and tar archives
223
+ // are currently supported.
224
+ func WriteArchive (bs []byte , mimetype string , path string ) error {
225
+ // Check if we need to convert the file first!
196
226
var rdr io.Reader
197
227
switch mimetype {
198
228
case "application/x-tar" :
199
- rdr = bytes .NewReader (file )
229
+ rdr = bytes .NewReader (bs )
200
230
case "application/zip" :
201
- zr , err := zip .NewReader (bytes .NewReader (file ), int64 (len (file )))
202
- if err != nil {
203
- return nil , cleanup , xerrors .Errorf ("read zip file: %w" , err )
204
- }
205
- tarBytes , err := archive .CreateTarFromZip (zr , maxFileSizeBytes )
206
- if err != nil {
207
- return nil , cleanup , xerrors .Errorf ("convert zip to tar: %w" , err )
231
+ if zr , err := zip .NewReader (bytes .NewReader (bs ), int64 (len (bs ))); err != nil {
232
+ return xerrors .Errorf ("read zip file: %w" , err )
233
+ } else if tarBytes , err := archive .CreateTarFromZip (zr , maxFileSizeBytes ); err != nil {
234
+ return xerrors .Errorf ("convert zip to tar: %w" , err )
235
+ } else {
236
+ rdr = bytes .NewReader (tarBytes )
208
237
}
209
- rdr = bytes .NewReader (tarBytes )
210
238
default :
211
- return nil , cleanup , xerrors .Errorf ("unsupported mimetype: %s" , mimetype )
239
+ return xerrors .Errorf ("unsupported mimetype: %s" , mimetype )
212
240
}
213
241
214
- if err := provisionersdk .Untar (tmpDir , rdr ); err != nil {
215
- return nil , cleanup , xerrors .Errorf ("untar: %w" , err )
216
- }
217
-
218
- module , diags := tfconfig .LoadModule (tmpDir )
219
- if diags .HasErrors () {
220
- return nil , cleanup , xerrors .Errorf ("load module: %s" , diags .Error ())
242
+ // Untar the file into the temporary directory
243
+ if err := provisionersdk .Untar (path , rdr ); err != nil {
244
+ return xerrors .Errorf ("untar: %w" , err )
221
245
}
222
246
223
- return module , cleanup , nil
247
+ return nil
224
248
}
225
249
226
- // loadVarsDefaults returns the default values for all variables passed to it.
227
- func loadVarsDefaults ( ctx context.Context , logger slog. Logger , variables [] * tfconfig. Variable ) (map [string ]string , error ) {
250
+ // VariableDefaults returns the default values for all variables passed to it.
251
+ func ( p * Parser ) VariableDefaults ( ctx context.Context ) (map [string ]string , error ) {
228
252
// iterate through vars to get the default values for all
229
253
// variables.
230
254
m := make (map [string ]string )
231
- for _ , v := range variables {
255
+ for _ , v := range p . module . Variables {
232
256
if v == nil {
233
257
continue
234
258
}
@@ -238,15 +262,21 @@ func loadVarsDefaults(ctx context.Context, logger slog.Logger, variables []*tfco
238
262
}
239
263
m [v .Name ] = strings .Trim (sv , `"` )
240
264
}
241
- logger .Debug (ctx , "found default values for variables" , slog .F ("defaults" , m ))
265
+ p . logger .Debug (ctx , "found default values for variables" , slog .F ("defaults" , m ))
242
266
return m , nil
243
267
}
244
268
245
- // loadParamsDefaults returns the default values of all coder_parameter data sources data sources provided.
246
- func loadParamsDefaults (ctx context.Context , logger slog.Logger , parser Parser , dataSources []* tfconfig.Resource ) (map [string ]string , error ) {
269
+ // CoderParameterDefaults returns the default values of all coder_parameter data sources
270
+ // in the parsed module.
271
+ func (p * Parser ) CoderParameterDefaults (ctx context.Context ) (map [string ]string , error ) {
247
272
defaultsM := make (map [string ]string )
248
- var skipped []string
249
- for _ , dataResource := range dataSources {
273
+ var (
274
+ skipped []string
275
+ file * hcl.File
276
+ diags hcl.Diagnostics
277
+ )
278
+
279
+ for _ , dataResource := range p .module .DataResources {
250
280
if dataResource == nil {
251
281
continue
252
282
}
@@ -256,15 +286,13 @@ func loadParamsDefaults(ctx context.Context, logger slog.Logger, parser Parser,
256
286
continue
257
287
}
258
288
259
- var file * hcl.File
260
- var diags hcl.Diagnostics
261
-
262
289
if ! strings .HasSuffix (dataResource .Pos .Filename , ".tf" ) {
263
290
continue
264
291
}
265
292
266
293
// We know in which HCL file is the data resource defined.
267
- file , diags = parser .ParseHCLFile (dataResource .Pos .Filename )
294
+ // NOTE: hclparse.Parser will cache multiple successive calls to parse the same file.
295
+ file , diags = p .underlying .ParseHCLFile (dataResource .Pos .Filename )
268
296
if diags .HasErrors () {
269
297
return nil , xerrors .Errorf ("can't parse the resource file %q: %s" , dataResource .Pos .Filename , diags .Error ())
270
298
}
@@ -299,13 +327,13 @@ func loadParamsDefaults(ctx context.Context, logger slog.Logger, parser Parser,
299
327
}
300
328
}
301
329
}
302
- logger .Debug (ctx , "found default values for parameters" , slog .F ("defaults" , defaultsM ), slog .F ("skipped" , skipped ))
330
+ p . logger .Debug (ctx , "found default values for parameters" , slog .F ("defaults" , defaultsM ), slog .F ("skipped" , skipped ))
303
331
return defaultsM , nil
304
332
}
305
333
306
- // EvalProvisionerTags evaluates the given workspaceTags based on the given
334
+ // evaluateWorkspaceTags evaluates the given workspaceTags based on the given
307
335
// default values for variables and coder_parameter data sources.
308
- func EvalProvisionerTags (varsDefaults , paramsDefaults , workspaceTags map [string ]string ) (map [string ]string , error ) {
336
+ func evaluateWorkspaceTags (varsDefaults , paramsDefaults , workspaceTags map [string ]string ) (map [string ]string , error ) {
309
337
// Filter only allowed data sources for preflight check.
310
338
// This is not strictly required but provides a friendlier error.
311
339
if err := validWorkspaceTagValues (workspaceTags ); err != nil {
@@ -421,29 +449,6 @@ func previewFileContent(fileRange hcl.Range) (string, error) {
421
449
return string (fileRange .SliceBytes (body )), nil
422
450
}
423
451
424
- // LoadTerraformVariables extracts all Terraform variables from module and converts them
425
- // to template variables. The variables are sorted by source position.
426
- func LoadTerraformVariables (module * tfconfig.Module ) ([]* proto.TemplateVariable , error ) {
427
- // Sort variables by (filename, line) to make the ordering consistent
428
- variables := make ([]* tfconfig.Variable , 0 , len (module .Variables ))
429
- for _ , v := range module .Variables {
430
- variables = append (variables , v )
431
- }
432
- sort .Slice (variables , func (i , j int ) bool {
433
- return compareSourcePos (variables [i ].Pos , variables [j ].Pos )
434
- })
435
-
436
- var templateVariables []* proto.TemplateVariable
437
- for _ , v := range variables {
438
- mv , err := convertTerraformVariable (v )
439
- if err != nil {
440
- return nil , err
441
- }
442
- templateVariables = append (templateVariables , mv )
443
- }
444
- return templateVariables , nil
445
- }
446
-
447
452
// convertTerraformVariable converts a Terraform variable to a template-wide variable, processed by Coder.
448
453
func convertTerraformVariable (variable * tfconfig.Variable ) (* proto.TemplateVariable , error ) {
449
454
var defaultData string
0 commit comments