convection 0.4.3 → 1.0.0.pre.beta.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 90b593fbb400063b388fc2f1ca38713b989a8d99
4
- data.tar.gz: a779862372fd3969006ab17f1f8729d06c4560bb
3
+ metadata.gz: 6ad55266b8d4a5fd1a16d8b8c396a3c2ecc22b27
4
+ data.tar.gz: 4c5ae734856b196c13f39fc42fd85fc69745a6bb
5
5
  SHA512:
6
- metadata.gz: c152eed6ec1f2c4ce484d74fb2401d7282e0f39d797bef322d6771d9725a014901ed12013a6b0bca719163ab891a42563fc37977ac720a9e867525874fd39441
7
- data.tar.gz: 0fc5f904fe4be7ce670b74d36304d40f44bd8581c11be1b55d647d399ca5b839d592eefd2e4d1e8183bb5d48133ec1062aafa54d9a0bf558e5055e5855aaa446
6
+ metadata.gz: 018341e258e456483c9f5ebde1116cd8a71196fb972b865c62f08f0f3b87dca8a0f010e6b9e9d995c36b39240b9cdc54b0dbb7abd9f58dd252951eed766c9447
7
+ data.tar.gz: 8a9a17d9ee215c6b6c1c26340e99bae7a13518f7a24d62362d02aa3302916a7f8cea93b07840c003cfa8f4859c65ffdf4af4b6496b256fb1101a2de42943cf96
@@ -0,0 +1,23 @@
1
+ <!-- Remove any sections that do not apply to your problems. HTML comments are ignored. -->
2
+ # Description
3
+ <!-- Describe your problem here. -->
4
+ <!-- Include references to relevant pull requests/commits here. -->
5
+
6
+ ## Expected behavior
7
+ <!-- Describe your expected behavior here. -->
8
+
9
+ ## Observed behavior
10
+ <!-- Describe your observed behavior here. -->
11
+
12
+ ## Steps to Reproduce
13
+ <!-- Describe the steps to reproduce the observed behavior. -->
14
+ <!-- Have a failing spec for this? :) -->
15
+
16
+ # Stack Trace or Log Messages
17
+ <!-- Use the following format for showing stack traces: -->
18
+ <details>
19
+ <summary>Stack trace for `brief error context`</summary>
20
+ <pre>
21
+ ... Full error context (collapses by default) ...
22
+ </pre>
23
+ </details>
@@ -0,0 +1,18 @@
1
+ <!-- Remove any sections that do not apply to your changes. HTML comments are ignored. -->
2
+ # Description
3
+ <!-- Describe your changes here. -->
4
+ <!-- Include references to relevant pull requests/commits here. -->
5
+ <!-- * Did you update documentation? -->
6
+ <!-- * Did you update test coverage? -->
7
+
8
+ # Motivation and Context
9
+ <!-- Describe the use case that requires your changes. -->
10
+
11
+ # Usage Examples
12
+ <!-- Code or screenshot examples of using your feature, if applicable. -->
13
+
14
+ # Testing Steps
15
+ <!-- List any steps required to test your changes. -->
16
+
17
+ # Post-merge Steps
18
+ <!-- List any steps required after merging your changes. -->
@@ -1,3 +1,4 @@
1
+ Copyright (c) 2016 Bryan Call and Erran Carey, Rapid7 LLC.
1
2
  Copyright (c) 2015 John Manero, Rapid7 LLC.
2
3
 
3
4
  MIT License
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
- # Convection [![Build Status](https://travis-ci.org/rapid7/convection.svg)](https://travis-ci.org/rapid7/convection)
1
+ # Convection [![Build Status](https://api.travis-ci.org/rapid7/convection.svg?branch=master)](https://travis-ci.org/rapid7/convection)
2
2
  _A fully generic, modular DSL for AWS CloudFormation_
3
3
 
4
4
  This gem aims to provide a reusable model for AWS CloudFormation in Ruby. It exposes a DSL for template definition, and a simple, decoupled abstraction of a CloudFormation Stack to compile and apply templates.
5
5
 
6
6
  ## Contributing
7
- Please read our [Contributing guidelines](CONTRIBUTING.md) for more information on contributing to Convection.
7
+ Please read our [Contributing guidelines](.github/CONTRIBUTING.md) for more information on contributing to Convection.
8
8
 
9
9
  ## Installation
10
10
  Add this line to your application's Gemfile:
@@ -23,9 +23,10 @@ Or install it yourself as:
23
23
 
24
24
  ##CLI Commands
25
25
  ###### Converging
26
- - To converge all stacks in your cloudfile run `convection converge`. If you provide the name of your stack as a additional argument such as `convection converge my-stack-name` then all stacks above and including the stack you specified will be converged.
26
+ - To converge all stacks in your cloudfile run `convection converge` in the same directory as your cloudfile or use `--cloudfiles` and specify the path to the cloudfile. If you provide the name of your stack as a additional argument such as `convection converge my-stack-name` then all stacks above and including the stack you specified will be converged.
27
27
  - To converge a stack group run `convection converge --stack_group YOUR_STACK_GROUP_NAME`
28
28
  - To converge a specific stack or a list of stacks run `convection converge --stacks stackA stackB ...`
29
+ - To converge multiple cloudfiles at the same time run use the `--cloudfiles` option providing the path to the cloudfiles. Example `bundle exec convection converge --cloudfiles us-east-1/Cloudfile eu-central-1/Cloudfile`
29
30
 
30
31
  ###### Diff
31
32
  - To display a diff between your local changes and the version of your stack in cloud formation of your changes run `convection diff`.
@@ -36,7 +37,7 @@ Or install it yourself as:
36
37
  - To print out a list of available cli options with their descriptions run `convection help`.
37
38
 
38
39
  ###### Print
39
- - To print out the cloud formation template for a specific stack run `convection print my-stack-name`.
40
+ - To print out the cloud formation template for a specific stack run `convection print-template my-stack-name`.
40
41
 
41
42
  ###### Validate
42
43
  - To validate your stack is not missing a required resource run `convection validate my-stack-name`.
@@ -45,29 +46,4 @@ Or install it yourself as:
45
46
  We highly recommend consulting the [getting started guide](./docs/getting-started.md) for a in depth walk through on how to to set up your project and create and deploy a stack. Example stacks and resources are available in the [convection/example](https://github.com/rapid7/convection/tree/master/example) folder
46
47
 
47
48
  ## License
48
- _Copyright (c) 2015 John Manero, Rapid7 LLC._
49
-
50
- ```
51
- MIT License
52
- ===========
53
-
54
- Permission is hereby granted, free of charge, to any person obtaining
55
- a copy of this software and associated documentation files (the
56
- "Software"), to deal in the Software without restriction, including
57
- without limitation the rights to use, copy, modify, merge, publish,
58
- distribute, sublicense, and/or sell copies of the Software, and to
59
- permit persons to whom the Software is furnished to do so, subject to
60
- the following conditions:
61
-
62
- The above copyright notice and this permission notice shall be
63
- included in all copies or substantial portions of the Software.
64
-
65
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
66
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
67
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
68
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
69
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
70
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
71
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
72
-
73
- ```
49
+ Convection is distributed under the MIT license - please refer to the [LICENSE](LICENSE.md) for more information.
@@ -1,47 +1,42 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'thor'
3
3
  require_relative '../lib/convection/control/cloud'
4
-
4
+ require 'thread'
5
5
  module Convection
6
6
  ##
7
7
  # Convection CLI
8
8
  ##
9
9
  class CLI < Thor
10
- class_option :cloudfile, :type => :string, :default => 'Cloudfile'
11
10
  def initialize(*args)
12
11
  super
13
- @cloud = Control::Cloud.new
14
12
  @cwd = Dir.getwd
13
+ @errors = false
15
14
  end
16
15
 
17
16
  desc 'converge STACK', 'Converge your cloud'
18
17
  option :stack_group, :type => :string, :desc => 'The name of a stack group defined in your cloudfile to converge'
19
18
  option :stacks, :type => :array, :desc => 'A ordered space separated list of stacks to converge'
19
+ option :verbose, :type => :boolean, :aliases => '--v', :desc => 'Show stack progress', default: true
20
+ option :'very-verbose', :type => :boolean, :aliases => '--vv', :desc => 'Show unchanged stacks', default: true
21
+ option :cloudfiles, :type => :array, :default => %w(Cloudfile)
22
+ option :delayed_output, :type => :boolean, :desc => 'Delay output until operation completion.', :default => false
20
23
  def converge(stack = nil)
21
- @cloud.configure(File.absolute_path(options['cloudfile'], @cwd))
22
- @cloud.converge(stack, stack_group: options[:stack_group], stacks: options[:stacks]) do |event, errors|
23
- say_status(*event.to_thor)
24
- errors.each do |error|
25
- say "* #{ error.message }"
26
- error.backtrace.each { |b| say " #{ b }" }
27
- end unless errors.nil?
28
- end
24
+ @outputs = []
25
+ operation('converge', stack)
26
+ print_outputs(@outputs) if @outputs && @outputs.any?
27
+ exit 1 if @errors
29
28
  end
30
29
 
31
30
  desc 'delete STACK', 'Delete stack(s) from your cloud'
32
31
  option :stack_group, :type => :string, :desc => 'The name of a stack group defined in your cloudfile to delete'
33
32
  option :stacks, :type => :array, :desc => 'A ordered space separated list of stacks to delete'
33
+ option :cloudfile, :type => :string, :default => 'Cloudfile'
34
+ option :verbose, :type => :boolean, :aliases => '--v', :desc => 'Show stack progress', default: true
35
+ option :'very-verbose', :type => :boolean, :aliases => '--vv', :desc => 'Show unchanged stacks', default: true
34
36
  def delete(stack = nil)
35
- @cloud.configure(File.absolute_path(options['cloudfile'], @cwd))
36
- print_status = lambda do |event, *errors|
37
- say_status(*event.to_thor)
38
- errors.flatten.each do |error|
39
- say "* #{ error.message }"
40
- error.backtrace.each { |b| say " #{ b }" }
41
- end unless errors.nil?
42
- end
37
+ init_cloud
43
38
 
44
- stacks = @cloud.stacks_until(stack, options, &print_status)
39
+ stacks = @cloud.stacks_until(stack, options, &method(:emit_events))
45
40
  if stacks.empty?
46
41
  say_status(:delete_failed, 'No stacks found matching the provided input (STACK, --stack-group, and/or --stacks).', :red)
47
42
  return
@@ -50,51 +45,183 @@ module Convection
50
45
 
51
46
  confirmation = ask('Are you sure you want to delete the above stack(s)?', limited_to: %w(yes no))
52
47
  if confirmation.eql?('yes')
53
- @cloud.delete(stacks, &print_status)
48
+ @cloud.delete(stacks, &method(:emit_events))
54
49
  else
55
50
  say_status(:delete_aborted, 'Aborted deletion of the above stack(s).', :green)
56
51
  end
57
52
  end
58
53
 
59
54
  desc 'diff STACK', 'Show changes that will be applied by converge'
60
- option :verbose, :type => :boolean, :aliases => '--v', :desc => 'Show stack progress'
61
- option :'very-verbose', :type => :boolean, :aliases => '--vv', :desc => 'Show unchanged stacks'
62
55
  option :stack_group, :type => :string, :desc => 'The name of a stack group defined in your cloudfile to diff'
63
56
  option :stacks, :type => :array, :desc => 'A ordered space separated list of stacks to diff'
57
+ option :verbose, :type => :boolean, :aliases => '--v', :desc => 'Show stack progress'
58
+ option :'very-verbose', :type => :boolean, :aliases => '--vv', :desc => 'Show unchanged stacks'
59
+ option :cloudfiles, :type => :array, :default => %w(Cloudfile)
60
+ option :delayed_output, :type => :boolean, :desc => 'Delay output until operation completion.', :default => false
64
61
  def diff(stack = nil)
62
+ @outputs = []
63
+ operation('diff', stack)
64
+ print_outputs(@outputs) if @outputs && @outputs.any?
65
+ exit 1 if @errors
66
+ end
67
+
68
+ desc 'print_template STACK', 'Print the rendered template for STACK'
69
+ option :cloudfile, :type => :string, :default => 'Cloudfile'
70
+ def print_template(stack)
71
+ init_cloud
72
+ puts @cloud.stacks[stack].to_json(true)
73
+ end
74
+
75
+ desc 'describe-tasks [--stacks STACKS]', 'Describe tasks for a given stack'
76
+ option :stacks, :type => :array, :desc => 'A ordered space separated list of stacks to diff', default: []
77
+ def describe_tasks
78
+ @cloud = Control::Cloud.new
65
79
  @cloud.configure(File.absolute_path(options['cloudfile'], @cwd))
66
- last_event = nil
67
80
 
68
- @cloud.diff(stack, stack_group: options[:stack_group], stacks: options[:stacks]) do |d|
69
- if d.is_a? Model::Event
70
- if options[:'very-verbose'] || d.name == :error
71
- say_status(*d.to_thor)
81
+ describe_stack_tasks(options[:stacks])
82
+ end
83
+
84
+ desc 'run-tasks [--stack STACK]', 'Run tasks for a given stack'
85
+ option :stack, :desc => 'The stack to run tasks for', :required => true
86
+ def run_tasks
87
+ @cloud = Control::Cloud.new
88
+ @cloud.configure(File.absolute_path(options['cloudfile'], @cwd))
89
+
90
+ run_stack_tasks(options[:stack])
91
+ end
92
+
93
+ desc 'validate STACK', 'Validate the rendered template for STACK'
94
+ option :cloudfile, :type => :string, :default => 'Cloudfile'
95
+ def validate(stack)
96
+ init_cloud
97
+ @cloud.stacks[stack].validate
98
+ end
99
+
100
+ no_commands do
101
+ attr_accessor :last_event
102
+
103
+ private
104
+
105
+ def operation(task_name, stack)
106
+ work_q = Queue.new
107
+ semaphore = Mutex.new
108
+ unless options[:delayed_output]
109
+ puts 'For easier reading when using multiple cloudfiles output can be delayed until task completion.'
110
+ puts 'If you would like delayed output please use the "--delayed_output true" option.'
111
+ end
112
+ options[:cloudfiles].each { |cloudfile| work_q.push(cloud: Control::Cloud.new, cloudfile_path: cloudfile) }
113
+ workers = (0...options[:cloudfiles].length).map do
114
+ Thread.new do
115
+ until work_q.empty?
116
+ output = []
117
+ cloud_array = work_q.pop(true)
118
+ cloud_array[:cloud].configure(File.absolute_path(cloud_array[:cloudfile_path], @cwd))
119
+ cloud = cloud_array[:cloud]
120
+ region = cloud.cloudfile.region
121
+ cloud.send(task_name, stack, stack_group: options[:stack_group], stacks: options[:stacks]) do |event, errors|
122
+ if options[:cloudfiles].length > 1 && options[:delayed_output]
123
+ output << { event: event, errors: errors }
124
+ else
125
+ emit_events(event, *errors, region: region)
126
+ end
127
+ semaphore.synchronize { @errors = errors.any? if errors }
128
+ end
129
+ if options[:cloudfiles].length > 1 && options[:delayed_output]
130
+ semaphore.synchronize { @outputs << { cloud_name: cloud.cloudfile.name, region: region, logging: output } }
131
+ end
132
+ end
133
+ end
134
+ end
135
+ workers.each(&:join)
136
+ end
137
+
138
+ def describe_stack_tasks(stacks_to_include)
139
+ @cloud.stacks.map do |stack_name, stack|
140
+ next if stacks_to_include.any? && !stacks_to_include.include?(stack_name)
141
+ tasks = stack.tasks.values.flatten.uniq
142
+ next if tasks.empty?
143
+
144
+ puts "Stack #{stack_name} (#{stack.cloud_name}) includes the following tasks:"
145
+ tasks.each_with_index do |task, index|
146
+ puts " #{index}. #{task}"
147
+ end
148
+ end
149
+ end
150
+
151
+ def run_stack_tasks(stack_name)
152
+ stack = @cloud.stacks[stack_name]
153
+ tasks = stack.tasks.values.flatten.uniq
154
+ if !stack
155
+ say_status(:task_failed, 'No stacks found matching the provided input (--stack).', :red)
156
+ exit 1
157
+ elsif tasks.empty?
158
+ say_status(:task_failed, "No tasks defined for the stack #{stack_name}. Define them in your Cloudfile.", :red)
159
+ exit 1
160
+ end
161
+
162
+ puts "The following tasks are available to execute for the stack #{stack_name} (#{stack.cloud_name}):"
163
+ tasks.each_with_index do |task, index|
164
+ puts " #{index}. #{task}"
165
+ end
166
+ choices = 0.upto(tasks.length - 1).map(&:to_s)
167
+ choice = ask('Which stack task would you like to execute? (ctrl-c to exit)', limited_to: choices)
168
+ task = tasks[choice.to_i]
169
+
170
+ say_status(:task_in_progress, "Task #{task} in progress for stack #{stack_name}.", :yellow)
171
+ task.call(stack)
172
+
173
+ if task.success?
174
+ say_status(:task_complete, "Task #{task} successfully completed for stack #{stack_name}.", :green)
175
+ else
176
+ say_status(:task_failed, "Task #{task} failed to complete for stack #{stack_name}.", :red)
177
+ exit 1
178
+ end
179
+ end
180
+
181
+ def emit_events(event, *errors, region: nil)
182
+ if event.is_a? Model::Event
183
+ if options[:'very-verbose'] || event.name == :error
184
+ print_info(event, region: region)
72
185
  elsif options[:verbose]
73
- say_status(*d.to_thor) if d.name == :compare
186
+ print_info(event, region: region) if event.name == :compare
74
187
  end
75
- last_event = d
76
- elsif d.is_a? Model::Diff
188
+ @last_event = event
189
+ elsif event.is_a? Model::Diff
77
190
  if !options[:'very-verbose'] && !options[:verbose]
78
- say_status(*last_event.to_thor) unless last_event.nil?
79
- last_event = nil
191
+ print_info(last_event, region: region) unless last_event.nil?
192
+ @last_event = nil
80
193
  end
81
- say_status(*d.to_thor)
194
+ print_info(event, region: region)
82
195
  else
83
- say_status(*d.to_thor)
196
+ print_info(event, region: region)
197
+ end
198
+
199
+ errors.each do |error|
200
+ say "* #{ error.message }"
201
+ error.backtrace.each { |trace| say " #{ trace }" }
84
202
  end
85
203
  end
86
- end
87
204
 
88
- desc 'print STACK', 'Print the rendered template for STACK'
89
- def print(stack)
90
- @cloud.configure(File.absolute_path(options['cloudfile'], @cwd))
91
- puts @cloud.stacks[stack].to_json(true)
92
- end
205
+ def print_info(say, region: nil)
206
+ print "#{region} " if region
207
+ say_status(*say.to_thor)
208
+ end
93
209
 
94
- desc 'validate STACK', 'Validate the rendered template for STACK'
95
- def validate(stack)
96
- @cloud.configure(File.absolute_path(options['cloudfile'], @cwd))
97
- @cloud.stacks[stack].validate
210
+ def print_outputs(outputs)
211
+ outputs.each do |output|
212
+ puts '********'
213
+ puts "Cloud name: #{output[:cloud_name]}. Region: #{output[:region]}."
214
+ puts '********'
215
+ output[:logging].each do |hash|
216
+ emit_events(hash[:event], *hash[:errors])
217
+ end
218
+ end
219
+ end
220
+
221
+ def init_cloud
222
+ @cloud = Control::Cloud.new
223
+ @cloud.configure(File.absolute_path(options['cloudfile'], @cwd))
224
+ end
98
225
  end
99
226
  end
100
227
  end
@@ -202,11 +202,11 @@ compare Compare local state of stack vpc (convection-demo-vpc) with remote temp
202
202
  create .Resources.DemoVPC.Properties.Tags.0.Key: Name
203
203
  create .Resources.DemoVPC.Properties.Tags.0.Value: convection-demo-vpc
204
204
  ```
205
- To see what the cloud formation template for your vpc template would look like you can run `convection print vpc`.
205
+ To see what the cloud formation template for your vpc template would look like you can run `convection print_template vpc`.
206
206
  This can help you verify that values referenced under the `stack` namespace are set correctly.
207
207
 
208
208
  ```json
209
- $> convection print vpc
209
+ $> convection print-template vpc
210
210
  {
211
211
  "AWSTemplateFormatVersion": "2010-09-09",
212
212
  "Description": "VPC with Public and Private Subnets (NAT)",
@@ -1,10 +1,10 @@
1
- # Convection [![Build Status](https://travis-ci.org/rapid7/convection.svg)](https://travis-ci.org/rapid7/convection)
1
+ # Convection [![Build Status](https://api.travis-ci.org/rapid7/convection.svg?branch=master)](https://travis-ci.org/rapid7/convection)
2
2
  _A fully generic, modular DSL for AWS CloudFormation_
3
3
 
4
4
  This gem aims to provide a reusable model for AWS CloudFormation in Ruby. It exposes a DSL for template definition, and a simple, decoupled abstraction of a CloudFormation Stack to compile and apply templates.
5
5
 
6
6
  ## Contributing
7
- Please read our [Contributing guidelines](CONTRIBUTING.md) for more information on contributing to Convection.
7
+ Please read our [Contributing guidelines](.github/CONTRIBUTING.md) for more information on contributing to Convection.
8
8
 
9
9
  ## Installation
10
10
  Add this line to your application's Gemfile:
@@ -23,9 +23,10 @@ Or install it yourself as:
23
23
 
24
24
  ##CLI Commands
25
25
  ###### Converging
26
- - To converge all stacks in your cloudfile run `convection converge`. If you provide the name of your stack as a additional argument such as `convection converge my-stack-name` then all stacks above and including the stack you specified will be converged.
26
+ - To converge all stacks in your cloudfile run `convection converge` in the same directory as your cloudfile or use `--cloudfiles` and specify the path to the cloudfile. If you provide the name of your stack as a additional argument such as `convection converge my-stack-name` then all stacks above and including the stack you specified will be converged.
27
27
  - To converge a stack group run `convection converge --stack_group YOUR_STACK_GROUP_NAME`
28
28
  - To converge a specific stack or a list of stacks run `convection converge --stacks stackA stackB ...`
29
+ - To converge multiple cloudfiles at the same time run use the `--cloudfiles` option providing the path to the cloudfiles. Example `bundle exec convection converge --cloudfiles us-east-1/Cloudfile eu-central-1/Cloudfile`
29
30
 
30
31
  ###### Diff
31
32
  - To display a diff between your local changes and the version of your stack in cloud formation of your changes run `convection diff`.
@@ -36,7 +37,7 @@ Or install it yourself as:
36
37
  - To print out a list of available cli options with their descriptions run `convection help`.
37
38
 
38
39
  ###### Print
39
- - To print out the cloud formation template for a specific stack run `convection print my-stack-name`.
40
+ - To print out the cloud formation template for a specific stack run `convection print-template my-stack-name`.
40
41
 
41
42
  ###### Validate
42
43
  - To validate your stack is not missing a required resource run `convection validate my-stack-name`.
@@ -45,29 +46,4 @@ Or install it yourself as:
45
46
  We highly recommend consulting the [getting started guide](./docs/getting-started.md) for a in depth walk through on how to to set up your project and create and deploy a stack. Example stacks and resources are available in the [convection/example](https://github.com/rapid7/convection/tree/master/example) folder
46
47
 
47
48
  ## License
48
- _Copyright (c) 2015 John Manero, Rapid7 LLC._
49
-
50
- ```
51
- MIT License
52
- ===========
53
-
54
- Permission is hereby granted, free of charge, to any person obtaining
55
- a copy of this software and associated documentation files (the
56
- "Software"), to deal in the Software without restriction, including
57
- without limitation the rights to use, copy, modify, merge, publish,
58
- distribute, sublicense, and/or sell copies of the Software, and to
59
- permit persons to whom the Software is furnished to do so, subject to
60
- the following conditions:
61
-
62
- The above copyright notice and this permission notice shall be
63
- included in all copies or substantial portions of the Software.
64
-
65
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
66
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
67
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
68
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
69
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
70
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
71
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
72
-
73
- ```
49
+ Convection is distributed under the MIT license - please refer to the [LICENSE](LICENSE.md) for more information.
@@ -7,6 +7,7 @@ module Convection
7
7
  # Control tour Clouds
8
8
  ##
9
9
  class Cloud
10
+ attr_accessor :cloudfile
10
11
  def configure(cloudfile)
11
12
  @cloudfile = Model::Cloudfile.new(cloudfile)
12
13
  end
@@ -69,10 +70,13 @@ module Convection
69
70
  return
70
71
  end
71
72
 
73
+ exit 1 if stack_initialization_errors?(&block)
74
+
72
75
  filter_deck(options, &block).each_value do |stack|
73
76
  block.call(Model::Event.new(:converge, "Stack #{ stack.name }", :info)) if block
74
77
  stack.apply(&block)
75
78
 
79
+ emit_credential_error_and_exit!(stack, &block) if stack.credential_error?
76
80
  if stack.error?
77
81
  block.call(Model::Event.new(:error, "Error converging stack #{ stack.name }", :error), stack.errors) if block
78
82
  break
@@ -88,6 +92,8 @@ module Convection
88
92
  end
89
93
 
90
94
  def delete(stacks, &block)
95
+ exit 1 if stack_initialization_errors?(&block)
96
+
91
97
  stacks.each do |stack|
92
98
  unless stack.exist?
93
99
  block.call(Model::Event.new(:delete_skipped, "Stack #{ stack.cloud_name } does not exist remotely", :warn))
@@ -114,10 +120,21 @@ module Convection
114
120
  return
115
121
  end
116
122
 
123
+ exit 1 if stack_initialization_errors?(&block)
124
+
117
125
  filter_deck(options, &block).each_value do |stack|
118
126
  block.call(Model::Event.new(:compare, "Compare local state of stack #{ stack.name } (#{ stack.cloud_name }) with remote template", :info))
119
127
 
120
128
  difference = stack.diff
129
+ # Find errors during diff
130
+ emit_credential_error_and_exit!(stack, &block) if stack.credential_error?
131
+ if stack.error?
132
+ errors = stack.errors.collect { |x| x.exception.message }
133
+ errors = errors.uniq.flatten
134
+ block.call(Model::Event.new(:error, "Error diffing stack #{ stack.name} Error(s): #{errors.join(', ')}", :error), stack.errors) if block
135
+ break
136
+ end
137
+
121
138
  if difference.empty?
122
139
  difference << Model::Event.new(:unchanged, "Stack #{ stack.cloud_name } has no changes", :info)
123
140
  end
@@ -125,9 +142,32 @@ module Convection
125
142
  difference.each { |diff| block.call(diff) }
126
143
 
127
144
  break if !to_stack.nil? && stack.name == to_stack
128
- sleep rand @cloudfile.splay || 2
129
145
  end
130
146
  end
147
+
148
+ def stack_initialization_errors?(&block)
149
+ errors = []
150
+ # Find errors during stack init
151
+ stacks.each_value do |stack|
152
+ if stack.error?
153
+ errors << stack.errors.collect { |x| x.exception.message }
154
+ end
155
+ end
156
+
157
+ unless errors.empty?
158
+ errors = errors.uniq.flatten
159
+ block.call(Model::Event.new(:error, "Error(s) during stack initialization #{errors.join(', ')}", :error), errors) if block
160
+ return true
161
+ end
162
+ false
163
+ end
164
+
165
+ def emit_credential_error_and_exit!(stack, &block)
166
+ event = Model::Event.new(:error, "Credentials expired while converging #{ stack.name }. " \
167
+ 'Visit the AWS console to track progress of the stack being created/updated.', :error)
168
+ block.call(event, stack.errors) if block
169
+ exit 1
170
+ end
131
171
  end
132
172
  end
133
173
  end
@@ -115,10 +115,12 @@ module Convection
115
115
 
116
116
  @attributes = options.delete(:attributes) { |_| Model::Attributes.new }
117
117
  @options = options
118
+ @retry_limit = options[:retry_limit] || 7
118
119
 
119
120
  client_options = {}.tap do |opt|
120
121
  opt[:region] = @region
121
122
  opt[:credentials] = @credentials unless @credentials.nil?
123
+ opt[:retry_limit] = @retry_limit
122
124
  end
123
125
  @ec2_client = Aws::EC2::Client.new(client_options)
124
126
  @cf_client = Aws::CloudFormation::Client.new(client_options)
@@ -143,10 +145,17 @@ module Convection
143
145
  # in the dependency tree to avoid the chicken-and-egg problem.
144
146
  @template.execute
145
147
 
146
- ## Get initial state
148
+ rescue Aws::Errors::ServiceError => e
149
+ @errors << e
150
+ end
151
+
152
+ def template_status
147
153
  get_status(cloud_name)
148
- return unless exist?
154
+ rescue Aws::Errors::ServiceError => e
155
+ @errors << e
156
+ end
149
157
 
158
+ def load_template_info
150
159
  get_resources
151
160
  get_template
152
161
  resource_attributes
@@ -185,6 +194,11 @@ module Convection
185
194
  @attributes.set(name, key, value)
186
195
  end
187
196
 
197
+ # @see Convection::Model::Attributes#fetch
198
+ def fetch(*args)
199
+ @attributes.fetch(*args)
200
+ end
201
+
188
202
  # @see Convection::Model::Attributes#get
189
203
  def get(*args)
190
204
  @attributes.get(*args)
@@ -209,6 +223,12 @@ module Convection
209
223
  [CREATE_COMPLETE, ROLLBACK_COMPLETE, UPDATE_COMPLETE, UPDATE_ROLLBACK_COMPLETE].include?(status)
210
224
  end
211
225
 
226
+ # @return [Boolean] whether a credential error occurred is the reason
227
+ # accessing the CloudFormation stack failed.
228
+ def credential_error?
229
+ error? && errors.all? { |e| e.class == Aws::EC2::Errors::RequestExpired }
230
+ end
231
+
212
232
  # @return [Boolean] whether the CloudFormation Stack is in one of
213
233
  # the several *_COMPLETE states.
214
234
  def delete_complete?
@@ -27,6 +27,12 @@ module Convection
27
27
  }
28
28
  end
29
29
 
30
+ def fn_import_value(value)
31
+ {
32
+ 'Fn::ImportValue' => value
33
+ }
34
+ end
35
+
30
36
  def fn_not(condition)
31
37
  {
32
38
  'Fn::Not' => [condition]
@@ -19,6 +19,14 @@ module Convection
19
19
  @parameters.include?(key) || @outputs.include?(key) || @resources.include?(key)
20
20
  end
21
21
 
22
+ def fetch(key, default = nil)
23
+ @parameters.fetch(key.to_s) do
24
+ @outputs.fetch(key.to_s) do
25
+ @resources.fetch(key.to_s, default)
26
+ end
27
+ end
28
+ end
29
+
22
30
  def [](key)
23
31
  @parameters[key.to_s] || @outputs[key.to_s] || @resources[key.to_s]
24
32
  end
@@ -40,6 +48,13 @@ module Convection
40
48
  @stacks.include?(stack) && @stacks[stack].include?(key)
41
49
  end
42
50
 
51
+ def fetch(stack, key, default = nil)
52
+ return get(stack, key, default) unless default.nil?
53
+ raise KeyError, "key '#{key}' not found for stack '#{stack}'" unless include?(stack, key)
54
+
55
+ @stacks[stack.to_s].fetch(key.to_s)
56
+ end
57
+
43
58
  def get(stack, key, default = nil)
44
59
  include?(stack, key) ? @stacks[stack.to_s][key.to_s] : default
45
60
  end
@@ -2,6 +2,7 @@ require_relative '../control/stack'
2
2
  require_relative '../dsl/helpers'
3
3
  require_relative '../model/attributes'
4
4
  require_relative '../model/template'
5
+ require 'thread'
5
6
 
6
7
  module Convection
7
8
  module DSL
@@ -14,6 +15,8 @@ module Convection
14
15
  attribute :name
15
16
  attribute :region
16
17
  attribute :splay
18
+ attribute :retry_limit
19
+ attribute :thread_count
17
20
 
18
21
  ## Helper to define a template in-line
19
22
  def template(*args, &block)
@@ -31,6 +34,7 @@ module Convection
31
34
  options[:region] ||= region
32
35
  options[:cloud] = name
33
36
  options[:attributes] = attributes
37
+ options[:retry_limit] = retry_limit
34
38
 
35
39
  @stacks[stack_name] = Control::Stack.new(stack_name, template, options, &block)
36
40
  @deck << @stacks[stack_name]
@@ -59,8 +63,22 @@ module Convection
59
63
  @stacks = {}
60
64
  @deck = []
61
65
  @stack_groups = {}
66
+ @thread_count ||= 2
62
67
 
63
68
  instance_eval(IO.read(cloudfile), cloudfile, 1)
69
+
70
+ work_q = Queue.new
71
+ @deck.each { |stack| work_q.push(stack) }
72
+ workers = (0...@thread_count).map do
73
+ Thread.new do
74
+ until work_q.empty?
75
+ stack = work_q.pop(true)
76
+ stack.template_status
77
+ stack.load_template_info if stack.exist?
78
+ end
79
+ end
80
+ end
81
+ workers.each(&:join)
64
82
  end
65
83
  end
66
84
  end
@@ -17,6 +17,7 @@ module Convection
17
17
  class << self
18
18
  ## Wrap private define_method
19
19
  def attach_resource(name, klass)
20
+ resource_dsl_methods[name.to_s] = klass
20
21
  define_method(name) do |rname, &block|
21
22
  resource = klass.new(rname, self)
22
23
  resource.instance_exec(&block) if block
@@ -26,10 +27,19 @@ module Convection
26
27
  end
27
28
 
28
29
  def attach_resource_collection(name, klass)
30
+ resource_collection_dsl_methods[name.to_s] = klass
29
31
  define_method(name) do |rname, &block|
30
32
  resource_collections[rname] = klass.new(rname, self, &block)
31
33
  end
32
34
  end
35
+
36
+ def resource_dsl_methods
37
+ @resource_dsl_methods ||= {}
38
+ end
39
+
40
+ def resource_collection_dsl_methods
41
+ @resource_collection_dsl_methods ||= {}
42
+ end
33
43
  end
34
44
  end
35
45
 
@@ -79,6 +89,12 @@ module Convection
79
89
  r = Model::Template::Resource.new(name, self)
80
90
 
81
91
  r.instance_exec(&block) if block
92
+ predefined_resources = DSL::Template::Resource.resource_dsl_methods.select { |_, resource_class| resource_class.type == r.type }.keys
93
+ if predefined_resources.any?
94
+ dsl_methods = predefined_resources.map { |resource| "##{resource}" }.join(', ')
95
+ warn "WARNING: The resource type #{r.type} is already defined. " \
96
+ "You can use any of the following resource methods instead of manually constructing a resource: #{dsl_methods}"
97
+ end
82
98
  resources[name] = r
83
99
  end
84
100
 
@@ -15,6 +15,7 @@ module Convection
15
15
  attribute :name
16
16
  attribute :value
17
17
  attribute :description
18
+ attribute :export_as
18
19
  attr_reader :template
19
20
 
20
21
  def initialize(name, parent)
@@ -31,6 +32,7 @@ module Convection
31
32
  'Description' => description
32
33
  }.tap do |output|
33
34
  render_condition(output)
35
+ output['Export'] = { 'Name' => export_as } if export_as
34
36
  end
35
37
  end
36
38
  end
@@ -8,8 +8,6 @@ module Convection
8
8
  # AWS::AutoScaling::AutoScalingGroup
9
9
  ##
10
10
  class AutoScalingGroup < Resource
11
- include Model::Mixin::Taggable
12
-
13
11
  type 'AWS::AutoScaling::AutoScalingGroup'
14
12
  property :availability_zone, 'AvailabilityZones', :array
15
13
  property :cooldown, 'Cooldown'
@@ -33,10 +31,34 @@ module Convection
33
31
  end
34
32
  end
35
33
 
34
+ def tag(key, value, propagate_at_launch: nil)
35
+ tags[key] = { value: value }
36
+ tags[key][:propagate_at_launch] = propagate_at_launch unless propagate_at_launch.nil?
37
+
38
+ tags[key]
39
+ end
40
+
41
+ def tags
42
+ @tags ||= {}
43
+ end
44
+
36
45
  def update_policy(&block)
37
46
  policy = ResourceAttribute::UpdatePolicy.new(self)
38
47
  policy.instance_exec(&block) if block
39
48
  end
49
+
50
+ private
51
+
52
+ def render_tags(resource)
53
+ rendered_tags = tags.map do |key, tag|
54
+ rendered = { 'Key' => key, 'Value' => tag[:value] }
55
+ rendered['PropagateAtLaunch'] = tag[:propagate_at_launch] if tag.key?(:propagate_at_launch)
56
+
57
+ rendered
58
+ end
59
+
60
+ resource['Properties']['Tags'] = rendered_tags unless rendered_tags.empty?
61
+ end
40
62
  end
41
63
  end
42
64
  end
@@ -9,6 +9,8 @@ module Convection
9
9
  class CloudFrontDefaultCacheBehavior < ResourceProperty
10
10
  property :allowed_methods, 'AllowedMethods', :type => :list, :default => %w(HEAD GET)
11
11
  property :cached_methods, 'CachedMethods', :type => :list
12
+ property :compress, 'Compress'
13
+ property :default_ttl, 'DefaultTTL'
12
14
  property :forwarded_values, 'ForwardedValues'
13
15
  property :min_ttl, 'MinTTL'
14
16
  property :smooth_streaming, 'SmoothStreaming'
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ module Convection::Model
4
+ describe Attributes do
5
+ subject { Attributes.new }
6
+
7
+ describe '#fetch' do
8
+ it 'raises a key error if the key was not defined' do
9
+ expect { subject.fetch('<stack-name>', '<attribute-key>') }.to raise_error(KeyError)
10
+ end
11
+
12
+ ['<truthy object>', true, false].each do |default|
13
+ it "supports #{default.inspect} as the default value when no value was set" do
14
+ expect { subject.fetch('<stack-name>', '<attribute-key>', default) }.to_not raise_error
15
+ end
16
+
17
+ it "supports #{default.inspect} as a default value" do
18
+ observed = subject.fetch('<stack-name>', '<attribute-key>', default)
19
+ expect(observed).to eq(default)
20
+ end
21
+ end
22
+
23
+ ['truthy object', true, false, nil].each do |value|
24
+ it "does not raise a key error when #{value.inspect} was previously set" do
25
+ subject.set('<stack-name>', '<attribute-key>', value)
26
+
27
+ expect { subject.fetch('<stack-name>', '<attribute-key>') }.to_not raise_error
28
+ end
29
+
30
+ it "supports #{value.inspect} as a return value" do
31
+ subject.set('<stack-name>', '<attribute-key>', value)
32
+
33
+ observed = subject.fetch('<stack-name>', '<attribute-key>')
34
+ expect(observed).to eq(value)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -10,8 +10,7 @@ class Convection::Model::Template
10
10
  fn_equals 'prod', 'prod'
11
11
  end
12
12
 
13
- resource 'SecurityGroup' do
14
- type 'AWS::EC2::SecurityGroup'
13
+ ec2_security_group 'SecurityGroup' do
15
14
  condition 'InProd'
16
15
  end
17
16
  end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ class Convection::Model::Template::Resource
4
+ describe AutoScalingGroup do
5
+ subject do
6
+ parent = double(:template)
7
+ allow(parent).to receive(:template).and_return(parent)
8
+
9
+ described_class.new('MyAutoScalingGroup', parent)
10
+ end
11
+
12
+ it 'should not render tags when none have been defined' do
13
+ expect(subject.render['Properties']['Tags']).to be_nil
14
+ end
15
+
16
+ it 'renders a regular tag' do
17
+ subject.tag '<tag-name>', '<tag-value>'
18
+ expect(subject.render['Properties']['Tags']).to include(a_hash_including('Key' => '<tag-name>'))
19
+ end
20
+
21
+ it 'renders a tag that should be propagated at launch' do
22
+ subject.tag '<tag-name>', '<tag-value>', propagate_at_launch: true
23
+ expect(subject.render['Properties']['Tags']).to include(a_hash_including('Key' => '<tag-name>', 'PropagateAtLaunch' => true))
24
+ end
25
+
26
+ it 'renders a tag that should not be propagated at launch' do
27
+ subject.tag '<tag-name>', '<tag-value>', propagate_at_launch: false
28
+ expect(subject.render['Properties']['Tags']).to include(a_hash_including('Key' => '<tag-name>', 'PropagateAtLaunch' => false))
29
+ end
30
+ end
31
+ end
@@ -19,6 +19,36 @@ class Convection::Model::Template
19
19
  end
20
20
  end
21
21
 
22
+ describe '#resource' do
23
+ context 'when defining a predefined resource' do
24
+ subject do
25
+ Convection.template do
26
+ resource 'FooInstance' do
27
+ type 'AWS::EC2::Instance'
28
+ end
29
+ end
30
+ end
31
+
32
+ it 'warns the user when they are using a predefined resource.' do
33
+ expect { subject.render }.to output(/.*already defined.*/).to_stderr
34
+ end
35
+ end
36
+
37
+ context 'when defining a undefined resource' do
38
+ subject do
39
+ Convection.template do
40
+ resource 'FooInstance' do
41
+ type 'FakeResource'
42
+ end
43
+ end
44
+ end
45
+
46
+ it 'warns the user when they are using a undefined resource.' do
47
+ expect { subject.render }.to_not output.to_stderr
48
+ end
49
+ end
50
+ end
51
+
22
52
  subject do
23
53
  template_json
24
54
  end
@@ -21,13 +21,13 @@ class Convection::Model::Template
21
21
  description 'Validations Test Template - Excessive Bytesize'
22
22
 
23
23
  200.times do |count|
24
- resource "EC2_INSTANCE_#{count}" do
25
- type 'AWS::EC2::Instance'
26
- property 'AvailabilityZone', 'us-east-1a'
24
+ ec2_instance "EC2_INSTANCE_#{count}" do
25
+ availability_zone 'us-east-1a'
27
26
  property 'ImageId', 'ami-76e27e1e'
28
27
  property 'KeyName', 'test'
29
- property 'SecurityGroupIds', ['sg-dd733c41', 'sg-dd738df3']
30
- property 'Tags', [{ 'Key' => 'Name', 'Value' => 'test-1' }]
28
+ security_group 'sg-dd733c41'
29
+ security_group 'sg-dd738df3'
30
+ tag 'Name', 'test-1'
31
31
  end
32
32
  end
33
33
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: convection
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.3
4
+ version: 1.0.0.pre.beta.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Manero
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-09-23 00:00:00.000000000 Z
11
+ date: 2016-10-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk
@@ -76,15 +76,17 @@ executables:
76
76
  extensions: []
77
77
  extra_rdoc_files: []
78
78
  files:
79
+ - ".github/CONTRIBUTING.md"
80
+ - ".github/ISSUE_TEMPLATE.md"
81
+ - ".github/PULL_REQUEST_TEMPLATE.md"
79
82
  - ".gitignore"
80
83
  - ".rubocop.yml"
81
84
  - ".rubocop_todo.yml"
82
85
  - ".ruby-version"
83
86
  - ".travis.yml"
84
87
  - ".yardopts"
85
- - CONTRIBUTING.md
86
88
  - Gemfile
87
- - LICENSE
89
+ - LICENSE.md
88
90
  - README.md
89
91
  - Rakefile
90
92
  - Thorfile
@@ -242,7 +244,9 @@ files:
242
244
  - spec/convection/control/stack/before_delete_tasks_spec.rb
243
245
  - spec/convection/control/stack/before_update_tasks_spec.rb
244
246
  - spec/convection/dsl/intrinsic_functions_spec.rb
247
+ - spec/convection/model/attributes_spec.rb
245
248
  - spec/convection/model/template/condition_spec.rb
249
+ - spec/convection/model/template/resource/aws_auto_scaling_auto_scaling_group_spec.rb
246
250
  - spec/convection/model/template/resource/directoryservice_simple_ad_spec.rb
247
251
  - spec/convection/model/template/resource/ec2_dhcp_options_spec.rb
248
252
  - spec/convection/model/template/resource/ec2_security_group_spec.rb
@@ -287,12 +291,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
287
291
  version: '0'
288
292
  required_rubygems_version: !ruby/object:Gem::Requirement
289
293
  requirements:
290
- - - ">="
294
+ - - ">"
291
295
  - !ruby/object:Gem::Version
292
- version: '0'
296
+ version: 1.3.1
293
297
  requirements: []
294
298
  rubyforge_project:
295
- rubygems_version: 2.4.3
299
+ rubygems_version: 2.4.5
296
300
  signing_key:
297
301
  specification_version: 4
298
302
  summary: A fully generic, modular DSL for AWS CloudFormation
@@ -306,7 +310,9 @@ test_files:
306
310
  - spec/convection/control/stack/before_delete_tasks_spec.rb
307
311
  - spec/convection/control/stack/before_update_tasks_spec.rb
308
312
  - spec/convection/dsl/intrinsic_functions_spec.rb
313
+ - spec/convection/model/attributes_spec.rb
309
314
  - spec/convection/model/template/condition_spec.rb
315
+ - spec/convection/model/template/resource/aws_auto_scaling_auto_scaling_group_spec.rb
310
316
  - spec/convection/model/template/resource/directoryservice_simple_ad_spec.rb
311
317
  - spec/convection/model/template/resource/ec2_dhcp_options_spec.rb
312
318
  - spec/convection/model/template/resource/ec2_security_group_spec.rb