Thanks to visit codestin.com
Credit goes to github.com

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 <[email protected]>
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

+4-172
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-
}
+182
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)