convection 0.0.1 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +26 -8
  4. data/.rubocop_todo.yml +77 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +10 -0
  7. data/Gemfile +9 -0
  8. data/README.md +27 -2
  9. data/Rakefile +11 -1
  10. data/bin/convection +49 -0
  11. data/convection.gemspec +5 -7
  12. data/example/.ruby-version +1 -0
  13. data/example/Cloudfile +13 -0
  14. data/example/deprecated/elb.rb +27 -0
  15. data/example/deprecated/iam_access_key.rb +18 -0
  16. data/example/deprecated/iam_group.rb +31 -0
  17. data/example/{iam_role.rb → deprecated/iam_role.rb} +21 -32
  18. data/example/deprecated/iam_user.rb +31 -0
  19. data/example/deprecated/rds.rb +70 -0
  20. data/example/{s3.rb → deprecated/s3.rb} +0 -0
  21. data/example/deprecated/sqs.rb +32 -0
  22. data/example/deprecated/vpc.rb +85 -0
  23. data/example/foobar.rb +22 -0
  24. data/example/output/vpc.json +335 -0
  25. data/example/security-groups.rb +40 -0
  26. data/example/trust_cloudtrail.rb +24 -0
  27. data/example/vpc.rb +63 -81
  28. data/ext/resource_generator.sh +21 -0
  29. data/lib/convection.rb +5 -4
  30. data/lib/convection/control/cloud.rb +59 -0
  31. data/lib/convection/control/stack.rb +261 -60
  32. data/lib/convection/dsl/helpers.rb +63 -5
  33. data/lib/convection/model/attributes.rb +60 -0
  34. data/lib/convection/model/cloudfile.rb +58 -0
  35. data/lib/convection/model/diff.rb +39 -0
  36. data/lib/convection/model/event.rb +62 -0
  37. data/lib/convection/model/exceptions.rb +18 -0
  38. data/lib/convection/model/mixin/cidr_block.rb +4 -4
  39. data/lib/convection/model/mixin/colorize.rb +20 -0
  40. data/lib/convection/model/mixin/conditional.rb +1 -3
  41. data/lib/convection/model/mixin/policy.rb +89 -0
  42. data/lib/convection/model/mixin/protocol.rb +29 -0
  43. data/lib/convection/model/mixin/taggable.rb +2 -2
  44. data/lib/convection/model/template.rb +248 -21
  45. data/lib/convection/model/template/condition.rb +56 -0
  46. data/lib/convection/model/template/mapping.rb +4 -3
  47. data/lib/convection/model/template/output.rb +9 -7
  48. data/lib/convection/model/template/parameter.rb +19 -4
  49. data/lib/convection/model/template/resource.rb +317 -23
  50. data/lib/convection/model/template/resource/aws_auto_scaling_auto_scaling_group.rb +39 -0
  51. data/lib/convection/model/template/resource/aws_auto_scaling_launch_configuration.rb +30 -0
  52. data/lib/convection/model/template/resource/aws_auto_scaling_scaling_policy.rb +20 -0
  53. data/lib/convection/model/template/resource/aws_cloud_watch_alarm.rb +31 -0
  54. data/lib/convection/model/template/resource/aws_ec2_instance.rb +10 -46
  55. data/lib/convection/model/template/resource/aws_ec2_internet_gateway.rb +3 -14
  56. data/lib/convection/model/template/resource/aws_ec2_network_acl.rb +45 -0
  57. data/lib/convection/model/template/resource/aws_ec2_network_acl_entry.rb +27 -0
  58. data/lib/convection/model/template/resource/aws_ec2_route.rb +7 -40
  59. data/lib/convection/model/template/resource/aws_ec2_route_table.rb +2 -17
  60. data/lib/convection/model/template/resource/aws_ec2_security_group.rb +24 -30
  61. data/lib/convection/model/template/resource/aws_ec2_security_group_ingres.rb +25 -0
  62. data/lib/convection/model/template/resource/aws_ec2_subnet.rb +21 -28
  63. data/lib/convection/model/template/resource/aws_ec2_subnet_network_acl_association.rb +18 -0
  64. data/lib/convection/model/template/resource/aws_ec2_subnet_route_table_association.rb +3 -24
  65. data/lib/convection/model/template/resource/aws_ec2_vpc.rb +20 -22
  66. data/lib/convection/model/template/resource/aws_ec2_vpc_gateway_attachment.rb +4 -28
  67. data/lib/convection/model/template/resource/aws_elasticache_cluster.rb +24 -0
  68. data/lib/convection/model/template/resource/aws_elasticache_parameter_group.rb +19 -0
  69. data/lib/convection/model/template/resource/aws_elasticache_security_group.rb +17 -0
  70. data/lib/convection/model/template/resource/aws_elasticache_security_group_ingress.rb +19 -0
  71. data/lib/convection/model/template/resource/aws_elb.rb +39 -0
  72. data/lib/convection/model/template/resource/aws_iam_access_key.rb +19 -0
  73. data/lib/convection/model/template/resource/aws_iam_group.rb +18 -0
  74. data/lib/convection/model/template/resource/aws_iam_instance_profile.rb +21 -0
  75. data/lib/convection/model/template/resource/aws_iam_policy.rb +28 -24
  76. data/lib/convection/model/template/resource/aws_iam_role.rb +88 -19
  77. data/lib/convection/model/template/resource/aws_iam_user.rb +53 -0
  78. data/lib/convection/model/template/resource/aws_logs_loggroup.rb +33 -0
  79. data/lib/convection/model/template/resource/aws_rds_db_instance.rb +59 -0
  80. data/lib/convection/model/template/resource/aws_rds_db_parameter_group.rb +27 -0
  81. data/lib/convection/model/template/resource/aws_rds_db_security_group.rb +40 -0
  82. data/lib/convection/model/template/resource/aws_rds_db_subnet_group.rb +26 -0
  83. data/lib/convection/model/template/resource/aws_route53_health_check.rb +17 -0
  84. data/lib/convection/model/template/resource/aws_route53_recordset.rb +30 -0
  85. data/lib/convection/model/template/resource/aws_s3_bucket.rb +8 -44
  86. data/lib/convection/model/template/resource/aws_s3_bucket_policy.rb +14 -19
  87. data/lib/convection/model/template/resource/aws_sns_topic.rb +19 -0
  88. data/lib/convection/model/template/resource/aws_sqs_queue.rb +31 -0
  89. data/lib/convection/model/template/resource/aws_sqs_queue_policy.rb +18 -0
  90. data/test/convection/model/test_conditions.rb +121 -0
  91. data/test/convection/model/test_elasticache.rb +97 -0
  92. data/test/convection/model/test_loggroups.rb +25 -0
  93. data/test/convection/model/test_rds.rb +76 -0
  94. data/test/convection/model/test_template.rb +64 -0
  95. data/test/convection/model/test_validation.rb +216 -0
  96. data/test/test_helper.rb +17 -0
  97. metadata +131 -50
@@ -0,0 +1,40 @@
1
+ require_relative '../lib/convection'
2
+
3
+ module Convection
4
+ module Demo
5
+ SECURITY_GROUPS = Convection.template do
6
+ description 'Demo Security Groups'
7
+
8
+ ec2_security_group 'FoobarELB' do
9
+ vpc stack.get('vpc', 'id')
10
+ description 'Foobar ELB Ingress'
11
+
12
+ ingress_rule(:tcp, 80, '0.0.0.0/0')
13
+ ingress_rule(:tcp, 443, '0.0.0.0/0')
14
+
15
+ tag 'Name', "sg-foobar-elb-#{ stack.cloud }"
16
+ tag 'Service', 'foobar'
17
+ tag 'Resource', 'ELB'
18
+ tag 'Scope', 'public'
19
+ tag 'Stack', stack.cloud
20
+
21
+ with_output
22
+ end
23
+
24
+ ec2_security_group 'Foobar' do
25
+ vpc stack.get('vpc', 'id')
26
+ description 'Foobar Ingress'
27
+
28
+ ingress_rule(:tcp, 8080) { source_group fn_ref('FoobarELB') }
29
+
30
+ tag 'Name', "sg-foobar-#{ stack.cloud }"
31
+ tag 'Service', 'foobar'
32
+ tag 'Resource', 'EC2'
33
+ tag 'Scope', 'private'
34
+ tag 'Stack', stack.cloud
35
+
36
+ with_output
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,24 @@
1
+ require 'convection'
2
+
3
+ module CLOUDTRAIL
4
+ #IAM role to create a log stream & put events
5
+ iam_role 'role' do
6
+ path "/"
7
+ #defines trust relationship
8
+ trust_cloudtrail
9
+
10
+ policy 'CreateStreamPolicy' do
11
+ allow do
12
+ resource 'arn:aws:logs:*:*:*'
13
+ action 'logs:CreateLogStream'
14
+ end
15
+ end
16
+
17
+ policy 'PutEventsPolicy' do
18
+ allow do
19
+ resource 'arn:aws:logs:*:*:*'
20
+ action 'logs:PutLogEvents'
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,85 +1,67 @@
1
- #!/usr/bin/env ruby
2
- require 'convection'
3
-
4
- test_template = Convection.template do
5
- description 'This is a test stack generated with Convection'
6
-
7
- parameter 'InstanceSize' do
8
- type 'String'
9
- description 'Instance Size'
10
- default 'm3.medium'
11
-
12
- allow 'm3.medium'
13
- allow 'm3.large'
14
- allow 'm3.xlarge'
15
- end
16
-
17
- mapping 'RegionalAMIs' do
18
- item 'us-east-1', 'hvm', 'ami-76e27e1e'
19
- item 'us-west-1', 'hvm', 'ami-d5180890'
20
- item 'us-east-1', 'pv', 'ami-64e27e0c'
21
- item 'us-west-1', 'pv', 'ami-c5180880'
22
- end
23
-
24
- mapping 'RegionalKeys' do
25
- item 'us-east-1', 'test', 'cf-test-keys'
26
- item 'us-west-1', 'test', 'cf-test-keys'
27
- end
28
-
29
- ## Define the VPC
30
- ec2_vpc 'TargetVPC' do
31
- network '100.65.0.0/18'
32
- subnet_length 25
33
-
34
- ## Add an InternetGateway
35
- add_internet_gateway
36
-
37
- ## Add a default routing table
38
- public_table = add_route_table('Public', :gateway_route => true)
39
-
40
- ## Define Subnets and Insatnces in each availability zone
41
- stack.availability_zones do |zone, i|
42
- add_subnet "Test#{ i }" do
43
- availability_zone zone
44
- associate_route_table public_table
45
-
46
- tag 'Service', 'Foo'
1
+ require_relative '../lib/convection'
2
+
3
+ module Convection
4
+ module Demo
5
+ VPC = Convection.template do
6
+ description 'Demo VPC'
7
+
8
+ ## Define the VPC
9
+ ec2_vpc 'TargetVPC' do
10
+ network stack['subnet']
11
+ subnet_length 24
12
+ enable_dns true
13
+
14
+ tag 'Name', stack.cloud
15
+ tag 'Stack', stack.cloud
16
+ with_output 'id'
17
+
18
+ ## Add an InternetGateway
19
+ add_internet_gateway
20
+
21
+ public_acl = add_network_acl 'Public' do
22
+ entry 'AllowAllIngress' do
23
+ action 'allow'
24
+ number 100
25
+ network '0.0.0.0/0'
26
+ protocol :any
27
+ range :From => 0,
28
+ :To => 65_535
29
+ end
30
+
31
+ entry 'AllowAllEgress' do
32
+ action 'allow'
33
+ number 100
34
+ egress true
35
+ network '0.0.0.0/0'
36
+ protocol :any
37
+ range :From => 0,
38
+ :To => 65_535
39
+ end
40
+
41
+ tag 'Name', "acl-public-#{ stack.cloud }"
42
+ tag 'Stack', stack.cloud
43
+ end
44
+
45
+ public_table = add_route_table 'Public', :gateway_route => true do
46
+ tag 'Name', "routes-public-#{ stack.cloud }"
47
+ tag 'Stack', stack.cloud
48
+ end
49
+
50
+ stack.availability_zones do |zone, i|
51
+ add_subnet "Public#{ i }" do
52
+ availability_zone zone
53
+ acl public_acl
54
+ route_table public_table
55
+
56
+ with_output
57
+
58
+ immutable_metadata "public-#{ stack.cloud }"
59
+ tag 'Name', "subnet-public-#{ stack.cloud }-#{ zone }"
60
+ tag 'Stack', stack.cloud
61
+ tag 'Service', 'Public'
62
+ end
63
+ end
47
64
  end
48
65
  end
49
-
50
- tag 'Name', join('-', 'cf-test-vpc', fn_ref('AWS::StackName'))
51
- end
52
-
53
- ec2_security_group 'BetterSecurityGroup' do
54
- ingress_rule do
55
- cidr_ip '0.0.0.0/0'
56
- from 22
57
- to 22
58
- protocol 'TCP'
59
- end
60
- egress_rule do
61
- cidr_ip '0.0.0.0/0'
62
- from 0
63
- to 65_535
64
- protocol(-1)
65
- end
66
-
67
- description 'Allow SSH traffic from all of the places'
68
- vpc_id fn_ref('TargetVPC')
69
-
70
- tag 'Name', join('-', fn_ref('AWS::StackName'), 'BetterSecurityGroup')
71
66
  end
72
67
  end
73
-
74
- # puts test_template.render
75
- # puts test_template.to_json
76
-
77
- # stack_e1 = Convection.stack('TestStackE1B1', test_template, :region => 'us-east-1')
78
- stack_w1 = Convection.stack('TestStackW1B2', test_template, :region => 'us-west-1')
79
-
80
- # puts stack_e1.status
81
- # puts stack_e1.apply
82
- puts stack_w1.to_json
83
-
84
- puts "Status #{ stack_w1.status }"
85
- # puts stack_w1.apply
@@ -0,0 +1,21 @@
1
+ #!/bin/bash
2
+ #
3
+ # Given content like the following passed as the first argument
4
+ # this should build the methods needed to write most of the convection
5
+ # code for you. You will have to do some massaging of the method names,
6
+ # but this takes most of the grunt work out.
7
+ #
8
+ # Example:
9
+ # % . ./resource_generator.sh
10
+ # or add it to your .profile
11
+ #
12
+ # % resource_generator ""Count" : String,
13
+ #"Handle" : String,
14
+ #"Timeout" : String"
15
+ #echo $1 | sed "s/\"//g" | cut -d ':' -f 1
16
+ #echo $1 | sed "s/\"//g" | cut -d ':' -f 1 | tr -d ' ' | xargs -I {} printf "def {}\(value\)\n property\(\'{}\', value\)\nend\n\n"
17
+
18
+
19
+ resource_generator() {
20
+ echo $1 | sed "s/\"//g" | cut -d ':' -f 1 | tr -d ' ' | xargs -I {} printf "def {}\(value\)\n property\(\'{}\', value\)\nend\n\n"
21
+ }
@@ -3,16 +3,17 @@
3
3
  ##
4
4
  module Convection
5
5
  class << self
6
- def template(&block)
7
- Model::Template.new(&block)
6
+ def template(*args, &block)
7
+ Model::Template.new(*args, &block)
8
8
  end
9
9
 
10
- def stack(name, template, options = {})
11
- Control::Stack.new(name, template, options)
10
+ def stack(*args)
11
+ Control::Stack.new(*args)
12
12
  end
13
13
  end
14
14
  end
15
15
 
16
16
  require_relative 'convection/version'
17
+ require_relative 'convection/model/attributes'
17
18
  require_relative 'convection/model/template'
18
19
  require_relative 'convection/control/stack'
@@ -0,0 +1,59 @@
1
+ require_relative '../model/cloudfile'
2
+ require_relative '../model/event'
3
+
4
+ module Convection
5
+ module Control
6
+ ##
7
+ # Control tour Clouds
8
+ ##
9
+ class Cloud
10
+ def configure(cloudfile)
11
+ @cloudfile = Model::Cloudfile.new(cloudfile)
12
+ end
13
+
14
+ def stacks
15
+ @cloudfile.stacks
16
+ end
17
+
18
+ def deck
19
+ @cloudfile.deck
20
+ end
21
+
22
+ def converge(to_stack, &block)
23
+ unless to_stack.nil? || stacks.include?(to_stack)
24
+ block.call(Model::Event.new(:error, "Stack #{ to_stack } is not defined", :error)) if block
25
+ return
26
+ end
27
+
28
+ deck.each do |stack|
29
+ block.call(Model::Event.new(:converge, "Stack #{ stack.name }", :info)) if block
30
+ stack.apply(&block)
31
+
32
+ if stack.error?
33
+ block.call(Model::Event.new(:error, "Error converging stack #{ stack.name }", :error), stack.errors) if block
34
+ break
35
+ end
36
+
37
+ ## Stop on converge error
38
+ break unless stack.success?
39
+
40
+ ## Stop here
41
+ break if !to_stack.nil? && stack.name == to_stack
42
+ sleep rand @cloudfile.splay || 2
43
+ end
44
+ end
45
+
46
+ def diff(&block)
47
+ @cloudfile.deck.each do |stack|
48
+ block.call(Model::Event.new(:compare, "Compare local state of stack #{ stack.name } (#{ stack.cloud_name }) with remote template", :info))
49
+ sleep rand @cloudfile.splay || 2
50
+
51
+ difference = stack.diff
52
+ next block.call(Model::Event.new(:unchanged, "Stack #{ stack.cloud_name } Has no changes", :info)) if difference.empty?
53
+
54
+ difference.each { |diff| block.call(diff) }
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -7,139 +7,338 @@ module Convection
7
7
  # Instantiation of a template in an account/region
8
8
  ##
9
9
  class Stack
10
+ attr_reader :id
10
11
  attr_reader :name
11
12
  attr_accessor :template
13
+ attr_reader :exist
14
+ attr_reader :status
15
+ alias_method :exist?, :exist
12
16
 
17
+ attr_reader :attributes
18
+ attr_reader :errors
19
+ attr_reader :options
20
+ attr_reader :resources
21
+ attr_reader :attribute_mapping_values
22
+ attr_reader :outputs
23
+
24
+ ## AWS-SDK
13
25
  attr_accessor :region
26
+ attr_accessor :cloud
27
+ attr_reader :capabilities
14
28
  attr_accessor :credentials
15
29
  attr_reader :parameters
16
30
  attr_reader :tags
17
31
  attr_accessor :on_failure
18
- attr_reader :capabilities
19
- attr_reader :options
20
32
 
21
33
  ## Valid Stack Statuses
22
- CREATE_IN_PROGRESS = 'CREATE_IN_PROGRESS'
23
- CREATE_FAILED = 'CREATE_FAILED'
24
34
  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'
35
+ CREATE_FAILED = 'CREATE_FAILED'
36
+ CREATE_IN_PROGRESS = 'CREATE_IN_PROGRESS'
30
37
  DELETE_COMPLETE = 'DELETE_COMPLETE'
31
- UPDATE_IN_PROGRESS = 'UPDATE_IN_PROGRESS'
32
- UPDATE_COMPLETE_CLEANUP_IN_PROGRESS = 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS'
38
+ DELETE_FAILED = 'DELETE_FAILED'
39
+ DELETE_IN_PROGRESS = 'DELETE_IN_PROGRESS'
40
+ ROLLBACK_COMPLETE = 'ROLLBACK_COMPLETE'
41
+ ROLLBACK_FAILED = 'ROLLBACK_FAILED'
42
+ ROLLBACK_IN_PROGRESS = 'ROLLBACK_IN_PROGRESS'
33
43
  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'
44
+ UPDATE_COMPLETE_CLEANUP_IN_PROGRESS = 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS'
45
+ UPDATE_FAILED = 'UPDATE_FAILED'
46
+ UPDATE_IN_PROGRESS = 'UPDATE_IN_PROGRESS'
37
47
  UPDATE_ROLLBACK_COMPLETE = 'UPDATE_ROLLBACK_COMPLETE'
48
+ UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS = 'UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS'
49
+ UPDATE_ROLLBACK_FAILED = 'UPDATE_ROLLBACK_FAILED'
50
+ UPDATE_ROLLBACK_IN_PROGRESS = 'UPDATE_ROLLBACK_IN_PROGRESS'
38
51
 
39
52
  ## Internal status
40
53
  NOT_CREATED = 'NOT_CREATED'
41
54
 
42
55
  def initialize(name, template, options = {})
43
56
  @name = name
44
- @template = template
57
+ @template = template.clone(self)
58
+ @errors = []
45
59
 
60
+ @cloud = options.delete(:cloud)
61
+ @cloud_name = options.delete(:cloud_name)
46
62
  @region = options.delete(:region) { |_| 'us-east-1' }
47
63
  @credentials = options.delete(:credentials)
48
64
  @parameters = options.delete(:parameters) { |_| {} } # Default empty hash
49
65
  @tags = options.delete(:tags) { |_| {} } # Default empty hash
50
-
51
- ## There can be only one...
66
+ options.delete(:disable_rollback) # There can be only one...
52
67
  @on_failure = options.delete(:on_failure) { |_| 'DELETE' }
53
- options.delete(:disable_rollback)
54
-
55
68
  @capabilities = options.delete(:capabilities) { |_| ['CAPABILITY_IAM'] }
69
+
70
+ @attributes = options.delete(:attributes) { |_| Model::Attributes.new }
56
71
  @options = options
57
72
 
58
- @ec2_client = AWS::EC2::Client.new(:region => region,
59
- :credentials => @credentials)
60
- @cf_client = AWS::CloudFormation::Client.new(:region => region,
61
- :credentials => @credentials)
73
+ client_options = {}.tap do |opt|
74
+ opt[:region] = @region
75
+ opt[:credentials] = @credentials unless @credentials.nil?
76
+ end
77
+ @ec2_client = Aws::EC2::Client.new(client_options)
78
+ @cf_client = Aws::CloudFormation::Client.new(client_options)
79
+
80
+ ## Remote state
81
+ @exist = false
82
+ @status = NOT_CREATED
83
+ @id = nil
84
+ @outputs = {}
85
+ @resources = {}
86
+ @current_template = {}
87
+ @last_event_seen = nil
88
+
89
+ ## Get initial state
90
+ get_status(cloud_name)
91
+ return unless exist?
92
+
93
+ get_resources
94
+ get_template
95
+ resource_attributes
96
+ get_events(1) # Get the latest page of events (Set @last_event_seen before starting)
97
+ rescue Aws::Errors::ServiceError => e
98
+ @errors << e
62
99
  end
63
100
 
64
- def stacks
65
- @stacks || cf_get_stacks
101
+ def cloud_name
102
+ return @cloud_name unless @cloud_name.nil?
103
+ return name if cloud.nil?
104
+ "#{ cloud }-#{ name }"
66
105
  end
67
106
 
68
- def status
69
- stacks[name].stack_status rescue NOT_CREATED
107
+ ##
108
+ # Attribute Accessors
109
+ ##
110
+ def include?(stack, key = nil)
111
+ return @attributes.include?(name, stack) if key.nil?
112
+ @attributes.include?(stack, key)
70
113
  end
71
114
 
72
- def exist?
73
- stacks.include?(name)
115
+ def [](key)
116
+ @attributes.get(name, key)
74
117
  end
75
118
 
76
- def complete?
77
- [CREATE_COMPLETE, UPDATE_COMPLETE].include?(status)
119
+ def []=(key, value)
120
+ @attributes.set(name, key, value)
78
121
  end
79
122
 
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)
123
+ def get(*args)
124
+ @attributes.get(*args)
125
+ end
126
+
127
+ ##
128
+ # Stack State
129
+ ##
130
+ def in_progress?
131
+ [CREATE_IN_PROGRESS, ROLLBACK_IN_PROGRESS, DELETE_IN_PROGRESS,
132
+ UPDATE_IN_PROGRESS, UPDATE_COMPLETE_CLEANUP_IN_PROGRESS,
133
+ UPDATE_ROLLBACK_IN_PROGRESS,
134
+ UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS
135
+ ].include?(status)
136
+ end
137
+
138
+ def complete?
139
+ [CREATE_COMPLETE, ROLLBACK_COMPLETE, UPDATE_COMPLETE, UPDATE_ROLLBACK_COMPLETE].include?(status)
85
140
  end
86
141
 
87
142
  def fail?
88
143
  [CREATE_FAILED, ROLLBACK_FAILED, DELETE_FAILED, UPDATE_ROLLBACK_FAILED].include?(status)
89
144
  end
90
145
 
146
+ def error?
147
+ !errors.empty?
148
+ end
149
+
150
+ def success?
151
+ !error? && complete?
152
+ end
153
+
154
+ ##
155
+ # Rendderers
156
+ ##
91
157
  def render
92
- template.render(self)
158
+ @template.render
159
+ end
160
+
161
+ def to_json(pretty = false)
162
+ @template.to_json(nil, pretty)
93
163
  end
94
164
 
95
- def to_json
96
- template.to_json(self)
165
+ def diff
166
+ @template.diff(@current_template)
97
167
  end
98
168
 
99
- def apply
100
- cf_get_stacks ## force-update status
169
+ ##
170
+ # Controllers
171
+ ##
172
+ def apply(&block)
101
173
  request_options = @options.clone.tap do |o|
102
- o[:stack_name] = name
103
174
  o[:template_body] = to_json
104
175
  o[:parameters] = cf_parameters
105
176
  o[:capabilities] = capabilities
106
177
  end
107
178
 
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.'
179
+ if exist?
180
+ if diff.empty? ## No Changes. Just get resources and move on
181
+ block.call(Model::Event.new(:complete, "Stack #{ name } has no changes", :info)) if block
182
+ get_status
183
+ return
184
+ end
185
+
186
+ ## Update
187
+ @cf_client.update_stack(request_options.tap do |o|
188
+ o[:stack_name] = id
189
+ end)
190
+ else
191
+ ## Create
192
+ @cf_client.create_stack(request_options.tap do |o|
193
+ o[:stack_name] = cloud_name
194
+
195
+ o[:tags] = cf_tags
196
+ o[:on_failure] = on_failure
197
+ end)
198
+
199
+ get_status(cloud_name) # Get ID of new stack
200
+ end
201
+
202
+ watch(&block) if block # Block execution on stack status
203
+ rescue Aws::Errors::ServiceError => e
204
+ @errors << e
117
205
  end
118
206
 
119
- def delete
207
+ def delete(&block)
120
208
  @cf_client.delete_stack(
121
- :stack_name => name
209
+ :stack_name => id
122
210
  )
211
+
212
+ ## Block execution on stack status
213
+ watch(&block) if block
214
+
215
+ get_status
216
+ rescue Aws::Errors::ServiceError => e
217
+ @errors << e
218
+ end
219
+
220
+ def watch(poll = 2, &block)
221
+ get_status
222
+
223
+ loop do
224
+ get_events.reverse_each do |event|
225
+ block.call(Model::Event.from_cf(event))
226
+ end if block
227
+
228
+ break unless in_progress?
229
+
230
+ sleep poll
231
+ get_status
232
+ end
233
+ rescue Aws::Errors::ServiceError => e
234
+ @errors << e
123
235
  end
124
236
 
125
237
  def availability_zones(&block)
126
238
  @availability_zones ||=
127
- @ec2_client.describe_availability_zones.availability_zone_info.map(&:zone_name).sort
239
+ @ec2_client.describe_availability_zones.availability_zones.map(&:zone_name).sort
128
240
 
129
241
  @availability_zones.each_with_index(&block) if block
130
242
  @availability_zones
131
243
  end
132
244
 
245
+ def validate
246
+ result = @cf_client.validate_template(:template_body => template.to_json)
247
+ fail result.context.http_response.inspect unless result.successful?
248
+ puts "\nTemplate validated successfully"
249
+ end
250
+
133
251
  private
134
252
 
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
253
+ def get_status(stack_name = id)
254
+ cf_stack = @cf_client.describe_stacks(:stack_name => stack_name).stacks.first
255
+
256
+ @id = cf_stack.stack_id
257
+ @status = cf_stack.stack_status
258
+ @exist = true
259
+
260
+ ## Parse outputs
261
+ @outputs = {}.tap do |collection|
262
+ cf_stack.outputs.each do |output|
263
+ collection[output[:output_key].to_s] = (JSON.parse(output[:output_value]) rescue output[:output_value])
141
264
  end
142
265
  end
266
+
267
+ ## Add outputs to attribute set
268
+ @attributes.load_outputs(self)
269
+ rescue Aws::CloudFormation::Errors::ValidationError # Stack does not exist
270
+ @exist = false
271
+ @status = NOT_CREATED
272
+ @id = nil
273
+ @outputs = {}
274
+ end
275
+
276
+ ## Fetch current resources
277
+ def get_resources
278
+ @resources = {}.tap do |collection|
279
+ @cf_client.list_stack_resources(:stack_name => @id).each do |page|
280
+ page.stack_resource_summaries.each do |resource|
281
+ collection[resource[:logical_resource_id]] = resource
282
+ end
283
+ end
284
+ end
285
+ rescue Aws::CloudFormation::Errors::ValidationError # Stack does not exist
286
+ @resources = {}
287
+ end
288
+
289
+ def get_template
290
+ @current_template = JSON.parse(@cf_client.get_template(:stack_name => id).template_body)
291
+ rescue Aws::CloudFormation::Errors::ValidationError # Stack does not exist
292
+ @current_template = {}
293
+ end
294
+
295
+ ## Fetch new stack events
296
+ def get_events(pages = nil, stack_name = id)
297
+ return [] unless exist?
298
+
299
+ [].tap do |collection|
300
+ @cf_client.describe_stack_events(:stack_name => stack_name).each do |page|
301
+ pages -= 1 unless pages.nil?
302
+
303
+ page.stack_events.each do |event|
304
+ if @last_event_seen == event.event_id
305
+ pages = 0 # Break page loop
306
+ break
307
+ end
308
+
309
+ collection << event
310
+ end
311
+
312
+ break if pages == 0
313
+ end
314
+
315
+ @last_event_seen = collection.first.event_id unless collection.empty?
316
+ end
317
+ rescue Aws::CloudFormation::Errors::ValidationError # Stack does not exist
318
+ end
319
+
320
+ ## TODO No. This will become unnecessary as current_state is fleshed out
321
+ def resource_attributes
322
+ @attribute_mapping_values = {}
323
+ @template.execute ## Populate mappings fro the template
324
+
325
+ @resources.each do |logical, resource|
326
+ next unless @template.attribute_mappings.include?(logical)
327
+
328
+ attribute_map = @template.attribute_mappings[logical]
329
+ case attribute_map[:type].to_sym
330
+ when :string
331
+ @attribute_mapping_values[attribute_map[:name]] = resource[:physical_resource_id]
332
+ when :array
333
+ @attribute_mapping_values[attribute_map[:name]] = [] unless @attribute_mapping_values[attribute_map[:name]].is_a?(Array)
334
+ @attribute_mapping_values[attribute_map[:name]].push(resource[:physical_resource_id])
335
+ else
336
+ fail TypeError, "Attribute Mapping must be defined with type `string` or `array`, not #{ type }"
337
+ end
338
+ end
339
+
340
+ ## Add mapped resource IDs to attributes
341
+ @attributes.load_resources(self)
143
342
  end
144
343
 
145
344
  def cf_parameters
@@ -163,3 +362,5 @@ module Convection
163
362
  end
164
363
  end
165
364
  end
365
+
366
+ require_relative '../model/event'