From 1a65ea224b2b5895b0c73f13f87c386277ea8185 Mon Sep 17 00:00:00 2001 From: "peter, jones (external - Project)" Date: Tue, 25 May 2021 16:19:10 +0100 Subject: [PATCH] Replace unit tests with new suite of unit tests with better coverage --- .rspec | 1 + .rubocop.yml | 37 ++ .ruby-version | 1 + Gemfile | 19 +- Gemfile.lock | 128 ++++-- Guardfile | 7 + README.md | 20 +- Rakefile | 25 +- .../templates/backend-ca-certs_spec.rb | 23 + spec/haproxy/templates/backend-crt_spec.rb | 23 + .../templates/blacklist_cidrs.txt_spec.rb | 60 +++ spec/haproxy/templates/bpm.yml_spec.rb | 114 +++++ spec/haproxy/templates/certs.ttar_spec.rb | 357 +++++++++++++++ spec/haproxy/templates/cidrs.ttar_spec.rb | 39 ++ .../haproxy/templates/client-ca-certs_spec.rb | 23 + .../templates/client-revocation-list_spec.rb | 23 + spec/haproxy/templates/drain_spec.rb | 79 ++++ .../backend_cf_tcp_routers_spec.rb | 56 +++ .../backend_http_routed_spec.rb | 201 +++++++++ .../haproxy_config/backend_http_spec.rb | 254 +++++++++++ .../haproxy_config/backend_tcp_spec.rb | 219 ++++++++++ .../frontend_cf_tcp_routing_spec.rb | 96 +++++ .../haproxy_config/frontend_http_spec.rb | 232 ++++++++++ .../haproxy_config/frontend_https_spec.rb | 358 ++++++++++++++++ .../haproxy_config/frontend_tcp_spec.rb | 163 +++++++ .../haproxy_config/frontend_wss_spec.rb | 353 +++++++++++++++ .../global_and_default_options_spec.rb | 405 ++++++++++++++++++ .../healthcheck_listener_spec.rb | 43 ++ .../haproxy_config/resolvers_spec.rb | 59 +++ .../haproxy_config/stats_listener_spec.rb | 76 ++++ .../templates/ssl_redirect.map_spec.rb | 32 ++ .../trusted_domain_cidrs.txt_spec.rb | 53 +++ .../templates/whitelist_cidrs.txt_spec.rb | 60 +++ spec/haproxy_templates_spec.rb | 177 -------- spec/spec_helper.rb | 79 ++++ 35 files changed, 3657 insertions(+), 238 deletions(-) create mode 100644 .rspec create mode 100644 .rubocop.yml create mode 100644 .ruby-version create mode 100644 Guardfile create mode 100644 spec/haproxy/templates/backend-ca-certs_spec.rb create mode 100644 spec/haproxy/templates/backend-crt_spec.rb create mode 100644 spec/haproxy/templates/blacklist_cidrs.txt_spec.rb create mode 100644 spec/haproxy/templates/bpm.yml_spec.rb create mode 100644 spec/haproxy/templates/certs.ttar_spec.rb create mode 100644 spec/haproxy/templates/cidrs.ttar_spec.rb create mode 100644 spec/haproxy/templates/client-ca-certs_spec.rb create mode 100644 spec/haproxy/templates/client-revocation-list_spec.rb create mode 100644 spec/haproxy/templates/drain_spec.rb create mode 100644 spec/haproxy/templates/haproxy_config/backend_cf_tcp_routers_spec.rb create mode 100644 spec/haproxy/templates/haproxy_config/backend_http_routed_spec.rb create mode 100644 spec/haproxy/templates/haproxy_config/backend_http_spec.rb create mode 100644 spec/haproxy/templates/haproxy_config/backend_tcp_spec.rb create mode 100644 spec/haproxy/templates/haproxy_config/frontend_cf_tcp_routing_spec.rb create mode 100644 spec/haproxy/templates/haproxy_config/frontend_http_spec.rb create mode 100644 spec/haproxy/templates/haproxy_config/frontend_https_spec.rb create mode 100644 spec/haproxy/templates/haproxy_config/frontend_tcp_spec.rb create mode 100644 spec/haproxy/templates/haproxy_config/frontend_wss_spec.rb create mode 100644 spec/haproxy/templates/haproxy_config/global_and_default_options_spec.rb create mode 100644 spec/haproxy/templates/haproxy_config/healthcheck_listener_spec.rb create mode 100644 spec/haproxy/templates/haproxy_config/resolvers_spec.rb create mode 100644 spec/haproxy/templates/haproxy_config/stats_listener_spec.rb create mode 100644 spec/haproxy/templates/ssl_redirect.map_spec.rb create mode 100644 spec/haproxy/templates/trusted_domain_cidrs.txt_spec.rb create mode 100644 spec/haproxy/templates/whitelist_cidrs.txt_spec.rb delete mode 100644 spec/haproxy_templates_spec.rb create mode 100644 spec/spec_helper.rb diff --git a/.rspec b/.rspec new file mode 100644 index 00000000..c99d2e73 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..6e9beefa --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,37 @@ +require: + - rubocop-rspec + - rubocop-rake + +AllCops: + NewCops: enable + +Layout/ArgumentAlignment: + Enabled: true + EnforcedStyle: with_fixed_indentation + +Layout/HeredocArgumentClosingParenthesis: + Enabled: true + +Metrics/BlockLength: + Enabled: false + +Layout/LineLength: + Enabled: false + +Naming/FileName: + Enabled: false + +RSpec/MultipleMemoizedHelpers: + Enabled: false + +RSpec/NestedGroups: + Max: 4 + +RSpec/MultipleExpectations: + Enabled: false + +RSpec/ExampleLength: + Enabled: false + +RSpec/DescribeClass: + Enabled: false diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 00000000..cb2b00e4 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.0.1 diff --git a/Gemfile b/Gemfile index 112f6a5e..a27af70c 100644 --- a/Gemfile +++ b/Gemfile @@ -1,10 +1,13 @@ +# frozen_string_literal: true + source 'https://rubygems.org' -group :test do - gem 'bosh-template' - gem 'rspec', '~> 3.0' - gem 'haproxy-tools' - gem 'pry' -end -gem 'rubocop', '~> 0.49.0' -gem 'semi_semantic', '~> 1.2' +gem 'bosh-template' +gem 'deep_merge' +gem 'guard-rspec' +gem 'haproxy-tools' +gem 'rake' +gem 'rspec' +gem 'rubocop' +gem 'rubocop-rake' +gem 'rubocop-rspec' diff --git a/Gemfile.lock b/Gemfile.lock index 1f09dde1..eecf7b80 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,65 +1,107 @@ GEM remote: https://rubygems.org/ specs: - ast (2.4.0) - bosh-template (2.2.0) + ast (2.4.2) + bosh-template (2.2.1) semi_semantic (~> 1.2.0) - coderay (1.1.2) - diff-lcs (1.3) + coderay (1.1.3) + deep_merge (1.2.1) + diff-lcs (1.4.4) + ffi (1.15.1) + formatador (0.2.5) + guard (2.17.0) + formatador (>= 0.2.4) + listen (>= 2.7, < 4.0) + lumberjack (>= 1.0.12, < 2.0) + nenv (~> 0.1) + notiffany (~> 0.0) + pry (>= 0.9.12) + shellany (~> 0.0) + thor (>= 0.18.1) + guard-compat (1.2.1) + guard-rspec (4.7.3) + guard (~> 2.1) + guard-compat (~> 1.1) + rspec (>= 2.99.0, < 4.0) haproxy-tools (0.6.0) net-scp treetop - method_source (0.9.2) - net-scp (2.0.0) - net-ssh (>= 2.6.5, < 6.0.0) - net-ssh (5.2.0) - parallel (1.17.0) - parser (2.6.3.0) - ast (~> 2.4.0) + listen (3.5.1) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + lumberjack (1.2.8) + method_source (1.0.0) + nenv (0.3.0) + net-scp (3.0.0) + net-ssh (>= 2.6.5, < 7.0.0) + net-ssh (6.1.0) + notiffany (0.1.3) + nenv (~> 0.1) + shellany (~> 0.0) + parallel (1.20.1) + parser (3.0.1.1) + ast (~> 2.4.1) polyglot (0.3.5) - powerpack (0.1.2) - pry (0.12.2) - coderay (~> 1.1.0) - method_source (~> 0.9.0) - rainbow (2.2.2) - rake - rake (12.3.3) - rspec (3.8.0) - rspec-core (~> 3.8.0) - rspec-expectations (~> 3.8.0) - rspec-mocks (~> 3.8.0) - rspec-core (3.8.0) - rspec-support (~> 3.8.0) - rspec-expectations (3.8.3) + pry (0.14.1) + coderay (~> 1.1) + method_source (~> 1.0) + rainbow (3.0.0) + rake (13.0.3) + rb-fsevent (0.11.0) + rb-inotify (0.10.1) + ffi (~> 1.0) + regexp_parser (2.1.1) + rexml (3.2.5) + rspec (3.10.0) + rspec-core (~> 3.10.0) + rspec-expectations (~> 3.10.0) + rspec-mocks (~> 3.10.0) + rspec-core (3.10.1) + rspec-support (~> 3.10.0) + rspec-expectations (3.10.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.8.0) - rspec-mocks (3.8.0) + rspec-support (~> 3.10.0) + rspec-mocks (3.10.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.8.0) - rspec-support (3.8.0) - rubocop (0.49.1) + rspec-support (~> 3.10.0) + rspec-support (3.10.2) + rubocop (1.15.0) parallel (~> 1.10) - parser (>= 2.3.3.1, < 3.0) - powerpack (~> 0.1) - rainbow (>= 1.99.1, < 3.0) + parser (>= 3.0.0.0) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml + rubocop-ast (>= 1.5.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (~> 1.0, >= 1.0.1) - ruby-progressbar (1.10.0) + unicode-display_width (>= 1.4.0, < 3.0) + rubocop-ast (1.5.0) + parser (>= 3.0.1.1) + rubocop-rake (0.5.1) + rubocop + rubocop-rspec (2.3.0) + rubocop (~> 1.0) + rubocop-ast (>= 1.1.0) + ruby-progressbar (1.11.0) semi_semantic (1.2.0) - treetop (1.6.10) + shellany (0.0.1) + thor (1.1.0) + treetop (1.6.11) polyglot (~> 0.3) - unicode-display_width (1.6.0) + unicode-display_width (2.0.0) PLATFORMS - ruby + x86_64-darwin-20 DEPENDENCIES bosh-template + deep_merge + guard-rspec haproxy-tools - pry - rspec (~> 3.0) - rubocop (~> 0.49.0) - semi_semantic (~> 1.2) + rake + rspec + rubocop + rubocop-rake + rubocop-rspec BUNDLED WITH - 2.0.1 + 2.2.15 diff --git a/Guardfile b/Guardfile new file mode 100644 index 00000000..5e1a9984 --- /dev/null +++ b/Guardfile @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +guard :rspec, cmd: 'rspec' do + watch(%r{jobs/(.*)/(.*)/(.*)\.erb$}) { |m| "spec/#{m[1]}/#{m[2]}/#{m[3]}_spec.rb" } + + watch(%r{spec/(.*)$}) { |m| "spec/#{m[1]}" } +end diff --git a/README.md b/README.md index 8a5b90e2..55d4a050 100644 --- a/README.md +++ b/README.md @@ -52,13 +52,27 @@ KEEPALIVED_VIP=10.244.50.2 templates/make_manifest warden 10.244.0.22 Feel free to contribute back to this via a pull request on a feature branch! Once merged, we'll cut a new final release for you. -### Config Tests +### Unit Tests and Linting -If you add a spec value, please add a corresponding test to `spec/haproxy_templates_spec.rb`. +If you change any erb logic in the jobs directory please add a corresponding test to `spec`. To run these tests: ``` cd haproxy_boshrelease bundle install -bundle exec rspec spec/haproxy_templates_spec.rb +bundle exec rake spec +``` + +To run lint with rubocop +``` +cd haproxy_boshrelease +bundle install +bundle exec rake lint +``` + +To watch the tests while developing +``` +cd haproxy_boshrelease +bundle install +bundle exec guard ``` diff --git a/Rakefile b/Rakefile index 3ac8caff..62a0c670 100644 --- a/Rakefile +++ b/Rakefile @@ -1,15 +1,26 @@ -desc "Generates a properties file for each job based on properties.X.Y used in templates" +# frozen_string_literal: true + +require 'rspec/core/rake_task' +require 'rubocop/rake_task' + +RSpec::Core::RakeTask.new(:spec) +task default: :spec + +desc 'Lint using RuboCop' +RuboCop::RakeTask.new(:lint) + +desc 'Generates a properties file for each job based on properties.X.Y used in templates' task :job_properties do - require "fileutils" - Dir["jobs/*"].each do |path| + require 'fileutils' + Dir['jobs/*'].each do |path| puts "Searching job #{File.basename(path)}..." FileUtils.chdir(path) do properties = [] - Dir["templates/*.erb"].each do |template_path| - properties |= File.read(template_path).scan(/\bproperties\.[\w\.]*\b/) + Dir['templates/*.erb'].each do |template_path| + properties |= File.read(template_path).scan(/\bproperties\.[\w.]*\b/) puts properties.join("\n") - File.open("properties", "w") { |file| file << properties.join("\n") } + File.open('properties', 'w') { |file| file << properties.join("\n") } end end end -end \ No newline at end of file +end diff --git a/spec/haproxy/templates/backend-ca-certs_spec.rb b/spec/haproxy/templates/backend-ca-certs_spec.rb new file mode 100644 index 00000000..765ed9b0 --- /dev/null +++ b/spec/haproxy/templates/backend-ca-certs_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rspec' + +describe 'config/backend-ca-certs.pem' do + let(:template) { haproxy_job.template('config/backend-ca-certs.pem') } + + describe 'ha_proxy.backend_ca_file' do + it 'has the correct contents' do + expect(template.render({ + 'ha_proxy' => { + 'backend_ca_file' => 'foobarbaz' + } + })).to eq("\nfoobarbaz\n\n") + end + + context 'when ha_proxy.backend_ca_file is not provided' do + it 'is empty' do + expect(template.render({})).to be_a_blank_string + end + end + end +end diff --git a/spec/haproxy/templates/backend-crt_spec.rb b/spec/haproxy/templates/backend-crt_spec.rb new file mode 100644 index 00000000..dacca3cb --- /dev/null +++ b/spec/haproxy/templates/backend-crt_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rspec' + +describe 'config/backend-crt.pem' do + let(:template) { haproxy_job.template('config/backend-crt.pem') } + + describe 'ha_proxy.backend_crt' do + it 'has the correct contents' do + expect(template.render({ + 'ha_proxy' => { + 'backend_crt' => 'foobarbaz' + } + })).to eq("\nfoobarbaz\n\n") + end + + context 'when ha_proxy.backend_crt is not provided' do + it 'is empty' do + expect(template.render({})).to be_a_blank_string + end + end + end +end diff --git a/spec/haproxy/templates/blacklist_cidrs.txt_spec.rb b/spec/haproxy/templates/blacklist_cidrs.txt_spec.rb new file mode 100644 index 00000000..116fd0a3 --- /dev/null +++ b/spec/haproxy/templates/blacklist_cidrs.txt_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'rspec' + +describe 'config/blacklist_cidrs.txt' do + let(:template) { haproxy_job.template('config/blacklist_cidrs.txt') } + + context 'when ha_proxy.cidr_blacklist is provided' do + context 'when an array of cidrs is provided' do + it 'has the correct contents' do + expect(template.render({ + 'ha_proxy' => { + 'cidr_blacklist' => [ + '10.0.0.0/8', + '192.168.2.0/24' + ] + } + })).to eq(<<~EXPECTED) + # generated from blacklist_cidrs.txt.erb + + # BEGIN blacklist cidrs + # detected cidrs provided as array in cleartext format + 10.0.0.0/8 + 192.168.2.0/24 + + # END blacklist cidrs + + EXPECTED + end + end + + context 'when a base64-encoded, gzipped config is provided' do + it 'has the correct contents' do + expect(template.render({ + 'ha_proxy' => { + 'cidr_blacklist' => gzip_and_b64_encode(<<~INPUT) + 10.0.0.0/8 + 192.168.2.0/24 + INPUT + } + })).to eq(<<~EXPECTED) + # generated from blacklist_cidrs.txt.erb + + # BEGIN blacklist cidrs + 10.0.0.0/8 + 192.168.2.0/24 + + # END blacklist cidrs + + EXPECTED + end + end + end + + context 'when ha_proxy.cidr_blacklist is not provided' do + it 'is empty' do + expect(template.render({})).to be_a_blank_string + end + end +end diff --git a/spec/haproxy/templates/bpm.yml_spec.rb b/spec/haproxy/templates/bpm.yml_spec.rb new file mode 100644 index 00000000..58e65299 --- /dev/null +++ b/spec/haproxy/templates/bpm.yml_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require 'rspec' +require 'bosh/template/test' +require 'json' +require 'yaml' + +describe 'config/bpm.yml' do + let(:template) { haproxy_job.template('config/bpm.yml') } + + it 'has the correct contents' do + bpm_yaml = template.render({ + 'ha_proxy' => { + 'max_open_files' => 123 + } + }) + + expect(bpm_yaml).to eq(<<~EXPECTED) + processes: + - name: haproxy + executable: /var/vcap/jobs/haproxy/bin/haproxy_wrapper + additional_volumes: + - path: /var/vcap/jobs/haproxy/config/cidrs + writable: true + - path: /var/vcap/jobs/haproxy/config/ssl + writable: true + - path: /var/vcap/sys/run/haproxy + writable: true + + unsafe: + unrestricted_volumes: [] + + limits: + open_files: 123 + capabilities: + - NET_BIND_SERVICE + EXPECTED + end + + context 'when ha_proxy.syslog_server with a path is provided' do + it 'grants BPM access to the syslog server path' do + bpm_yaml = template.render({ + 'ha_proxy' => { + 'max_open_files' => 123, + 'syslog_server' => '/syslog/server' + } + }) + + expect(bpm_yaml).to eq(<<~EXPECTED) + processes: + - name: haproxy + executable: /var/vcap/jobs/haproxy/bin/haproxy_wrapper + additional_volumes: + - path: /var/vcap/jobs/haproxy/config/cidrs + writable: true + - path: /var/vcap/jobs/haproxy/config/ssl + writable: true + - path: /var/vcap/sys/run/haproxy + writable: true + + unsafe: + unrestricted_volumes: [{"path":"/syslog/server"}] + + limits: + open_files: 123 + capabilities: + - NET_BIND_SERVICE + EXPECTED + end + end + + context 'when ha_proxy.additional_unrestricted_volumes are provided' do + it 'grants BPM access to the volumes' do + bpm_yaml = template.render({ + 'ha_proxy' => { + 'max_open_files' => 123, + 'additional_unrestricted_volumes' => [ + # See following for format + # https://github.com/cloudfoundry/bpm-release/blob/master/docs/config.md + { + 'path' => '/my-volume', + 'writeable' => false + }, + { + 'path' => '/my-volume', + 'mount_only' => true + } + ] + } + }) + + expect(bpm_yaml).to eq(<<~EXPECTED) + processes: + - name: haproxy + executable: /var/vcap/jobs/haproxy/bin/haproxy_wrapper + additional_volumes: + - path: /var/vcap/jobs/haproxy/config/cidrs + writable: true + - path: /var/vcap/jobs/haproxy/config/ssl + writable: true + - path: /var/vcap/sys/run/haproxy + writable: true + + unsafe: + unrestricted_volumes: [{"path":"/my-volume","writeable":false},{"path":"/my-volume","mount_only":true}] + + limits: + open_files: 123 + capabilities: + - NET_BIND_SERVICE + EXPECTED + end + end +end diff --git a/spec/haproxy/templates/certs.ttar_spec.rb b/spec/haproxy/templates/certs.ttar_spec.rb new file mode 100644 index 00000000..8c498e7a --- /dev/null +++ b/spec/haproxy/templates/certs.ttar_spec.rb @@ -0,0 +1,357 @@ +# frozen_string_literal: true + +require 'rspec' + +describe 'config/certs.ttar' do + let(:template) { haproxy_job.template('config/certs.ttar') } + + describe 'ha_proxy.ssl_pem' do + let(:ttar) do + template.render({ + 'ha_proxy' => { + 'ssl_pem' => ssl_pem + } + }) + end + + context 'when ssl_pem is an array of objects' do + let(:ssl_pem) do + [{ + 'cert_chain' => 'cert_chain 0 contents', + 'private_key' => 'private_key 0 contents' + }, { + 'cert_chain' => 'cert_chain 1 contents', + 'private_key' => 'private_key 1 contents' + }] + end + + it 'has the correct contents' do + expect(ttar_entry(ttar, '/var/vcap/jobs/haproxy/config/ssl/cert-0.pem')).to eq(<<~EXPECTED) + + cert_chain 0 contents + private_key 0 contents + + EXPECTED + + expect(ttar_entry(ttar, '/var/vcap/jobs/haproxy/config/ssl/cert-1.pem')).to eq(<<~EXPECTED) + + cert_chain 1 contents + private_key 1 contents + + + EXPECTED + end + end + + context 'when ssl_pem is provided as an array of strings' do + let(:ssl_pem) do + [ + 'cert 0 contents', + 'cert 1 contents' + ] + end + + it 'has the correct contents' do + expect(ttar_entry(ttar, '/var/vcap/jobs/haproxy/config/ssl/cert-0.pem')).to eq(<<~EXPECTED) + + cert 0 contents + + EXPECTED + + expect(ttar_entry(ttar, '/var/vcap/jobs/haproxy/config/ssl/cert-1.pem')).to eq(<<~EXPECTED) + + cert 1 contents + + + EXPECTED + end + end + + context 'when ssl_pem is provided as a string' do + let(:ssl_pem) { 'cert 0 contents' } + + it 'has the correct contents' do + expect(ttar_entry(ttar, '/var/vcap/jobs/haproxy/config/ssl/cert-0.pem').strip).to eq('cert 0 contents') + end + end + end + + describe 'ha_proxy.crt_list' do + describe 'ha_proxy.crt_list[].ssl_pem' do + let(:ttar) do + template.render({ + 'ha_proxy' => { + 'crt_list' => [{ + 'ssl_pem' => ssl_pem + }] + } + }) + end + + context 'when ssl_pem is a string' do + let(:ssl_pem) { 'cert 0 contents' } + + it 'has the correct contents' do + expect(ttar_entry(ttar, '/var/vcap/jobs/haproxy/config/ssl/cert-0.pem')).to eq(<<~EXPECTED) + + cert 0 contents + + EXPECTED + end + + it 'is referenced in the crt-list' do + expect(ttar_entry(ttar, '/var/vcap/jobs/haproxy/config/ssl/crt-list')).to eq(<<~EXPECTED) + + /var/vcap/jobs/haproxy/config/ssl/cert-0.pem + + + EXPECTED + end + end + + context 'when ssl_pem is an array' do + let(:ssl_pem) do + { + 'cert_chain' => 'cert_chain 0 contents', + 'private_key' => 'private_key 0 contents' + } + end + + it 'has the correct contents' do + expect(ttar_entry(ttar, '/var/vcap/jobs/haproxy/config/ssl/cert-0.pem')).to eq(<<~EXPECTED) + + cert_chain 0 contents + private_key 0 contents + + EXPECTED + end + + it 'is referenced in the crt-list' do + expect(ttar_entry(ttar, '/var/vcap/jobs/haproxy/config/ssl/crt-list')).to eq(<<~EXPECTED) + + /var/vcap/jobs/haproxy/config/ssl/cert-0.pem + + + EXPECTED + end + end + end + + describe 'ha_proxy.crt_list[].client_ca_file' do + let(:ttar) do + template.render({ + 'ha_proxy' => { + 'crt_list' => [{ + 'ssl_pem' => 'ssl_pem contents', + 'client_ca_file' => 'client_ca_file contents' + }] + } + }) + end + + it 'references the client ca file in the crt-list' do + expect(ttar_entry(ttar, '/var/vcap/jobs/haproxy/config/ssl/crt-list')).to eq(<<~EXPECTED) + + /var/vcap/jobs/haproxy/config/ssl/cert-0.pem [ca-file /var/vcap/jobs/haproxy/config/ssl/ca-file-0.pem] + + + EXPECTED + end + + it 'has the correct ca file contents' do + expect(ttar_entry(ttar, '/var/vcap/jobs/haproxy/config/ssl/ca-file-0.pem')).to eq(<<~EXPECTED) + + client_ca_file contents + + EXPECTED + end + + context 'when ha_proxy.client_ca_file is also configured globally' do + let(:ttar) do + template.render({ + 'ha_proxy' => { + 'crt_list' => [{ + 'client_ca_file' => 'client_ca_file contents' + }], + 'client_ca_file' => 'client_ca_file contents' + } + }) + end + + it 'aborts with a meaningful error message' do + expect do + ttar + end.to raise_error /Conflicting configuration. Please configure 'client_ca_file' either globally OR in 'crt_list' entries, but not both/ + end + end + end + + describe 'ha_proxycrt_list[].client_revocation_list' do + let(:ttar) do + template.render({ + 'ha_proxy' => { + 'crt_list' => [{ + 'ssl_pem' => 'ssl_pem contents', + 'client_revocation_list' => 'client_revocation_list contents' + }] + } + }) + end + + it 'references the revocation list in the crt-list' do + expect(ttar_entry(ttar, '/var/vcap/jobs/haproxy/config/ssl/crt-list')).to eq(<<~EXPECTED) + + /var/vcap/jobs/haproxy/config/ssl/cert-0.pem [crl-file /var/vcap/jobs/haproxy/config/ssl/crl-file-0.pem] + + + EXPECTED + end + + it 'has the correct crl file contents' do + expect(ttar_entry(ttar, '/var/vcap/jobs/haproxy/config/ssl/crl-file-0.pem')).to eq(<<~EXPECTED) + + client_revocation_list contents + + EXPECTED + end + + context 'when ha_proxy.client_revocation_list is also configured globally' do + let(:ttar) do + template.render({ + 'ha_proxy' => { + 'crt_list' => [{ + 'client_revocation_list' => 'client_revocation_list contents' + }], + 'client_revocation_list' => 'client_revocation_list contents' + } + }) + end + + it 'aborts with a meaningful error message' do + expect do + ttar + end.to raise_error /Conflicting configuration. Please configure 'client_revocation_list' either globally OR in 'crt_list' entries, but not both/ + end + end + end + + describe 'ha_proxy.crt_list[].verify' do + let(:ttar) do + template.render({ + 'ha_proxy' => { + 'crt_list' => [{ + 'verify' => 'required', + 'ssl_pem' => 'ssl_pem contents' + }] + } + }) + end + + it 'is included in the crt list' do + expect(ttar_entry(ttar, '/var/vcap/jobs/haproxy/config/ssl/crt-list')).to eq(<<~EXPECTED) + + /var/vcap/jobs/haproxy/config/ssl/cert-0.pem [verify required] + + + EXPECTED + end + end + + describe 'ha_proxy.crt_list[].snifilter' do + let(:ttar) do + template.render({ + 'ha_proxy' => { + 'crt_list' => [{ + 'snifilter' => snifilter, + 'ssl_pem' => 'ssl_pem contents' + }] + } + }) + end + + context 'when snilter is an array' do + let(:snifilter) do + [ + '*.domain.tld', + '!secure.domain.tld' + ] + end + + it 'is included in the crt list' do + expect(ttar_entry(ttar, '/var/vcap/jobs/haproxy/config/ssl/crt-list')).to eq(<<~EXPECTED) + + /var/vcap/jobs/haproxy/config/ssl/cert-0.pem *.domain.tld !secure.domain.tld + + + EXPECTED + end + end + + context 'when snilter is a string' do + let(:snifilter) { '*.domain.tld' } + + it 'is included in the crt list' do + expect(ttar_entry(ttar, '/var/vcap/jobs/haproxy/config/ssl/crt-list')).to eq(<<~EXPECTED) + + /var/vcap/jobs/haproxy/config/ssl/cert-0.pem *.domain.tld + + + EXPECTED + end + end + end + + describe 'ha_proxy.crt_list[].ssl_ciphers' do + let(:ttar) do + template.render({ + 'ha_proxy' => { + 'crt_list' => [{ + 'ssl_ciphers' => 'AES:ALL:!aNULL:!eNULL:+RC4:@STRENGTH', + 'ssl_pem' => 'ssl_pem contents' + }] + } + }) + end + + it 'is included in the crt list' do + expect(ttar_entry(ttar, '/var/vcap/jobs/haproxy/config/ssl/crt-list')).to eq(<<~EXPECTED) + + /var/vcap/jobs/haproxy/config/ssl/cert-0.pem [ciphers AES:ALL:!aNULL:!eNULL:+RC4:@STRENGTH] + + + EXPECTED + end + end + end + + describe 'ha_proxy.ext_crt_list' do + let(:ttar) do + template.render({ + 'ha_proxy' => { + 'crt_list' => [{ + 'ssl_pem' => 'ssl_pem contents' + }], + 'ext_crt_list' => true + } + }) + end + + # FIXME: is there a nicer way than using sed for this? + it 'is referenced in the crt list' do + expect(ttar_entry(ttar, '/var/vcap/jobs/haproxy/config/ssl/crt-list')).to eq(<<~EXPECTED) + + /var/vcap/jobs/haproxy/config/ssl/cert-0.pem + + + #OPTIONAL_EXT_CERTS + + EXPECTED + end + end + + context 'when no certificate-related properties are provided' do + it 'is empty' do + expect(template.render({})).to be_a_blank_string + end + end +end diff --git a/spec/haproxy/templates/cidrs.ttar_spec.rb b/spec/haproxy/templates/cidrs.ttar_spec.rb new file mode 100644 index 00000000..a6511cf4 --- /dev/null +++ b/spec/haproxy/templates/cidrs.ttar_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'rspec' + +describe 'config/cidrs.ttar' do + let(:template) { haproxy_job.template('config/cidrs.ttar') } + + describe 'ha_proxy.cidrs_in_file' do + let(:ttar) do + template.render({ + 'ha_proxy' => { + 'cidrs_in_file' => [{ + 'cidrs' => [ + '5.22.1.3', + '5.22.12.3' + ], + 'name' => 'sample_cidrs' + }] + } + }) + end + + it 'has the correct contents' do + expect(ttar_entry(ttar, '/var/vcap/jobs/haproxy/config/cidrs/sample_cidrs')).to eq(<<~EXPECTED) + + # generated by cidrs.ttar.erb + 5.22.1.3 + 5.22.12.3 + + EXPECTED + end + + context 'when ha_proxy.cidrs_in_file is not provided' do + it 'is empty' do + expect(template.render({})).to be_a_blank_string + end + end + end +end diff --git a/spec/haproxy/templates/client-ca-certs_spec.rb b/spec/haproxy/templates/client-ca-certs_spec.rb new file mode 100644 index 00000000..85caaae4 --- /dev/null +++ b/spec/haproxy/templates/client-ca-certs_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rspec' + +describe 'config/client-ca-certs.pem' do + let(:template) { haproxy_job.template('config/client-ca-certs.pem') } + + describe 'ha_proxy.client_ca_file' do + it 'has the correct contents' do + expect(template.render({ + 'ha_proxy' => { + 'client_ca_file' => 'foobarbaz' + } + })).to eq("\nfoobarbaz\n\n") + end + + context 'when ha_proxy.client_ca_file is not provided' do + it 'is empty' do + expect(template.render({})).to be_a_blank_string + end + end + end +end diff --git a/spec/haproxy/templates/client-revocation-list_spec.rb b/spec/haproxy/templates/client-revocation-list_spec.rb new file mode 100644 index 00000000..3c336c83 --- /dev/null +++ b/spec/haproxy/templates/client-revocation-list_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rspec' + +describe 'config/client-revocation-list.pem' do + let(:template) { haproxy_job.template('config/client-revocation-list.pem') } + + describe 'ha_proxy.client_revocation_list' do + it 'has the correct contents' do + expect(template.render({ + 'ha_proxy' => { + 'client_revocation_list' => 'foobarbaz' + } + })).to eq("\nfoobarbaz\n\n") + end + + context 'when ha_proxy.client_revocation_list is not provided' do + it 'is empty' do + expect(template.render({})).to be_a_blank_string + end + end + end +end diff --git a/spec/haproxy/templates/drain_spec.rb b/spec/haproxy/templates/drain_spec.rb new file mode 100644 index 00000000..c4d5e0f7 --- /dev/null +++ b/spec/haproxy/templates/drain_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require 'rspec' + +describe 'bin/drain' do + let(:template) { haproxy_job.template('bin/drain') } + + describe 'ha_proxy.drain_enable' do + context 'when enabled' do + it 'includes drain logic' do + expect(template.render({ + 'ha_proxy' => { + 'drain_enable' => true + } + })).to eq(<<~EXPECTED) + #!/bin/bash + # vim: set ft=sh + + set -e + + pidfile=/var/vcap/sys/run/bpm/haproxy/haproxy.pid + logfile=/var/vcap/sys/log/haproxy/drain.log + + mkdir -p "$(dirname ${logfile})" + + if [[ ! -f ${pidfile} ]]; then + echo "$(date): pidfile does not exist" >> ${logfile} + echo 0 + exit 0 + fi + + pid="$(cat ${pidfile})" + + haproxy_pids=$(pgrep -P $pid -l | grep 'haproxy$' | awk '{print $1}') + + for haproxy_pid in $haproxy_pids; do + kill -USR1 "${haproxy_pid}" + echo "$(date): triggering drain for process ${haproxy_pid}" >> ${logfile} + done + + echo 30 + EXPECTED + end + + context 'when a custom ha_proxy.drain_timeout is provided' do + it 'overrides the default timeout' do + expect(template.render({ + 'ha_proxy' => { + 'drain_enable' => true, + 'drain_timeout' => 123 + } + }).split(/\n/).last).to eq('echo 123') + end + end + end + + context 'when disabled' do + it 'does not include drain logic' do + expect(template.render({ + 'ha_proxy' => { + 'drain_enable' => false + } + })).to eq(<<~EXPECTED) + #!/bin/bash + # vim: set ft=sh + + set -e + + pidfile=/var/vcap/sys/run/bpm/haproxy/haproxy.pid + logfile=/var/vcap/sys/log/haproxy/drain.log + + echo "drain is disabled" >> ${logfile} + echo 0 + exit 0 + EXPECTED + end + end + end +end diff --git a/spec/haproxy/templates/haproxy_config/backend_cf_tcp_routers_spec.rb b/spec/haproxy/templates/haproxy_config/backend_cf_tcp_routers_spec.rb new file mode 100644 index 00000000..3c86ea41 --- /dev/null +++ b/spec/haproxy/templates/haproxy_config/backend_cf_tcp_routers_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'rspec' + +describe 'config/haproxy.config backend cf_tcp_routers' do + let(:template) { haproxy_job.template('config/haproxy.config') } + + let(:tcp_router_link) do + Bosh::Template::Test::Link.new( + name: 'tcp_router', + instances: [Bosh::Template::Test::LinkInstance.new(address: 'tcp.cf.com')] + ) + end + + let(:haproxy_conf) do + parse_haproxy_config(template.render({ 'ha_proxy' => properties }, consumes: [tcp_router_link])) + end + + let(:backend_cf_tcp_routers) { haproxy_conf['backend cf_tcp_routers'] } + + let(:properties) { {} } + + it 'has the correct mode' do + expect(backend_cf_tcp_routers).to include('mode tcp') + end + + it 'has a healthcheck' do + expect(backend_cf_tcp_routers).to include('option httpchk GET /health') + end + + context 'when a custom ha_proxy.tcp_backend_config is provided' do + let(:properties) do + { + 'tcp_backend_config' => 'custom backend config' + } + end + + it 'is included in the backend configuration' do + expect(backend_cf_tcp_routers).to include('custom backend config') + end + end + + it 'has the correct servers' do + expect(backend_cf_tcp_routers).to include('server node0 tcp.cf.com check port 80 inter 1000') + end + + context 'when no tcp_router link is provided' do + let(:haproxy_conf) do + parse_haproxy_config(template.render(properties)) + end + + it 'is not included' do + expect(haproxy_conf).not_to have_key('backend cf_tcp_routers') + end + end +end diff --git a/spec/haproxy/templates/haproxy_config/backend_http_routed_spec.rb b/spec/haproxy/templates/haproxy_config/backend_http_routed_spec.rb new file mode 100644 index 00000000..92a0a521 --- /dev/null +++ b/spec/haproxy/templates/haproxy_config/backend_http_routed_spec.rb @@ -0,0 +1,201 @@ +# frozen_string_literal: true + +require 'rspec' + +describe 'config/haproxy.config backend http-routed-backend-X' do + let(:template) { haproxy_job.template('config/haproxy.config') } + + let(:haproxy_conf) do + parse_haproxy_config(template.render({ 'ha_proxy' => properties })) + end + + let(:default_properties) do + { + 'routed_backend_servers' => { + '/images' => { + 'servers' => ['10.0.0.2', '10.0.0.3'], + 'port' => '443' + }, + '/auth' => { + 'servers' => ['10.0.0.8', '10.0.0.9'], + 'port' => '8080' + } + } + } + end + + let(:properties) { default_properties } + + let(:backend_images) { haproxy_conf['backend http-routed-backend-9c1bb7'] } + let(:backend_auth) { haproxy_conf['backend http-routed-backend-7d2f30'] } + + it 'has the correct mode' do + expect(backend_images).to include('mode http') + expect(backend_auth).to include('mode http') + end + + it 'uses round-robin load balancing' do + expect(backend_images).to include('balance roundrobin') + expect(backend_auth).to include('balance roundrobin') + end + + context 'when ha_proxy.compress_types are provided' do + let(:properties) do + default_properties.deep_merge({ 'compress_types' => 'text/html text/plain text/css' }) + end + + it 'configures the compression type and algorithm' do + expect(backend_images).to include('compression algo gzip') + expect(backend_images).to include('compression type text/html text/plain text/css') + + expect(backend_auth).to include('compression algo gzip') + expect(backend_auth).to include('compression type text/html text/plain text/css') + end + end + + it 'configures the backend servers' do + expect(backend_images).to include('server node0 10.0.0.2:443 check inter 1000') + expect(backend_images).to include('server node1 10.0.0.3:443 check inter 1000') + expect(backend_auth).to include('server node0 10.0.0.8:8080 check inter 1000') + expect(backend_auth).to include('server node1 10.0.0.9:8080 check inter 1000') + end + + context 'when ha_proxy.resolvers are provided' do + let(:properties) do + default_properties.deep_merge({ 'resolvers' => [{ 'public' => '1.1.1.1' }] }) + end + + it 'sets the resolver on the server configuration' do + expect(backend_images).to include('server node0 10.0.0.2:443 resolvers default check inter 1000') + expect(backend_images).to include('server node1 10.0.0.3:443 resolvers default check inter 1000') + expect(backend_auth).to include('server node0 10.0.0.8:8080 resolvers default check inter 1000') + expect(backend_auth).to include('server node1 10.0.0.9:8080 resolvers default check inter 1000') + end + end + + context 'when backend_use_http_health is true' do + let(:properties) do + default_properties.deep_merge({ + 'routed_backend_servers' => { + '/images' => { + 'backend_use_http_health' => true + } + } + }) + end + + it 'configures the healthcheck' do + expect(backend_images).to include('option httpchk GET /health') + end + + it 'uses the backend port for the healthcheck' do + expect(backend_images).to include('server node0 10.0.0.2:443 check inter 1000 port 443') + expect(backend_images).to include('server node1 10.0.0.3:443 check inter 1000 port 443') + end + + context 'when backend_http_health_port is provided' do + let(:properties) do + default_properties.deep_merge({ + 'routed_backend_servers' => { + '/images' => { + 'backend_use_http_health' => true, + 'backend_http_health_port' => 9999 + } + } + }) + end + + # FIXME: if backend_http_health_port is provided but backend_use_http_health is false, it should error + + it 'configures the correct check port on the servers' do + expect(backend_images).to include('server node0 10.0.0.2:443 check inter 1000 port 9999') + expect(backend_images).to include('server node1 10.0.0.3:443 check inter 1000 port 9999') + end + end + + context 'when backend_http_health_uri is provided' do + let(:properties) do + default_properties.deep_merge({ + 'routed_backend_servers' => { + '/images' => { + 'backend_use_http_health' => true, + 'backend_http_health_uri' => '/alive' + } + } + }) + end + + # FIXME: if backend_http_health_uri is provided but backend_use_http_health is false, it should error + + it 'overrides the default health check uri' do + expect(backend_images).to include('option httpchk GET /alive') + end + end + end + + # FIXME: ha_proxy.backend_crt is not supported for routed http backends + + context 'when backend_ssl is verify' do + let(:properties) do + default_properties.deep_merge({ + 'routed_backend_servers' => { + '/images' => { + 'backend_ssl' => 'verify' + } + } + }) + end + + it 'configures the server to use ssl: verify' do + expect(backend_images).to include('server node0 10.0.0.2:443 check inter 1000 ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem') + expect(backend_images).to include('server node1 10.0.0.3:443 check inter 1000 ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem') + end + + context 'when ha_proxy.backend_ssl_verifyhost is provided' do + let(:properties) do + default_properties.deep_merge({ + 'routed_backend_servers' => { + '/images' => { + 'backend_ssl' => 'verify', + 'backend_verifyhost' => 'backend.com' + } + } + }) + end + + # FIXME: it should probably error if backend_ssl_verifyhost is provided but backend_ssl is not 'verify' + + it 'configures the server to use ssl: verify with verifyhost for the provided host name' do + expect(backend_images).to include('server node0 10.0.0.2:443 check inter 1000 ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem verifyhost backend.com') + expect(backend_images).to include('server node1 10.0.0.3:443 check inter 1000 ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem verifyhost backend.com') + end + end + end + + context 'when ha_proxy.backend_ssl is noverify' do + let(:properties) do + default_properties.deep_merge({ + 'routed_backend_servers' => { + '/images' => { + 'backend_ssl' => 'noverify' + } + } + }) + end + + it 'configures the server to use ssl: verify none' do + expect(backend_images).to include('server node0 10.0.0.2:443 check inter 1000 ssl verify none') + expect(backend_images).to include('server node1 10.0.0.3:443 check inter 1000 ssl verify none') + end + end + + context 'when ha_proxy.routed_backend_servers is not provided' do + let(:haproxy_conf) do + parse_haproxy_config(template.render({})) + end + + it 'is not included' do + expect(haproxy_conf).not_to have_key(/backend http-routed-backend/) + end + end +end diff --git a/spec/haproxy/templates/haproxy_config/backend_http_spec.rb b/spec/haproxy/templates/haproxy_config/backend_http_spec.rb new file mode 100644 index 00000000..b80d9146 --- /dev/null +++ b/spec/haproxy/templates/haproxy_config/backend_http_spec.rb @@ -0,0 +1,254 @@ +# frozen_string_literal: true + +require 'rspec' + +describe 'config/haproxy.config backend http-routers' do + let(:template) { haproxy_job.template('config/haproxy.config') } + + let(:haproxy_conf) do + parse_haproxy_config(template.render({ 'ha_proxy' => properties })) + end + + let(:properties) { {} } + let(:backend_http_routers) { haproxy_conf['backend http-routers'] } + + it 'has the correct mode' do + expect(backend_http_routers).to include('mode http') + end + + it 'uses round-robin load balancing' do + expect(backend_http_routers).to include('balance roundrobin') + end + + context 'when ha_proxy.compress_types are provided' do + let(:properties) { { 'compress_types' => 'text/html text/plain text/css' } } + + it 'configures the compression type and algorithm' do + expect(backend_http_routers).to include('compression algo gzip') + expect(backend_http_routers).to include('compression type text/html text/plain text/css') + end + end + + context 'when ha_proxy.backend_config is provided' do + let(:properties) do + { + 'backend_config' => 'custom backend config' + } + end + + it 'includes the config' do + expect(backend_http_routers).to include('custom backend config') + end + end + + context 'when ha_proxy.custom_http_error_files is provided' do + let(:properties) do + { + 'custom_http_error_files' => { + '503' => '

503 Service Unavailable

' + } + } + end + + it 'includes the errorfiles' do + expect(backend_http_routers).to include('errorfile 503 /var/vcap/jobs/haproxy/errorfiles/custom503.http') + end + end + + context 'when ha_proxy.backend_use_http_health is true' do + let(:properties) do + { + 'backend_use_http_health' => true, + 'backend_servers' => ['10.0.0.1', '10.0.0.2'] + } + end + + it 'configures the healthcheck' do + expect(backend_http_routers).to include('option httpchk GET /health') + end + + it 'adds the healthcheck to the server config' do + expect(backend_http_routers).to include('server node0 10.0.0.1:80 check inter 1000 port 8080') + expect(backend_http_routers).to include('server node1 10.0.0.2:80 check inter 1000 port 8080') + end + + context 'when backend_http_health_uri is provided' do + let(:properties) do + { + 'backend_use_http_health' => true, + 'backend_http_health_uri' => '1.2.3.5/health', + 'backend_servers' => ['10.0.0.1', '10.0.0.2'] + } + end + + # FIXME: if backend_http_health_uri is provided but backend_use_http_health is false, it should error + + it 'configures the healthcheck' do + expect(backend_http_routers).to include('option httpchk GET 1.2.3.5/health') + end + + it 'adds the healthcheck to the server config' do + expect(backend_http_routers).to include('server node0 10.0.0.1:80 check inter 1000 port 8080') + expect(backend_http_routers).to include('server node1 10.0.0.2:80 check inter 1000 port 8080') + end + end + + context 'when backend_http_health_port is provided' do + let(:properties) do + { + 'backend_use_http_health' => true, + 'backend_http_health_port' => 8081, + 'backend_servers' => ['10.0.0.1', '10.0.0.2'] + } + end + + # FIXME: if backend_http_health_port is provided but backend_use_http_health is false, it should error + + it 'configures the healthcheck' do + expect(backend_http_routers).to include('option httpchk GET /health') + end + + it 'adds the healthcheck to the server config' do + expect(backend_http_routers).to include('server node0 10.0.0.1:80 check inter 1000 port 8081') + expect(backend_http_routers).to include('server node1 10.0.0.2:80 check inter 1000 port 8081') + end + end + end + + context 'when backend servers are provided via ha_proxy.backend_servers' do + let(:properties) do + { + 'backend_servers' => ['10.0.0.1', '10.0.0.2'] + } + end + + it 'configures the servers' do + expect(backend_http_routers).to include('server node0 10.0.0.1:80 check inter 1000') + expect(backend_http_routers).to include('server node1 10.0.0.2:80 check inter 1000') + end + end + + context 'when ha_proxy.backend_crt is provided' do + let(:properties) do + { + 'backend_servers' => ['10.0.0.1', '10.0.0.2'], + 'backend_crt' => 'backend_crt contents' + } + end + + it 'configures the server to use the provided certificate' do + expect(backend_http_routers).to include('server node0 10.0.0.1:80 crt /var/vcap/jobs/haproxy/config/backend-crt.pem check inter 1000') + expect(backend_http_routers).to include('server node1 10.0.0.2:80 crt /var/vcap/jobs/haproxy/config/backend-crt.pem check inter 1000') + end + end + + context 'when ha_proxy.backend_ssl is verify' do + let(:properties) do + { + 'backend_servers' => ['10.0.0.1', '10.0.0.2'], + 'backend_ssl' => 'verify' + } + end + + it 'configures the server to use ssl: verify' do + expect(backend_http_routers).to include('server node0 10.0.0.1:80 check inter 1000 ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem') + expect(backend_http_routers).to include('server node1 10.0.0.2:80 check inter 1000 ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem') + end + + context 'when ha_proxy.backend_ssl_verifyhost is provided' do + let(:properties) do + { + 'backend_servers' => ['10.0.0.1', '10.0.0.2'], + 'backend_ssl' => 'verify', + 'backend_ssl_verifyhost' => 'backend.com' + } + end + + # FIXME: it should probably error if backend_ssl_verifyhost is provided but backend_ssl is not 'verify' + + it 'configures the server to use ssl: verify with verifyhost for the provided host name' do + expect(backend_http_routers).to include('server node0 10.0.0.1:80 check inter 1000 ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem verifyhost backend.com') + expect(backend_http_routers).to include('server node1 10.0.0.2:80 check inter 1000 ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem verifyhost backend.com') + end + end + end + + context 'when ha_proxy.backend_ssl is noverify' do + let(:properties) do + { + 'backend_servers' => ['10.0.0.1', '10.0.0.2'], + 'backend_ssl' => 'noverify' + } + end + + it 'configures the server to use ssl: verify none' do + expect(backend_http_routers).to include('server node1 10.0.0.2:80 check inter 1000 ssl verify none') + expect(backend_http_routers).to include('server node1 10.0.0.2:80 check inter 1000 ssl verify none') + end + end + + context 'when ha_proxy.backend_port is provided' do + let(:properties) do + { + 'backend_servers' => ['10.0.0.1', '10.0.0.2'], + 'backend_port' => 7000 + } + end + + it 'overrides the default port' do + expect(backend_http_routers).to include('server node0 10.0.0.1:7000 check inter 1000') + expect(backend_http_routers).to include('server node1 10.0.0.2:7000 check inter 1000') + end + end + + context 'when ha_proxy.resolvers are provided' do + let(:properties) do + { + 'resolvers' => [{ 'public' => '1.1.1.1' }], + 'backend_servers' => ['10.0.0.1', '10.0.0.2'] + } + end + + it 'sets the resolver on the server configuration' do + expect(backend_http_routers).to include('server node0 10.0.0.1:80 resolvers default check inter 1000') + expect(backend_http_routers).to include('server node1 10.0.0.2:80 resolvers default check inter 1000') + end + end + + context 'when the backend configuration is provided via the http_backend link' do + let(:http_backend_link) do + Bosh::Template::Test::Link.new( + name: 'http_backend', + instances: [ + # will appear in same AZ + Bosh::Template::Test::LinkInstance.new(address: 'backend.az1.internal', az: 'az1'), + + # will appear in another AZ + Bosh::Template::Test::LinkInstance.new(address: 'backend.az2.internal', az: 'az2') + ] + ) + end + + let(:haproxy_conf) do + parse_haproxy_config(template.render({ 'ha_proxy' => properties }, consumes: [http_backend_link])) + end + + it 'correctly configures the servers' do + expect(backend_http_routers).to include('server node0 backend.az1.internal:80 check inter 1000') + expect(backend_http_routers).to include('server node1 backend.az2.internal:80 check inter 1000') + end + + context 'when ha_proxy.backend_prefer_local_az is true' do + let(:properties) do + { 'backend_prefer_local_az' => true } + end + + # FIXME: if backend_prefer_local_az is true, but no http_backend link is provided then it should probably error + + it 'configures servers in other azs as backup servers' do + expect(backend_http_routers).to include('server node0 backend.az1.internal:80 check inter 1000') + expect(backend_http_routers).to include('server node1 backend.az2.internal:80 check inter 1000 backup') + end + end + end +end diff --git a/spec/haproxy/templates/haproxy_config/backend_tcp_spec.rb b/spec/haproxy/templates/haproxy_config/backend_tcp_spec.rb new file mode 100644 index 00000000..0f1e794f --- /dev/null +++ b/spec/haproxy/templates/haproxy_config/backend_tcp_spec.rb @@ -0,0 +1,219 @@ +# frozen_string_literal: true + +require 'rspec' + +describe 'config/haproxy.config custom TCP backends' do + let(:template) { haproxy_job.template('config/haproxy.config') } + + let(:haproxy_conf) do + parse_haproxy_config(template.render({ 'ha_proxy' => properties }, consumes: [backend_tcp_link])) + end + + let(:backend_tcp_link) do + Bosh::Template::Test::Link.new( + name: 'tcp_backend', + instances: [ + # will appear in same AZ + Bosh::Template::Test::LinkInstance.new(address: 'postgres.az1.com', name: 'postgres', az: 'az1'), + + # will appear in another AZ + Bosh::Template::Test::LinkInstance.new(address: 'postgres.az2.com', name: 'postgres', az: 'az2') + ] + ) + end + + let(:backend_tcp_redis) { haproxy_conf['backend tcp-redis'] } + let(:backend_tcp_mysql) { haproxy_conf['backend tcp-mysql'] } + let(:backend_tcp_postgres_via_link) { haproxy_conf['backend tcp-postgres'] } + + let(:default_properties) do + { + 'tcp_link_port' => 5432, + 'tcp' => [{ + 'name' => 'redis', + 'port' => 6380, + 'backend_port' => 6379, + 'backend_servers' => ['10.0.0.1', '10.0.0.2'] + }, { + 'name' => 'mysql', + 'port' => 3307, + 'backend_port' => 3306, + 'backend_servers' => ['11.0.0.1', '11.0.0.2'] + }] + } + end + + let(:properties) { default_properties } + + it 'has the correct mode' do + expect(backend_tcp_redis).to include('mode tcp') + expect(backend_tcp_mysql).to include('mode tcp') + expect(backend_tcp_postgres_via_link).to include('mode tcp') + end + + context 'when balance is provided (not available via link)' do + let(:properties) do + { + 'tcp_link_port' => 5432, + 'tcp' => [{ + 'name' => 'redis', + 'port' => 6380, + 'backend_port' => 6379, + 'backend_servers' => ['10.0.0.1', '10.0.0.2'], + 'balance' => 'leastconn' + }, { + 'name' => 'mysql', + 'port' => 3307, + 'backend_port' => 3306, + 'backend_servers' => ['11.0.0.1', '11.0.0.2'], + 'balance' => 'leastconn' + }] + } + end + + it 'uses the specified balancing algorithm' do + expect(backend_tcp_redis).to include('balance leastconn') + expect(backend_tcp_mysql).to include('balance leastconn') + end + end + + # FIXME: when backend_port is not provided, the check port is empty creating an invalid config + + # FIXME: tcp backend ignores ha_proxy.backend_prefer_local_az + + it 'configures the backend servers' do + expect(backend_tcp_redis).to include('server node0 10.0.0.1:6379 check port 6379 inter 1000') + expect(backend_tcp_redis).to include('server node1 10.0.0.2:6379 check port 6379 inter 1000') + + expect(backend_tcp_mysql).to include('server node0 11.0.0.1:3306 check port 3306 inter 1000') + expect(backend_tcp_mysql).to include('server node1 11.0.0.2:3306 check port 3306 inter 1000') + + expect(backend_tcp_postgres_via_link).to include('server node0 postgres.az1.com:5432 check port 5432 inter 1000') + expect(backend_tcp_postgres_via_link).to include('server node1 postgres.az2.com:5432 check port 5432 inter 1000 backup') + end + + context 'when a server is included in backend_servers_local' do + let(:properties) do + { + 'tcp_link_port' => 5432, + 'tcp' => [{ + 'name' => 'redis', + 'port' => 6380, + 'backend_port' => 6379, + 'backend_servers' => ['10.0.0.1', '10.0.0.2'], + 'backend_servers_local' => ['10.0.0.1'] + }] + } + end + + it 'configures non-local servers as "backups"' do + expect(backend_tcp_redis).to include('server node0 10.0.0.1:6379 check port 6379 inter 1000') + expect(backend_tcp_redis).to include('server node1 10.0.0.2:6379 check port 6379 inter 1000 backup') + end + end + + context 'when ha_proxy.tcp_link_check_port is provided' do + let(:properties) { default_properties.merge({ 'tcp_link_check_port' => 9000 }) } + + it 'overrides the health check port' do + expect(backend_tcp_redis).to include('server node0 10.0.0.1:6379 check port 9000 inter 1000') + expect(backend_tcp_redis).to include('server node1 10.0.0.2:6379 check port 9000 inter 1000') + + expect(backend_tcp_mysql).to include('server node0 11.0.0.1:3306 check port 9000 inter 1000') + expect(backend_tcp_mysql).to include('server node1 11.0.0.2:3306 check port 9000 inter 1000') + + expect(backend_tcp_postgres_via_link).to include('server node0 postgres.az1.com:5432 check port 9000 inter 1000') + expect(backend_tcp_postgres_via_link).to include('server node1 postgres.az2.com:5432 check port 9000 inter 1000 backup') + end + end + + context 'when ha_proxy.resolvers are provided' do + let(:properties) do + default_properties.merge({ 'resolvers' => [{ 'public' => '1.1.1.1' }] }) + end + + it 'sets the resolver on the server configuration' do + expect(backend_tcp_redis).to include('server node0 10.0.0.1:6379 resolvers default check port 6379 inter 1000') + expect(backend_tcp_redis).to include('server node1 10.0.0.2:6379 resolvers default check port 6379 inter 1000') + + expect(backend_tcp_mysql).to include('server node0 11.0.0.1:3306 resolvers default check port 3306 inter 1000') + expect(backend_tcp_mysql).to include('server node1 11.0.0.2:3306 resolvers default check port 3306 inter 1000') + + expect(backend_tcp_postgres_via_link).to include('server node0 postgres.az1.com:5432 resolvers default check port 5432 inter 1000') + expect(backend_tcp_postgres_via_link).to include('server node1 postgres.az2.com:5432 resolvers default check port 5432 inter 1000 backup') + end + end + + context 'when backend_ssl is verify' do + let(:properties) do + { + 'tcp_link_port' => 5432, + 'tcp' => [{ + 'name' => 'redis', + 'port' => 6380, + 'backend_port' => 6379, + 'backend_servers' => ['10.0.0.1', '10.0.0.2'], + 'backend_ssl' => 'verify' + }] + } + end + + it 'configures the server to use ssl: verify' do + expect(backend_tcp_redis).to include('server node0 10.0.0.1:6379 check port 6379 inter 1000 ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem') + expect(backend_tcp_redis).to include('server node1 10.0.0.2:6379 check port 6379 inter 1000 ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem') + end + + context 'when ha_proxy.backend_ssl_verifyhost is provided' do + let(:properties) do + { + 'tcp_link_port' => 5432, + 'tcp' => [{ + 'name' => 'redis', + 'port' => 6380, + 'backend_port' => 6379, + 'backend_servers' => ['10.0.0.1', '10.0.0.2'], + 'backend_ssl' => 'verify', + 'backend_verifyhost' => 'backend.com' + }] + } + end + + # FIXME: it should probably error if backend_ssl_verifyhost is provided but backend_ssl is not 'verify' + + it 'configures the server to use ssl: verify with verifyhost for the provided host name' do + expect(backend_tcp_redis).to include('server node0 10.0.0.1:6379 check port 6379 inter 1000 ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem verifyhost backend.com') + expect(backend_tcp_redis).to include('server node1 10.0.0.2:6379 check port 6379 inter 1000 ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem verifyhost backend.com') + end + end + end + + context 'when ha_proxy.backend_ssl is noverify' do + let(:properties) do + { + 'tcp_link_port' => 5432, + 'tcp' => [{ + 'name' => 'redis', + 'port' => 6380, + 'backend_port' => 6379, + 'backend_servers' => ['10.0.0.1', '10.0.0.2'], + 'backend_ssl' => 'noverify' + }] + } + end + + it 'configures the server to use ssl: verify none' do + expect(backend_tcp_redis).to include('server node0 10.0.0.1:6379 check port 6379 inter 1000 ssl verify none') + expect(backend_tcp_redis).to include('server node1 10.0.0.2:6379 check port 6379 inter 1000 ssl verify none') + end + end + + context 'when ha_proxy.tcp is not provided' do + let(:haproxy_conf) do + parse_haproxy_config(template.render({})) + end + + it 'is not included' do + expect(haproxy_conf).not_to have_key(/backend tcp/) + end + end +end diff --git a/spec/haproxy/templates/haproxy_config/frontend_cf_tcp_routing_spec.rb b/spec/haproxy/templates/haproxy_config/frontend_cf_tcp_routing_spec.rb new file mode 100644 index 00000000..3ecd9611 --- /dev/null +++ b/spec/haproxy/templates/haproxy_config/frontend_cf_tcp_routing_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require 'rspec' +require 'tempfile' + +describe 'config/haproxy.config frontend cf_tcp_routing' do + let(:template) { haproxy_job.template('config/haproxy.config') } + + let(:tcp_router_link) do + Bosh::Template::Test::Link.new( + name: 'tcp_router', + instances: [Bosh::Template::Test::LinkInstance.new(address: 'tcp.cf.com')] + ) + end + + let(:haproxy_conf) do + parse_haproxy_config(template.render({ 'ha_proxy' => properties }, consumes: [tcp_router_link])) + end + + let(:frontend_cf_tcp_routing) { haproxy_conf['frontend cf_tcp_routing'] } + + let(:properties) { {} } + + it 'has the correct mode' do + expect(frontend_cf_tcp_routing).to include('mode tcp') + end + + it 'uses default port range of 1024-1123' do + expect(frontend_cf_tcp_routing).to include('bind :1024-1123') + end + + context 'when ha_proxy.binding_ip is provided' do + let(:properties) do + { + 'binding_ip' => '1.2.3.4' + } + end + + it 'overrides the binding ip' do + expect(frontend_cf_tcp_routing).to include('bind 1.2.3.4:1024-1123') + end + end + + context 'when ha_proxy.tcp_routing.port_range is provided' do + let(:properties) do + { + 'tcp_routing' => { + 'port_range' => '100-200' + } + } + end + + it 'overrides the port range' do + expect(frontend_cf_tcp_routing).to include('bind :100-200') + end + end + + it 'has the correct backend' do + expect(frontend_cf_tcp_routing).to include('default_backend cf_tcp_routers') + end + + context 'when ha_proxy.drain_enable is true' do + let(:properties) do + { + 'drain_enable' => true + } + end + + it 'has a default grace period of 0 milliseconds' do + expect(frontend_cf_tcp_routing).to include('grace 0') + end + + context('when ha_proxy.drain_frontend_grace_time is provided') do + let(:properties) do + { + 'drain_enable' => true, + 'drain_frontend_grace_time' => 12 + } + end + + it 'overrides the grace period' do + expect(frontend_cf_tcp_routing).to include('grace 12000') + end + end + end + + context 'when no tcp_router link is provided' do + let(:haproxy_conf) do + parse_haproxy_config(template.render(properties)) + end + + it 'is not included' do + expect(haproxy_conf).not_to have_key('frontend cf_tcp_routing') + end + end +end diff --git a/spec/haproxy/templates/haproxy_config/frontend_http_spec.rb b/spec/haproxy/templates/haproxy_config/frontend_http_spec.rb new file mode 100644 index 00000000..9cf93aed --- /dev/null +++ b/spec/haproxy/templates/haproxy_config/frontend_http_spec.rb @@ -0,0 +1,232 @@ +# frozen_string_literal: true + +require 'rspec' + +describe 'config/haproxy.config HTTP frontend' do + let(:template) { haproxy_job.template('config/haproxy.config') } + + let(:haproxy_conf) do + parse_haproxy_config(template.render({ 'ha_proxy' => properties })) + end + + let(:frontend_http) { haproxy_conf['frontend http-in'] } + let(:properties) { {} } + + context 'when ha_proxy.drain_enable is true' do + let(:properties) do + { 'drain_enable' => true } + end + + it 'has a default grace period of 0 milliseconds' do + expect(frontend_http).to include('grace 0') + end + + context('when ha_proxy.drain_frontend_grace_time is provided') do + let(:properties) do + { 'drain_enable' => true, 'drain_frontend_grace_time' => 12 } + end + + # FIXME: if drain_frontend_grace_time is provided but drain_enable is false then it should error + + it 'overrides the grace period' do + expect(frontend_http).to include('grace 12000') + end + end + end + + it 'binds to all interfaces by default' do + expect(frontend_http).to include('bind :80') + end + + context 'when ha_proxy.binding_ip is provided' do + let(:properties) do + { 'binding_ip' => '1.2.3.4' } + end + + it 'binds to the provided ip' do + expect(frontend_http).to include('bind 1.2.3.4:80') + end + + context 'when ha_proxy.v4v6 is true and binding_ip is ::' do + let(:properties) do + { 'v4v6' => true, 'binding_ip' => '::' } + end + + it 'enables ipv6' do + expect(frontend_http).to include('bind :::80 v4v6') + end + end + + context 'when ha_proxy.accept_proxy is true' do + let(:properties) do + { 'accept_proxy' => true } + end + + it 'sets accept-proxy' do + expect(frontend_http).to include('bind :80 accept-proxy') + end + end + end + + context 'when a custom ha_proxy.frontend_config is provided' do + let(:properties) do + { 'frontend_config' => 'custom config content' } + end + + it 'includes the custom config' do + expect(frontend_http).to include('custom config content') + end + end + + context 'when a ha_proxy.cidr_whitelist is provided' do + let(:properties) do + { 'cidr_whitelist' => ['172.168.4.1/32', '10.2.0.0/16'] } + end + + it 'sets the correct acl and content accept rules' do + expect(frontend_http).to include('acl whitelist src -f /var/vcap/jobs/haproxy/config/whitelist_cidrs.txt') + expect(frontend_http).to include('tcp-request content accept if whitelist') + end + end + + context 'when a ha_proxy.cidr_blacklist is provided' do + let(:properties) do + { 'cidr_blacklist' => ['172.168.4.1/32', '10.2.0.0/16'] } + end + + it 'sets the correct acl and content reject rules' do + expect(frontend_http).to include('acl blacklist src -f /var/vcap/jobs/haproxy/config/blacklist_cidrs.txt') + expect(frontend_http).to include('tcp-request content reject if blacklist') + end + end + + context 'when ha_proxy.block_all is provided' do + let(:properties) do + { 'block_all' => true } + end + + it 'sets the correct content reject rules' do + expect(frontend_http).to include('tcp-request content reject') + end + end + + it 'correct request capturing configuration' do + expect(frontend_http).to include('capture request header Host len 256') + end + + it 'has the correct default backend' do + expect(frontend_http).to include('default_backend http-routers') + end + + context 'when ha_proxy.http_request_deny_conditions are provided' do + let(:properties) do + { + 'http_request_deny_conditions' => [{ + 'condition' => [{ + 'acl_name' => 'block_host', + 'acl_rule' => 'hdr_beg(host) -i login' + }, { + 'acl_name' => 'whitelist_ips', + 'acl_rule' => 'src 5.22.5.11 5.22.5.12', + 'negate' => true + }] + }] + } + end + + it 'adds the correct acls and http-request deny rules' do + expect(frontend_http).to include('acl block_host hdr_beg(host) -i login') + expect(frontend_http).to include('acl whitelist_ips src 5.22.5.11 5.22.5.12') + + expect(frontend_http).to include('http-request deny if block_host !whitelist_ips') + end + end + + context 'when ha_proxy.headers are provided' do + let(:properties) do + { 'headers' => ['X-Application-ID: my-custom-header', 'MyCustomHeader: 3'] } + end + + it 'adds the request headers' do + expect(frontend_http).to include('http-request add-header X-Application-ID:\ my-custom-header ""') + expect(frontend_http).to include('http-request add-header MyCustomHeader:\ 3 ""') + end + end + + context 'when ha_proxy.rsp_headers are provided' do + let(:properties) do + { 'rsp_headers' => ['X-Application-ID: my-custom-header', 'MyCustomHeader: 3'] } + end + + it 'adds the response headers' do + expect(frontend_http).to include('http-response add-header X-Application-ID:\ my-custom-header ""') + expect(frontend_http).to include('http-response add-header MyCustomHeader:\ 3 ""') + end + end + + context 'when ha_proxy.internal_only_domains are provided' do + let(:properties) do + { 'internal_only_domains' => ['bosh.internal'] } + end + + it 'adds the correct acl and http-request deny rules' do + expect(frontend_http).to include('acl private src -f /var/vcap/jobs/haproxy/config/trusted_domain_cidrs.txt') + expect(frontend_http).to include('acl internal hdr(Host) -m sub bosh.internal') + expect(frontend_http).to include('http-request deny if internal !private') + end + end + + context 'when ha_proxy.routed_backend_servers are provided' do + let(:properties) do + { + 'routed_backend_servers' => { + '/images' => { + 'port' => 12_000, + 'servers' => ['10.0.0.1'] + } + } + } + end + + it 'grants access to the backend servers' do + expect(frontend_http).to include('acl routed_backend_9c1bb7 path_beg /images') + expect(frontend_http).to include('use_backend http-routed-backend-9c1bb7 if routed_backend_9c1bb7') + end + end + + it 'adds the X-Forwarded-Proto header' do + expect(frontend_http).to include('acl xfp_exists hdr_cnt(X-Forwarded-Proto) gt 0') + expect(frontend_http).to include('http-request add-header X-Forwarded-Proto "http" if ! xfp_exists') + end + + context 'when ha_proxy.https_redirect_all is true' do + let(:properties) do + { 'https_redirect_all' => true } + end + + it 'adds the redirect rule' do + expect(frontend_http).to include('redirect scheme https code 301 if !{ ssl_fc }') + end + end + + context 'when ha_proxy.https_redirect_all is false (the default)' do + let(:properties) do + { 'https_redirect_all' => false } + end + + it 'only redirects domains specified in the redirect map' do + expect(frontend_http).to include('acl ssl_redirect hdr(host),lower,map_end(/var/vcap/jobs/haproxy/config/ssl_redirect.map,false) -m str true') + expect(frontend_http).to include('redirect scheme https code 301 if ssl_redirect') + end + end + + context 'when ha_proxy.disable_http is true' do + let(:properties) do + { 'disable_http' => true } + end + + it 'removes the http frontend' do + expect(haproxy_conf).not_to have_key('frontend http-in') + end + end +end diff --git a/spec/haproxy/templates/haproxy_config/frontend_https_spec.rb b/spec/haproxy/templates/haproxy_config/frontend_https_spec.rb new file mode 100644 index 00000000..62f8ea4c --- /dev/null +++ b/spec/haproxy/templates/haproxy_config/frontend_https_spec.rb @@ -0,0 +1,358 @@ +# frozen_string_literal: true + +require 'rspec' + +describe 'config/haproxy.config HTTPS frontend' do + let(:template) { haproxy_job.template('config/haproxy.config') } + + let(:haproxy_conf) do + parse_haproxy_config(template.render({ 'ha_proxy' => properties })) + end + + let(:frontend_https) { haproxy_conf['frontend https-in'] } + let(:default_properties) do + { + 'ssl_pem' => 'ssl pem contents' + } + end + + let(:properties) { default_properties } + + context 'when ha_proxy.drain_enable is true' do + let(:properties) do + default_properties.merge({ 'drain_enable' => true }) + end + + it 'has a default grace period of 0 milliseconds' do + expect(frontend_https).to include('grace 0') + end + + context('when ha_proxy.drain_frontend_grace_time is provided') do + let(:properties) do + default_properties.merge({ 'drain_enable' => true, 'drain_frontend_grace_time' => 12 }) + end + + # FIXME: if drain_frontend_grace_time is provided but drain_enable is false then it should error + + it 'overrides the grace period' do + expect(frontend_https).to include('grace 12000') + end + end + end + + it 'binds to all interfaces by default' do + expect(frontend_https).to include('bind :443 ssl crt /var/vcap/jobs/haproxy/config/ssl') + end + + context 'when ha_proxy.binding_ip is provided' do + let(:properties) do + default_properties.merge({ 'binding_ip' => '1.2.3.4' }) + end + + it 'binds to the provided ip' do + expect(frontend_https).to include('bind 1.2.3.4:443 ssl crt /var/vcap/jobs/haproxy/config/ssl') + end + + context 'when ha_proxy.v4v6 is true and binding_ip is ::' do + let(:properties) do + default_properties.merge({ 'v4v6' => true, 'binding_ip' => '::' }) + end + + it 'enables ipv6' do + expect(frontend_https).to include('bind :::443 ssl crt /var/vcap/jobs/haproxy/config/ssl v4v6') + end + end + + context 'when ha_proxy.accept_proxy is true' do + let(:properties) do + default_properties.merge({ 'accept_proxy' => true }) + end + + it 'sets accept-proxy' do + expect(frontend_https).to include('bind :443 accept-proxy ssl crt /var/vcap/jobs/haproxy/config/ssl') + end + end + end + + context 'when mutual tls is enabled' do + let(:properties) do + default_properties.merge({ 'client_cert' => 'client_cert contents' }) + end + + it 'configures ssl to use the client ca' do + expect(frontend_https).to include('bind :443 ssl crt /var/vcap/jobs/haproxy/config/ssl ca-file /etc/ssl/certs/ca-certificates.crt verify optional') + end + + context 'when ha_proxy.client_cert_ignore_err is true' do + let(:properties) do + default_properties.merge({ 'client_cert' => 'client_cert contents', 'client_cert_ignore_err' => true }) + end + + # FIXME: if client_cert_ignore_err is true but client_cert is not provided, then it should error + + it 'adds the crt-ignore-err flag' do + expect(frontend_https).to include('bind :443 ssl crt /var/vcap/jobs/haproxy/config/ssl ca-file /etc/ssl/certs/ca-certificates.crt verify optional crt-ignore-err true') + end + end + + context 'when ha_proxy.client_revocation_list is provided' do + let(:properties) do + default_properties.merge({ 'client_cert' => 'client_cert contents', 'client_revocation_list' => 'client_revocation_list contents' }) + end + + # FIXME: if client_revocation_list is provided but client_cert is not provided, then it should error + + it 'references the crl list' do + expect(frontend_https).to include('bind :443 ssl crt /var/vcap/jobs/haproxy/config/ssl ca-file /etc/ssl/certs/ca-certificates.crt verify optional crl-file /var/vcap/jobs/haproxy/config/client-revocation-list.pem') + end + end + end + + context 'when ha_proxy.forwarded_client_cert is always_forward_only (the default)' do + it 'deletes the X-Forwarded-Client-Cert header by default' do + expect(frontend_https).to include('http-request del-header X-Forwarded-Client-Cert') + end + end + + context 'when ha_proxy.forwarded_client_cert is forward_only' do + let(:properties) do + default_properties.merge({ 'forwarded_client_cert' => 'forward_only' }) + end + + it 'deletes the X-Forwarded-Client-Cert header' do + expect(frontend_https).to include('http-request del-header X-Forwarded-Client-Cert') + end + + context 'when mutual TLS is enabled' do + let(:properties) do + default_properties.merge({ + 'client_cert' => 'client_cert contents', + 'forwarded_client_cert' => 'forward_only' + }) + end + + it 'only deletes the X-Forwarded-Client-Cert header when mTLS is not used' do + expect(frontend_https).to include('http-request del-header X-Forwarded-Client-Cert if ! { ssl_c_used }') + end + end + end + + context 'when ha_proxy.forwarded_client_cert is sanitize_set' do + let(:properties) do + default_properties.merge({ 'forwarded_client_cert' => 'sanitize_set' }) + end + + it 'deletes the X-Forwarded-Client-Cert header' do + expect(frontend_https).to include('http-request del-header X-Forwarded-Client-Cert') + end + + context 'when mutual TLS is enabled' do + let(:properties) do + default_properties.merge({ + 'client_cert' => 'client_cert contents', + 'forwarded_client_cert' => 'sanitize_set' + }) + end + + it 'sets X-Forwarded-Client-Cert to the client cert for mTLS connections ' do + expect(frontend_https).to include('http-request del-header X-Forwarded-Client-Cert') + expect(frontend_https).to include('http-request set-header X-Forwarded-Client-Cert %[ssl_c_der,base64] if { ssl_c_used }') + end + end + end + + context 'when ha_proxy.forwarded_client_cert is forward_only_if_route_service' do + let(:properties) do + default_properties.merge({ 'forwarded_client_cert' => 'forward_only_if_route_service' }) + end + + it 'deletes the X-Forwarded-Client-Cert header for non-route service requests' do + expect(frontend_https).to include('acl route_service_request hdr(X-Cf-Proxy-Signature) -m found') + expect(frontend_https).to include('http-request del-header X-Forwarded-Client-Cert if !route_service_request') + expect(frontend_https).to include('http-request set-header X-Forwarded-Client-Cert %[ssl_c_der,base64] if { ssl_c_used }') + end + end + + context 'when ha_proxy.hsts_enable is true' do + let(:properties) do + default_properties.merge({ 'hsts_enable' => true }) + end + + it 'sets the Strict-Transport-Security header' do + expect(frontend_https).to include('http-response set-header Strict-Transport-Security max-age=31536000;') + end + + context 'when ha_proxy.hsts_max_age is provided' do + let(:properties) do + default_properties.merge({ 'hsts_enable' => true, 'hsts_max_age' => 9999 }) + end + + it 'sets the Strict-Transport-Security header with the correct max-age' do + expect(frontend_https).to include('http-response set-header Strict-Transport-Security max-age=9999;') + end + end + + context 'when ha_proxy.hsts_include_subdomains is true' do + let(:properties) do + default_properties.merge({ 'hsts_enable' => true, 'hsts_include_subdomains' => true }) + end + + # FIXME: hsts_include_subdomains is true but hsts_enable is false, then it should error + + it 'sets the includeSubDomains flag' do + expect(frontend_https).to include('http-response set-header Strict-Transport-Security max-age=31536000;\ includeSubDomains;') + end + end + + context 'when ha_proxy.hsts_preload is true' do + let(:properties) do + default_properties.merge({ 'hsts_enable' => true, 'hsts_preload' => true }) + end + + # FIXME: hsts_preload is true but hsts_enable is false, then it should error + + it 'sets the preload flag' do + expect(frontend_https).to include('http-response set-header Strict-Transport-Security max-age=31536000;\ preload;') + end + end + end + + it 'correct request capturing configuration' do + expect(frontend_https).to include('capture request header Host len 256') + end + + it 'has the correct default backend' do + expect(frontend_https).to include('default_backend http-routers') + end + + context 'when ha_proxy.http_request_deny_conditions are provided' do + let(:properties) do + default_properties.merge({ + 'http_request_deny_conditions' => [{ + 'condition' => [{ + 'acl_name' => 'block_host', + 'acl_rule' => 'hdr_beg(host) -i login' + }, { + 'acl_name' => 'whitelist_ips', + 'acl_rule' => 'src 5.22.5.11 5.22.5.12', + 'negate' => true + }] + }] + }) + end + + it 'adds the correct acls and http-request deny rules' do + expect(frontend_https).to include('acl block_host hdr_beg(host) -i login') + expect(frontend_https).to include('acl whitelist_ips src 5.22.5.11 5.22.5.12') + + expect(frontend_https).to include('http-request deny if block_host !whitelist_ips') + end + end + + context 'when a custom ha_proxy.frontend_config is provided' do + let(:properties) do + default_properties.merge({ 'frontend_config' => 'custom config content' }) + end + + it 'includes the custom config' do + expect(frontend_https).to include('custom config content') + end + end + + context 'when a ha_proxy.cidr_whitelist is provided' do + let(:properties) do + default_properties.merge({ 'cidr_whitelist' => ['172.168.4.1/32', '10.2.0.0/16'] }) + end + + it 'sets the correct acl and content accept rules' do + expect(frontend_https).to include('acl whitelist src -f /var/vcap/jobs/haproxy/config/whitelist_cidrs.txt') + expect(frontend_https).to include('tcp-request content accept if whitelist') + end + end + + context 'when a ha_proxy.cidr_blacklist is provided' do + let(:properties) do + default_properties.merge({ 'cidr_blacklist' => ['172.168.4.1/32', '10.2.0.0/16'] }) + end + + it 'sets the correct acl and content reject rules' do + expect(frontend_https).to include('acl blacklist src -f /var/vcap/jobs/haproxy/config/blacklist_cidrs.txt') + expect(frontend_https).to include('tcp-request content reject if blacklist') + end + end + + context 'when ha_proxy.block_all is provided' do + let(:properties) do + default_properties.merge({ 'block_all' => true }) + end + + it 'sets the correct content reject rules' do + expect(frontend_https).to include('tcp-request content reject') + end + end + + context 'when ha_proxy.headers are provided' do + let(:properties) do + default_properties.merge({ 'headers' => ['X-Application-ID: my-custom-header', 'MyCustomHeader: 3'] }) + end + + it 'adds the request headers' do + expect(frontend_https).to include('http-request add-header X-Application-ID:\ my-custom-header ""') + expect(frontend_https).to include('http-request add-header MyCustomHeader:\ 3 ""') + end + end + + context 'when ha_proxy.rsp_headers are provided' do + let(:properties) do + default_properties.merge({ 'rsp_headers' => ['X-Application-ID: my-custom-header', 'MyCustomHeader: 3'] }) + end + + it 'adds the response headers' do + expect(frontend_https).to include('http-response add-header X-Application-ID:\ my-custom-header ""') + expect(frontend_https).to include('http-response add-header MyCustomHeader:\ 3 ""') + end + end + + context 'when ha_proxy.internal_only_domains are provided' do + let(:properties) do + default_properties.merge({ 'internal_only_domains' => ['bosh.internal'] }) + end + + it 'adds the correct acl and http-request deny rules' do + expect(frontend_https).to include('acl private src -f /var/vcap/jobs/haproxy/config/trusted_domain_cidrs.txt') + expect(frontend_https).to include('acl internal hdr(Host) -m sub bosh.internal') + expect(frontend_https).to include('http-request deny if internal !private') + end + end + + context 'when ha_proxy.routed_backend_servers are provided' do + let(:properties) do + default_properties.merge({ + 'routed_backend_servers' => { + '/images' => { + 'port' => 12_000, + 'servers' => ['10.0.0.1'] + } + } + }) + end + + it 'grants access to the backend servers' do + expect(frontend_https).to include('acl routed_backend_9c1bb7 path_beg /images') + expect(frontend_https).to include('use_backend http-routed-backend-9c1bb7 if routed_backend_9c1bb7') + end + end + + it 'adds the X-Forwarded-Proto header' do + expect(frontend_https).to include('acl xfp_exists hdr_cnt(X-Forwarded-Proto) gt 0') + expect(frontend_https).to include('http-request add-header X-Forwarded-Proto "https" if ! xfp_exists') + end + + context 'when no ssl options are provided' do + let(:properties) { {} } + + it 'removes the https frontend' do + expect(haproxy_conf).not_to have_key('frontend https-in') + end + end +end diff --git a/spec/haproxy/templates/haproxy_config/frontend_tcp_spec.rb b/spec/haproxy/templates/haproxy_config/frontend_tcp_spec.rb new file mode 100644 index 00000000..4df6b6c8 --- /dev/null +++ b/spec/haproxy/templates/haproxy_config/frontend_tcp_spec.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +require 'rspec' + +describe 'config/haproxy.config custom TCP frontends' do + let(:template) { haproxy_job.template('config/haproxy.config') } + + let(:haproxy_conf) do + parse_haproxy_config(template.render({ 'ha_proxy' => properties }, consumes: [backend_tcp_link])) + end + + let(:backend_tcp_link) do + Bosh::Template::Test::Link.new( + name: 'tcp_backend', + instances: [Bosh::Template::Test::LinkInstance.new(address: 'postgres.backend.com', name: 'postgres')] + ) + end + + let(:frontend_tcp_redis) { haproxy_conf['frontend tcp-frontend_redis'] } + let(:frontend_tcp_mysql) { haproxy_conf['frontend tcp-frontend_mysql'] } + let(:frontend_tcp_postgres_via_link) { haproxy_conf['frontend tcp-frontend_postgres'] } + + let(:default_properties) do + { + 'tcp_link_port' => 5432, + 'tcp' => [{ + 'name' => 'redis', + 'port' => 6379, + 'backend_servers' => ['10.0.0.1', '10.0.0.2'] + }, { + 'name' => 'mysql', + 'port' => 3306, + 'backend_servers' => ['11.0.0.1', '11.0.0.2'] + }] + } + end + + let(:properties) { default_properties } + + it 'has the correct mode' do + expect(frontend_tcp_redis).to include('mode tcp') + expect(frontend_tcp_mysql).to include('mode tcp') + expect(frontend_tcp_postgres_via_link).to include('mode tcp') + end + + it 'has the correct default backend' do + expect(frontend_tcp_redis).to include('default_backend tcp-redis') + expect(frontend_tcp_mysql).to include('default_backend tcp-mysql') + expect(frontend_tcp_postgres_via_link).to include('default_backend tcp-postgres') + end + + context 'when ha_proxy.drain_enable is true' do + let(:properties) do + default_properties.merge({ 'drain_enable' => true }) + end + + it 'has a default grace period of 0 milliseconds' do + expect(frontend_tcp_redis).to include('grace 0') + expect(frontend_tcp_mysql).to include('grace 0') + expect(frontend_tcp_postgres_via_link).to include('grace 0') + end + + context('when ha_proxy.drain_frontend_grace_time is provided') do + let(:properties) do + default_properties.merge({ 'drain_enable' => true, 'drain_frontend_grace_time' => 12 }) + end + + # FIXME: if drain_frontend_grace_time is provided but drain_enable is false then it should error + + it 'overrides the grace period' do + expect(frontend_tcp_redis).to include('grace 12000') + expect(frontend_tcp_mysql).to include('grace 12000') + expect(frontend_tcp_postgres_via_link).to include('grace 12000') + end + end + end + + it 'binds to all interfaces by default' do + expect(frontend_tcp_redis).to include('bind :6379') + expect(frontend_tcp_mysql).to include('bind :3306') + expect(frontend_tcp_postgres_via_link).to include('bind :5432') + end + + context 'when ha_proxy.binding_ip is provided' do + let(:properties) do + default_properties.merge({ 'binding_ip' => '1.2.3.4' }) + end + + it 'binds to the provided ip' do + expect(frontend_tcp_redis).to include('bind 1.2.3.4:6379') + expect(frontend_tcp_mysql).to include('bind 1.2.3.4:3306') + expect(frontend_tcp_postgres_via_link).to include('bind 1.2.3.4:5432') + end + + context 'when ha_proxy.v4v6 is true and binding_ip is ::' do + let(:properties) do + default_properties.merge({ 'v4v6' => true, 'binding_ip' => '::' }) + end + + it 'enables ipv6' do + expect(frontend_tcp_redis).to include('bind :::6379 v4v6') + expect(frontend_tcp_mysql).to include('bind :::3306 v4v6') + expect(frontend_tcp_postgres_via_link).to include('bind :::5432 v4v6') + end + end + end + + context 'when ssl is enabled on custom backends (not links)' do + let(:default_properties) do + { + 'tcp_link_port' => 5432, + 'tcp' => [{ + 'name' => 'redis', + 'port' => 6379, + 'backend_servers' => ['10.0.0.1', '10.0.0.2'], + 'ssl' => true + }, { + 'name' => 'mysql', + 'port' => 3306, + 'backend_servers' => ['11.0.0.1', '11.0.0.2'], + 'ssl' => true + }] + } + end + + it 'adds the default ssl options' do + expect(frontend_tcp_redis).to include('bind :6379 ssl') + expect(frontend_tcp_mysql).to include('bind :3306 ssl') + end + + context 'when ha_proxy.accept_proxy is true' do + let(:properties) do + default_properties.merge({ 'accept_proxy' => true }) + end + + it 'sets accept-proxy' do + expect(frontend_tcp_redis).to include('bind :6379 accept-proxy ssl') + expect(frontend_tcp_mysql).to include('bind :3306 accept-proxy ssl') + end + + context 'when ha_proxy.disable_tcp_accept_proxy is true' do + let(:properties) do + default_properties.merge({ 'accept_proxy' => true, 'disable_tcp_accept_proxy' => true }) + end + + it 'does not set accept-proxy' do + expect(frontend_tcp_redis).to include('bind :6379 ssl') + expect(frontend_tcp_mysql).to include('bind :3306 ssl') + end + end + end + end + + context 'when ha_proxy.tcp is not provided' do + let(:haproxy_conf) do + parse_haproxy_config(template.render({})) + end + + it 'is not included' do + expect(haproxy_conf).not_to have_key(/frontend tcp/) + end + end +end diff --git a/spec/haproxy/templates/haproxy_config/frontend_wss_spec.rb b/spec/haproxy/templates/haproxy_config/frontend_wss_spec.rb new file mode 100644 index 00000000..4db44903 --- /dev/null +++ b/spec/haproxy/templates/haproxy_config/frontend_wss_spec.rb @@ -0,0 +1,353 @@ +# frozen_string_literal: true + +require 'rspec' + +describe 'config/haproxy.config HTTPS Websockets frontend' do + let(:template) { haproxy_job.template('config/haproxy.config') } + + let(:haproxy_conf) do + parse_haproxy_config(template.render({ 'ha_proxy' => properties })) + end + + let(:frontend_wss) { haproxy_conf['frontend wss-in'] } + + # FIXME: wss frontend should error if neither ssl_pem or crt_list are + # Currently the config is invalid without one of these options + + let(:default_properties) do + { + 'enable_4443' => true, + 'ssl_pem' => 'ssl pem contents' + } + end + + let(:properties) { default_properties } + + context 'when ha_proxy.drain_enable is true' do + let(:properties) do + default_properties.merge({ 'drain_enable' => true }) + end + + it 'has a default grace period of 0 milliseconds' do + expect(frontend_wss).to include('grace 0') + end + + context('when ha_proxy.drain_frontend_grace_time is provided') do + let(:properties) do + default_properties.merge({ 'drain_enable' => true, 'drain_frontend_grace_time' => 12 }) + end + + # FIXME: if drain_frontend_grace_time is provided but drain_enable is false then it should error + + it 'overrides the grace period' do + expect(frontend_wss).to include('grace 12000') + end + end + end + + it 'binds to all interfaces by default' do + expect(frontend_wss).to include('bind :4443 ssl crt /var/vcap/jobs/haproxy/config/ssl') + end + + context 'when ha_proxy.binding_ip is provided' do + let(:properties) do + default_properties.merge({ 'binding_ip' => '1.2.3.4' }) + end + + it 'binds to the provided ip' do + expect(frontend_wss).to include('bind 1.2.3.4:4443 ssl crt /var/vcap/jobs/haproxy/config/ssl') + end + + context 'when ha_proxy.v4v6 is true and binding_ip is ::' do + let(:properties) do + default_properties.merge({ 'v4v6' => true, 'binding_ip' => '::' }) + end + + it 'enables ipv6' do + expect(frontend_wss).to include('bind :::4443 ssl crt /var/vcap/jobs/haproxy/config/ssl v4v6') + end + end + + context 'when ha_proxy.accept_proxy is true' do + let(:properties) do + default_properties.merge({ 'accept_proxy' => true }) + end + + it 'sets accept-proxy' do + expect(frontend_wss).to include('bind :4443 accept-proxy ssl crt /var/vcap/jobs/haproxy/config/ssl') + end + end + end + + context 'when mutual tls is enabled' do + let(:properties) do + default_properties.merge({ 'client_cert' => 'client_cert contents' }) + end + + it 'configures ssl to use the client ca' do + expect(frontend_wss).to include('bind :4443 ssl crt /var/vcap/jobs/haproxy/config/ssl ca-file /etc/ssl/certs/ca-certificates.crt verify optional') + end + + context 'when ha_proxy.client_cert_ignore_err is true' do + let(:properties) do + default_properties.merge({ 'client_cert' => 'client_cert contents', 'client_cert_ignore_err' => true }) + end + + # FIXME: if client_cert_ignore_err is true but client_cert is not provided, then it should error + + it 'adds the crt-ignore-err flag' do + expect(frontend_wss).to include('bind :4443 ssl crt /var/vcap/jobs/haproxy/config/ssl ca-file /etc/ssl/certs/ca-certificates.crt verify optional crt-ignore-err true') + end + end + + context 'when ha_proxy.client_revocation_list is provided' do + let(:properties) do + default_properties.merge({ 'client_cert' => 'client_cert contents', 'client_revocation_list' => 'client_revocation_list contents' }) + end + + # FIXME: if client_revocation_list is provided but client_cert is not provided, then it should error + + it 'references the crl list' do + expect(frontend_wss).to include('bind :4443 ssl crt /var/vcap/jobs/haproxy/config/ssl ca-file /etc/ssl/certs/ca-certificates.crt verify optional crl-file /var/vcap/jobs/haproxy/config/client-revocation-list.pem') + end + end + end + + describe 'X-Forwarded-Client-Cert header' do + context 'when forwarded_client_cert is always_forward_only (the default)' do + it 'deletes the X-Forwarded-Client-Cert header by default' do + expect(frontend_wss).to include('http-request del-header X-Forwarded-Client-Cert') + end + end + + context 'when forwarded_client_cert is forward_only' do + let(:properties) do + default_properties.merge({ 'forwarded_client_cert' => 'forward_only' }) + end + + it 'deletes the X-Forwarded-Client-Cert header' do + expect(frontend_wss).to include('http-request del-header X-Forwarded-Client-Cert') + end + + context 'when mutual TLS is enabled' do + let(:properties) do + default_properties.merge({ + 'client_cert' => 'client_cert contents', + 'forwarded_client_cert' => 'forward_only' + }) + end + + it 'only deletes the X-Forwarded-Client-Cert header when mTLS is not used' do + expect(frontend_wss).to include('http-request del-header X-Forwarded-Client-Cert if ! { ssl_c_used }') + end + end + end + + context 'when forwarded_client_cert is sanitize_set' do + let(:properties) do + default_properties.merge({ 'forwarded_client_cert' => 'sanitize_set' }) + end + + it 'deletes the X-Forwarded-Client-Cert header' do + expect(frontend_wss).to include('http-request del-header X-Forwarded-Client-Cert') + end + + context 'when mutual TLS is enabled' do + let(:properties) do + default_properties.merge({ + 'client_cert' => 'client_cert contents', + 'forwarded_client_cert' => 'sanitize_set' + }) + end + + it 'sets X-Forwarded-Client-Cert to the client cert for mTLS connections ' do + expect(frontend_wss).to include('http-request del-header X-Forwarded-Client-Cert') + expect(frontend_wss).to include('http-request set-header X-Forwarded-Client-Cert %[ssl_c_der,base64] if { ssl_c_used }') + end + end + end + end + + context 'when ha_proxy.hsts_enable is true' do + let(:properties) do + default_properties.merge({ 'hsts_enable' => true }) + end + + it 'sets the Strict-Transport-Security header' do + expect(frontend_wss).to include('http-response set-header Strict-Transport-Security max-age=31536000;') + end + + context 'when ha_proxy.hsts_max_age is provided' do + let(:properties) do + default_properties.merge({ 'hsts_enable' => true, 'hsts_max_age' => 9999 }) + end + + it 'sets the Strict-Transport-Security header with the correct max-age' do + expect(frontend_wss).to include('http-response set-header Strict-Transport-Security max-age=9999;') + end + end + + context 'when ha_proxy.hsts_include_subdomains is true' do + let(:properties) do + default_properties.merge({ 'hsts_enable' => true, 'hsts_include_subdomains' => true }) + end + + # FIXME: hsts_include_subdomains is true but hsts_enable is false, then it should error + + it 'sets the includeSubDomains flag' do + expect(frontend_wss).to include('http-response set-header Strict-Transport-Security max-age=31536000;\ includeSubDomains;') + end + end + + context 'when ha_proxy.hsts_preload is true' do + let(:properties) do + default_properties.merge({ 'hsts_enable' => true, 'hsts_preload' => true }) + end + + # FIXME: hsts_preload is true but hsts_enable is false, then it should error + + it 'sets the preload flag' do + expect(frontend_wss).to include('http-response set-header Strict-Transport-Security max-age=31536000;\ preload;') + end + end + end + + it 'correct request capturing configuration' do + expect(frontend_wss).to include('capture request header Host len 256') + end + + it 'has the correct default backend' do + expect(frontend_wss).to include('default_backend http-routers') + end + + context 'when ha_proxy.http_request_deny_conditions are provided' do + let(:properties) do + default_properties.merge({ + 'http_request_deny_conditions' => [{ + 'condition' => [{ + 'acl_name' => 'block_host', + 'acl_rule' => 'hdr_beg(host) -i login' + }, { + 'acl_name' => 'whitelist_ips', + 'acl_rule' => 'src 5.22.5.11 5.22.5.12', + 'negate' => true + }] + }] + }) + end + + it 'adds the correct acls and http-request deny rules' do + expect(frontend_wss).to include('acl block_host hdr_beg(host) -i login') + expect(frontend_wss).to include('acl whitelist_ips src 5.22.5.11 5.22.5.12') + + expect(frontend_wss).to include('http-request deny if block_host !whitelist_ips') + end + end + + context 'when a custom ha_proxy.frontend_config is provided' do + let(:properties) do + default_properties.merge({ 'frontend_config' => 'custom config content' }) + end + + it 'includes the custom config' do + expect(frontend_wss).to include('custom config content') + end + end + + context 'when a ha_proxy.cidr_whitelist is provided' do + let(:properties) do + default_properties.merge({ 'cidr_whitelist' => ['172.168.4.1/32', '10.2.0.0/16'] }) + end + + it 'sets the correct acl and content accept rules' do + expect(frontend_wss).to include('acl whitelist src -f /var/vcap/jobs/haproxy/config/whitelist_cidrs.txt') + expect(frontend_wss).to include('tcp-request content accept if whitelist') + end + end + + context 'when a ha_proxy.cidr_blacklist is provided' do + let(:properties) do + default_properties.merge({ 'cidr_blacklist' => ['172.168.4.1/32', '10.2.0.0/16'] }) + end + + it 'sets the correct acl and content reject rules' do + expect(frontend_wss).to include('acl blacklist src -f /var/vcap/jobs/haproxy/config/blacklist_cidrs.txt') + expect(frontend_wss).to include('tcp-request content reject if blacklist') + end + end + + context 'when ha_proxy.block_all is provided' do + let(:properties) do + default_properties.merge({ 'block_all' => true }) + end + + it 'sets the correct content reject rules' do + expect(frontend_wss).to include('tcp-request content reject') + end + end + + context 'when ha_proxy.headers are provided' do + let(:properties) do + default_properties.merge({ 'headers' => ['X-Application-ID: my-custom-header', 'MyCustomHeader: 3'] }) + end + + it 'adds the request headers' do + expect(frontend_wss).to include('http-request add-header X-Application-ID:\ my-custom-header ""') + expect(frontend_wss).to include('http-request add-header MyCustomHeader:\ 3 ""') + end + end + + context 'when ha_proxy.rsp_headers are provided' do + let(:properties) do + default_properties.merge({ 'rsp_headers' => ['X-Application-ID: my-custom-header', 'MyCustomHeader: 3'] }) + end + + it 'adds the response headers' do + expect(frontend_wss).to include('http-response add-header X-Application-ID:\ my-custom-header ""') + expect(frontend_wss).to include('http-response add-header MyCustomHeader:\ 3 ""') + end + end + + context 'when ha_proxy.internal_only_domains are provided' do + let(:properties) do + default_properties.merge({ 'internal_only_domains' => ['bosh.internal'] }) + end + + it 'adds the correct acl and http-request deny rules' do + expect(frontend_wss).to include('acl private src -f /var/vcap/jobs/haproxy/config/trusted_domain_cidrs.txt') + expect(frontend_wss).to include('acl internal hdr(Host) -m sub bosh.internal') + expect(frontend_wss).to include('http-request deny if internal !private') + end + end + + context 'when ha_proxy.routed_backend_servers are provided' do + let(:properties) do + default_properties.merge({ + 'routed_backend_servers' => { + '/images' => { + 'port' => 12_000, + 'servers' => ['10.0.0.1'] + } + } + }) + end + + it 'grants access to the backend servers' do + expect(frontend_wss).to include('acl routed_backend_9c1bb7 path_beg /images') + expect(frontend_wss).to include('use_backend http-routed-backend-9c1bb7 if routed_backend_9c1bb7') + end + end + + it 'adds the X-Forwarded-Proto header' do + expect(frontend_wss).to include('acl xfp_exists hdr_cnt(X-Forwarded-Proto) gt 0') + expect(frontend_wss).to include('http-request add-header X-Forwarded-Proto "https" if ! xfp_exists') + end + + context 'when websockets are not enabled (default)' do + let(:properties) { {} } + + it 'removes the wss frontend' do + expect(haproxy_conf).not_to have_key('frontend wss-in') + end + end +end diff --git a/spec/haproxy/templates/haproxy_config/global_and_default_options_spec.rb b/spec/haproxy/templates/haproxy_config/global_and_default_options_spec.rb new file mode 100644 index 00000000..a47a944d --- /dev/null +++ b/spec/haproxy/templates/haproxy_config/global_and_default_options_spec.rb @@ -0,0 +1,405 @@ +# frozen_string_literal: true + +require 'rspec' +require 'haproxy-tools' + +describe 'config/haproxy.config global and default options' do + let(:template) { haproxy_job.template('config/haproxy.config') } + + let(:haproxy_conf) do + parse_haproxy_config(template.render({ 'ha_proxy' => properties })) + end + + let(:global) { haproxy_conf['global'] } + let(:defaults) { haproxy_conf['defaults'] } + + let(:properties) { {} } + + it 'renders a valid haproxy template' do + expect do + HAProxy::Parser.new.parse(template.render({})) + end.not_to raise_error + end + + it 'has expected defaults' do + expect(defaults).to include('log global') + expect(defaults).to include('option log-health-checks') + expect(defaults).to include('option log-separate-errors') + expect(defaults).to include('option http-server-close') + expect(defaults).to include('option httplog') + expect(defaults).to include('option forwardfor') + expect(defaults).to include('option contstats') + end + + it 'has expected global options' do + expect(global).to include('daemon') + expect(global).to include('user vcap') + expect(global).to include('group vcap') + expect(global).to include('spread-checks 4') + expect(global).to include('stats timeout 2m') + end + + context 'when ha_proxy.raw_config is provided' do + it 'replaces the entire haproxy config contents' do + expect(template.render({ + 'ha_proxy' => { + 'raw_config' => 'custom_config' + } + })).to eq("custom_config\n") + end + end + + context 'when ha_proxy.syslog_server is provided' do + let(:properties) do + { + 'syslog_server' => '/my/server' + } + end + + it 'configures logging correctly' do + expect(global).to include('log /my/server len 1024 format raw syslog info') + end + end + + context 'when ha_proxy.log_max_length is provided' do + let(:properties) do + { + 'log_max_length' => 9999 + } + end + + it 'configures logging correctly' do + expect(global).to include('log stdout len 9999 format raw syslog info') + end + end + + context 'when ha_proxy.log_format is provided' do + let(:properties) do + { + 'log_format' => 'custom-format' + } + end + + it 'configures logging correctly' do + expect(global).to include('log stdout len 1024 format custom-format syslog info') + end + end + + context 'when ha_proxy.log_level is provided' do + let(:properties) do + { + 'log_level' => 'trace' + } + end + + it 'configures logging correctly' do + expect(global).to include('log stdout len 1024 format raw syslog trace') + end + end + + context 'when ha_proxy.global_config is provided' do + let(:properties) do + { + 'global_config' => 'custom-global-config' + } + end + + it 'adds custom global config' do + expect(global).to include('custom-global-config') + end + end + + context 'when ha_proxy.nbproc is provided' do + let(:properties) do + { + 'nbproc' => 3, + 'syslog_server' => '/dev/log' + } + end + + it 'configures the number of processes' do + expect(global).to include('nbproc 3') + end + + context 'when nbproc is more than 1' do + it 'configures a stats socket per process' do + expect(global).to include('stats socket /var/vcap/sys/run/haproxy/stats1.sock mode 600 expose-fd listeners level admin process 1') + expect(global).to include('stats socket /var/vcap/sys/run/haproxy/stats2.sock mode 600 expose-fd listeners level admin process 2') + expect(global).to include('stats socket /var/vcap/sys/run/haproxy/stats3.sock mode 600 expose-fd listeners level admin process 3') + end + end + + context 'when nbproc is 1' do + let(:properties) do + { + 'nbproc' => 1 + } + end + + it 'configures a single stats socket' do + expect(global).to include('stats socket /var/vcap/sys/run/haproxy/stats.sock mode 600 expose-fd listeners level admin') + end + end + + context 'when the syslog_server is the default and there is more than one process' do + let(:properties) do + { + 'nbproc' => 3 + } + end + + it 'returns a meaningful error message' do + expect do + global + end.to raise_error /ha_proxy.syslog_server cannot be stdout or stderr when ha_proxy.nbproc > 1/ + end + end + end + + context 'when ha_proxy.nbthread is provided' do + let(:properties) do + { + 'nbthread' => 7 + } + end + + it 'sets nbthread' do + expect(global).to include('nbthread 7') + end + end + + context 'when ha_proxy.disable_tls_10 is provided' do + let(:properties) do + { + 'disable_tls_10' => true + } + end + + it 'disables TLS 1.0' do + expect(global).to include('ssl-default-server-options no-sslv3 no-tlsv10 no-tls-tickets') + expect(global).to include('ssl-default-bind-options no-sslv3 no-tlsv10 no-tls-tickets') + end + end + + context 'when ha_proxy.disable_tls_11 is provided' do + let(:properties) do + { + 'disable_tls_11' => true + } + end + + it 'disables TLS 1.1' do + expect(global).to include('ssl-default-server-options no-sslv3 no-tlsv11 no-tls-tickets') + expect(global).to include('ssl-default-bind-options no-sslv3 no-tlsv11 no-tls-tickets') + end + end + + context 'when ha_proxy.disable_tls_tickets is provided' do + let(:properties) do + { + 'disable_tls_tickets' => false + } + end + + it 'enables TLS tickets when changed from default' do + expect(global).to include('ssl-default-server-options no-sslv3') + expect(global).to include('ssl-default-bind-options no-sslv3') + end + end + + context 'when ha_proxy.ssl_ciphers is provided' do + let(:properties) do + { + 'ssl_ciphers' => 'ECDHE-ECDSA-CHACHA20-POLY1305' + } + end + + it 'overrides the allowed ciphers' do + expect(global).to include('ssl-default-server-ciphers ECDHE-ECDSA-CHACHA20-POLY1305') + expect(global).to include('ssl-default-bind-ciphers ECDHE-ECDSA-CHACHA20-POLY1305') + end + end + + context 'when ha_proxy.max_connections is provided' do + let(:properties) do + { + 'max_connections' => 9999 + } + end + + it 'sets the number of max connections' do + expect(global).to include('maxconn 9999') + expect(defaults).to include('maxconn 9999') + end + end + + context 'when ha_proxy.reload_hard_stop_after is provided' do + let(:properties) do + { + 'reload_hard_stop_after' => '30m' + } + end + + it 'sets hard-stop-after' do + expect(global).to include('hard-stop-after 30m') + end + end + + context 'when ha_proxy.lua_scripts is provided' do + let(:properties) do + { + 'lua_scripts' => [ + '/var/vcap/packages/something/something/darkside.lua' + ] + } + end + + it 'includes the external lua script' do + expect(global).to include('lua-load /var/vcap/packages/something/something/darkside.lua') + end + end + + context 'when ha_proxy.default_dh_param is provided' do + let(:properties) do + { + 'default_dh_param' => 8888 + } + end + + it 'sets tune.ssl.default-dh-param' do + expect(global).to include('tune.ssl.default-dh-param 8888') + end + end + + context 'when ha_proxy.buffer_size_bytes is provided' do + let(:properties) do + { + 'buffer_size_bytes' => 7777 + } + end + + it 'sets tune.bufsize' do + expect(global).to include('tune.bufsize 7777') + end + end + + context 'when ha_proxy.max_rewrite is provided' do + let(:properties) do + { + 'max_rewrite' => 6666 + } + end + + it 'sets tune.maxrewrite' do + expect(global).to include('tune.maxrewrite 6666') + end + end + + context 'when ha_proxy.connect_timeout is provided' do + let(:properties) do + { + 'connect_timeout' => 4 + } + end + + it 'sets timeout connect in milliseconds' do + expect(defaults).to include(/timeout connect\s+4000ms/) + end + end + + context 'when ha_proxy.client_timeout is provided' do + let(:properties) do + { + 'client_timeout' => 5 + } + end + + it 'sets timeout client in milliseconds' do + expect(defaults).to include(/timeout client\s+5000ms/) + end + end + + context 'when ha_proxy.server_timeout is provided' do + let(:properties) do + { + 'server_timeout' => 6 + } + end + + it 'sets the timeout server in milliseconds' do + expect(defaults).to include(/timeout server\s+6000ms/) + end + end + + context 'when ha_proxy.websocket_timeout is provided' do + let(:properties) do + { + 'websocket_timeout' => 7 + } + end + + it 'sets the timeout tunnel in milliseconds' do + expect(defaults).to include(/timeout tunnel\s+7000ms/) + end + end + + context 'when ha_proxy.keepalive_timeout is provided' do + let(:properties) do + { + 'keepalive_timeout' => 8 + } + end + + it 'sets timeout http-keep-alive in milliseconds' do + expect(defaults).to include(/timeout http-keep-alive\s+8000ms/) + end + end + + context 'when ha_proxy.request_timeout is provided' do + let(:properties) do + { + 'request_timeout' => 9 + } + end + + it 'sets timeout http-request in milliseconds' do + expect(defaults).to include(/timeout http-request\s+9000ms/) + end + end + + context 'when ha_proxy.queue_timeout is provided' do + let(:properties) do + { + 'queue_timeout' => 10 + } + end + + it 'sets the timeout queue in milliseconds' do + expect(defaults).to include(/timeout queue\s+10000ms/) + end + end + + context 'when ha_proxy.default_config is provided' do + let(:properties) do + { + 'default_config' => 'my default config' + } + end + + it 'appends the custom default config' do + expect(defaults).to include('my default config') + end + end + + context 'when ha_proxy.backend_prefer_local_az is provided' do + let(:properties) do + { + 'backend_prefer_local_az' => true + } + end + + it 'enables the allbackups options' do + expect(defaults).to include('option allbackups') + end + end +end diff --git a/spec/haproxy/templates/haproxy_config/healthcheck_listener_spec.rb b/spec/haproxy/templates/haproxy_config/healthcheck_listener_spec.rb new file mode 100644 index 00000000..d413d700 --- /dev/null +++ b/spec/haproxy/templates/haproxy_config/healthcheck_listener_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'rspec' +require 'haproxy-tools' + +describe 'config/haproxy.config healthcheck listeners' do + let(:template) { haproxy_job.template('config/haproxy.config') } + + let(:haproxy_conf) do + parse_haproxy_config(template.render({ 'ha_proxy' => properties })) + end + + context 'when ha_proxy.enable_health_check_http is true' do + let(:healthcheck_listener) { haproxy_conf['listen health_check_http_url'] } + + let(:properties) do + { + 'enable_health_check_http' => true + } + end + + it 'adds a health check listener for the http-routers' do + expect(healthcheck_listener).to include('bind :8080') + expect(healthcheck_listener).to include('mode http') + expect(healthcheck_listener).to include('monitor-uri /health') + expect(healthcheck_listener).to include('acl http-routers_down nbsrv(http-routers) eq 0') + expect(healthcheck_listener).to include('monitor fail if http-routers_down') + end + + context 'when health_check_port is not the default' do + let(:properties) do + { + 'enable_health_check_http' => true, + 'health_check_port' => 1234 + } + end + + it 'sets the correct port' do + expect(healthcheck_listener).to include('bind :1234') + end + end + end +end diff --git a/spec/haproxy/templates/haproxy_config/resolvers_spec.rb b/spec/haproxy/templates/haproxy_config/resolvers_spec.rb new file mode 100644 index 00000000..347c7359 --- /dev/null +++ b/spec/haproxy/templates/haproxy_config/resolvers_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'rspec' +require 'haproxy-tools' + +describe 'config/haproxy.config resolvers' do + let(:template) { haproxy_job.template('config/haproxy.config') } + + let(:haproxy_conf) do + parse_haproxy_config(template.render({ 'ha_proxy' => properties })) + end + + context 'when ha_proxy.resolvers are provided' do + let(:resolvers_default) { haproxy_conf['resolvers default'] } + + let(:default_properties) do + { + 'resolvers' => [ + { 'public' => '1.1.1.1' }, + { 'private' => '10.1.1.1' } + ] + } + end + + let(:properties) { default_properties } + + it 'configures a resolver' do + expect(resolvers_default).to include('hold valid 10s') + expect(resolvers_default).to include('timeout retry 1s') + expect(resolvers_default).to include('resolve_retries 3') + expect(resolvers_default).to include('nameserver public 1.1.1.1:53') + expect(resolvers_default).to include('nameserver private 10.1.1.1:53') + end + + context 'when ha_proxy.dns_hold is provided' do + let(:properties) { default_properties.merge({ 'dns_hold' => '30s' }) } + + it 'overrides the dns hold for the resolver' do + expect(resolvers_default).to include('hold valid 30s') + end + end + + context 'when ha_proxy.resolve_retry_timeout is provided' do + let(:properties) { default_properties.merge({ 'resolve_retry_timeout' => '5s' }) } + + it 'overrides the resolve_retry_timeout for the resolver' do + expect(resolvers_default).to include('timeout retry 5s') + end + end + + context 'when ha_proxy.resolve_retries is provided' do + let(:properties) { default_properties.merge({ 'resolve_retries' => 10 }) } + + it 'overrides the resolve_retries for the resolver' do + expect(resolvers_default).to include('resolve_retries 10') + end + end + end +end diff --git a/spec/haproxy/templates/haproxy_config/stats_listener_spec.rb b/spec/haproxy/templates/haproxy_config/stats_listener_spec.rb new file mode 100644 index 00000000..64c6166b --- /dev/null +++ b/spec/haproxy/templates/haproxy_config/stats_listener_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require 'rspec' +require 'haproxy-tools' + +describe 'config/haproxy.config stats listener' do + let(:template) { haproxy_job.template('config/haproxy.config') } + + let(:haproxy_conf) do + parse_haproxy_config(template.render({ 'ha_proxy' => properties })) + end + + context 'when ha_proxy.stats_enable is true' do + let(:default_properties) do + { + 'nbproc' => 2, + 'syslog_server' => '/dev/log', + 'stats_enable' => true, + 'stats_user' => 'admin', + 'stats_password' => 'secret', + 'stats_uri' => 'foo' + } + end + + let(:properties) { default_properties } + + let(:stats_listener_proc1) { haproxy_conf['listen stats_1'] } + let(:stats_listener_proc2) { haproxy_conf['listen stats_2'] } + + it 'sets up a stats listener for each process' do + expect(stats_listener_proc1).to include('bind *:9000') + expect(stats_listener_proc1).to include('bind-process 1') + expect(stats_listener_proc1).to include('acl private src 0.0.0.0/32') + expect(stats_listener_proc1).to include('http-request deny unless private') + expect(stats_listener_proc1).to include('mode http') + expect(stats_listener_proc1).to include('stats enable') + expect(stats_listener_proc1).to include('stats hide-version') + expect(stats_listener_proc1).to include('stats realm "Haproxy Statistics"') + expect(stats_listener_proc1).to include('stats uri /foo') + expect(stats_listener_proc1).to include('stats auth admin:secret') + + expect(stats_listener_proc2).to include('bind *:9001') + expect(stats_listener_proc2).to include('bind-process 2') + expect(stats_listener_proc2).to include('acl private src 0.0.0.0/32') + expect(stats_listener_proc2).to include('http-request deny unless private') + expect(stats_listener_proc2).to include('mode http') + expect(stats_listener_proc2).to include('stats enable') + expect(stats_listener_proc2).to include('stats hide-version') + expect(stats_listener_proc2).to include('stats realm "Haproxy Statistics"') + expect(stats_listener_proc2).to include('stats uri /foo') + expect(stats_listener_proc2).to include('stats auth admin:secret') + end + + context 'when ha_proxy.trusted_stats_cidrs is set' do + let(:properties) do + default_properties.merge({ 'trusted_stats_cidrs' => '1.2.3.4/32' }) + end + + it 'has the correct acl' do + expect(stats_listener_proc1).to include('acl private src 1.2.3.4/32') + expect(stats_listener_proc2).to include('acl private src 1.2.3.4/32') + end + end + + context 'when ha_proxy.stats_bind is set' do + let(:properties) do + default_properties.merge({ 'stats_bind' => '1.2.3.4:5000' }) + end + + it 'overrides the default bind address' do + expect(stats_listener_proc1).to include('bind 1.2.3.4:5000') + expect(stats_listener_proc2).to include('bind 1.2.3.4:5001') + end + end + end +end diff --git a/spec/haproxy/templates/ssl_redirect.map_spec.rb b/spec/haproxy/templates/ssl_redirect.map_spec.rb new file mode 100644 index 00000000..cf13e2e6 --- /dev/null +++ b/spec/haproxy/templates/ssl_redirect.map_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'rspec' + +describe 'config/ssl_redirect.map' do + let(:template) { haproxy_job.template('config/ssl_redirect.map') } + + context 'when ha_proxy.https_redirect_domains is provided' do + it 'has the correct contents' do + expect(template.render({ + 'ha_proxy' => { + 'https_redirect_domains' => [ + 'google.com', + 'bing.com' + ] + } + })).to eq(<<~EXPECTED) + + google.com true + + bing.com true + + EXPECTED + end + end + + context 'when ha_proxy.https_redirect_domains is not provided' do + it 'is empty' do + expect(template.render({})).to be_a_blank_string + end + end +end diff --git a/spec/haproxy/templates/trusted_domain_cidrs.txt_spec.rb b/spec/haproxy/templates/trusted_domain_cidrs.txt_spec.rb new file mode 100644 index 00000000..54deb8d8 --- /dev/null +++ b/spec/haproxy/templates/trusted_domain_cidrs.txt_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'rspec' + +describe 'config/trusted_domain_cidrs.txt' do + let(:template) { haproxy_job.template('config/trusted_domain_cidrs.txt') } + + describe 'ha_proxy.trusted_domain_cidrs' do + context 'when a space-separated list of cidrs is provided' do + it 'has the correct contents' do + expect(template.render({ + 'ha_proxy' => { + 'trusted_domain_cidrs' => '10.0.0.0/8 192.168.2.0/24' + } + })).to eq(<<~EXPECTED) + # generated from trusted_domain_cidrs.txt.erb + + # BEGIN trusted_domain cidrs + 10.0.0.0/8 + 192.168.2.0/24 + + # END trusted_domain cidrs + + EXPECTED + end + end + + # FIXME: this feature does not seem to be documented + context 'when a newline-separated, gzipped, base64-encoded list of cidrs is provided' do + it 'has the correct contents' do + expect(template.render({ + 'ha_proxy' => { + 'trusted_domain_cidrs' => gzip_and_b64_encode("10.0.0.0/8\n192.168.2.0/24") + } + })).to eq(<<~EXPECTED) + # generated from trusted_domain_cidrs.txt.erb + + # BEGIN trusted_domain cidrs + 10.0.0.0/8 + 192.168.2.0/24 + # END trusted_domain cidrs + + EXPECTED + end + end + + context 'when ha_proxy.trusted_domain_cidrs is not provided' do + it 'is empty' do + expect(template.render({})).to be_a_blank_string + end + end + end +end diff --git a/spec/haproxy/templates/whitelist_cidrs.txt_spec.rb b/spec/haproxy/templates/whitelist_cidrs.txt_spec.rb new file mode 100644 index 00000000..330d5072 --- /dev/null +++ b/spec/haproxy/templates/whitelist_cidrs.txt_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'rspec' + +describe 'config/whitelist_cidrs.txt' do + let(:template) { haproxy_job.template('config/whitelist_cidrs.txt') } + + context 'when ha_proxy.cidr_whitelist is provided' do + context 'when an array of cidrs is provided' do + it 'has the correct contents' do + expect(template.render({ + 'ha_proxy' => { + 'cidr_whitelist' => [ + '10.0.0.0/8', + '192.168.2.0/24' + ] + } + })).to eq(<<~EXPECTED) + # generated from whitelist_cidrs.txt.erb + + # BEGIN whitelist cidrs + # detected cidrs provided as array in cleartext format + 10.0.0.0/8 + 192.168.2.0/24 + + # END whitelist cidrs + + EXPECTED + end + end + + context 'when a base64-encoded, gzipped config is provided' do + it 'has the correct contents' do + expect(template.render({ + 'ha_proxy' => { + 'cidr_whitelist' => gzip_and_b64_encode(<<~INPUT) + 10.0.0.0/8 + 192.168.2.0/24 + INPUT + } + })).to eq(<<~EXPECTED) + # generated from whitelist_cidrs.txt.erb + + # BEGIN whitelist cidrs + 10.0.0.0/8 + 192.168.2.0/24 + + # END whitelist cidrs + + EXPECTED + end + end + end + + context 'when ha_proxy.cidr_whitelist is not provided' do + it 'is empty' do + expect(template.render({})).to be_a_blank_string + end + end +end diff --git a/spec/haproxy_templates_spec.rb b/spec/haproxy_templates_spec.rb deleted file mode 100644 index 871628ed..00000000 --- a/spec/haproxy_templates_spec.rb +++ /dev/null @@ -1,177 +0,0 @@ -# rubocop: disable LineLength -# rubocop: disable BlockLength -require 'rspec' -require 'bosh/template/test' -require 'yaml' -require 'json' -require 'haproxy-tools' -require 'pry' -require 'tempfile' - -describe 'haproxy' do - let(:release_path) { File.join(File.dirname(__FILE__), '..') } - let(:release) { Bosh::Template::Test::ReleaseDir.new(release_path) } - let(:job) { release.job('haproxy') } - - let(:default_manifest_properties) do - { - 'ha_proxy' => { - 'threads' => 1, - 'nbproc' => 1, - 'nbthread' => 1, - 'syslog_server' => 'stdout', - 'log_level' => 'info', - 'buffer_size_bytes' => '16384', - 'internal_only_domains' => [], - 'trusted_domain_cidrs' => '0.0.0.0/32', - 'strict_sni' => false, - 'ssl_pem' => nil, - 'crt_list' => nil, - 'reload_hard_stop_after' => '5m', - 'reload_max_instances' => 4, - 'enable_health_check_http' => false, - 'health_check_port' => '8080', - 'disable_http' => false, - 'enable_4443' => false, - 'https_redirect_domains' => [], - 'https_redirect_all' => false, - 'ssl_ciphers' => 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS', - 'hsts_enable' => false, - 'hsts_max_age' => '31536000', - 'hsts_include_subdomains' => false, - 'hsts_preload' => false, - 'default_dh_param' => 2048, - 'disable_tls_tickets' => false, - 'disable_tls_10' => false, - 'disable_tls_11' => false, - 'connect_timeout' => 5, - 'client_timeout' => 30, - 'server_timeout' => 30, - 'websocket_timeout' => 3600, - 'keepalive_timeout' => 6, - 'request_timeout' => 5, - 'queue_timeout' => 30, - 'stats_enable' => false, - 'stats_bind' => '*:9000', - 'stats_uri' => 'haproxy_stats', - 'trusted_stats_cidrs' => '0.0.0.0/32', - 'backend_servers' => [], - 'backend_ssl' => 'off', - 'backend_port' => '80', - 'compress_types' => '', - 'routed_backend_servers' => {}, - 'client_cert' => false, - 'forwarded_client_cert' => 'sanitize_set', - 'tcp' => [], - 'dns_hold' => '10s', - 'resolve_retry_timeout' => '1s', - 'resolve_retries' => 3, - 'accept_proxy' => false, - 'disable_tcp_accept_proxy' => false, - 'binding_ip' => '', - 'v4v6' => false, - 'cidr_blacklist' => '~', - 'cidr_whitelist' => '~', - 'block_all' => false, - 'tcp_routing' => { - 'port_range' => '1024-1123' - }, - 'lua_scripts' => [], - 'backend_use_http_health' => false, - 'backend_http_health_uri' => '/health', - 'backend_http_health_port' => 8080, - 'max_open_files' => '256000', - 'max_connections' => '64000', - 'drain_enable' => false, - 'drain_timeout' => 30, - 'drain_frontend_grace_time' => 0, - 'backend_prefer_local_az' => false - } - } - end - -def dumpConfig(src, dst) - file = File.open(src) - file_content = file.read - File.open(dst, "w") { |f| f.write file_content } - file.close() -end - - describe 'config/haproxy.config' do - let(:template) { job.template('config/haproxy.config') } - let(:config_file) { Tempfile.new(['config', '.cfg']) } - let(:manifest_properties) { default_manifest_properties } - - before :each do - rendered_template = template.render(manifest_properties) - config_file.write(rendered_template) - config_file.rewind - end - - after do - config_file.close - config_file.unlink - end - - describe 'when given a valid set of properties' do - it 'renders a valid haproxy template' do - expect{HAProxy::Config.parse_file(config_file.path)}.to_not raise_error - end - end - - describe 'when custom_http_error_files are provided' do - let(:manifest_properties) do - default_manifest_properties['ha_proxy']['custom_http_error_files'] = { - "503" => "content of errorfile 503", - "403" => "content of errorfile 403" - } - default_manifest_properties - end - - it 'set all errorfiles with theirs error status and location' do - rendered_hash = HAProxy::Config.parse_file(config_file.path) - expect(rendered_hash.backend('http-routers').config['errorfile 503']).to eq("/var/vcap/jobs/haproxy/errorfiles/custom503.http") - expect(rendered_hash.backend('http-routers').config['errorfile 403']).to eq("/var/vcap/jobs/haproxy/errorfiles/custom403.http") - end - end - - describe 'when a tcp backend is provided' do - tcp_backend = { - "name" => "wss", - "port" => 4443, - "backend_servers" => ["10.20.10.10", "10.20.10.11"], - "balance" => "roundrobin", - "backend_port" => 80, - "ssl" => true, - "backend_ssl" => "verify", - "backend_verifyhost" => "example.com", - "health_check_http" => 4444 - } - - describe 'when not using tcp_link_check_port' do - let(:manifest_properties) do - default_manifest_properties['ha_proxy']['tcp'] = [ tcp_backend ] - default_manifest_properties - end - - it 'uses the backend port for health checks' do - rendered_hash = HAProxy::Config.parse_file(config_file.path) - expect(rendered_hash.backend('tcp-wss').servers['node0'].attributes['port']).to eq("80") - end - end - - describe 'when using tcp_link_check_port' do - let(:manifest_properties) do - default_manifest_properties['ha_proxy']['tcp'] = [ tcp_backend ] - default_manifest_properties['ha_proxy']['tcp_link_check_port'] = 1234 - - default_manifest_properties - end - it 'uses the tcp_link_check_port port for health checks instead' do - rendered_hash = HAProxy::Config.parse_file(config_file.path) - expect(rendered_hash.backend('tcp-wss').servers['node0'].attributes['port']).to eq("1234") - end - end - end - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 00000000..c9f3ccdc --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require 'bosh/template/test' +require 'base64' +require 'zlib' +require 'stringio' +require 'rubygems/package' +require 'deep_merge' + +module SharedContext + extend RSpec::SharedContext + + let(:release_path) { File.join(File.dirname(__FILE__), '..') } + let(:release) { Bosh::Template::Test::ReleaseDir.new(release_path) } + let(:haproxy_job) { release.job('haproxy') } +end + +RSpec.configure do |config| + config.include SharedContext +end + +RSpec::Matchers.define :be_a_blank_string do + match do |thing| + thing =~ /^\s*$/ + end +end + +def gzip_and_b64_encode(input) + io = StringIO.new + gz = Zlib::GzipWriter.new(io) + gz.write(input) + gz.close + Base64.encode64(io.string) +end + +# extracts entry from ttar format +# https://github.com/ideaship/ttar +def ttar_entry(ttar, path) + entries = ttar.split(/========================== 0600 (.*)/) + paths = [] + entries.each.with_index do |e, i| + return entries[i + 1] if e == path + + paths.push(e) if e =~ %r{/var/vcap} + end + + raise "Entry #{path} not found in ttar, found: #{paths.inspect}" +end + +# converts haproxy config into hash of arrays grouped +# by top-level values eg +# { +# "global" => [ +# "nbproc 4", +# "daemon", +# "stats timeout 2m" +# ] +# } +def parse_haproxy_config(config) # rubocop:disable Metrics/AbcSize + # remove comments and empty lines + config = config.split(/\n/).reject { |l| l.empty? || l =~ /^\s*#.*$/ }.join("\n") + + # split into top-level groups + config.split(/^([^\s].*)/).drop(1).each_slice(2).map do |group| + key = group[0] + properties = group[1] + + # remove empty lines + properties = properties.split(/\n/).reject(&:empty?).join("\n") + + # unindent propertiess + properties = properties.gsub(/^\s+/, '') + + # split and strip leading/trailing whitespace + properties = properties.split(/\n/).map(&:strip) + + [key, properties] + end.to_h +end