convection 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rubocop.yml +16 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE +23 -0
  6. data/README.md +224 -0
  7. data/Rakefile +2 -0
  8. data/Thorfile +5 -0
  9. data/convection.gemspec +27 -0
  10. data/example/iam_role.rb +63 -0
  11. data/example/s3.rb +13 -0
  12. data/example/vpc.rb +85 -0
  13. data/lib/convection.rb +18 -0
  14. data/lib/convection/control/stack.rb +165 -0
  15. data/lib/convection/dsl/helpers.rb +15 -0
  16. data/lib/convection/dsl/intrinsic_functions.rb +79 -0
  17. data/lib/convection/model/mixin/cidr_block.rb +17 -0
  18. data/lib/convection/model/mixin/conditional.rb +21 -0
  19. data/lib/convection/model/mixin/taggable.rb +48 -0
  20. data/lib/convection/model/template.rb +127 -0
  21. data/lib/convection/model/template/mapping.rb +42 -0
  22. data/lib/convection/model/template/output.rb +37 -0
  23. data/lib/convection/model/template/parameter.rb +44 -0
  24. data/lib/convection/model/template/resource.rb +64 -0
  25. data/lib/convection/model/template/resource/aws_ec2_instance.rb +69 -0
  26. data/lib/convection/model/template/resource/aws_ec2_internet_gateway.rb +55 -0
  27. data/lib/convection/model/template/resource/aws_ec2_route.rb +55 -0
  28. data/lib/convection/model/template/resource/aws_ec2_route_table.rb +60 -0
  29. data/lib/convection/model/template/resource/aws_ec2_security_group.rb +104 -0
  30. data/lib/convection/model/template/resource/aws_ec2_subnet.rb +66 -0
  31. data/lib/convection/model/template/resource/aws_ec2_subnet_route_table_association.rb +39 -0
  32. data/lib/convection/model/template/resource/aws_ec2_vpc.rb +116 -0
  33. data/lib/convection/model/template/resource/aws_ec2_vpc_gateway_attachment.rb +43 -0
  34. data/lib/convection/model/template/resource/aws_iam_policy.rb +45 -0
  35. data/lib/convection/model/template/resource/aws_iam_role.rb +45 -0
  36. data/lib/convection/model/template/resource/aws_s3_bucket.rb +67 -0
  37. data/lib/convection/model/template/resource/aws_s3_bucket_policy.rb +40 -0
  38. data/lib/convection/version.rb +6 -0
  39. 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'