.. _Runway blueprints: https://github.com/onicagroup/runway/tree/master/runway/blueprints
.. _troposphere: https://github.com/cloudtools/troposphere

.. _Blueprint:
.. _Blueprints:

==========
Blueprints
==========

:ref:`Blueprints <term-blueprint>` are python classes that dynamically build CloudFormation templates.
Where you would specify a raw Cloudformation template in a stack using the ``template_path`` key, you instead specify a :ref:`Blueprint <term-blueprint>` python file using the ``class_path`` key.

Traditionally :ref:`Blueprints <term-blueprint>` are built using troposphere_, but that is not absolutely necessary.

Making your own should be easy, and you can take a lot of examples from `Runway blueprints`_.
In the end, all that is required is that the :ref:`Blueprint <term-blueprint>` is a subclass of ``runway.cfngin.blueprints.base`` and it have the following methods:

.. code-block:: python

  # Initializes the Blueprint
  def __init__(self, name, context, mappings=None):

  # Updates self.template to create the actual template
  def create_template(self):

  # Returns a tuple: (version, rendered_template)
  def render_template(self):


Variables
=========

A Blueprint can define a ``VARIABLES`` property that defines the variables
it accepts from the `Config Variables <configuration.html#variables>`_.

``VARIABLES`` should be a dictionary of ``<variable name>: <variable
definition>``. The variable definition should be a dictionary which
supports the following optional keys:

**type:**
  The type for the variable value. This can either be a native python
  type or one of the `Variable Types`_.

**default:**
  The default value that should be used for the variable if none is
  provided in the config.

**description:**
  A string that describes the purpose of the variable.

**validator:**
  An optional function that can do custom validation of the variable. A
  validator function should take a single argument, the value being validated,
  and should return the value if validation is successful. If there is an
  issue validating the value, an exception (``ValueError``, ``TypeError``, etc)
  should be raised by the function.

**no_echo:**
  Only valid for variables whose type subclasses ``CFNType``. Whether to
  mask the parameter value whenever anyone makes a call that describes the
  stack. If you set the value to true, the parameter value is masked with
  asterisks (*****).

**allowed_values:**
  Only valid for variables whose type subclasses ``CFNType``. The set of
  values that should be allowed for the CloudFormation Parameter.

**allowed_pattern:**
  Only valid for variables whose type subclasses ``CFNType``. A regular
  expression that represents the patterns you want to allow for the
  CloudFormation Parameter.

**max_length:**
  Only valid for variables whose type subclasses ``CFNType``. The maximum
  length of the value for the CloudFormation Parameter.

**min_length:**
  Only valid for variables whose type subclasses ``CFNType``. The minimum
  length of the value for the CloudFormation Parameter.

**max_value:**
  Only valid for variables whose type subclasses ``CFNType``. The max
  value for the CloudFormation Parameter.

**min_value:**
  Only valid for variables whose type subclasses ``CFNType``. The min
  value for the CloudFormation Parameter.

**constraint_description:**
  Only valid for variables whose type subclasses ``CFNType``. A string
  that explains the constraint when the constraint is violated for the
  CloudFormation Parameter.


Variable Types
==============

Any native python type can be specified as the ``type`` for a variable.
You can also use the following custom types:

TroposphereType
---------------

The ``TroposphereType`` can be used to generate resources for use in the
:ref:`Blueprint <term-blueprint>` directly from user-specified configuration. Which case applies depends
on what ``type`` was chosen, and how it would be normally used in the :ref:`Blueprint <term-blueprint>`
(and CloudFormation in general).

Resource Types
^^^^^^^^^^^^^^

When ``type`` is a `Resource Type`_, the value specified by the user in the
configuration file must be a dictionary, but with two possible structures.

When ``many`` is disabled, the top-level dictionary keys correspond to
parameters of the ``type`` constructor. The key-value pairs will be used
directly, and one object will be created and stored in the variable.

When ``many`` is enabled, the top-level dictionary *keys* are resource titles,
and the corresponding *values* are themselves dictionaries, to be used as
parameters for creating each of multiple ``type`` objects. A list of those
objects will be stored in the variable.

.. _Resource Type: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html

Property Types
^^^^^^^^^^^^^^

When ``type`` is a `Property Type`_ the value specified by the user in the
configuration file must be a dictionary or a list of dictionaries.

When ``many`` is disabled, the top-level dictionary keys correspond to
parameters of the ``type`` constructor. The key-value pairs will be used
directly, and one object will be created and stored in the variable.

When ``many`` is enabled, a list of dictionaries is expected. For each element,
one corresponding call will be made to the ``type`` constructor, and all the
objects produced will be stored (also as a list) in the variable.

.. _Property Type: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-product-property-reference.html

Optional variables
^^^^^^^^^^^^^^^^^^

In either case, when ``optional`` is enabled, the variable may have no value
assigned, or be explicitly assigned a null value. When that happens the
variable's final value will be ``None``.

Example
^^^^^^^

Below is an annotated example:

.. code-block:: python

    from runway.cfngin.blueprints.base import Blueprint
    from runway.cfngin.blueprints.variables.types import TroposphereType
    from troposphere import s3, sns

    class Buckets(Blueprint):

        VARIABLES = {
            # Specify that Buckets will be a list of s3.Bucket types.
            # This means the config should a dictionary of dictionaries
            # which will be converted into troposphere buckets.
            "Buckets": {
                "type": TroposphereType(s3.Bucket, many=True),
                "description": "S3 Buckets to create.",
            },
            # Specify that only a single bucket can be passed.
            "SingleBucket": {
                "type": TroposphereType(s3.Bucket),
                "description": "A single S3 bucket",
            },
            # Specify that Subscriptions will be a list of sns.Subscription types.
            # Note: sns.Subscription is the property type, not the standalone
            # sns.SubscriptionResource.
            "Subscriptions": {
                "type": TroposphereType(sns.Subscription, many=True),
                "description": "Multiple SNS subscription designations"
            },
            # Specify that only a single subscription can be passed, and that it
            # is made optional.
            "SingleOptionalSubscription": {
                "type": TroposphereType(sns.Subscription, optional=True),
                "description": "A single, optional SNS subscription designation"
            }
        }

        def create_template(self):
            t = self.template
            variables = self.get_variables()

            # The Troposphere s3 buckets have already been created when we
            # access variables["Buckets"], we just need to add them as
            # resources to the template.
            [t.add_resource(bucket) for bucket in variables["Buckets"]]

            # Add the single bucket to the template. You can use
            # `Ref(single_bucket)` to pass CloudFormation references to the
            # bucket just as you would with any other Troposphere type.
            # single_bucket = variables["SingleBucket"]
            t.add_resource(single_bucket)

            subscriptions = variables["Subscriptions"]
            optional_subscription = variables["SingleOptionalSubscription"]
            # Handle it in some special way...
            if optional_subscription is not None:
                subscriptions.append(optional_subscription)

            t.add_resource(sns.Topic(
                TopicName="one-test",
                Subscriptions=))

            t.add_resource(sns.Topic(
                TopicName="another-test",
                Subscriptions=subscriptions))



A sample config for the above:

.. code-block:: yaml

  stacks:
    - name: buckets
      class_path: path.to.above.Buckets
      variables:
        Buckets:
          # resource name (title) that will be added to CloudFormation.
          FirstBucket:
            # name of the s3 bucket
            BucketName: my-first-bucket
          SecondBucket:
            BucketName: my-second-bucket
        SingleBucket:
          # resource name (title) that will be added to CloudFormation.
          MySingleBucket:
            BucketName: my-single-bucket
        Subscriptions:
          - Endpoint: one-lambda
            Protocol: lambda
          - Endpoint: another-lambda
            Protocol: lambda
        # The following could be omitted entirely
        SingleOptionalSubscription:
          Endpoint: a-third-lambda
          Protocol: lambda


CFNType
-------

The ``CFNType`` can be used to signal that a variable should be submitted
to CloudFormation as a Parameter instead of only available to the
Blueprint when rendering. This is useful if you want to leverage AWS-
Specific Parameter types (e.g. ``List<AWS::EC2::Image::Id>``) or Systems
Manager Parameter Store values (e.g. ``AWS::SSM::Parameter::Value<String>``).
See ``runway.cfngin.blueprints.variables.types`` for available subclasses of the
``CFNType``.

Example
^^^^^^^

Below is an annotated example:

.. code-block:: python

    from runway.cfngin.blueprints.base import Blueprint
    from runway.cfngin.blueprints.variables.types import (
        CFNString,
        EC2AvailabilityZoneNameList,
    )


    class SampleBlueprint(Blueprint):

        VARIABLES = {
            "String": {
                "type": str,
                "description": "Simple string variable",
            },
            "List": {
                "type": list,
                "description": "Simple list variable",
            },
            "CloudFormationString": {
                "type": CFNString,
                "description": "A variable which will create a CloudFormation Parameter of type String",
            },
            "CloudFormationSpecificType": {
                "type": EC2AvailabilityZoneNameList,
                "description": "A variable which will create a CloudFormation Parameter of type List<AWS::EC2::AvailabilityZone::Name>"
            },
        }

        def create_template(self):
            t = self.template

            # `get_variables` returns a dictionary of <variable name>: <variablevalue>.
            # For the subclasses of `CFNType`, the values are
            # instances of `CFNParameter` which have a `ref` helper property
            # which will return a troposphere `Ref` to the parameter name.
            variables = self.get_variables()

            t.add_output(Output("StringOutput", variables["String"]))

            # variables["List"] is a native list
            for index, value in enumerate(variables["List"]):
                t.add_output(Output("ListOutput:{}".format(index), value))


            # `CFNParameter` values (which wrap variables with a `type`
            # that is a `CFNType` subclass) can be converted to troposphere
            # `Ref` objects with the `ref` property
            t.add_output(Output("CloudFormationStringOutput",
                                variables["CloudFormationString"].ref))
            t.add_output(Output("CloudFormationSpecificTypeOutput",
                                variables["CloudFormationSpecificType"].ref))


Utilizing Stack name within your Blueprint
==========================================

Sometimes your :ref:`Blueprint <term-blueprint>` might want to utilize the already existing stack name
within your :ref:`Blueprint <term-blueprint>`. Runway's CFNgin provides access to both the fully qualified
stack name matching what’s shown in the CloudFormation console, in addition to
the stacks short name you have set in your YAML config.

Referencing Fully Qualified Stack name
--------------------------------------

The fully qualified name is a combination of the CFNgin namespace + the short
name (what you set as ``name`` in your YAML config file). If your CFNgin
namespace is ``CFNginIsCool`` and the stacks short name is
``myAwesomeEC2Instance``, the fully qualified name would be::

    CFNginIsCool-myAwesomeEC2Instance

To use this in your :ref:`Blueprint <term-blueprint>`, you can get the name from context using ``self.context.get_fqn(self.name)``.

Referencing the Stack short name
--------------------------------

The Stack short name is the name you specified for the stack within your YAML
config. It does not include the namespace. If your CFNgin namespace is
``CFNginIsCool`` and the stacks short name is ``myAwesomeEC2Instance``, the
short name would be::

    myAwesomeEC2Instance

To use this in your :ref:`Blueprint <term-blueprint>`, you can get the name from self.name: ``self.name``

Example
^^^^^^^

Below is an annotated example creating a security group:

.. code-block:: python

  # we are importing Ref to allow for CFN References in the EC2 resource.  Tags
  # will be used to set the Name tag
  from troposphere import Ref, ec2, Tags
  from runway.cfngin.blueprints.base import Blueprint
  # CFNString is imported to allow for stand alone stack use
  from runway.cfngin.blueprints.variables.types import CFNString

  class SampleBlueprint(Blueprint):

    # VpcId set here to allow for Blueprint to be reused
    VARIABLES = {
    "VpcId": {
        "type": CFNString,
        "description": "The VPC to create the Security group in",
        }
    }


    def create_template(self):
        template = self.template
        # Assigning the variables to a variable
        variables = self.get_variables()
        # now adding a SecurityGroup resource named `SecurityGroup` to the CFN template
        template.add_resource(
          ec2.SecurityGroup(
            "SecurityGroup",
            # Referencing the VpcId set as the variable
            VpcId=variables['VpcId'].ref,
            # Setting the group description as the fully qualified name
            GroupDescription=self.context.get_fqn(self.name),
            # setting the Name tag to be the stack short name
            Tags=Tags(
              Name=self.name
              )
            )
          )


Testing Blueprints
==================

When writing your own :ref:`Blueprints <term-blueprint>` its useful to write tests for them in order
to make sure they behave the way you expect they would, especially if there is
any complex logic inside.

To this end, a sub-class of the ``unittest.TestCase`` class has been
provided: ``runway.cfngin.blueprints.testutil.BlueprintTestCase``. You use it
like the regular TestCase class, but it comes with an addition assertion:
``assertRenderedBlueprint``. This assertion takes a Blueprint object and renders
it, then compares it to an expected output, usually in
``tests/fixtures/blueprints``.


Yaml (CFNgin) format tests
---------------------------

In order to wrap the ``BlueprintTestCase`` tests in a format similar to CFNgin's
stack format, the ``YamlDirTestGenerator`` class is provided. When subclassed in
a directory, it will search for yaml files in that directory with certain
structure and execute a test case for it. As an example:

.. code-block:: yaml

  ---
  namespace: test
  stacks:
    - name: test_stack
      class_path: cfngin_blueprints.s3.Buckets
      variables:
        var1: val1

When run from tests, this will create a template fixture file called
test_stack.json containing the output from the ``cfngin_blueprints.s3.Buckets``
template.
