Skip to content

Commit 4ba2589

Browse files
committed
refactor
1 parent 3db3e6b commit 4ba2589

File tree

3 files changed

+139
-113
lines changed

3 files changed

+139
-113
lines changed

provisioner/terraform/parse.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,21 @@ func (s *server) Parse(sess *provisionersdk.Session, _ *proto.ParseRequest, _ <-
2121
defer span.End()
2222

2323
// Load the module and print any parse errors.
24-
module, diags := tfconfig.LoadModule(sess.WorkDirectory)
24+
// module, diags := tfconfig.LoadModule(sess.WorkDirectory)
25+
// if diags.HasErrors() {
26+
// }
27+
28+
parser, diags := tfparse.New(sess.WorkDirectory, tfparse.WithLogger(s.logger.Named("tfparse")))
2529
if diags.HasErrors() {
2630
return provisionersdk.ParseErrorf("load module: %s", formatDiagnostics(sess.WorkDirectory, diags))
2731
}
2832

29-
workspaceTags, err := tfparse.WorkspaceTags(ctx, s.logger, nil, module)
33+
workspaceTags, err := parser.WorkspaceTags(ctx)
3034
if err != nil {
3135
return provisionersdk.ParseErrorf("can't load workspace tags: %v", err)
3236
}
3337

34-
templateVariables, err := tfparse.LoadTerraformVariables(module)
38+
templateVariables, err := parser.TemplateVariables()
3539
if err != nil {
3640
return provisionersdk.ParseErrorf("can't load template variables: %v", err)
3741
}

provisioner/terraform/tfparse/tfextract.go

Lines changed: 111 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -31,25 +31,60 @@ import (
3131
// introducing a circular dependency
3232
const maxFileSizeBytes = 10 * (10 << 20) // 10 MB
3333

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 {
3938
ParseHCLFile(filename string) (*hcl.File, hcl.Diagnostics)
4039
}
4140

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
4977
}
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) {
5085
tags := map[string]string{}
5186
var skipped []string
52-
for _, dataResource := range module.DataResources {
87+
for _, dataResource := range p.module.DataResources {
5388
if dataResource.Type != "coder_workspace_tags" {
5489
skipped = append(skipped, strings.Join([]string{"data", dataResource.Type, dataResource.Name}, "."))
5590
continue
@@ -62,7 +97,7 @@ func WorkspaceTags(ctx context.Context, logger slog.Logger, parser Parser, modul
6297
continue
6398
}
6499
// 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)
66101
if diags.HasErrors() {
67102
return nil, xerrors.Errorf("can't parse the resource file: %s", diags.Error())
68103
}
@@ -119,54 +154,33 @@ func WorkspaceTags(ctx context.Context, logger slog.Logger, parser Parser, modul
119154
}
120155
}
121156
}
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))
123158
return tags, nil
124159
}
125160

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) {
148162
// This only gets us the expressions. We need to evaluate them.
149163
// Example: var.region -> "us"
150-
tags, err = WorkspaceTags(ctx, logger, parser, module)
164+
tags, err := p.WorkspaceTags(ctx)
151165
if err != nil {
152166
return nil, xerrors.Errorf("extract workspace tags: %w", err)
153167
}
154168

155169
// To evaluate the expressions, we need to load the default values for
156170
// variables and parameters.
157-
varsDefaults, err := loadVarsDefaults(ctx, logger, maps.Values(module.Variables))
171+
varsDefaults, err := p.VariableDefaults(ctx)
158172
if err != nil {
159173
return nil, xerrors.Errorf("load variable defaults: %w", err)
160174
}
161-
paramsDefaults, err := loadParamsDefaults(ctx, logger, parser, maps.Values(module.DataResources))
175+
paramsDefaults, err := p.CoderParameterDefaults(ctx)
162176
if err != nil {
163177
return nil, xerrors.Errorf("load parameter defaults: %w", err)
164178
}
165179

166180
// Evaluate the tags expressions given the inputs.
167181
// This will resolve any variables or parameters to their default
168182
// values.
169-
evalTags, err := EvalProvisionerTags(varsDefaults, paramsDefaults, tags)
183+
evalTags, err := evaluateWorkspaceTags(varsDefaults, paramsDefaults, tags)
170184
if err != nil {
171185
return nil, xerrors.Errorf("eval provisioner tags: %w", err)
172186
}
@@ -181,54 +195,64 @@ func WorkspaceTagDefaultsFromFile(ctx context.Context, logger slog.Logger, file
181195
return evalTags, nil
182196
}
183197

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)
190205
}
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)
193217
}
218+
return templateVariables, nil
219+
}
194220

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!
196226
var rdr io.Reader
197227
switch mimetype {
198228
case "application/x-tar":
199-
rdr = bytes.NewReader(file)
229+
rdr = bytes.NewReader(bs)
200230
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)
208237
}
209-
rdr = bytes.NewReader(tarBytes)
210238
default:
211-
return nil, cleanup, xerrors.Errorf("unsupported mimetype: %s", mimetype)
239+
return xerrors.Errorf("unsupported mimetype: %s", mimetype)
212240
}
213241

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)
221245
}
222246

223-
return module, cleanup, nil
247+
return nil
224248
}
225249

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) {
228252
// iterate through vars to get the default values for all
229253
// variables.
230254
m := make(map[string]string)
231-
for _, v := range variables {
255+
for _, v := range p.module.Variables {
232256
if v == nil {
233257
continue
234258
}
@@ -238,15 +262,21 @@ func loadVarsDefaults(ctx context.Context, logger slog.Logger, variables []*tfco
238262
}
239263
m[v.Name] = strings.Trim(sv, `"`)
240264
}
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))
242266
return m, nil
243267
}
244268

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) {
247272
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 {
250280
if dataResource == nil {
251281
continue
252282
}
@@ -256,15 +286,13 @@ func loadParamsDefaults(ctx context.Context, logger slog.Logger, parser Parser,
256286
continue
257287
}
258288

259-
var file *hcl.File
260-
var diags hcl.Diagnostics
261-
262289
if !strings.HasSuffix(dataResource.Pos.Filename, ".tf") {
263290
continue
264291
}
265292

266293
// 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)
268296
if diags.HasErrors() {
269297
return nil, xerrors.Errorf("can't parse the resource file %q: %s", dataResource.Pos.Filename, diags.Error())
270298
}
@@ -299,13 +327,13 @@ func loadParamsDefaults(ctx context.Context, logger slog.Logger, parser Parser,
299327
}
300328
}
301329
}
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))
303331
return defaultsM, nil
304332
}
305333

306-
// EvalProvisionerTags evaluates the given workspaceTags based on the given
334+
// evaluateWorkspaceTags evaluates the given workspaceTags based on the given
307335
// 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) {
309337
// Filter only allowed data sources for preflight check.
310338
// This is not strictly required but provides a friendlier error.
311339
if err := validWorkspaceTagValues(workspaceTags); err != nil {
@@ -421,29 +449,6 @@ func previewFileContent(fileRange hcl.Range) (string, error) {
421449
return string(fileRange.SliceBytes(body)), nil
422450
}
423451

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-
447452
// convertTerraformVariable converts a Terraform variable to a template-wide variable, processed by Coder.
448453
func convertTerraformVariable(variable *tfconfig.Variable) (*proto.TemplateVariable, error) {
449454
var defaultData string

0 commit comments

Comments
 (0)