Skip to content

Commit 5ad4747

Browse files
chore(provisioner/terraform): extract terraform parsing logic to package tfparse (#15230)
Related to #15087 Extracts the logic for extracting variables and workspace tags to a separate package `tfparse`. --------- Co-authored-by: Danielle Maywood <danielle@themaywoods.com>
1 parent d9f1aaf commit 5ad4747

File tree

2 files changed

+186
-172
lines changed

2 files changed

+186
-172
lines changed

provisioner/terraform/parse.go

Lines changed: 4 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,15 @@
11
package terraform
22

33
import (
4-
"context"
5-
"encoding/json"
64
"fmt"
7-
"os"
85
"path/filepath"
9-
"slices"
10-
"sort"
116
"strings"
127

13-
"github.com/hashicorp/hcl/v2"
14-
"github.com/hashicorp/hcl/v2/hclparse"
15-
"github.com/hashicorp/hcl/v2/hclsyntax"
168
"github.com/hashicorp/terraform-config-inspect/tfconfig"
179
"github.com/mitchellh/go-wordwrap"
18-
"golang.org/x/xerrors"
1910

2011
"github.com/coder/coder/v2/coderd/tracing"
12+
"github.com/coder/coder/v2/provisioner/terraform/tfparse"
2113
"github.com/coder/coder/v2/provisionersdk"
2214
"github.com/coder/coder/v2/provisionersdk/proto"
2315
)
@@ -34,12 +26,12 @@ func (s *server) Parse(sess *provisionersdk.Session, _ *proto.ParseRequest, _ <-
3426
return provisionersdk.ParseErrorf("load module: %s", formatDiagnostics(sess.WorkDirectory, diags))
3527
}
3628

37-
workspaceTags, err := s.loadWorkspaceTags(ctx, module)
29+
workspaceTags, err := tfparse.WorkspaceTags(ctx, s.logger, module)
3830
if err != nil {
3931
return provisionersdk.ParseErrorf("can't load workspace tags: %v", err)
4032
}
4133

42-
templateVariables, err := loadTerraformVariables(module)
34+
templateVariables, err := tfparse.LoadTerraformVariables(module)
4335
if err != nil {
4436
return provisionersdk.ParseErrorf("can't load template variables: %v", err)
4537
}
@@ -50,160 +42,7 @@ func (s *server) Parse(sess *provisionersdk.Session, _ *proto.ParseRequest, _ <-
5042
}
5143
}
5244

53-
var rootTemplateSchema = &hcl.BodySchema{
54-
Blocks: []hcl.BlockHeaderSchema{
55-
{
56-
Type: "data",
57-
LabelNames: []string{"type", "name"},
58-
},
59-
},
60-
}
61-
62-
var coderWorkspaceTagsSchema = &hcl.BodySchema{
63-
Attributes: []hcl.AttributeSchema{
64-
{
65-
Name: "tags",
66-
},
67-
},
68-
}
69-
70-
func (s *server) loadWorkspaceTags(ctx context.Context, module *tfconfig.Module) (map[string]string, error) {
71-
workspaceTags := map[string]string{}
72-
73-
for _, dataResource := range module.DataResources {
74-
if dataResource.Type != "coder_workspace_tags" {
75-
s.logger.Debug(ctx, "skip resource as it is not a coder_workspace_tags", "resource_name", dataResource.Name, "resource_type", dataResource.Type)
76-
continue
77-
}
78-
79-
var file *hcl.File
80-
var diags hcl.Diagnostics
81-
parser := hclparse.NewParser()
82-
83-
if !strings.HasSuffix(dataResource.Pos.Filename, ".tf") {
84-
s.logger.Debug(ctx, "only .tf files can be parsed", "filename", dataResource.Pos.Filename)
85-
continue
86-
}
87-
// We know in which HCL file is the data resource defined.
88-
file, diags = parser.ParseHCLFile(dataResource.Pos.Filename)
89-
90-
if diags.HasErrors() {
91-
return nil, xerrors.Errorf("can't parse the resource file: %s", diags.Error())
92-
}
93-
94-
// Parse root to find "coder_workspace_tags".
95-
content, _, diags := file.Body.PartialContent(rootTemplateSchema)
96-
if diags.HasErrors() {
97-
return nil, xerrors.Errorf("can't parse the resource file: %s", diags.Error())
98-
}
99-
100-
// Iterate over blocks to locate the exact "coder_workspace_tags" data resource.
101-
for _, block := range content.Blocks {
102-
if !slices.Equal(block.Labels, []string{"coder_workspace_tags", dataResource.Name}) {
103-
continue
104-
}
105-
106-
// Parse "coder_workspace_tags" to find all key-value tags.
107-
resContent, _, diags := block.Body.PartialContent(coderWorkspaceTagsSchema)
108-
if diags.HasErrors() {
109-
return nil, xerrors.Errorf(`can't parse the resource coder_workspace_tags: %s`, diags.Error())
110-
}
111-
112-
if resContent == nil {
113-
continue // workspace tags are not present
114-
}
115-
116-
if _, ok := resContent.Attributes["tags"]; !ok {
117-
return nil, xerrors.Errorf(`"tags" attribute is required by coder_workspace_tags`)
118-
}
119-
120-
expr := resContent.Attributes["tags"].Expr
121-
tagsExpr, ok := expr.(*hclsyntax.ObjectConsExpr)
122-
if !ok {
123-
return nil, xerrors.Errorf(`"tags" attribute is expected to be a key-value map`)
124-
}
125-
126-
// Parse key-value entries in "coder_workspace_tags"
127-
for _, tagItem := range tagsExpr.Items {
128-
key, err := previewFileContent(tagItem.KeyExpr.Range())
129-
if err != nil {
130-
return nil, xerrors.Errorf("can't preview the resource file: %v", err)
131-
}
132-
key = strings.Trim(key, `"`)
133-
134-
value, err := previewFileContent(tagItem.ValueExpr.Range())
135-
if err != nil {
136-
return nil, xerrors.Errorf("can't preview the resource file: %v", err)
137-
}
138-
139-
s.logger.Info(ctx, "workspace tag found", "key", key, "value", value)
140-
141-
if _, ok := workspaceTags[key]; ok {
142-
return nil, xerrors.Errorf(`workspace tag "%s" is defined multiple times`, key)
143-
}
144-
workspaceTags[key] = value
145-
}
146-
}
147-
}
148-
return workspaceTags, nil
149-
}
150-
151-
func previewFileContent(fileRange hcl.Range) (string, error) {
152-
body, err := os.ReadFile(fileRange.Filename)
153-
if err != nil {
154-
return "", err
155-
}
156-
return string(fileRange.SliceBytes(body)), nil
157-
}
158-
159-
func loadTerraformVariables(module *tfconfig.Module) ([]*proto.TemplateVariable, error) {
160-
// Sort variables by (filename, line) to make the ordering consistent
161-
variables := make([]*tfconfig.Variable, 0, len(module.Variables))
162-
for _, v := range module.Variables {
163-
variables = append(variables, v)
164-
}
165-
sort.Slice(variables, func(i, j int) bool {
166-
return compareSourcePos(variables[i].Pos, variables[j].Pos)
167-
})
168-
169-
var templateVariables []*proto.TemplateVariable
170-
for _, v := range variables {
171-
mv, err := convertTerraformVariable(v)
172-
if err != nil {
173-
return nil, err
174-
}
175-
templateVariables = append(templateVariables, mv)
176-
}
177-
return templateVariables, nil
178-
}
179-
180-
// Converts a Terraform variable to a template-wide variable, processed by Coder.
181-
func convertTerraformVariable(variable *tfconfig.Variable) (*proto.TemplateVariable, error) {
182-
var defaultData string
183-
if variable.Default != nil {
184-
var valid bool
185-
defaultData, valid = variable.Default.(string)
186-
if !valid {
187-
defaultDataRaw, err := json.Marshal(variable.Default)
188-
if err != nil {
189-
return nil, xerrors.Errorf("parse variable %q default: %w", variable.Name, err)
190-
}
191-
defaultData = string(defaultDataRaw)
192-
}
193-
}
194-
195-
return &proto.TemplateVariable{
196-
Name: variable.Name,
197-
Description: variable.Description,
198-
Type: variable.Type,
199-
DefaultValue: defaultData,
200-
// variable.Required is always false. Empty string is a valid default value, so it doesn't enforce required to be "true".
201-
Required: variable.Default == nil,
202-
Sensitive: variable.Sensitive,
203-
}, nil
204-
}
205-
206-
// formatDiagnostics returns a nicely formatted string containing all of the
45+
// FormatDiagnostics returns a nicely formatted string containing all of the
20746
// error details within the tfconfig.Diagnostics. We need to use this because
20847
// the default format doesn't provide much useful information.
20948
func formatDiagnostics(baseDir string, diags tfconfig.Diagnostics) string {
@@ -246,10 +85,3 @@ func formatDiagnostics(baseDir string, diags tfconfig.Diagnostics) string {
24685

24786
return spacer + strings.TrimSpace(msgs.String())
24887
}
249-
250-
func compareSourcePos(x, y tfconfig.SourcePos) bool {
251-
if x.Filename != y.Filename {
252-
return x.Filename < y.Filename
253-
}
254-
return x.Line < y.Line
255-
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
package tfparse
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"os"
7+
"slices"
8+
"sort"
9+
"strings"
10+
11+
"github.com/coder/coder/v2/provisionersdk/proto"
12+
13+
"github.com/hashicorp/hcl/v2"
14+
"github.com/hashicorp/hcl/v2/hclparse"
15+
"github.com/hashicorp/hcl/v2/hclsyntax"
16+
"github.com/hashicorp/terraform-config-inspect/tfconfig"
17+
"golang.org/x/xerrors"
18+
19+
"cdr.dev/slog"
20+
)
21+
22+
// WorkspaceTags extracts tags from coder_workspace_tags data sources defined in module.
23+
func WorkspaceTags(ctx context.Context, logger slog.Logger, module *tfconfig.Module) (map[string]string, error) {
24+
workspaceTags := map[string]string{}
25+
26+
for _, dataResource := range module.DataResources {
27+
if dataResource.Type != "coder_workspace_tags" {
28+
logger.Debug(ctx, "skip resource as it is not a coder_workspace_tags", "resource_name", dataResource.Name, "resource_type", dataResource.Type)
29+
continue
30+
}
31+
32+
var file *hcl.File
33+
var diags hcl.Diagnostics
34+
parser := hclparse.NewParser()
35+
36+
if !strings.HasSuffix(dataResource.Pos.Filename, ".tf") {
37+
logger.Debug(ctx, "only .tf files can be parsed", "filename", dataResource.Pos.Filename)
38+
continue
39+
}
40+
// We know in which HCL file is the data resource defined.
41+
file, diags = parser.ParseHCLFile(dataResource.Pos.Filename)
42+
if diags.HasErrors() {
43+
return nil, xerrors.Errorf("can't parse the resource file: %s", diags.Error())
44+
}
45+
46+
// Parse root to find "coder_workspace_tags".
47+
content, _, diags := file.Body.PartialContent(rootTemplateSchema)
48+
if diags.HasErrors() {
49+
return nil, xerrors.Errorf("can't parse the resource file: %s", diags.Error())
50+
}
51+
52+
// Iterate over blocks to locate the exact "coder_workspace_tags" data resource.
53+
for _, block := range content.Blocks {
54+
if !slices.Equal(block.Labels, []string{"coder_workspace_tags", dataResource.Name}) {
55+
continue
56+
}
57+
58+
// Parse "coder_workspace_tags" to find all key-value tags.
59+
resContent, _, diags := block.Body.PartialContent(coderWorkspaceTagsSchema)
60+
if diags.HasErrors() {
61+
return nil, xerrors.Errorf(`can't parse the resource coder_workspace_tags: %s`, diags.Error())
62+
}
63+
64+
if resContent == nil {
65+
continue // workspace tags are not present
66+
}
67+
68+
if _, ok := resContent.Attributes["tags"]; !ok {
69+
return nil, xerrors.Errorf(`"tags" attribute is required by coder_workspace_tags`)
70+
}
71+
72+
expr := resContent.Attributes["tags"].Expr
73+
tagsExpr, ok := expr.(*hclsyntax.ObjectConsExpr)
74+
if !ok {
75+
return nil, xerrors.Errorf(`"tags" attribute is expected to be a key-value map`)
76+
}
77+
78+
// Parse key-value entries in "coder_workspace_tags"
79+
for _, tagItem := range tagsExpr.Items {
80+
key, err := previewFileContent(tagItem.KeyExpr.Range())
81+
if err != nil {
82+
return nil, xerrors.Errorf("can't preview the resource file: %v", err)
83+
}
84+
key = strings.Trim(key, `"`)
85+
86+
value, err := previewFileContent(tagItem.ValueExpr.Range())
87+
if err != nil {
88+
return nil, xerrors.Errorf("can't preview the resource file: %v", err)
89+
}
90+
91+
logger.Info(ctx, "workspace tag found", "key", key, "value", value)
92+
93+
if _, ok := workspaceTags[key]; ok {
94+
return nil, xerrors.Errorf(`workspace tag %q is defined multiple times`, key)
95+
}
96+
workspaceTags[key] = value
97+
}
98+
}
99+
}
100+
return workspaceTags, nil
101+
}
102+
103+
var rootTemplateSchema = &hcl.BodySchema{
104+
Blocks: []hcl.BlockHeaderSchema{
105+
{
106+
Type: "data",
107+
LabelNames: []string{"type", "name"},
108+
},
109+
},
110+
}
111+
112+
var coderWorkspaceTagsSchema = &hcl.BodySchema{
113+
Attributes: []hcl.AttributeSchema{
114+
{
115+
Name: "tags",
116+
},
117+
},
118+
}
119+
120+
func previewFileContent(fileRange hcl.Range) (string, error) {
121+
body, err := os.ReadFile(fileRange.Filename)
122+
if err != nil {
123+
return "", err
124+
}
125+
return string(fileRange.SliceBytes(body)), nil
126+
}
127+
128+
// LoadTerraformVariables extracts all Terraform variables from module and converts them
129+
// to template variables. The variables are sorted by source position.
130+
func LoadTerraformVariables(module *tfconfig.Module) ([]*proto.TemplateVariable, error) {
131+
// Sort variables by (filename, line) to make the ordering consistent
132+
variables := make([]*tfconfig.Variable, 0, len(module.Variables))
133+
for _, v := range module.Variables {
134+
variables = append(variables, v)
135+
}
136+
sort.Slice(variables, func(i, j int) bool {
137+
return compareSourcePos(variables[i].Pos, variables[j].Pos)
138+
})
139+
140+
var templateVariables []*proto.TemplateVariable
141+
for _, v := range variables {
142+
mv, err := convertTerraformVariable(v)
143+
if err != nil {
144+
return nil, err
145+
}
146+
templateVariables = append(templateVariables, mv)
147+
}
148+
return templateVariables, nil
149+
}
150+
151+
// convertTerraformVariable converts a Terraform variable to a template-wide variable, processed by Coder.
152+
func convertTerraformVariable(variable *tfconfig.Variable) (*proto.TemplateVariable, error) {
153+
var defaultData string
154+
if variable.Default != nil {
155+
var valid bool
156+
defaultData, valid = variable.Default.(string)
157+
if !valid {
158+
defaultDataRaw, err := json.Marshal(variable.Default)
159+
if err != nil {
160+
return nil, xerrors.Errorf("parse variable %q default: %w", variable.Name, err)
161+
}
162+
defaultData = string(defaultDataRaw)
163+
}
164+
}
165+
166+
return &proto.TemplateVariable{
167+
Name: variable.Name,
168+
Description: variable.Description,
169+
Type: variable.Type,
170+
DefaultValue: defaultData,
171+
// variable.Required is always false. Empty string is a valid default value, so it doesn't enforce required to be "true".
172+
Required: variable.Default == nil,
173+
Sensitive: variable.Sensitive,
174+
}, nil
175+
}
176+
177+
func compareSourcePos(x, y tfconfig.SourcePos) bool {
178+
if x.Filename != y.Filename {
179+
return x.Filename < y.Filename
180+
}
181+
return x.Line < y.Line
182+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy