opsworks-cli 0.2.4 → 0.3.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 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