aptible-cli 0.19.3 → 0.19.4

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
  SHA256:
3
- metadata.gz: f20b71eb3388732ade1e6e53cb1f076761d40a06d460faf08fa3e468f239acc1
4
- data.tar.gz: 3e603d02a1dbe3e8168209f9a0f061d29851a48189d28dc549bff3a7d203a5e1
3
+ metadata.gz: f3f0904b2033596eb17ff98125288e5a2adec95ae6e379b95a5b8131b81bfe54
4
+ data.tar.gz: 9c002c17e22dd77ff79360671d0057455a70ff4ee2311d1466651a5c39b3c1a2
5
5
  SHA512:
6
- metadata.gz: e195096e2854788c488477ca6d42fd1beb15ce2b6044f0be32a774e6b3915c387694e4608c7546a94915dcb32b3f0844f4864544e4a64806dca23379c0383598
7
- data.tar.gz: b2fb1ef99eb836cac0e80419d362277b05877826e8cf4b8f8cb78f937168d4ab658b81f9fea6c7ea0559cd5fe6f926aec896869b7fa8737e4503536d39dfb161
6
+ metadata.gz: 0c1b0857f175135e6d7feb64a9a6a7b9633b73823f1f32100473f2f90c5b529a6dcf041b55bf4af47a92af08c425b24f4ce1abfacce3deea74b1e031e0bf5fa0
7
+ data.tar.gz: ccb65684ab24eb9055adee5e17a04d0a9dbf2cdc496ba1b71a9a6a229e83ee092ad43c2ee660f73b5da95b47b875bdf2d0212d6ede3d932455ff67188d4b47c1
data/README.md CHANGED
@@ -31,6 +31,7 @@ Commands:
31
31
  aptible apps # List all applications
32
32
  aptible apps:create HANDLE # Create a new application
33
33
  aptible apps:deprovision # Deprovision an app
34
+ aptible apps:rename OLD_HANDLE NEW_HANDLE [--environment ENVIRONMENT_HANDLE] # Rename an app handle. In order for the new app handle to appear in log drain and metric drain destinations, you must restart the app.
34
35
  aptible apps:scale SERVICE [--container-count COUNT] [--container-size SIZE_MB] # Scale a service
35
36
  aptible backup:list DB_HANDLE # List backups for a database
36
37
  aptible backup:orphaned # List backups associated with deprovisioned databases
@@ -50,6 +51,7 @@ Commands:
50
51
  aptible db:list # List all databases
51
52
  aptible db:modify HANDLE [--iops IOPS] [--volume-type [gp2, gp3]] # Modify a database disk
52
53
  aptible db:reload HANDLE # Reload a database
54
+ aptible db:rename OLD_HANDLE NEW_HANDLE [--environment ENVIRONMENT_HANDLE] # Rename a database handle. In order for the new database handle to appear in log drain and metric drain destinations, you must reload the database.
53
55
  aptible db:replicate HANDLE REPLICA_HANDLE [--container-size SIZE_MB] [--disk-size SIZE_GB] [--logical --version VERSION] [--key-arn KEY_ARN] # Create a replica/follower of a database
54
56
  aptible db:restart HANDLE [--container-size SIZE_MB] [--disk-size SIZE_GB] [--iops IOPS] [--volume-type [gp2, gp3]] # Restart a database
55
57
  aptible db:tunnel HANDLE # Create a local tunnel to a database
@@ -69,6 +71,7 @@ Commands:
69
71
  aptible endpoints:tls:modify [--app APP] ENDPOINT_HOSTNAME # Modify an App TLS Endpoint
70
72
  aptible environment:ca_cert # Retrieve the CA certificate associated with the environment
71
73
  aptible environment:list # List all environments
74
+ aptible environment:rename OLD_HANDLE NEW_HANDLE # Rename an environment handle. In order for the new environment handle to appear in log drain/metric destinations, you must restart the apps/databases in this environment.
72
75
  aptible help [COMMAND] # Describe available commands or one specific command
73
76
  aptible log_drain:create:datadog HANDLE --url DATADOG_URL --environment ENVIRONMENT [--drain-apps true/false] [--drain_databases true/false] [--drain_ephemeral_sessions true/false] [--drain_proxies true/false] # Create a Datadog Log Drain
74
77
  aptible log_drain:create:elasticsearch HANDLE --db DATABASE_HANDLE --environment ENVIRONMENT [--drain-apps true/false] [--drain_databases true/false] [--drain_ephemeral_sessions true/false] [--drain_proxies true/false] # Create an Elasticsearch Log Drain
@@ -87,6 +90,8 @@ Commands:
87
90
  aptible metric_drain:deprovision HANDLE --environment ENVIRONMENT # Deprovisions a Metric Drain
88
91
  aptible metric_drain:list # List all Metric Drains
89
92
  aptible operation:cancel OPERATION_ID # Cancel a running operation
93
+ aptible operation:follow OPERATION_ID # Follow logs of a running operation
94
+ aptible operation:logs OPERATION_ID # View logs for given operation
90
95
  aptible rebuild # Rebuild an app, and restart its services
91
96
  aptible restart # Restart all services associated with an app
92
97
  aptible services # List Services for an App
@@ -154,6 +154,19 @@ module Aptible
154
154
  raise Thor::Error, err
155
155
  end
156
156
 
157
+ def validate_image_type(type)
158
+ available_types = []
159
+
160
+ Aptible::Api::DatabaseImage.all(token: fetch_token).each do |i|
161
+ return true if i.type == type
162
+ available_types << i.type
163
+ end
164
+
165
+ err = "No Database Image of type \"#{type}\""
166
+ err = "#{err}, valid types: #{available_types.uniq.join(', ')}"
167
+ raise Thor::Error, err
168
+ end
169
+
157
170
  def render_database(database, account)
158
171
  Formatter.render(Renderer.current) do |root|
159
172
  root.keyed_object('connection_url') do |node|
@@ -1,4 +1,5 @@
1
1
  require 'aptible/api'
2
+ require 'net/http'
2
3
 
3
4
  module Aptible
4
5
  module CLI
@@ -48,6 +49,15 @@ module Aptible
48
49
  operation.update!(cancelled: true)
49
50
  end
50
51
 
52
+ def operation_logs(operation)
53
+ res = get_operation_logs_redirect(operation)
54
+ s3_file_request = get_operation_logs_s3_file(res.body)
55
+
56
+ m = "Printing out results of operation logs for #{operation.id}"
57
+ CLI.logger.info m
58
+ puts s3_file_request.body
59
+ end
60
+
51
61
  def prettify_operation(o)
52
62
  bits = [o.status, o.type, "##{o.id}"]
53
63
  if o.resource.respond_to?(:handle)
@@ -55,6 +65,37 @@ module Aptible
55
65
  end
56
66
  bits.join ' '
57
67
  end
68
+
69
+ private
70
+
71
+ def get_operation_logs_redirect(operation)
72
+ uri = URI(operation.logs_url)
73
+ headers = { 'Authorization' => "Bearer #{fetch_token}" }
74
+ http = Net::HTTP.new(uri.host, uri.port)
75
+ http.use_ssl = true
76
+ res = http.request(Net::HTTP::Get.new(uri.request_uri, headers))
77
+ # note: res body with a 200 is target redirect location for download
78
+ if !res || res.code != '200' || res.body.nil?
79
+ raise Thor::Error, 'Unable to retrieve the operation\'s logs. '\
80
+ 'If the issue persists please contact support for assistance.'
81
+ end
82
+ res
83
+ end
84
+
85
+ def get_operation_logs_s3_file(location)
86
+ s3_uri = URI(location)
87
+ http = Net::HTTP.new(s3_uri.host, s3_uri.port)
88
+ http.use_ssl = true
89
+
90
+ # follow the link with redirect and retrieve it from s3 directly
91
+ res = http.request(Net::HTTP::Get.new(s3_uri.request_uri))
92
+ if !res || res.code != '200'
93
+ raise Thor::Error, 'Unable to retrieve operation logs, '\
94
+ "S3 returned response code #{res.code}. "\
95
+ 'If the issue persists please contact support for assistance.'
96
+ end
97
+ res
98
+ end
58
99
  end
59
100
  end
60
101
  end
@@ -94,6 +94,29 @@ module Aptible
94
94
  raise if e.response.status != 404
95
95
  end
96
96
  end
97
+
98
+ desc 'apps:rename OLD_HANDLE NEW_HANDLE [--environment'\
99
+ ' ENVIRONMENT_HANDLE]', 'Rename an app handle. In order'\
100
+ ' for the new app handle to appear in log drain and metric'\
101
+ ' drain destinations, you must restart the app.'
102
+ option :environment
103
+ define_method 'apps:rename' do |old_handle, new_handle|
104
+ env = ensure_environment(options)
105
+ app = ensure_app(options.merge(app: old_handle))
106
+ app.update!(handle: new_handle)
107
+ m1 = "In order for the new app name (#{new_handle}) to appear"\
108
+ ' in log drain and metric drain destinations, you must'\
109
+ ' restart the app.'
110
+ m2 = 'You can restart your app with this command: "aptible '\
111
+ "restart --app #{new_handle} --environment #{env.handle}\""
112
+ m3 = 'Warning - Git remote addresses must be updated to match'\
113
+ ' the new handle, if using Dockerfile deploy. '\
114
+ "(git@beta.aptible.com:#{app.account.handle}"\
115
+ "/#{new_handle}.git)"
116
+ CLI.logger.warn m1
117
+ CLI.logger.info m2
118
+ CLI.logger.warn m3
119
+ end
97
120
  end
98
121
  end
99
122
  end
@@ -84,6 +84,7 @@ module Aptible
84
84
  version = options[:version]
85
85
 
86
86
  if version && type
87
+ validate_image_type(type)
87
88
  image = find_database_image(type, version)
88
89
  db_opts[:type] = image.type
89
90
  db_opts[:database_image] = image
@@ -91,6 +92,7 @@ module Aptible
91
92
  raise Thor::Error, '--type is required when passing --version'
92
93
  else
93
94
  db_opts[:type] = type || 'postgresql'
95
+ validate_image_type(db_opts[:type])
94
96
  end
95
97
 
96
98
  database = account.create_database!(db_opts)
@@ -346,6 +348,24 @@ module Aptible
346
348
  end
347
349
  end
348
350
  end
351
+
352
+ desc 'db:rename OLD_HANDLE NEW_HANDLE [--environment'\
353
+ ' ENVIRONMENT_HANDLE]', 'Rename a database handle. In order'\
354
+ ' for the new database handle to appear in log drain and'\
355
+ ' metric drain destinations, you must reload the database.'
356
+ option :environment
357
+ define_method 'db:rename' do |old_handle, new_handle|
358
+ env = ensure_environment(options)
359
+ db = ensure_database(options.merge(db: old_handle))
360
+ db.update!(handle: new_handle)
361
+ m1 = "In order for the new database name (#{new_handle}) to"\
362
+ ' appear in log drain and metric drain destinations,'\
363
+ ' you must reload the database.'
364
+ m2 = 'You can reload your database with this command: "aptible'\
365
+ " db:reload #{new_handle} --environment #{env.handle}\""
366
+ CLI.logger.warn m1
367
+ CLI.logger.info m2
368
+ end
349
369
  end
350
370
  end
351
371
  end
@@ -41,6 +41,25 @@ module Aptible
41
41
  end
42
42
  end
43
43
  end
44
+
45
+ desc 'environment:rename OLD_HANDLE NEW_HANDLE',
46
+ 'Rename an environment handle. In order for the new'\
47
+ ' environment handle to appear in log drain/metric'\
48
+ ' destinations, you must restart the apps/databases in'\
49
+ ' this environment.'
50
+ define_method 'environment:rename' do |old_handle, new_handle|
51
+ env = ensure_environment(options.merge(environment: old_handle))
52
+ env.update!(handle: new_handle)
53
+ m1 = "In order for the new environment handle (#{new_handle})"\
54
+ ' to appear in log drain and metric drain destinations,'\
55
+ ' you must restart the apps and databases in this'\
56
+ ' environment. Also be aware of the following resources'\
57
+ ' that may need names adjusted:'
58
+ m2 = "* Git remote URLs (ex: git@beta.aptible.com:#{new_handle}"\
59
+ '/APP_HANDLE.git)'
60
+ m3 = '* Your own external scripts (e.g. for CI/CD)'
61
+ [m1, m2, m3].each { |val| CLI.logger.info val }
62
+ end
44
63
  end
45
64
  end
46
65
  end
@@ -8,6 +8,7 @@ module Aptible
8
8
  'EU1' => 'https://app.datadoghq.eu',
9
9
  'US1-FED' => 'https://app.ddog-gov.com'
10
10
  }.freeze
11
+ PATH = '/api/v1/series'.freeze
11
12
 
12
13
  def self.included(thor)
13
14
  thor.class_eval do
@@ -106,7 +107,7 @@ module Aptible
106
107
  "Valid options are #{sites}"
107
108
  end
108
109
 
109
- config[:series_url] = site
110
+ config[:series_url] = site + PATH
110
111
  end
111
112
  opts = {
112
113
  handle: handle,
@@ -16,6 +16,39 @@ module Aptible
16
16
  CLI.logger.info m
17
17
  o.update!(cancelled: true)
18
18
  end
19
+
20
+ desc 'operation:follow OPERATION_ID',
21
+ 'Follow logs of a running operation'
22
+ define_method 'operation:follow' do |operation_id|
23
+ o = Aptible::Api::Operation.find(operation_id, token: fetch_token)
24
+ raise "Operation ##{operation_id} not found" if o.nil?
25
+
26
+ if %w(failed succeeded).include? o.status
27
+ raise Thor::Error, "This operation has already #{o.status}. " \
28
+ 'Run the following command to retrieve ' \
29
+ "the operation's logs:\n" \
30
+ "aptible operation:logs #{o.id}"
31
+ end
32
+
33
+ CLI.logger.info "Streaming logs for #{prettify_operation(o)}..."
34
+
35
+ attach_to_operation_logs(o)
36
+ end
37
+
38
+ desc 'operation:logs OPERATION_ID', 'View logs for given operation'
39
+ define_method 'operation:logs' do |operation_id|
40
+ o = Aptible::Api::Operation.find(operation_id, token: fetch_token)
41
+ raise "Operation ##{operation_id} not found" if o.nil?
42
+
43
+ unless %w(succeeded failed).include? o.status
44
+ e = 'Error - You can view the logs when operation is complete.'
45
+ raise Thor::Error, e
46
+ end
47
+
48
+ m = "Requesting operation logs for #{prettify_operation(o)}..."
49
+ CLI.logger.info m
50
+ operation_logs(o)
51
+ end
19
52
  end
20
53
  end
21
54
  end
@@ -1,5 +1,5 @@
1
1
  module Aptible
2
2
  module CLI
3
- VERSION = '0.19.3'.freeze
3
+ VERSION = '0.19.4'.freeze
4
4
  end
5
5
  end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+
3
+ describe Aptible::CLI::Helpers::Database do
4
+ subject { Class.new.send(:include, described_class).new }
5
+
6
+ describe '#validate_image_type' do
7
+ let(:pg) do
8
+ Fabricate(:database_image, type: 'postgresql', version: '10')
9
+ end
10
+
11
+ let(:redis) do
12
+ Fabricate(:database_image, type: 'redis', version: '9.4')
13
+ end
14
+
15
+ let(:token) { 'some-token' }
16
+
17
+ before do
18
+ allow(subject).to receive(:fetch_token).and_return(token)
19
+ allow(Aptible::Api::DatabaseImage).to receive(:all)
20
+ .and_return([pg, redis])
21
+ end
22
+
23
+ it 'Raises an error if provided an invalid type' do
24
+ bad_type = 'cassandra'
25
+ err = "No Database Image of type \"#{bad_type}\", " \
26
+ "valid types: #{pg.type}, #{redis.type}"
27
+ expect do
28
+ subject.validate_image_type(bad_type)
29
+ end.to raise_error(Thor::Error, err)
30
+ end
31
+
32
+ it 'Retruns true when provided a valid type' do
33
+ expect(subject.validate_image_type(pg.type)).to be(true)
34
+ end
35
+ end
36
+ end
@@ -197,6 +197,47 @@ describe Aptible::CLI::Agent do
197
197
  end
198
198
  end
199
199
 
200
+ describe '#apps:rename' do
201
+ before do
202
+ allow(Aptible::Api::App).to receive(:all) { [app] }
203
+ allow(Aptible::Api::Account).to receive(:all) { [account] }
204
+ end
205
+
206
+ before(:each) do
207
+ allow(subject).to receive(:options)
208
+ .and_return(environment: account.handle)
209
+ end
210
+
211
+ context 'with environment and app' do
212
+ it 'should rename properly' do
213
+ expect(app).to receive(:update!)
214
+ .with(handle: 'hello2').and_return(app)
215
+ subject.send('apps:rename', 'hello', 'hello2')
216
+ expect(captured_logs).to include(
217
+ 'In order for the new app name (hello2) to appear in log drain and '\
218
+ 'metric drain destinations, you must restart the app.'
219
+ )
220
+ expect(captured_logs).to include(
221
+ "(git@beta.aptible.com:#{account.handle}/hello2.git)"
222
+ )
223
+ end
224
+ it 'should fail if app does not exist' do
225
+ expect { subject.send('apps:rename', 'hello2', 'hello3') }
226
+ .to raise_error(/Could not find app hello/)
227
+ end
228
+ it 'should raise error if update fails' do
229
+ response = Faraday::Response.new(status: 422)
230
+ error = HyperResource::ClientError.new('ActiveRecord::RecordInvalid:'\
231
+ ' Validation failed: Handle has already been taken, Handle has already'\
232
+ ' been taken', response: response)
233
+ expect(app).to receive(:update!)
234
+ .with(handle: 'hello2').and_raise(error)
235
+ expect { subject.send('apps:rename', 'hello', 'hello2') }
236
+ .to raise_error(HyperResource::ClientError)
237
+ end
238
+ end
239
+ end
240
+
200
241
  describe '#apps:scale' do
201
242
  before do
202
243
  allow(Aptible::Api::App).to receive(:all) { [app] }
@@ -22,6 +22,9 @@ describe Aptible::CLI::Agent do
22
22
  before do
23
23
  allow(Aptible::Api::Account).to receive(:all).and_return([account])
24
24
  end
25
+ before do
26
+ subject.stub(:validate_image_type) { true }
27
+ end
25
28
 
26
29
  def expect_provision_database(create_opts, provision_opts = {})
27
30
  db = Fabricate(:database)
@@ -876,4 +879,39 @@ describe Aptible::CLI::Agent do
876
879
  expect(captured_output_text).to eq(expected)
877
880
  end
878
881
  end
882
+
883
+ describe '#db:rename' do
884
+ before do
885
+ allow(subject).to receive(:options)
886
+ .and_return(environment: account.handle)
887
+ allow(Aptible::Api::Account).to receive(:all) { [account] }
888
+ end
889
+ context 'with environment and db' do
890
+ it 'should rename properly' do
891
+ expect(database).to receive(:update!)
892
+ .with(handle: 'foo2').and_return(database)
893
+ subject.send('db:rename', database.handle, 'foo2')
894
+ expect(captured_logs).to include(
895
+ 'In order for the new database name (foo2) to appear in log drain '\
896
+ 'and metric drain destinations, you must reload the database.'
897
+ )
898
+ end
899
+ it 'should fail if db does not exist' do
900
+ expect { subject.send('db:rename', 'foo2', 'foo3') }
901
+ .to raise_error(/Could not find database foo2/)
902
+ end
903
+ it 'should raise error if update fails' do
904
+ response = Faraday::Response.new(status: 500)
905
+ error = HyperResource::ClientError.new(
906
+ 'An error occurred: Validation failed: Handle has '\
907
+ 'already been taken, Handle has already been taken',
908
+ response: response
909
+ )
910
+ expect(database).to receive(:update!)
911
+ .with(handle: 'foo2').and_raise(error)
912
+ expect { subject.send('db:rename', database.handle, 'foo2') }
913
+ .to raise_error(HyperResource::ClientError)
914
+ end
915
+ end
916
+ end
879
917
  end
@@ -10,47 +10,82 @@ describe Aptible::CLI::Agent do
10
10
 
11
11
  let(:token) { double 'token' }
12
12
 
13
- before do
13
+ before(:each) do
14
14
  allow(subject).to receive(:fetch_token) { token }
15
15
  allow(Aptible::Api::Account).to receive(:all).with(token: token)
16
16
  .and_return([a1, a2])
17
17
  end
18
18
 
19
- it 'lists avaliable environments' do
20
- subject.send('environment:list')
19
+ describe('#environment:list') do
20
+ it 'lists avaliable environments' do
21
+ subject.send('environment:list')
21
22
 
22
- expect(captured_output_text.split("\n")).to include('foo')
23
- expect(captured_output_text.split("\n")).to include('bar')
23
+ expect(captured_output_text.split("\n")).to include('foo')
24
+ expect(captured_output_text.split("\n")).to include('bar')
25
+ end
24
26
  end
25
27
 
26
- it 'fetches certs for all avaliable environments' do
27
- subject.send('environment:ca_cert')
28
-
29
- expect(captured_output_text.split("\n")).to include('account 1 cert')
30
- expect(captured_output_text.split("\n")).to include('--account 2 cert--')
31
-
32
- expected_accounts = [
33
- {
34
- 'handle' => 'foo',
35
- 'ca_body' => 'account 1 cert',
36
- 'created_at' => fmt_time(a1.created_at)
37
- },
38
- {
39
- 'handle' => 'bar',
40
- 'ca_body' => '--account 2 cert--',
41
- 'created_at' => fmt_time(a2.created_at)
42
- }
43
- ]
44
- expect(captured_output_json.map! { |account| account.except('id') })
45
- .to eq(expected_accounts)
46
- end
28
+ describe('#environment:ca_cert') do
29
+ it 'fetches certs for all avaliable environments' do
30
+ subject.send('environment:ca_cert')
31
+
32
+ expect(captured_output_text.split("\n")).to include('account 1 cert')
33
+ expect(captured_output_text.split("\n")).to include('--account 2 cert--')
34
+
35
+ expected_accounts = [
36
+ {
37
+ 'handle' => 'foo',
38
+ 'ca_body' => 'account 1 cert',
39
+ 'created_at' => fmt_time(a1.created_at)
40
+ },
41
+ {
42
+ 'handle' => 'bar',
43
+ 'ca_body' => '--account 2 cert--',
44
+ 'created_at' => fmt_time(a2.created_at)
45
+ }
46
+ ]
47
+ expect(captured_output_json.map! { |account| account.except('id') })
48
+ .to eq(expected_accounts)
49
+ end
47
50
 
48
- it 'fetches certs for specified environment' do
49
- subject.options = { environment: 'foo' }
50
- subject.send('environment:ca_cert')
51
+ it 'fetches certs for specified environment' do
52
+ subject.options = { environment: 'foo' }
53
+ subject.send('environment:ca_cert')
54
+
55
+ expect(captured_output_text.split("\n")).to include('account 1 cert')
56
+ expect(captured_output_text.split("\n"))
57
+ .to_not include('--account 2 cert--')
58
+ end
59
+ end
51
60
 
52
- expect(captured_output_text.split("\n")).to include('account 1 cert')
53
- expect(captured_output_text.split("\n"))
54
- .to_not include('--account 2 cert--')
61
+ describe('#environment:rename') do
62
+ it 'should rename properly' do
63
+ expect(a1).to receive(:update!)
64
+ .with(handle: 'foo-renamed').and_return(a1)
65
+ subject.send('environment:rename', 'foo', 'foo-renamed')
66
+ expect(captured_logs).to include(
67
+ 'In order for the new environment handle (foo-renamed)'
68
+ )
69
+ expect(captured_logs).to include(
70
+ '* Your own external scripts (e.g. for CI/CD)'
71
+ )
72
+ expect(captured_logs).to include(
73
+ '* Git remote URLs (ex: git@beta.aptible.com:foo-renamed'
74
+ )
75
+ end
76
+ it 'should fail if env does not exist' do
77
+ expect { subject.send('environment:rename', 'foo1', 'foo2') }
78
+ .to raise_error(/Could not find environment foo1/)
79
+ end
80
+ it 'should raise error if update fails' do
81
+ response = Faraday::Response.new(status: 422)
82
+ error = HyperResource::ClientError.new('ActiveRecord::RecordInvalid:'\
83
+ ' Validation failed: Handle has already been taken, Handle has already'\
84
+ ' been taken', response: response)
85
+ expect(a1).to receive(:update!)
86
+ .with(handle: 'bar').and_raise(error)
87
+ expect { subject.send('environment:rename', 'foo', 'bar') }
88
+ .to raise_error(HyperResource::ClientError)
89
+ end
55
90
  end
56
91
  end
@@ -145,7 +145,7 @@ describe Aptible::CLI::Agent do
145
145
  drain_type: :datadog,
146
146
  drain_configuration: {
147
147
  api_key: 'foobar',
148
- series_url: 'https://app.datadoghq.eu'
148
+ series_url: 'https://app.datadoghq.eu/api/v1/series'
149
149
  }
150
150
  }
151
151
  expect_provision_metric_drain(opts)
@@ -3,6 +3,8 @@ require 'spec_helper'
3
3
  describe Aptible::CLI::Agent do
4
4
  let(:token) { 'some-token' }
5
5
  let(:operation) { Fabricate(:operation) }
6
+ let(:net_http_double) { double('Net::HTTP') }
7
+ let(:net_http_get_double) { double('Net::HTTP::Get') }
6
8
 
7
9
  before do
8
10
  allow(subject).to receive(:fetch_token).and_return(token)
@@ -26,4 +28,174 @@ describe Aptible::CLI::Agent do
26
28
  subject.send('operation:cancel', 1)
27
29
  end
28
30
  end
31
+
32
+ describe '#operation:follow' do
33
+ it 'fails if the operation cannot be found' do
34
+ expect(Aptible::Api::Operation).to receive(:find).with(1, token: token)
35
+ .and_return(nil)
36
+
37
+ expect { subject.send('operation:follow', 1) }
38
+ .to raise_error('Operation #1 not found')
39
+ end
40
+
41
+ it 'connects to a running operation' do
42
+ op = Fabricate(:operation, status: 'running', type: 'restart')
43
+ expect(Aptible::Api::Operation).to receive(:find)
44
+ .with(op.id.to_s, token: token).and_return(op)
45
+
46
+ expect(subject).to receive(:attach_to_operation_logs).with(op)
47
+ subject.send('operation:follow', op.id.to_s)
48
+ end
49
+
50
+ it 'connects to a queued operation' do
51
+ op = Fabricate(:operation, status: 'queued', type: 'restart')
52
+ expect(Aptible::Api::Operation).to receive(:find)
53
+ .with(op.id.to_s, token: token).and_return(op)
54
+
55
+ expect(subject).to receive(:attach_to_operation_logs).with(op)
56
+ subject.send('operation:follow', op.id.to_s)
57
+ end
58
+
59
+ it 'does not connect to a failed operation' do
60
+ id = 34
61
+ status = 'failed'
62
+ op = Fabricate(:operation, id: id, status: status)
63
+ expect(Aptible::Api::Operation).to receive(:find)
64
+ .with(op.id.to_s, token: token).and_return(op)
65
+
66
+ expect { subject.send('operation:follow', op.id.to_s) }
67
+ .to raise_error(Thor::Error, /aptible operation:logs #{id}/)
68
+ end
69
+
70
+ it 'does not connect to a succeeded operation' do
71
+ id = 43
72
+ status = 'succeeded'
73
+ op = Fabricate(:operation, id: id, status: status)
74
+ expect(Aptible::Api::Operation).to receive(:find)
75
+ .with(op.id.to_s, token: token).and_return(op)
76
+
77
+ expect { subject.send('operation:follow', op.id.to_s) }
78
+ .to raise_error(Thor::Error, /aptible operation:logs #{id}/)
79
+ end
80
+ end
81
+
82
+ describe '#operation:logs' do
83
+ it 'sends operation logs request when subcommand sent' do
84
+ operation_id = SecureRandom.uuid
85
+ expect(Aptible::Api::Operation).to receive(:find).with(1, token: token)
86
+ .and_return(Fabricate(
87
+ :operation, status: 'succeeded', id: operation_id
88
+ ))
89
+
90
+ # stub out operations call
91
+ response = instance_double(Net::HTTPResponse, body: 'https://s3.aptible.com/not-real/s3')
92
+
93
+ # stub out s3 call
94
+ s3_response = instance_double(Net::HTTPResponse, body: 'Mock logs')
95
+
96
+ allow(Net::HTTP).to receive(:new).twice do |_, _, _|
97
+ net_http_double
98
+ end
99
+ expect(response).to receive(:code).and_return('200')
100
+ expect(s3_response).to receive(:code).and_return('200')
101
+ expect(net_http_double).to receive(:use_ssl=).twice
102
+ expect(net_http_double).to receive(:request).twice do |request|
103
+ if request.path == "/operations/#{operation_id}/logs"
104
+ response
105
+ elsif request.path == '/not-real/s3'
106
+ s3_response
107
+ end
108
+ end
109
+
110
+ subject.send('operation:logs', 1)
111
+ end
112
+
113
+ it 'errors when operation is not found' do
114
+ expect(Aptible::Api::Operation).to receive(:find).with(1, token: token)
115
+ .and_return(nil)
116
+
117
+ expect { subject.send('operation:logs', 1) }
118
+ .to raise_error('Operation #1 not found')
119
+ end
120
+ it 'errors when operation is not status expected' do
121
+ operation_id = SecureRandom.uuid
122
+ expect(Aptible::Api::Operation).to receive(:find).with(1, token: token)
123
+ .and_return(Fabricate(:operation, status: 'queued', id: operation_id))
124
+
125
+ expect { subject.send('operation:logs', 1) }
126
+ .to raise_error('Error - You can view the logs when operation '\
127
+ 'is complete.')
128
+ end
129
+ it 'errors when operation logs are not found' do
130
+ operation_id = SecureRandom.uuid
131
+ expect(Aptible::Api::Operation).to receive(:find).with(1, token: token)
132
+ .and_return(
133
+ Fabricate(:operation, status: 'succeeded', id: operation_id)
134
+ )
135
+
136
+ # stub out operations call
137
+ response = Net::HTTPSuccess.new(1.0, '404', 'Not Found')
138
+ expect_any_instance_of(Net::HTTP)
139
+ .to receive(:request)
140
+ .with(an_instance_of(Net::HTTP::Get))
141
+ .and_return(response)
142
+
143
+ expect { subject.send('operation:logs', 1) }
144
+ .to raise_error('Unable to retrieve the operation\'s logs. '\
145
+ 'If the issue persists please contact support for assistance.')
146
+ end
147
+ it 'errors when body is empty' do
148
+ operation_id = SecureRandom.uuid
149
+ expect(Aptible::Api::Operation).to receive(:find).with(1, token: token)
150
+ .and_return(Fabricate(
151
+ :operation, status: 'succeeded', id: operation_id
152
+ ))
153
+
154
+ # stub out operations call
155
+ response = instance_double(Net::HTTPResponse, body: nil)
156
+
157
+ allow(Net::HTTP).to receive(:new) do |_, _, _|
158
+ net_http_double
159
+ end
160
+ expect(response).to receive(:code).and_return('200')
161
+ expect(net_http_double).to receive(:use_ssl=)
162
+ expect(net_http_double).to receive(:request).and_return(response)
163
+
164
+ expect { subject.send('operation:logs', 1) }
165
+ .to raise_error('Unable to retrieve the operation\'s logs. '\
166
+ 'If the issue persists please contact support for assistance.')
167
+ end
168
+ it 'errors when s3 itself returns an error code' do
169
+ operation_id = SecureRandom.uuid
170
+ expect(Aptible::Api::Operation).to receive(:find).with(1, token: token)
171
+ .and_return(Fabricate(
172
+ :operation, status: 'succeeded', id: operation_id
173
+ ))
174
+
175
+ # stub out operations call
176
+ response = instance_double(Net::HTTPResponse, body: 'https://s3.aptible.com/not-real/s3')
177
+
178
+ # stub out s3 call (to fail)
179
+ expect(response).to receive(:code).and_return('200')
180
+ s3_response = Net::HTTPSuccess.new(1.0, '404', 'Not Found')
181
+
182
+ allow(Net::HTTP).to receive(:new).twice do |_, _, _|
183
+ net_http_double
184
+ end
185
+ expect(net_http_double).to receive(:use_ssl=).twice
186
+ expect(net_http_double).to receive(:request).twice do |request|
187
+ if request.path == "/operations/#{operation_id}/logs"
188
+ response
189
+ elsif request.path == '/not-real/s3'
190
+ s3_response
191
+ end
192
+ end
193
+
194
+ expect { subject.send('operation:logs', 1) }
195
+ .to raise_error('Unable to retrieve operation logs, '\
196
+ 'S3 returned response code 404. '\
197
+ 'If the issue persists please contact support for '\
198
+ 'assistance.')
199
+ end
200
+ end
29
201
  end
@@ -1,7 +1,12 @@
1
1
  class StubOperation < OpenStruct; end
2
2
 
3
+ def mock_logs_url(id)
4
+ "https://api.aptible.com/operations/#{id}/logs"
5
+ end
6
+
3
7
  Fabricator(:operation, from: :stub_operation) do
4
8
  status 'queued'
5
- resource { Fabricate(:app) }
6
9
  errors { Aptible::Resource::Errors.new }
10
+ resource { Fabricate(:app) }
11
+ after_save { |operation| operation.logs_url = mock_logs_url(operation.id) }
7
12
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aptible-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.19.3
4
+ version: 0.19.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Frank Macreery
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-03-01 00:00:00.000000000 Z
11
+ date: 2022-09-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aptible-resource
@@ -326,6 +326,7 @@ files:
326
326
  - script/sync-readme-usage
327
327
  - spec/aptible/cli/agent_spec.rb
328
328
  - spec/aptible/cli/formatter_spec.rb
329
+ - spec/aptible/cli/helpers/database_spec.rb
329
330
  - spec/aptible/cli/helpers/git_remote_handle_strategy_spec.rb
330
331
  - spec/aptible/cli/helpers/handle_from_git_remote_spec.rb
331
332
  - spec/aptible/cli/helpers/operation_spec.rb
@@ -382,7 +383,7 @@ homepage: https://github.com/aptible/aptible-cli
382
383
  licenses:
383
384
  - MIT
384
385
  metadata: {}
385
- post_install_message:
386
+ post_install_message:
386
387
  rdoc_options: []
387
388
  require_paths:
388
389
  - lib
@@ -398,12 +399,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
398
399
  version: '0'
399
400
  requirements: []
400
401
  rubygems_version: 3.0.3.1
401
- signing_key:
402
+ signing_key:
402
403
  specification_version: 4
403
404
  summary: Command-line interface for Aptible services
404
405
  test_files:
405
406
  - spec/aptible/cli/agent_spec.rb
406
407
  - spec/aptible/cli/formatter_spec.rb
408
+ - spec/aptible/cli/helpers/database_spec.rb
407
409
  - spec/aptible/cli/helpers/git_remote_handle_strategy_spec.rb
408
410
  - spec/aptible/cli/helpers/handle_from_git_remote_spec.rb
409
411
  - spec/aptible/cli/helpers/operation_spec.rb