hygroscope 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +25 -0
- data/Rakefile +1 -0
- data/bin/hygroscope +5 -0
- data/hygroscope.gemspec +23 -0
- data/lib/hygroscope.rb +11 -0
- data/lib/hygroscope/cli.rb +345 -0
- data/lib/hygroscope/paramset.rb +47 -0
- data/lib/hygroscope/payload.rb +64 -0
- data/lib/hygroscope/stack.rb +114 -0
- data/lib/hygroscope/template.rb +71 -0
- metadata +169 -0
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
data/Gemfile
ADDED
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
data/hygroscope.gemspec
ADDED
@@ -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: []
|