convection 0.4.3 → 1.0.0.pre.beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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