aptible-cli 0.19.0 → 0.19.3

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
  SHA256:
3
- metadata.gz: ee325f4c1746513670e64830cd10d8856fc39cc96e836c8f822e4c34ed6c45ea
4
- data.tar.gz: 0dab373981390ea9d3429a6f0f43634eef13285bd090542329866bb1ff24620c
3
+ metadata.gz: f20b71eb3388732ade1e6e53cb1f076761d40a06d460faf08fa3e468f239acc1
4
+ data.tar.gz: 3e603d02a1dbe3e8168209f9a0f061d29851a48189d28dc549bff3a7d203a5e1
5
5
  SHA512:
6
- metadata.gz: 27fb8c12ceb02f977f29777bd418a2aa943c754d6b8cdc6a37ee55995bbe4a45dea9683c7191b4529017484b37dd3ef6dfcd3f2d70d2010ea032a0d1199409fd
7
- data.tar.gz: b30888eb2d2855b388d04013eca773f4e69f77734c92aeee442499e7a266708c5a85800e0552ad0e739a40d1beb41956babcc4a2b9a0483d12be86939ac95c32
6
+ metadata.gz: e195096e2854788c488477ca6d42fd1beb15ce2b6044f0be32a774e6b3915c387694e4608c7546a94915dcb32b3f0844f4864544e4a64806dca23379c0383598
7
+ data.tar.gz: b2fb1ef99eb836cac0e80419d362277b05877826e8cf4b8f8cb78f937168d4ab658b81f9fea6c7ea0559cd5fe6f926aec896869b7fa8737e4503536d39dfb161
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
@@ -57,6 +57,7 @@ Commands:
57
57
  aptible db:versions # List available database versions
58
58
  aptible deploy [OPTIONS] [VAR1=VAL1] [VAR2=VAL2] [...] # Deploy an app
59
59
  aptible endpoints:database:create DATABASE # Create a Database Endpoint
60
+ aptible endpoints:database:modify --database DATABASE ENDPOINT_HOSTNAME # Modify a Database Endpoint
60
61
  aptible endpoints:deprovision [--app APP | --database DATABASE] ENDPOINT_HOSTNAME # Deprovision an App or Database Endpoint
61
62
  aptible endpoints:https:create [--app APP] SERVICE # Create an App HTTPS Endpoint
62
63
  aptible endpoints:https:modify [--app APP] ENDPOINT_HOSTNAME # Modify an App HTTPS Endpoint
@@ -80,9 +81,9 @@ Commands:
80
81
  aptible log_drain:list # List all Log Drains
81
82
  aptible login # Log in to Aptible
82
83
  aptible logs [--app APP | --database DATABASE] # Follows logs from a running app or database
83
- aptible metric_drain:create:datadog HANDLE --api_key DATADOG_API_KEY --environment ENVIRONMENT # Create a Datadog Metric Drain
84
+ aptible metric_drain:create:datadog HANDLE --api_key DATADOG_API_KEY --site DATADOG_SITE --environment ENVIRONMENT # Create a Datadog Metric Drain
84
85
  aptible metric_drain:create:influxdb HANDLE --db DATABASE_HANDLE --environment ENVIRONMENT # Create an InfluxDB Metric Drain
85
- aptible metric_drain:create:influxdb:custom HANDLE --username USERNAME --password PASSWORD --url URL_INCLUDING_PORT # Create an InfluxDB Metric Drain
86
+ aptible metric_drain:create:influxdb:custom HANDLE --username USERNAME --password PASSWORD --url URL_INCLUDING_PORT --db INFLUX_DATABASE_NAME --environment ENVIRONMENT # Create an InfluxDB Metric Drain
86
87
  aptible metric_drain:deprovision HANDLE --environment ENVIRONMENT # Deprovisions a Metric Drain
87
88
  aptible metric_drain:list # List all Metric Drains
88
89
  aptible operation:cancel OPERATION_ID # Cancel a running operation
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}",
@@ -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) }
@@ -22,7 +22,9 @@ module Aptible
22
22
  thor.instance_exec(self) do |builder|
23
23
  option :environment
24
24
 
25
- if builder.app?
25
+ if builder.database?
26
+ option :database
27
+ elsif builder.app?
26
28
  app_options
27
29
 
28
30
  if builder.create?
@@ -32,11 +34,6 @@ module Aptible
32
34
  desc: 'Enable Default Domain on this Endpoint'
33
35
  )
34
36
 
35
- option(
36
- :internal,
37
- type: :boolean,
38
- desc: 'Restrict this Endpoint to internal traffic'
39
- )
40
37
  end
41
38
 
42
39
  if builder.ports?
@@ -56,6 +53,14 @@ module Aptible
56
53
  end
57
54
  end
58
55
 
56
+ if builder.create?
57
+ option(
58
+ :internal,
59
+ type: :boolean,
60
+ desc: 'Restrict this Endpoint to internal traffic'
61
+ )
62
+ end
63
+
59
64
  option(
60
65
  :ip_whitelist,
61
66
  type: :array,
@@ -151,6 +156,12 @@ module Aptible
151
156
  end
152
157
 
153
158
  options.delete(:app)
159
+ elsif database?
160
+ params[:internal] = options.delete(:internal) do
161
+ create? ? false : nil
162
+ end
163
+
164
+ options.delete(:database)
154
165
  else
155
166
  params[:internal] = false
156
167
  end
@@ -33,6 +33,22 @@ module Aptible
33
33
  provision_vhost_and_explain(service, vhost)
34
34
  end
35
35
 
36
+ database_modify_flags = Helpers::Vhost::OptionSetBuilder.new do
37
+ database!
38
+ end
39
+
40
+ desc 'endpoints:database:modify --database DATABASE ' \
41
+ 'ENDPOINT_HOSTNAME',
42
+ 'Modify a Database Endpoint'
43
+ database_modify_flags.declare_options(self)
44
+ define_method 'endpoints:database:modify' do |hostname|
45
+ database = ensure_database(options.merge(db: options[:database]))
46
+ vhost = find_vhost(each_service(database), hostname)
47
+ vhost.update!(**database_modify_flags.prepare(database.account,
48
+ options))
49
+ provision_vhost_and_explain(vhost.service, vhost)
50
+ end
51
+
36
52
  tcp_create_flags = Helpers::Vhost::OptionSetBuilder.new do
37
53
  app!
38
54
  create!
@@ -55,7 +55,9 @@ module Aptible
55
55
 
56
56
  desc 'metric_drain:create:influxdb:custom HANDLE '\
57
57
  '--username USERNAME --password PASSWORD ' \
58
- '--url URL_INCLUDING_PORT',
58
+ '--url URL_INCLUDING_PORT ' \
59
+ '--db INFLUX_DATABASE_NAME ' \
60
+ '--environment ENVIRONMENT',
59
61
  'Create an InfluxDB Metric Drain'
60
62
  option :db, type: :string
61
63
  option :username, type: :string
@@ -82,7 +84,9 @@ module Aptible
82
84
  end
83
85
 
84
86
  desc 'metric_drain:create:datadog HANDLE '\
85
- '--api_key DATADOG_API_KEY --environment ENVIRONMENT',
87
+ '--api_key DATADOG_API_KEY '\
88
+ '--site DATADOG_SITE ' \
89
+ '--environment ENVIRONMENT',
86
90
  'Create a Datadog Metric Drain'
87
91
  option :api_key, type: :string
88
92
  option :site, type: :string
@@ -1,5 +1,5 @@
1
1
  module Aptible
2
2
  module CLI
3
- VERSION = '0.19.0'.freeze
3
+ VERSION = '0.19.3'.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)
@@ -94,6 +94,45 @@ describe Aptible::CLI::Agent do
94
94
  stub_options(ip_whitelist: %w(1.1.1.1))
95
95
  subject.send('endpoints:database:create', 'mydb')
96
96
  end
97
+
98
+ it 'creates an internal Database Endpoint' do
99
+ expect_create_vhost(db.service, internal: true)
100
+ stub_options(internal: true)
101
+ subject.send('endpoints:database:create', 'mydb')
102
+ end
103
+ end
104
+
105
+ describe 'endpoints:database:modify' do
106
+ it 'does not change anything if no options are passed' do
107
+ v = Fabricate(:vhost, service: db.service)
108
+ expect_modify_vhost(v, {})
109
+ stub_options(database: 'mydb')
110
+ subject.send('endpoints:database:modify', v.external_host)
111
+ end
112
+
113
+ it 'adds an IP whitelist' do
114
+ v = Fabricate(:vhost, service: db.service)
115
+ expect_modify_vhost(v, ip_whitelist: %w(1.1.1.1))
116
+
117
+ stub_options(database: 'mydb', ip_whitelist: %w(1.1.1.1))
118
+ subject.send('endpoints:database:modify', v.external_host)
119
+ end
120
+
121
+ it 'removes an IP whitelist' do
122
+ v = Fabricate(:vhost, service: db.service)
123
+ expect_modify_vhost(v, ip_whitelist: [])
124
+
125
+ stub_options(database: 'mydb', :'no-ip_whitelist' => true)
126
+ subject.send('endpoints:database:modify', v.external_host)
127
+ end
128
+
129
+ it 'does not allow disabling and adding an IP whitelist' do
130
+ v = Fabricate(:vhost, service: db.service)
131
+ stub_options(database: 'mydb', ip_whitelist: %w(1.1.1.1),
132
+ :'no-ip_whitelist' => true)
133
+ expect { subject.send('endpoints:database:modify', v.external_host) }
134
+ .to raise_error(/conflicting.*no-ip-whitelist.*ip-whitelist/im)
135
+ end
97
136
  end
98
137
 
99
138
  describe 'endpoints:list' do
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.0
4
+ version: 0.19.3
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: 2021-07-27 00:00:00.000000000 Z
11
+ date: 2022-03-01 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
@@ -368,7 +382,7 @@ homepage: https://github.com/aptible/aptible-cli
368
382
  licenses:
369
383
  - MIT
370
384
  metadata: {}
371
- post_install_message:
385
+ post_install_message:
372
386
  rdoc_options: []
373
387
  require_paths:
374
388
  - lib
@@ -383,8 +397,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
383
397
  - !ruby/object:Gem::Version
384
398
  version: '0'
385
399
  requirements: []
386
- rubygems_version: 3.0.3
387
- signing_key:
400
+ rubygems_version: 3.0.3.1
401
+ signing_key:
388
402
  specification_version: 4
389
403
  summary: Command-line interface for Aptible services
390
404
  test_files: