cfer 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/cfer.gemspec ADDED
@@ -0,0 +1,37 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'cfer/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "cfer"
8
+ spec.version = Cfer::VERSION
9
+ spec.authors = ["Sean Edwards"]
10
+ spec.email = ["stedwards87+git@gmail.com"]
11
+
12
+ spec.summary = %q{Toolkit for automating infrastructure using AWS CloudFormation}
13
+ spec.description = spec.summary
14
+ spec.homepage = "https://github.com/seanedwards/cfer"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = 'bin'
19
+ spec.executables = 'cfer'
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_runtime_dependency 'docile'
23
+ spec.add_runtime_dependency 'thor'
24
+ spec.add_runtime_dependency 'activesupport'
25
+ spec.add_runtime_dependency 'aws-sdk'
26
+ spec.add_runtime_dependency 'aws-sdk-resources'
27
+ spec.add_runtime_dependency 'preconditions'
28
+ spec.add_runtime_dependency 'semantic'
29
+ spec.add_runtime_dependency 'rainbow'
30
+ spec.add_runtime_dependency 'highline'
31
+ spec.add_runtime_dependency 'rugged'
32
+ spec.add_runtime_dependency 'table_print'
33
+ spec.add_runtime_dependency "rake"
34
+
35
+ spec.add_development_dependency "bundler"
36
+ spec.add_development_dependency "yard"
37
+ end
@@ -0,0 +1,84 @@
1
+ description 'Example stack template for a small EC2 instance'
2
+
3
+ # NOTE: This template depends on vpc.rb
4
+
5
+
6
+ # By not specifying a default value, a parameter becomes required.
7
+ # Specify this parameter by adding `--parameters KeyName:<ec2-keyname>` to your CLI options.
8
+ parameter :KeyName
9
+
10
+ # We define some more parameters the same way we did in the VPC template.
11
+ # Cfer will interpret the default value by looking up the stack output named `vpcid`
12
+ # on the stack named `vpc`.
13
+ #
14
+ # If you created the VPC stack with a different name, you can overwrite these default values
15
+ # by adding `VpcId:@<vpc-stack-name>.vpcid SubnetId:@<vpc-stack-name>.subnetid1`
16
+ # to your `--parameters` option
17
+ parameter :VpcId, default: '@vpc.vpcid'
18
+ parameter :SubnetId, default: '@vpc.subnetid1'
19
+
20
+ # This is the Ubuntu 14.04 LTS HVM AMI provided by Amazon.
21
+ parameter :ImageId, default: 'ami-d05e75b8'
22
+ parameter :InstanceType, default: 't2.medium'
23
+
24
+ # Define a security group to be applied to an instance.
25
+ # This one will allow SSH access from anywhere, and no other inbound traffic.
26
+ resource :instancesg, "AWS::EC2::SecurityGroup" do
27
+ group_description 'Wide-open SSH'
28
+ vpc_id Fn::ref(:VpcId)
29
+
30
+ # Parameter values can be Ruby arrays and hashes. These will be transformed to JSON.
31
+ # You could write your own functions to make stuff like this easier, too.
32
+ security_group_ingress [
33
+ {
34
+ CidrIp: '0.0.0.0/0',
35
+ IpProtocol: 'tcp',
36
+ FromPort: 22,
37
+ ToPort: 22
38
+ }
39
+ ]
40
+ end
41
+
42
+ # We can define extension objects, which extend the basic JSON-building
43
+ # functionality of Cfer. Cfer provides a few of these, but you're free
44
+ # to define your own by creating a class that matches the name of an
45
+ # CloudFormation resource type, inheriting from `Cfer::AWS::Resource`
46
+ # inside the `CferExt` module:
47
+ module CferExt::AWS::EC2
48
+ # This class adds methods to resources with the type `AWS::EC2::Instance`
49
+ # Remember, this class could go in your own gem to be shared between your templates
50
+ # in a way that works with the rest of your infrastructure.
51
+ class Instance < Cfer::Cfn::Resource
52
+ def boot_script(data)
53
+ # This function simply wraps a bash script in the little bit of extra
54
+ # sugar (hashbang + base64 encoding) that EC2 requires for userdata boot scripts.
55
+ # See the AWS docs here: http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html
56
+ script = <<-EOS.strip_heredoc
57
+ #!/bin/bash
58
+ #{data}
59
+ EOS
60
+
61
+ user_data Base64.encode64(script)
62
+ end
63
+ end
64
+ end
65
+
66
+ resource :instance, "AWS::EC2::Instance" do
67
+ # Using the extension defined above, we can have the instance write a simple
68
+ # file to show that it's working. When you converge this template, there
69
+ # should be a `welcome.txt` file sitting in the `ubuntu` user's home directory.
70
+ boot_script "echo 'Welcome to Cfer!' > /home/ubuntu/welcome.txt"
71
+
72
+ image_id Fn::ref(:ImageId)
73
+ instance_type Fn::ref(:InstanceType)
74
+ key_name Fn::ref(:KeyName)
75
+
76
+ network_interfaces [ {
77
+ AssociatePublicIpAddress: "true",
78
+ DeviceIndex: "0",
79
+ GroupSet: [ Fn::ref(:instancesg) ],
80
+ SubnetId: Fn::ref(:SubnetId)
81
+ } ]
82
+ end
83
+
84
+ output :instance, Fn::ref(:instance)
data/examples/vpc.rb ADDED
@@ -0,0 +1,84 @@
1
+ description 'Stack template for a simple example VPC'
2
+
3
+ # This template creates the following resources for a basic beginning AWS VPC setup:
4
+ #
5
+ # 1) A VPC
6
+ # 2) A route table to control network routing
7
+ # 3) An Internet gateway to route traffic to the public internet
8
+ # 4) 3 subnets, one in each of the account's first 3 availability zones
9
+ # 5) A default network route to the IGW
10
+ # 6) Associated plumbing resources to link it all together
11
+
12
+ # Parameters may be defined using the `parameter` function
13
+ parameter :VpcName, default: 'Example VPC'
14
+
15
+ # Resources are created using the `resource` function, accepting the following arguments:
16
+ # 1) The resource name (string or symbol)
17
+ # 2) The resource type. See the AWS CloudFormation docs for the available resource types: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html
18
+ resource :vpc, 'AWS::EC2::VPC' do
19
+ # Each line within the resource block sets a single property.
20
+ # These properties are simply camelized using the ActiveSupport gem's `camelize` function.
21
+ # This means that the `cidr_block` function will set the `CidrBlock` property.
22
+ cidr_block '172.42.0.0/16'
23
+
24
+ # Following this pattern, `enable_dns_support` sets the `EnableDnsSupport` property.
25
+ enable_dns_support true
26
+ enable_dns_hostnames true
27
+ instance_tenancy 'default'
28
+
29
+ # The `tag` function is available on all resources, and adds keys to the resource's `Tags` property. It accepts the following arguments:
30
+ # 1) Tag name (symbol or string)
31
+ # 2) Tag value
32
+ tag :DefaultVpc, true
33
+
34
+ # Parameters are required at template generation time, and therefore may be referenced using the `parameters` hash anywhere in a template.
35
+ # This will render the parameter value as a string constant in the CloudFormation JSON output
36
+ tag :Name, parameters[:VpcName]
37
+ end
38
+
39
+ # If there are no properties to set on a resource, the block may be omitted entirely
40
+ resource :defaultigw, 'AWS::EC2::InternetGateway'
41
+
42
+ resource :vpcigw, 'AWS::EC2::VPCGatewayAttachment' do
43
+ # Fn::ref serves the same purpose as CloudFormation's {"Ref": ""} intrinsic function.
44
+ vpc_id Fn::ref(:vpc)
45
+ internet_gateway_id Fn::ref(:defaultigw)
46
+ end
47
+
48
+ resource :routetable, 'AWS::EC2::RouteTable' do
49
+ vpc_id Fn::ref(:vpc)
50
+ end
51
+
52
+ (1..3).each do |i|
53
+ resource "subnet#{i}", 'AWS::EC2::Subnet' do
54
+ # Other CloudFormation intrinsics, such as `Fn::Select` and `AWS::Region` are available as Ruby objects
55
+ # Inspecting these functions will reveal that they simply return a Ruby hash representing the same CloudFormation structures
56
+ availability_zone Fn::select(i, Fn::get_azs(AWS::region))
57
+ cidr_block "172.42.#{i}.0/24"
58
+ vpc_id Fn::ref(:vpc)
59
+ end
60
+
61
+ resource "srta#{i}".to_sym, 'AWS::EC2::SubnetRouteTableAssociation' do
62
+ subnet_id Fn::ref("subnet#{i}")
63
+ route_table_id Fn::ref(:routetable)
64
+ end
65
+
66
+ # Functions do not need to be called in any particular order.
67
+ # The `output` function defines a stack output, which may be referenced from another stack using the `@stack_name.output_name` format
68
+ output "subnetid#{i}", Fn::ref("subnet#{i}")
69
+ end
70
+
71
+ # The `resource` function accepts one additional parameter that was not addressed above: the options hash
72
+ # Additional options passed here will be placed inside the resource, but outside the `Properties` block.
73
+ # In this case, we've specified that the default route explicitly depends on the VPC Internet Gateway.
74
+ # (As of this writing, this is actually a required workaround for this template,
75
+ # because the gateway must be attached to the VPC before a route can be created to it.)
76
+ resource :defaultroute, 'AWS::EC2::Route', DependsOn: [:vpcigw] do
77
+ route_table_id Fn::ref(:routetable)
78
+ gateway_id Fn::ref(:defaultigw)
79
+ destination_cidr_block '0.0.0.0/0'
80
+ end
81
+
82
+ output :vpcid, Fn::ref(:vpc)
83
+
84
+
data/lib/cfer.rb ADDED
@@ -0,0 +1,236 @@
1
+ require 'active_support/all'
2
+ require 'aws-sdk'
3
+ require 'logger'
4
+ require 'json'
5
+ require 'rugged'
6
+ require 'preconditions'
7
+ require 'rainbow'
8
+
9
+
10
+ # Contains extensions that Cfer will dynamically use
11
+ module CferExt
12
+ module AWS
13
+ end
14
+ end
15
+
16
+ # Contains the core Cfer logic
17
+ module Cfer
18
+ module Cfn
19
+ end
20
+
21
+ module Core
22
+ end
23
+
24
+ # The Cfer logger
25
+ LOGGER = Logger.new(STDERR)
26
+ LOGGER.level = Logger::INFO
27
+ LOGGER.formatter = proc { |severity, datetime, progname, msg|
28
+ msg = case severity
29
+ when 'FATAL'
30
+ Rainbow(msg).red.bright
31
+ when 'ERROR'
32
+ Rainbow(msg).red
33
+ when 'WARN'
34
+ Rainbow(msg).yellow
35
+ when 'DEBUG'
36
+ Rainbow(msg).black.bright
37
+ else
38
+ msg
39
+ end
40
+
41
+ "#{msg}\n"
42
+ }
43
+
44
+ class << self
45
+
46
+ def converge!(stack_name, options = {})
47
+ config(options)
48
+ tmpl = options[:template] || "#{stack_name}.rb"
49
+ cfn = options[:aws_options] || {}
50
+
51
+ cfn_stack = Cfer::Cfn::Client.new(cfn.merge(stack_name: stack_name))
52
+ stack = Cfer::stack_from_file(tmpl, options.merge(client: cfn_stack))
53
+
54
+ begin
55
+ cfn_stack.converge(stack, options)
56
+ if options[:follow]
57
+ tail! stack_name, options
58
+ end
59
+ rescue Aws::CloudFormation::Errors::ValidationError => e
60
+ Cfer::LOGGER.info "CFN validation error: #{e.message}"
61
+ end
62
+ describe! stack_name, options
63
+ end
64
+
65
+ def describe!(stack_name, options = {})
66
+ config(options)
67
+ cfn = options[:aws_options] || {}
68
+ cfn_stack = Cfer::Cfn::Client.new(cfn.merge(stack_name: stack_name)).fetch_stack
69
+
70
+ Cfer::LOGGER.debug "Describe stack: #{cfn_stack}"
71
+
72
+ case options[:output_format] || 'table'
73
+ when 'json'
74
+ puts render_json(cfn_stack, options)
75
+ when 'table'
76
+ puts "Status: #{cfn_stack[:stack_status]}"
77
+ puts "Description: #{cfn_stack[:description]}" if cfn_stack[:description]
78
+ puts ""
79
+ parameters = (cfn_stack[:parameters] || []).map { |param| {:Type => "Parameter", :Key => param[:parameter_key], :Value => param[:parameter_value]} }
80
+ outputs = (cfn_stack[:outputs] || []).map { |output| {:Type => "Output", :Key => output[:output_key], :Value => output[:output_value]} }
81
+ tp parameters + outputs, :Type, :Key, {:Value => {:width => 80}}
82
+ else
83
+ raise Cfer::Util::CferError, "Invalid output format #{options[:output_format]}."
84
+ end
85
+ end
86
+
87
+ def tail!(stack_name, options = {}, &block)
88
+ config(options)
89
+ cfn = options[:aws_options] || {}
90
+ cfn_client = Cfer::Cfn::Client.new(cfn.merge(stack_name: stack_name))
91
+ if block
92
+ cfn_client.tail(options, &block)
93
+ else
94
+ cfn_client.tail(options) do |event|
95
+ Cfer::LOGGER.info "%s %-30s %-40s %-20s %s" % [event.timestamp, color_map(event.resource_status), event.resource_type, event.logical_resource_id, event.resource_status_reason]
96
+ end
97
+ end
98
+ describe! stack_name, options
99
+ end
100
+
101
+ def generate!(tmpl, options = {})
102
+ config(options)
103
+ cfn_stack = Cfer::Cfn::Client.new(options[:aws_options] || {})
104
+ stack = Cfer::stack_from_file(tmpl, options.merge(client: cfn_stack)).to_h
105
+ puts render_json(stack, options)
106
+ end
107
+
108
+ # Builds a Cfer::Core::Stack from a Ruby block
109
+ #
110
+ # @param options [Hash] The stack options
111
+ # @param block The block containing the Cfn DSL
112
+ # @option options [Hash] :parameters The CloudFormation stack parameters
113
+ # @return [Cfer::Core::Stack] The assembled stack object
114
+ def stack_from_block(options = {}, &block)
115
+ s = Cfer::Core::Stack.new(options)
116
+ templatize_errors('block') do
117
+ s.build_from_block(&block)
118
+ end
119
+ s
120
+ end
121
+
122
+ # Builds a Cfer::Core::Stack from a ruby script
123
+ #
124
+ # @param file [String] The file containing the Cfn DSL
125
+ # @param options [Hash] (see #stack_from_block)
126
+ # @return [Cfer::Core::Stack] The assembled stack object
127
+ def stack_from_file(file, options = {})
128
+ s = Cfer::Core::Stack.new(options)
129
+ templatize_errors(file) do
130
+ s.build_from_file file
131
+ end
132
+ s
133
+ end
134
+
135
+ private
136
+
137
+ def config(options)
138
+ Cfer::LOGGER.debug "Options: #{options}"
139
+ Cfer::LOGGER.level = Logger::DEBUG if options[:verbose]
140
+
141
+ Aws.config.update region: options[:region] if options[:region]
142
+ Aws.config.update credentials: Aws::SharedCredentials.new(profile_name: options[:profile]) if options[:profile]
143
+ end
144
+
145
+ def render_json(obj, options = {})
146
+ if options[:pretty_print]
147
+ puts JSON.pretty_generate(obj)
148
+ else
149
+ puts obj.to_json
150
+ end
151
+ end
152
+
153
+ def templatize_errors(base_loc)
154
+ yield
155
+ rescue SyntaxError => e
156
+ raise Cfer::Util::TemplateError.new([]), e.message
157
+ rescue StandardError => e
158
+ raise Cfer::Util::TemplateError.new(convert_backtrace(base_loc, e)), e.message
159
+ end
160
+
161
+ def convert_backtrace(base_loc, exception)
162
+ continue_search = true
163
+ exception.backtrace_locations.take_while { |loc|
164
+ continue_search = false if loc.path == base_loc
165
+ continue_search || loc.path == base_loc
166
+ }
167
+ end
168
+
169
+
170
+ COLORS_MAP = {
171
+ 'CREATE_IN_PROGRESS' => {
172
+ color: :yellow
173
+ },
174
+ 'DELETE_IN_PROGRESS' => {
175
+ color: :yellow
176
+ },
177
+ 'UPDATE_IN_PROGRESS' => {
178
+ color: :green
179
+ },
180
+
181
+ 'CREATE_FAILED' => {
182
+ color: :red,
183
+ finished: true
184
+ },
185
+ 'DELETE_FAILED' => {
186
+ color: :red,
187
+ finished: true
188
+ },
189
+ 'UPDATE_FAILED' => {
190
+ color: :red,
191
+ finished: true
192
+ },
193
+
194
+ 'CREATE_COMPLETE' => {
195
+ color: :green,
196
+ finished: true
197
+ },
198
+ 'DELETE_COMPLETE' => {
199
+ color: :green,
200
+ finished: true
201
+ },
202
+ 'UPDATE_COMPLETE' => {
203
+ color: :green,
204
+ finished: true
205
+ },
206
+
207
+ 'DELETE_SKIPPED' => {
208
+ color: :yellow
209
+ },
210
+
211
+ 'ROLLBACK_IN_PROGRESS' => {
212
+ color: :red
213
+ },
214
+ 'ROLLBACK_COMPLETE' => {
215
+ color: :red,
216
+ finished: true
217
+ }
218
+ }
219
+
220
+ def color_map(str)
221
+ if COLORS_MAP.include?(str)
222
+ Rainbow(str).send(COLORS_MAP[str][:color])
223
+ else
224
+ str
225
+ end
226
+ end
227
+
228
+ def stopped_state?(str)
229
+ COLORS_MAP[str][:finished] || false
230
+ end
231
+ end
232
+ end
233
+
234
+ Dir["#{File.dirname(__FILE__)}/cfer/**/*.rb"].each { |f| require(f) }
235
+ Dir["#{File.dirname(__FILE__)}/cferext/**/*.rb"].each { |f| require(f) }
236
+
data/lib/cfer/block.rb ADDED
@@ -0,0 +1,33 @@
1
+ require 'docile'
2
+
3
+ module Cfer
4
+ # Represents the base class of a Cfer DSL
5
+ class Block < ActiveSupport::HashWithIndifferentAccess
6
+ # Evaluates a DSL directly from a Ruby block, calling pre- and post- hooks.
7
+ # @param args [Array<Object>] Extra arguments to be passed into the block.
8
+ def build_from_block(*args, &block)
9
+ pre_block
10
+ Docile.dsl_eval(self, *args, &block) if block
11
+ post_block
12
+ self
13
+ end
14
+
15
+ # Evaluates a DSL from a Ruby script file
16
+ # @param args [Array<Object>] (see: #build_from_block)
17
+ # @param file [File] The Ruby script to evaluate
18
+ def build_from_file(*args, file)
19
+ build_from_block(*args) do
20
+ instance_eval File.read(file), file
21
+ end
22
+ self
23
+ end
24
+
25
+ # Executed just before the DSL is evaluated
26
+ def pre_block
27
+ end
28
+
29
+ # Executed just after the DSL is evaluated
30
+ def post_block
31
+ end
32
+ end
33
+ end