aptible-cli 0.26.5 → 0.26.7

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: 13c94bbbfe4b853700fec4e895ab0eebd02f0c5ffcddf6cfd0a597c42f665d37
4
- data.tar.gz: c495f4582ca15271799caa1b71a3922c3e5bbaafa692eda9476862465971556e
3
+ metadata.gz: d11060c760d01bcedf1163619f8412205f95dec58b2fe1f40f58378854e7aefb
4
+ data.tar.gz: a1b8645bb717f084310500ac86e8e1d92c14ac78cc5883019c7de29a737fc1be
5
5
  SHA512:
6
- metadata.gz: eb5b9e8adc78ba955f5125ca6c673894fea37fb8dd9a61775b4f44f87b9afc2c7fd9dbf8c645cd380af6fca7f754f03654a139539d9180060c6475544320b864
7
- data.tar.gz: d86d87bb3c7d8dd6c57692c63c0592b99f950cd96b8b6bce1038678678a0c983c6b734ef380852b6edb8bb1e4e205fa6938b5045c8d5337f82fd4c40fb5aac2e
6
+ metadata.gz: edc9d96e02224c1d9520bc382a5415c9425bdc108ff045d6f55d87aca2376985c99749a82d2496c37a18c9066da704bb2625edde5bb00ef870e95e2bd4e3a948
7
+ data.tar.gz: 91c07735699ab92a1ca5f0facb86835a0271e3a5486f8965b45d2dd4f9579268edb240ba3be6f0682c3f341db31fb3e207500ef87284704fedcde301786b839a
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- aptible-cli (0.26.5)
4
+ aptible-cli (0.26.7)
5
5
  activesupport (>= 4.0, < 6.0)
6
6
  aptible-api (~> 1.12)
7
7
  aptible-auth (~> 1.4)
data/README.md CHANGED
@@ -31,6 +31,7 @@ Commands:
31
31
  aptible apps:deprovision [--app APP] # Deprovision an app
32
32
  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.
33
33
  aptible apps:scale [--app APP] SERVICE [--container-count COUNT] [--container-size SIZE_MB] [--container-profile PROFILE] # Scale a service
34
+ aptible apps:settings HANDLE # Display deployment related settings for an app
34
35
  aptible backup:list DB_HANDLE # List backups for a database
35
36
  aptible backup:orphaned # List backups associated with deprovisioned databases
36
37
  aptible backup:purge BACKUP_ID # Permanently delete a backup and any copies of it
@@ -209,6 +209,17 @@ module Aptible
209
209
  )
210
210
  end
211
211
 
212
+ def current_setting(app)
213
+ setting_link = app.links['current_setting']
214
+ return unless setting_link
215
+
216
+ Aptible::Api::Setting.find_by_url(
217
+ setting_link.href,
218
+ token: fetch_token,
219
+ headers: { 'Prefer' => 'no_sensitive_extras=false' }
220
+ )
221
+ end
222
+
212
223
  private
213
224
 
214
225
  def handle_strategies
@@ -14,6 +14,35 @@ module Aptible
14
14
  alb
15
15
  ).freeze
16
16
 
17
+ SSL_PROTOCOL_VALUES = [
18
+ 'TLSv1 TLSv1.1 TLSv1.2',
19
+ 'TLSv1 TLSv1.1 TLSv1.2 PFS',
20
+ 'TLSv1.1 TLSv1.2',
21
+ 'TLSv1.1 TLSv1.2 PFS',
22
+ 'TLSv1.2',
23
+ 'TLSv1.2 PFS',
24
+ 'TLSv1.2 PFS TLSv1.3',
25
+ 'TLSv1.3'
26
+ ].freeze
27
+
28
+ ALB_PROTOCOL_VALUES = SSL_PROTOCOL_VALUES
29
+
30
+ ELB_PROTOCOL_VALUES =
31
+ SSL_PROTOCOL_VALUES.reject { |v| v.include?(' PFS') }.freeze
32
+
33
+ SSL_PROTOCOL_ALB_DESC = (
34
+ 'Specify the allowed SSL protocols. Valid options: ' +
35
+ ALB_PROTOCOL_VALUES.map { |v| "\"#{v}\"" }.join(', ') +
36
+ '. PFS options require an HTTPS (ALB) endpoint. ' \
37
+ 'Use "default" to reset to the platform default'
38
+ ).freeze
39
+
40
+ SSL_PROTOCOL_ELB_DESC = (
41
+ 'Specify the allowed SSL protocols. Valid options: ' +
42
+ ELB_PROTOCOL_VALUES.map { |v| "\"#{v}\"" }.join(', ') +
43
+ '. Use "default" to reset to the platform default'
44
+ ).freeze
45
+
17
46
  def initialize(&block)
18
47
  FLAGS.each { |f| instance_variable_set("@#{f}", false) }
19
48
  instance_exec(&block) if block
@@ -53,6 +82,14 @@ module Aptible
53
82
  )
54
83
  end
55
84
 
85
+ option(
86
+ :idle_timeout,
87
+ type: :string,
88
+ desc: 'Timeout (seconds) to enforce idle timeouts while ' \
89
+ 'sending and receiving responses. Use "default" to ' \
90
+ 'reset to the platform default'
91
+ )
92
+
56
93
  if builder.alb?
57
94
  option(
58
95
  :load_balancing_algorithm_type,
@@ -69,6 +106,50 @@ module Aptible
69
106
  desc: "Share this Endpoint's load balancer with other " \
70
107
  'Endpoints'
71
108
  )
109
+
110
+ option(
111
+ :force_ssl,
112
+ type: :boolean,
113
+ desc: 'Redirect all HTTP requests to HTTPS, and ' \
114
+ 'enable the Strict-Transport-Security header (HSTS)'
115
+ )
116
+
117
+ option(
118
+ :maintenance_page_url,
119
+ type: :string,
120
+ desc: 'The URL of a maintenance page to cache and serve ' \
121
+ 'when requests time out, or your app is unhealthy. ' \
122
+ 'Use "default" to reset to the platform default'
123
+ )
124
+
125
+ option(
126
+ :release_healthcheck_timeout,
127
+ type: :string,
128
+ desc: 'Timeout (seconds) to wait for your app to ' \
129
+ 'respond to a release health check. Use "default" ' \
130
+ 'to reset to the platform default'
131
+ )
132
+
133
+ option(
134
+ :show_elb_healthchecks,
135
+ type: :boolean,
136
+ desc: 'Show all runtime health check requets in the ' \
137
+ "endpoint's logs"
138
+ )
139
+
140
+ option(
141
+ :ssl_protocols_override,
142
+ type: :string,
143
+ desc: SSL_PROTOCOL_ALB_DESC
144
+ )
145
+
146
+ option(
147
+ :strict_health_checks,
148
+ type: :boolean,
149
+ desc: 'Require containers to respond to health checks ' \
150
+ 'with a 200 OK HTTP response.'
151
+ )
152
+
72
153
  end
73
154
  end
74
155
 
@@ -128,6 +209,27 @@ module Aptible
128
209
  desc: 'The fingerprint of an existing Certificate to use ' \
129
210
  'on this Endpoint'
130
211
  )
212
+
213
+ unless builder.alb?
214
+ option(
215
+ :ssl_protocols_override,
216
+ type: :string,
217
+ desc: SSL_PROTOCOL_ELB_DESC
218
+ )
219
+
220
+ option(
221
+ :ssl_ciphers_override,
222
+ type: :string,
223
+ desc: 'Specify the allowed SSL ciphers. ' \
224
+ 'Use "default" to reset to the platform default'
225
+ )
226
+
227
+ option(
228
+ :disable_weak_cipher_suites,
229
+ type: :boolean,
230
+ desc: 'Block the SSLv3 protocol and RC4 ciphers'
231
+ )
232
+ end
131
233
  end
132
234
  end
133
235
  end
@@ -137,6 +239,7 @@ module Aptible
137
239
  verify_option_conflicts(options)
138
240
 
139
241
  params = {}
242
+ settings = {}
140
243
 
141
244
  params[:ip_whitelist] = options.delete(:ip_whitelist) do
142
245
  create? ? [] : nil
@@ -203,6 +306,56 @@ module Aptible
203
306
  params[:shared] = options.delete(:shared)
204
307
  end
205
308
 
309
+ if (proto = options[:ssl_protocols_override]) &&
310
+ proto != 'default'
311
+ unless ALB_PROTOCOL_VALUES.include?(proto)
312
+ raise Thor::Error,
313
+ "Invalid --ssl-protocols-override: \"#{proto}\". " \
314
+ "Valid options are: #{ALB_PROTOCOL_VALUES.join(', ')}"
315
+ end
316
+
317
+ if !ELB_PROTOCOL_VALUES.include?(proto) && !alb?
318
+ raise Thor::Error,
319
+ "Invalid --ssl-protocols-override: \"#{proto}\". " \
320
+ 'PFS options are only valid for an HTTPS (ALB) endpoint. ' \
321
+ "Valid options are: #{ELB_PROTOCOL_VALUES.join(', ')}"
322
+ end
323
+ end
324
+
325
+ vhost_settings = %i(
326
+ idle_timeout
327
+ maintenance_page_url
328
+ release_healthcheck_timeout
329
+ ssl_protocols_override
330
+ ssl_ciphers_override
331
+ )
332
+
333
+ vhost_settings.each do |key|
334
+ val = options.delete(key)
335
+ next if val.nil?
336
+
337
+ settings[key.to_s.upcase] = case val
338
+ when 'default'
339
+ ''
340
+ else
341
+ val
342
+ end
343
+ end
344
+
345
+ boolean_vhost_settings = %i(
346
+ force_ssl
347
+ show_elb_healthchecks
348
+ strict_health_checks
349
+ disable_weak_cipher_suites
350
+ )
351
+
352
+ boolean_vhost_settings.each do |key|
353
+ value = options.delete(key)
354
+ next if value.nil?
355
+
356
+ settings[key.to_s.upcase] = value.to_s
357
+ end
358
+
206
359
  options.delete(:environment)
207
360
 
208
361
  # NOTE: This is here to ensure that specs don't test for options
@@ -210,7 +363,7 @@ module Aptible
210
363
  # this.
211
364
  raise "Unexpected options: #{options}" if options.any?
212
365
 
213
- params.delete_if { |_, v| v.nil? }
366
+ [params.delete_if { |_, v| v.nil? }, settings]
214
367
  end
215
368
 
216
369
  FLAGS.each do |f|
@@ -2,8 +2,11 @@ module Aptible
2
2
  module CLI
3
3
  module Helpers
4
4
  module Vhost
5
- def provision_vhost_and_explain(service, vhost)
6
- op = vhost.create_operation!(type: 'provision')
5
+ def provision_vhost_and_explain(service, vhost, settings)
6
+ op = vhost.create_operation!(
7
+ type: 'provision',
8
+ **(settings.empty? ? {} : { settings: settings })
9
+ )
7
10
  attach_to_operation_logs(op)
8
11
 
9
12
  Formatter.render(Renderer.current) do |root|
@@ -7,7 +7,9 @@ module Aptible
7
7
  POST_PROCESSED_KEYS = {
8
8
  'Tls' => 'TLS',
9
9
  'Dns' => 'DNS',
10
- 'Ip' => 'IP'
10
+ 'Ip' => 'IP',
11
+ 'Ssl' => 'SSL',
12
+ 'Elb' => 'ELB'
11
13
  }.freeze
12
14
 
13
15
  def visit(node, io)
@@ -95,11 +95,14 @@ module Aptible
95
95
  node.value('created_at', operation.created_at)
96
96
  end
97
97
 
98
- def inject_app(node, app, account)
98
+ def inject_app(node, app, account, setting = nil,
99
+ include_services: true)
99
100
  node.value('id', app.id)
100
101
  node.value('handle', app.handle)
101
102
  node.value('created_at', app.created_at)
102
103
 
104
+ attach_account(node, account)
105
+
103
106
  node.value('status', app.status)
104
107
  node.value('git_remote', app.git_repo)
105
108
 
@@ -109,15 +112,24 @@ module Aptible
109
112
  end
110
113
  end
111
114
 
112
- node.list('services') do |services_list|
113
- app.each_service do |service|
114
- services_list.object do |n|
115
- inject_service(n, service, NO_NESTING)
115
+ if include_services
116
+ node.list('services') do |services_list|
117
+ app.each_service do |service|
118
+ services_list.object do |n|
119
+ inject_service(n, service, NO_NESTING)
120
+ end
116
121
  end
117
122
  end
118
123
  end
119
124
 
120
- attach_account(node, account)
125
+ unless setting.nil?
126
+ node.value('docker_image',
127
+ setting.settings['APTIBLE_DOCKER_IMAGE'])
128
+ node.value('private_registry_username',
129
+ setting.sensitive_settings['APTIBLE_PRIVATE_REGISTRY_USERNAME'])
130
+ node.value('private_registry_password',
131
+ setting.sensitive_settings['APTIBLE_PRIVATE_REGISTRY_PASSWORD'])
132
+ end
121
133
  end
122
134
 
123
135
  def inject_database_minimal(node, database, account)
@@ -213,6 +225,12 @@ module Aptible
213
225
 
214
226
  node.value('internal', vhost.internal)
215
227
 
228
+ unless vhost.current_setting.nil?
229
+ vhost.current_setting.settings.each do |k, v|
230
+ node.value(k.downcase, v)
231
+ end
232
+ end
233
+
216
234
  ip_whitelist = if vhost.ip_whitelist.any?
217
235
  vhost.ip_whitelist.join(' ')
218
236
  else
@@ -26,7 +26,8 @@ module Aptible
26
26
  accounts.each do |account|
27
27
  account.each_app do |app|
28
28
  node.object do |n|
29
- ResourceFormatter.inject_app(n, app, account)
29
+ setting = current_setting(app)
30
+ ResourceFormatter.inject_app(n, app, account, setting)
30
31
  end
31
32
  end
32
33
  end
@@ -125,6 +126,32 @@ module Aptible
125
126
  end
126
127
  end
127
128
 
129
+ desc 'apps:settings HANDLE', 'Display deployment related settings for an app'
130
+ option :environment, aliases: '--env'
131
+ define_method 'apps:settings' do |handle|
132
+ telemetry(__method__, options.merge(handle: handle))
133
+
134
+ environment = nil
135
+ if options[:environment]
136
+ environment = environment_from_handle(options[:environment])
137
+ end
138
+ app = app_from_handle(handle, environment)
139
+
140
+ raise Thor::Error, "Could not find app #{handle}" if app.nil?
141
+
142
+ app = with_sensitive(app)
143
+ setting = current_setting(app)
144
+
145
+ Formatter.render(Renderer.current) do |root|
146
+ root.object do |node|
147
+ ResourceFormatter.inject_app(
148
+ node, app, app.account, setting,
149
+ include_services: false
150
+ )
151
+ end
152
+ end
153
+ end
154
+
128
155
  desc 'apps:rename OLD_HANDLE NEW_HANDLE [--environment'\
129
156
  ' ENVIRONMENT_HANDLE]', 'Rename an app handle. In order'\
130
157
  ' for the new app handle to appear in log drain and metric'\
@@ -27,13 +27,18 @@ module Aptible
27
27
  service = database.service
28
28
  raise Thor::Error, 'Database is not provisioned' if service.nil?
29
29
 
30
+ prepared_params, settings = database_create_flags.prepare(
31
+ database.account,
32
+ options
33
+ )
34
+
30
35
  vhost = service.create_vhost!(
31
36
  type: 'tcp',
32
37
  platform: 'elb',
33
- **database_create_flags.prepare(database.account, options)
38
+ **prepared_params
34
39
  )
35
40
 
36
- provision_vhost_and_explain(service, vhost)
41
+ provision_vhost_and_explain(service, vhost, settings)
37
42
  end
38
43
 
39
44
  database_modify_flags = Helpers::Vhost::OptionSetBuilder.new do
@@ -49,9 +54,14 @@ module Aptible
49
54
 
50
55
  database = ensure_database(options.merge(db: options[:database]))
51
56
  vhost = find_vhost(each_service(database), hostname)
52
- vhost.update!(**database_modify_flags.prepare(database.account,
53
- options))
54
- provision_vhost_and_explain(vhost.service, vhost)
57
+
58
+ prepared_params, settings = database_modify_flags.prepare(
59
+ database.account,
60
+ options
61
+ )
62
+
63
+ vhost.update!(**prepared_params)
64
+ provision_vhost_and_explain(vhost.service, vhost, settings)
55
65
  end
56
66
 
57
67
  tcp_create_flags = Helpers::Vhost::OptionSetBuilder.new do
@@ -246,18 +256,26 @@ module Aptible
246
256
  no_commands do
247
257
  def create_app_vhost(flags, options, process_type, **attrs)
248
258
  service = ensure_service(options, process_type)
259
+
260
+ prepared_params, settings =
261
+ flags.prepare(service.account, options)
262
+
249
263
  vhost = service.create_vhost!(
250
- **flags.prepare(service.account, options),
264
+ **prepared_params,
251
265
  **attrs
252
266
  )
253
- provision_vhost_and_explain(service, vhost)
267
+ provision_vhost_and_explain(service, vhost, settings)
254
268
  end
255
269
 
256
270
  def modify_app_vhost(flags, options, hostname)
257
271
  app = ensure_app(options)
258
272
  vhost = find_vhost(each_service(app), hostname)
259
- vhost.update!(**flags.prepare(vhost.service.account, options))
260
- provision_vhost_and_explain(vhost.service, vhost)
273
+
274
+ prepared_params, settings =
275
+ flags.prepare(vhost.service.account, options)
276
+
277
+ vhost.update!(**prepared_params)
278
+ provision_vhost_and_explain(vhost.service, vhost, settings)
261
279
  end
262
280
  end
263
281
  end
@@ -1,5 +1,5 @@
1
1
  module Aptible
2
2
  module CLI
3
- VERSION = '0.26.5'.freeze
3
+ VERSION = '0.26.7'.freeze
4
4
  end
5
5
  end
@@ -0,0 +1,94 @@
1
+ require 'spec_helper'
2
+
3
+ describe Aptible::CLI::Helpers::Vhost::OptionSetBuilder do
4
+ def register_options(builder)
5
+ klass = Class.new(Thor) { include Aptible::CLI::Helpers::App }
6
+ builder.declare_options(klass)
7
+ klass.instance_variable_get(:@method_options)
8
+ end
9
+
10
+ describe '--ssl-protocols-override option description' do
11
+ context 'HTTPS endpoints (ALB, alb! flag set)' do
12
+ let(:builder) do
13
+ described_class.new do
14
+ app!
15
+ tls!
16
+ alb!
17
+ end
18
+ end
19
+
20
+ it 'includes PFS values' do
21
+ desc = register_options(builder)[:ssl_protocols_override].description
22
+ expect(desc).to include('PFS')
23
+ end
24
+ end
25
+
26
+ context 'TLS endpoints (ELB, tls! without alb!)' do
27
+ let(:builder) do
28
+ described_class.new do
29
+ app!
30
+ tls!
31
+ end
32
+ end
33
+
34
+ it 'does not include PFS values' do
35
+ desc = register_options(builder)[:ssl_protocols_override].description
36
+ expect(desc).not_to include('PFS')
37
+ end
38
+
39
+ it 'is still present' do
40
+ expect(register_options(builder)).to have_key(:ssl_protocols_override)
41
+ end
42
+ end
43
+
44
+ context 'gRPC endpoints (ELB, tls! without alb!)' do
45
+ let(:builder) do
46
+ described_class.new do
47
+ app!
48
+ port!
49
+ tls!
50
+ end
51
+ end
52
+
53
+ it 'does not include PFS values' do
54
+ desc = register_options(builder)[:ssl_protocols_override].description
55
+ expect(desc).not_to include('PFS')
56
+ end
57
+ end
58
+
59
+ context 'TCP endpoints (no tls! flag)' do
60
+ let(:builder) do
61
+ described_class.new do
62
+ app!
63
+ ports!
64
+ end
65
+ end
66
+
67
+ it 'is absent' do
68
+ expect(register_options(builder)).not_to have_key(:ssl_protocols_override)
69
+ end
70
+ end
71
+ end
72
+
73
+ describe 'SSL_PROTOCOL_ALB_DESC' do
74
+ subject { described_class::SSL_PROTOCOL_ALB_DESC }
75
+
76
+ it 'lists all PFS protocol values' do
77
+ pfs_values = described_class::SSL_PROTOCOL_VALUES.select { |v| v.include?('PFS') }
78
+ pfs_values.each { |v| is_expected.to include(v) }
79
+ end
80
+ end
81
+
82
+ describe 'SSL_PROTOCOL_ELB_DESC' do
83
+ subject { described_class::SSL_PROTOCOL_ELB_DESC }
84
+
85
+ it 'contains no PFS values' do
86
+ is_expected.not_to include('PFS')
87
+ end
88
+
89
+ it 'lists all non-PFS protocol values' do
90
+ non_pfs_values = described_class::SSL_PROTOCOL_VALUES.reject { |v| v.include?('PFS') }
91
+ non_pfs_values.each { |v| is_expected.to include(v) }
92
+ end
93
+ end
94
+ end
@@ -25,6 +25,9 @@ describe Aptible::CLI::ResourceFormatter do
25
25
  shared: false
26
26
  )
27
27
 
28
+ vhost.current_configuration = Fabricate(:setting, settings: {},
29
+ vhost: vhost)
30
+
28
31
  expected = [
29
32
  'Id: 12',
30
33
  'Hostname: foo.io',
@@ -101,98 +101,218 @@ describe Aptible::CLI::Agent do
101
101
  .to eq("=== #{account2.handle}\n#{app2.handle}\n")
102
102
  end
103
103
 
104
- it 'includes services in JSON' do
105
- account = Fabricate(:account, handle: 'account')
106
- app = Fabricate(:app, account: account, handle: 'app')
107
- allow(Aptible::Api::Account).to receive(:all).and_return([account])
108
- allow(Aptible::Api::App).to receive(:all).and_return([app])
104
+ context 'with JSON output format' do
105
+ around do |example|
106
+ ClimateControl.modify(APTIBLE_OUTPUT_FORMAT: 'json') { example.run }
107
+ end
109
108
 
110
- s1 = Fabricate(
111
- :service,
112
- app: app, process_type: 's1', command: 'true', container_count: 2,
113
- instance_class: 'm5'
114
- )
115
- s2 = Fabricate(
116
- :service,
117
- app: app, process_type: 's2', container_memory_limit_mb: 2048,
118
- instance_class: 'r5'
119
- )
109
+ it 'includes services in JSON' do
110
+ account = Fabricate(:account, handle: 'account')
111
+ app = Fabricate(:app, account: account, handle: 'app')
112
+ allow(Aptible::Api::Account).to receive(:all).and_return([account])
113
+ allow(Aptible::Api::App).to receive(:all).and_return([app])
114
+
115
+ s1 = Fabricate(
116
+ :service,
117
+ app: app, process_type: 's1', command: 'true', container_count: 2,
118
+ instance_class: 'm5'
119
+ )
120
+ s2 = Fabricate(
121
+ :service,
122
+ app: app, process_type: 's2', container_memory_limit_mb: 2048,
123
+ instance_class: 'r5'
124
+ )
120
125
 
121
- expected_json = [
122
- {
123
- 'environment' => {
124
- 'id' => account.id,
125
- 'handle' => account.handle,
126
- 'created_at' => fmt_time(account.created_at)
127
- },
128
- 'handle' => app.handle,
129
- 'id' => app.id,
130
- 'status' => app.status,
131
- 'git_remote' => app.git_repo,
132
- 'created_at' => fmt_time(app.created_at),
133
- 'services' => [
134
- {
135
- 'service' => s1.process_type,
136
- 'id' => s1.id,
137
- 'command' => s1.command,
138
- 'container_count' => s1.container_count,
139
- 'container_profile' => 'm',
140
- 'container_size' => s1.container_memory_limit_mb,
141
- 'created_at' => fmt_time(s1.created_at)
126
+ expected_json = [
127
+ {
128
+ 'environment' => {
129
+ 'id' => account.id,
130
+ 'handle' => account.handle,
131
+ 'created_at' => fmt_time(account.created_at)
142
132
  },
143
- {
144
- 'service' => s2.process_type,
145
- 'id' => s2.id,
146
- 'command' => 'CMD',
147
- 'container_count' => s2.container_count,
148
- 'container_profile' => 'r',
149
- 'container_size' => s2.container_memory_limit_mb,
150
- 'created_at' => fmt_time(s2.created_at)
151
- }
152
- ]
153
- }
154
- ]
133
+ 'handle' => app.handle,
134
+ 'id' => app.id,
135
+ 'status' => app.status,
136
+ 'git_remote' => app.git_repo,
137
+ 'created_at' => fmt_time(app.created_at),
138
+ 'services' => [
139
+ {
140
+ 'service' => s1.process_type,
141
+ 'id' => s1.id,
142
+ 'command' => s1.command,
143
+ 'container_count' => s1.container_count,
144
+ 'container_profile' => 'm',
145
+ 'container_size' => s1.container_memory_limit_mb,
146
+ 'created_at' => fmt_time(s1.created_at)
147
+ },
148
+ {
149
+ 'service' => s2.process_type,
150
+ 'id' => s2.id,
151
+ 'command' => 'CMD',
152
+ 'container_count' => s2.container_count,
153
+ 'container_profile' => 'r',
154
+ 'container_size' => s2.container_memory_limit_mb,
155
+ 'created_at' => fmt_time(s2.created_at)
156
+ }
157
+ ]
158
+ }
159
+ ]
155
160
 
156
- subject.send('apps')
161
+ subject.send('apps')
157
162
 
158
- expect(captured_output_json).to eq(expected_json)
159
- end
163
+ expect(captured_output_json).to eq(expected_json)
164
+ end
160
165
 
161
- it 'includes the last deploy operation in JSON' do
162
- account = Fabricate(:account, handle: 'account')
163
- op = Fabricate(:operation, type: 'deploy', status: 'succeeded')
164
- app = Fabricate(:app, account: account, handle: 'app',
165
- last_deploy_operation: op)
166
- allow(Aptible::Api::Account).to receive(:all).and_return([account])
167
- allow(Aptible::Api::App).to receive(:all).and_return([app])
168
-
169
- expected_json = [
170
- {
171
- 'environment' => {
172
- 'id' => account.id,
173
- 'handle' => account.handle,
174
- 'created_at' => fmt_time(account.created_at)
175
- },
176
- 'handle' => app.handle,
177
- 'id' => app.id,
178
- 'status' => app.status,
179
- 'git_remote' => app.git_repo,
180
- 'created_at' => fmt_time(app.created_at),
181
- 'last_deploy_operation' =>
182
- {
183
- 'id' => op.id,
184
- 'status' => op.status,
185
- 'git_ref' => op.git_ref,
186
- 'user_email' => op.user_email,
187
- 'created_at' => op.created_at
166
+ it 'includes the last deploy operation in JSON' do
167
+ account = Fabricate(:account, handle: 'account')
168
+ op = Fabricate(:operation, type: 'deploy', status: 'succeeded')
169
+ app = Fabricate(:app, account: account, handle: 'app',
170
+ last_deploy_operation: op)
171
+ allow(Aptible::Api::Account).to receive(:all).and_return([account])
172
+ allow(Aptible::Api::App).to receive(:all).and_return([app])
173
+
174
+ expected_json = [
175
+ {
176
+ 'environment' => {
177
+ 'id' => account.id,
178
+ 'handle' => account.handle,
179
+ 'created_at' => fmt_time(account.created_at)
180
+ },
181
+ 'handle' => app.handle,
182
+ 'id' => app.id,
183
+ 'status' => app.status,
184
+ 'git_remote' => app.git_repo,
185
+ 'created_at' => fmt_time(app.created_at),
186
+ 'last_deploy_operation' =>
187
+ {
188
+ 'id' => op.id,
189
+ 'status' => op.status,
190
+ 'git_ref' => op.git_ref,
191
+ 'user_email' => op.user_email,
192
+ 'created_at' => op.created_at
193
+ },
194
+ 'services' => []
195
+ }
196
+ ]
197
+
198
+ subject.send('apps')
199
+
200
+ expect(captured_output_json).to eq(expected_json)
201
+ end
202
+
203
+ it 'includes docker image and registry settings' do
204
+ account = Fabricate(:account, handle: 'account')
205
+ setting = Fabricate(
206
+ :setting,
207
+ settings: { 'APTIBLE_DOCKER_IMAGE' => 'quay.io/myorg/myapp:latest' },
208
+ sensitive_settings: {
209
+ 'APTIBLE_PRIVATE_REGISTRY_USERNAME' => 'registryuser',
210
+ 'APTIBLE_PRIVATE_REGISTRY_PASSWORD' => 'registrypass'
211
+ }
212
+ )
213
+ app = Fabricate(:app, account: account, handle: 'app')
214
+ allow(Aptible::Api::Account).to receive(:all).and_return([account])
215
+ allow(subject).to receive(:current_setting).with(app)
216
+ .and_return(setting)
217
+
218
+ expected_json = [
219
+ {
220
+ 'environment' => {
221
+ 'id' => account.id,
222
+ 'handle' => account.handle,
223
+ 'created_at' => fmt_time(account.created_at)
188
224
  },
189
- 'services' => []
225
+ 'handle' => app.handle,
226
+ 'id' => app.id,
227
+ 'status' => app.status,
228
+ 'git_remote' => app.git_repo,
229
+ 'created_at' => fmt_time(app.created_at),
230
+ 'services' => [],
231
+ 'docker_image' => 'quay.io/myorg/myapp:latest',
232
+ 'private_registry_username' => 'registryuser',
233
+ 'private_registry_password' => 'registrypass'
234
+ }
235
+ ]
236
+
237
+ subject.send('apps')
238
+
239
+ expect(captured_output_json).to eq(expected_json)
240
+ end
241
+
242
+ it 'omits docker image and registry settings when no current_setting' do
243
+ account = Fabricate(:account, handle: 'account')
244
+ app = Fabricate(:app, account: account, handle: 'app')
245
+ allow(Aptible::Api::Account).to receive(:all).and_return([account])
246
+ allow(subject).to receive(:current_setting).with(app)
247
+ .and_return(nil)
248
+
249
+ subject.send('apps')
250
+
251
+ json = captured_output_json
252
+ expect(json.first).not_to have_key('docker_image')
253
+ expect(json.first).not_to have_key('private_registry_username')
254
+ expect(json.first).not_to have_key('private_registry_password')
255
+ end
256
+ end
257
+ end
258
+
259
+ describe '#apps:settings' do
260
+ it 'displays app settings in JSON' do
261
+ ClimateControl.modify(APTIBLE_OUTPUT_FORMAT: 'json') do
262
+ setting = Fabricate(
263
+ :setting,
264
+ settings: { 'APTIBLE_DOCKER_IMAGE' => 'quay.io/myorg/myapp:latest' },
265
+ sensitive_settings: {
266
+ 'APTIBLE_PRIVATE_REGISTRY_USERNAME' => 'registryuser',
267
+ 'APTIBLE_PRIVATE_REGISTRY_PASSWORD' => 'registrypass'
268
+ }
269
+ )
270
+ allow(subject).to receive(:app_from_handle)
271
+ .with('hello', nil).and_return(app)
272
+ allow(subject).to receive(:current_setting).with(app)
273
+ .and_return(setting)
274
+
275
+ subject.send('apps:settings', 'hello')
276
+
277
+ json = captured_output_json
278
+ expect(json['handle']).to eq('hello')
279
+ expect(json['docker_image']).to eq('quay.io/myorg/myapp:latest')
280
+ expect(json['private_registry_username']).to eq('registryuser')
281
+ expect(json['private_registry_password']).to eq('registrypass')
282
+ expect(json).not_to have_key('services')
283
+ expect(json).not_to have_key('last_deploy_operation')
284
+ end
285
+ end
286
+
287
+ it 'displays app settings in text' do
288
+ setting = Fabricate(
289
+ :setting,
290
+ settings: { 'APTIBLE_DOCKER_IMAGE' => 'quay.io/myorg/myapp:latest' },
291
+ sensitive_settings: {
292
+ 'APTIBLE_PRIVATE_REGISTRY_USERNAME' => 'registryuser',
293
+ 'APTIBLE_PRIVATE_REGISTRY_PASSWORD' => 'registrypass'
190
294
  }
191
- ]
295
+ )
296
+ allow(subject).to receive(:app_from_handle)
297
+ .with('hello', nil).and_return(app)
298
+ allow(subject).to receive(:current_setting).with(app)
299
+ .and_return(setting)
300
+
301
+ subject.send('apps:settings', 'hello')
302
+
303
+ output = captured_output_text
304
+ expect(output).to include('quay.io/myorg/myapp:latest')
305
+ expect(output).to include('registryuser')
306
+ expect(output).to include('registrypass')
307
+ expect(output).not_to include('services')
308
+ end
192
309
 
193
- subject.send('apps')
310
+ it 'raises an error when app is not found' do
311
+ allow(subject).to receive(:app_from_handle)
312
+ .with('nope', nil).and_return(nil)
194
313
 
195
- expect(captured_output_json).to eq(expected_json)
314
+ expect { subject.send('apps:settings', 'nope') }
315
+ .to raise_error(Thor::Error, /Could not find app nope/)
196
316
  end
197
317
  end
198
318
 
@@ -25,12 +25,12 @@ describe Aptible::CLI::Agent do
25
25
  end
26
26
  end
27
27
 
28
- def expect_create_vhost(service, options)
28
+ def expect_create_vhost(service, options, settings: nil)
29
29
  expect(service).to receive(:create_vhost!).with(
30
30
  hash_including(options)
31
31
  ) do |args|
32
32
  Fabricate(:vhost, service: service, **args).tap do |v|
33
- expect_operation(v, 'provision')
33
+ expect_operation(v, 'provision', settings: settings)
34
34
  expect(v).to receive(:reload).and_return(v)
35
35
  expect(Aptible::CLI::ResourceFormatter).to receive(:inject_vhost)
36
36
  .with(an_instance_of(Aptible::CLI::Formatter::Object), v, service)
@@ -49,8 +49,16 @@ describe Aptible::CLI::Agent do
49
49
  end
50
50
  end
51
51
 
52
- def expect_operation(vhost, type)
53
- expect(vhost).to receive(:create_operation!).with(type: type) do
52
+ def expect_operation(vhost, type, settings: nil)
53
+ expect(vhost).to receive(:create_operation!) do |args|
54
+ expect(args[:type]).to eq(type)
55
+
56
+ if settings.nil?
57
+ expect(args).not_to have_key(:settings)
58
+ else
59
+ expect(args[:settings]).to eq(settings)
60
+ end
61
+
54
62
  Fabricate(:operation).tap do |o|
55
63
  expect(subject).to receive(:attach_to_operation_logs).with(o)
56
64
  end
@@ -170,7 +178,13 @@ describe Aptible::CLI::Agent do
170
178
  it 'lists Endpoints' do
171
179
  s = Fabricate(:service, database: db)
172
180
  v1 = Fabricate(:vhost, service: s)
181
+ v1.current_setting = Fabricate(:setting,
182
+ settings: { 'IDLE_TIMEOUT' => '123' },
183
+ vhost: v1)
173
184
  v2 = Fabricate(:vhost, service: s)
185
+ v2.current_setting = Fabricate(:setting,
186
+ settings: { 'FORCE_SSL' => 'true' },
187
+ vhost: v2)
174
188
 
175
189
  stub_options(database: db.handle)
176
190
  subject.send('endpoints:list')
@@ -179,6 +193,8 @@ describe Aptible::CLI::Agent do
179
193
 
180
194
  expect(lines).to include("Hostname: #{v1.external_host}")
181
195
  expect(lines).to include("Hostname: #{v2.external_host}")
196
+ expect(lines).to include('Idle Timeout: 123')
197
+ expect(lines).to include('Force SSL: true')
182
198
 
183
199
  expect(lines[0]).not_to eq("\n")
184
200
  expect(lines[-1]).not_to eq("\n")
@@ -243,6 +259,105 @@ describe Aptible::CLI::Agent do
243
259
  stub_options
244
260
  end
245
261
 
262
+ shared_examples 'shared create and modify ALB settings examples' do |method|
263
+ context 'App Vhost Settings (string)' do
264
+ string_options = %i(
265
+ idle_timeout
266
+ maintenance_page_url
267
+ release_healthcheck_timeout
268
+ )
269
+
270
+ let(:value) { 'some value' }
271
+
272
+ string_options.each do |option|
273
+ context "--#{option.to_s.tr('_', '-')}" do
274
+ it 'passes a value if provided' do
275
+ wanted = { option.to_s.upcase => value }
276
+ expect_create_vhost(service, {}, { settings: wanted })
277
+ stub_options(option => value)
278
+ subject.send(method, 'web')
279
+ end
280
+
281
+ it 'passes nothing if not provided' do
282
+ expect_create_vhost(service, {})
283
+ subject.send(method, 'web')
284
+ end
285
+
286
+ context 'reverting to default' do
287
+ it 'sends an empty string if passed an empty string' do
288
+ wanted = { option.to_s.upcase => '' }
289
+ expect_create_vhost(service, {}, { settings: wanted })
290
+ stub_options(option => '')
291
+ subject.send(method, 'web')
292
+ end
293
+
294
+ it 'sends an empty string if passed the string "default"' do
295
+ wanted = { option.to_s.upcase => '' }
296
+ expect_create_vhost(service, {}, { settings: wanted })
297
+ stub_options(option => 'default')
298
+ subject.send(method, 'web')
299
+ end
300
+ end
301
+ end
302
+ end
303
+ end
304
+
305
+ context '--ssl-protocols-override' do
306
+ it 'passes a valid non-PFS value' do
307
+ wanted = { 'SSL_PROTOCOLS_OVERRIDE' => 'TLSv1.2' }
308
+ expect_create_vhost(service, {}, { settings: wanted })
309
+ stub_options(ssl_protocols_override: 'TLSv1.2')
310
+ subject.send(method, 'web')
311
+ end
312
+
313
+ it 'passes a valid PFS value' do
314
+ wanted = { 'SSL_PROTOCOLS_OVERRIDE' => 'TLSv1.2 PFS' }
315
+ expect_create_vhost(service, {}, { settings: wanted })
316
+ stub_options(ssl_protocols_override: 'TLSv1.2 PFS')
317
+ subject.send(method, 'web')
318
+ end
319
+
320
+ it 'raises an error for an invalid value' do
321
+ stub_options(ssl_protocols_override: 'TLSv1.0')
322
+ expect { subject.send(method, 'web') }
323
+ .to raise_error(/invalid --ssl-protocols-override/im)
324
+ end
325
+
326
+ it 'passes nothing if not provided' do
327
+ expect_create_vhost(service, {})
328
+ subject.send(method, 'web')
329
+ end
330
+
331
+ it 'sends an empty string if passed the string "default"' do
332
+ wanted = { 'SSL_PROTOCOLS_OVERRIDE' => '' }
333
+ expect_create_vhost(service, {}, { settings: wanted })
334
+ stub_options(ssl_protocols_override: 'default')
335
+ subject.send(method, 'web')
336
+ end
337
+ end
338
+
339
+ context 'App Vhost Settings (boolean)' do
340
+ boolean_options = %i(
341
+ force_ssl
342
+ show_elb_healthchecks
343
+ strict_health_checks
344
+ )
345
+
346
+ boolean_options.each do |option|
347
+ [true, false].each do |value|
348
+ context "--#{value ? '' : 'no-'}#{option.to_s.tr('_', '-')}" do
349
+ it "sets the value to the string '#{value}'" do
350
+ wanted = { option.to_s.upcase => value.to_s }
351
+ expect_create_vhost(service, {}, { settings: wanted })
352
+ stub_options(option => value)
353
+ subject.send(method, 'web')
354
+ end
355
+ end
356
+ end
357
+ end
358
+ end
359
+ end
360
+
246
361
  shared_examples 'shared create app vhost examples' do |method|
247
362
  context 'App Vhost Options' do
248
363
  it 'fails if the app does not exist' do
@@ -440,10 +555,64 @@ describe Aptible::CLI::Agent do
440
555
  end
441
556
  end
442
557
 
558
+ shared_examples 'shared create idle timeout examples' do |method|
559
+ context 'IDLE_TIMEOUT' do
560
+ it 'passes idle_timeout if provided' do
561
+ expect_create_vhost(service, {}, { settings: { 'IDLE_TIMEOUT' => '30' } })
562
+ stub_options(idle_timeout: '30')
563
+ subject.send(method, 'web')
564
+ end
565
+ end
566
+ end
567
+
568
+ shared_examples 'shared create non-alb tls settings examples' do |method|
569
+ context '--ssl-protocols-override' do
570
+ it 'passes a valid non-PFS value' do
571
+ wanted = { 'SSL_PROTOCOLS_OVERRIDE' => 'TLSv1.2' }
572
+ expect_create_vhost(service, {}, { settings: wanted })
573
+ stub_options(ssl_protocols_override: 'TLSv1.2')
574
+ subject.send(method, 'web')
575
+ end
576
+
577
+ it 'raises an error for a PFS value' do
578
+ stub_options(ssl_protocols_override: 'TLSv1.2 PFS')
579
+ expect { subject.send(method, 'web') }
580
+ .to raise_error(/pfs.*only.*alb/im)
581
+ end
582
+
583
+ it 'raises an error for an invalid value' do
584
+ stub_options(ssl_protocols_override: 'TLSv1.0')
585
+ expect { subject.send(method, 'web') }
586
+ .to raise_error(/invalid --ssl-protocols-override/im)
587
+ end
588
+ end
589
+
590
+ context 'SSL_CIPHERS_OVERRIDE' do
591
+ it 'passes ssl_ciphers_override if provided' do
592
+ wanted = { 'SSL_CIPHERS_OVERRIDE' => 'HIGH:!aNULL' }
593
+ expect_create_vhost(service, {}, { settings: wanted })
594
+ stub_options(ssl_ciphers_override: 'HIGH:!aNULL')
595
+ subject.send(method, 'web')
596
+ end
597
+ end
598
+
599
+ context 'DISABLE_WEAK_CIPHER_SUITES' do
600
+ [true, false].each do |value|
601
+ it "sets disable_weak_cipher_suites to '#{value}'" do
602
+ wanted = { 'DISABLE_WEAK_CIPHER_SUITES' => value.to_s }
603
+ expect_create_vhost(service, {}, { settings: wanted })
604
+ stub_options(disable_weak_cipher_suites: value)
605
+ subject.send(method, 'web')
606
+ end
607
+ end
608
+ end
609
+ end
610
+
443
611
  describe 'endpoints:tcp:create' do
444
612
  m = 'endpoints:tcp:create'
445
613
  include_examples 'shared create app vhost examples', m
446
614
  include_examples 'shared create tcp vhost examples', m
615
+ include_examples 'shared create idle timeout examples', m
447
616
 
448
617
  it 'creates a TCP Endpoint' do
449
618
  expect_create_vhost(
@@ -465,6 +634,8 @@ describe Aptible::CLI::Agent do
465
634
  include_examples 'shared create app vhost examples', m
466
635
  include_examples 'shared create tcp vhost examples', m
467
636
  include_examples 'shared create tls vhost examples', m
637
+ include_examples 'shared create idle timeout examples', m
638
+ include_examples 'shared create non-alb tls settings examples', m
468
639
 
469
640
  it 'creates a TLS Endpoint' do
470
641
  expect_create_vhost(
@@ -484,6 +655,7 @@ describe Aptible::CLI::Agent do
484
655
  m = 'endpoints:https:create'
485
656
  include_examples 'shared create app vhost examples', m
486
657
  include_examples 'shared create tls vhost examples', m
658
+ include_examples 'shared create and modify ALB settings examples', m
487
659
 
488
660
  it 'creates a HTTP Endpoint' do
489
661
  expect_create_vhost(
@@ -516,6 +688,8 @@ describe Aptible::CLI::Agent do
516
688
  m = 'endpoints:grpc:create'
517
689
  include_examples 'shared create app vhost examples', m
518
690
  include_examples 'shared create tls vhost examples', m
691
+ include_examples 'shared create idle timeout examples', m
692
+ include_examples 'shared create non-alb tls settings examples', m
519
693
 
520
694
  it 'creates a gRPC Endpoint' do
521
695
  expect_create_vhost(
@@ -28,6 +28,7 @@ Fabricator(:app, from: :stub_app) do
28
28
  services { [] }
29
29
  configurations { [] }
30
30
  current_configuration { nil }
31
+ current_setting { nil }
31
32
  errors { Aptible::Resource::Errors.new }
32
33
  created_at { Time.now }
33
34
 
@@ -37,6 +38,11 @@ Fabricator(:app, from: :stub_app) do
37
38
  href: "/accounts/#{attrs[:account].id}"
38
39
  )
39
40
  }
41
+ if attrs[:current_setting]
42
+ hash[:current_setting] = OpenStruct.new(
43
+ href: "/settings/#{attrs[:current_setting].id}"
44
+ )
45
+ end
40
46
  OpenStruct.new(hash)
41
47
  end
42
48
 
@@ -0,0 +1,8 @@
1
+ class StubSetting < StubAptibleResource; end
2
+
3
+ Fabricator(:setting, from: :stub_setting) do
4
+ settings { {} }
5
+ sensitive_settings { {} }
6
+
7
+ after_create { |setting| setting.vhost.settings << setting if setting.vhost }
8
+ end
@@ -8,6 +8,8 @@ Fabricator(:vhost, from: :stub_vhost) do
8
8
  ip_whitelist { [] }
9
9
  container_ports { [] }
10
10
  created_at { Time.now }
11
+ settings { [] }
12
+ current_setting { nil }
11
13
 
12
14
  after_create { |vhost| vhost.service.vhosts << vhost }
13
15
  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.26.5
4
+ version: 0.26.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Frank Macreery
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-12 00:00:00.000000000 Z
11
+ date: 2026-06-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -563,6 +563,7 @@ files:
563
563
  - spec/aptible/cli/helpers/ssh_spec.rb
564
564
  - spec/aptible/cli/helpers/token_spec.rb
565
565
  - spec/aptible/cli/helpers/tunnel_spec.rb
566
+ - spec/aptible/cli/helpers/vhost/option_set_builder_spec.rb
566
567
  - spec/aptible/cli/renderer/json_spec.rb
567
568
  - spec/aptible/cli/renderer/text_spec.rb
568
569
  - spec/aptible/cli/resource_formatter_spec.rb
@@ -608,6 +609,7 @@ files:
608
609
  - spec/fabricators/operation_fabricator.rb
609
610
  - spec/fabricators/service_fabricator.rb
610
611
  - spec/fabricators/service_sizing_policy_fabricator.rb
612
+ - spec/fabricators/setting_fabricator.rb
611
613
  - spec/fabricators/stack_fabricator.rb
612
614
  - spec/fabricators/vhost_fabricator.rb
613
615
  - spec/mock/git
@@ -657,6 +659,7 @@ test_files:
657
659
  - spec/aptible/cli/helpers/ssh_spec.rb
658
660
  - spec/aptible/cli/helpers/token_spec.rb
659
661
  - spec/aptible/cli/helpers/tunnel_spec.rb
662
+ - spec/aptible/cli/helpers/vhost/option_set_builder_spec.rb
660
663
  - spec/aptible/cli/renderer/json_spec.rb
661
664
  - spec/aptible/cli/renderer/text_spec.rb
662
665
  - spec/aptible/cli/resource_formatter_spec.rb
@@ -702,6 +705,7 @@ test_files:
702
705
  - spec/fabricators/operation_fabricator.rb
703
706
  - spec/fabricators/service_fabricator.rb
704
707
  - spec/fabricators/service_sizing_policy_fabricator.rb
708
+ - spec/fabricators/setting_fabricator.rb
705
709
  - spec/fabricators/stack_fabricator.rb
706
710
  - spec/fabricators/vhost_fabricator.rb
707
711
  - spec/mock/git