1
1
package terraform
2
2
3
3
import (
4
- "context"
5
- "encoding/json"
6
4
"fmt"
7
- "os"
8
5
"path/filepath"
9
- "slices"
10
- "sort"
11
6
"strings"
12
7
13
- "github.com/hashicorp/hcl/v2"
14
- "github.com/hashicorp/hcl/v2/hclparse"
15
- "github.com/hashicorp/hcl/v2/hclsyntax"
16
8
"github.com/hashicorp/terraform-config-inspect/tfconfig"
17
9
"github.com/mitchellh/go-wordwrap"
18
- "golang.org/x/xerrors"
19
10
20
11
"github.com/coder/coder/v2/coderd/tracing"
12
+ "github.com/coder/coder/v2/provisioner/terraform/tfparse"
21
13
"github.com/coder/coder/v2/provisionersdk"
22
14
"github.com/coder/coder/v2/provisionersdk/proto"
23
15
)
@@ -34,12 +26,12 @@ func (s *server) Parse(sess *provisionersdk.Session, _ *proto.ParseRequest, _ <-
34
26
return provisionersdk .ParseErrorf ("load module: %s" , formatDiagnostics (sess .WorkDirectory , diags ))
35
27
}
36
28
37
- workspaceTags , err := s . loadWorkspaceTags (ctx , module )
29
+ workspaceTags , err := tfparse . WorkspaceTags (ctx , s . logger , module )
38
30
if err != nil {
39
31
return provisionersdk .ParseErrorf ("can't load workspace tags: %v" , err )
40
32
}
41
33
42
- templateVariables , err := loadTerraformVariables (module )
34
+ templateVariables , err := tfparse . LoadTerraformVariables (module )
43
35
if err != nil {
44
36
return provisionersdk .ParseErrorf ("can't load template variables: %v" , err )
45
37
}
@@ -50,160 +42,7 @@ func (s *server) Parse(sess *provisionersdk.Session, _ *proto.ParseRequest, _ <-
50
42
}
51
43
}
52
44
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
207
46
// error details within the tfconfig.Diagnostics. We need to use this because
208
47
// the default format doesn't provide much useful information.
209
48
func formatDiagnostics (baseDir string , diags tfconfig.Diagnostics ) string {
@@ -246,10 +85,3 @@ func formatDiagnostics(baseDir string, diags tfconfig.Diagnostics) string {
246
85
247
86
return spacer + strings .TrimSpace (msgs .String ())
248
87
}
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
- }
0 commit comments