-
Notifications
You must be signed in to change notification settings - Fork 394
/
output.go
154 lines (136 loc) · 4.25 KB
/
output.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
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package addon
import (
"errors"
"fmt"
"strings"
"gopkg.in/yaml.v3"
)
// AWS CloudFormation resource types.
const (
secretManagerSecretType = "AWS::SecretsManager::Secret"
iamManagedPolicyType = "AWS::IAM::ManagedPolicy"
securityGroupType = "AWS::EC2::SecurityGroup"
)
// Output represents an output from a CloudFormation template.
type Output struct {
// Name is the Logical ID of the output.
Name string
// IsSecret is true if the output value refers to a SecretsManager ARN. Otherwise, false.
IsSecret bool
// IsManagedPolicy is true if the output value refers to an IAM ManagedPolicy ARN. Otherwise, false.
IsManagedPolicy bool
// SecurityGroup is true if the output value refers a SecurityGroup ARN. Otherwise, false.
IsSecurityGroup bool
}
// Outputs parses the Outputs section of a CloudFormation template to extract logical IDs and returns them.
func Outputs(template string) ([]Output, error) {
type cfnTemplate struct {
Resources yaml.Node `yaml:"Resources"`
Outputs yaml.Node `yaml:"Outputs"`
}
var tpl cfnTemplate
if err := yaml.Unmarshal([]byte(template), &tpl); err != nil {
return nil, fmt.Errorf("unmarshal addon cloudformation template: %w", err)
}
typeFor, err := parseTypeByLogicalID(&tpl.Resources)
if err != nil {
return nil, err
}
outputNodes, err := parseOutputNodes(&tpl.Outputs)
if err != nil {
return nil, err
}
var outputs []Output
for _, outputNode := range outputNodes {
output := Output{
Name: outputNode.name(),
IsSecret: false,
IsManagedPolicy: false,
IsSecurityGroup: false,
}
ref, ok := outputNode.ref()
if ok {
output.IsSecret = typeFor[ref] == secretManagerSecretType
output.IsManagedPolicy = typeFor[ref] == iamManagedPolicyType
output.IsSecurityGroup = typeFor[ref] == securityGroupType
}
outputs = append(outputs, output)
}
return outputs, nil
}
// parseTypeByLogicalID returns a map where the key is the resource's logical ID and the value is the CloudFormation Type
// of the resource such as "AWS::IAM::Role".
func parseTypeByLogicalID(resourcesNode *yaml.Node) (typeFor map[string]string, err error) {
if resourcesNode.Kind != yaml.MappingNode {
// "Resources" is a required field in CloudFormation, check if it's defined as a map.
return nil, errors.New(`"Resources" field in cloudformation template is not a map`)
}
typeFor = make(map[string]string)
for _, content := range mappingContents(resourcesNode) {
logicalIDNode := content.keyNode
fieldsNode := content.valueNode
fields := struct {
Type string `yaml:"Type"`
}{}
if err := fieldsNode.Decode(&fields); err != nil {
return nil, fmt.Errorf(`decode the "Type" field of resource "%s": %w`, logicalIDNode.Value, err)
}
typeFor[logicalIDNode.Value] = fields.Type
}
return typeFor, nil
}
func parseOutputNodes(outputsNode *yaml.Node) ([]*outputNode, error) {
if outputsNode.IsZero() {
// "Outputs" is an optional field so we can skip it.
return nil, nil
}
if outputsNode.Kind != yaml.MappingNode {
return nil, errors.New(`"Outputs" field in cloudformation template is not a map`)
}
var nodes []*outputNode
for _, content := range mappingContents(outputsNode) {
nameNode := content.keyNode
fields := struct {
Value yaml.Node `yaml:"Value"`
}{}
if err := content.valueNode.Decode(&fields); err != nil {
return nil, fmt.Errorf(`decode the "Value" field of output "%s": %w`, nameNode.Value, err)
}
nodes = append(nodes, &outputNode{
nameNode: nameNode,
valueNode: &fields.Value,
})
}
return nodes, nil
}
type outputNode struct {
nameNode *yaml.Node
valueNode *yaml.Node
}
func (n *outputNode) name() string {
return n.nameNode.Value
}
func (n *outputNode) ref() (string, bool) {
switch n.valueNode.Kind {
case yaml.ScalarNode:
// It's a string like "!Ref MyDynamoDBTable"
if n.valueNode.Tag != "!Ref" {
return "", false
}
return strings.TrimSpace(n.valueNode.Value), true
case yaml.MappingNode:
// Check if it's a map like "Ref: MyDynamoDBTable"
fields := struct {
Ref string `yaml:"Ref"`
}{}
_ = n.valueNode.Decode(&fields)
if fields.Ref == "" {
return "", false
}
return fields.Ref, true
default:
return "", false
}
}