aptible-cli 0.19.1 → 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: 8984c4633a4c6cfa79a8cba5fa5856079e4388677a4a6726f55eab3506ba82b5
4
- data.tar.gz: d593ebd83e743458ea09ea1ffea94bf76679d6a10d12091d0f044701f85e4f5b
3
+ metadata.gz: f3f0904b2033596eb17ff98125288e5a2adec95ae6e379b95a5b8131b81bfe54
4
+ data.tar.gz: 9c002c17e22dd77ff79360671d0057455a70ff4ee2311d1466651a5c39b3c1a2
5
5
  SHA512:
6
- metadata.gz: cc247fe45bb6ec934de142e4ca51e19c369a792ac92defc717b91436b293bb80c3fbbedeebc03efe4dd3cda4e3e983f8c53e95d8da5b30de53dc684bdd2d1a5e
7
- data.tar.gz: ac409bc0d1e171f48f1a72035f31e8a69edc9b69e8aa586f6fcc70d52628ff698bc499f21e7b90fd38749b9779473ad6a9701606a843848217b1b3b732551f2c
6
+ metadata.gz: 0c1b0857f175135e6d7feb64a9a6a7b9633b73823f1f32100473f2f90c5b529a6dcf041b55bf4af47a92af08c425b24f4ce1abfacce3deea74b1e031e0bf5fa0
7
+ data.tar.gz: ccb65684ab24eb9055adee5e17a04d0a9dbf2cdc496ba1b71a9a6a229e83ee092ad43c2ee660f73b5da95b47b875bdf2d0212d6ede3d932455ff67188d4b47c1
data/Gemfile CHANGED
@@ -1,6 +1,9 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem 'pry', github: 'fancyremarker/pry', branch: 'aptible'
3
+ gem 'pry',
4
+ git: 'https://github.com/fancyremarker/pry.git',
5
+ branch: 'aptible'
6
+
4
7
  gem 'activesupport', '~> 4.0'
5
8
  gem 'rack', '~> 1.0'
6
9
 
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
data/aptible-cli.gemspec CHANGED
@@ -22,12 +22,13 @@ Gem::Specification.new do |spec|
22
22
 
23
23
  spec.add_dependency 'aptible-resource', '~> 1.1'
24
24
  spec.add_dependency 'aptible-api', '~> 1.2'
25
- spec.add_dependency 'aptible-auth', '~> 1.2.3'
25
+ spec.add_dependency 'aptible-auth', '~> 1.2.4'
26
26
  spec.add_dependency 'aptible-billing', '~> 1.0'
27
27
  spec.add_dependency 'thor', '~> 0.20.0'
28
- spec.add_dependency 'git'
28
+ spec.add_dependency 'git', '< 1.10'
29
29
  spec.add_dependency 'term-ansicolor'
30
30
  spec.add_dependency 'chronic_duration', '~> 0.10.6'
31
+ spec.add_dependency 'cbor'
31
32
 
32
33
  # Temporarily pin ffi until https://github.com/ffi/ffi/issues/868 is fixed
33
34
  spec.add_dependency 'ffi', '<= 1.14.1' if Gem.win_platform?
@@ -145,29 +145,32 @@ module Aptible
145
145
 
146
146
  # If the user has added a security key and their computer supports it,
147
147
  # allow them to use it
148
- if u2f && !which('u2f-host').nil?
148
+ # https://developers.yubico.com/libfido2/Manuals
149
+ # installation: https://github.com/Yubico/libfido2#installation
150
+ if u2f && !which('fido2-assert').nil? && !which('fido2-token').nil?
149
151
  origin = Aptible::Auth::Resource.new.get.href
150
152
  app_id = Aptible::Auth::Resource.new.utf_trusted_facets.href
151
-
152
153
  challenge = u2f.fetch('challenge')
153
154
 
154
- devices = u2f.fetch('devices').map do |dev|
155
- Helpers::SecurityKey::Device.new(
156
- dev.fetch('version'), dev.fetch('key_handle')
157
- )
158
- end
155
+ device_info = security_key_device(u2f, app_id)
159
156
 
160
- puts 'Enter your 2FA token or touch your Security Key once it ' \
161
- 'starts blinking.'
157
+ if device_info[:locations].count > 0 && device_info[:device]
158
+ puts "\nEnter your 2FA token or touch your Security Key " \
159
+ 'once it starts blinking.'
162
160
 
163
- mfa_threads << Thread.new do
164
- token_options[:u2f] = Helpers::SecurityKey.authenticate(
165
- origin, app_id, challenge, devices
166
- )
161
+ mfa_threads << Thread.new do
162
+ token_options[:u2f] = Helpers::SecurityKey.authenticate(
163
+ origin,
164
+ app_id,
165
+ challenge,
166
+ device_info[:device],
167
+ device_info[:locations]
168
+ )
167
169
 
168
- puts ''
170
+ puts ''
169
171
 
170
- q.push(nil)
172
+ q.push(nil)
173
+ end
171
174
  end
172
175
  end
173
176
 
@@ -202,6 +205,78 @@ module Aptible
202
205
 
203
206
  private
204
207
 
208
+ def security_key_device(u2f, app_id)
209
+ devices = u2f.fetch('devices').map do |dev|
210
+ version = dev.fetch('version')
211
+ rp_id =
212
+ if version == 'U2F_V2'
213
+ app_id
214
+ else
215
+ u2f['payload']['rpId']
216
+ end
217
+
218
+ Helpers::SecurityKey::Device.new(
219
+ dev.fetch('version'),
220
+ dev.fetch('key_handle'),
221
+ dev.fetch('name'),
222
+ rp_id
223
+ )
224
+ end
225
+
226
+ result = {
227
+ locations: [],
228
+ device: nil
229
+ }
230
+
231
+ device_locations = Helpers::SecurityKey.device_locations
232
+
233
+ if device_locations.count.zero?
234
+ no_keys = 'WARNING: no security keys detected on machine'
235
+ CLI.logger.warn(no_keys) if device_locations.count.zero?
236
+ else
237
+ result[:locations] = device_locations
238
+ no_creds = 'No credentials associated with user'
239
+ raise Error, no_creds if devices.count.zero?
240
+
241
+ result[:device] = devices[0]
242
+ if devices.count > 1
243
+ credential = security_credential(devices)
244
+ result[:device] = credential
245
+ end
246
+ end
247
+
248
+ result
249
+ end
250
+
251
+ # The name for our backend model is U2FDevice.
252
+ # However, really what we are storing is a security credential.
253
+ # Here we figure out which security credential to pass to fido2-assert.
254
+ def security_credential(devices)
255
+ puts 'There are multiple credentials associated ' \
256
+ 'with this user. Please select the ' \
257
+ "credential you want to use for authentication:\n"
258
+
259
+ device = nil
260
+ while device.nil?
261
+ devices.each_with_index do |dev, index|
262
+ puts "#{index}: #{dev.name}"
263
+ end
264
+
265
+ puts ''
266
+
267
+ device_index = ask(
268
+ 'Enter the credential number you want to use: '
269
+ )
270
+
271
+ # https://stackoverflow.com/a/1235990
272
+ next unless /\A\d+\z/ =~ device_index
273
+
274
+ device = devices[device_index.to_i]
275
+ end
276
+
277
+ device
278
+ end
279
+
205
280
  def deprecated(msg)
206
281
  CLI.logger.warn([
207
282
  "DEPRECATION NOTICE: #{msg}",
@@ -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
@@ -1,3 +1,6 @@
1
+ require 'openssl'
2
+ require 'cbor'
3
+
1
4
  module Aptible
2
5
  module CLI
3
6
  module Helpers
@@ -8,21 +11,32 @@ module Aptible
8
11
 
9
12
  class AuthenticatorParameters
10
13
  attr_reader :origin, :challenge, :app_id, :version, :key_handle
11
- attr_reader :request
14
+ attr_reader :request, :rp_id, :device_location
15
+ attr_reader :client_data, :assert_str, :version
12
16
 
13
- def initialize(origin, challenge, app_id, device)
17
+ def initialize(origin, challenge, app_id, device, device_location)
14
18
  @origin = origin
15
19
  @challenge = challenge
16
20
  @app_id = app_id
17
21
  @version = device.version
18
22
  @key_handle = device.key_handle
19
-
20
- @request = {
21
- 'challenge' => challenge,
22
- 'appId' => app_id,
23
- 'version' => version,
24
- 'keyHandle' => key_handle
25
- }
23
+ @rp_id = device.rp_id
24
+ @version = device.version
25
+ @device_location = device_location
26
+ @client_data = {
27
+ type: 'webauthn.get',
28
+ challenge: challenge,
29
+ origin: origin,
30
+ crossOrigin: false
31
+ }.to_json
32
+ key_handle = Base64.strict_encode64(
33
+ Base64.urlsafe_decode64(device.key_handle)
34
+ )
35
+ client_data_hash = Digest::SHA256.base64digest(@client_data)
36
+ in_str = "#{client_data_hash}\n" \
37
+ "#{device.rp_id}\n" \
38
+ "#{key_handle}"
39
+ @assert_str = in_str
26
40
  end
27
41
  end
28
42
 
@@ -51,9 +65,50 @@ module Aptible
51
65
  end
52
66
  end
53
67
 
54
- class Authenticator
68
+ class DeviceMapper
55
69
  attr_reader :pid
56
70
 
71
+ def initialize(pid, out_read, err_read)
72
+ @pid = pid
73
+ @out_read = out_read
74
+ @err_read = err_read
75
+ end
76
+
77
+ def exited(status)
78
+ out, err = [@out_read, @err_read].map(&:read).map(&:chomp)
79
+
80
+ if status.exitstatus == 0
81
+ U2F_LOGGER.info("#{self.class}: ok: #{out}")
82
+ [nil, out]
83
+ else
84
+ U2F_LOGGER.warn("#{self.class}: err: #{err}")
85
+ [nil, nil]
86
+ end
87
+ ensure
88
+ [@out_read, @err_read].each(&:close)
89
+ end
90
+
91
+ def self.spawn
92
+ out_read, out_write = IO.pipe
93
+ err_read, err_write = IO.pipe
94
+
95
+ pid = Process.spawn(
96
+ 'fido2-token -L',
97
+ out: out_write, err: err_write,
98
+ close_others: true
99
+ )
100
+
101
+ U2F_LOGGER.debug("#{self}: spawned #{pid}")
102
+
103
+ [out_write, err_write].each(&:close)
104
+
105
+ new(pid, out_read, err_read)
106
+ end
107
+ end
108
+
109
+ class Authenticator
110
+ attr_reader :pid, :auth
111
+
57
112
  def initialize(auth, pid, out_read, err_read)
58
113
  @auth = auth
59
114
  @pid = pid
@@ -61,13 +116,55 @@ module Aptible
61
116
  @err_read = err_read
62
117
  end
63
118
 
119
+ def formatted_out(out)
120
+ arr = out.split("\n")
121
+ authenticator_data = arr[2]
122
+ signature = arr[3]
123
+ appid = auth.app_id if auth.version == 'U2F_V2'
124
+ client_data_json = Base64.urlsafe_encode64(auth.client_data)
125
+
126
+ {
127
+ id: auth.key_handle,
128
+ rawId: auth.key_handle,
129
+ clientExtensionResults: { appid: appid },
130
+ type: 'public-key',
131
+ response: {
132
+ clientDataJSON: client_data_json,
133
+ authenticatorData: Base64.urlsafe_encode64(
134
+ CBOR.decode(
135
+ Base64.strict_decode64(authenticator_data)
136
+ )
137
+ ),
138
+ signature: signature
139
+ }
140
+ }
141
+ end
142
+
143
+ def fido_err_msg(err)
144
+ match = err.match(/(FIDO_ERR.+)/)
145
+ return nil unless match
146
+ result = match.captures || []
147
+ no_cred = "\nCredential not found on device, " \
148
+ 'are you sure you selected the right ' \
149
+ 'credential for this device?'
150
+ err_map = {
151
+ 'FIDO_ERR_NO_CREDENTIALS' => no_cred
152
+ }
153
+
154
+ return err_map[result[0]] if result.count > 0
155
+
156
+ nil
157
+ end
158
+
64
159
  def exited(status)
65
160
  out, err = [@out_read, @err_read].map(&:read).map(&:chomp)
66
161
 
67
162
  if status.exitstatus == 0
68
163
  U2F_LOGGER.info("#{self.class} #{@auth.key_handle}: ok: #{out}")
69
- [nil, JSON.parse(out)]
164
+ [nil, out]
70
165
  else
166
+ err_msg = fido_err_msg(err)
167
+ CLI.logger.error(err_msg) if err_msg
71
168
  U2F_LOGGER.warn("#{self.class} #{@auth.key_handle}: err: #{err}")
72
169
  [ThrottledAuthenticator.spawn(@auth), nil]
73
170
  end
@@ -81,7 +178,7 @@ module Aptible
81
178
  err_read, err_write = IO.pipe
82
179
 
83
180
  pid = Process.spawn(
84
- 'u2f-host', '-aauthenticate', '-o', auth.origin,
181
+ "fido2-assert -G #{auth.device_location}",
85
182
  in: in_read, out: out_write, err: err_write,
86
183
  close_others: true
87
184
  )
@@ -90,7 +187,7 @@ module Aptible
90
187
 
91
188
  [in_read, out_write, err_write].each(&:close)
92
189
 
93
- in_write.write(auth.request.to_json)
190
+ in_write.write(auth.assert_str)
94
191
  in_write.close
95
192
 
96
193
  new(auth, pid, out_read, err_read)
@@ -98,18 +195,40 @@ module Aptible
98
195
  end
99
196
 
100
197
  class Device
101
- attr_reader :version, :key_handle
198
+ attr_reader :version, :key_handle, :rp_id, :name
102
199
 
103
- def initialize(version, key_handle)
200
+ def initialize(version, key_handle, name, rp_id)
104
201
  @version = version
105
202
  @key_handle = key_handle
203
+ @name = name
204
+ @rp_id = rp_id
106
205
  end
107
206
  end
108
207
 
109
- def self.authenticate(origin, app_id, challenge, devices)
110
- procs = Hash[devices.map do |device|
208
+ def self.device_locations
209
+ w = DeviceMapper.spawn
210
+ _, status = Process.wait2
211
+ _, out = w.exited(status)
212
+ # parse output and only log device
213
+ matches = out.split("\n").map { |s| s.match(/^(\S+):\s/) }
214
+ results = []
215
+ matches.each do |m|
216
+ capture = m.captures
217
+ results << capture[0] if m && capture.count.positive?
218
+ end
219
+
220
+ results
221
+ end
222
+
223
+ def self.authenticate(origin, app_id, challenge,
224
+ device, device_locations)
225
+ procs = Hash[device_locations.map do |location|
111
226
  params = AuthenticatorParameters.new(
112
- origin, challenge, app_id, device
227
+ origin,
228
+ challenge,
229
+ app_id,
230
+ device,
231
+ location
113
232
  )
114
233
  w = Authenticator.spawn(params)
115
234
  [w.pid, w]
@@ -124,7 +243,7 @@ module Aptible
124
243
  r, out = w.exited(status)
125
244
 
126
245
  procs[r.pid] = r if r
127
- return out if out
246
+ return w.formatted_out(out) if out
128
247
  end
129
248
  ensure
130
249
  procs.values.map(&:pid).each { |p| Process.kill(:SIGTERM, p) }
@@ -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.1'.freeze
3
+ VERSION = '0.19.4'.freeze
4
4
  end
5
5
  end
@@ -187,8 +187,16 @@ describe Aptible::CLI::Agent do
187
187
  'u2f' => {
188
188
  'challenge' => 'some 123',
189
189
  'devices' => [
190
- { 'version' => 'U2F_V2', 'key_handle' => '123' },
191
- { 'version' => 'U2F_V2', 'key_handle' => '456' }
190
+ {
191
+ 'version' => 'U2F_V2',
192
+ 'key_handle' => '123',
193
+ 'name' => 'primary'
194
+ },
195
+ {
196
+ 'version' => 'U2F_V2',
197
+ 'key_handle' => '456',
198
+ 'name' => 'secondary'
199
+ }
192
200
  ]
193
201
  }
194
202
  )
@@ -215,16 +223,28 @@ describe Aptible::CLI::Agent do
215
223
  end
216
224
 
217
225
  it 'should call into U2F if supported' do
218
- allow(subject).to receive(:which).and_return('u2f-host')
226
+ allow(subject).to receive(:which).and_return('fido2-token')
227
+ allow(subject).to receive(:which).and_return('fido2-assert')
219
228
  allow(subject).to receive(:ask).with('2FA Token: ') { sleep }
229
+ allow(subject).to receive(:ask).with(
230
+ 'Enter the credential number you want to use: '
231
+ ) { '0' }
220
232
 
221
233
  e = make_oauth2_error(
222
234
  'otp_token_required',
223
235
  'u2f' => {
224
236
  'challenge' => 'some 123',
225
237
  'devices' => [
226
- { 'version' => 'U2F_V2', 'key_handle' => '123' },
227
- { 'version' => 'U2F_V2', 'key_handle' => '456' }
238
+ {
239
+ 'version' => 'U2F_V2',
240
+ 'key_handle' => '123',
241
+ 'name' => 'primary'
242
+ },
243
+ {
244
+ 'version' => 'U2F_V2',
245
+ 'key_handle' => '456',
246
+ 'name' => 'secondary'
247
+ }
228
248
  ]
229
249
  }
230
250
  )
@@ -238,15 +258,17 @@ describe Aptible::CLI::Agent do
238
258
 
239
259
  expect(subject).to receive(:puts).with(/security key/i)
240
260
 
261
+ expect(Aptible::CLI::Helpers::SecurityKey)
262
+ .to receive(:device_locations)
263
+ .and_return(['ioreg://4295020796'])
264
+
241
265
  expect(Aptible::CLI::Helpers::SecurityKey).to receive(:authenticate)
242
266
  .with(
243
267
  'https://auth.aptible.com/',
244
268
  'https://auth.aptible.com/u2f/trusted_facets',
245
269
  'some 123',
246
- array_including(
247
- instance_of(Aptible::CLI::Helpers::SecurityKey::Device),
248
- instance_of(Aptible::CLI::Helpers::SecurityKey::Device)
249
- )
270
+ instance_of(Aptible::CLI::Helpers::SecurityKey::Device),
271
+ ['ioreg://4295020796']
250
272
  ).and_return(u2f)
251
273
 
252
274
  expect(Aptible::Auth::Token).to receive(:create)
@@ -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.1
4
+ version: 0.19.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Frank Macreery
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-08-02 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
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 1.2.3
47
+ version: 1.2.4
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 1.2.3
54
+ version: 1.2.4
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: aptible-billing
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -84,16 +84,16 @@ dependencies:
84
84
  name: git
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - ">="
87
+ - - "<"
88
88
  - !ruby/object:Gem::Version
89
- version: '0'
89
+ version: '1.10'
90
90
  type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - ">="
94
+ - - "<"
95
95
  - !ruby/object:Gem::Version
96
- version: '0'
96
+ version: '1.10'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: term-ansicolor
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -122,6 +122,20 @@ dependencies:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
124
  version: 0.10.6
125
+ - !ruby/object:Gem::Dependency
126
+ name: cbor
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
125
139
  - !ruby/object:Gem::Dependency
126
140
  name: activesupport
127
141
  requirement: !ruby/object:Gem::Requirement
@@ -312,6 +326,7 @@ files:
312
326
  - script/sync-readme-usage
313
327
  - spec/aptible/cli/agent_spec.rb
314
328
  - spec/aptible/cli/formatter_spec.rb
329
+ - spec/aptible/cli/helpers/database_spec.rb
315
330
  - spec/aptible/cli/helpers/git_remote_handle_strategy_spec.rb
316
331
  - spec/aptible/cli/helpers/handle_from_git_remote_spec.rb
317
332
  - spec/aptible/cli/helpers/operation_spec.rb
@@ -383,13 +398,14 @@ required_rubygems_version: !ruby/object:Gem::Requirement
383
398
  - !ruby/object:Gem::Version
384
399
  version: '0'
385
400
  requirements: []
386
- rubygems_version: 3.0.3
401
+ rubygems_version: 3.0.3.1
387
402
  signing_key:
388
403
  specification_version: 4
389
404
  summary: Command-line interface for Aptible services
390
405
  test_files:
391
406
  - spec/aptible/cli/agent_spec.rb
392
407
  - spec/aptible/cli/formatter_spec.rb
408
+ - spec/aptible/cli/helpers/database_spec.rb
393
409
  - spec/aptible/cli/helpers/git_remote_handle_strategy_spec.rb
394
410
  - spec/aptible/cli/helpers/handle_from_git_remote_spec.rb
395
411
  - spec/aptible/cli/helpers/operation_spec.rb