opsworks-cli 0.2.4 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b5ab9727e55ff6564f7d6f82d6cbeee4d55005f9
4
- data.tar.gz: 02ce23e2d07cdccc433660baaf45a9ea031d741a
3
+ metadata.gz: 2ab03c769a18190d2590e765e79e992e820685da
4
+ data.tar.gz: ce762573e029e65e1776bcfae265f7611762fc85
5
5
  SHA512:
6
- metadata.gz: 97b2db684720a9bfc43df78b9ce0ea49afe4687ab40833ac23aeb0f41708807062b5de207736bdb44fe94061c2a945ff42f1d09054b40ac8b28d29650b425595
7
- data.tar.gz: 1e2201866045a971e81bc2bc10db039576565ad5d94bd910fe852361542b61065fc1d453c2dea310c8821f2598f55d0b71dbe50d418b612036931420c8b24139
6
+ metadata.gz: d347483d2342efc5d4cbbd882f310044bd327269868105eab6ea53e9f4a368cb2420c0cd2b9d4853a2cb409cfcb0e2eb0b6fddfa18932025f6bda958c0e9a8da
7
+ data.tar.gz: ca593191e47af2a66c8f072d1c1fca78f869c9cc0784c06e8b5c33819b63cba6afeabc66d0c677d98261c4f4b95ee4ef979ff9c6eb7879ddf13b0943e745948b
data/README.md CHANGED
@@ -32,11 +32,20 @@ When you add credentials, make sure to name the account `default`.
32
32
  ```
33
33
  $ opsworks help
34
34
  Commands:
35
- opsworks deploy [--stack STACK] APP # Deploy an OpsWorks app
36
- opsworks exec [--stack STACK] RECIPE # Execute a Chef recipe
37
- opsworks status [--stack STACK] APP # Display the most recent deployment of an app
38
- opsworks update [--stack STACK] # Update OpsWorks custom cookbooks
39
- opsworks version # Print OpsWorks CLI version
35
+ opsworks apps:create APP [--stack STACK] # Create a new OpsWorks app
36
+ opsworks apps:deploy APP [--stack STACK] # Deploy an OpsWorks app
37
+ opsworks apps:status APP [--stack STACK] # Display the most recent deployment of an app
38
+ opsworks config:get KEY [--stack STACK] # Get a single config value
39
+ opsworks config:set KEY VALUE [--stack STACK] # Set a config value
40
+ opsworks config:unset KEY [--stack STACK] # Unset a config value
41
+ opsworks help [COMMAND] # Describe available commands or one specific command
42
+ opsworks iam:allow USER [--stack STACK] # Allow an IAM user on a stack
43
+ opsworks iam:lockdown [--stack STACK] # Remove all stack permissions
44
+ opsworks recipes:add LAYER EVENT RECIPE [--stack STACK] # Add a recipe to a given layer and lifecycle event
45
+ opsworks recipes:run RECIPE [--stack STACK] # Execute a Chef recipe
46
+ opsworks update [--stack STACK] # Update OpsWorks custom cookbooks
47
+ opsworks upgrade-chef [--stack STACK] # Upgrade Chef version
48
+ opsworks version # Print OpsWorks CLI version
40
49
  ```
41
50
 
42
51
  ## Contributing
@@ -5,12 +5,10 @@ require_relative 'helpers/keychain'
5
5
  require_relative 'helpers/options'
6
6
 
7
7
  require_relative 'subcommands/update'
8
- require_relative 'subcommands/exec'
9
- require_relative 'subcommands/deploy'
10
- require_relative 'subcommands/status'
11
- require_relative 'subcommands/allow'
12
- require_relative 'subcommands/lockdown'
13
8
  require_relative 'subcommands/upgrade_chef'
9
+ require_relative 'subcommands/recipes'
10
+ require_relative 'subcommands/apps'
11
+ require_relative 'subcommands/iam'
14
12
  require_relative 'subcommands/config'
15
13
 
16
14
  module OpsWorks
@@ -19,12 +17,10 @@ module OpsWorks
19
17
  include Thor::Actions
20
18
 
21
19
  include Subcommands::Update
22
- include Subcommands::Exec
23
- include Subcommands::Deploy
24
- include Subcommands::Status
25
- include Subcommands::Allow
26
- include Subcommands::Lockdown
27
20
  include Subcommands::UpgradeChef
21
+ include Subcommands::Recipes
22
+ include Subcommands::Apps
23
+ include Subcommands::IAM
28
24
  include Subcommands::Config
29
25
 
30
26
  desc 'version', 'Print OpsWorks CLI version'
@@ -0,0 +1,90 @@
1
+ require 'opsworks/deployment'
2
+
3
+ module OpsWorks
4
+ module CLI
5
+ module Subcommands
6
+ module Apps
7
+ # rubocop:disable MethodLength
8
+ # rubocop:disable CyclomaticComplexity
9
+ # rubocop:disable PerceivedComplexity
10
+ def self.included(thor)
11
+ thor.class_eval do
12
+ desc 'apps:deploy APP [--stack STACK]', 'Deploy an OpsWorks app'
13
+ option :stack, type: :array
14
+ option :timeout, type: :numeric
15
+ define_method 'apps:deploy' do |name|
16
+ fetch_keychain_credentials unless env_credentials?
17
+ stacks = parse_stacks(options.merge(active: true))
18
+ deployments = stacks.map do |stack|
19
+ next unless (app = stack.find_app_by_name(name))
20
+ say "Deploying to #{stack.name}..."
21
+ stack.deploy_app(app)
22
+ end
23
+ deployments.compact!
24
+ OpsWorks::Deployment.wait(deployments, options[:timeout])
25
+ unless deployments.all?(&:success?)
26
+ failures = []
27
+ deployments.each_with_index do |deployment, i|
28
+ failures << stacks[i].name unless deployment.success?
29
+ end
30
+ fail "Deploy failed on #{failures.join(', ')}"
31
+ end
32
+ end
33
+
34
+ desc 'apps:status APP [--stack STACK]',
35
+ 'Display the most recent deployment of an app'
36
+ option :stack, type: :array
37
+ define_method 'apps:status' do |name|
38
+ fetch_keychain_credentials unless env_credentials?
39
+
40
+ table = parse_stacks(options).map do |stack|
41
+ next unless (app = stack.find_app_by_name(name))
42
+ if (deployment = app.last_deployment)
43
+ deployed_at = formatted_time(deployment.created_at)
44
+ else
45
+ deployed_at = '-'
46
+ end
47
+ [stack.name, name, "(#{app.revision})", deployed_at]
48
+ end
49
+ # Sort output in descending date order
50
+ table.compact!
51
+ table.sort! { |x, y| y.last <=> x.last }
52
+ print_table table
53
+ end
54
+
55
+ desc 'apps:create APP [--stack STACK]', 'Create a new OpsWorks app'
56
+ option :stack, type: :array
57
+ option :type, default: 'other'
58
+ option :git_url
59
+ option :shortname
60
+ define_method 'apps:create' do |name|
61
+ unless %w(other).include?(options[:type])
62
+ fail "Unsupported type: #{options[:type]}"
63
+ end
64
+
65
+ fail 'Git URL not yet supported' if options[:git_url]
66
+
67
+ fetch_keychain_credentials unless env_credentials?
68
+ stacks = parse_stacks(options)
69
+
70
+ stacks.each do |stack|
71
+ next if stack.apps.map(&:name).include?(name)
72
+ say "Creating app on #{stack.name}."
73
+ stack.create_app(name, options)
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def formatted_time(timestamp)
80
+ timestamp.strftime('%Y-%m-%d %H:%M:%S %Z')
81
+ end
82
+ end
83
+ end
84
+ # rubocop:enable PerceivedComplexity
85
+ # rubocop:enable CyclomaticComplexity
86
+ # rubocop:enable MethodLength
87
+ end
88
+ end
89
+ end
90
+ end
@@ -3,16 +3,17 @@ require 'opsworks/permission'
3
3
  module OpsWorks
4
4
  module CLI
5
5
  module Subcommands
6
- module Allow
6
+ module IAM
7
7
  # rubocop:disable MethodLength
8
8
  # rubocop:disable CyclomaticComplexity
9
9
  def self.included(thor)
10
10
  thor.class_eval do
11
- desc 'allow USER [--stack STACK]', 'Allow an IAM user on a stack'
11
+ desc 'iam:allow USER [--stack STACK]',
12
+ 'Allow an IAM user on a stack'
12
13
  option :stack, type: :array
13
14
  option :ssh, type: :boolean, default: true
14
15
  option :sudo, type: :boolean, default: true
15
- def allow(user)
16
+ define_method 'iam:allow' do |user|
16
17
  fetch_keychain_credentials unless env_credentials?
17
18
  stacks = parse_stacks(options.merge(active: true))
18
19
  stacks.each do |stack|
@@ -22,6 +23,20 @@ module OpsWorks
22
23
  permission.update(ssh: options[:ssh], sudo: options[:sudo])
23
24
  end
24
25
  end
26
+
27
+ desc 'iam:lockdown [--stack STACK]', 'Remove all stack permissions'
28
+ option :stack, type: :array
29
+ define_method 'iam:lockdown' do
30
+ fetch_keychain_credentials unless env_credentials?
31
+ stacks = parse_stacks(options.merge(active: true))
32
+ stacks.each do |stack|
33
+ say "Locking down #{stack.name}..."
34
+ stack.permissions.each do |permission|
35
+ permission.update(ssh: false, sudo: false)
36
+ end
37
+ end
38
+ end
39
+
25
40
  end
26
41
  end
27
42
  # rubocop:enable CyclomaticComplexity
@@ -3,21 +3,22 @@ require 'opsworks/deployment'
3
3
  module OpsWorks
4
4
  module CLI
5
5
  module Subcommands
6
- module Exec
6
+ module Recipes
7
7
  # rubocop:disable MethodLength
8
8
  # rubocop:disable CyclomaticComplexity
9
9
  def self.included(thor)
10
10
  thor.class_eval do
11
- desc 'exec RECIPE [--stack STACK]', 'Execute a Chef recipe'
11
+ desc 'recipes:run RECIPE [--stack STACK]', 'Execute a Chef recipe'
12
12
  option :stack, type: :array
13
- def exec(recipe)
13
+ option :timeout, type: :numeric
14
+ define_method 'recipes:run' do |recipe|
14
15
  fetch_keychain_credentials unless env_credentials?
15
16
  stacks = parse_stacks(options.merge(active: true))
16
17
  deployments = stacks.map do |stack|
17
18
  say "Executing recipe on #{stack.name}..."
18
19
  stack.execute_recipe(recipe)
19
20
  end
20
- OpsWorks::Deployment.wait(deployments)
21
+ OpsWorks::Deployment.wait(deployments, options[:timeout])
21
22
  unless deployments.all?(&:success?)
22
23
  failures = []
23
24
  deployments.each_with_index do |deployment, i|
@@ -26,6 +27,22 @@ module OpsWorks
26
27
  fail "Command failed on #{failures.join(', ')}"
27
28
  end
28
29
  end
30
+
31
+ desc 'recipes:add LAYER EVENT RECIPE [--stack STACK]',
32
+ 'Add a recipe to a given layer and lifecycle event'
33
+ option :stack, type: :array
34
+ define_method 'recipes:add' do |layername, event, recipe|
35
+ fetch_keychain_credentials unless env_credentials?
36
+ stacks = parse_stacks(options)
37
+ stacks.each do |stack|
38
+ layer = stack.layers.find { |l| l.shortname == layername }
39
+ next unless layer
40
+ next if layer.custom_recipes[event].include?(recipe)
41
+
42
+ say "Adding recipe to #{stack.name}."
43
+ layer.add_custom_recipe(event, recipe)
44
+ end
45
+ end
29
46
  end
30
47
  end
31
48
  # rubocop:enable CyclomaticComplexity
@@ -14,6 +14,7 @@ module OpsWorks
14
14
 
15
15
  desc 'update [--stack STACK]', 'Update OpsWorks custom cookbooks'
16
16
  option :stack, type: :array
17
+ option :timeout, type: :numeric
17
18
  def update
18
19
  fetch_keychain_credentials unless env_credentials?
19
20
  stacks = parse_stacks(options.merge(active: true))
@@ -21,7 +22,7 @@ module OpsWorks
21
22
  say "Updating #{stack.name}..."
22
23
  stack.update_custom_cookbooks
23
24
  end
24
- OpsWorks::Deployment.wait(deployments)
25
+ OpsWorks::Deployment.wait(deployments, options[:timeout])
25
26
  unless deployments.all?(&:success?)
26
27
  failures = []
27
28
  deployments.each_with_index do |deployment, i|
@@ -1,5 +1,5 @@
1
1
  module OpsWorks
2
2
  module CLI
3
- VERSION = '0.2.4'
3
+ VERSION = '0.3.0'
4
4
  end
5
5
  end
@@ -0,0 +1,36 @@
1
+ require 'opsworks/resource'
2
+ require 'thor'
3
+
4
+ module OpsWorks
5
+ class Layer < Resource
6
+ attr_accessor :id, :name, :shortname, :custom_recipes
7
+
8
+ # rubocop:disable MethodLength
9
+ def self.from_collection_response(response)
10
+ response.data[:layers].map do |hash|
11
+ # Make custom_recipes accessible by string or symbol
12
+ custom_recipes = Thor::CoreExt::HashWithIndifferentAccess.new(
13
+ hash[:custom_recipes]
14
+ )
15
+ new(
16
+ id: hash[:layer_id],
17
+ name: hash[:name],
18
+ shortname: hash[:shortname],
19
+ custom_recipes: custom_recipes
20
+ )
21
+ end
22
+ end
23
+ # rubocop:enable MethodLength
24
+
25
+ def add_custom_recipe(event, recipe)
26
+ return if custom_recipes[event].include?(recipe)
27
+
28
+ custom_recipes[event] ||= []
29
+ custom_recipes[event].push recipe
30
+ self.class.client.update_layer(
31
+ layer_id: id,
32
+ custom_recipes: custom_recipes
33
+ )
34
+ end
35
+ end
36
+ end
@@ -1,9 +1,11 @@
1
1
  require 'jsonpath'
2
+ require 'active_support/core_ext/hash/slice'
2
3
 
3
4
  require 'opsworks/resource'
4
5
  require 'opsworks/app'
5
6
  require 'opsworks/instance'
6
7
  require 'opsworks/permission'
8
+ require 'opsworks/layer'
7
9
 
8
10
  module OpsWorks
9
11
  # rubocop:disable ClassLength
@@ -54,6 +56,10 @@ module OpsWorks
54
56
  @instances ||= initialize_instances
55
57
  end
56
58
 
59
+ def layers
60
+ @layers ||= initialize_layers
61
+ end
62
+
57
63
  def upgrade_chef(version, options = {})
58
64
  self.class.client.update_stack(
59
65
  stack_id: id,
@@ -97,6 +103,12 @@ module OpsWorks
97
103
  )
98
104
  end
99
105
 
106
+ def create_app(name, options = {})
107
+ options = options.slice(:type, :shortname)
108
+ options.merge!(stack_id: id, name: name)
109
+ self.class.client.create_app(options)
110
+ end
111
+
100
112
  private
101
113
 
102
114
  def initialize_apps
@@ -106,12 +118,15 @@ module OpsWorks
106
118
  end
107
119
 
108
120
  # rubocop:disable Eval
121
+ # rubocop:disable MethodLength
109
122
  def replace_hash_at_path(hash, key, value)
110
123
  path = JsonPath.new(key).path
111
124
  if value
112
125
  # REVIEW: Is there a better way to parse the JSON Path and ensure
113
126
  # a value at the location?
114
- hash.default_proc = ->(h, k) { h[k] = Hash.new(&h.default_proc) }
127
+ (0...(path.length - 1)).each do |i|
128
+ eval("hash#{path[0..i].join('')} ||= {}")
129
+ end
115
130
  eval("hash#{path.join('')} = #{value.inspect}")
116
131
  elsif JsonPath.new(key).on(hash).count > 0
117
132
  # Path value is present, but we need to unset it
@@ -121,6 +136,7 @@ module OpsWorks
121
136
 
122
137
  hash
123
138
  end
139
+ # rubocop:enable MethodLength
124
140
  # rubocop:enable Eval
125
141
 
126
142
  def initialize_permissions
@@ -135,6 +151,12 @@ module OpsWorks
135
151
  Instance.from_collection_response(response)
136
152
  end
137
153
 
154
+ def initialize_layers
155
+ return [] unless id
156
+ response = self.class.client.describe_layers(stack_id: id)
157
+ Layer.from_collection_response(response)
158
+ end
159
+
138
160
  def create_deployment(options = {})
139
161
  response = self.class.client.create_deployment(
140
162
  options.merge(stack_id: id)
data/opsworks-cli.gemspec CHANGED
@@ -23,6 +23,7 @@ Gem::Specification.new do |spec|
23
23
  spec.add_dependency 'thor'
24
24
  spec.add_dependency 'aws-sdk'
25
25
  spec.add_dependency 'jsonpath'
26
+ spec.add_dependency 'activesupport'
26
27
 
27
28
  spec.add_development_dependency 'aws-keychain-util'
28
29
  spec.add_development_dependency 'bundler', '~> 1.5'
@@ -0,0 +1,90 @@
1
+ require 'spec_helper'
2
+
3
+ describe OpsWorks::CLI::Agent do
4
+ context 'apps' do
5
+ let(:app_name) { 'aptible' }
6
+ let(:stacks) { 2.times.map { Fabricate(:stack) } }
7
+
8
+ before { allow(subject).to receive(:say) }
9
+ before { allow(OpsWorks::Deployment).to receive(:wait) }
10
+ before { allow(OpsWorks::Stack).to receive(:all) { stacks } }
11
+ before { allow(OpsWorks::Stack).to receive(:active) { stacks } }
12
+
13
+ describe 'apps:deploy' do
14
+ let(:app) { Fabricate(:app, name: app_name) }
15
+ let(:success) { Fabricate(:deployment, status: 'successful') }
16
+ let(:failure) { Fabricate(:deployment, status: 'failed') }
17
+
18
+ before do
19
+ stacks.each { |stack| allow(stack).to receive(:apps) { [app] } }
20
+ end
21
+
22
+ it 'should update custom cookbooks on all stacks' do
23
+ expect(stacks[0]).to receive(:deploy_app).with(app) { success }
24
+ expect(stacks[1]).to receive(:deploy_app).with(app) { success }
25
+ subject.send('apps:deploy', app_name)
26
+ end
27
+
28
+ it 'should not fail if some stacks are inactive' do
29
+ allow(OpsWorks::Stack).to receive(:active) { [stacks[0]] }
30
+ expect(stacks[0]).to receive(:deploy_app).with(app) { success }
31
+ expect(stacks[1]).not_to receive(:deploy_app)
32
+ subject.send('apps:deploy', app_name)
33
+ end
34
+
35
+ it 'should optionally run on a subset of stacks' do
36
+ expect(stacks[0]).to receive(:deploy_app).with(app) { success }
37
+ expect(stacks[1]).not_to receive(:deploy_app)
38
+
39
+ allow(subject).to receive(:options) { { stack: [stacks[0].name] } }
40
+ subject.send('apps:deploy', app_name)
41
+ end
42
+
43
+ it 'should not fail if a stack does not have the app' do
44
+ allow(stacks[0]).to receive(:apps) { [] }
45
+ expect(stacks[1]).to receive(:deploy_app).with(app) { success }
46
+ expect { subject.send('apps:deploy', app_name) }.not_to raise_error
47
+ end
48
+
49
+ it 'should fail if any update fails' do
50
+ expect(stacks[0]).to receive(:deploy_app).with(app) { failure }
51
+
52
+ allow(subject).to receive(:options) { { stack: [stacks[0].name] } }
53
+ expect { subject.send('apps:deploy', app_name) }.to raise_error
54
+ end
55
+ end
56
+
57
+ describe 'apps:create' do
58
+ # TODO: Figure out why Thor doesn't populate options from defaults
59
+ # when methods are invoked directly
60
+ let(:options) { { type: 'other' } }
61
+
62
+ before do
63
+ stacks.each { |stack| allow(stack).to receive(:apps) { [] } }
64
+ end
65
+
66
+ it 'should fail with a helpful error on unsupported type' do
67
+ options.merge!(type: 'foobar')
68
+ allow(subject).to receive(:options) { options }
69
+ expect { subject.send('apps:create', app_name) }.to raise_error
70
+ end
71
+
72
+ xit 'should accept a Git URL'
73
+
74
+ it 'should create an app' do
75
+ allow(subject).to receive(:options) { options }
76
+ expect(stacks[0]).to receive(:create_app).with(app_name, options)
77
+ expect(stacks[1]).to receive(:create_app).with(app_name, options)
78
+ subject.send('apps:create', app_name)
79
+ end
80
+
81
+ it 'should accept a different shortname' do
82
+ options.merge!(shortname: 'foobar')
83
+ allow(subject).to receive(:options) { options }
84
+ expect(stacks[0]).to receive(:create_app).with(app_name, options)
85
+ expect(stacks[1]).to receive(:create_app).with(app_name, options)
86
+ subject.send('apps:create', app_name)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -38,6 +38,16 @@ describe OpsWorks::CLI::Agent do
38
38
  expect(stack.custom_json['env']['FOO']).to eq 'baz'
39
39
  end
40
40
 
41
+ it 'should work with deep nested hashes' do
42
+ stack.custom_json = { 'app' => { 'var' => 'value' } }
43
+ expect(client).to receive(:update_stack) do |hash|
44
+ json = JSON.parse(hash[:custom_json])
45
+ expect(json['app']['env']['FOO']).to eq 'baz'
46
+ end
47
+ subject.send('config:set', 'app.env.FOO', 'baz')
48
+ expect(stack.custom_json['app']['env']['FOO']).to eq 'baz'
49
+ end
50
+
41
51
  it 'should set the variable, if it is unset' do
42
52
  stack.custom_json = {}
43
53
  expect(client).to receive(:update_stack) do |hash|
@@ -0,0 +1,68 @@
1
+ require 'spec_helper'
2
+
3
+ describe OpsWorks::CLI::Agent do
4
+ context 'iam' do
5
+ let(:permissions) { 2.times.map { Fabricate(:permission) } }
6
+ let(:user) { permissions[0].user }
7
+
8
+ before { allow(subject).to receive(:say) }
9
+ before { allow(OpsWorks::Stack).to receive(:all) { stacks } }
10
+ before { allow(OpsWorks::Stack).to receive(:active) { stacks } }
11
+
12
+ describe 'iam:allow' do
13
+ let(:stacks) do
14
+ 2.times.map do |i|
15
+ Fabricate(:stack).tap do |stack|
16
+ allow(stack).to receive(:find_permission_by_user) { permissions[i] }
17
+ end
18
+ end
19
+ end
20
+
21
+ it 'should update all matching permissions' do
22
+ expect(permissions[0]).to receive(:update)
23
+ expect(permissions[1]).to receive(:update)
24
+ subject.send('iam:allow', user)
25
+ end
26
+
27
+ it 'should optionally run on a subset of stacks' do
28
+ expect(permissions[0]).to receive(:update)
29
+ expect(permissions[1]).not_to receive(:update)
30
+
31
+ allow(subject).to receive(:options) { { stack: [stacks[0].name] } }
32
+ subject.send('iam:allow', user)
33
+ end
34
+
35
+ it 'should accept :ssh and :sudo options' do
36
+ expect(permissions[0]).to receive(:update).with(ssh: true, sudo: false)
37
+
38
+ allow(subject).to receive(:options) do
39
+ { stack: [stacks[0].name], ssh: true, sudo: false }
40
+ end
41
+ subject.send('iam:allow', user)
42
+ end
43
+ end
44
+
45
+ describe 'iam:lockdown' do
46
+ let(:stack) do
47
+ Fabricate(:stack).tap do |stack|
48
+ allow(stack).to receive(:permissions) { permissions }
49
+ end
50
+ end
51
+ let(:stacks) { [stack] }
52
+
53
+ it 'should lock down all stacks' do
54
+ expect(permissions[0]).to receive(:update).with(ssh: false, sudo: false)
55
+ expect(permissions[1]).to receive(:update).with(ssh: false, sudo: false)
56
+ subject.send('iam:lockdown')
57
+ end
58
+
59
+ it 'should optionally run on a subset of stacks' do
60
+ expect(permissions[0]).to receive(:update).with(ssh: false, sudo: false)
61
+ expect(permissions[1]).to receive(:update).with(ssh: false, sudo: false)
62
+
63
+ allow(subject).to receive(:options) { { stacks: [stack.name] } }
64
+ subject.send('iam:lockdown')
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ describe OpsWorks::CLI::Agent do
4
+ context 'recipes' do
5
+ let(:recipe) { 'hotpockets::install' }
6
+ let(:stacks) { 2.times.map { Fabricate(:stack) } }
7
+
8
+ before { allow(subject).to receive(:say) }
9
+ before { allow(OpsWorks::Deployment).to receive(:wait) }
10
+ before { allow(OpsWorks::Stack).to receive(:all) { stacks } }
11
+ before { allow(OpsWorks::Stack).to receive(:active) { stacks } }
12
+
13
+ describe 'recipes:run' do
14
+ let(:success) { Fabricate(:deployment, status: 'successful') }
15
+ let(:failure) { Fabricate(:deployment, status: 'failed') }
16
+
17
+ it 'should update custom cookbooks on all stacks' do
18
+ expect(stacks[0]).to receive(:execute_recipe).with(recipe) { success }
19
+ expect(stacks[1]).to receive(:execute_recipe).with(recipe) { success }
20
+ subject.send('recipes:run', recipe)
21
+ end
22
+
23
+ it 'should optionally run on a subset of stacks' do
24
+ expect(stacks[0]).to receive(:execute_recipe).with(recipe) { success }
25
+ expect(stacks[1]).not_to receive(:execute_recipe)
26
+
27
+ allow(subject).to receive(:options) { { stack: [stacks[0].name] } }
28
+ subject.send('recipes:run', recipe)
29
+ end
30
+
31
+ it 'should fail if any update fails' do
32
+ expect(stacks[0]).to receive(:execute_recipe).with(recipe) { failure }
33
+
34
+ allow(subject).to receive(:options) { { stack: [stacks[0].name] } }
35
+ expect { subject.send('recipes:run', recipe) }.to raise_error
36
+ end
37
+ end
38
+ end
39
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opsworks-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Frank Macreery
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-11-19 00:00:00.000000000 Z
11
+ date: 2014-12-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activesupport
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'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: aws-keychain-util
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -171,17 +185,16 @@ files:
171
185
  - lib/opsworks/cli/agent.rb
172
186
  - lib/opsworks/cli/helpers/keychain.rb
173
187
  - lib/opsworks/cli/helpers/options.rb
174
- - lib/opsworks/cli/subcommands/allow.rb
188
+ - lib/opsworks/cli/subcommands/apps.rb
175
189
  - lib/opsworks/cli/subcommands/config.rb
176
- - lib/opsworks/cli/subcommands/deploy.rb
177
- - lib/opsworks/cli/subcommands/exec.rb
178
- - lib/opsworks/cli/subcommands/lockdown.rb
179
- - lib/opsworks/cli/subcommands/status.rb
190
+ - lib/opsworks/cli/subcommands/iam.rb
191
+ - lib/opsworks/cli/subcommands/recipes.rb
180
192
  - lib/opsworks/cli/subcommands/update.rb
181
193
  - lib/opsworks/cli/subcommands/upgrade_chef.rb
182
194
  - lib/opsworks/cli/version.rb
183
195
  - lib/opsworks/deployment.rb
184
196
  - lib/opsworks/instance.rb
197
+ - lib/opsworks/layer.rb
185
198
  - lib/opsworks/permission.rb
186
199
  - lib/opsworks/resource.rb
187
200
  - lib/opsworks/stack.rb
@@ -191,11 +204,10 @@ files:
191
204
  - spec/fabricators/opsworks/permission_fabricator.rb
192
205
  - spec/fabricators/opsworks/stack_fabricator.rb
193
206
  - spec/opsworks/cli/agent_spec.rb
194
- - spec/opsworks/cli/subcommands/allow_spec.rb
207
+ - spec/opsworks/cli/subcommands/apps_spec.rb
195
208
  - spec/opsworks/cli/subcommands/config_spec.rb
196
- - spec/opsworks/cli/subcommands/deploy_spec.rb
197
- - spec/opsworks/cli/subcommands/exec_spec.rb
198
- - spec/opsworks/cli/subcommands/lockdown_spec.rb
209
+ - spec/opsworks/cli/subcommands/iam_spec.rb
210
+ - spec/opsworks/cli/subcommands/recipes_spec.rb
199
211
  - spec/opsworks/cli/subcommands/update_spec.rb
200
212
  - spec/spec_helper.rb
201
213
  homepage: https://github.com/aptible/opsworks-cli
@@ -228,10 +240,9 @@ test_files:
228
240
  - spec/fabricators/opsworks/permission_fabricator.rb
229
241
  - spec/fabricators/opsworks/stack_fabricator.rb
230
242
  - spec/opsworks/cli/agent_spec.rb
231
- - spec/opsworks/cli/subcommands/allow_spec.rb
243
+ - spec/opsworks/cli/subcommands/apps_spec.rb
232
244
  - spec/opsworks/cli/subcommands/config_spec.rb
233
- - spec/opsworks/cli/subcommands/deploy_spec.rb
234
- - spec/opsworks/cli/subcommands/exec_spec.rb
235
- - spec/opsworks/cli/subcommands/lockdown_spec.rb
245
+ - spec/opsworks/cli/subcommands/iam_spec.rb
246
+ - spec/opsworks/cli/subcommands/recipes_spec.rb
236
247
  - spec/opsworks/cli/subcommands/update_spec.rb
237
248
  - spec/spec_helper.rb
@@ -1,38 +0,0 @@
1
- require 'opsworks/deployment'
2
-
3
- module OpsWorks
4
- module CLI
5
- module Subcommands
6
- module Deploy
7
- # rubocop:disable MethodLength
8
- # rubocop:disable CyclomaticComplexity
9
- def self.included(thor)
10
- thor.class_eval do
11
- desc 'deploy APP [--stack STACK]', 'Deploy an OpsWorks app'
12
- option :stack, type: :array
13
- def deploy(name)
14
- fetch_keychain_credentials unless env_credentials?
15
- stacks = parse_stacks(options.merge(active: true))
16
- deployments = stacks.map do |stack|
17
- next unless (app = stack.find_app_by_name(name))
18
- say "Deploying to #{stack.name}..."
19
- stack.deploy_app(app)
20
- end
21
- deployments.compact!
22
- OpsWorks::Deployment.wait(deployments)
23
- unless deployments.all?(&:success?)
24
- failures = []
25
- deployments.each_with_index do |deployment, i|
26
- failures << stacks[i].name unless deployment.success?
27
- end
28
- fail "Deploy failed on #{failures.join(', ')}"
29
- end
30
- end
31
- end
32
- end
33
- # rubocop:enable CyclomaticComplexity
34
- # rubocop:enable MethodLength
35
- end
36
- end
37
- end
38
- end
@@ -1,30 +0,0 @@
1
- require 'opsworks/permission'
2
-
3
- module OpsWorks
4
- module CLI
5
- module Subcommands
6
- module Lockdown
7
- # rubocop:disable MethodLength
8
- # rubocop:disable CyclomaticComplexity
9
- def self.included(thor)
10
- thor.class_eval do
11
- desc 'lockdown [--stack STACK]', 'Remove all stack permissions'
12
- option :stack, type: :array
13
- def lockdown
14
- fetch_keychain_credentials unless env_credentials?
15
- stacks = parse_stacks(options.merge(active: true))
16
- stacks.each do |stack|
17
- say "Locking down #{stack.name}..."
18
- stack.permissions.each do |permission|
19
- permission.update(ssh: false, sudo: false)
20
- end
21
- end
22
- end
23
- end
24
- end
25
- # rubocop:enable CyclomaticComplexity
26
- # rubocop:enable MethodLength
27
- end
28
- end
29
- end
30
- end
@@ -1,45 +0,0 @@
1
- module OpsWorks
2
- module CLI
3
- module Subcommands
4
- module Status
5
- # rubocop:disable MethodLength
6
- # rubocop:disable CyclomaticComplexity
7
- def self.included(thor)
8
- thor.class_eval do
9
- include Helpers::Keychain
10
- include Helpers::Options
11
-
12
- desc 'status APP [--stack STACK]',
13
- 'Display the most recent deployment of an app'
14
- option :stack, type: :array
15
- def status(name)
16
- fetch_keychain_credentials unless env_credentials?
17
-
18
- table = parse_stacks(options).map do |stack|
19
- next unless (app = stack.find_app_by_name(name))
20
- if (deployment = app.last_deployment)
21
- deployed_at = formatted_time(deployment.created_at)
22
- else
23
- deployed_at = '-'
24
- end
25
- [stack.name, name, "(#{app.revision})", deployed_at]
26
- end
27
- # Sort output in descending date order
28
- table.compact!
29
- table.sort! { |x, y| y.last <=> x.last }
30
- print_table table
31
- end
32
-
33
- private
34
-
35
- def formatted_time(timestamp)
36
- timestamp.strftime('%Y-%m-%d %H:%M:%S %Z')
37
- end
38
- end
39
- end
40
- # rubocop:enable CyclomaticComplexity
41
- # rubocop:enable MethodLength
42
- end
43
- end
44
- end
45
- end
@@ -1,42 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe OpsWorks::CLI::Agent do
4
- describe '#allow' do
5
- let(:permissions) { 2.times.map { Fabricate(:permission) } }
6
- let(:user) { permissions[0].user }
7
- let(:stacks) do
8
- 2.times.map do |i|
9
- Fabricate(:stack).tap do |stack|
10
- allow(stack).to receive(:find_permission_by_user) { permissions[i] }
11
- end
12
- end
13
- end
14
-
15
- before { allow(subject).to receive(:say) }
16
- before { allow(OpsWorks::Stack).to receive(:all) { stacks } }
17
- before { allow(OpsWorks::Stack).to receive(:active) { stacks } }
18
-
19
- it 'should update all matching permissions' do
20
- expect(permissions[0]).to receive(:update)
21
- expect(permissions[1]).to receive(:update)
22
- subject.allow(user)
23
- end
24
-
25
- it 'should optionally run on a subset of stacks' do
26
- expect(permissions[0]).to receive(:update)
27
- expect(permissions[1]).not_to receive(:update)
28
-
29
- allow(subject).to receive(:options) { { stack: [stacks[0].name] } }
30
- subject.allow(user)
31
- end
32
-
33
- it 'should accept :ssh and :sudo options' do
34
- expect(permissions[0]).to receive(:update).with(ssh: true, sudo: false)
35
-
36
- allow(subject).to receive(:options) do
37
- { stack: [stacks[0].name], ssh: true, sudo: false }
38
- end
39
- subject.allow(user)
40
- end
41
- end
42
- end
@@ -1,53 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe OpsWorks::CLI::Agent do
4
- describe '#deploy' do
5
- let(:app_name) { 'aptible' }
6
- let(:app) { Fabricate(:app, name: app_name) }
7
-
8
- let(:stacks) { 2.times.map { Fabricate(:stack) } }
9
- let(:deployment) { Fabricate(:deployment, status: 'successful') }
10
-
11
- before { allow(subject).to receive(:say) }
12
- before { allow(OpsWorks::Deployment).to receive(:wait) }
13
- before { allow(OpsWorks::Stack).to receive(:all) { stacks } }
14
- before { allow(OpsWorks::Stack).to receive(:active) { stacks } }
15
-
16
- before { stacks.each { |stack| allow(stack).to receive(:apps) { [app] } } }
17
-
18
- it 'should update custom cookbooks on all stacks' do
19
- expect(stacks[0]).to receive(:deploy_app).with(app) { deployment }
20
- expect(stacks[1]).to receive(:deploy_app).with(app) { deployment }
21
- subject.deploy(app_name)
22
- end
23
-
24
- it 'should not fail if some stacks are inactive' do
25
- allow(OpsWorks::Stack).to receive(:active) { [stacks[0]] }
26
- expect(stacks[0]).to receive(:deploy_app).with(app) { deployment }
27
- expect(stacks[1]).not_to receive(:deploy_app)
28
- subject.deploy(app_name)
29
- end
30
-
31
- it 'should optionally run on a subset of stacks' do
32
- expect(stacks[0]).to receive(:deploy_app).with(app) { deployment }
33
- expect(stacks[1]).not_to receive(:deploy_app)
34
-
35
- allow(subject).to receive(:options) { { stack: [stacks[0].name] } }
36
- subject.deploy(app_name)
37
- end
38
-
39
- it 'should not fail if a stack does not have the app' do
40
- allow(stacks[0]).to receive(:apps) { [] }
41
- expect(stacks[1]).to receive(:deploy_app).with(app) { deployment }
42
- expect { subject.deploy(app_name) }.not_to raise_error
43
- end
44
-
45
- it 'should fail if any update fails' do
46
- failure = Fabricate(:deployment, status: 'failed')
47
- expect(stacks[0]).to receive(:deploy_app).with(app) { failure }
48
-
49
- allow(subject).to receive(:options) { { stack: [stacks[0].name] } }
50
- expect { subject.deploy(app_name) }.to raise_error
51
- end
52
- end
53
- end
@@ -1,37 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe OpsWorks::CLI::Agent do
4
- describe '#exec' do
5
- let(:recipe) { 'hotpockets::install' }
6
-
7
- let(:stacks) { 2.times.map { Fabricate(:stack) } }
8
- let(:deployment) { Fabricate(:deployment, status: 'successful') }
9
-
10
- before { allow(subject).to receive(:say) }
11
- before { allow(OpsWorks::Deployment).to receive(:wait) }
12
- before { allow(OpsWorks::Stack).to receive(:all) { stacks } }
13
- before { allow(OpsWorks::Stack).to receive(:active) { stacks } }
14
-
15
- it 'should update custom cookbooks on all stacks' do
16
- expect(stacks[0]).to receive(:execute_recipe).with(recipe) { deployment }
17
- expect(stacks[1]).to receive(:execute_recipe).with(recipe) { deployment }
18
- subject.exec(recipe)
19
- end
20
-
21
- it 'should optionally run on a subset of stacks' do
22
- expect(stacks[0]).to receive(:execute_recipe).with(recipe) { deployment }
23
- expect(stacks[1]).not_to receive(:execute_recipe)
24
-
25
- allow(subject).to receive(:options) { { stack: [stacks[0].name] } }
26
- subject.exec(recipe)
27
- end
28
-
29
- it 'should fail if any update fails' do
30
- failure = Fabricate(:deployment, status: 'failed')
31
- expect(stacks[0]).to receive(:execute_recipe).with(recipe) { failure }
32
-
33
- allow(subject).to receive(:options) { { stack: [stacks[0].name] } }
34
- expect { subject.exec(recipe) }.to raise_error
35
- end
36
- end
37
- end
@@ -1,31 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe OpsWorks::CLI::Agent do
4
- describe '#lockdown' do
5
- let(:permissions) { 2.times.map { Fabricate(:permission) } }
6
- let(:user) { permissions[0].user }
7
- let(:stack) do
8
- Fabricate(:stack).tap do |stack|
9
- allow(stack).to receive(:permissions) { permissions }
10
- end
11
- end
12
-
13
- before { allow(subject).to receive(:say) }
14
- before { allow(OpsWorks::Stack).to receive(:all) { [stack] } }
15
- before { allow(OpsWorks::Stack).to receive(:active) { [stack] } }
16
-
17
- it 'should lock down all stacks' do
18
- expect(permissions[0]).to receive(:update).with(ssh: false, sudo: false)
19
- expect(permissions[1]).to receive(:update).with(ssh: false, sudo: false)
20
- subject.lockdown
21
- end
22
-
23
- it 'should optionally run on a subset of stacks' do
24
- expect(permissions[0]).to receive(:update).with(ssh: false, sudo: false)
25
- expect(permissions[1]).to receive(:update).with(ssh: false, sudo: false)
26
-
27
- allow(subject).to receive(:options) { { stacks: [stack.name] } }
28
- subject.lockdown
29
- end
30
- end
31
- end