convection 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rubocop.yml +16 -0
- data/Gemfile +4 -0
- data/LICENSE +23 -0
- data/README.md +224 -0
- data/Rakefile +2 -0
- data/Thorfile +5 -0
- data/convection.gemspec +27 -0
- data/example/iam_role.rb +63 -0
- data/example/s3.rb +13 -0
- data/example/vpc.rb +85 -0
- data/lib/convection.rb +18 -0
- data/lib/convection/control/stack.rb +165 -0
- data/lib/convection/dsl/helpers.rb +15 -0
- data/lib/convection/dsl/intrinsic_functions.rb +79 -0
- data/lib/convection/model/mixin/cidr_block.rb +17 -0
- data/lib/convection/model/mixin/conditional.rb +21 -0
- data/lib/convection/model/mixin/taggable.rb +48 -0
- data/lib/convection/model/template.rb +127 -0
- data/lib/convection/model/template/mapping.rb +42 -0
- data/lib/convection/model/template/output.rb +37 -0
- data/lib/convection/model/template/parameter.rb +44 -0
- data/lib/convection/model/template/resource.rb +64 -0
- data/lib/convection/model/template/resource/aws_ec2_instance.rb +69 -0
- data/lib/convection/model/template/resource/aws_ec2_internet_gateway.rb +55 -0
- data/lib/convection/model/template/resource/aws_ec2_route.rb +55 -0
- data/lib/convection/model/template/resource/aws_ec2_route_table.rb +60 -0
- data/lib/convection/model/template/resource/aws_ec2_security_group.rb +104 -0
- data/lib/convection/model/template/resource/aws_ec2_subnet.rb +66 -0
- data/lib/convection/model/template/resource/aws_ec2_subnet_route_table_association.rb +39 -0
- data/lib/convection/model/template/resource/aws_ec2_vpc.rb +116 -0
- data/lib/convection/model/template/resource/aws_ec2_vpc_gateway_attachment.rb +43 -0
- data/lib/convection/model/template/resource/aws_iam_policy.rb +45 -0
- data/lib/convection/model/template/resource/aws_iam_role.rb +45 -0
- data/lib/convection/model/template/resource/aws_s3_bucket.rb +67 -0
- data/lib/convection/model/template/resource/aws_s3_bucket_policy.rb +40 -0
- data/lib/convection/version.rb +6 -0
- metadata +375 -0
data/lib/convection.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
##
|
2
|
+
# Root module
|
3
|
+
##
|
4
|
+
module Convection
|
5
|
+
class << self
|
6
|
+
def template(&block)
|
7
|
+
Model::Template.new(&block)
|
8
|
+
end
|
9
|
+
|
10
|
+
def stack(name, template, options = {})
|
11
|
+
Control::Stack.new(name, template, options)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
require_relative 'convection/version'
|
17
|
+
require_relative 'convection/model/template'
|
18
|
+
require_relative 'convection/control/stack'
|
@@ -0,0 +1,165 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Convection
|
5
|
+
module Control
|
6
|
+
##
|
7
|
+
# Instantiation of a template in an account/region
|
8
|
+
##
|
9
|
+
class Stack
|
10
|
+
attr_reader :name
|
11
|
+
attr_accessor :template
|
12
|
+
|
13
|
+
attr_accessor :region
|
14
|
+
attr_accessor :credentials
|
15
|
+
attr_reader :parameters
|
16
|
+
attr_reader :tags
|
17
|
+
attr_accessor :on_failure
|
18
|
+
attr_reader :capabilities
|
19
|
+
attr_reader :options
|
20
|
+
|
21
|
+
## Valid Stack Statuses
|
22
|
+
CREATE_IN_PROGRESS = 'CREATE_IN_PROGRESS'
|
23
|
+
CREATE_FAILED = 'CREATE_FAILED'
|
24
|
+
CREATE_COMPLETE = 'CREATE_COMPLETE'
|
25
|
+
ROLLBACK_IN_PROGRESS = 'ROLLBACK_IN_PROGRESS'
|
26
|
+
ROLLBACK_FAILED = 'ROLLBACK_FAILED'
|
27
|
+
ROLLBACK_COMPLETE = 'ROLLBACK_COMPLETE'
|
28
|
+
DELETE_IN_PROGRESS = 'DELETE_IN_PROGRESS'
|
29
|
+
DELETE_FAILED = 'DELETE_FAILED'
|
30
|
+
DELETE_COMPLETE = 'DELETE_COMPLETE'
|
31
|
+
UPDATE_IN_PROGRESS = 'UPDATE_IN_PROGRESS'
|
32
|
+
UPDATE_COMPLETE_CLEANUP_IN_PROGRESS = 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS'
|
33
|
+
UPDATE_COMPLETE = 'UPDATE_COMPLETE'
|
34
|
+
UPDATE_ROLLBACK_IN_PROGRESS = 'UPDATE_ROLLBACK_IN_PROGRESS'
|
35
|
+
UPDATE_ROLLBACK_FAILED = 'UPDATE_ROLLBACK_FAILED'
|
36
|
+
UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS = 'UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS'
|
37
|
+
UPDATE_ROLLBACK_COMPLETE = 'UPDATE_ROLLBACK_COMPLETE'
|
38
|
+
|
39
|
+
## Internal status
|
40
|
+
NOT_CREATED = 'NOT_CREATED'
|
41
|
+
|
42
|
+
def initialize(name, template, options = {})
|
43
|
+
@name = name
|
44
|
+
@template = template
|
45
|
+
|
46
|
+
@region = options.delete(:region) { |_| 'us-east-1' }
|
47
|
+
@credentials = options.delete(:credentials)
|
48
|
+
@parameters = options.delete(:parameters) { |_| {} } # Default empty hash
|
49
|
+
@tags = options.delete(:tags) { |_| {} } # Default empty hash
|
50
|
+
|
51
|
+
## There can be only one...
|
52
|
+
@on_failure = options.delete(:on_failure) { |_| 'DELETE' }
|
53
|
+
options.delete(:disable_rollback)
|
54
|
+
|
55
|
+
@capabilities = options.delete(:capabilities) { |_| ['CAPABILITY_IAM'] }
|
56
|
+
@options = options
|
57
|
+
|
58
|
+
@ec2_client = AWS::EC2::Client.new(:region => region,
|
59
|
+
:credentials => @credentials)
|
60
|
+
@cf_client = AWS::CloudFormation::Client.new(:region => region,
|
61
|
+
:credentials => @credentials)
|
62
|
+
end
|
63
|
+
|
64
|
+
def stacks
|
65
|
+
@stacks || cf_get_stacks
|
66
|
+
end
|
67
|
+
|
68
|
+
def status
|
69
|
+
stacks[name].stack_status rescue NOT_CREATED
|
70
|
+
end
|
71
|
+
|
72
|
+
def exist?
|
73
|
+
stacks.include?(name)
|
74
|
+
end
|
75
|
+
|
76
|
+
def complete?
|
77
|
+
[CREATE_COMPLETE, UPDATE_COMPLETE].include?(status)
|
78
|
+
end
|
79
|
+
|
80
|
+
def rollback?
|
81
|
+
[ROLLBACK_IN_PROGRESS, ROLLBACK_FAILED, ROLLBACK_COMPLETE,
|
82
|
+
UPDATE_ROLLBACK_IN_PROGRESS, UPDATE_ROLLBACK_FAILED,
|
83
|
+
UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS,
|
84
|
+
UPDATE_ROLLBACK_COMPLETE].include?(status)
|
85
|
+
end
|
86
|
+
|
87
|
+
def fail?
|
88
|
+
[CREATE_FAILED, ROLLBACK_FAILED, DELETE_FAILED, UPDATE_ROLLBACK_FAILED].include?(status)
|
89
|
+
end
|
90
|
+
|
91
|
+
def render
|
92
|
+
template.render(self)
|
93
|
+
end
|
94
|
+
|
95
|
+
def to_json
|
96
|
+
template.to_json(self)
|
97
|
+
end
|
98
|
+
|
99
|
+
def apply
|
100
|
+
cf_get_stacks ## force-update status
|
101
|
+
request_options = @options.clone.tap do |o|
|
102
|
+
o[:stack_name] = name
|
103
|
+
o[:template_body] = to_json
|
104
|
+
o[:parameters] = cf_parameters
|
105
|
+
o[:capabilities] = capabilities
|
106
|
+
end
|
107
|
+
|
108
|
+
return @cf_client.update_stack(request_options) if exist?
|
109
|
+
@cf_client.create_stack(request_options.tap do |o|
|
110
|
+
o[:tags] = cf_tags
|
111
|
+
o[:on_failure] = @on_failure
|
112
|
+
end)
|
113
|
+
rescue AWS::CloudFormation::Errors::ValidationError => e
|
114
|
+
## TODO Return something sane
|
115
|
+
## SDK throws this as an error >.<
|
116
|
+
raise e unless e.message == 'No updates are to be performed.'
|
117
|
+
end
|
118
|
+
|
119
|
+
def delete
|
120
|
+
@cf_client.delete_stack(
|
121
|
+
:stack_name => name
|
122
|
+
)
|
123
|
+
end
|
124
|
+
|
125
|
+
def availability_zones(&block)
|
126
|
+
@availability_zones ||=
|
127
|
+
@ec2_client.describe_availability_zones.availability_zone_info.map(&:zone_name).sort
|
128
|
+
|
129
|
+
@availability_zones.each_with_index(&block) if block
|
130
|
+
@availability_zones
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def cf_get_stacks
|
136
|
+
@stacks = {}.tap do |col|
|
137
|
+
cf_stacks = @cf_client.list_stacks.stack_summaries rescue []
|
138
|
+
cf_stacks.each do |s|
|
139
|
+
next if s.stack_status == DELETE_COMPLETE
|
140
|
+
col[s.stack_name] = s
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def cf_parameters
|
146
|
+
parameters.map do |p|
|
147
|
+
{
|
148
|
+
:parameter_key => p[0].to_s,
|
149
|
+
:parameter_value => p[1].to_s,
|
150
|
+
:use_previous_value => false
|
151
|
+
}
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def cf_tags
|
156
|
+
tags.map do |p|
|
157
|
+
{
|
158
|
+
:key => p[0].to_s,
|
159
|
+
:value => p[1].to_s
|
160
|
+
}
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Convection
|
2
|
+
module DSL
|
3
|
+
##
|
4
|
+
# Template DSL
|
5
|
+
##
|
6
|
+
module Helpers
|
7
|
+
def attribute(name)
|
8
|
+
define_method(name) do |value = nil|
|
9
|
+
instance_variable_set("@#{ name }", value) unless value.nil?
|
10
|
+
instance_variable_get("@#{ name }")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Convection
|
2
|
+
module DSL
|
3
|
+
##
|
4
|
+
# Formatting helpers for Intrinsic Functions
|
5
|
+
module IntrinsicFunctions
|
6
|
+
def base64(content)
|
7
|
+
{
|
8
|
+
'Fn::Base64' => content
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
def fn_and(*conditions)
|
13
|
+
{
|
14
|
+
'Fn::And' => conditions
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def fn_equals(value_1, value_2)
|
19
|
+
{
|
20
|
+
'Fn::Equals' => [value_1, value_2]
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def fn_if(condition, value_true, value_false)
|
25
|
+
{
|
26
|
+
'Fn::If' => [condition, value_true, value_false]
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def fn_not(condition)
|
31
|
+
{
|
32
|
+
'Fn::Not' => [condition]
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def fn_or(*conditions)
|
37
|
+
{
|
38
|
+
'Fn::Or' => conditions
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
def find_in_map(map_name, key_1, key_2)
|
43
|
+
{
|
44
|
+
'Fn::FindInMap' => [map_name, key_1, key_2]
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
def get_att(resource, attr_name)
|
49
|
+
{
|
50
|
+
'Fn::GetAtt' => [resource, attr_name]
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
def get_azs(region)
|
55
|
+
{
|
56
|
+
'Fn::GetAZs' => region
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
def join(delimiter, *values)
|
61
|
+
{
|
62
|
+
'Fn::Join' => [delimiter, values]
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
def select(index, *objects)
|
67
|
+
{
|
68
|
+
'Fn::Select' => [index, objects]
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
def fn_ref(resource)
|
73
|
+
{
|
74
|
+
'Ref' => resource
|
75
|
+
}
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'netaddr'
|
2
|
+
|
3
|
+
module Convection
|
4
|
+
module Model
|
5
|
+
module Mixin
|
6
|
+
##
|
7
|
+
# Add condition helpers
|
8
|
+
##
|
9
|
+
module CIDRBlock
|
10
|
+
def network(*args)
|
11
|
+
@network = NetAddr::CIDR.create(*args) unless args.empty?
|
12
|
+
property('CidrBlock', @network.to_s)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Convection
|
2
|
+
module Model
|
3
|
+
module Mixin
|
4
|
+
##
|
5
|
+
# Add condition helpers
|
6
|
+
##
|
7
|
+
module Conditional
|
8
|
+
def condition(setter = nil)
|
9
|
+
@condition = setter unless setter.nil?
|
10
|
+
@condition
|
11
|
+
end
|
12
|
+
|
13
|
+
def render_condition(resource)
|
14
|
+
resource.tap do |r|
|
15
|
+
r['Condition'] = condition unless condition.nil?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Convection
|
4
|
+
module Model
|
5
|
+
##
|
6
|
+
# CloudFormation ResourceTag set
|
7
|
+
##
|
8
|
+
class Tags < Hash
|
9
|
+
def render
|
10
|
+
map do |t|
|
11
|
+
{
|
12
|
+
:Key => t[0].to_s,
|
13
|
+
:Value => t[1]
|
14
|
+
}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module Mixin
|
20
|
+
##
|
21
|
+
# Add tag helpers to taddable resources
|
22
|
+
##
|
23
|
+
module Taggable
|
24
|
+
def tags
|
25
|
+
@tags ||= Tags.new
|
26
|
+
end
|
27
|
+
|
28
|
+
def tag(key, value)
|
29
|
+
tags[key] = value
|
30
|
+
end
|
31
|
+
|
32
|
+
## Helper for Asgard
|
33
|
+
def immutable_metadata(purpose, target = '')
|
34
|
+
tag('immutable_metadata', JSON.generate(
|
35
|
+
:purpose => purpose,
|
36
|
+
:target => target
|
37
|
+
))
|
38
|
+
end
|
39
|
+
|
40
|
+
def render_tags(resource)
|
41
|
+
resource.tap do |r|
|
42
|
+
r['Properties']['Tags'] = tags.render unless tags.empty?
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require_relative '../dsl/helpers'
|
2
|
+
require_relative '../dsl/intrinsic_functions'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Convection
|
6
|
+
module DSL
|
7
|
+
##
|
8
|
+
# Template DSL
|
9
|
+
##
|
10
|
+
module Template
|
11
|
+
def parameter(name, &block)
|
12
|
+
pa = Model::Template::Parameter.new(name, self)
|
13
|
+
|
14
|
+
pa.instance_exec(&block) if block
|
15
|
+
parameters[name] = pa
|
16
|
+
end
|
17
|
+
|
18
|
+
def mapping(name, &block)
|
19
|
+
m = Model::Template::Mapping.new(name, self)
|
20
|
+
|
21
|
+
m.instance_exec(&block) if block
|
22
|
+
mappings[name] = m
|
23
|
+
end
|
24
|
+
|
25
|
+
# def condition(name, &block)
|
26
|
+
# c = Model::Template::Condition.new
|
27
|
+
# c.instance_exec(&block) if block
|
28
|
+
#
|
29
|
+
# conditions[name] = c
|
30
|
+
# end
|
31
|
+
|
32
|
+
def resource(name, &block)
|
33
|
+
r = Model::Template::Resource.new(name, self)
|
34
|
+
|
35
|
+
r.instance_exec(&block) if block
|
36
|
+
resources[name] = r
|
37
|
+
end
|
38
|
+
|
39
|
+
def output(name, &block)
|
40
|
+
o = Model::Template::Output.new(name, self)
|
41
|
+
|
42
|
+
o.instance_exec(&block) if block
|
43
|
+
outputs[name] = o
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
module Model
|
49
|
+
##
|
50
|
+
# Mapable hash
|
51
|
+
##
|
52
|
+
class Collection < Hash
|
53
|
+
def map(&block)
|
54
|
+
result = {}
|
55
|
+
|
56
|
+
each do |key, value|
|
57
|
+
result[key] = block.call(value)
|
58
|
+
end
|
59
|
+
|
60
|
+
result
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# Template container class
|
66
|
+
##
|
67
|
+
class Template
|
68
|
+
extend DSL::Helpers
|
69
|
+
|
70
|
+
include DSL::IntrinsicFunctions
|
71
|
+
include DSL::Template
|
72
|
+
|
73
|
+
DEFAULT_VERSION = '2010-09-09'
|
74
|
+
|
75
|
+
attribute :version
|
76
|
+
attribute :description
|
77
|
+
attribute :region
|
78
|
+
|
79
|
+
attr_reader :stack
|
80
|
+
attr_reader :parameters
|
81
|
+
attr_reader :mappings
|
82
|
+
attr_reader :conditions
|
83
|
+
attr_reader :resources
|
84
|
+
attr_reader :outputs
|
85
|
+
|
86
|
+
def initialize(stack = Control::Stack.new('default', self), &block)
|
87
|
+
@definition = block
|
88
|
+
@stack = stack
|
89
|
+
|
90
|
+
@version = DEFAULT_VERSION
|
91
|
+
@description = ''
|
92
|
+
|
93
|
+
@parameters = Collection.new
|
94
|
+
@mappings = Collection.new
|
95
|
+
@conditions = Collection.new
|
96
|
+
@resources = Collection.new
|
97
|
+
@outputs = Collection.new
|
98
|
+
end
|
99
|
+
|
100
|
+
def render(stack = nil)
|
101
|
+
## Instantiate a new template with the definition block and an other stack
|
102
|
+
return Template.new(stack, &@definition).render unless stack.nil?
|
103
|
+
|
104
|
+
instance_exec(&@definition)
|
105
|
+
{
|
106
|
+
'AWSTemplateFormatVersion' => version,
|
107
|
+
'Description' => description,
|
108
|
+
'Parameters' => parameters.map(&:render),
|
109
|
+
'Mappings' => mappings.map(&:render),
|
110
|
+
'Conditions' => conditions.map(&:render),
|
111
|
+
'Resources' => resources.map(&:render),
|
112
|
+
'Outputs' => outputs.map(&:render)
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
def to_json(stack = nil)
|
117
|
+
JSON.pretty_generate(render(stack))
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
require_relative 'template/parameter'
|
124
|
+
require_relative 'template/mapping'
|
125
|
+
# require_relative 'template/condition'
|
126
|
+
require_relative 'template/resource'
|
127
|
+
require_relative 'template/output'
|