hygroscope 1.0.0

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 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: []