-
Notifications
You must be signed in to change notification settings - Fork 38
/
parser.go
540 lines (451 loc) · 15 KB
/
parser.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
package parser
import (
"context"
"errors"
"fmt"
"net/url"
"reflect"
"strings"
hcl "github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
tfaddr "github.com/hashicorp/terraform-registry-address"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
core "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/sets"
"github.com/seal-io/walrus/pkg/dao"
"github.com/seal-io/walrus/pkg/dao/model"
"github.com/seal-io/walrus/pkg/dao/model/resourcestate"
"github.com/seal-io/walrus/pkg/dao/types"
"github.com/seal-io/walrus/pkg/dao/types/object"
"github.com/seal-io/walrus/pkg/templates/translator"
"github.com/seal-io/walrus/utils/json"
"github.com/seal-io/walrus/utils/log"
"github.com/seal-io/walrus/utils/strs"
)
// ConnectorSeparator is used to separate the connector id and the instance name.
const ConnectorSeparator = "connector--"
// The type terraform data implements the standard resource lifecycle,
// but does not directly take any other actions.
// Resource components should skip the data type.
const TerraformTypeData = "terraform_data"
type StateParser struct{}
// GetComponentsAndExtractDependencies returns the components and dependency components after parse the resource state.
//
// GetComponentsAndExtractDependencies returns list must not be `nil` unless unexpected input or raising error,
// it can be used to clean stale items safety if got an empty list.
func (StateParser) GetComponentsAndExtractDependencies(
ctx context.Context,
mc model.ClientSet,
run *model.ResourceRun,
) (
components model.ResourceComponents,
dependencies map[string][]string,
err error,
) {
logger := log.WithName("deployer").WithName("tf").WithName("parser")
dependencies = make(map[string][]string)
s, err := mc.ResourceStates().Query().
Where(resourcestate.ResourceID(run.ResourceID)).
Only(ctx)
if err != nil {
return nil, nil, err
}
var runState state
if err := json.Unmarshal([]byte(s.Data), &runState); err != nil {
return nil, nil, err
}
var (
// Maps components unique index to its dependencies components unique indexes.
componentDependencies = make(map[string][]string)
// Maps terraform module key to resource.
moduleComponentMap = make(map[string]*model.ResourceComponent)
key = dao.ResourceComponentGetUniqueKey
)
for _, rs := range runState.Resources {
if rs.Type == TerraformTypeData {
continue
}
switch rs.Mode {
default:
logger.Errorf("unknown resource mode: %s", rs.Mode)
continue
case types.ResourceComponentModeManaged, types.ResourceComponentModeData:
}
// Try to get the connectorID id from the provider.
connectorID, err := ParseInstanceProviderConnector(rs.Provider)
if err != nil {
logger.Errorf("invalid provider format: %s", rs.Provider)
continue
}
if connectorID == "" {
logger.Warnf("connector is empty, provider: %v", rs.Provider)
continue
}
classResourceComponents := &model.ResourceComponent{
ProjectID: run.ProjectID,
EnvironmentID: run.EnvironmentID,
ResourceID: run.ResourceID,
ConnectorID: object.ID(connectorID),
Mode: rs.Mode,
Type: rs.Type,
Name: rs.Name,
DeployerType: run.DeployerType,
Shape: types.ResourceComponentShapeClass,
}
classResourceComponents.Edges.Instances = make(model.ResourceComponents, len(rs.Instances))
// The module key is used to identify the terraform resource module.
mk := strs.Join(".", rs.Module, rs.Type, rs.Name)
if rs.Mode == types.ResourceComponentModeData {
mk = strs.Join(".", rs.Module, rs.Mode, rs.Type, rs.Name)
}
moduleComponentMap[mk] = classResourceComponents
for i, is := range rs.Instances {
instanceID, err := ParseInstanceID(rs, is)
if err != nil {
logger.Errorf("parse instance id failed: %v, instance: %v",
err, is)
continue
}
if instanceID == "" {
logger.Errorf("instance id is empty, instance: %v", is)
continue
}
// The index key is used to identify the terraform resource instance.
indexKey, err := ParseIndexKey(rs, is)
if err != nil {
logger.Errorf("parse index key failed: %v, instance: %v", err, is)
continue
}
// FIXME(thxCode): as a good solution,
// the https://registry.terraform.io/providers/hashicorp/helm should provide a complete ID.
if rs.Type == "helm_release" && !strings.Contains(instanceID, "/") {
// NB(thxCode): the ID of helm_release resource doesn't include namespace,
// so we can't fetch the real Helm Release record that under specified namespace.
// In order to recognize the real Helm Release record,
// we should enrich the instanceID with the namespace name.
md, err := ParseInstanceMetadata(is)
if err != nil {
logger.Errorf("parse instance metadata failed: %v, instance attributes: %s",
err, string(is.Attributes))
continue
}
if nsr := json.Get(md, "namespace"); nsr.String() != "" {
instanceID = nsr.String() + "/" + instanceID
} else {
instanceID = core.NamespaceDefault + "/" + instanceID
}
}
name, err := url.QueryUnescape(instanceID)
if err != nil {
name = instanceID
logger.Errorf("unescape instance id failed: %v, instance id: %s", err, instanceID)
}
instanceResource := &model.ResourceComponent{
ProjectID: run.ProjectID,
EnvironmentID: run.EnvironmentID,
ResourceID: run.ResourceID,
ConnectorID: object.ID(connectorID),
Mode: rs.Mode,
Type: rs.Type,
Name: name,
Shape: types.ResourceComponentShapeInstance,
DeployerType: run.DeployerType,
IndexKey: indexKey,
}
// Assume that the first instance's dependencies are the dependencies of the class resource.
if _, ok := moduleComponentMap[key(classResourceComponents)]; !ok {
componentDependencies[key(classResourceComponents)] = is.Dependencies
}
dependencies[key(instanceResource)] = append(
dependencies[key(instanceResource)],
key(classResourceComponents),
)
classResourceComponents.Edges.Instances[i] = instanceResource
componentDependencies[key(instanceResource)] = is.Dependencies
}
components = append(components, classResourceComponents)
}
// Get resource dependencies.
for k, v := range componentDependencies {
for _, d := range v {
moduleResource, ok := moduleComponentMap[d]
if !ok {
logger.Warnf("dependency resource not found, module key: %s", d)
continue
}
dependencies[k] = append(dependencies[k], key(moduleResource))
}
}
return components, dependencies, nil
}
// GetOutputMap returns the original outputs after parsed the resource run output(terraform state).
//
// Since we mutate the output names before executing a terraform deployment,
// the output's name(hcl label) is not the same as the original one defined on the terraform template.
//
// This function is used for bridging the referring between multiple (walrus)resources.
// Use GetOriginalOutputsMap if wanna the original outputs.
func (StateParser) GetOutputMap(stateData string) (map[string]types.OutputValue, error) {
if len(stateData) == 0 {
return nil, nil
}
// Get outputs from state, expected format:
// {
// "outputs": {}
// }.
r := json.Get(strs.ToBytes(&stateData), "outputs")
if !r.Exists() || !r.IsObject() {
return map[string]types.OutputValue{}, nil
}
var osm map[string]types.OutputValue
if err := json.Unmarshal(strs.ToBytes(&r.Raw), &osm); err != nil {
return nil, err
}
return osm, nil
}
// GetOriginalOutputs returns the original outputs after parsed the resource run output(terraform state).
//
// The given run must carry the resource on the edges, especially the resource's name.
//
// This function returns the original outputs,
// which means the output's name(hcl label) is the same as the original one defined on the terraform template.
func (p StateParser) GetOriginalOutputs(stateData, resourceName string) ([]types.OutputValue, error) {
osm, err := p.GetOutputMap(stateData)
if err != nil {
return nil, err
}
var (
prefix = resourceName + "_"
oss = make([]types.OutputValue, 0, len(osm))
count int
)
for _, mn := range sets.StringKeySet(osm).List() {
// E.g. `n` is in the form of `{resource name}_{output name}`.
n := strings.TrimPrefix(mn, prefix)
if n == mn {
continue
}
o := osm[mn]
count++
s := translator.SchemaOfType(
o.Type,
translator.Options{
Name: n,
Sensitive: o.Sensitive,
Order: count,
})
v := o.Value
if o.Sensitive {
v = []byte(`"<sensitive>"`)
}
oss = append(oss, types.OutputValue{
Name: n,
Value: v,
Type: o.Type,
Schema: s,
})
}
return oss, nil
}
// GetOriginalOutputsMap is similar to GetOriginalOutputs,
// but returns the original outputs in map form.
func (p StateParser) GetOriginalOutputsMap(stateData, resourceName string) (map[string]types.OutputValue, error) {
oss, err := p.GetOriginalOutputs(stateData, resourceName)
if err != nil {
return nil, err
}
osm := make(map[string]types.OutputValue, len(oss))
for i := range oss {
osm[oss[i].Name] = oss[i]
}
return osm, nil
}
type Provider = tfaddr.Provider
// AbsProviderConfig is the absolute address of a provider configuration
// within a particular module instance.
type AbsProviderConfig struct {
Provider Provider
Alias string
}
// ParseInstanceProviderConnector get the provider connector from the provider instance string.
func ParseInstanceProviderConnector(providerString string) (string, error) {
providerConfig, err := ParseAbsProviderString(providerString)
if err != nil {
return "", err
}
if providerConfig.Alias == "" {
return "", nil
}
providers := strings.Split(providerConfig.Alias, ConnectorSeparator)
if len(providers) != 2 {
return "", fmt.Errorf("provider name error: %s", providerString)
}
return providers[1], nil
}
// ParseInstanceID get the real instance id from the instance object state.
// The instance id is stored in the "name" attribute of resource component.
func ParseInstanceID(rs resourceState, is instanceObjectState) (string, error) {
if is.Attributes != nil {
ty, err := ctyjson.ImpliedType(is.Attributes)
if err != nil {
return "", err
}
val, err := ctyjson.Unmarshal(is.Attributes, ty)
if err != nil {
return "", err
}
for key, value := range val.AsValueMap() {
if key == "id" {
if value.IsNull() {
return "", nil
}
switch value.Type() {
case cty.String:
return value.AsString(), nil
case cty.Number:
return value.AsBigFloat().String(), nil
default:
return "", fmt.Errorf("unsupported type for id: %s, value: %s", value, value.Type().FriendlyName())
}
}
}
}
if is.AttributesFlat != nil {
if id, ok := is.AttributesFlat["id"]; ok {
return id, nil
}
}
return ParseIndexKey(rs, is)
}
// ParseInstanceMetadata get the metadata from the instance object state.
func ParseInstanceMetadata(is instanceObjectState) ([]byte, error) {
if is.Attributes == nil {
return nil, errors.New("no attributes")
}
arr := json.Get(is.Attributes, "metadata").Array()
switch l := len(arr); {
case l == 0:
return nil, errors.New("not found metadata")
case l > 1:
return nil, errors.New("not singular metadata")
}
if !arr[0].IsObject() {
return nil, errors.New("metadata is not an object")
}
return strs.ToBytes(&arr[0].Raw), nil
}
// ParseStateProviders parse terraform state and get providers.
func ParseStateProviders(s string) ([]string, error) {
if s == "" {
return nil, nil
}
providers := sets.NewString()
var runState state
if err := json.Unmarshal([]byte(s), &runState); err != nil {
return nil, err
}
for _, resource := range runState.Resources {
pAddr, err := ParseAbsProviderString(resource.Provider)
if err != nil {
return nil, err
}
providers.Insert(pAddr.Provider.Type)
}
return providers.List(), nil
}
func parseAbsProvider(traversal hcl.Traversal) (hcl.Traversal, error) {
remain := traversal
for len(remain) > 0 {
var next string
switch tt := remain[0].(type) {
case hcl.TraverseRoot:
next = tt.Name
case hcl.TraverseAttr:
next = tt.Name
case hcl.TraverseIndex:
return nil, errors.New("provider address cannot contain module indexes")
}
if next != "provider" {
remain = remain[1:]
continue
}
var retRemain hcl.Traversal
if len(remain) > 0 {
retRemain = make(hcl.Traversal, len(remain))
copy(retRemain, remain)
if tt, ok := retRemain[0].(hcl.TraverseAttr); ok {
retRemain[0] = hcl.TraverseRoot{
Name: tt.Name,
SrcRange: tt.SrcRange,
}
}
return retRemain, nil
}
}
return nil, fmt.Errorf("invalid provider configuration address %q", traversal)
}
// ParseAbsProviderConfig parses the given traversal as an absolute provider configuration address.
func ParseAbsProviderConfig(traversal hcl.Traversal) (*AbsProviderConfig, error) {
remain, err := parseAbsProvider(traversal)
if err != nil {
return nil, err
}
if len(remain) < 2 || remain.RootName() != "provider" {
return nil, errors.New("provider address must begin with \"provider.\", followed by a provider type name")
}
if len(remain) > 3 {
return nil, errors.New("extraneous operators after provider configuration alias")
}
ret := &AbsProviderConfig{}
if tt, ok := remain[1].(hcl.TraverseIndex); ok {
if !tt.Key.Type().Equals(cty.String) {
return nil, errors.New("the prefix \"provider.\" must be followed by a provider type name")
}
p, err := tfaddr.ParseProviderSource(tt.Key.AsString())
if err != nil {
return nil, err
}
ret.Provider = p
} else {
return nil, errors.New("the prefix \"provider.\" must be followed by a provider type name")
}
if len(remain) == 3 {
if tt, ok := remain[2].(hcl.TraverseAttr); ok {
ret.Alias = tt.Name
} else {
return nil, errors.New("provider type name must be followed by a configuration alias name")
}
}
return ret, nil
}
func ParseAbsProviderString(str string) (*AbsProviderConfig, error) {
traversal, diags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1})
if diags.HasErrors() {
return nil, fmt.Errorf("invalid provider configuration address %s", str)
}
ret, err := ParseAbsProviderConfig(traversal)
if err != nil {
return nil, fmt.Errorf("invalid provider configuration address %q: %w", str, err)
}
return ret, nil
}
// ParseIndexKey parse the index key from the instance object state.
// The index key is used to identify the terraform resource instance, e.g. `helm_release.foo[0]`.
func ParseIndexKey(rs resourceState, is instanceObjectState) (string, error) {
logger := log.WithName("deployer").WithName("tf").WithName("parser")
if rs.Type == "" || rs.Name == "" {
return "", errors.New("resource type or name is empty")
}
if is.IndexKey != nil {
switch reflect.TypeOf(is.IndexKey).Kind() {
case reflect.String:
return strs.Join(".", rs.Module, rs.Type, rs.Name) + fmt.Sprintf("[\"%v\"]", is.IndexKey), nil
case reflect.Int, reflect.Float64:
return strs.Join(".", rs.Module, rs.Type, rs.Name) + fmt.Sprintf("[%v]", is.IndexKey), nil
default:
logger.Warnf("unsupported index key: %v", is.IndexKey)
}
}
return strs.Join(".", rs.Module, rs.Type, rs.Name), nil
}