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