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