stacks-on-stacks 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 47fd4b4169a979a44c61abfe3250b825a8bed55d
4
+ data.tar.gz: ccff3d88abd9a52f52c0dd1178d6d323e513b57c
5
+ SHA512:
6
+ metadata.gz: 3bfdf61ffb1c26134184e1d578dba0fe72407a6840e2c6af92f68d84ab9ffe23f0ee5433cd8bf0945203e948e97e312e337dd912b608ec88e0606bd276105383
7
+ data.tar.gz: 7ff3b298f9a60ccf105a3691ff1a5d225a4ab9455f09ee9c08d6f8f3d7d30cd337e7fbabb5c15fee662c5543dd7e49fdd2873dca6b420ba64111ab8327c9b5b1
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in stacks-on-stacks.gemspec
4
+ gemspec
5
+
6
+ gem 'aws-sdk'
7
+ gem 'inifile'
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Geoff Hayes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,36 @@
1
+
2
+ stacks-on-stacks
3
+ ================
4
+
5
+ Stack management for AWS OpsWorks
6
+
7
+ # Stacks::On::Stacks
8
+
9
+ TODO: Write a gem description
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ gem 'stacks-on-stacks'
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install stacks-on-stacks
24
+
25
+ ## Usage
26
+
27
+ TODO: Write usage instructions here
28
+
29
+ ## Contributing
30
+
31
+ 1. Fork it
32
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
33
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
34
+ 4. Push to the branch (`git push origin my-new-feature`)
35
+ 5. Create new Pull Request
36
+
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,415 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'json'
5
+ require 'open3'
6
+ require 'pp'
7
+ require 'securerandom'
8
+ require 'aws-sdk'
9
+ require 'inifile'
10
+
11
+ class Stack
12
+ module Helpers
13
+
14
+ def self.load_config!(opts={})
15
+ @@config = if File.exists?('.stack') # runs in `pwd`
16
+ JSON(File.read('.stack'))
17
+ else
18
+ {}
19
+ end.merge(opts)
20
+ end
21
+
22
+ def self.config_hash
23
+ @@config
24
+ end
25
+
26
+ # We'll try to pull from ENV or our config
27
+ def self.config(var, default=nil)
28
+ ENV[var.to_s.upcase] || @@config[var.to_s] || default
29
+ end
30
+
31
+ def self.error(message)
32
+ raise "Error: #{message}"
33
+ exit 1
34
+ end
35
+
36
+ # TODO: Should we use a cache here or pull this live?
37
+ # We're probably going to want to persist settings into where this is being called
38
+ def self.describe
39
+ # File.read("#{File.dirname(__FILE__)}/.server-cache") || `aws ec2 describe-instances`
40
+ cmd = "aws ec2 describe-instances"
41
+ if !config(:profile).nil?
42
+ cmd << " --profile #{config(:profile)}"
43
+ end
44
+
45
+ `#{cmd}`
46
+ end
47
+
48
+ # Takes an <serverish> and returns an instance
49
+ def self.get_serverish(name)
50
+
51
+ # We're going to take a few options here
52
+ case
53
+ when matchData = /(?<env>[\w-]+):(?<name>[\w-]+)/.match(name)
54
+ env, name = matchData[:env], matchData[:name]
55
+ inst = get_instance(env, name)
56
+ "Unable to find server: #{env}:#{name}" if inst.nil?
57
+ return env, get_internal_address(inst)
58
+ when matchData = /(?<env>[\w-]+)::(?<type>[\w.-]+)/.match(name) # Looks like a name
59
+ inst = get_instances_by_type(matchData[:env], matchData[:type],)[0]
60
+ pp [ 'Getting Default Instance', matchData[:env], matchData[:type] ]
61
+ return matchData[:env], get_internal_address(inst)
62
+ when matchData = /(?<env>[\w-]+):(?<name>[\w.-]+)/.match(name) # Looks like a name
63
+ return matchData[:env], matchData[:name]
64
+ else # Default env server
65
+ env = name
66
+ inst = get_instances_by_type(env,'worker')[0]
67
+ pp [ 'Getting Default Instance', inst ]
68
+ return env, get_internal_address(inst)
69
+ end
70
+
71
+ end
72
+
73
+ def self.get_instances(env=nil)
74
+ servers = JSON(describe)
75
+ reservations = servers['Reservations']
76
+ instances = reservations.map { |reservation| reservation['Instances'].first }
77
+ return instances if env.nil?
78
+ instances.select { |instance| tag = instance['Tags'].select { |t| t['Key'] == "opsworks:stack" }.first rescue nil; tag && tag['Value'] == env }
79
+ end
80
+
81
+ def self.get_instance(env, name)
82
+ pp ['Getting instance', env, name]
83
+ inst = get_instances.select { |instance| instance['State']['Name'] != "terminated" && instance['Tags'].select { |t| t['Key'] == "Name" }[0]['Value'] == "#{env} - #{name}" rescue false }.first
84
+ error("Unable to find instance #{env}, #{name}") if inst.nil?
85
+ inst
86
+ end
87
+
88
+ def self.get_instances_by_type(env, type)
89
+ pp ['Getting instances',env,type]
90
+ get_instances.select do |instance|
91
+ instance['Tags'].select { |t| t['Key'] == "opsworks:stack" }.first['Value'] == "#{env}" &&
92
+ instance['Tags'].select { |t| t['Key'] == "opsworks:layer:#{type}" }.count > 0 &&
93
+ instance['State']['Name'] != "terminated" rescue false
94
+ end
95
+ end
96
+
97
+ def self.get_address(instance)
98
+ instance['PublicDnsName'] || instance['PublicIpAddress']
99
+ end
100
+
101
+ def self.get_ssh(env, address, cmd=nil)
102
+ bastion = bastion(env)
103
+
104
+ if config(:ssh_user)
105
+ bastion = "#{config(:ssh_user)}@#{bastion}"
106
+ end
107
+
108
+ base = ['ssh', '-t', bastion]
109
+
110
+ if config(:identity)
111
+ base.concat ['-i', config(:identity)]
112
+ end
113
+
114
+ base << "'" + ['ssh', '-t', address, cmd ? "\"#{cmd}\"" : nil].join(' ') + "'"
115
+
116
+ base.join(' ')
117
+ end
118
+
119
+ def self.get_internal_address(instance)
120
+ instance['PrivateIpAddress'] || instance['PrivateDnsName']
121
+ end
122
+
123
+ def self.bastion(env)
124
+ get_address(get_instance(env, config(:bastion,'bastion')))
125
+ end
126
+
127
+ def self.run_with_output(env, address, cmd)
128
+ cmd = get_ssh(env, address, cmd)
129
+ puts "Running #{get_ssh(env, address, cmd)}"
130
+
131
+ Net::SSH.start(address) do |ssh|
132
+ ssh.exec!("sudo #{cmd}") do |channel, stream, data|
133
+ STDOUT << data if stream == :stdout
134
+ end
135
+ end
136
+ end
137
+
138
+ def self.open3(cmd, tag=nil)
139
+ stdin, @stdin = IO.pipe
140
+ @stdout, stdout = IO.pipe
141
+ @stderr, stderr = IO.pipe
142
+
143
+ puts "Running #{cmd}"
144
+ puts "\033];#{tag}\007" if tag
145
+ Open3.pipeline cmd
146
+ puts "Stacks on Stacks Finished"
147
+ end
148
+
149
+ def self.parse_opts(args)
150
+ opts = {}
151
+ res = []
152
+
153
+ while args.length > 0
154
+ arg = args.shift
155
+
156
+ case
157
+ when arg =~ /^[-]{2,2}([\w_-]+)=([^ ]*)$/i
158
+ key, val = $1, $2
159
+ when arg =~ /^[-]{2,2}([\w_-]+)+$/i
160
+ key = $1
161
+ val = args.shift
162
+ else
163
+ res.push arg # add arg
164
+ next
165
+ end
166
+
167
+ key = key.gsub('-','_') # valid symbol names
168
+
169
+ opts[key] = val
170
+ end
171
+
172
+ return res, opts
173
+ end
174
+
175
+ def self.wait_on_aws_deployment(deployment)
176
+ deployment_id = deployment.data[:deployment_id]
177
+ deployment_desc = nil
178
+
179
+ while true
180
+ deployment_desc = AWS.ops_works.client.describe_deployments(deployment_ids: [deployment_id])
181
+
182
+ status = deployment_desc.data[:deployments].first[:status]
183
+
184
+ case status
185
+ when 'running'
186
+ sleep 10
187
+ when 'successful'
188
+ return true
189
+ else
190
+ raise "Failed to run deployment: #{deployment_id} - #{status}"
191
+ end
192
+ end
193
+
194
+ return true if deployment_desc.data[:status] == 'successful'
195
+ end
196
+
197
+ def self.doc(opts)
198
+ @@docs ||= {}
199
+ @@docs.merge!(opts)
200
+ end
201
+
202
+ def self.docs(method)
203
+ (@@docs || {})[method.to_sym]
204
+ end
205
+ end
206
+
207
+ module Commands
208
+ def self.configure(*args)
209
+ args, opts = Stack::Helpers.parse_opts(args)
210
+ if args.length > 0
211
+ raise "Configure doesn't take any args, got: #{args}"
212
+ end
213
+
214
+ opts = Stack::Helpers.config_hash.merge(opts)
215
+
216
+ opts.delete_if { |key,val| val.nil? || val == "" } # remove blank keys
217
+
218
+ pp ['Setting Config', opts]
219
+
220
+ json = JSON.pretty_generate(opts)
221
+ File.open(".stack", 'w') { |file| file.write(json) }
222
+ end
223
+
224
+ def self.run(serverish, *cmds)
225
+ env, address = Stack::Helpers.get_serverish(serverish)
226
+
227
+ cmd = "sudo #{cmds.join(' ')}"
228
+
229
+ ssh = Stack::Helpers.get_ssh(env, address, cmd)
230
+
231
+ Stack::Helpers.open3(ssh, "#{serverish} [run `#{cmd}`]")
232
+ end
233
+
234
+ def self.nohup(serverish, *cmds)
235
+ env, address = Stack::Helpers.get_serverish(serverish)
236
+
237
+ log_file = "/tmp/nohup-#{SecureRandom.uuid}.out"
238
+ cmd = "touch #{log_file} && nohup sudo #{cmds.join(' ')} > #{log_file} 2>&1& sudo tail -f #{log_file}"
239
+
240
+ ssh = Stack::Helpers.get_ssh(env, address, cmd)
241
+
242
+ Stack::Helpers.open3(ssh, "#{serverish} [run `#{cmd}`]")
243
+ end
244
+
245
+ def self.tail(serverish, log_file=nil, lines=150)
246
+ env, address = Stack::Helpers.get_serverish(serverish)
247
+ app_env = Stack::Helpers.config(:env) || env
248
+
249
+ log_file ||= "/srv/www/#{Stack::Helpers.config(:app)}/shared/log/#{app_env}.log"
250
+
251
+ unless log_file.include?('/')
252
+ log_file = "/srv/www/#{Stack::Helpers.config(:app)}/shared/log/#{log_file}"
253
+ end
254
+
255
+ cmd = "sudo tail -n #{lines.to_i} -f #{log_file}"
256
+
257
+ ssh = Stack::Helpers.get_ssh(env, address, cmd)
258
+
259
+ Stack::Helpers.open3(ssh, "#{serverish} [tail]")
260
+ end
261
+
262
+ def self.list(env)
263
+
264
+ Stack::Helpers.get_instances(env).each do |instance|
265
+ puts "Server - #{instance['PublicDnsName'] || instance['PublicIp']}"
266
+ instance['Tags'].each do |tag|
267
+ puts "\t#{tag['Key']}: #{tag['Value']}"
268
+ end
269
+ end
270
+
271
+ end
272
+
273
+ def self.ssh(serverish)
274
+ env, address = Stack::Helpers.get_serverish(serverish)
275
+ ssh = Stack::Helpers.get_ssh(env, address)
276
+
277
+ Stack::Helpers.open3(ssh, "#{serverish} [ssh]")
278
+ end
279
+
280
+ def self.console(serverish)
281
+ env, address = Stack::Helpers.get_serverish(serverish)
282
+ app_env = Stack::Helpers.config(:env) || env
283
+ app = Stack::Helpers.config(:app)
284
+
285
+ cmd = "sudo su deploy -c \\\"cd /srv/www/#{app}/current && RAILS_ENV=#{app_env} bundle exec rails console\\\""
286
+
287
+ ssh = Stack::Helpers.get_ssh(env, address, cmd)
288
+
289
+ Stack::Helpers.open3(ssh, "#{serverish} [console]")
290
+ end
291
+
292
+ def self.db(serverish)
293
+ env, address = Stack::Helpers.get_serverish(serverish)
294
+ app_env = Stack::Helpers.config(:env) || env
295
+ app = Stack::Helpers.config(:app)
296
+
297
+ cmd = "sudo su deploy -c \\\"cd /srv/www/#{app}/current && RAILS_ENV=#{app_env} bundle exec rails db -p\\\""
298
+
299
+ ssh = Stack::Helpers.get_ssh(env, address, cmd)
300
+
301
+ Stack::Helpers.open3(ssh, "#{serverish} [db-console]")
302
+ end
303
+
304
+ def self.rake(serverish, rake_task, *args)
305
+ env, address = Stack::Helpers.get_serverish(serverish)
306
+ app_env = Stack::Helpers.config(:env) || env
307
+ app = Stack::Helpers.config(:app)
308
+ # accepts either type of args, e.g. task:run[1,2,3] or task:run 1 2 3
309
+ # should fail if both given
310
+ arg_string = args.count > 0 ? "[#{args.join(',')}]" : ""
311
+
312
+ cmd = "sudo su deploy -c \\\"cd /srv/www/#{app}/current && RAILS_ENV=#{app_env} bundle exec rake #{rake_task}#{arg_string}\\\""
313
+
314
+ ssh = Stack::Helpers.get_ssh(env, address, cmd)
315
+
316
+ Stack::Helpers.open3(ssh, "#{serverish} [rake] (rake #{rake_task} [#{args.join(',')}])")
317
+ end
318
+
319
+ Stack::Helpers.doc deploy: {
320
+ arguments: [
321
+ { name: :stack_id, desc: "Id of stack" },
322
+ { name: :app_id, desc: "Id of app" },
323
+ { name: :wait, desc: "Wait for deployment to complete?", default: true }
324
+ # { name: :branch, desc: "Branch for deploy" }
325
+ ]
326
+ }
327
+ def self.deploy(stack_id, app_id, wait="true")
328
+ # First, try to pull these from the environment
329
+ iam_key = Stack::Helpers.config(:iam_key)
330
+ iam_secret = Stack::Helpers.config(:iam_secret)
331
+ # region = Stack::Helpers.config(:region) -- region is messing this all up
332
+
333
+ # Otherwise, we'll pull them from config
334
+ if ( iam_key.nil? || iam_secret.nil? || region.nil? ) && Stack::Helpers.config(:aws_config_file)
335
+ aws_config = IniFile.load(Stack::Helpers.config(:aws_config_file))
336
+ profile = Stack::Helpers.config(:profile) ? "profile #{Stack::Helpers.config(:profile)}" : "default"
337
+
338
+ pp ['Stack', 'Using aws config', Stack::Helpers.config(:aws_config_file), profile]
339
+
340
+ conf = aws_config[profile]
341
+
342
+ iam_key = conf['aws_access_key_id']
343
+ iam_secret = conf['aws_secret_access_key']
344
+ end
345
+
346
+ raise ArgumentError, "Must set IAM_KEY environment variable" if iam_key.nil? || iam_key.length == 0
347
+ raise ArgumentError, "Must set IAM_SECRET environment variable" if iam_secret.nil? || iam_secret.length == 0
348
+
349
+ AWS.config({
350
+ access_key_id: iam_key,
351
+ secret_access_key: iam_secret
352
+ })
353
+
354
+ deployment = AWS.ops_works.client.create_deployment(stack_id: stack_id, app_id: app_id, command: {name: 'deploy'})
355
+
356
+ if wait && wait[0] == "t"
357
+ Stack::Helpers.wait_on_aws_deployment(deployment)
358
+ end
359
+
360
+ # TODO: Wait on deploy?
361
+ # TODO: Revision management
362
+ # revision = `git fetch && git rev-parse origin/#{stack[:branch]}` rescue nil
363
+ end
364
+ end
365
+ end
366
+
367
+ # Main Routine
368
+ puts "\033];Stack\007"
369
+ puts "Welcome to Stacks on Stacks"
370
+
371
+ if ARGV[0].nil? || ARGV[0] == ""
372
+ puts ""
373
+ puts "usage: stack {run,tail,list,ssh,console}"
374
+ puts "\t <serverish>: {env, env:name, env::type, env:ip-address}"
375
+ puts "\t e.g."
376
+ puts "\t stack list <env>"
377
+ puts "\t stack tail <serverish> [something.log] (default: /srv/www/<app>/shared/log/<env>.log, if no '/' found in log, defaults to directory /srv/www/<app>/shared/log/)"
378
+ puts "\t stack run <serverish> cat /srv/www/app/shared/log/production.log"
379
+ puts "\t stack ssh <serverish>"
380
+ puts "\t stack console <serverish>"
381
+ puts "\t stack db <serverish>"
382
+ puts "\t stack rake <serverish> <namespace>:<rake_task>"
383
+ puts "\t stack configure --key=val --key2=val2"
384
+ puts "\t stack deploy <stack_id> <app_id>"
385
+ puts "\t"
386
+ puts "\t config variables: { app: '<app name> for /srv/www/<app>/current directory', profile: 'aws profile in ~/.aws/config', identity: 'e.g. ~/.ssh/id_rsa', ssh_user: 'geoff for ssh geoff@<host>', bastion: 'layer type as a bastion server' }"
387
+ exit
388
+ end
389
+
390
+
391
+ # TODO: Check method signature and print disparties
392
+ args = ARGV
393
+ args, opts = Stack::Helpers.parse_opts(args)
394
+ Stack::Helpers.load_config!(opts)
395
+ # pp ['Stack',args,opts]
396
+
397
+ if Stack::Commands.respond_to?(args[0])
398
+ method = args.shift
399
+
400
+ begin
401
+ Stack::Commands.send(method, *args)
402
+ rescue ArgumentError => e
403
+ if Stack::Helpers.docs(method)
404
+ puts "Usage: stack #{method} #{Stack::Helpers.docs(method).inspect}"
405
+ else
406
+ raise e
407
+ end
408
+ end
409
+
410
+ puts "Ending Stacks on Stacks Session"
411
+ else
412
+ raise "Unknown command: #{args[0]}"
413
+ end
414
+
415
+ puts "\033];bash\007"
@@ -0,0 +1,9 @@
1
+ require "stacks/on/stacks/version"
2
+
3
+ module Stacks
4
+ module On
5
+ module Stacks
6
+ # Your code goes here...
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ module Stacks
2
+ module On
3
+ module Stacks
4
+ VERSION = "0.0.1"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'stacks/on/stacks/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "stacks-on-stacks"
8
+ spec.version = Stacks::On::Stacks::VERSION
9
+ spec.authors = ["Geoff Hayes"]
10
+ spec.email = ["hayesgm@gmail.com"]
11
+ spec.description = %q{Stacks on Stacks is a management CLI for common tasks on AWS OpsWorks. You'll be able to SSH to your servers (via a bastion) and send off deploys.}
12
+ spec.summary = %q{Stacks on Stack is a CLI for managing an OpsWorks stack}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: stacks-on-stacks
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Geoff Hayes
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-01-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Stacks on Stacks is a management CLI for common tasks on AWS OpsWorks. You'll
42
+ be able to SSH to your servers (via a bastion) and send off deploys.
43
+ email:
44
+ - hayesgm@gmail.com
45
+ executables:
46
+ - stack
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - .gitignore
51
+ - Gemfile
52
+ - LICENSE
53
+ - README.md
54
+ - Rakefile
55
+ - bin/stack
56
+ - lib/stacks/on/stacks.rb
57
+ - lib/stacks/on/stacks/version.rb
58
+ - stacks-on-stacks.gemspec
59
+ homepage: ''
60
+ licenses:
61
+ - MIT
62
+ metadata: {}
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - '>='
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubyforge_project:
79
+ rubygems_version: 2.1.11
80
+ signing_key:
81
+ specification_version: 4
82
+ summary: Stacks on Stack is a CLI for managing an OpsWorks stack
83
+ test_files: []