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
- }