hygroscope 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cd6ed704c9f7b15170bc39f4162300946f7f690f
4
+ data.tar.gz: fd84d911d968cc9a883ded9bb45b4cbe417467db
5
+ SHA512:
6
+ metadata.gz: a45f21a18bee67f59a412a5115d3a193cf53df152b5441de6090d3aed062c3a25fcf2442720af0af77e6e09bbf8e972ab46f29d842a5d5c734613e80b23cc795
7
+ data.tar.gz: 813ac9c2ec94513bb791be5715efa8513bf25998a482b3df1a15d6ddac0e0df37660dfc00788037e2064a7da4bedc4e77bb454857f80d1bab58f87fd4d26d3d8
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Jay Perry
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,25 @@
1
+ - - -
2
+
3
+ > **hy·gro·scope**<br>
4
+ > _ˈhīɡrəˌskōp/_, noun<br>
5
+ > an instrument that gives an indication of the humidity of the air.
6
+
7
+ - - -
8
+
9
+ Hygroscope is a Thor-based command-line tool for managing the launch of complex CloudFormation stacks.
10
+
11
+ [CloudFormation](http://aws.amazon.com/cloudformation/) is a great way to manage infrastructure resources using code, but it has some aspects that make it a pain:
12
+
13
+ 1. Templates must be written in JSON, which, in addition to being difficult for a human to read, does not support niceties such as inline comments and repeated blocks.
14
+ 2. Launching CloudFormation stacks requires knowledge of the various parameters that need to be provided, and it is difficult to repeatably launch a stack since parameters are not saved in any convenient way.
15
+ 3. There is no easy mechanism to send a payload of data to an instance during stack creation (for instance scripts and recipes to bootstrap an instance).
16
+ 4. Finally, it is difficult to launch stacks that build upon already-existing stacks (i.e. an application stack within an existing VPC stack) because one must manually provide a variety of identifiers (subnets, IP addresses, security groups).
17
+
18
+ Hygroscope aims to solve each of these specific problems in an opinionated way:
19
+
20
+ 1. CF templates are written in YAML and processed using [cfoo](https://github.com/drrb/cfoo), which provides a variety of convenience methods that increase readability.
21
+ 2. Hygroscope can interactively prompt for each parameter and save inputted parameters to a file called a paramset. Additional stack launches can make use of existing paramsets, or can use paramsets as the basis and prompt for updated parameters.
22
+ 3. A payload directory, if present, will be packaged and uploaded to S3. Hygroscope will generate and pass to CF a signed time-limited URL for accessing and downloading the payload, or the CloudFormation template can manage an instance profile granting indefinite access to the payload.
23
+ 4. If an existing stack is specified, its outputs will be fetched and passed through as input parameters when launching a new stack.
24
+
25
+ Hygroscope is currently under development but mostly functional. Run `hygroscope help` to view inline command documentation and options. See [template structure](https://github.com/agperson/hygroscope/wiki/Structure-of-a-Hygroscopic-Template) for information about the format of hygroscopic templates.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
data/bin/hygroscope ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.push File.expand_path('../../lib', __FILE__)
3
+ require 'hygroscope/cli'
4
+
5
+ Hygroscope::Cli.start(ARGV)
@@ -0,0 +1,23 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'hygroscope'
3
+ s.version = '1.0.0'
4
+ s.summary = 'CloudFormation launcher'
5
+ s.description = 'A tool for managing the launch of complex CloudFormation stacks'
6
+ s.authors = ['Daniel Silverman']
7
+ s.email = 'dsilverman@brightcove.com'
8
+ s.homepage = ''
9
+ s.license = 'MIT'
10
+
11
+ s.files = `git ls-files -z`.split("\x0")
12
+ s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
13
+
14
+ s.add_dependency 'thor'
15
+ s.add_dependency 'cfoo'
16
+ s.add_dependency 'aws-sdk', '>= 2.0.0.pre'
17
+ s.add_dependency 'archive-zip'
18
+ s.add_dependency 'json_color'
19
+
20
+ s.add_development_dependency 'bundler'
21
+ s.add_development_dependency 'rake'
22
+ s.add_development_dependency 'rubocop'
23
+ end
data/lib/hygroscope.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'thor'
2
+ require 'fileutils'
3
+ require 'tempfile'
4
+ require 'yaml'
5
+ require 'aws-sdk'
6
+
7
+ require_relative 'hygroscope/cli'
8
+ require_relative 'hygroscope/stack'
9
+ require_relative 'hygroscope/template'
10
+ require_relative 'hygroscope/paramset'
11
+ require_relative 'hygroscope/payload'
@@ -0,0 +1,345 @@
1
+ require 'hygroscope'
2
+
3
+ module Hygroscope
4
+ class Cli < Thor
5
+ include Thor::Actions
6
+
7
+ def initialize(*args)
8
+ super(*args)
9
+ end
10
+
11
+ no_commands do
12
+ def say_fail(message)
13
+ say_status('error', message, :red)
14
+ abort
15
+ end
16
+
17
+ def colorize_status(status)
18
+ case status.downcase
19
+ when /failed$/
20
+ set_color(status, :red)
21
+ when /progress$/
22
+ set_color(status, :yellow)
23
+ when /complete$/
24
+ set_color(status, :green)
25
+ end
26
+ end
27
+
28
+ def word_wrap(string, length = 80, delim = $INPUT_RECORD_SEPARATOR)
29
+ string.scan(/.{#{length}}|.+/).map(&:strip).join(delim)
30
+ end
31
+
32
+ def countdown(text, time = 5)
33
+ print "#{text} "
34
+ time.downto(0) do |i|
35
+ $stdout.write("\b")
36
+ $stdout.write(i)
37
+ $stdout.flush
38
+ sleep 1
39
+ end
40
+ end
41
+
42
+ def check_path
43
+ say_fail('Hygroscope must be run from the top level of a hygroscopic directory.') unless
44
+ File.directory?(File.join(Dir.pwd, 'template')) &&
45
+ File.directory?(File.join(Dir.pwd, 'paramsets'))
46
+ end
47
+
48
+ def hygro_path
49
+ Dir.pwd
50
+ end
51
+
52
+ def hygro_name
53
+ File.basename(Dir.pwd)
54
+ end
55
+
56
+ def template_path
57
+ File.join(hygro_path, 'template')
58
+ end
59
+ end
60
+
61
+ desc 'prepare', 'Prepare to create or update a stack by generating the template, assembling parameters, and managing payload upload', hide: true
62
+ def prepare
63
+ # Generate the template
64
+ t = Hygroscope::Template.new(template_path)
65
+
66
+ # If the paramset exists load it, otherwise instantiate an empty one
67
+ p = Hygroscope::ParamSet.new(options[:paramset])
68
+
69
+ # TODO: Load and merge outputs from previous invocations -- how???
70
+
71
+ # User provided a paramset, so load it and determine which parameters
72
+ # are set and which need to be prompted.
73
+ if options[:paramset]
74
+ pkeys = p.parameters.keys
75
+ tkeys = t.parameters.keys
76
+
77
+ # Filter out any parameters that are not present in the template
78
+ filtered = pkeys - tkeys
79
+ pkeys = pkeys.select { |k, _v| tkeys.include?(k) }
80
+ say_status('info', "Keys in paramset not requested by template: #{filtered.join(', ')}", :blue) unless filtered.empty?
81
+
82
+ # If ask option was passed, consider every parameter missing
83
+ missing = options[:ask] ? tkeys : tkeys - pkeys
84
+ else
85
+ # No paramset provided, so every parameter is missing!
86
+ missing = t.parameters.keys
87
+ end
88
+
89
+ # Prompt for each missing param and save it to the paramset
90
+ missing.each do |key|
91
+ # Do not prompt for keys prefixed with "Hygroscope"
92
+ next if key =~ /^Hygroscope/
93
+
94
+ say()
95
+ type = t.parameters[key]['Type']
96
+ default = options[:ask] && pkeys.include?(key) ? p.get(key) : t.parameters[key]['Default'] || ''
97
+ description = t.parameters[key]['Description'] || false
98
+ values = t.parameters[key]['AllowedValues'] || false
99
+ no_echo = t.parameters[key]['NoEcho'] || false
100
+
101
+ ask_opts = {}
102
+ ask_opts[:default] = default unless default.empty?
103
+ ask_opts[:limited_to] = values if values
104
+ ask_opts[:echo] = false if no_echo
105
+
106
+ say("#{description} (#{type})") if description
107
+ answer = ''
108
+ # TODO: Better input validation
109
+ answer = ask(key, :cyan, ask_opts) until answer != ''
110
+ p.set(key, answer)
111
+ end
112
+
113
+ unless missing.empty?
114
+ if yes?('Save changes to paramset?')
115
+ unless options[:paramset]
116
+ p.name = ask('Paramset name', :cyan, default: options[:name])
117
+ end
118
+ p.save!
119
+ end
120
+ end
121
+
122
+ # Upload payload
123
+ payload_path = File.join(Dir.pwd, 'payload')
124
+ if File.directory?(payload_path)
125
+ payload = Hygroscope::Payload.new(payload_path)
126
+ payload.prefix = options[:name]
127
+ url = payload.upload!
128
+ signed_url = payload.generate_url
129
+ p.set('HygroscopePayload', url) if missing.include?('HygroscopePayload')
130
+ p.set('HygroscopePayloadSignedUrl', signed_url) if missing.include?('HygroscopePayloadSignedUrl')
131
+ say_status('ok', 'Payload uploaded to:', :green)
132
+ say_status('', url)
133
+ end
134
+
135
+ [t, p]
136
+ end
137
+
138
+ desc 'create', "Create a new stack.\nUse the --name option to launch more than one stack from the same template.\nCommand prompts for parameters unless --paramset is specified."
139
+ method_option :name,
140
+ aliases: '-n',
141
+ default: File.basename(Dir.pwd),
142
+ desc: 'Name of stack'
143
+ method_option :paramset,
144
+ aliases: '-p',
145
+ required: false,
146
+ desc: 'Name of saved paramset to use (optional)'
147
+ method_option :ask,
148
+ aliases: '-a',
149
+ type: :boolean,
150
+ default: false,
151
+ desc: 'Still prompt for parameters even when using a paramset'
152
+ def create
153
+ check_path
154
+ validate
155
+ template, paramset = prepare
156
+
157
+ s = Hygroscope::Stack.new(options[:name])
158
+ s.parameters = paramset.parameters
159
+ s.template = template.compress
160
+ s.tags['X-Hygroscope-Template'] = File.basename(Dir.pwd)
161
+ s.capabilities = ['CAPABILITY_IAM']
162
+ s.create!
163
+
164
+ status
165
+ end
166
+
167
+ desc 'update', "Update a running stack.\nCommand prompts for parameters unless a --paramset is specified."
168
+ method_option :name,
169
+ aliases: '-n',
170
+ default: File.basename(Dir.pwd),
171
+ desc: 'Name of stack'
172
+ method_option :paramset,
173
+ aliases: '-p',
174
+ required: false,
175
+ desc: 'Name of saved paramset to use (optional)'
176
+ method_option :ask,
177
+ aliases: '-a',
178
+ type: :boolean,
179
+ default: false,
180
+ desc: 'Still prompt for parameters even when using a paramset'
181
+ def update
182
+ # TODO: Right now update just does the same thing as create, not taking
183
+ # into account the complications of updating (which params to keep,
184
+ # whether to re-upload the payload, etc.)
185
+ check_path
186
+ validate
187
+ template, paramset = prepare
188
+
189
+ s = Hygroscope::Stack.new(options[:name])
190
+ s.parameters = paramset.parameters
191
+ s.template = template.compress
192
+ s.capabilities = ['CAPABILITY_IAM']
193
+ s.update!
194
+
195
+ status
196
+ end
197
+
198
+ desc 'delete', 'Delete a running stack after asking for confirmation.'
199
+ method_option :name,
200
+ aliases: '-n',
201
+ default: File.basename(Dir.pwd),
202
+ desc: 'Name of stack'
203
+ method_option :force,
204
+ aliases: '-f',
205
+ type: :boolean,
206
+ default: false,
207
+ desc: 'Delete without asking for confirmation'
208
+ def delete
209
+ check_path
210
+ if options[:force] || yes?("Really delete stack #{options[:name]} [y/N]?")
211
+ say('Deleting stack!')
212
+ stack = Hygroscope::Stack.new(options[:name])
213
+ stack.delete!
214
+ status
215
+ end
216
+ end
217
+
218
+ desc 'status', 'View status of stack create/update/delete action.\nUse the --name option to change which stack is reported upon.'
219
+ method_option :name,
220
+ aliases: '-n',
221
+ default: File.basename(Dir.pwd),
222
+ desc: 'Name of stack'
223
+ def status
224
+ check_path
225
+ stack = Hygroscope::Stack.new(options[:name])
226
+
227
+ # Query and display the status of the stack and its resources. Refresh
228
+ # every 10 seconds until the user aborts or an error is encountered.
229
+ begin
230
+ s = stack.describe
231
+
232
+ system('clear') || system('cls')
233
+
234
+ header = {
235
+ 'Name:' => s.stack_name,
236
+ 'Created:' => s.creation_time,
237
+ 'Status:' => colorize_status(s.stack_status)
238
+ }
239
+
240
+ print_table header
241
+ puts
242
+
243
+ type_width = terminal_width < 80 ? 30 : terminal_width - 50
244
+ output_width = terminal_width < 80 ? 54 : terminal_width - 31
245
+
246
+ puts set_color(sprintf(' %-28s %-*s %-18s ', 'Resource', type_width, 'Type', 'Status'), :white, :on_blue)
247
+ resources = stack.list_resources
248
+ resources.each do |r|
249
+ puts sprintf(' %-28s %-*s %-18s ', r[:name][0..26], type_width, r[:type][0..type_width], colorize_status(r[:status]))
250
+ end
251
+
252
+ if s.stack_status.downcase =~ /complete$/
253
+ puts
254
+ puts set_color(sprintf(' %-28s %-*s ', 'Output', output_width, 'Value'), :white, :on_yellow)
255
+ s.outputs.each do |o|
256
+ puts sprintf(' %-28s %-*s ', o.output_key, output_width, o.output_value)
257
+ end
258
+
259
+ puts "\nMore information: https://console.aws.amazon.com/cloudformation/home"
260
+ break
261
+ elsif s.stack_status.downcase =~ /failed$/
262
+ puts "\nMore information: https://console.aws.amazon.com/cloudformation/home"
263
+ break
264
+ else
265
+ puts "\nMore information: https://console.aws.amazon.com/cloudformation/home"
266
+ countdown('Updating in', 9)
267
+ puts
268
+ end
269
+ rescue Aws::CloudFormation::Errors::ValidationError
270
+ say_fail('Stack not found')
271
+ rescue Interrupt
272
+ abort
273
+ end while true
274
+ end
275
+
276
+ desc 'generate', "Generate and display JSON output from template files.\nTo validate that the template is well-formed use the 'validate' command."
277
+ method_option :color,
278
+ aliases: '-c',
279
+ type: :boolean,
280
+ default: true,
281
+ desc: 'Colorize JSON output'
282
+ def generate
283
+ check_path
284
+ t = Hygroscope::Template.new(template_path)
285
+ if options[:color]
286
+ require 'json_color'
287
+ puts JsonColor.colorize(t.process)
288
+ else
289
+ puts t.process
290
+ end
291
+ end
292
+
293
+ desc 'validate', "Generate JSON from template files and validate that it is well-formed.\nThis utilzies the CloudFormation API to validate the template but does not detect logical errors."
294
+ def validate
295
+ check_path
296
+ begin
297
+ t = Hygroscope::Template.new(template_path)
298
+ t.validate
299
+ rescue Aws::CloudFormation::Errors::ValidationError => e
300
+ say_status('error', 'Validation error', :red)
301
+ print_wrapped e.message, indent: 2
302
+ abort
303
+ rescue Hygroscope::TemplateYamlParseError => e
304
+ say_status('error', 'YAML parsing error', :red)
305
+ puts e
306
+ abort
307
+ rescue => e
308
+ say_status('error', 'Unexpected error', :red)
309
+ print_wrapped e.message, indent: 2
310
+ abort
311
+ else
312
+ say_status('ok', 'Template is valid', :green)
313
+ end
314
+ end
315
+
316
+ desc 'paramset', "List saved paramsets.\nIf --name is passed, shows all parameters in the named set."
317
+ method_option :name,
318
+ aliases: '-n',
319
+ required: false,
320
+ desc: 'Name of a paramset'
321
+ def paramset
322
+ if options[:name]
323
+ begin
324
+ p = Hygroscope::ParamSet.new(options[:name])
325
+ rescue Hygroscope::ParamSetNotFoundError
326
+ raise("Paramset #{options[:name]} does not exist!")
327
+ end
328
+ say "Parameters for '#{hygro_name}' paramset '#{p.name}':", :yellow
329
+ print_table p.parameters, indent: 2
330
+ say "\nTo edit existing parameters, use the 'create' command with the --ask flag."
331
+ else
332
+ files = Dir.glob(File.join(hygro_path, 'paramsets', '*.{yml,yaml}'))
333
+ if files.empty?
334
+ say "No saved paramsets for '#{hygro_name}'.", :red
335
+ else
336
+ say "Saved paramsets for '#{hygro_name}':", :yellow
337
+ files.map do |f|
338
+ say ' ' + File.basename(f, File.extname(f))
339
+ end
340
+ say "\nTo list parameters in a set, use the --name option."
341
+ end
342
+ end
343
+ end
344
+ end
345
+ end
@@ -0,0 +1,47 @@
1
+ require 'hygroscope'
2
+
3
+ module Hygroscope
4
+ class ParamSetNotFoundError < StandardError
5
+ end
6
+
7
+ class ParamSet
8
+ attr_accessor :name, :path
9
+ attr_reader :parameters
10
+
11
+ def initialize(name = nil)
12
+ @parameters = {}
13
+ @path = File.join(Dir.pwd, 'paramsets')
14
+
15
+ if name
16
+ @name = name
17
+ self.load!
18
+ end
19
+ end
20
+
21
+ def load!
22
+ files = Dir.glob(File.join(@path, @name + '.{yml,yaml}'))
23
+ if files.empty?
24
+ fail Hygroscope::ParamSetNotFoundError
25
+ else
26
+ @file = files.first
27
+ @parameters = YAML.load_file(@file)
28
+ end
29
+ end
30
+
31
+ def save!
32
+ # If this is a new paramset, construct a filename
33
+ savefile = @file || File.join(@path, @name + '.yaml')
34
+ File.open(savefile, 'w') do |f|
35
+ YAML.dump(@parameters, f)
36
+ end
37
+ end
38
+
39
+ def get(key)
40
+ @parameters[key]
41
+ end
42
+
43
+ def set(key, value)
44
+ @parameters[key] = value
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,64 @@
1
+ require 'hygroscope'
2
+ require 'archive/zip'
3
+
4
+ module Hygroscope
5
+ class Payload
6
+ attr_writer :prefix
7
+ attr_reader :path, :bucket, :archive, :key
8
+
9
+ def initialize(path)
10
+ @path = path
11
+
12
+ # TODO: This will fail if using root creds or lacking GetUser priv,
13
+ # neither of which should be the case when using hygroscope -- but
14
+ # we should check and error before getting to this point.
15
+ @account_id = Aws::IAM::Client.new.get_user.user.arn.split(':')[4]
16
+ @bucket = "hygroscope-payloads-#{@account_id}"
17
+ @name = "payload-#{Time.new.to_i}.zip"
18
+
19
+ @client = Aws::S3::Client.new
20
+ end
21
+
22
+ def prefix
23
+ @prefix || File.dirname(File.dirname(@path))
24
+ end
25
+
26
+ def key
27
+ "#{@prefix}/#{@name}"
28
+ end
29
+
30
+ def create_bucket
31
+ # Returns success if bucket already exists
32
+ @client.create_bucket(bucket: @bucket, acl: 'private')
33
+ end
34
+
35
+ def prepare
36
+ archive_path = File.join(Dir.tmpdir, @name)
37
+ Archive::Zip.archive(archive_path, "#{@path}/.")
38
+
39
+ @archive = File.open(archive_path)
40
+ at_exit { File.unlink(@archive) }
41
+ end
42
+
43
+ def send
44
+ @client.put_object(
45
+ bucket: @bucket,
46
+ key: key,
47
+ body: @archive
48
+ )
49
+ end
50
+
51
+ def upload!
52
+ create_bucket
53
+ prepare
54
+ send
55
+
56
+ "s3://#{@bucket}/#{key}"
57
+ end
58
+
59
+ def generate_url(_timeout = 900)
60
+ signer = Aws::S3::Presigner.new(client: @client)
61
+ signer.presigned_url(:get_object, bucket: @bucket, key: key)
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,114 @@
1
+ require 'hygroscope'
2
+
3
+ module Hygroscope
4
+ class Stack
5
+ attr_accessor :name, :tags, :parameters
6
+ attr_writer :template, :capabilities, :on_failure, :timeout
7
+ attr_reader :client
8
+
9
+ def initialize(name)
10
+ @name = name
11
+ @parameters = {}
12
+ @tags = {}
13
+ @template = ''
14
+ @capabilities = []
15
+ @timeout = 15
16
+ @on_failure = 'DO_NOTHING'
17
+ @client = Aws::CloudFormation::Client.new
18
+ end
19
+
20
+ def create!
21
+ stack_parameters = []
22
+ @parameters.each do |k, v|
23
+ stack_parameters << {
24
+ parameter_key: k,
25
+ parameter_value: v
26
+ }
27
+ end
28
+
29
+ stack_tags = []
30
+ @tags.each do |k, v|
31
+ stack_tags << {
32
+ key: k,
33
+ value: v
34
+ }
35
+ end
36
+
37
+ stack_opts = {
38
+ stack_name: @name,
39
+ template_body: @template,
40
+ parameters: stack_parameters,
41
+ timeout_in_minutes: @timeout,
42
+ on_failure: @on_failure
43
+ }
44
+
45
+ stack_opts['capabilities'] = @capabilities unless @capabilities.empty?
46
+ stack_opts['tags'] = stack_tags
47
+
48
+ begin
49
+ stack_id = @client.create_stack(stack_opts)
50
+ rescue => e
51
+ raise e
52
+ end
53
+
54
+ stack_id
55
+ end
56
+
57
+ def update!
58
+ stack_parameters = []
59
+ @parameters.each do |k, v|
60
+ stack_parameters << {
61
+ parameter_key: k,
62
+ parameter_value: v
63
+ }
64
+ end
65
+
66
+ stack_opts = {
67
+ stack_name: @name,
68
+ template_body: @template,
69
+ parameters: stack_parameters
70
+ }
71
+
72
+ stack_opts['capabilities'] = @capabilities unless @capabilities.empty?
73
+
74
+ begin
75
+ stack_id = @client.create_stack(stack_opts)
76
+ rescue => e
77
+ raise e
78
+ end
79
+
80
+ stack_id
81
+ end
82
+
83
+ def delete!
84
+ @client.delete_stack(stack_name: @name)
85
+ rescue => e
86
+ raise e
87
+ end
88
+
89
+ def describe
90
+ resp = @client.describe_stacks(stack_name: @name)
91
+ rescue => e
92
+ raise e
93
+ else
94
+ resp.stacks.first
95
+ end
96
+
97
+ def list_resources
98
+ resp = @client.describe_stack_resources(stack_name: @name)
99
+ rescue => e
100
+ raise e
101
+ else
102
+ resources = []
103
+ resp.stack_resources.each do |r|
104
+ resources << {
105
+ name: r.logical_resource_id,
106
+ type: r.resource_type,
107
+ status: r.resource_status
108
+ }
109
+ end
110
+
111
+ resources
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,71 @@
1
+ require 'hygroscope'
2
+ require 'cfoo'
3
+
4
+ module Hygroscope
5
+ class TemplateYamlParseError < StandardError
6
+ end
7
+
8
+ class Template
9
+ attr_reader :path
10
+
11
+ def initialize(path)
12
+ @path = path
13
+ end
14
+
15
+ # Process a set of files with cfoo and return JSON
16
+ def process
17
+ return @template if @template
18
+
19
+ out = StringIO.new
20
+ err = StringIO.new
21
+
22
+ files = Dir.glob(File.join(@path, '*.{yml,yaml}'))
23
+ cfoo = Cfoo::Factory.new(out, err).cfoo
24
+
25
+ Dir.chdir('/') do
26
+ cfoo.process(*files)
27
+ end
28
+
29
+ if err.string.empty?
30
+ @template = out.string
31
+ @template
32
+ else
33
+ fail TemplateYamlParseError, err.string
34
+ end
35
+ end
36
+
37
+ def compress
38
+ JSON.parse(process).to_json
39
+ end
40
+
41
+ def parameters
42
+ template = JSON.parse(process)
43
+ template['Parameters'] || []
44
+ end
45
+
46
+ # Process a set of files with cfoo and write JSON to a temporary file
47
+ def process_to_file
48
+ file = Tempfile.new(['hygroscope-', '.json'])
49
+ file.write(process)
50
+ file.close
51
+
52
+ at_exit { file.unlink }
53
+
54
+ file
55
+ end
56
+
57
+ def validate
58
+ # Parsing the template to JSON and then re-outputting it is a form of
59
+ # compression (removing all extra spaces) to keep within the 50KB limit
60
+ # for CloudFormation templates.
61
+ template = self.compress
62
+
63
+ begin
64
+ stack = Hygroscope::Stack.new('template-validator')
65
+ stack.client.validate_template(template_body: template)
66
+ rescue => e
67
+ raise e
68
+ end
69
+ end
70
+ end
71
+ end
metadata ADDED
@@ -0,0 +1,169 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hygroscope
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Daniel Silverman
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-02-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: cfoo
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: aws-sdk
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 2.0.0.pre
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 2.0.0.pre
55
+ - !ruby/object:Gem::Dependency
56
+ name: archive-zip
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: json_color
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: bundler
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: A tool for managing the launch of complex CloudFormation stacks
126
+ email: dsilverman@brightcove.com
127
+ executables:
128
+ - hygroscope
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - ".gitignore"
133
+ - Gemfile
134
+ - LICENSE.txt
135
+ - README.md
136
+ - Rakefile
137
+ - bin/hygroscope
138
+ - hygroscope.gemspec
139
+ - lib/hygroscope.rb
140
+ - lib/hygroscope/cli.rb
141
+ - lib/hygroscope/paramset.rb
142
+ - lib/hygroscope/payload.rb
143
+ - lib/hygroscope/stack.rb
144
+ - lib/hygroscope/template.rb
145
+ homepage: ''
146
+ licenses:
147
+ - MIT
148
+ metadata: {}
149
+ post_install_message:
150
+ rdoc_options: []
151
+ require_paths:
152
+ - lib
153
+ required_ruby_version: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ required_rubygems_version: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: '0'
163
+ requirements: []
164
+ rubyforge_project:
165
+ rubygems_version: 2.4.5
166
+ signing_key:
167
+ specification_version: 4
168
+ summary: CloudFormation launcher
169
+ test_files: []