Skip to content

Commit

Permalink
Merge ae1f261 into 8ebdef7
Browse files Browse the repository at this point in the history
  • Loading branch information
bkrodgers committed Nov 17, 2016
2 parents 8ebdef7 + ae1f261 commit 16ecc44
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 8 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Expand Up @@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## [3.5.0] - 2016-11-17

- Support for AWS's new features supporting [cross template references](https://aws.amazon.com/blogs/aws/aws-cloudformation-update-yaml-cross-stack-references-simplified-substitution/). (see [#119](https://github.com/MonsantoCo/cloudformation-template-generator/pull/119))

- Support for [Fn::Sub](https://aws.amazon.com/blogs/aws/aws-cloudformation-update-yaml-cross-stack-references-simplified-substitution/), which provides a cleaner alternative to `Fn::Join` and `Fn::GetAtt`. (see [#119](https://github.com/MonsantoCo/cloudformation-template-generator/pull/119))

## [3.4.0] - 2016-11-07

### Added
Expand Down
4 changes: 3 additions & 1 deletion README.md
Expand Up @@ -173,14 +173,16 @@ This project packages certain useful custom CloudFormation types. These are Lam
tasks that CloudFormation does not natively support. In order to use them, you must upload the Lambda function
to your account and region. The code for these functions is found in this repo under assets/custom-types.

## Remote Route 53 entries
#### Remote Route 53 entries
A given domain (or hosted zone, more specifically) must be managed out of a single AWS account. This poses problems if you want to create resources under that domain in templates that will run out of other accounts. A CloudFormation template can only work in one given account. However, with Cloud Formation's custom type functionality, we use custom code to assume a role in the account that owns the hosted zone. This requires some setup steps for each hosted zone and each account. For instructions, please see: https://github.com/MonsantoCo/cloudformation-template-generator/blob/master/assets/custom-types/remote-route53/README.md for more.

## Working with Cloudformation Concatenating
In the CloudFormation DSL, there is support for concatenating strings, parameters, and function calls together to build strings.
This can get really ugly as they are chained together.
There is a [string interpolator](http://monsantoco.github.io/cloudformation-template-generator/latest/api/#com.monsanto.arch.cloudformation.model.package$$AwsStringInterpolator) to make this easier.

**Update 11/17/2016:** While we are not deprecating this functionality at this time, CFTG now supports `Fn::Sub`, a native way to do something very similar. It can replace both `Fn::Join` and many uses of `Fn::GetAtt`. Read more [here](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html).

## Releasing

This project uses the sbt release plugin. After the changes you want to
Expand Down
Expand Up @@ -35,6 +35,8 @@ object AmazonFunctionCall extends DefaultJsonProtocol {
case not: `Fn::Not` => implicitly[JsonWriter[`Fn::Not`#CFBackingType] ].write(not.arguments)
case and: `Fn::And` => implicitly[JsonWriter[`Fn::And`#CFBackingType] ].write(and.arguments)
case or: `Fn::Or` => implicitly[JsonWriter[`Fn::Or`#CFBackingType] ].write(or.arguments)
case sub: `Fn::Sub` => sub.serializeArguments
case imp: `Fn::ImportValue` => implicitly[JsonWriter[`Fn::ImportValue`#CFBackingType] ].write(imp.arguments)
case cfr: ConditionFnRef => implicitly[JsonWriter[ConditionFnRef#CFBackingType] ].write(cfr.arguments)
case s: `Fn::Select`[_] => s.serializeArguments
case f: `Fn::If`[_] => f.serializeArguments
Expand Down Expand Up @@ -131,6 +133,19 @@ case class `Fn::FindInMap`[R](mapName: Token[MappingRef[R]], outerKey: Token[Str
case class `Fn::Base64`(toEncode: Token[String])
extends AmazonFunctionCall[String]("Fn::Base64"){type CFBackingType = Token[String] ; val arguments = toEncode}

case class `Fn::ImportValue`(importValue: Token[String])
extends AmazonFunctionCall[String]("Fn::ImportValue"){type CFBackingType = Token[String] ; val arguments = importValue}

case class `Fn::Sub`(template: Token[String], subs: Option[Map[Token[String], Token[String]]] = None)
extends AmazonFunctionCall[String]("Fn::Sub"){
type CFBackingType = (Token[String], Option[Map[Token[String], Token[String]]]) ;
val arguments = (template, subs)
def serializeArguments = arguments._2 match {
case Some(x) => arguments.toJson
case None => arguments._1.toJson
}
}

case class `Fn::Equals`(a: Token[String], b: Token[String])
extends NestableAmazonFunctionCall[String]("Fn::Equals")
{type CFBackingType = (Token[String], Token[String]) ; val arguments = (a, b)}
Expand Down Expand Up @@ -183,10 +198,12 @@ object `Fn::Base64` extends DefaultJsonProtocol {
// but you also want to be able to pass a literal ResourceRef[R]
sealed trait Token[R]
object Token extends DefaultJsonProtocol {
implicit def fromAnyTup[R1: JsonFormat, R2: JsonFormat](r: (R1, R2)): (AnyToken[R1], AnyToken[R2]) = (AnyToken(r._1), AnyToken(r._2))
implicit def fromAnySeq[R: JsonFormat](r: Seq[R]): AnyToken[Seq[R]] = AnyToken(r)
implicit def fromAny[R: JsonFormat](r: R): AnyToken[R] = AnyToken(r)
implicit def fromOptionAny[R: JsonFormat](or: Option[R]): Option[AnyToken[R]] = or.map(r => Token.fromAny(r))
implicit def fromString(s: String): StringToken = StringToken(s)
implicit def fromStrTup(r: (String, String)): (StringToken, StringToken) = (StringToken(r._1), StringToken(r._2))
implicit def fromBoolean(s: Boolean): BooleanToken = BooleanToken(s)
implicit def fromInt(s: Int): IntToken = IntToken(s)
implicit def fromFunction[R](f: AmazonFunctionCall[R]): FunctionCallToken[R] = FunctionCallToken[R](f)
Expand Down
17 changes: 12 additions & 5 deletions src/main/scala/com/monsanto/arch/cloudformation/model/Output.scala
Expand Up @@ -10,17 +10,24 @@ import scala.collection.immutable.ListMap
* Created by Ryan Richt on 2/15/15
*/

case class Output[R](name: String, Description: String, Value: Token[R])(implicit format: JsonFormat[Token[R]]) {
case class Output[R](name: String, Description: String, Value: Token[R], Export: Option[Token[String]] = None)(implicit format: JsonFormat[Token[R]]) {
def valueAsJson = Token.format[Token[R]].write(Value)
}

object Output extends DefaultJsonProtocol {

implicit def format[A] : JsonWriter[Output[A]] = new JsonWriter[Output[A]] {
override def write(obj: Output[A]) = JsObject(
"Description" -> JsString(obj.Description),
"Value" -> obj.valueAsJson
)
override def write(obj: Output[A]) = {
JsObject(Map(
"Description" -> JsString(obj.Description),
"Value" -> obj.valueAsJson)
++ exportAsJsonTuple(obj))
}

def exportAsJsonTuple(obj: Output[A]): Option[(String, JsValue)] = obj.Export match {
case None => None
case Some(x) => "Export" -> JsObject("Name" -> x.toJson)
}
}

implicit val seqFormat: JsonWriter[Seq[Output[_]]] = new JsonWriter[Seq[Output[_]]] {
Expand Down
Expand Up @@ -27,6 +27,7 @@ trait Conditions {
trait Outputs {
implicit class RichResource[R <: Resource[R]](r: R) {
def andOutput(name: String, description: String) = Template.fromResource(r) ++ Template.fromOutput( Output(name, description, ResourceRef(r)) )
def andOutput(name: String, description: String, export: Token[String]) = Template.fromResource(r) ++ Template.fromOutput( Output(name, description, ResourceRef(r), Some(export)) )
}
}

Expand Down Expand Up @@ -185,7 +186,7 @@ trait Subnet extends AvailabilityZone with Outputs {
Tags = AmazonTag.fromName(name)
)

f(sub) ++ sub.andOutput(name, name)
f(sub) ++ sub.andOutput(name, name, `Fn::Sub`(s"$${AWS::StackName}-${name}"))
}

def nat(routeTables: Seq[`AWS::EC2::RouteTable`], ga: `AWS::EC2::VPCGatewayAttachment`)
Expand Down Expand Up @@ -536,7 +537,7 @@ trait VPC extends Outputs {
EnableDnsHostnames = true
)

f(vpc) ++ vpc.andOutput("VPCID", "VPC Info")
f(vpc) ++ vpc.andOutput("VPCID", "VPC Info", `Fn::Sub`(s"$${AWS::StackName}-VPCID"))
}
}

Expand Down
@@ -0,0 +1,86 @@
package com.monsanto.arch.cloudformation.model

import com.monsanto.arch.cloudformation.model.AmazonFunctionCall._
import com.monsanto.arch.cloudformation.model._
import com.monsanto.arch.cloudformation.model.resource._
import Token._
import org.scalatest.{FunSpec, Matchers}
import spray.json._
import DefaultJsonProtocol._

/**
* Created by Ryan Richt on 2/26/15
*/
class IntrinsicFunctions_UT extends FunSpec with Matchers {

describe("Fn::Sub"){

it("no args"){

val test: Token[String] = `Fn::Sub`(s"This is a $${test} template")

val expected = JsObject(
"Fn::Sub"-> JsString(s"This is a $${test} template")
)

test.toJson should be(expected)
}

it("one arg"){

val test: Token[String] = `Fn::Sub`(
s"This is a $${test} template",
Some(Map("test" -> "value"))
)

val expected = JsObject(
"Fn::Sub"-> JsArray(
JsString(s"This is a $${test} template"),
JsObject("test" -> JsString("value"))
)
)

test.toJson should be(expected)
}

it("two args"){

val test: Token[String] = `Fn::Sub`(
s"This is a $${test} template",
Some(Map("test" -> "value", "test2" -> "value2"))
)

val expected = JsObject(
"Fn::Sub"-> JsArray(
JsString(s"This is a $${test} template"),
JsObject("test" -> JsString("value"), "test2" -> JsString("value2"))
)
)

test.toJson should be(expected)
}
}

describe("Fn::ImportValue") {
it("should serialize with static string") {
val test: Token[String] = `Fn::ImportValue`("Test-Import-Value")

val expected = JsObject(
"Fn::ImportValue" -> JsString("Test-Import-Value")
)
test.toJson should be(expected)
}

it("should serialize with an embedded function") {
val test: Token[String] = `Fn::ImportValue`(`Fn::Join`("", Seq("str1", "str2")))

val expected = JsObject(
"Fn::ImportValue" -> JsObject("Fn::Join" -> JsArray(
JsString(""),
JsArray(JsString("str1"), JsString("str2"))
)
))
test.toJson should be(expected)
}
}
}
Expand Up @@ -135,6 +135,54 @@ class Template_UT extends FlatSpec with Matchers with VPC with Subnet with Avail
""".stripMargin
}


it should "be able to add an output of type Token[String] with a static export" in new JsonWritingMatcher {
import spray.json._
val token : Token[String] = ResourceRef(resource1)
val output = Output(
name = "out1",
Description = "test",
Value = token,
Export = Some("export-test")
)
output.toJson shouldMatch
s"""
|{
| "Description": "test",
| "Value": {
| "Ref": "${resource1.name}"
| },
| "Export": {
| "Name": "export-test"
| }
|}
""".stripMargin
}


it should "be able to add an output of type Token[String] with a function export" in new JsonWritingMatcher {
import spray.json._
val token : Token[String] = ResourceRef(resource1)
val output = Output(
name = "out1",
Description = "test",
Value = token,
Export = Some(`Fn::Sub`(s"$${AWS::StackName}-test-export"))
)
output.toJson shouldMatch
s"""
|{
| "Description": "test",
| "Value": {
| "Ref": "${resource1.name}"
| },
| "Export": {
| "Name": {"Fn::Sub": "$${AWS::StackName}-test-export"}
| }
|}
""".stripMargin
}

it should "be able to add outputs" in {
val outputs = Seq(output1, output2)
val template = Template.EMPTY ++ outputs
Expand Down

0 comments on commit 16ecc44

Please sign in to comment.