¯\_(ツ)_/¯

thunder@home:~$

This is my home blog, mostly to share some useful info or code snippets
~ 2 mins

Hello, Jenkins geeks.

Once upon a time, I was writing a pipeline which had to read AWS CloudFormation template from YAML… But there was a problem – short CloudFormation functions…

And here is the short story how to do that.

Actually, this code was written at 2018:

package com.example.package

import java.util.Arrays
import java.util.HashMap
import java.util.Map

import org.yaml.snakeyaml.constructor.AbstractConstruct
import org.yaml.snakeyaml.constructor.SafeConstructor
import org.yaml.snakeyaml.error.YAMLException
import org.yaml.snakeyaml.nodes.MappingNode
import org.yaml.snakeyaml.nodes.Node
import org.yaml.snakeyaml.nodes.ScalarNode
import org.yaml.snakeyaml.nodes.SequenceNode
import org.yaml.snakeyaml.nodes.Tag
import org.yaml.snakeyaml.LoaderOptions

import com.cloudbees.groovy.cps.NonCPS

/**
 * Allows snakeyaml to parse YAML templates that contain short forms of
 * CloudFormation intrinsic functions.
 *
 */
public class IntrinsicsYamlConstructor extends SafeConstructor implements Serializable {
    public IntrinsicsYamlConstructor() {
        super(new LoaderOptions())
        this.yamlConstructors.put(new Tag("!And"), new ConstructFunction(true, false))
        this.yamlConstructors.put(new Tag("!Base64"), new ConstructFunction(true, false))
        this.yamlConstructors.put(new Tag("!Cidr"), new ConstructFunction(true, false))
        this.yamlConstructors.put(new Tag("!Condition"), new ConstructFunction(true, false))
        this.yamlConstructors.put(new Tag("!Equals"), new ConstructFunction(true, false))
        this.yamlConstructors.put(new Tag("!FindInMap"), new ConstructFunction(true, false))
        this.yamlConstructors.put(new Tag("!GetAtt"), new ConstructFunction(true, true))
        this.yamlConstructors.put(new Tag("!GetAZs"), new ConstructFunction(true, false))
        this.yamlConstructors.put(new Tag("!If"), new ConstructFunction(true, false))
        this.yamlConstructors.put(new Tag("!ImportValue"), new ConstructFunction(true, false))
        this.yamlConstructors.put(new Tag("!Join"), new ConstructFunction(true, false))
        this.yamlConstructors.put(new Tag("!Not"), new ConstructFunction(true, false))
        this.yamlConstructors.put(new Tag("!Or"), new ConstructFunction(true, false))
        this.yamlConstructors.put(new Tag("!Ref"), new ConstructFunction(false, false))
        this.yamlConstructors.put(new Tag("!Select"), new ConstructFunction(true, false))
        this.yamlConstructors.put(new Tag("!Split"), new ConstructFunction(true, false))
        this.yamlConstructors.put(new Tag("!Sub"), new ConstructFunction(true, false))
    }

    private class ConstructFunction extends AbstractConstruct {
        private final boolean attachFnPrefix
        private final boolean forceSequenceValue

        public ConstructFunction(boolean attachFnPrefix, boolean forceSequenceValue) {
            this.attachFnPrefix = attachFnPrefix
            this.forceSequenceValue = forceSequenceValue
        }

        @NonCPS
        public Object construct(Node node) {
            String key = node.getTag().getValue().substring(1)
            String prefix = attachFnPrefix ? "Fn::" : ""
            Map<String, Object> result = new HashMap<String, Object>()

            result.put(prefix + key, constructIntrinsicValueObject(node))
            return result
        }

        @NonCPS
        protected Object constructIntrinsicValueObject(Node node) {
            if (node instanceof ScalarNode) {
                Object val = (String) constructScalar((ScalarNode) node)
                if (forceSequenceValue) {
                    val = Arrays.asList(((String) val).split("\\."))
                }
                return val
            } else if (node instanceof SequenceNode) {
                return constructSequence((SequenceNode) node)
            } else if (node instanceof MappingNode) {
                return constructMapping((MappingNode) node)
            }
            throw new YAMLException("Intrisic function arguments cannot be parsed.")
        }
    }
}

And now how to use it:

import com.example.package.IntrinsicsYamlConstructor

String text = '''
AWSTemplateFormatVersion: "2010-09-09"

Mappings:
  RegionMap:
    us-east-1:
      AMI: "ami-0ff8a91507f77f867"
    us-west-1:
      AMI: "ami-0bdb828fd58c52235"
    us-west-2:
      AMI: "ami-a0cfeed8"
    eu-west-1:
      AMI: "ami-047bb4163c506cd98"
    sa-east-1:
      AMI: "ami-07b14488da8ea02a0"
    ap-southeast-1:
      AMI: "ami-08569b978cc4dfa10"
    ap-southeast-2:
      AMI: "ami-09b42976632b27e9b"
    ap-northeast-1:
      AMI: "ami-06cd52961ce9f0d85"

Parameters:
  EnvType:
    Description: Environment type.
    Default: test
    Type: String
    AllowedValues: [prod, dev, test]
    ConstraintDescription: must specify prod, dev, or test.

Conditions:
  CreateProdResources: !Equals [!Ref EnvType, prod]
  CreateDevResources: !Equals [!Ref EnvType, "dev"]

Resources:
  EC2Instance:
    Type: "AWS::EC2::Instance"
    Properties:
      ImageId: !FindInMap [RegionMap, !Ref "AWS::Region", AMI]
      InstanceType: !If [CreateProdResources, c1.xlarge, !If [CreateDevResources, m1.large, m1.small]]
  MountPoint:
    Type: "AWS::EC2::VolumeAttachment"
    Condition: CreateProdResources
    Properties:
      InstanceId: !Ref EC2Instance
      VolumeId: !Ref NewVolume
      Device: /dev/sdh
  NewVolume:
    Type: "AWS::EC2::Volume"
    Condition: CreateProdResources
    Properties:
      Size: 100
      AvailabilityZone: !GetAtt EC2Instance.AvailabilityZone
'''
Yaml yaml = new Yaml(new IntrinsicsYamlConstructor())
template = yaml.load(text)

CloudFormation template taken from AWS Sample templates

Happy Scripting!

Thank You For Reading