internal/pkg/addon/output.go (121 lines of code) (raw):
// 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
}
}