#!/usr/bin/env ruby

# Copyright 2013-2014 Bazaarvoice, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require 'bundler/setup'
require 'cloudformation-ruby-dsl/cfntemplate'
require 'cloudformation-ruby-dsl/table'

# Note: this is only intended to demonstrate the cloudformation-ruby-dsl. It compiles
#   and validates correctly, but won't produce a viable CloudFormation stack.

template do

  parameter 'Label',
            :Description => 'The label to apply to the servers.',
            :Type => 'String',
            :MinLength => '2',
            :MaxLength => '25',
            :AllowedPattern => '[_a-zA-Z0-9]*',
            :ConstraintDescription => 'Maximum length of the Label parameter may not exceed 25 characters and may only contain letters, numbers and underscores.',
            # The :Immutable attribute is a Ruby CFN extension.  It affects the behavior of the '<template> update ...'
            # operation in that a stack update may not change the values of parameters marked w/:Immutable => true.
            :Immutable => true

  parameter 'InstanceType',
            :Description => 'EC2 instance type',
            :Type => 'String',
            :Default => 'm2.xlarge',
            :AllowedValues => %w(t1.micro m1.small m1.medium m1.large m1.xlarge m2.xlarge m2.2xlarge m2.4xlarge c1.medium c1.xlarge),
            :ConstraintDescription => 'Must be a valid EC2 instance type.'

  parameter 'ImageId',
            :Description => 'EC2 Image ID',
            :Type => 'String',
            :Default => 'ami-255bbc4c',
            :AllowedPattern => 'ami-[a-f0-9]{8}',
            :ConstraintDescription => 'Must be ami-XXXXXXXX (where X is a hexadecimal digit)'

  parameter 'KeyPairName',
            :Description => 'Name of KeyPair to use.',
            :Type => 'String',
            :MinLength => '1',
            :MaxLength => '40',
            :Default => parameters['Label']

  parameter 'EmailAddress',
            :Type => 'String',
            :Description => 'Email address at which to send notification events.'

  mapping 'InlineExampleMap',
          :team1 => {
              :name => 'test1',
              :email => 'test1@example.com',
          },
          :team2 => {
              :name => 'test2',
              :email => 'test2@example.com',
          }

  # Generates mappings from external files with various formats.
  mapping 'JsonExampleMap', 'maps/map.json'

  mapping 'RubyExampleMap', 'maps/map.rb'

  mapping 'YamlExampleMap', 'maps/map.yaml'

  # Loads JSON mappings dynamically from example directory.
  Dir.entries('maps/more_maps').each_with_index do |path, index|
    next if path == "." or path == ".."
    mapping "ExampleMap#{index - 1}", "maps/more_maps/#{path}"
  end

  # Selects all rows in the table which match the name/value pairs of the predicate object and returns a
  # set of nested maps, where the key for the map at level n is the key at index n in the specified keys,
  # except for the last key in the specified keys which is used to determine the value of the leaf-level map.
  text = Table.load 'maps/table.txt'
  mapping 'TableExampleMap',
      text.get_map({ :column0 => 'foo' }, :column1, :column2, :column3)

  # Shows how to create a table useful for looking up subnets that correspond to a particular env/region for eg. vpc placement.
  vpc = Table.load 'maps/vpc.txt'
  mapping 'TableExampleMultimap',
          vpc.get_multimap({ :visibility => 'private', :zone => ['a', 'c'] }, :env, :region, :subnet)

  # The tag type is a DSL extension; it is not a property of actual CloudFormation templates.
  #   These tags are excised from the template and used to generate a series of --tag arguments which are passed to CloudFormation when a stack is created.
  #   They do not ultimately appear in the expanded CloudFormation template.
  #   The diff subcommand will compare tags with the running stack and identify any changes, but a stack update will do the diff and throw an error on any
  #   changes. The tags are propagated to all resources created by the stack, including the stack itself.
  #
  # Amazon has set the following restrictions on CloudFormation tags:
  #   => limit 10
  #   => immutable (you may not update a stack with new tags or different values for existing tags -- they will be rejected)
  #
  tag :MyTag => 'MyValue'
  tag :MyOtherTag => 'My Value With Spaces'

  resource 'SecurityGroup', :Type => 'AWS::EC2::SecurityGroup', :Properties => {
      :GroupDescription => 'Lets any vpc traffic in.',
      :SecurityGroupIngress => {:IpProtocol => '-1', :FromPort => '0', :ToPort => '65535', :CidrIp => "10.0.0.0/8"}
  }

  resource "ASG", :Type => 'AWS::AutoScaling::AutoScalingGroup', :Properties => {
      :AvailabilityZones => 'us-east-1',
      :HealthCheckType => 'EC2',
      :LaunchConfigurationName => ref('LaunchConfig'),
      :MinSize => 1,
      :MaxSize => 5,
      :NotificationConfiguration => {
          :TopicARN => ref('EmailSNSTopic'),
          :NotificationTypes => %w(autoscaling:EC2_INSTANCE_LAUNCH autoscaling:EC2_INSTANCE_LAUNCH_ERROR autoscaling:EC2_INSTANCE_TERMINATE autoscaling:EC2_INSTANCE_TERMINATE_ERROR),
      },
      :Tags => [
          {
              :Key => 'Name',
              # Grabs a value in an external map file.
              :Value => find_in_map('TableExampleMap', 'corge', 'grault'),
              :PropagateAtLaunch => 'true',
          },
          {
              :Key => 'Label',
              :Value => parameters['Label'],
              :PropagateAtLaunch => 'true',
          }
      ],
  }

  resource 'EmailSNSTopic', :Type => 'AWS::SNS::Topic', :Properties => {
      :Subscription => [
          {
              :Endpoint => ref('EmailAddress'),
              :Protocol => 'email',
          },
      ],
  }

  resource 'WaitConditionHandle', :Type => 'AWS::CloudFormation::WaitConditionHandle', :Properties => {}

  resource 'WaitCondition', :Type => 'AWS::CloudFormation::WaitCondition', :DependsOn => 'ASG', :Properties => {
      :Handle => ref('WaitConditionHandle'),
      :Timeout => 1200,
      :Count => "1"
  }

  resource 'LaunchConfig', :Type => 'AWS::AutoScaling::LaunchConfiguration', :Properties => {
      :ImageId => parameters['ImageId'],
      :KeyName => ref('KeyPairName'),
      :IamInstanceProfile => ref('InstanceProfile'),
      :InstanceType => ref('InstanceType'),
      :InstanceMonitoring => 'false',
      :SecurityGroups => [ref('SecurityGroup')],
      :BlockDeviceMappings => [
          {:DeviceName => '/dev/sdb', :VirtualName => 'ephemeral0'},
          {:DeviceName => '/dev/sdc', :VirtualName => 'ephemeral1'},
          {:DeviceName => '/dev/sdd', :VirtualName => 'ephemeral2'},
          {:DeviceName => '/dev/sde', :VirtualName => 'ephemeral3'},
      ],
      # Loads an external userdata script with an interpolated argument.
      :UserData => base64(interpolate(file('userdata.sh'), time: Time.now)),
  }

  resource 'InstanceProfile', :Type => 'AWS::IAM::InstanceProfile', :Properties => {
      # use cfn intrinsic conditional to choose the 2nd value because the expression evaluates to false
      :Path => fn_if(equal(3, 0), '/unselected/', '/'),
      :Roles => [ ref('InstanceRole') ],
  }

  resource 'InstanceRole', :Type => 'AWS::IAM::Role', :Properties => {
      :AssumeRolePolicyDocument => {
          :Statement => [
              {
                  :Effect => 'Allow',
                  :Principal => { :Service => [ 'ec2.amazonaws.com' ] },
                  :Action => [ 'sts:AssumeRole' ],
              },
          ],
      },
      :Path => '/',
  }

  # add conditions that can be used elsewhere in the template
  condition 'myCondition', fn_and(equal("one", "two"), not_equal("three", "four"))

  output 'EmailSNSTopicARN',
          :Value => ref('EmailSNSTopic'),
          :Description => 'ARN of SNS Topic used to send emails on events.'

  output 'MappingLookup',
          :Value => find_in_map('TableExampleMap', 'corge', 'grault'),
          :Description => 'An example map lookup.'

end.exec!