convection 0.0.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.
- 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'
|