diff --git a/Gemfile.lock b/Gemfile.lock index 3e85b63f..080d42cc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - aptible-cli (0.26.0) + aptible-cli (0.27.0) activesupport (>= 4.0, < 6.0) aptible-api (~> 1.12) aptible-auth (~> 1.4) @@ -35,7 +35,7 @@ GEM tzinfo (~> 1.1) addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) - aptible-api (1.12.0) + aptible-api (1.12.1) aptible-auth aptible-resource gem_config @@ -120,7 +120,7 @@ GEM parser (2.7.2.0) ast (~> 2.4.1) powerpack (0.1.3) - pry (0.14.2) + pry (0.15.2) coderay (~> 1.1) method_source (~> 1.0) public_suffix (3.1.1) diff --git a/lib/aptible/cli/helpers/vhost.rb b/lib/aptible/cli/helpers/vhost.rb index 7ec5c852..7d58551a 100644 --- a/lib/aptible/cli/helpers/vhost.rb +++ b/lib/aptible/cli/helpers/vhost.rb @@ -2,8 +2,11 @@ module Aptible module CLI module Helpers module Vhost - def provision_vhost_and_explain(service, vhost) - op = vhost.create_operation!(type: 'provision') + def provision_vhost_and_explain(service, vhost, settings) + op = vhost.create_operation!( + type: 'provision', + **(settings.empty? ? {} : { settings: settings }) + ) attach_to_operation_logs(op) Formatter.render(Renderer.current) do |root| diff --git a/lib/aptible/cli/helpers/vhost/option_set_builder.rb b/lib/aptible/cli/helpers/vhost/option_set_builder.rb index 22e3b9d8..49aef755 100644 --- a/lib/aptible/cli/helpers/vhost/option_set_builder.rb +++ b/lib/aptible/cli/helpers/vhost/option_set_builder.rb @@ -69,6 +69,76 @@ def declare_options(thor) desc: "Share this Endpoint's load balancer with other " \ 'Endpoints' ) + + option( + :client_body_timeout, + type: :string, + desc: 'Timeout (seconds) for receiving the request body, ' \ + 'applying only between successive read operations ' \ + 'rather than to the entire request body transmission' + ) + + option( + :force_ssl, + type: :boolean, + desc: 'Redirect all HTTP requests to HTTPS, and ' \ + 'enable the Strict-Transport-Security header (HSTS)' + ) + + option( + :idle_timeout, + type: :string, + desc: 'Timeout (seconds) to enforce idle timeouts while ' \ + 'sending and receiving responses' + ) + + option( + :ignore_invalid_headers, + type: :boolean, + desc: 'Controls whether header fields with invalid names ' \ + 'should be dropped by the endpoint' + ) + + option( + :maintenance_page_url, + type: :string, + desc: 'The URL of a maintenance page to cache and serve ' \ + 'when requests time out, or your app is unhealthy' + ) + + option( + :nginx_error_log_level, + type: :string, + desc: "Sets the log level for the endpoint's error logs" + ) + + option( + :release_healthcheck_timeout, + type: :string, + desc: 'Timeout (seconds) to wait for your app to ' \ + 'respond to a release health check' + ) + + option( + :show_elb_healthchecks, + type: :boolean, + desc: 'Show all runtime health check requets in the ' \ + "endpoint's logs" + ) + + option( + :ssl_protocols_override, + type: :string, + desc: 'Specify a list of allowed SSL protocols' + ) + + option( + :strict_health_checks, + type: :boolean, + desc: 'Require containers to respond to health checks ' \ + 'with a 200 OK HTTP response.' + ) + end end @@ -128,6 +198,18 @@ def declare_options(thor) desc: 'The fingerprint of an existing Certificate to use ' \ 'on this Endpoint' ) + + option( + :ssl_ciphers_override, + type: :string, + desc: 'Specify the allowed SSL ciphers' + ) + + option( + :ssl_protocols_override, + type: :string, + desc: 'Specify a list of allowed SSL protocols' + ) end end end @@ -137,6 +219,7 @@ def prepare(account, options) verify_option_conflicts(options) params = {} + settings = {} params[:ip_whitelist] = options.delete(:ip_whitelist) do create? ? [] : nil @@ -203,6 +286,53 @@ def prepare(account, options) params[:shared] = options.delete(:shared) end + vhost_settings = %i( + client_body_timeout + idle_timeout + maintenance_page_url + nginx_error_log_level + release_healthcheck_timeout + ssl_protocols_override + ssl_ciphers_override + ) + + vhost_settings.each do |key| + val = options.delete(key) + next if val.nil? + + settings[key.to_s.upcase] = case val + when 'default' + '' + else + val + end + end + + boolean_vhost_settings = %i( + force_ssl + show_elb_healthchecks + strict_health_checks + ) + + boolean_vhost_settings.each do |key| + value = options.delete(key) + next if value.nil? + + settings[key.to_s.upcase] = value.to_s + end + + # This one we pass through to nginx for whatever rason, so + # "on" and "off" are the exected values + ignore_invalid_headers = options.delete(:ignore_invalid_headers) + unless ignore_invalid_headers.nil? + settings['IGNORE_INVALID_HEADERS'] = case ignore_invalid_headers + when true + 'on' + when false + 'off' + end + end + options.delete(:environment) # NOTE: This is here to ensure that specs don't test for options @@ -210,7 +340,7 @@ def prepare(account, options) # this. raise "Unexpected options: #{options}" if options.any? - params.delete_if { |_, v| v.nil? } + [params.delete_if { |_, v| v.nil? }, settings] end FLAGS.each do |f| diff --git a/lib/aptible/cli/renderer/text.rb b/lib/aptible/cli/renderer/text.rb index efa35f57..79e4e2f5 100644 --- a/lib/aptible/cli/renderer/text.rb +++ b/lib/aptible/cli/renderer/text.rb @@ -7,7 +7,9 @@ class Text < Base POST_PROCESSED_KEYS = { 'Tls' => 'TLS', 'Dns' => 'DNS', - 'Ip' => 'IP' + 'Ip' => 'IP', + 'Ssl' => 'SSL', + 'Elb' => 'ELB' }.freeze def visit(node, io) diff --git a/lib/aptible/cli/resource_formatter.rb b/lib/aptible/cli/resource_formatter.rb index c8230a54..72158204 100644 --- a/lib/aptible/cli/resource_formatter.rb +++ b/lib/aptible/cli/resource_formatter.rb @@ -209,6 +209,12 @@ def inject_vhost(node, vhost, service) node.value('internal', vhost.internal) + unless vhost.current_setting.nil? + vhost.current_setting.settings.each do |k, v| + node.value(k.downcase, v) + end + end + ip_whitelist = if vhost.ip_whitelist.any? vhost.ip_whitelist.join(' ') else diff --git a/lib/aptible/cli/subcommands/endpoints.rb b/lib/aptible/cli/subcommands/endpoints.rb index eb57716d..f1578ec5 100644 --- a/lib/aptible/cli/subcommands/endpoints.rb +++ b/lib/aptible/cli/subcommands/endpoints.rb @@ -27,13 +27,18 @@ def self.included(thor) service = database.service raise Thor::Error, 'Database is not provisioned' if service.nil? + prepared_params, settings = database_create_flags.prepare( + database.account, + options + ) + vhost = service.create_vhost!( type: 'tcp', platform: 'elb', - **database_create_flags.prepare(database.account, options) + **prepared_params ) - provision_vhost_and_explain(service, vhost) + provision_vhost_and_explain(service, vhost, settings) end database_modify_flags = Helpers::Vhost::OptionSetBuilder.new do @@ -49,9 +54,14 @@ def self.included(thor) database = ensure_database(options.merge(db: options[:database])) vhost = find_vhost(each_service(database), hostname) - vhost.update!(**database_modify_flags.prepare(database.account, - options)) - provision_vhost_and_explain(vhost.service, vhost) + + prepared_params, settings = database_modify_flags.prepare( + database.account, + options + ) + + vhost.update!(**prepared_params) + provision_vhost_and_explain(vhost.service, vhost, settings) end tcp_create_flags = Helpers::Vhost::OptionSetBuilder.new do @@ -246,18 +256,26 @@ def self.included(thor) no_commands do def create_app_vhost(flags, options, process_type, **attrs) service = ensure_service(options, process_type) + + prepared_params, settings = + flags.prepare(service.account, options) + vhost = service.create_vhost!( - **flags.prepare(service.account, options), + **prepared_params, **attrs ) - provision_vhost_and_explain(service, vhost) + provision_vhost_and_explain(service, vhost, settings) end def modify_app_vhost(flags, options, hostname) app = ensure_app(options) vhost = find_vhost(each_service(app), hostname) - vhost.update!(**flags.prepare(vhost.service.account, options)) - provision_vhost_and_explain(vhost.service, vhost) + + prepared_params, settings = + flags.prepare(vhost.service.account, options) + + vhost.update!(**prepared_params) + provision_vhost_and_explain(vhost.service, vhost, settings) end end end diff --git a/lib/aptible/cli/version.rb b/lib/aptible/cli/version.rb index feaf6c51..a12714ab 100644 --- a/lib/aptible/cli/version.rb +++ b/lib/aptible/cli/version.rb @@ -1,5 +1,5 @@ module Aptible module CLI - VERSION = '0.26.0'.freeze + VERSION = '0.27.0'.freeze end end diff --git a/spec/aptible/cli/resource_formatter_spec.rb b/spec/aptible/cli/resource_formatter_spec.rb index 43a8671d..57b8731f 100644 --- a/spec/aptible/cli/resource_formatter_spec.rb +++ b/spec/aptible/cli/resource_formatter_spec.rb @@ -25,6 +25,9 @@ def capture(m, *args) shared: false ) + vhost.current_configuration = Fabricate(:setting, settings: {}, + vhost: vhost) + expected = [ 'Id: 12', 'Hostname: foo.io', diff --git a/spec/aptible/cli/subcommands/endpoints_spec.rb b/spec/aptible/cli/subcommands/endpoints_spec.rb index 593b6742..a08bdb25 100644 --- a/spec/aptible/cli/subcommands/endpoints_spec.rb +++ b/spec/aptible/cli/subcommands/endpoints_spec.rb @@ -22,12 +22,12 @@ def expect_create_certificate(account, options) end end - def expect_create_vhost(service, options) + def expect_create_vhost(service, options, settings: nil) expect(service).to receive(:create_vhost!).with( hash_including(options) ) do |args| Fabricate(:vhost, service: service, **args).tap do |v| - expect_operation(v, 'provision') + expect_operation(v, 'provision', settings: settings) expect(v).to receive(:reload).and_return(v) expect(Aptible::CLI::ResourceFormatter).to receive(:inject_vhost) .with(an_instance_of(Aptible::CLI::Formatter::Object), v, service) @@ -46,8 +46,16 @@ def expect_modify_vhost(vhost, options) end end - def expect_operation(vhost, type) - expect(vhost).to receive(:create_operation!).with(type: type) do + def expect_operation(vhost, type, settings: nil) + expect(vhost).to receive(:create_operation!) do |args| + expect(args[:type]).to eq(type) + + if settings.nil? + expect(args).not_to have_key(:settings) + else + expect(args[:settings]).to eq(settings) + end + Fabricate(:operation).tap do |o| expect(subject).to receive(:attach_to_operation_logs).with(o) end @@ -161,7 +169,13 @@ def stub_options(**opts) it 'lists Endpoints' do s = Fabricate(:service, database: db) v1 = Fabricate(:vhost, service: s) + v1.current_setting = Fabricate(:setting, + settings: { 'IDLE_TIMEOUT' => '123' }, + vhost: v1) v2 = Fabricate(:vhost, service: s) + v2.current_setting = Fabricate(:setting, + settings: { 'FORCE_SSL' => 'true' }, + vhost: v2) stub_options(database: db.handle) subject.send('endpoints:list') @@ -170,6 +184,8 @@ def stub_options(**opts) expect(lines).to include("Hostname: #{v1.external_host}") expect(lines).to include("Hostname: #{v2.external_host}") + expect(lines).to include('Idle Timeout: 123') + expect(lines).to include('Force SSL: true') expect(lines[0]).not_to eq("\n") expect(lines[-1]).not_to eq("\n") @@ -227,6 +243,87 @@ def stub_options(**opts) stub_options end + shared_examples 'shared create and modify ALB settings examples' do |method| + context 'App Vhost Settings (string)' do + string_options = %i( + client_body_timeout + idle_timeout + maintenance_page_url + nginx_error_log_level + release_healthcheck_timeout + ssl_protocols_override + ) + + let(:value) { 'some value' } + + string_options.each do |option| + context "--#{option.to_s.tr('_', '-')}" do + it 'passes a value if provided' do + wanted = { option.to_s.upcase => value } + expect_create_vhost(service, {}, { settings: wanted }) + stub_options(option => value) + subject.send(method, 'web') + end + + it 'passes nothing if not provided' do + expect_create_vhost(service, {}) + subject.send(method, 'web') + end + + context 'reverting to default' do + it 'sends an empty string if passed an empty string' do + wanted = { option.to_s.upcase => '' } + expect_create_vhost(service, {}, { settings: wanted }) + stub_options(option => '') + subject.send(method, 'web') + end + + it 'sends an empty string if passed the string "default"' do + wanted = { option.to_s.upcase => '' } + expect_create_vhost(service, {}, { settings: wanted }) + stub_options(option => 'default') + subject.send(method, 'web') + end + end + end + end + end + + context 'App Vhost Settings (boolean)' do + boolean_options = %i( + force_ssl + show_elb_healthchecks + strict_health_checks + ) + + boolean_options.each do |option| + [true, false].each do |value| + context "--#{value ? '' : 'no-'}#{option.to_s.tr('_', '-')}" do + it "sets the value to the string '#{value}'" do + wanted = { option.to_s.upcase => value.to_s } + expect_create_vhost(service, {}, { settings: wanted }) + stub_options(option => value) + subject.send(method, 'web') + end + end + end + end + end + + context 'Strange Vhost settings' do + { true => 'on', false => 'off' }.each do |bool, value| + context "--#{bool ? '' : 'no-'}ignore_invalid_headers" do + it "sets the value to the string '#{value}'" do + wanted = { 'IGNORE_INVALID_HEADERS' => value } + expect_create_vhost(service, {}, { settings: wanted }) + stub_options(ignore_invalid_headers: bool) + subject.send(method, 'web') + end + end + end + end + end + shared_examples 'shared create app vhost examples' do |method| context 'App Vhost Options' do it 'fails if the app does not exist' do @@ -468,6 +565,7 @@ def stub_options(**opts) m = 'endpoints:https:create' include_examples 'shared create app vhost examples', m include_examples 'shared create tls vhost examples', m + include_examples 'shared create and modify ALB settings examples', m it 'creates a HTTP Endpoint' do expect_create_vhost( diff --git a/spec/fabricators/setting_fabricator.rb b/spec/fabricators/setting_fabricator.rb new file mode 100644 index 00000000..021131bf --- /dev/null +++ b/spec/fabricators/setting_fabricator.rb @@ -0,0 +1,9 @@ +class StubConfiguration < OpenStruct +end + +Fabricator(:setting, from: :stub_configuration) do + settings { {} } + sensitive_settings { {} } + + after_create { |setting| vhost.settings << setting } +end diff --git a/spec/fabricators/vhost_fabricator.rb b/spec/fabricators/vhost_fabricator.rb index 1b4d6225..04b9ec75 100644 --- a/spec/fabricators/vhost_fabricator.rb +++ b/spec/fabricators/vhost_fabricator.rb @@ -8,6 +8,8 @@ class StubVhost < OpenStruct; end ip_whitelist { [] } container_ports { [] } created_at { Time.now } + settings { [] } + current_setting { nil } after_create { |vhost| vhost.service.vhosts << vhost } end