├── .dockerignore ├── .gitignore ├── .rspec ├── .travis.yml ├── DEVELOPMENT.md ├── Dockerfile ├── Dockerfile.testhelper ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── bin └── lb ├── build ├── edge.sh └── latest.sh ├── docker-compose.test.yml ├── entrypoint.sh ├── errors ├── 200.http ├── 502.http ├── 503.http └── 504.http ├── lib ├── kontena │ ├── acme_challenges.rb │ ├── actors │ │ ├── acme_challenge_server.rb │ │ ├── etcd_watcher.rb │ │ ├── haproxy_config_generator.rb │ │ ├── haproxy_config_writer.rb │ │ ├── haproxy_process.rb │ │ ├── haproxy_spawner.rb │ │ ├── lb_supervisor.rb │ │ └── syslog_server.rb │ ├── cert_manager.rb │ ├── cert_splitter.rb │ ├── logging.rb │ ├── models │ │ ├── common_service.rb │ │ ├── service.rb │ │ ├── tcp_service.rb │ │ └── upstream.rb │ ├── templates │ │ └── haproxy │ │ │ ├── _stats.text.erb │ │ │ ├── http_backends.text.erb │ │ │ ├── http_in.text.erb │ │ │ ├── main.text.erb │ │ │ └── tcp_proxies.text.erb │ └── views │ │ ├── common.rb │ │ ├── haproxy.rb │ │ ├── http_backends.rb │ │ ├── http_in.rb │ │ └── tcp_proxies.rb └── kontena_lb.rb ├── prepare_test.sh ├── spec ├── fixtures │ └── ssl │ │ ├── bundle1.pem │ │ ├── bundle2_invalid.pem │ │ └── test1.pem ├── kontena │ ├── acme_challenges_spec.rb │ ├── actors │ │ ├── acme_challenge_server_spec.rb │ │ ├── etcd_watcher_spec.rb │ │ ├── haproxy_config_generator_spec.rb │ │ ├── haproxy_config_writer_spec.rb │ │ ├── haproxy_spawner_spec.rb │ │ ├── lb_supervisor_spec.rb │ │ └── syslog_server_spec.rb │ ├── cert_manager_spec.rb │ ├── cert_splitter_spec.rb │ ├── models │ │ ├── service_spec.rb │ │ ├── tcp_service_spec.rb │ │ └── upstream_spec.rb │ └── views │ │ ├── haproxy_spec.rb │ │ ├── http_backends_spec.rb │ │ ├── http_in_spec.rb │ │ └── tcp_proxies_spec.rb ├── spec_helper.rb └── support │ └── fixtures_helper.rb └── test ├── acme_challenge_test.bats ├── basic_auth_test.bats ├── ciphers.bats ├── common.bash ├── cookie_stickyness_test.bats ├── custom_settings_test.bats ├── empty_upstreams_test.bats ├── error_page_test.bats ├── global_settings_test.bats ├── health_checks_test.bats ├── redirect_test.bats ├── server ├── Dockerfile ├── Gemfile ├── Gemfile.lock └── config.ru ├── ssl ├── localhost │ ├── cert.pem │ └── key.pem └── test-1 │ ├── cert.pem │ └── key.pem ├── ssl_config_tests.bats ├── ssl_disabled_test.bats ├── ssl_test.bats ├── virtual_host_test.bats └── virtual_path_test.bats /.dockerignore: -------------------------------------------------------------------------------- 1 | *.pem 2 | .env 3 | test/ 4 | docker-compose.test.yml 5 | docker-compose.yml 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pem 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: ruby 3 | services: 4 | - docker 5 | rvm: 6 | - 2.4.1 7 | env: 8 | global: 9 | - DOCKER_USERNAME=kontenabot 10 | - secure: "ggCnij9ZOhZ19tQRKKCo3RJGbtbBbqCHGzbfHJfJBKdzKtsqDAHrlz3VmajS2UALZ9TyXLTyM2Qt5nH0CO8VUaxSxOi53r1wFmg7RFGGTg2rdkQ/Vf/lXIQANzO9ICjTTGTArbE8KNy0Ri1Z0gvlDXYOBLtSaJHE1xTaKSGr54S4LjhNZox7R3frN8qcqHH37ygLlpe0dAZbdXeBls5e4/CeBdHrZNmwY9iRJiwDGYLHvC37QD9WEXY0vua8f1qU/95MuTGoRkbpoEDJJ/epFVEolcaYjtj7Y1Gf2+CG60WqjYmGSD9xrxwbVVj7JH+eTRqUb8NZYD9b7Lpe/m9P6JN/9FmqIHJ5XYenoKoPlyLbLXPkP+8bk8e9EBLwbROopR8+tqKxHmel6twajDhMd6MCxWApzsMfN9BntaLBKce0DuepdyIcgd8XGhZVqPxVUSXGjC8UmKafI9hTDIBsKGMyarJ5ZT4ZAF1o9Wx5aSte5jsgry0DGOOr9GFxgQobuHm8RsDt2osG4dDkeOC9XbPa/4whBPdTgHEBoIhakZAh/4vqWSa6L6a98ppSjpSmxz9LVk0+aVlmV8763oEvhGa3ibnYsYRGNDORAxbO+rOQowmvcIWq6yHaUBwWeeeGU7Y//Hq65QjY/ZPp6Z2kSE4+wSu/9nmuc88xdTIgGWI=" # for docker hub 11 | cache: bundler 12 | script: 13 | - bundle exec rspec spec/ 14 | - ./prepare_test.sh && bats test/ 15 | deploy: 16 | - provider: script 17 | script: ./build/edge.sh 18 | on: 19 | branch: master 20 | rvm: 2.4.1 21 | - provider: script 22 | script: ./build/latest.sh 23 | on: 24 | tags: true 25 | rvm: 2.4.1 -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | Kontena loadbalancer uses confd to generate up-to-date config for the HAProxy 4 | 5 | 6 | # Testing 7 | 8 | Testing the loadbalancer is done locally using excellent [Bats](https://github.com/sstephenson/bats) framework. 9 | 10 | ## Setup 11 | 12 | ### Install Bats 13 | 14 | Install Bats, e.g. on OSX you can do it with `brew install bats` 15 | 16 | ### Prepare test Docker images and helpers 17 | 18 | ``` 19 | ./prepare_test.sh 20 | ``` 21 | 22 | The script builds few Docker images and runs couple of instances of the loadbalancer with Docker compose. 23 | 24 | When the script exits your test cases are ready to be run. 25 | 26 | ## Run the tests 27 | 28 | To run all test cases run: 29 | ``` 30 | $ bats test/ 31 | ✓ basic auth gives 401 without user and password 32 | ✓ basic auth gives 200 with valid user and password 33 | ✓ basic auth gives 200 with valid user and password, password encrypted 34 | ✓ supports cookie stickyness 35 | ✓ supports cookie stickyness with custom cookie config 36 | ✓ supports cookie stickyness with custom cookie prefix 37 | ✓ returns custom error page 38 | ✓ returns health check page if configured in env 39 | ✓ returns error if health not configured in env 40 | ✓ supports health check uri setting for balanced service 41 | ✓ redirects *.foo.com -> foo.com 42 | ✓ supports ssl with invalid cert ignored 43 | ✓ supports virtual_hosts 44 | ✓ supports wildcard virtual_hosts 45 | ✓ supports virtual_hosts + virtual_path 46 | ✓ supports virtual_hosts + virtual_path + keep_virtual_path 47 | ✓ handles empty upstreams 48 | ✓ on duplicate virtual_hosts first one in alphabets wins 49 | ✓ prioritizes first vhost+vpath, then vhost and finally vpath 50 | ✓ works with domain:port host header 51 | ✓ supports virtual_path 52 | ✓ supports virtual_path + keep_virtual_path 53 | 54 | 22 tests, 0 failures 55 | ``` 56 | 57 | The tests are organized so that each `.bats` file container logically related test cases, a.k.a test suite. 58 | 59 | If you want to run individual suite run: 60 | ``` 61 | $ bats test/virtual_path_test.bats 62 | ✓ supports virtual_path 63 | ✓ supports virtual_path + keep_virtual_path 64 | 65 | 2 tests, 0 failures 66 | ``` 67 | 68 | ## Writing test cases 69 | 70 | Typical test case has three steps: 71 | - Write proper data to etcd 72 | - Wait for ConfD to reload the config 73 | - Test with actual HTTP request(s) 74 | 75 | See existing test cases for "template" 76 | 77 | ## Test against new changes in LB 78 | 79 | When you make changes to LB you must naturally re-build and re-run the docker containers. If you've started the base setup with `prepare_test.sh` script you can stop the existing containers with: 80 | ``` 81 | $ docker-compose -f docker-compose.test.yml stop 82 | ``` 83 | 84 | To re-build images and containers issue: 85 | ``` 86 | $ docker-compose -f docker-compose.test.yml up --build -d 87 | ``` 88 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM haproxy:1.8.14-alpine 2 | MAINTAINER Kontena, Inc. 3 | 4 | ENV STATS_PASSWORD=secret \ 5 | PATH="/app/bin:${PATH}" 6 | 7 | RUN apk update && apk --update add bash tzdata ruby ruby-irb ruby-bigdecimal \ 8 | ruby-io-console ruby-json ruby-rake ca-certificates libssl1.0 openssl libstdc++ \ 9 | ruby-webrick ruby-etc 10 | 11 | ADD Gemfile Gemfile.lock /app/ 12 | 13 | RUN apk --update add --virtual build-dependencies ruby-dev build-base openssl-dev && \ 14 | gem install bundler --no-ri --no-rdoc && \ 15 | cd /app ; bundle install --without development test && \ 16 | apk del build-dependencies 17 | 18 | ADD . /app 19 | ADD errors/* /etc/haproxy/errors/ 20 | EXPOSE 80 443 21 | WORKDIR /app 22 | 23 | ENTRYPOINT ["/app/entrypoint.sh"] 24 | -------------------------------------------------------------------------------- /Dockerfile.testhelper: -------------------------------------------------------------------------------- 1 | FROM alpine:3.5 2 | 3 | RUN apk update && apk --update add curl && \ 4 | curl -sL https://github.com/coreos/etcd/releases/download/v2.3.7/etcd-v2.3.7-linux-amd64.tar.gz -o etcd-v2.3.7-linux-amd64.tar.gz && \ 5 | tar xzvf etcd-v2.3.7-linux-amd64.tar.gz && \ 6 | mv etcd-v2.3.7-linux-amd64/etcdctl /usr/bin/ && \ 7 | rm etcd-v2.3.7-linux-amd64.tar.gz && \ 8 | rm -rf etcd-v2.3.7-linux-amd64/ 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'concurrent-ruby', '~> 1.0' 4 | gem 'concurrent-ruby-edge' 5 | gem 'hanami-view', '~> 1.0.0' 6 | gem 'etcd' 7 | 8 | group :development, :test do 9 | gem 'rspec', require: false 10 | end 11 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | concurrent-ruby (1.0.5) 5 | concurrent-ruby-edge (0.3.1) 6 | concurrent-ruby (= 1.0.5) 7 | diff-lcs (1.2.5) 8 | etcd (0.3.0) 9 | mixlib-log 10 | hanami-utils (1.0.2) 11 | transproc (~> 1.0) 12 | hanami-view (1.0.1) 13 | hanami-utils (~> 1.0) 14 | tilt (~> 2.0, >= 2.0.1) 15 | mixlib-log (1.6.0) 16 | rspec (3.4.0) 17 | rspec-core (~> 3.4.0) 18 | rspec-expectations (~> 3.4.0) 19 | rspec-mocks (~> 3.4.0) 20 | rspec-core (3.4.4) 21 | rspec-support (~> 3.4.0) 22 | rspec-expectations (3.4.0) 23 | diff-lcs (>= 1.2.0, < 2.0) 24 | rspec-support (~> 3.4.0) 25 | rspec-mocks (3.4.1) 26 | diff-lcs (>= 1.2.0, < 2.0) 27 | rspec-support (~> 3.4.0) 28 | rspec-support (3.4.1) 29 | tilt (2.0.8) 30 | transproc (1.0.2) 31 | 32 | PLATFORMS 33 | ruby 34 | 35 | DEPENDENCIES 36 | concurrent-ruby (~> 1.0) 37 | concurrent-ruby-edge 38 | etcd 39 | hanami-view (~> 1.0.0) 40 | rspec 41 | 42 | BUNDLED WITH 43 | 1.15.3 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | Copyright 2015 Kontena, Inc. 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kontena Load Balancer 2 | 3 | Kontena Load Balancer is a HAproxy / confd service that is configured to watch changes in etcd. Load Balancers may be described in kontena.yml and services are connected automatically by linking services to these load balancer services. If load balanced service is scaled/re-deployed then the load balancer will reload it's configuration on the fly without dropping connections. 4 | 5 | The Kontena Load Balancer key features: 6 | 7 | * Zero downtime when load balancer configuration changes 8 | * Fully automated configuration 9 | * Dynamic routing 10 | * Support for TCP and HTTP traffic 11 | * SSL termination on multiple certificates 12 | * Link certificates from Kontena Vault 13 | 14 | ## Getting Started 15 | 16 | Please see our [Load Balancer](https://www.kontena.io/docs/using-kontena/loadbalancer) guide. 17 | 18 | ## Contact Us 19 | 20 | Found a bug? Suggest a feature? Have a question? Please [submit an issue](https://github.com/kontena/kontena/issues) or email us at info@kontena.io. 21 | 22 | Follow us on Twitter: [@KontenaInc](https://twitter.com/KontenaInc). 23 | 24 | Gitter: [Join chat](https://gitter.im/kontena/kontena). 25 | 26 | ## License 27 | 28 | Kontena software is open source, and you can use it for any purpose, personal or commercial. Kontena is licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for full license text. 29 | -------------------------------------------------------------------------------- /bin/lb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $stdout.sync = true 4 | require_relative '../lib/kontena_lb' 5 | 6 | puts "~~ Kontena LoadBalancer ~~\n\n" 7 | 8 | Kontena::CertManager.boot 9 | 10 | supervisor = Kontena::Actors::LbSupervisor.spawn!(name: 'lb_supervisor', link: true) 11 | supervisor << :start 12 | 13 | sleep 14 | -------------------------------------------------------------------------------- /build/edge.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -z "$DOCKER_USERNAME" ] && [ ! -z "$DOCKER_PASSWORD" ]; then 4 | docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" 5 | docker build -t kontena/lb:edge . 6 | docker push kontena/lb:edge 7 | fi -------------------------------------------------------------------------------- /build/latest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -z "$TRAVIS_TAG" ] && [ ! -z "$DOCKER_PASSWORD" ]; then 4 | docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" 5 | TAG=${TRAVIS_TAG/v/} 6 | docker build -t kontena/lb:latest -t "kontena/lb:$TAG" . 7 | docker push "kontena/lb:$TAG" 8 | docker push kontena/lb:latest 9 | fi -------------------------------------------------------------------------------- /docker-compose.test.yml: -------------------------------------------------------------------------------- 1 | etcd: 2 | image: kontena/etcd:2.3.3 3 | command: --listen-client-urls http://172.17.0.1:2379,http://localhost:2379 --advertise-client-urls http://172.17.0.1:2379,http://localhost:2379 4 | net: host 5 | 6 | lb: 7 | build: . 8 | command: -log-level=debug 9 | ports: 10 | - 8180:80 11 | - 8443:443 12 | 13 | environment: 14 | KONTENA_SERVICE_NAME: "lb" 15 | KONTENA_LB_HEALTH_URI: "/health" 16 | KONTENA_LB_SSL_CIPHERS: ECDHE-RSA-AES128-GCM-SHA256 17 | KONTENA_LB_CUSTOM_SETTINGS: | 18 | option dontlognull 19 | KONTENA_LB_GLOBAL_SETTINGS: | 20 | ssl-default-bind-options force-tlsv12 21 | ACME_CHALLENGE_LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0: | 22 | LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0.9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI 23 | 24 | SSL_CERT_test1: | 25 | -----BEGIN CERTIFICATE----- 26 | MIIC9TCCAd2gAwIBAgIJAK94fUzfHt1pMA0GCSqGSIb3DQEBCwUAMBExDzANBgNV 27 | BAMMBnRlc3QtMTAeFw0xNzEwMjYxMDA5MDNaFw0yMDA3MjIxMDA5MDNaMBExDzAN 28 | BgNVBAMMBnRlc3QtMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMP9 29 | 6ym6ptYHFh0o2aoGqTUL+Di+BYNMTDA2CciHtMPL7/SqdtS8Dej0hguyiee37D07 30 | b+Lo9I4/wBYVtCIUrDqqQvYwkAsUbZKD+nXWalfpSGtt3iFT4nVCg833yK6b6/JN 31 | TqpMmruMjn9sadzEAxabHU9at/j9ZCLrQBdgrGRhGJcCgPSc3jTLAEz5gf45F3DO 32 | vD/4aZYCsjS2qFHvtzFBP8pVqRP9CiXvCw/tuwN7wA7wBeJ185JPax8FfSELaDbZ 33 | Yd63GEVVCmM87HS8faCCbffmaciPoHPl8TyM3+FaGSot3HT6VLnkVFeQR470nKxT 34 | 0PX/iSD++b05l8GGjKECAwEAAaNQME4wHQYDVR0OBBYEFLJn8DEv3K4ISyzh8Du5 35 | GnOeT6WIMB8GA1UdIwQYMBaAFLJn8DEv3K4ISyzh8Du5GnOeT6WIMAwGA1UdEwQF 36 | MAMBAf8wDQYJKoZIhvcNAQELBQADggEBAL/KI42JqasFCofB+ii4L6qodHohhaw1 37 | tso6YQTSwvwYDG/tmjUdIRB3W2Vi4eUUHjhH0/CJk/jZaTOpQrC/Kd89gcQr+UzM 38 | uXx2bDGMp6VszE9wFPBpczoPndmc9ExPijbtOD4Q5NRpOgxs55JoVLyOEijvSfEn 39 | dQetBiLeudiYkQT89APysveqkj2WojK6H3Obvns9bZ5HlnGcWP71ttY1/HyIMd1P 40 | 2d10zFA66rriEwlcsOTKuusJ0y2OUOA6Fzi3v1vInDvx21QzLX3NMUtqn3t5qtto 41 | UQCkJrk2TE6mrL92igBV/+ckB5cGvJ5L8viyvijezX3xjOzn9JGhFhA= 42 | -----END CERTIFICATE----- 43 | -----BEGIN PRIVATE KEY----- 44 | MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDD/espuqbWBxYd 45 | KNmqBqk1C/g4vgWDTEwwNgnIh7TDy+/0qnbUvA3o9IYLsonnt+w9O2/i6PSOP8AW 46 | FbQiFKw6qkL2MJALFG2Sg/p11mpX6Uhrbd4hU+J1QoPN98ium+vyTU6qTJq7jI5/ 47 | bGncxAMWmx1PWrf4/WQi60AXYKxkYRiXAoD0nN40ywBM+YH+ORdwzrw/+GmWArI0 48 | tqhR77cxQT/KVakT/Qol7wsP7bsDe8AO8AXidfOST2sfBX0hC2g22WHetxhFVQpj 49 | POx0vH2ggm335mnIj6Bz5fE8jN/hWhkqLdx0+lS55FRXkEeO9JysU9D1/4kg/vm9 50 | OZfBhoyhAgMBAAECggEBAIm3y93npUHxes2EneZGhfGbdpFQnQkUvNiHsDozeYa3 51 | r+YpPhTgC5os8GAZ1aN4bszcDhPRA79M9onOOGRWSGt0plbd6umOMixpBr50qwcZ 52 | CmVKr3KVwiQJWBqLyX1AXPxG7EboSzYMXzkUkhKpvU3OMztGkM2qKAoNalzC9oAV 53 | KdxTp52bG2iekBNj1b839jl57eRmJsGcAcRXMtxblvak84LEfPhnXSUp0nhhTHD2 54 | Dr+jvxF+q8TbIyf+StKppi3KmHliSr1qqLKC3+9qsHRCw0REnqOTJ04QHjo3QjUU 55 | 3Rlpal/b9yFyxT5o9+doQKKU7EISRwERfn2POLOO+pECgYEA916+BKBF8jq4Yghu 56 | G+b/zEZpeVeRYbPnxCgqrqI83+tistMbJxI1cHch7VLl6hVngD+U4BOnXRUGwHmG 57 | UxH7mzurX7ULJfoTSpN+4yMsd9kuBvlzoqgzaxg6Cl1adRQJUcaf1Ki3DN7ToFMU 58 | WRQdFNaVrEvEA5VefPeAk+utMLUCgYEAytRRhDMafqpFzVj/LSvYDVuMQCHMqRpJ 59 | S71HyW+qunyhzlYSUqk9IHWLDIET0bIZhVa4yThvnv3nekJ+OGPsci1Yi3uOziJK 60 | Oedv+UMu83p/Lo7pgMlfjj8EjOtAd9d3Sn35YIH8hQDF2iyCFABaVFCFwMkVEE8A 61 | rZu8naT8m70CgYEAz5kbLxaynM7a3qrkfVYnZm/RJJxwzeYFo4FyEIznOaR5eEni 62 | h6+oWXIhbuIbQZAlBGRXtJXJ5zw2JmHWcPCuj2BMOk3dxUlR10xhOI3US+Bf2EqQ 63 | 2Pj/7eivDPO7bnYaPB7NE9Nji9GVGP+gHAHdRhewFKChJ8C7Q3US2xD2j+ECgYEA 64 | k/aZVOh23opWi3vuA0TlwrDTOoGtrHrpl2AIe3GDybFb1ItDqJufZQt6mW+cRrA3 65 | H+dovBn4i7LL54uUSozSk2RzIKXNQqEPJvin3d3d5W6qUwucWgANPlbIegiwKfy8 66 | IFKP1pBc56Xtr8AiUHcFblajjETkodYQN5XR3erbAL0CgYEA6KfaCQkIe3NbTN6H 67 | o1hmwFgF2p4gjM202GwR6A4f4uvgrmnd76YDTmZidFbGfFw5igB3Gfisjd9nthtZ 68 | +NUq8hr7lx+ZJ413LyF/9CQAQn1l5BuIpcDGDirm8od4nZvg53qN1Q8AqEzZFcMw 69 | IJepvjbc/iTWfYqrWuod1wcBU5A= 70 | -----END PRIVATE KEY----- 71 | SSL_CERTS: | 72 | -----BEGIN CERTIFICATE----- 73 | MIIC+zCCAeOgAwIBAgIJAJXVkdCXj6aqMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV 74 | BAMMCWxvY2FsaG9zdDAeFw0xNzEwMjYwOTA1MjdaFw0yMDA3MjIwOTA1MjdaMBQx 75 | EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 76 | ggEBAKzwoA0yPoG+VqJr4F69Iaif0SumXfgt16zfaJJYKYqCOoq9qVjX89jA6CQ5 77 | q+zn7066ihK1pFHA2T+Be7rMRo76p1VG2YW1NQ1Dj6LENVqgJqqVaVJbe67pvTLz 78 | dzZRLbjjpRV8M83CiSYTm+p53qBAsvl5DWQBRIJwK45LIi5kz7UxqI+R+SPLQJqC 79 | 4+U8TDDJ1pEdZJ4qlb9eJuP8l6TXtLrSuaLzhTPg08JGigAag39Mg1F2nkNSCI6w 80 | R7qGw3EhWgHjhsNCS/jP/w+JKQbQyuDuO5bYCVI9zBGbMtpP6xeDKDhmDGTguvYN 81 | +opSq1Aija93kk6N9ueFxlY+y4UCAwEAAaNQME4wHQYDVR0OBBYEFMZSiX50Bls8 82 | j8j66O0FXr/ZWtYFMB8GA1UdIwQYMBaAFMZSiX50Bls8j8j66O0FXr/ZWtYFMAwG 83 | A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBABhw+3Pm0nLhwEAtmUscH0bx 84 | XenjMxP5F3GxIj8eJfIWO4fXS/LIXTcM0ghKbm6q0zdBVra/nLMvrjyxYJlOqn04 85 | WyvBLH2EXjvIl8Bl/JLvLlddJ4gCfu5zW4d7Bs2+MBPVvfzzfFHKlmua5GbNxPoM 86 | hbVhZfWWqvN493mBOLE1j/1Bgch4zZGwJefP1+YuI2QHTDk8XmtBkBWymRAD4hBa 87 | 8Yg+XLBHfcGeqpIiCRXnTlQuxabmZlDmwh/M+Cxiac676z76EN62+zcAgd1NpAO6 88 | BRkJQadwDC4A9ogEkBYXwG0X9/+ZB6CzG4Vx+vdSupkFMp9b0z2vcodfO39KONg= 89 | -----END CERTIFICATE----- 90 | -----BEGIN PRIVATE KEY----- 91 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCs8KANMj6Bvlai 92 | a+BevSGon9Erpl34Ldes32iSWCmKgjqKvalY1/PYwOgkOavs5+9OuooStaRRwNk/ 93 | gXu6zEaO+qdVRtmFtTUNQ4+ixDVaoCaqlWlSW3uu6b0y83c2US2446UVfDPNwokm 94 | E5vqed6gQLL5eQ1kAUSCcCuOSyIuZM+1MaiPkfkjy0CaguPlPEwwydaRHWSeKpW/ 95 | Xibj/Jek17S60rmi84Uz4NPCRooAGoN/TINRdp5DUgiOsEe6hsNxIVoB44bDQkv4 96 | z/8PiSkG0Mrg7juW2AlSPcwRmzLaT+sXgyg4Zgxk4Lr2DfqKUqtQIo2vd5JOjfbn 97 | hcZWPsuFAgMBAAECggEAF2tikUbrlhBblRU8xjeglkBGSD34XcJ/gYajl6XewkYO 98 | fXlftItSF1lQVo+Ey8lA7A1w40W74eJWyTXUtqAxMe2ZuX+lt2iprYknq2gcvZAQ 99 | jGs9XwzKfA5lM8Aqta1anr4dPgKa1VNx1Tk0lRU548O/OO9+s7tENtHP5C4ii9uc 100 | EPTq8V0vm2VbZOW17Yfw6a7RlxEvHP3JorECUvQwgYUSwgjQ2LLBEsCVpWo9w3z6 101 | ffCbz9X6JJ8DeqKS524o1RMyifdGgynnB8zVYsLwnEPTp5mFDec5+jWLjTCO/m+e 102 | IiGuaeeUBSG+oLREDjhw+bkb3wY/MGZsC546tOsKRQKBgQDW4XhAA5ES5ZNG5yMC 103 | rvBF1qtHFXxd060kn7Ndz6oGkGuZ99G0+NXHuaSP3CqEuOYdg2NS2LX32EHhwnFF 104 | /uRR4UKHLKJL/Ai1jk8/L+7sQSezzzW/Mek5v4lxH9hflfR2zH99Qb5Zemld5I1f 105 | rBuEGvLUTfv1nGCD+5PPZWnwCwKBgQDOCJGiSV4gozTO+Uq9yIuWJ9JB5JpM6IVF 106 | CNxmBseFLNZcnKb/Bl/NTH87Y7uk4r6nQ0VMfwv+dwd+wa/y0eau92T+PXo2sspp 107 | vlwGPP5RrcsDpOl5jiT022/MI9ybgnjNWFE3bA4DuCNua49LRTxW8Q1Leiu740EU 108 | XcnrFMecrwKBgBX5bMCvHLDgBVWk4XGuzid2MoHMcrFtqjEqm78mM28EadyO+UUW 109 | hVYtZ+TGURrNhcrS2t9oBgPYe7RInCjaTiMJdDI6oEZA+esHKJd/oWFLsHG06Pwq 110 | cH1VVwrYhNoRjbRwaUE37e1clVXiv4pfIVk7IEYRy4hse3pDyfPVnSXNAoGBAKX4 111 | 7SiopbTxBId+9yCvPxM0/QGr4Ej4PvN/0dw2td+oYP62CykBv4coio4TJ4QKTL99 112 | R4P6DHVu+ZC5Ar4/LO/hx2+vopYRrVFF0egMlmrB7/r9jD8prMe7RfJTKVH05s+0 113 | x6g32YpReelnqEVgft0izizxO+3dgf2gGBrR4INtAoGAIC3t93txnUNlRu0He6mL 114 | alFBjeDBl80/bhM74Aqo0veAHHwNZw5xg4dSwe5ydv5Ei6fLl5A4mqInPfRJhEVe 115 | WpuPlhWSprWv6eunCzG1huWrda4/0aXiOc/n5mdHAZ6v7sBrKEWWSiZlZ6+jay5M 116 | adNVFeXMFuwC0t7u0nUTQss= 117 | -----END PRIVATE KEY----- 118 | -----BEGIN CERTIFICATE----- 119 | MIIDQzCCAiugAwIBAgIJAOU1G4gSj6nYMA0GCSqGSIb3DQEBCwUAMDgxCjAIBgNV 120 | BAMMASoxHTAbBgNVBAoMFE15IENvbXBhbnkgTmFtZSBMVEQuMQswCQYDVQQGEwJV 121 | UzAeFw0xNjA2MDMwNjU2MTdaFw0xOTA1MTkwNjU2MTdaMDgxCjAIBgNVBAMMASox 122 | HTAbBgNVBAoMFE15IENvbXBhbnkgTmFtZSBMVEQuMQswCQYDVQQGEwJVUzCCASIw 123 | DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK4TvlMa3/9YK1aLSWBh1QPGulQd 124 | aYLoCSDYoTMv0YOO4lLDPBkU6fzjwWX44nesBjUmN61QxBkpGNTs9x6pNvd4YI7N 125 | arhxSljROf42vNWkoVKl0Ls3fgUxMAcbFYf38i5PLzVLebsIZR2+sVe6QdwWAame 126 | nhlEE1ubnx46xROCTLkjfzLlYdSJBNPx8gKovgf31gWU9dtVrdkBclHmglspXZt0 127 | removed_to_invalidate_certs 128 | Qi89hSFKjDTFeovydxbaOR3k5PKqOFD1Lou9Ho87TeLBNNYkA16IdJ63dLMCAwEA 129 | AaNQME4wHQYDVR0OBBYEFNO0I5ogOcgUbWmaBj1hA8qt7F0rMB8GA1UdIwQYMBaA 130 | FNO0I5ogOcgUbWmaBj1hA8qt7F0rMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEL 131 | BQADggEBAHpJiP8ZblJmEuSP0lAJRovTPGhKZA4Pgpa9zimETCruBTkTZyCcNwD3 132 | htHgrqkCieoV5RK5juyHVQo5GiVptcWxVSuGgr9tKtWTYlfB0xkRRT9No0bjT9J8 133 | cGTZm+lZyQ85AuwOPSe1wFUDPuLu8xJVPJoIeDNaKqjZleVZUNafPSKRWhi0X6rt 134 | JmwgQdUBqRgZYHL6Zpcr2E5oyc4tL10pYcIQchyDHg0IHyVb/xz072RFRKj0zP4A 135 | 2wVolufLJCFVbc8g1vEDgRjsuGLN55bJ0CTsn14GcOLsBntqKYyKm6lwV3+dDRM/ 136 | AacJXcMY+aRv/l5fNl0oMRcZpRr3gew= 137 | -----END CERTIFICATE----- 138 | -----BEGIN PRIVATE KEY----- 139 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCuE75TGt//WCtW 140 | i0lgYdUDxrpUHWmC6Akg2KEzL9GDjuJSwzwZFOn848Fl+OJ3rAY1JjetUMQZKRjU 141 | 7PceqTb3eGCOzWq4cUpY0Tn+NrzVpKFSpdC7N34FMTAHGxWH9/IuTy81S3m7CGUd 142 | vrFXukHcFgGpnp4ZRBNbm58eOsUTgky5I38y5WHUiQTT8fICqL4H99YFlPXbVa3Z 143 | AXJR5oJbKV2bdAnWkLO2NbpK72+VsjV1wiZXMr44aQ67dZwdZZ11eWZyjfcrXQ2z 144 | f6NxhRlzJg/ItkIvPYUhSow0xXqL8ncW2jkd5OTyqjhQ9S6LvR6PO03iwTTWJANe 145 | iHSet3SzAgMBAAECggEBAIo8y4ubb/6Kuf/EJMURa+PP0PAzWzLFqVoYLgtEEhFz 146 | Sm+G8xbH8PkOtOqRtuZqCZPzgYt09AU3Ca0tcSE8J5ZmVeeRYQqPjQbzQCaMuXC/ 147 | iAzl+NhzvPPKl+VMsNCFKiF0aHzeLxFEHWh9or+T/fEU2MUmXU6bLPQ2pSmQaiiO 148 | pEDH0d5xuWP90LgfPJoKcBlai140Ml+gLTnTfBIElQg06TiSSQM2yVfF5sz7Sbe2 149 | pjTqyVNDP01pBoUpR2BAB/JbeqaWM8jnnIi+35uE1kvttq97T3WfBHcW6j7RN5N8 150 | HmsxDcG8GPV/6Z7j5ZGHACsQsI6bSbXdFACDOM4e74ECgYEA54I0Tut4NBlI0RkD 151 | removed_to_invalidate_certs 152 | v4ZWS2blGUguFGybgyaeJemqW+CdcBTzglsOXDi4C1NrLqgX3+c8PULsN0wns5B0 153 | KoRU+oBrK/coPT1/J0cnLWqdfcMCgYEAwH4o7HIk8zour+7f1fNB7642dbndCQut 154 | c+Kf48WDQpCOLCHvHujOY92XCVkEVP63lsc/JJCr9s8fxnUIX2z5/bwpKZn4f4Zy 155 | fzwslzIWCLXAyPTHaux9u+8vtJRTPaQ0YYOigSn1xdLu5Xu/761jStQTs1i4MN3P 156 | VI68gffWDlECgYEA4FhAAn6TRMF/3AmGqOGbJeibbaACp8FAe4x5qJhIz7Ekau0R 157 | gRrAds6IgQ2O1w0dWTXmBsRdriOSxz/+4If5FibHOoHFDcvVw/lnZkwS5+g6CUR0 158 | Wc2Nk/bu+yLCijsgr7ywlplEua2WB5+jwxPsGbjaoodnujjfAJwmLg/UQOsCgYAq 159 | OAF1yps8FZjD0ZqabF4b2ZPsQjWulDcY4a274Ugmw1nLaC3wE5Og56sGy9VdZviR 160 | Q2Yf+PMekNMhTe3mMBqsgiZtD24nWi+mpGYLS1r10hdUfAt48iGppI5MBvQy4t7y 161 | PFLaDX/wQZFQF9JDGT5b3SPtBBpx7VRZ8Wx6/Qaf4QKBgDN7nX2YAt0t60kFzL2V 162 | hdSuTbjdDTNYALHrPlOiUV0LXOWSpeJpjI7BbFYbwEXHWyJ6XJs/uS1pX9AfglXY 163 | KNjEBaOzuTkyIX1THmpnt9417EVQeCYYbMvnKvzhVi32Enr5p8Y5t8i75lrfpvta 164 | XxGBmgtsa++i6DvXUp/Jv1Xy 165 | -----END PRIVATE KEY----- 166 | links: 167 | - service-a 168 | - service-b 169 | - service-c 170 | 171 | lb_no_health: 172 | build: . 173 | command: -log-level=debug 174 | ports: 175 | - 8181:80 176 | environment: 177 | - KONTENA_SERVICE_NAME=lb_no_health 178 | 179 | service-a: 180 | build: test/server 181 | container_name: service-a 182 | hostname: service-a 183 | 184 | service-b: 185 | build: test/server 186 | container_name: service-b 187 | hostname: service-b 188 | 189 | service-c: 190 | build: test/server 191 | container_name: service-c 192 | hostname: service-c 193 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eo pipefail 3 | 4 | 5 | rm -f /var/run/haproxy.pid > /dev/null 2>&1 6 | rm -f /etc/haproxy/haproxy.cfg > /dev/null 2>&1 7 | 8 | 9 | if [ -z "$ETCD_NODE" ]; then 10 | IP=$(/sbin/ip route | awk '/default/ { print $3 }') 11 | ETCD_NODE=$IP 12 | fi 13 | 14 | if [ -z $KONTENA_STACK_NAME ] || [ "$KONTENA_STACK_NAME" == "null" ]; then 15 | LB_NAME=$KONTENA_SERVICE_NAME 16 | else 17 | LB_NAME="$KONTENA_STACK_NAME/$KONTENA_SERVICE_NAME" 18 | fi 19 | 20 | export ETCD_NODE=$ETCD_NODE 21 | export ETCD_PATH="/kontena/haproxy/$LB_NAME" 22 | exec /app/bin/lb 23 | -------------------------------------------------------------------------------- /errors/200.http: -------------------------------------------------------------------------------- 1 | HTTP/1.0 200 OK 2 | Cache-Control: no-cache 3 | Connection: close 4 | Content-Type: text/html 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 200 - OK 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 |
22 |

200 Everything seems to be 200 - OK

23 |
24 |

Kontena Load Balancer

25 |
26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /errors/502.http: -------------------------------------------------------------------------------- 1 | HTTP/1.0 502 Bad Gateway 2 | Cache-Control: no-cache 3 | Connection: close 4 | Content-Type: text/html 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 502 — Bad Gateway 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 |
22 |

502 Bad Gateway

23 |
24 |

Kontena Load Balancer

25 |
26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /errors/503.http: -------------------------------------------------------------------------------- 1 | HTTP/1.0 503 Service Unavailable 2 | Cache-Control: no-cache 3 | Connection: close 4 | Content-Type: text/html 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 503 — Service Unavailable 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 |
22 |

503 Service Unavailable

23 |
24 |

Kontena Load Balancer

25 |
26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /errors/504.http: -------------------------------------------------------------------------------- 1 | HTTP/1.0 504 Gateway timeout 2 | Cache-Control: no-cache 3 | Connection: close 4 | Content-Type: text/html 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 504 - Gateway timeout 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 |
22 |

504 Gateway timeout

23 |
24 |

Kontena Load Balancer

25 |
26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /lib/kontena/acme_challenges.rb: -------------------------------------------------------------------------------- 1 | module Kontena 2 | class AcmeChallenges 3 | include Kontena::Logging 4 | 5 | def self.env 6 | ENV 7 | end 8 | 9 | # @return [Boolean] 10 | def self.configured? 11 | env.any? { |env, value| env.start_with? 'ACME_CHALLENGE_' } 12 | end 13 | 14 | def self.load_env(env) 15 | challenges = {} 16 | 17 | env.each do |env, value| 18 | _, prefix, suffix = env.partition(/^ACME_CHALLENGE_/) 19 | 20 | if prefix 21 | challenges[suffix] = value 22 | end 23 | end 24 | 25 | challenges 26 | end 27 | 28 | # Setup from ENV 29 | def self.boot(env = ENV) 30 | manager = new(load_env(env)) 31 | end 32 | 33 | attr_reader :challenges 34 | 35 | # @param challenges [Hash{String => String}] ACME challenge token => keyAuthorization 36 | def initialize(challenges) 37 | @challenges = challenges 38 | end 39 | 40 | # @return [Boolean] 41 | def challenges? 42 | !challenges.empty? 43 | end 44 | 45 | # @param challenge [String] ACME challenge token from /.well-known/acme-challenge/... 46 | # @return [String, nil] ACME challenge keyAuthorization 47 | def respond(challenge) 48 | @challenges[challenge] 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/kontena/actors/acme_challenge_server.rb: -------------------------------------------------------------------------------- 1 | module Kontena::Actors 2 | class AcmeChallengeServer < Concurrent::Actor::RestartingContext 3 | include Kontena::Logging 4 | 5 | PORT = 54321 6 | 7 | class Servlet < WEBrick::HTTPServlet::AbstractServlet 8 | def initialize(server, acme_challenges) 9 | super(server) 10 | @acme_challenges = acme_challenges 11 | end 12 | 13 | def do_GET(req, res) 14 | parts = req.path.split('/') 15 | 16 | raise WEBrick::HTTPStatus::NotFound unless parts.length == 4 17 | 18 | challenge = parts[-1] 19 | 20 | if key_authorization = @acme_challenges.respond(challenge) 21 | res.status = 200 22 | res.content_type = 'text/plain' 23 | res.body = key_authorization 24 | else 25 | res.status = 404 26 | res.content_type = 'text/plain' 27 | res.body = "No key authorization for challenge: #{challenge}\n" 28 | end 29 | end 30 | end 31 | 32 | def initialize(acme_challenges, port: PORT, webrick_options: {}) 33 | @acme_challenges = acme_challenges 34 | 35 | info "initialize" 36 | 37 | @server = WEBrick::HTTPServer.new( 38 | BindAddress: '127.0.0.1', 39 | Port: port, 40 | **webrick_options 41 | ) 42 | @server.mount '/.well-known/acme-challenge', Servlet, @acme_challenges 43 | end 44 | 45 | # @param [Symbol,Array] msg 46 | def on_message(msg) 47 | case msg 48 | when :start 49 | start 50 | else 51 | pass 52 | end 53 | end 54 | 55 | def start 56 | # this blocks the actor executor thread in the accept() loop 57 | # the webrick servlets run as separate threads managed by webrick 58 | @server.start 59 | end 60 | 61 | def default_executor 62 | Concurrent.global_io_executor 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/kontena/actors/etcd_watcher.rb: -------------------------------------------------------------------------------- 1 | require 'concurrent/timer_task' 2 | require 'etcd' 3 | 4 | module Kontena::Actors 5 | class EtcdWatcher < Concurrent::Actor::RestartingContext 6 | include Kontena::Logging 7 | 8 | attr_reader :client, :path 9 | 10 | def initialize(host, path) 11 | info "initialized with etcd host=#{host} path=#{path}" 12 | @host = host 13 | @path = path 14 | @read_task = Concurrent::TimerTask.new(execution_interval: 1) { 15 | read_etcd 16 | } 17 | end 18 | 19 | def default_executor 20 | Concurrent.global_io_executor 21 | end 22 | 23 | # @param [Symbol,Array] msg 24 | def on_message(msg) 25 | command, _ = msg 26 | case command 27 | when :start 28 | start 29 | else 30 | pass 31 | end 32 | end 33 | 34 | def start 35 | @read_task.execute 36 | end 37 | 38 | def client 39 | @client ||= Etcd::Client.new(host: @host, port: 2379) 40 | end 41 | 42 | def read_etcd 43 | response = client.get(path, recursive: true) 44 | self.parent << [:generate_config, response.freeze] 45 | rescue Etcd::KeyNotFound 46 | client.set(path, dir: true) 47 | retry 48 | rescue => exc 49 | error "#{exc.class}: #{exc.message}" 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/kontena/actors/haproxy_config_generator.rb: -------------------------------------------------------------------------------- 1 | module Kontena::Actors 2 | class HaproxyConfigGenerator < Concurrent::Actor::RestartingContext 3 | include Kontena::Logging 4 | 5 | KEY_SEPARATOR = '/'.freeze 6 | 7 | def initialize 8 | info "initialized" 9 | end 10 | 11 | # @param [Message] msg 12 | def on_message(msg) 13 | command, *args = msg 14 | case command 15 | when :update 16 | update(args[0]) 17 | else 18 | pass 19 | end 20 | end 21 | 22 | # @param [Etcd::Node] node 23 | def update(node) 24 | root = node.key 25 | services = [] 26 | tcp_services = [] 27 | node.children.each do |c| 28 | if c.key == "#{root}/services" 29 | services = generate_services(c) 30 | elsif c.key == "#{root}/tcp-services" 31 | tcp_services = generate_tcp_services(c) 32 | end 33 | end 34 | 35 | config = Kontena::Views::Haproxy.render({ 36 | format: :text, services: services, tcp_services: tcp_services 37 | }).each_line.reject{ |l| l.strip == ''.freeze }.join 38 | parent << [:write_config, config.freeze] 39 | end 40 | 41 | # @param [Etcd::Node] node 42 | # @return [Array] 93 | def generate_tcp_services(node) 94 | services = [] 95 | node.children.sort_by { |c| c.key }.each do |c| 96 | service = generate_tcp_service(c) 97 | if service.upstreams.size > 0 && service.external_port 98 | services << service 99 | end 100 | end 101 | services.freeze 102 | 103 | services 104 | end 105 | 106 | # @param [Etcd::Node] 107 | # @param [Kontena::Models::TcpService] 108 | def generate_tcp_service(node) 109 | service = Kontena::Models::TcpService.new(node.key.split(KEY_SEPARATOR)[-1]) 110 | node.children.each do |c| 111 | key = c.key.split(KEY_SEPARATOR)[-1].to_sym 112 | 113 | case key 114 | when :upstreams 115 | service.upstreams = c.children.sort_by{ |u| u.key }.map { |u| 116 | Kontena::Models::Upstream.new(u.key.split(KEY_SEPARATOR)[-1], u.value) 117 | } 118 | when :balance 119 | service.balance = c.value 120 | when :external_port 121 | service.external_port = c.value 122 | when :health_check_uri 123 | service.health_check_uri = c.value 124 | when :custom_settings 125 | service.custom_settings = c.value.split("\n") 126 | end 127 | end 128 | service.freeze 129 | 130 | service 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /lib/kontena/actors/haproxy_config_writer.rb: -------------------------------------------------------------------------------- 1 | module Kontena::Actors 2 | class HaproxyConfigWriter < Concurrent::Actor::RestartingContext 3 | include Kontena::Logging 4 | 5 | attr_accessor :config_file 6 | 7 | # @param [String] config_file 8 | def initialize(config_file = '/etc/haproxy/haproxy.cfg') 9 | self.config_file = config_file 10 | @old_config = '' 11 | end 12 | 13 | def on_message(msg) 14 | command, *args = msg 15 | case command 16 | when :update 17 | update_config(args[0]) 18 | when :config_written? 19 | config_written? 20 | else 21 | pass 22 | end 23 | end 24 | 25 | def config_written? 26 | !@old_config.empty? 27 | end 28 | 29 | # @param [String] config 30 | def update_config(config) 31 | if @old_config != config 32 | write_config(config) 33 | @old_config = config 34 | parent << :update_haproxy 35 | end 36 | end 37 | 38 | # @param [String] config 39 | def write_config(config) 40 | File.write(config_file, config.to_s) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/kontena/actors/haproxy_process.rb: -------------------------------------------------------------------------------- 1 | 2 | module Kontena::Actors 3 | class HaproxyProcess < Concurrent::Actor::Context 4 | include Kontena::Logging 5 | 6 | # @param [String] cmd 7 | def initialize(cmd) 8 | @cmd = cmd 9 | end 10 | 11 | def on_message(msg) 12 | command, _ = msg 13 | 14 | case command 15 | when :run 16 | run 17 | when :pid 18 | @pid 19 | else 20 | pass 21 | end 22 | end 23 | 24 | def run 25 | @pid = Process.spawn(@cmd.join(' ')) 26 | info "HAProxy process #{@cmd} started (pid #{@pid})" 27 | @wait_pid = Concurrent::Future.execute { wait_pid(self.reference) } 28 | @pid 29 | end 30 | 31 | def wait_pid(reference) 32 | begin 33 | _, status = Process.wait2(@pid) 34 | info "process exited #{@pid}" 35 | ensure 36 | reference.tell([:terminate!, status]) 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/kontena/actors/haproxy_spawner.rb: -------------------------------------------------------------------------------- 1 | require_relative 'haproxy_process' 2 | require 'securerandom' 3 | 4 | module Kontena::Actors 5 | class HaproxySpawner < Concurrent::Actor::RestartingContext 6 | include Kontena::Logging 7 | 8 | ADMIN_SOCK = '/var/run/haproxy-admin.sock' 9 | 10 | # @param [String] haproxy_bin 11 | # @param [String] config_file 12 | def initialize(haproxy_bin = '/usr/local/sbin/haproxy', config_file = '/etc/haproxy/haproxy.cfg') 13 | @current_pid = nil 14 | @haproxy_cmd = [haproxy_bin, '-f', config_file, '-db'] 15 | @validate_cmd = [haproxy_bin, '-c -f', config_file] 16 | end 17 | 18 | def on_message(msg) 19 | command, _ = msg 20 | 21 | case command 22 | when :update 23 | update_haproxy 24 | when :terminated 25 | if children.size == 1 26 | raise "we don't have any child processes, all hope is gone" 27 | end 28 | else 29 | pass 30 | end 31 | end 32 | 33 | def update_haproxy 34 | if validate_config 35 | if children.size > 0 36 | reload_haproxy 37 | else 38 | start_haproxy 39 | end 40 | end 41 | end 42 | 43 | def start_haproxy 44 | spawn_process(@haproxy_cmd) 45 | end 46 | 47 | def validate_config 48 | info "validating config" 49 | system(@validate_cmd.join(' ')) == true 50 | end 51 | 52 | def reload_haproxy 53 | process = children.last 54 | info "child processes: #{children.map{ |c| c.ask!(:pid) }}" 55 | pid = process.ask!(:pid) 56 | reload_cmd = @haproxy_cmd + ['-x', ADMIN_SOCK, '-sf', pid.to_s] 57 | spawn_process(reload_cmd) 58 | end 59 | 60 | def spawn_process(cmd) 61 | process_uuid = "haproxy-process-#{SecureRandom.uuid}" 62 | process = Kontena::Actors::HaproxyProcess.spawn!(name: process_uuid, args: [cmd]) 63 | process << :run 64 | process 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/kontena/actors/lb_supervisor.rb: -------------------------------------------------------------------------------- 1 | module Kontena::Actors 2 | class LbSupervisor < Concurrent::Actor::RestartingContext 3 | include Kontena::Logging 4 | 5 | def on_message(msg) 6 | command, *args = msg 7 | 8 | case command 9 | when :start 10 | start 11 | when :generate_config 12 | generate_config(args[0]) 13 | when :write_config 14 | write_config(args[0]) 15 | when :update_haproxy 16 | update_haproxy 17 | when :terminated 18 | raise "child actors should not terminate!" 19 | when :reset 20 | info "child #{envelope.sender_path} did reset" 21 | if envelope.sender == @spawner 22 | handle_spawner_reset 23 | end 24 | else 25 | pass 26 | end 27 | end 28 | 29 | # @return [String] 30 | def etcd_node 31 | ENV.fetch('ETCD_NODE') { '127.0.0.1' } 32 | end 33 | 34 | # @return [String] 35 | def etcd_path 36 | ENV.fetch('ETCD_PATH') 37 | end 38 | 39 | def acme_challenges 40 | @acme_challenges ||= Kontena::AcmeChallenges.boot 41 | end 42 | 43 | def start 44 | @syslog_server = SyslogServer.spawn!(name: 'syslog_server', supervise: true) 45 | @syslog_server << :start 46 | @acme_challenge_server = AcmeChallengeServer.spawn!(name: 'acme_challenge_server', supervise: true, args: [ 47 | acme_challenges, 48 | ]) 49 | @acme_challenge_server << :start 50 | 51 | @config_generator = HaproxyConfigGenerator.spawn!(name: 'haproxy_config_generator', supervise: true) 52 | @config_writer = HaproxyConfigWriter.spawn!(name: 'haproxy_config_writer', supervise: true) 53 | @spawner = HaproxySpawner.spawn!(name: 'haproxy_spawner', supervise: true) 54 | 55 | @etcd_watcher = EtcdWatcher.spawn!(name: 'etcd_watcher', args: [etcd_node, etcd_path]) 56 | @etcd_watcher << :start 57 | end 58 | 59 | def generate_config(value) 60 | @config_generator << [:update, value] 61 | end 62 | 63 | def write_config(value) 64 | @config_writer << [:update, value] 65 | end 66 | 67 | def update_haproxy 68 | @spawner << :update 69 | end 70 | 71 | def handle_spawner_reset 72 | if @config_writer.ask!(:config_written?) 73 | @spawner << :update 74 | else 75 | warn "cannot reset HAProxySpawner because config has not yet written" 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/kontena/actors/syslog_server.rb: -------------------------------------------------------------------------------- 1 | module Kontena::Actors 2 | class SyslogServer < Concurrent::Actor::RestartingContext 3 | include Kontena::Logging 4 | 5 | LINE_REGEX = /^<(\d+)>(.*)/ 6 | 7 | def initialize 8 | info "initialized" 9 | @server = UDPSocket.new 10 | end 11 | 12 | # @param [Symbol,Array] msg 13 | def on_message(msg) 14 | command, _ = msg 15 | case command 16 | when :start 17 | start 18 | else 19 | pass 20 | end 21 | end 22 | 23 | def start 24 | @server.bind('127.0.0.1', 514) 25 | info "started" 26 | loop do 27 | data, _ = @server.recvfrom(1024) 28 | handle_data(data) 29 | end 30 | rescue Errno::EACCES => exc 31 | error exc.message 32 | end 33 | 34 | # @param [String] data 35 | def handle_data(data) 36 | if line = data.match(LINE_REGEX) 37 | puts line[2] if line[2] 38 | end 39 | rescue => exc 40 | error exc.message 41 | end 42 | 43 | def default_executor 44 | Concurrent.global_io_executor 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/kontena/cert_manager.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'openssl' 3 | 4 | module Kontena 5 | class CertManager 6 | include Kontena::Logging 7 | 8 | # Setup /etc/haproxy/certs from ENV 9 | def self.boot(env = ENV) 10 | cert_splitter = Kontena::CertSplitter.new(env) 11 | cert_manager = new(cert_splitter.to_h) 12 | 13 | if cert_manager.ssl_certs? 14 | cert_manager.setup 15 | cert_manager.write_certs 16 | end 17 | end 18 | 19 | attr_reader :ssl_certs 20 | 21 | # @param ssl_certs [Hash String>] 22 | def initialize(ssl_certs) 23 | @ssl_certs = ssl_certs 24 | end 25 | 26 | # @return [Boolean] 27 | def ssl_certs? 28 | !ssl_certs.empty? 29 | end 30 | 31 | def setup 32 | FileUtils.mkdir_p('/etc/haproxy/certs') 33 | end 34 | 35 | # @param [Hash String>] certs 36 | # @return [Integer] number of certs written 37 | def write_certs 38 | ssl_certs.each do |name, cert| 39 | write_cert(name, cert) 40 | end 41 | end 42 | 43 | # @param [String] name without .pem suffix 44 | # @param [String] cert 45 | def write_cert(name, cert) 46 | File.write("/etc/haproxy/certs/#{name}.pem", cert) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/kontena/cert_splitter.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | 3 | module Kontena 4 | class CertSplitter 5 | include Kontena::Logging 6 | 7 | # @return [Hash String>] 8 | def self.from_env(env = ENV) 9 | cert_splitter = new(env) 10 | cert_splitter.to_h 11 | end 12 | 13 | def initialize(env = ENV) 14 | @env = env 15 | end 16 | 17 | # @param [String] cert_bundle 18 | # @return [Array] 19 | def split_certs(cert_bundle) 20 | certs = [] 21 | buffer = '' 22 | cert_bundle.lines.each do |l| 23 | buffer << l 24 | if l.match(/-----END (.*)PRIVATE KEY-----/) 25 | certs << buffer.strip 26 | buffer = '' 27 | end 28 | end 29 | 30 | certs 31 | end 32 | 33 | # @return [Array] 34 | def ssl_certs 35 | @env['SSL_CERTS'] ? split_certs(@env['SSL_CERTS']) : [] 36 | end 37 | 38 | # @return [Hash String>] 39 | def ssl_cert_glob 40 | @env.select{|env, value| env.start_with? 'SSL_CERT_' } 41 | end 42 | 43 | def each 44 | i = 1 45 | 46 | ssl_certs.each do |cert| 47 | if valid_cert? cert 48 | yield "cert#{i}_gen", cert 49 | end 50 | i += 1 51 | end 52 | 53 | ssl_cert_glob.each do |name, cert| 54 | if valid_cert? cert 55 | yield name, cert 56 | end 57 | end 58 | end 59 | 60 | # @return [Hash String>] 61 | def to_h 62 | certs = {} 63 | 64 | each do |name, cert| 65 | certs[name] = cert 66 | end 67 | 68 | certs 69 | end 70 | 71 | # @param [String] cert 72 | # @return [Boolean] 73 | def valid_cert?(cert) 74 | certificate = OpenSSL::X509::Certificate.new(cert) 75 | info "valid certificate: #{certificate.subject.to_s}" 76 | 77 | true 78 | rescue 79 | warn "invalid certificate: #{cert[0..50]}" 80 | false 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/kontena/logging.rb: -------------------------------------------------------------------------------- 1 | module Kontena 2 | 3 | module Logging 4 | 5 | def self.initialize_logger(log_target = STDOUT) 6 | if ENV['DEBUG'] 7 | @logger = Logger.new(log_target) 8 | @logger.level = Logger::DEBUG 9 | else 10 | @logger = Logger.new(log_target) 11 | @logger.level = ENV['LOG_LEVEL'] ? ENV['LOG_LEVEL'].to_i : Logger::INFO 12 | end 13 | @logger 14 | end 15 | 16 | def self.logger 17 | defined?(@logger) ? @logger : initialize_logger 18 | end 19 | 20 | def logger 21 | Logging.logger 22 | end 23 | 24 | # Send a debug message 25 | # @param [String] string 26 | def debug(string) 27 | logger.debug(self.class.name) { string } 28 | end 29 | 30 | # Send a info message 31 | # @param [String] string 32 | def info(string) 33 | logger.info(self.class.name) { string } 34 | end 35 | 36 | # Send a warning message 37 | # @param [String] string 38 | def warn(string) 39 | logger.warn(self.class.name) { string } 40 | end 41 | 42 | # Send an error message 43 | # @param [String] string 44 | def error(string) 45 | logger.error(self.class.name) { string } 46 | end 47 | 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/kontena/models/common_service.rb: -------------------------------------------------------------------------------- 1 | module Kontena::Models 2 | module CommonService 3 | 4 | attr_accessor :name, 5 | :upstreams, 6 | :balance, 7 | :custom_settings 8 | 9 | def initialize(name) 10 | @name = name 11 | @upstreams = [] 12 | @balance = 'roundrobin' 13 | @custom_settings = [] 14 | end 15 | 16 | def custom_settings? 17 | @custom_settings.size > 0 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/kontena/models/service.rb: -------------------------------------------------------------------------------- 1 | require_relative 'common_service' 2 | 3 | module Kontena::Models 4 | class Service 5 | include CommonService 6 | 7 | attr_accessor :virtual_hosts, 8 | :virtual_paths, 9 | :keep_virtual_path, 10 | :health_check_uri, 11 | :health_check_port, 12 | :cookie, 13 | :basic_auth_secrets 14 | 15 | def initialize(name) 16 | super 17 | @balance = 'roundrobin' 18 | @virtual_hosts = [] 19 | @virtual_paths = [] 20 | @keep_virtual_path = false 21 | @health_check_uri = nil 22 | @health_check_port = nil 23 | @cookie = nil 24 | @basic_auth_secrets = nil 25 | end 26 | 27 | def keep_virtual_path? 28 | @keep_virtual_path.to_s == 'true' 29 | end 30 | 31 | def virtual_hosts? 32 | @virtual_hosts.size > 0 33 | end 34 | 35 | def virtual_paths? 36 | @virtual_paths.size > 0 37 | end 38 | 39 | def cookie? 40 | !@cookie.nil? 41 | end 42 | 43 | def basic_auth? 44 | !@basic_auth_secrets.nil? 45 | end 46 | 47 | def health_check? 48 | !@health_check_uri.nil? 49 | end 50 | 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/kontena/models/tcp_service.rb: -------------------------------------------------------------------------------- 1 | require_relative 'common_service' 2 | 3 | module Kontena::Models 4 | class TcpService 5 | include CommonService 6 | 7 | attr_accessor :external_port 8 | 9 | def initialize(name) 10 | super(name) 11 | @balance = 'leastconn' 12 | @external_port = nil 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/kontena/models/upstream.rb: -------------------------------------------------------------------------------- 1 | module Kontena::Models 2 | class Upstream 3 | 4 | attr_reader :name, :value 5 | 6 | def initialize(name, value) 7 | @name = name 8 | @value = value 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/kontena/templates/haproxy/_stats.text.erb: -------------------------------------------------------------------------------- 1 | listen stats 2 | mode http 3 | bind 0.0.0.0:1000 4 | stats enable 5 | stats uri / 6 | stats refresh 10s 7 | stats show-node 8 | stats show-legends 9 | 10 | # if authentication is wanted 11 | acl auth_ok http_auth(stats-auth) 12 | http-request auth unless auth_ok 13 | 14 | userlist stats-auth 15 | user stats insecure-password <%= ENV['STATS_PASSWORD'] %> 16 | -------------------------------------------------------------------------------- /lib/kontena/templates/haproxy/http_backends.text.erb: -------------------------------------------------------------------------------- 1 | <% if acme_challenges? %> 2 | backend acme_challenge 3 | option forwardfor 4 | 5 | server localhost 127.0.0.1:54321 6 | 7 | <% end %> 8 | <% services.each do |service| %> 9 | backend <%= service.name %> 10 | option forwardfor 11 | <% service.custom_settings.each do |setting| %> 12 | <%= setting %> 13 | <% end %> 14 | 15 | <% if service.cookie %> 16 | <% if service.cookie.empty? %> 17 | cookie KONTENA_SERVERID insert indirect nocache 18 | <% else %> 19 | <%= service.cookie %> 20 | <% end %> 21 | <% end %> 22 | 23 | balance <%= service.balance %> 24 | 25 | <% if service.basic_auth? %> 26 | acl auth_ok_<%= service.name %> http_auth(auth_users_<%= service.name %>) 27 | http-request auth realm <%= service.name %> unless auth_ok_<%= service.name %> 28 | <% end %> 29 | 30 | <% if service.virtual_paths? && !service.keep_virtual_path? %> 31 | <% service.virtual_paths.each do |virtual_path| %> 32 | reqrep ^([^\ :]*)\ <%= virtual_path %>[/]?(.*) \1\ /\2 33 | <% end %> 34 | <% end %> 35 | 36 | <% if service.health_check? %> 37 | option httpchk GET <%= service.health_check_uri %> 38 | <% end %> 39 | 40 | <% service.upstreams.each do |upstream| %> 41 | server <%= upstream.name %> <%= upstream.value %> check <% if service.health_check_port %>port <%= service.health_check_port %> <% end %><% if service.cookie? %>cookie <%= upstream.name %><% end %> 42 | <% end %> 43 | 44 | <% if service.basic_auth? %> 45 | userlist auth_users_<%= service.name %> 46 | <%= service.basic_auth_secrets%> 47 | <% end %> 48 | 49 | <% end %> 50 | -------------------------------------------------------------------------------- /lib/kontena/templates/haproxy/http_in.text.erb: -------------------------------------------------------------------------------- 1 | listen http-in 2 | bind *:80<% if accept_proxy? %> accept-proxy<% end %> 3 | http-request replace-header Host (.*):.* \1 4 | <% if ssl? %> 5 | bind *:443 ssl crt /etc/haproxy/certs/ no-sslv3<% if accept_proxy? %> accept-proxy<% end %><% if http2? %> alpn h2,http/1.1<% end %> 6 | reqadd X-Forwarded-Proto:\ https if { ssl_fc } 7 | reqadd X-Forwarded-Port:\ 443 if { ssl_fc } 8 | <% end %> 9 | 10 | errorfile 502 /etc/haproxy/errors/502.http 11 | errorfile 503 /etc/haproxy/errors/503.http 12 | 13 | <% if health_uri %> 14 | monitor-uri <%= health_uri %> 15 | errorfile 200 /etc/haproxy/errors/200.http 16 | <% end %> 17 | 18 | <% if acme_challenges? %> 19 | acl acme_challenge path_beg /.well-known/acme-challenge/ 20 | use_backend acme_challenge if acme_challenge 21 | <% end %> 22 | 23 | <% services.each do |service| %> 24 | <% service.virtual_hosts.each do |virtual_host| %> 25 | <% if virtual_host.start_with?('*.') %> 26 | acl host_<%= service.name %> hdr_end(host) -i <%= virtual_host.sub('*.', '.') %> 27 | <% else %> 28 | acl host_<%= service.name %> hdr(host) -i <%= virtual_host %> 29 | <% end %> 30 | <% end %> 31 | <% if service.virtual_paths? %> 32 | acl host_<%= service.name %>_virtual_path url_beg <%= service.virtual_paths.join(' ') %> 33 | <% end %> 34 | <% end %> 35 | 36 | <% services.select { |s| s.virtual_hosts? && s.virtual_paths? }.each do |service| %> 37 | use_backend <%= service.name %> if host_<%= service.name %> host_<%= service.name %>_virtual_path 38 | <% end %> 39 | <% services.select { |s| s.virtual_hosts? && !s.virtual_paths? }.each do |service| %> 40 | use_backend <%= service.name %> if host_<%= service.name %> 41 | <% end %> 42 | <% services.select { |s| s.virtual_paths? && !s.virtual_hosts? }.each do |service| %> 43 | use_backend <%= service.name %> if host_<%= service.name %>_virtual_path 44 | <% end %> 45 | -------------------------------------------------------------------------------- /lib/kontena/templates/haproxy/main.text.erb: -------------------------------------------------------------------------------- 1 | global 2 | pidfile /var/run/haproxy.pid 3 | user nobody 4 | group nobody 5 | maxconn 100000 6 | log <%= ENV['SYSLOG_TARGET'] || '127.0.0.1 local1 info' %> 7 | tune.ssl.default-dh-param 2048 8 | ssl-default-bind-ciphers <%= ENV['KONTENA_LB_SSL_CIPHERS'] || 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA' %> 9 | stats socket <%= Kontena::Actors::HaproxySpawner::ADMIN_SOCK %> mode 660 level admin expose-fd listeners 10 | <% if ENV['KONTENA_LB_GLOBAL_SETTINGS'] %> 11 | <% ENV['KONTENA_LB_GLOBAL_SETTINGS'].split("\n").each do |setting| %> 12 | <%= setting %> 13 | <% end %> 14 | <% end %> 15 | 16 | defaults 17 | log global 18 | mode http 19 | option splice-auto 20 | option http-keep-alive 21 | option redispatch 22 | option httplog 23 | retries 3 24 | timeout http-request 5s 25 | timeout queue 1m 26 | timeout connect 5s 27 | timeout client 1m 28 | timeout server 1m 29 | timeout http-keep-alive 10s 30 | timeout check 10s 31 | <% if ENV['KONTENA_LB_CUSTOM_SETTINGS'] %> 32 | <% ENV['KONTENA_LB_CUSTOM_SETTINGS'].split("\n").each do |setting| %> 33 | <%= setting %> 34 | <% end %> 35 | <% end %> 36 | 37 | <% if services.size > 0 || ssl? || health_uri? %> 38 | <%= http_in %> 39 | <% end %> 40 | 41 | <%= http_backends %> 42 | <%= tcp_proxies %> 43 | 44 | <%= render partial: 'haproxy/stats', format: :text %> 45 | 46 | -------------------------------------------------------------------------------- /lib/kontena/templates/haproxy/tcp_proxies.text.erb: -------------------------------------------------------------------------------- 1 | <% services.each do |service| %> 2 | <% if service.external_port %> 3 | listen <%= service.name %> 4 | bind *:<%= service.external_port %><% if ENV['KONTENA_LB_ACCEPT_PROXY'] %> accept-proxy<% end %> 5 | mode tcp 6 | <% service.custom_settings.each do |setting| %> 7 | <%= setting %> 8 | <% end %> 9 | 10 | balance <%= service.balance %> 11 | 12 | <% service.upstreams.each do |upstream| %> 13 | server <%= upstream.name %> <%= upstream.value %> check 14 | <% end %> 15 | <% end %> 16 | <% end %> -------------------------------------------------------------------------------- /lib/kontena/views/common.rb: -------------------------------------------------------------------------------- 1 | module Kontena::Views 2 | module Common 3 | 4 | def ssl? 5 | ENV['SSL_CERTS'] || ENV.any? { |env, value| env.start_with? 'SSL_CERT_' } 6 | end 7 | 8 | def health_uri? 9 | ENV['KONTENA_LB_HEALTH_URI'] 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/kontena/views/haproxy.rb: -------------------------------------------------------------------------------- 1 | require 'erb' 2 | require_relative 'common' 3 | require_relative 'http_in' 4 | require_relative 'http_backends' 5 | require_relative 'tcp_proxies' 6 | 7 | module Kontena::Views 8 | class Haproxy 9 | include Hanami::View 10 | include Kontena::Views::Common 11 | 12 | format :text 13 | template 'haproxy/main' 14 | 15 | def http_in 16 | _raw Kontena::Views::HttpIn.render(format: :text, services: services) 17 | end 18 | 19 | def http_backends 20 | _raw Kontena::Views::HttpBackends.render(format: :text, services: services) 21 | end 22 | 23 | def tcp_proxies 24 | _raw Kontena::Views::TcpProxies.render(format: :text, services: tcp_services) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/kontena/views/http_backends.rb: -------------------------------------------------------------------------------- 1 | module Kontena::Views 2 | class HttpBackends 3 | include Hanami::View 4 | 5 | def acme_challenges? 6 | Kontena::AcmeChallenges.configured? 7 | end 8 | 9 | format :text 10 | template 'haproxy/http_backends' 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/kontena/views/http_in.rb: -------------------------------------------------------------------------------- 1 | require_relative 'common' 2 | 3 | module Kontena::Views 4 | class HttpIn 5 | include Hanami::View 6 | include Kontena::Views::Common 7 | 8 | format :text 9 | template 'haproxy/http_in' 10 | 11 | def acme_challenges? 12 | Kontena::AcmeChallenges.configured? 13 | end 14 | 15 | def accept_proxy? 16 | ENV['KONTENA_LB_ACCEPT_PROXY'] 17 | end 18 | 19 | def http2? 20 | ENV['KONTENA_LB_HTTP2'].to_s != 'false' 21 | end 22 | 23 | def health_uri 24 | if uri = ENV['KONTENA_LB_HEALTH_URI'] 25 | _raw uri 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/kontena/views/tcp_proxies.rb: -------------------------------------------------------------------------------- 1 | module Kontena::Views 2 | class TcpProxies 3 | include Hanami::View 4 | 5 | format :text 6 | template 'haproxy/tcp_proxies' 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/kontena_lb.rb: -------------------------------------------------------------------------------- 1 | require 'concurrent/actor' 2 | require 'concurrent/future' 3 | require 'hanami/view' 4 | require 'webrick' 5 | 6 | require_relative 'kontena/logging' 7 | require_relative 'kontena/models/service' 8 | require_relative 'kontena/models/tcp_service' 9 | require_relative 'kontena/models/upstream' 10 | 11 | require_relative 'kontena/acme_challenges' 12 | require_relative 'kontena/cert_splitter' 13 | require_relative 'kontena/cert_manager' 14 | 15 | require_relative 'kontena/views/haproxy' 16 | 17 | require_relative 'kontena/actors/lb_supervisor' 18 | require_relative 'kontena/actors/acme_challenge_server' 19 | require_relative 'kontena/actors/etcd_watcher' 20 | require_relative 'kontena/actors/haproxy_config_generator' 21 | require_relative 'kontena/actors/haproxy_config_writer' 22 | require_relative 'kontena/actors/haproxy_spawner' 23 | require_relative 'kontena/actors/syslog_server' 24 | 25 | Hanami::View.configure do 26 | root 'lib/kontena/templates' 27 | end 28 | Hanami::View.load! 29 | -------------------------------------------------------------------------------- /prepare_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker pull nabz/docker-sslscan 3 | 4 | docker build -t lbtesthelper -f Dockerfile.testhelper . 5 | docker-compose -f docker-compose.test.yml stop 6 | docker-compose -f docker-compose.test.yml rm -f 7 | docker-compose -f docker-compose.test.yml up -d etcd 8 | sleep 3 9 | docker-compose -f docker-compose.test.yml build 10 | docker-compose -f docker-compose.test.yml up -d 11 | 12 | docker build -f Dockerfile.testhelper -t lbtesthelper . 13 | -------------------------------------------------------------------------------- /spec/fixtures/ssl/bundle1.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC+zCCAeOgAwIBAgIJAJXVkdCXj6aqMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV 3 | BAMMCWxvY2FsaG9zdDAeFw0xNzEwMjYwOTA1MjdaFw0yMDA3MjIwOTA1MjdaMBQx 4 | EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 5 | ggEBAKzwoA0yPoG+VqJr4F69Iaif0SumXfgt16zfaJJYKYqCOoq9qVjX89jA6CQ5 6 | q+zn7066ihK1pFHA2T+Be7rMRo76p1VG2YW1NQ1Dj6LENVqgJqqVaVJbe67pvTLz 7 | dzZRLbjjpRV8M83CiSYTm+p53qBAsvl5DWQBRIJwK45LIi5kz7UxqI+R+SPLQJqC 8 | 4+U8TDDJ1pEdZJ4qlb9eJuP8l6TXtLrSuaLzhTPg08JGigAag39Mg1F2nkNSCI6w 9 | R7qGw3EhWgHjhsNCS/jP/w+JKQbQyuDuO5bYCVI9zBGbMtpP6xeDKDhmDGTguvYN 10 | +opSq1Aija93kk6N9ueFxlY+y4UCAwEAAaNQME4wHQYDVR0OBBYEFMZSiX50Bls8 11 | j8j66O0FXr/ZWtYFMB8GA1UdIwQYMBaAFMZSiX50Bls8j8j66O0FXr/ZWtYFMAwG 12 | A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBABhw+3Pm0nLhwEAtmUscH0bx 13 | XenjMxP5F3GxIj8eJfIWO4fXS/LIXTcM0ghKbm6q0zdBVra/nLMvrjyxYJlOqn04 14 | WyvBLH2EXjvIl8Bl/JLvLlddJ4gCfu5zW4d7Bs2+MBPVvfzzfFHKlmua5GbNxPoM 15 | hbVhZfWWqvN493mBOLE1j/1Bgch4zZGwJefP1+YuI2QHTDk8XmtBkBWymRAD4hBa 16 | 8Yg+XLBHfcGeqpIiCRXnTlQuxabmZlDmwh/M+Cxiac676z76EN62+zcAgd1NpAO6 17 | BRkJQadwDC4A9ogEkBYXwG0X9/+ZB6CzG4Vx+vdSupkFMp9b0z2vcodfO39KONg= 18 | -----END CERTIFICATE----- 19 | -----BEGIN PRIVATE KEY----- 20 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCs8KANMj6Bvlai 21 | a+BevSGon9Erpl34Ldes32iSWCmKgjqKvalY1/PYwOgkOavs5+9OuooStaRRwNk/ 22 | gXu6zEaO+qdVRtmFtTUNQ4+ixDVaoCaqlWlSW3uu6b0y83c2US2446UVfDPNwokm 23 | E5vqed6gQLL5eQ1kAUSCcCuOSyIuZM+1MaiPkfkjy0CaguPlPEwwydaRHWSeKpW/ 24 | Xibj/Jek17S60rmi84Uz4NPCRooAGoN/TINRdp5DUgiOsEe6hsNxIVoB44bDQkv4 25 | z/8PiSkG0Mrg7juW2AlSPcwRmzLaT+sXgyg4Zgxk4Lr2DfqKUqtQIo2vd5JOjfbn 26 | hcZWPsuFAgMBAAECggEAF2tikUbrlhBblRU8xjeglkBGSD34XcJ/gYajl6XewkYO 27 | fXlftItSF1lQVo+Ey8lA7A1w40W74eJWyTXUtqAxMe2ZuX+lt2iprYknq2gcvZAQ 28 | jGs9XwzKfA5lM8Aqta1anr4dPgKa1VNx1Tk0lRU548O/OO9+s7tENtHP5C4ii9uc 29 | EPTq8V0vm2VbZOW17Yfw6a7RlxEvHP3JorECUvQwgYUSwgjQ2LLBEsCVpWo9w3z6 30 | ffCbz9X6JJ8DeqKS524o1RMyifdGgynnB8zVYsLwnEPTp5mFDec5+jWLjTCO/m+e 31 | IiGuaeeUBSG+oLREDjhw+bkb3wY/MGZsC546tOsKRQKBgQDW4XhAA5ES5ZNG5yMC 32 | rvBF1qtHFXxd060kn7Ndz6oGkGuZ99G0+NXHuaSP3CqEuOYdg2NS2LX32EHhwnFF 33 | /uRR4UKHLKJL/Ai1jk8/L+7sQSezzzW/Mek5v4lxH9hflfR2zH99Qb5Zemld5I1f 34 | rBuEGvLUTfv1nGCD+5PPZWnwCwKBgQDOCJGiSV4gozTO+Uq9yIuWJ9JB5JpM6IVF 35 | CNxmBseFLNZcnKb/Bl/NTH87Y7uk4r6nQ0VMfwv+dwd+wa/y0eau92T+PXo2sspp 36 | vlwGPP5RrcsDpOl5jiT022/MI9ybgnjNWFE3bA4DuCNua49LRTxW8Q1Leiu740EU 37 | XcnrFMecrwKBgBX5bMCvHLDgBVWk4XGuzid2MoHMcrFtqjEqm78mM28EadyO+UUW 38 | hVYtZ+TGURrNhcrS2t9oBgPYe7RInCjaTiMJdDI6oEZA+esHKJd/oWFLsHG06Pwq 39 | cH1VVwrYhNoRjbRwaUE37e1clVXiv4pfIVk7IEYRy4hse3pDyfPVnSXNAoGBAKX4 40 | 7SiopbTxBId+9yCvPxM0/QGr4Ej4PvN/0dw2td+oYP62CykBv4coio4TJ4QKTL99 41 | R4P6DHVu+ZC5Ar4/LO/hx2+vopYRrVFF0egMlmrB7/r9jD8prMe7RfJTKVH05s+0 42 | x6g32YpReelnqEVgft0izizxO+3dgf2gGBrR4INtAoGAIC3t93txnUNlRu0He6mL 43 | alFBjeDBl80/bhM74Aqo0veAHHwNZw5xg4dSwe5ydv5Ei6fLl5A4mqInPfRJhEVe 44 | WpuPlhWSprWv6eunCzG1huWrda4/0aXiOc/n5mdHAZ6v7sBrKEWWSiZlZ6+jay5M 45 | adNVFeXMFuwC0t7u0nUTQss= 46 | -----END PRIVATE KEY----- 47 | -------------------------------------------------------------------------------- /spec/fixtures/ssl/bundle2_invalid.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDQzCCAiugAwIBAgIJAOU1G4gSj6nYMA0GCSqGSIb3DQEBCwUAMDgxCjAIBgNV 3 | BAMMASoxHTAbBgNVBAoMFE15IENvbXBhbnkgTmFtZSBMVEQuMQswCQYDVQQGEwJV 4 | UzAeFw0xNjA2MDMwNjU2MTdaFw0xOTA1MTkwNjU2MTdaMDgxCjAIBgNVBAMMASox 5 | HTAbBgNVBAoMFE15IENvbXBhbnkgTmFtZSBMVEQuMQswCQYDVQQGEwJVUzCCASIw 6 | DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK4TvlMa3/9YK1aLSWBh1QPGulQd 7 | aYLoCSDYoTMv0YOO4lLDPBkU6fzjwWX44nesBjUmN61QxBkpGNTs9x6pNvd4YI7N 8 | arhxSljROf42vNWkoVKl0Ls3fgUxMAcbFYf38i5PLzVLebsIZR2+sVe6QdwWAame 9 | nhlEE1ubnx46xROCTLkjfzLlYdSJBNPx8gKovgf31gWU9dtVrdkBclHmglspXZt0 10 | removed_to_invalidate_certs 11 | Qi89hSFKjDTFeovydxbaOR3k5PKqOFD1Lou9Ho87TeLBNNYkA16IdJ63dLMCAwEA 12 | AaNQME4wHQYDVR0OBBYEFNO0I5ogOcgUbWmaBj1hA8qt7F0rMB8GA1UdIwQYMBaA 13 | FNO0I5ogOcgUbWmaBj1hA8qt7F0rMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEL 14 | BQADggEBAHpJiP8ZblJmEuSP0lAJRovTPGhKZA4Pgpa9zimETCruBTkTZyCcNwD3 15 | htHgrqkCieoV5RK5juyHVQo5GiVptcWxVSuGgr9tKtWTYlfB0xkRRT9No0bjT9J8 16 | cGTZm+lZyQ85AuwOPSe1wFUDPuLu8xJVPJoIeDNaKqjZleVZUNafPSKRWhi0X6rt 17 | JmwgQdUBqRgZYHL6Zpcr2E5oyc4tL10pYcIQchyDHg0IHyVb/xz072RFRKj0zP4A 18 | 2wVolufLJCFVbc8g1vEDgRjsuGLN55bJ0CTsn14GcOLsBntqKYyKm6lwV3+dDRM/ 19 | AacJXcMY+aRv/l5fNl0oMRcZpRr3gew= 20 | -----END CERTIFICATE----- 21 | -----BEGIN PRIVATE KEY----- 22 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCuE75TGt//WCtW 23 | i0lgYdUDxrpUHWmC6Akg2KEzL9GDjuJSwzwZFOn848Fl+OJ3rAY1JjetUMQZKRjU 24 | 7PceqTb3eGCOzWq4cUpY0Tn+NrzVpKFSpdC7N34FMTAHGxWH9/IuTy81S3m7CGUd 25 | vrFXukHcFgGpnp4ZRBNbm58eOsUTgky5I38y5WHUiQTT8fICqL4H99YFlPXbVa3Z 26 | AXJR5oJbKV2bdAnWkLO2NbpK72+VsjV1wiZXMr44aQ67dZwdZZ11eWZyjfcrXQ2z 27 | f6NxhRlzJg/ItkIvPYUhSow0xXqL8ncW2jkd5OTyqjhQ9S6LvR6PO03iwTTWJANe 28 | iHSet3SzAgMBAAECggEBAIo8y4ubb/6Kuf/EJMURa+PP0PAzWzLFqVoYLgtEEhFz 29 | Sm+G8xbH8PkOtOqRtuZqCZPzgYt09AU3Ca0tcSE8J5ZmVeeRYQqPjQbzQCaMuXC/ 30 | iAzl+NhzvPPKl+VMsNCFKiF0aHzeLxFEHWh9or+T/fEU2MUmXU6bLPQ2pSmQaiiO 31 | pEDH0d5xuWP90LgfPJoKcBlai140Ml+gLTnTfBIElQg06TiSSQM2yVfF5sz7Sbe2 32 | pjTqyVNDP01pBoUpR2BAB/JbeqaWM8jnnIi+35uE1kvttq97T3WfBHcW6j7RN5N8 33 | HmsxDcG8GPV/6Z7j5ZGHACsQsI6bSbXdFACDOM4e74ECgYEA54I0Tut4NBlI0RkD 34 | removed_to_invalidate_certs 35 | v4ZWS2blGUguFGybgyaeJemqW+CdcBTzglsOXDi4C1NrLqgX3+c8PULsN0wns5B0 36 | KoRU+oBrK/coPT1/J0cnLWqdfcMCgYEAwH4o7HIk8zour+7f1fNB7642dbndCQut 37 | c+Kf48WDQpCOLCHvHujOY92XCVkEVP63lsc/JJCr9s8fxnUIX2z5/bwpKZn4f4Zy 38 | fzwslzIWCLXAyPTHaux9u+8vtJRTPaQ0YYOigSn1xdLu5Xu/761jStQTs1i4MN3P 39 | VI68gffWDlECgYEA4FhAAn6TRMF/3AmGqOGbJeibbaACp8FAe4x5qJhIz7Ekau0R 40 | gRrAds6IgQ2O1w0dWTXmBsRdriOSxz/+4If5FibHOoHFDcvVw/lnZkwS5+g6CUR0 41 | Wc2Nk/bu+yLCijsgr7ywlplEua2WB5+jwxPsGbjaoodnujjfAJwmLg/UQOsCgYAq 42 | OAF1yps8FZjD0ZqabF4b2ZPsQjWulDcY4a274Ugmw1nLaC3wE5Og56sGy9VdZviR 43 | Q2Yf+PMekNMhTe3mMBqsgiZtD24nWi+mpGYLS1r10hdUfAt48iGppI5MBvQy4t7y 44 | PFLaDX/wQZFQF9JDGT5b3SPtBBpx7VRZ8Wx6/Qaf4QKBgDN7nX2YAt0t60kFzL2V 45 | hdSuTbjdDTNYALHrPlOiUV0LXOWSpeJpjI7BbFYbwEXHWyJ6XJs/uS1pX9AfglXY 46 | KNjEBaOzuTkyIX1THmpnt9417EVQeCYYbMvnKvzhVi32Enr5p8Y5t8i75lrfpvta 47 | XxGBmgtsa++i6DvXUp/Jv1Xy 48 | -----END PRIVATE KEY----- 49 | -------------------------------------------------------------------------------- /spec/fixtures/ssl/test1.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC9TCCAd2gAwIBAgIJAK94fUzfHt1pMA0GCSqGSIb3DQEBCwUAMBExDzANBgNV 3 | BAMMBnRlc3QtMTAeFw0xNzEwMjYxMDA5MDNaFw0yMDA3MjIxMDA5MDNaMBExDzAN 4 | BgNVBAMMBnRlc3QtMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMP9 5 | 6ym6ptYHFh0o2aoGqTUL+Di+BYNMTDA2CciHtMPL7/SqdtS8Dej0hguyiee37D07 6 | b+Lo9I4/wBYVtCIUrDqqQvYwkAsUbZKD+nXWalfpSGtt3iFT4nVCg833yK6b6/JN 7 | TqpMmruMjn9sadzEAxabHU9at/j9ZCLrQBdgrGRhGJcCgPSc3jTLAEz5gf45F3DO 8 | vD/4aZYCsjS2qFHvtzFBP8pVqRP9CiXvCw/tuwN7wA7wBeJ185JPax8FfSELaDbZ 9 | Yd63GEVVCmM87HS8faCCbffmaciPoHPl8TyM3+FaGSot3HT6VLnkVFeQR470nKxT 10 | 0PX/iSD++b05l8GGjKECAwEAAaNQME4wHQYDVR0OBBYEFLJn8DEv3K4ISyzh8Du5 11 | GnOeT6WIMB8GA1UdIwQYMBaAFLJn8DEv3K4ISyzh8Du5GnOeT6WIMAwGA1UdEwQF 12 | MAMBAf8wDQYJKoZIhvcNAQELBQADggEBAL/KI42JqasFCofB+ii4L6qodHohhaw1 13 | tso6YQTSwvwYDG/tmjUdIRB3W2Vi4eUUHjhH0/CJk/jZaTOpQrC/Kd89gcQr+UzM 14 | uXx2bDGMp6VszE9wFPBpczoPndmc9ExPijbtOD4Q5NRpOgxs55JoVLyOEijvSfEn 15 | dQetBiLeudiYkQT89APysveqkj2WojK6H3Obvns9bZ5HlnGcWP71ttY1/HyIMd1P 16 | 2d10zFA66rriEwlcsOTKuusJ0y2OUOA6Fzi3v1vInDvx21QzLX3NMUtqn3t5qtto 17 | UQCkJrk2TE6mrL92igBV/+ckB5cGvJ5L8viyvijezX3xjOzn9JGhFhA= 18 | -----END CERTIFICATE----- 19 | -----BEGIN PRIVATE KEY----- 20 | MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDD/espuqbWBxYd 21 | KNmqBqk1C/g4vgWDTEwwNgnIh7TDy+/0qnbUvA3o9IYLsonnt+w9O2/i6PSOP8AW 22 | FbQiFKw6qkL2MJALFG2Sg/p11mpX6Uhrbd4hU+J1QoPN98ium+vyTU6qTJq7jI5/ 23 | bGncxAMWmx1PWrf4/WQi60AXYKxkYRiXAoD0nN40ywBM+YH+ORdwzrw/+GmWArI0 24 | tqhR77cxQT/KVakT/Qol7wsP7bsDe8AO8AXidfOST2sfBX0hC2g22WHetxhFVQpj 25 | POx0vH2ggm335mnIj6Bz5fE8jN/hWhkqLdx0+lS55FRXkEeO9JysU9D1/4kg/vm9 26 | OZfBhoyhAgMBAAECggEBAIm3y93npUHxes2EneZGhfGbdpFQnQkUvNiHsDozeYa3 27 | r+YpPhTgC5os8GAZ1aN4bszcDhPRA79M9onOOGRWSGt0plbd6umOMixpBr50qwcZ 28 | CmVKr3KVwiQJWBqLyX1AXPxG7EboSzYMXzkUkhKpvU3OMztGkM2qKAoNalzC9oAV 29 | KdxTp52bG2iekBNj1b839jl57eRmJsGcAcRXMtxblvak84LEfPhnXSUp0nhhTHD2 30 | Dr+jvxF+q8TbIyf+StKppi3KmHliSr1qqLKC3+9qsHRCw0REnqOTJ04QHjo3QjUU 31 | 3Rlpal/b9yFyxT5o9+doQKKU7EISRwERfn2POLOO+pECgYEA916+BKBF8jq4Yghu 32 | G+b/zEZpeVeRYbPnxCgqrqI83+tistMbJxI1cHch7VLl6hVngD+U4BOnXRUGwHmG 33 | UxH7mzurX7ULJfoTSpN+4yMsd9kuBvlzoqgzaxg6Cl1adRQJUcaf1Ki3DN7ToFMU 34 | WRQdFNaVrEvEA5VefPeAk+utMLUCgYEAytRRhDMafqpFzVj/LSvYDVuMQCHMqRpJ 35 | S71HyW+qunyhzlYSUqk9IHWLDIET0bIZhVa4yThvnv3nekJ+OGPsci1Yi3uOziJK 36 | Oedv+UMu83p/Lo7pgMlfjj8EjOtAd9d3Sn35YIH8hQDF2iyCFABaVFCFwMkVEE8A 37 | rZu8naT8m70CgYEAz5kbLxaynM7a3qrkfVYnZm/RJJxwzeYFo4FyEIznOaR5eEni 38 | h6+oWXIhbuIbQZAlBGRXtJXJ5zw2JmHWcPCuj2BMOk3dxUlR10xhOI3US+Bf2EqQ 39 | 2Pj/7eivDPO7bnYaPB7NE9Nji9GVGP+gHAHdRhewFKChJ8C7Q3US2xD2j+ECgYEA 40 | k/aZVOh23opWi3vuA0TlwrDTOoGtrHrpl2AIe3GDybFb1ItDqJufZQt6mW+cRrA3 41 | H+dovBn4i7LL54uUSozSk2RzIKXNQqEPJvin3d3d5W6qUwucWgANPlbIegiwKfy8 42 | IFKP1pBc56Xtr8AiUHcFblajjETkodYQN5XR3erbAL0CgYEA6KfaCQkIe3NbTN6H 43 | o1hmwFgF2p4gjM202GwR6A4f4uvgrmnd76YDTmZidFbGfFw5igB3Gfisjd9nthtZ 44 | +NUq8hr7lx+ZJ413LyF/9CQAQn1l5BuIpcDGDirm8od4nZvg53qN1Q8AqEzZFcMw 45 | IJepvjbc/iTWfYqrWuod1wcBU5A= 46 | -----END PRIVATE KEY----- 47 | -------------------------------------------------------------------------------- /spec/kontena/acme_challenges_spec.rb: -------------------------------------------------------------------------------- 1 | describe Kontena::AcmeChallenges do 2 | let(:challenge_token) { 'LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0' } 3 | let(:key_authorization) { 'LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0.9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI' } 4 | 5 | subject { described_class.new( 6 | challenge_token => key_authorization, 7 | ) } 8 | 9 | describe '#configured?' do 10 | let(:env) { { } } 11 | 12 | before do 13 | allow(described_class).to receive(:env).and_return(env) 14 | end 15 | 16 | context 'with an empty env' do 17 | it 'is false' do 18 | expect(described_class.configured?).to be_falsey 19 | end 20 | end 21 | 22 | context 'with non-challenge envs' do 23 | let(:env) { { 'FOO' => 'bar' } } 24 | 25 | it 'is false' do 26 | expect(described_class.configured?).to be_falsey 27 | end 28 | end 29 | 30 | context 'with challenge envs' do 31 | let(:env) { { "ACME_CHALLENGE_#{challenge_token}" => key_authorization } } 32 | 33 | it 'is true' do 34 | expect(described_class.configured?).to be_truthy 35 | end 36 | end 37 | end 38 | 39 | describe '#boot' do 40 | it 'loads nothing from env empty env' do 41 | challenges = described_class.boot({}) 42 | 43 | expect(challenges.challenges?).to be_falsey 44 | end 45 | 46 | it 'loads challenge from ACME_CHALLENGE_*' do 47 | challenges = described_class.boot({"ACME_CHALLENGE_#{challenge_token}" => key_authorization}) 48 | 49 | expect(challenges.challenges).to eq(challenge_token => key_authorization) 50 | end 51 | end 52 | 53 | describe '#respond' do 54 | it 'returns nil for a non-existant challenge' do 55 | expect(subject.respond('foo')).to be_nil 56 | end 57 | 58 | it 'returns the key authorization for a known challenge' do 59 | expect(subject.respond(challenge_token)).to eq key_authorization 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/kontena/actors/acme_challenge_server_spec.rb: -------------------------------------------------------------------------------- 1 | describe Kontena::Actors::AcmeChallengeServer do 2 | let(:acme_challenges) { instance_double(Kontena::AcmeChallenges) } 3 | 4 | subject { described_class.new(acme_challenges, 5 | webrick_options: { :DoNotListen => true }, 6 | ) } 7 | let(:server) { subject.instance_variable_get('@server') } 8 | 9 | describe 'HTTP' do 10 | let(:http_request) { instance_double(WEBrick::HTTPRequest, 11 | request_method: http_method, 12 | unparsed_uri: http_path, 13 | path: WEBrick::HTTPUtils::normalize_path(http_path), 14 | ) } 15 | let(:http_response) { WEBrick::HTTPResponse.new(server.config) } 16 | 17 | before do 18 | # XXX: should not be mocking the WEBrick::HTTPRequest 19 | allow(http_request).to receive(:script_name=) 20 | allow(http_request).to receive(:path_info=) 21 | end 22 | 23 | context 'GET /' do 24 | let(:http_method) { 'GET' } 25 | let(:http_path) { '/' } 26 | 27 | it 'responds with HTTP 404' do 28 | expect{server.service(http_request, http_response)}.to raise_error(WEBrick::HTTPStatus::NotFound) 29 | end 30 | end 31 | 32 | context 'GET /.well-known/acme-challenge' do 33 | let(:http_method) { 'GET' } 34 | let(:http_path) { '/.well-known/acme-challenge' } 35 | 36 | it 'responds with HTTP 404' do 37 | expect{server.service(http_request, http_response)}.to raise_error(WEBrick::HTTPStatus::NotFound) 38 | end 39 | end 40 | 41 | context 'GET /.well-known/acme-challenge/:token' do 42 | let(:acme_token) { 'LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0' } 43 | let(:acme_authorization) { 'LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0.9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI' } 44 | let(:http_method) { 'GET' } 45 | let(:http_path) { "/.well-known/acme-challenge/#{acme_token}" } 46 | 47 | context 'with a matching ACME_CHALLENGE' do 48 | before do 49 | expect(acme_challenges).to receive(:respond).with(acme_token).and_return(acme_authorization) 50 | end 51 | 52 | it 'responds with HTTP 200' do 53 | server.service(http_request, http_response) 54 | 55 | expect(http_response.status).to eq 200 56 | expect(http_response.body).to eq acme_authorization 57 | end 58 | end 59 | 60 | context 'without any matching ACME_CHALLENGE' do 61 | before do 62 | expect(acme_challenges).to receive(:respond).with(acme_token).and_return(nil) 63 | end 64 | 65 | it 'responds with HTTP 440' do 66 | server.service(http_request, http_response) 67 | 68 | expect(http_response.status).to eq 404 69 | end 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/kontena/actors/etcd_watcher_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | describe Kontena::Actors::EtcdWatcher do 3 | 4 | end 5 | -------------------------------------------------------------------------------- /spec/kontena/actors/haproxy_config_generator_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | describe Kontena::Actors::HaproxyConfigGenerator do 3 | 4 | describe '#generate_service' do 5 | let(:node) do 6 | double(:node, key: '/foo/bar', children: [ 7 | double(:upstreams, key: '/foo/bar/upstreams', children: [ 8 | double(:a, key: '/foo/bar/upstreams/a', value: 'a:8080'), 9 | double(:b, key: '/foo/bar/upstreams/b', value: 'b:8080'), 10 | ]) 11 | ]) 12 | end 13 | 14 | it 'generates a service object with upstreams' do 15 | service = subject.generate_service(node) 16 | expect(service.upstreams.size).to eq(2) 17 | end 18 | 19 | it 'generates service object with balance' do 20 | node.children << double(:balance, key: "#{node.key}/balance", value: 'leastconn') 21 | service = subject.generate_service(node) 22 | expect(service.balance).to eq('leastconn') 23 | end 24 | 25 | it 'generates service object with virtual_path' do 26 | node.children << double(:keep_virtual_path, key: "#{node.key}/virtual_path", value: '/api') 27 | service = subject.generate_service(node) 28 | expect(service.virtual_paths).to eq(['/api']) 29 | end 30 | 31 | it 'generates service object with keep_virtual_path' do 32 | node.children << double(:keep_virtual_path, key: "#{node.key}/keep_virtual_path", value: "true") 33 | service = subject.generate_service(node) 34 | expect(service.keep_virtual_path?).to be_truthy 35 | end 36 | 37 | it 'generates service object with virtual_hosts' do 38 | virtual_hosts = ['api.domain.com', 'www.domain.com'] 39 | node.children << double(:keep_virtual_hosts, key: "#{node.key}/virtual_hosts", value: virtual_hosts.join(',')) 40 | service = subject.generate_service(node) 41 | expect(service.virtual_hosts).to eq(virtual_hosts) 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/kontena/actors/haproxy_config_writer_spec.rb: -------------------------------------------------------------------------------- 1 | describe Kontena::Actors::HaproxyConfigWriter do 2 | let(:parent) { double(:parent) } 3 | 4 | before(:each) do 5 | allow(subject).to receive(:parent).and_return(parent) 6 | allow(subject).to receive(:write_config) 7 | end 8 | 9 | describe '#update_config' do 10 | it 'writes config on first call' do 11 | expect(subject).to receive(:write_config).once 12 | expect(parent).to receive(:<<).once 13 | subject.update_config('updated config') 14 | end 15 | 16 | it 'does not write config when config has no changes' do 17 | expect(subject).to receive(:write_config).once 18 | expect(parent).to receive(:<<).once 19 | 2.times do 20 | subject.update_config('updated config') 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/kontena/actors/haproxy_spawner_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | describe Kontena::Actors::HaproxySpawner do 3 | 4 | describe '#update_haproxy' do 5 | it 'does not start or update haproxy if config is invalid' do 6 | allow(subject).to receive(:validate_config).and_return(false) 7 | expect(subject).not_to receive(:start_haproxy) 8 | expect(subject).not_to receive(:reload_haproxy) 9 | subject.update_haproxy 10 | end 11 | 12 | it 'starts haproxy if config is valid and haproxy is not yet running' do 13 | allow(subject).to receive(:children).and_return([]) 14 | allow(subject).to receive(:validate_config).and_return(true) 15 | expect(subject).to receive(:start_haproxy) 16 | expect(subject).not_to receive(:reload_haproxy) 17 | subject.update_haproxy 18 | end 19 | 20 | it 'reloads haproxy if config is valid and haproxy is already running' do 21 | allow(subject).to receive(:validate_config).and_return(true) 22 | allow(subject).to receive(:children).and_return([1]) 23 | expect(subject).not_to receive(:start_haproxy) 24 | expect(subject).to receive(:reload_haproxy) 25 | subject.update_haproxy 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/kontena/actors/lb_supervisor_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | describe Kontena::Actors::LbSupervisor do 3 | 4 | end 5 | -------------------------------------------------------------------------------- /spec/kontena/actors/syslog_server_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | describe Kontena::Actors::SyslogServer do 3 | 4 | describe '#handle_data' do 5 | it 'outputs valid data' do 6 | data = '<30> daa daa' 7 | expect(subject).to receive(:puts).with(' daa daa') 8 | subject.handle_data(data) 9 | end 10 | 11 | it 'does not output invalid data' do 12 | data = 'daa daa' 13 | expect(subject).not_to receive(:puts) 14 | subject.handle_data(data) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/kontena/cert_manager_spec.rb: -------------------------------------------------------------------------------- 1 | describe Kontena::CertManager do 2 | include FixturesHelper 3 | 4 | let(:ssl_bundle1) { fixture('ssl/bundle1.pem').rstrip } 5 | let(:ssl_bundle2_invalid) { fixture('ssl/bundle2_invalid.pem').rstrip } 6 | let(:ssl_test1) { fixture('ssl/test1.pem').rstrip } 7 | 8 | subject { described_class.new( 9 | 'cert1_gen' => ssl_bundle1, 10 | 'SSL_CERT_test1' => ssl_test1, 11 | ) } 12 | 13 | describe '#boot' do 14 | it 'does nothing with an empty env' do 15 | expect_any_instance_of(described_class).to_not receive(:setup) 16 | expect_any_instance_of(described_class).to_not receive(:write_cert) 17 | 18 | described_class.boot({}) 19 | end 20 | 21 | it 'does nothing with an empty SSL_CERTS' do 22 | expect_any_instance_of(described_class).to_not receive(:setup) 23 | expect_any_instance_of(described_class).to_not receive(:write_cert) 24 | 25 | described_class.boot({ 'SSL_CERTS' => '' }) 26 | end 27 | 28 | it 'setups and writes with SSL_CERTS' do 29 | expect_any_instance_of(described_class).to receive(:setup) 30 | expect_any_instance_of(described_class).to receive(:write_cert).with('cert1_gen', ssl_bundle1) 31 | 32 | described_class.boot({ 'SSL_CERTS' => ssl_bundle1 + "\n" + ssl_bundle2_invalid }) 33 | end 34 | 35 | it 'setups and writes with SSL_CERTS + SSL_CERT_*' do 36 | expect_any_instance_of(described_class).to receive(:setup) 37 | expect_any_instance_of(described_class).to receive(:write_cert).with('cert1_gen', ssl_bundle1) 38 | expect_any_instance_of(described_class).to receive(:write_cert).with('SSL_CERT_test1', ssl_test1) 39 | 40 | described_class.boot({ 'SSL_CERTS' => ssl_bundle1 + "\n" + ssl_bundle2_invalid, 'SSL_CERT_test1' => ssl_test1, }) 41 | end 42 | 43 | it 'setups and writes with SSL_CERT_*' do 44 | expect_any_instance_of(described_class).to receive(:setup) 45 | expect_any_instance_of(described_class).to receive(:write_cert).with('SSL_CERT_bundle1', ssl_bundle1) 46 | expect_any_instance_of(described_class).to receive(:write_cert).with('SSL_CERT_test1', ssl_test1) 47 | 48 | described_class.boot({ 'SSL_CERT_bundle1' => ssl_bundle1, 'SSL_CERT_test1' => ssl_test1, }) 49 | end 50 | end 51 | 52 | describe '#write_certs' do 53 | it 'writes each cert if valid' do 54 | expect(subject).to receive(:write_cert).once.with('cert1_gen', ssl_bundle1) 55 | expect(subject).to receive(:write_cert).once.with('SSL_CERT_test1', ssl_test1) 56 | 57 | subject.write_certs 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/kontena/cert_splitter_spec.rb: -------------------------------------------------------------------------------- 1 | describe Kontena::CertSplitter do 2 | include FixturesHelper 3 | 4 | let(:ssl_bundle1) { fixture('ssl/bundle1.pem').rstrip } 5 | let(:ssl_bundle2_invalid) { fixture('ssl/bundle2_invalid.pem').rstrip } 6 | let(:ssl_test1) { fixture('ssl/test1.pem').rstrip } 7 | 8 | describe '#split_certs' do 9 | it 'returns an array of strings' do 10 | expect(subject.split_certs([ssl_bundle1, ssl_bundle2_invalid].join("\n"))).to eq [ 11 | ssl_bundle1.rstrip, 12 | ssl_bundle2_invalid.rstrip, 13 | ] 14 | end 15 | 16 | it 'returns an empty array for an empty string' do 17 | expect(subject.split_certs('')).to eq [] 18 | end 19 | end 20 | 21 | describe '#to_h' do 22 | subject { described_class.new(env) } 23 | 24 | context 'with an empty env' do 25 | let(:env) { { 26 | 27 | } } 28 | 29 | it 'returns an empty hash' do 30 | expect(subject.to_h).to eq({}) 31 | end 32 | end 33 | 34 | context 'with an empty SSL_CERTS env' do 35 | let(:env) { { 36 | 'SSL_CERTS' => '', 37 | } } 38 | 39 | it 'returns an empty hash' do 40 | expect(subject.to_h).to eq({}) 41 | end 42 | end 43 | 44 | context 'with SSL_CERTS' do 45 | let(:env) { { 46 | 'SSL_CERTS' => [ssl_bundle1, ssl_bundle2_invalid].join("\n"), 47 | } } 48 | 49 | it 'returns a hash with the valid cert' do 50 | expect(subject.to_h).to eq( 51 | 'cert1_gen' => ssl_bundle1, 52 | ) 53 | end 54 | end 55 | 56 | context 'with SSL_CERTS + SSL_CERT_*' do 57 | let(:env) { { 58 | 'SSL_CERTS' => [ssl_bundle1, ssl_bundle2_invalid].join("\n"), 59 | 'SSL_CERT_test1' => ssl_test1, 60 | 61 | } } 62 | 63 | it 'returns a hash with the valid cert' do 64 | expect(subject.to_h).to eq( 65 | 'cert1_gen' => ssl_bundle1, 66 | 'SSL_CERT_test1' => ssl_test1, 67 | ) 68 | end 69 | end 70 | 71 | context 'with SSL_CERT_*' do 72 | let(:env) { { 73 | 'SSL_CERT_bundle1' => ssl_bundle1, 74 | 'SSL_CERT_bundle2' => ssl_bundle2_invalid, 75 | } } 76 | 77 | it 'returns a hash with the valid cert' do 78 | expect(subject.to_h).to eq( 79 | 'SSL_CERT_bundle1' => ssl_bundle1, 80 | ) 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /spec/kontena/models/service_spec.rb: -------------------------------------------------------------------------------- 1 | describe Kontena::Models::Service do 2 | 3 | let(:subject) { described_class.new('test-service') } 4 | 5 | describe '#keep_virtual_path?' do 6 | it 'returns false by default' do 7 | expect(subject.keep_virtual_path?).to be_falsey 8 | end 9 | 10 | it 'returns false if keep virtual path is set to invalid value' do 11 | subject.keep_virtual_path = 'foo' 12 | expect(subject.keep_virtual_path?).to be_falsey 13 | end 14 | 15 | it 'returns true if keep virtual path is set' do 16 | subject.keep_virtual_path = 'true' 17 | expect(subject.keep_virtual_path?).to be_truthy 18 | end 19 | end 20 | 21 | describe '#virtual_hosts?' do 22 | it 'returns false by default' do 23 | expect(subject.virtual_hosts?).to be_falsey 24 | end 25 | 26 | it 'returns true if virtual hosts are set' do 27 | subject.virtual_hosts = ['www.domain.com'] 28 | expect(subject.virtual_hosts?).to be_truthy 29 | end 30 | end 31 | 32 | describe '#cookie?' do 33 | it 'returns false by default' do 34 | expect(subject.cookie?).to be_falsey 35 | end 36 | 37 | it 'returns true when cookie is set to empty string' do 38 | subject.cookie = '' 39 | expect(subject.cookie?).to be_truthy 40 | end 41 | 42 | it 'returns true when cookie is set' do 43 | subject.cookie = 'cookie KONTENA_SERVERID insert indirect nocache' 44 | end 45 | end 46 | 47 | describe '#custom_settings?' do 48 | it 'returns false by default' do 49 | expect(subject.custom_settings?).to be_falsey 50 | end 51 | 52 | it 'returns true when custom settings' do 53 | subject.custom_settings = '...' 54 | expect(subject.custom_settings?).to be_truthy 55 | end 56 | end 57 | end -------------------------------------------------------------------------------- /spec/kontena/models/tcp_service_spec.rb: -------------------------------------------------------------------------------- 1 | describe Kontena::Models::TcpService do 2 | 3 | end -------------------------------------------------------------------------------- /spec/kontena/models/upstream_spec.rb: -------------------------------------------------------------------------------- 1 | describe Kontena::Models::Upstream do 2 | 3 | end -------------------------------------------------------------------------------- /spec/kontena/views/haproxy_spec.rb: -------------------------------------------------------------------------------- 1 | describe Kontena::Views::Haproxy do 2 | 3 | describe '.render' do 4 | context 'http-in' do 5 | it 'does not configure http-in by default' do 6 | output = described_class.render( 7 | format: :text, 8 | services: [], 9 | tcp_services: [] 10 | ) 11 | expect(output.match(/listen http-in/)).to be_falsey 12 | end 13 | 14 | it 'configures http-in if health uri is defined' do 15 | allow(ENV).to receive(:[]) 16 | allow(ENV).to receive(:[]).with('KONTENA_LB_HEALTH_URI').and_return('/__health') 17 | output = described_class.render( 18 | format: :text, 19 | services: [], 20 | tcp_services: [] 21 | ) 22 | expect(output.match(/listen http-in/)).to be_truthy 23 | end 24 | 25 | it 'configures http-in if ssl certs' do 26 | allow(ENV).to receive(:[]) 27 | allow(ENV).to receive(:[]).with('SSL_CERTS').and_return('cert...') 28 | output = described_class.render( 29 | format: :text, 30 | services: [], 31 | tcp_services: [] 32 | ) 33 | expect(output.match(/listen http-in/)).to be_truthy 34 | end 35 | end 36 | context 'stats' do 37 | it 'configures stats auth' do 38 | allow(ENV).to receive(:[]) 39 | allow(ENV).to receive(:[]).with('STATS_PASSWORD').and_return('secrettzzz') 40 | output = described_class.render( 41 | format: :text, 42 | services: [], 43 | tcp_services: [] 44 | ) 45 | expect(output.match(/userlist stats-auth/)).to be_truthy 46 | expect(output.match(/user stats insecure-password secrettzzz/)) 47 | end 48 | end 49 | end 50 | end -------------------------------------------------------------------------------- /spec/kontena/views/http_backends_spec.rb: -------------------------------------------------------------------------------- 1 | describe Kontena::Views::HttpBackends do 2 | 3 | describe '.render' do 4 | context 'balance' do 5 | it 'sets balance to roundrobin by default' do 6 | services = [ 7 | Kontena::Models::Service.new('foo').tap { |s| 8 | s.upstreams = [Kontena::Models::Upstream.new('foo-1', '10.81.3.2:8080')] 9 | } 10 | ] 11 | output = described_class.render( 12 | format: :text, 13 | services: services 14 | ) 15 | expect(output.match(/balance roundrobin/)).to be_truthy 16 | end 17 | 18 | it 'sets balance to configured value' do 19 | services = [ 20 | Kontena::Models::Service.new('foo').tap { |s| 21 | s.balance = 'leastconn' 22 | s.upstreams = [Kontena::Models::Upstream.new('foo-1', '10.81.3.2:8080')] 23 | } 24 | ] 25 | output = described_class.render( 26 | format: :text, 27 | services: services 28 | ) 29 | expect(output.match(/balance leastconn/)).to be_truthy 30 | end 31 | end 32 | 33 | context 'cookies' do 34 | it 'does not add cookie policy if cookie is not set' do 35 | services = [ 36 | Kontena::Models::Service.new('foo').tap { |s| 37 | s.upstreams = [Kontena::Models::Upstream.new('foo-1', '10.81.3.2:8080')] 38 | } 39 | ] 40 | output = described_class.render( 41 | format: :text, 42 | services: services, 43 | tcp_services: [] 44 | ) 45 | expect(output.match(/cookie/)).to be_falsey 46 | end 47 | 48 | it 'adds default cookie policy if cookie value is empty string' do 49 | services = [ 50 | Kontena::Models::Service.new('foo').tap { |s| 51 | s.cookie = '' 52 | s.upstreams = [Kontena::Models::Upstream.new('foo-1', '10.81.3.2:8080')] 53 | } 54 | ] 55 | output = described_class.render( 56 | format: :text, 57 | services: services, 58 | tcp_services: [] 59 | ) 60 | expect(output.match(/cookie KONTENA_SERVERID insert indirect nocache/)).to be_truthy 61 | end 62 | 63 | it 'adds custom cookie policy if cookie value is not empty string' do 64 | services = [ 65 | Kontena::Models::Service.new('foo').tap { |s| 66 | s.cookie = 'cookie FOO_ID insert indirect nocache' 67 | s.upstreams = [Kontena::Models::Upstream.new('foo-1', '10.81.3.2:8080')] 68 | } 69 | ] 70 | output = described_class.render( 71 | format: :text, 72 | services: services, 73 | tcp_services: [] 74 | ) 75 | expect(output.match(/cookie FOO_ID insert indirect nocache/)).to be_truthy 76 | end 77 | end 78 | 79 | context 'basic auth' do 80 | it 'adds basic auth config' do 81 | services = [ 82 | Kontena::Models::Service.new('foo').tap { |s| 83 | s.basic_auth_secrets = 'user admin insecure-password passwd' 84 | s.upstreams = [Kontena::Models::Upstream.new('foo-1', '10.81.3.2:8080')] 85 | } 86 | ] 87 | output = described_class.render( 88 | format: :text, 89 | services: services, 90 | tcp_services: [] 91 | ) 92 | expect(output.match(/http-request auth realm foo/)).to be_truthy 93 | expect(output.match(/userlist auth_users_foo/)).to be_truthy 94 | expect(output.match(/user admin insecure-password passwd/)).to be_truthy 95 | end 96 | 97 | it 'does not add basic auth if basic_auth_secrets is not set' do 98 | services = [ 99 | Kontena::Models::Service.new('foo').tap { |s| 100 | s.upstreams = [Kontena::Models::Upstream.new('foo-1', '10.81.3.2:8080')] 101 | } 102 | ] 103 | output = described_class.render( 104 | format: :text, 105 | services: services, 106 | tcp_services: [] 107 | ) 108 | expect(output.match(/http-request auth realm foo/)).to be_falsey 109 | expect(output.match(/userlist auth_users_foo/)).to be_falsey 110 | end 111 | end 112 | 113 | context 'health check' do 114 | it 'adds healthcheck uri' do 115 | services = [ 116 | Kontena::Models::Service.new('foo').tap { |s| 117 | s.health_check_uri = '/healthz' 118 | s.upstreams = [Kontena::Models::Upstream.new('foo-1', '10.81.3.2:8080')] 119 | } 120 | ] 121 | output = described_class.render( 122 | format: :text, 123 | services: services, 124 | tcp_services: [] 125 | ) 126 | expect(output.match(/option httpchk GET \/healthz/)).to be_truthy 127 | end 128 | 129 | it 'uses given health_check port for http checks' do 130 | services = [ 131 | Kontena::Models::Service.new('foo').tap { |s| 132 | s.health_check_port = 9090 133 | s.upstreams = [Kontena::Models::Upstream.new('foo-1', '10.81.3.2:8080')] 134 | } 135 | ] 136 | output = described_class.render( 137 | format: :text, 138 | services: services, 139 | tcp_services: [] 140 | ) 141 | 142 | expect(output.match(/server foo-1 10.81.3.2:8080 check port 9090/)).to be_truthy 143 | end 144 | 145 | it 'defaults to backend port for http checks' do 146 | services = [ 147 | Kontena::Models::Service.new('foo').tap { |s| 148 | s.upstreams = [Kontena::Models::Upstream.new('foo-1', '10.81.3.2:8080')] 149 | } 150 | ] 151 | output = described_class.render( 152 | format: :text, 153 | services: services, 154 | tcp_services: [] 155 | ) 156 | expect(output.match(/server foo-1 10.81.3.2:8080 check/)).to be_truthy 157 | end 158 | end 159 | 160 | describe 'acme_challenges?' do 161 | let(:output) { described_class.render( 162 | format: :text, 163 | services: [], 164 | tcp_services: [] 165 | ) } 166 | 167 | context 'when not configured' do 168 | before do 169 | allow(Kontena::AcmeChallenges).to receive(:configured?).and_return(false) 170 | end 171 | 172 | it 'does not configure any ACL' do 173 | expect(output).to_not match /backend acme_challenge/ 174 | end 175 | end 176 | 177 | context 'when configured' do 178 | before do 179 | allow(Kontena::AcmeChallenges).to receive(:configured?).and_return(true) 180 | end 181 | 182 | it 'configures the ACL' do 183 | expect(output).to match /backend acme_challenge/ 184 | end 185 | end 186 | end 187 | end 188 | end 189 | -------------------------------------------------------------------------------- /spec/kontena/views/http_in_spec.rb: -------------------------------------------------------------------------------- 1 | describe Kontena::Views::HttpIn do 2 | 3 | describe '.render' do 4 | context 'bind' do 5 | it 'bings to port 80' do 6 | output = described_class.render( 7 | format: :text, 8 | services: [] 9 | ) 10 | expect(output.match(/bind \*:80/)).to be_truthy 11 | end 12 | 13 | it 'does not accept proxy protocol by default' do 14 | output = described_class.render( 15 | format: :text, 16 | services: [] 17 | ) 18 | expect(output.match(/accept-proxy/)).to be_falsey 19 | end 20 | 21 | it 'accepts proxy protocol if env is set' do 22 | allow(ENV).to receive(:[]) 23 | allow(ENV).to receive(:[]).with('KONTENA_LB_ACCEPT_PROXY').and_return('true') 24 | output = described_class.render( 25 | format: :text, 26 | services: [] 27 | ) 28 | expect(output.match(/accept-proxy/)).to be_truthy 29 | end 30 | 31 | it 'does not bind to port 443 by default' do 32 | output = described_class.render( 33 | format: :text, 34 | services: [] 35 | ) 36 | expect(output.match(/bind \*:443/)).to be_falsey 37 | end 38 | 39 | it 'binds to port 443 if SSL certs exist' do 40 | allow(ENV).to receive(:[]) 41 | allow(ENV).to receive(:[]).with('SSL_CERTS').and_return('certs') 42 | output = described_class.render( 43 | format: :text, 44 | services: [] 45 | ) 46 | expect(output.match(/bind \*:443/)).to be_truthy 47 | end 48 | 49 | it 'supports http2 if SSL certs exist' do 50 | allow(ENV).to receive(:[]) 51 | allow(ENV).to receive(:[]).with('SSL_CERTS').and_return('certs') 52 | output = described_class.render( 53 | format: :text, 54 | services: [] 55 | ) 56 | expect(output.match(/alpn h2/)).to be_truthy 57 | end 58 | 59 | it 'allows to disable http2 support' do 60 | allow(ENV).to receive(:[]) 61 | allow(ENV).to receive(:[]).with('KONTENA_LB_HTTP2').and_return('false') 62 | allow(ENV).to receive(:[]).with('SSL_CERTS').and_return('certs') 63 | output = described_class.render( 64 | format: :text, 65 | services: [] 66 | ) 67 | expect(output.match(/alpn h2/)).to be_falsey 68 | end 69 | end 70 | 71 | context 'monitor-uri' do 72 | it 'does not add monitor-uri by default' do 73 | output = described_class.render( 74 | format: :text, 75 | services: [] 76 | ) 77 | expect(output.match(/monitor-uri/)).to be_falsey 78 | end 79 | 80 | it 'adds monitor-uri if health uri is set' do 81 | allow(ENV).to receive(:[]) 82 | allow(ENV).to receive(:[]).with('KONTENA_LB_HEALTH_URI').and_return('/__health') 83 | output = described_class.render( 84 | format: :text, 85 | services: [] 86 | ) 87 | expect(output.match(/monitor-uri \/__health/)).to be_truthy 88 | end 89 | end 90 | 91 | describe 'acme_challenges?' do 92 | let(:output) { described_class.render( 93 | format: :text, 94 | services: [] 95 | ) } 96 | 97 | context 'when not configured' do 98 | before do 99 | allow(Kontena::AcmeChallenges).to receive(:configured?).and_return(false) 100 | end 101 | 102 | it 'does not configure any ACL' do 103 | expect(output).to_not match /use_backend acme_challenge/ 104 | end 105 | end 106 | 107 | context 'when configured' do 108 | before do 109 | allow(Kontena::AcmeChallenges).to receive(:configured?).and_return(true) 110 | end 111 | 112 | it 'configures the ACL' do 113 | expect(output).to match /use_backend acme_challenge/ 114 | end 115 | end 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /spec/kontena/views/tcp_proxies_spec.rb: -------------------------------------------------------------------------------- 1 | describe Kontena::Views::TcpProxies do 2 | 3 | let(:service_class) { Kontena::Models::TcpService } 4 | let(:upstream_class) { Kontena::Models::Upstream } 5 | 6 | describe '.render' do 7 | context 'bind' do 8 | it 'sets accept-proxy if env is set' do 9 | allow(ENV).to receive(:[]).with('KONTENA_LB_ACCEPT_PROXY').and_return('true') 10 | services = [ 11 | service_class.new('foo').tap { |s| 12 | s.external_port = 8080 13 | s.upstreams = [upstream_class.new('foo-1', '10.81.3.2:8080')] 14 | } 15 | ] 16 | output = described_class.render( 17 | format: :text, 18 | services: services 19 | ) 20 | expect(output.match(/accept-proxy/)).to be_truthy 21 | end 22 | 23 | it 'does not set accept-proxy without env' do 24 | services = [ 25 | service_class.new('foo').tap { |s| 26 | s.external_port = 8080 27 | s.upstreams = [upstream_class.new('foo-1', '10.81.3.2:8080')] 28 | } 29 | ] 30 | output = described_class.render( 31 | format: :text, 32 | services: services 33 | ) 34 | expect(output.match(/accept-proxy/)).to be_falsey 35 | end 36 | end 37 | 38 | context 'balance' do 39 | it 'sets balance to leastconn by default' do 40 | services = [ 41 | service_class.new('foo').tap { |s| 42 | s.external_port = 8080 43 | s.upstreams = [upstream_class.new('foo-1', '10.81.3.2:8080')] 44 | } 45 | ] 46 | output = described_class.render( 47 | format: :text, 48 | services: services 49 | ) 50 | expect(output.match(/balance leastconn/)).to be_truthy 51 | end 52 | 53 | it 'sets balance to configured value' do 54 | services = [ 55 | service_class.new('foo').tap { |s| 56 | s.balance = 'roundrobin' 57 | s.external_port = 8080 58 | s.upstreams = [upstream_class.new('foo-1', '10.81.3.2:8080')] 59 | } 60 | ] 61 | output = described_class.render( 62 | format: :text, 63 | services: services 64 | ) 65 | expect(output.match(/balance roundrobin/)).to be_truthy 66 | end 67 | end 68 | end 69 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | ENV['LOG_LEVEL'] ||= Logger::ERROR.to_s 3 | require_relative '../lib/kontena_lb' 4 | require_relative './support/fixtures_helper' 5 | 6 | RSpec.configure do |config| 7 | # rspec-expectations config goes here. You can use an alternate 8 | # assertion/expectation library such as wrong or the stdlib/minitest 9 | # assertions if you prefer. 10 | config.expect_with :rspec do |expectations| 11 | # This option will default to `true` in RSpec 4. It makes the `description` 12 | # and `failure_message` of custom matchers include text for helper methods 13 | # defined using `chain`, e.g.: 14 | # be_bigger_than(2).and_smaller_than(4).description 15 | # # => "be bigger than 2 and smaller than 4" 16 | # ...rather than: 17 | # # => "be bigger than 2" 18 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 19 | end 20 | 21 | # rspec-mocks config goes here. You can use an alternate test double 22 | # library (such as bogus or mocha) by changing the `mock_with` option here. 23 | config.mock_with :rspec do |mocks| 24 | # Prevents you from mocking or stubbing a method that does not exist on 25 | # a real object. This is generally recommended, and will default to 26 | # `true` in RSpec 4. 27 | mocks.verify_partial_doubles = true 28 | end 29 | 30 | # The settings below are suggested to provide a good initial experience 31 | # with RSpec, but feel free to customize to your heart's content. 32 | =begin 33 | # These two settings work together to allow you to limit a spec run 34 | # to individual examples or groups you care about by tagging them with 35 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 36 | # get run. 37 | config.filter_run :focus 38 | config.run_all_when_everything_filtered = true 39 | 40 | # Allows RSpec to persist some state between runs in order to support 41 | # the `--only-failures` and `--next-failure` CLI options. We recommend 42 | # you configure your source control system to ignore this file. 43 | config.example_status_persistence_file_path = "spec/examples.txt" 44 | 45 | # Limits the available syntax to the non-monkey patched syntax that is 46 | # recommended. For more details, see: 47 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 48 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 49 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 50 | config.disable_monkey_patching! 51 | 52 | # This setting enables warnings. It's recommended, but in some cases may 53 | # be too noisy due to issues in dependencies. 54 | config.warnings = true 55 | 56 | # Many RSpec users commonly either run the entire suite or an individual 57 | # file, and it's useful to allow more verbose output when running an 58 | # individual spec file. 59 | if config.files_to_run.one? 60 | # Use the documentation formatter for detailed output, 61 | # unless a formatter has already been configured 62 | # (e.g. via a command-line flag). 63 | config.default_formatter = 'doc' 64 | end 65 | 66 | # Print the 10 slowest examples and example groups at the 67 | # end of the spec run, to help surface which specs are running 68 | # particularly slow. 69 | config.profile_examples = 10 70 | 71 | # Run specs in random order to surface order dependencies. If you find an 72 | # order dependency and want to debug it, you can fix the order by providing 73 | # the seed, which is printed after each run. 74 | # --seed 1234 75 | config.order = :random 76 | 77 | # Seed global randomization in this process using the `--seed` CLI option. 78 | # Setting this allows you to use `--seed` to deterministically reproduce 79 | # test failures related to randomization by passing the same `--seed` value 80 | # as the one that triggered the failure. 81 | Kernel.srand config.seed 82 | =end 83 | end 84 | -------------------------------------------------------------------------------- /spec/support/fixtures_helper.rb: -------------------------------------------------------------------------------- 1 | module FixturesHelper 2 | FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/' 3 | 4 | def fixture_path(file) 5 | File.expand_path(FIXTURES_PATH + file) 6 | end 7 | def fixture(file) 8 | IO.read(fixture_path(file)) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/acme_challenge_test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "common" 4 | 5 | setup() { 6 | etcdctl rm --recursive /kontena/haproxy/lb/services || true 7 | 8 | sleep 1 9 | } 10 | 11 | @test "uses the challenges from ACME_CHALLENGE_*" { 12 | run curl -s http://localhost:8180/.well-known/acme-challenge/LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0 13 | 14 | [ "$status" -eq 0 ] 15 | [ "$output" = "LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0.9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI" ] 16 | } 17 | -------------------------------------------------------------------------------- /test/basic_auth_test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "common" 4 | 5 | 6 | setup() { 7 | etcdctl rm --recursive /kontena/haproxy/lb/services/service-b || true 8 | } 9 | 10 | 11 | @test "basic auth gives 401 without user and password" { 12 | etcdctl set /kontena/haproxy/lb/services/service-b/virtual_hosts www.foo.com 13 | etcdctl set /kontena/haproxy/lb/services/service-b/upstreams/server service-b:9292 14 | etcdctl set /kontena/haproxy/lb/services/service-b/basic_auth_secrets "user admin insecure-password passwd" 15 | sleep 1 16 | run curl -sL -w "%{http_code}" -H "Host: www.foo.com" http://localhost:8180/ -o /dev/null 17 | [ "${lines[0]}" = "401" ] 18 | 19 | } 20 | 21 | 22 | @test "basic auth gives 200 with valid user and password" { 23 | etcdctl set /kontena/haproxy/lb/services/service-b/virtual_hosts www.foo.com 24 | etcdctl set /kontena/haproxy/lb/services/service-b/upstreams/server service-b:9292 25 | etcdctl set /kontena/haproxy/lb/services/service-b/basic_auth_secrets "user admin insecure-password passwd" 26 | sleep 1 27 | run curl -s -H "Host: www.foo.com" http://admin:passwd@localhost:8180/ 28 | [ "${lines[0]}" = "service-b" ] 29 | 30 | } 31 | 32 | @test "basic auth gives 200 with valid user and password, password encrypted" { 33 | # generated with: docker run -ti --rm alpine mkpasswd -m sha-512 passwd 34 | PASSWD='$6$n6KqSRo5Y.ifWXS/$H0y19JyooSMPYVqSTd2AEmLNo0PZnxTp5dx4W31vsWICZ3FYU5jMScJ64K8HgLXgFVyFYVq0EQ7XqgT5hCbg/1' 35 | etcdctl set /kontena/haproxy/lb/services/service-b/virtual_hosts www.foo.com 36 | etcdctl set /kontena/haproxy/lb/services/service-b/upstreams/server service-b:9292 37 | etcdctl set /kontena/haproxy/lb/services/service-b/basic_auth_secrets "user admin password $PASSWD" 38 | sleep 1 39 | 40 | run curl -s -H "Host: www.foo.com" http://admin:passwd@localhost:8180/ 41 | [ "${lines[0]}" = "service-b" ] 42 | 43 | } 44 | -------------------------------------------------------------------------------- /test/ciphers.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "common" 4 | 5 | @test "ciphers are set" { 6 | run sslscan localhost:8443 7 | assert_output_contains "ECDHE-RSA-AES128-GCM-SHA256" 1 8 | assert_output_contains "ECDHE-ECDSA-AES128-GCM-SHA256" 0 9 | } 10 | -------------------------------------------------------------------------------- /test/common.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | etcdctl() { 4 | docker run --rm --net=host --entrypoint=/usr/bin/etcdctl lbtesthelper "$@" 5 | } 6 | curl() { 7 | docker run --rm --net=host -v $BATS_TEST_DIRNAME:/test --entrypoint=/usr/bin/curl lbtesthelper "$@" 8 | } 9 | config() { 10 | docker exec kontenaloadbalancer_lb_1 cat /etc/haproxy/haproxy.cfg 11 | } 12 | sslscan() { 13 | docker run --rm --net=host nabz/docker-sslscan "$@" 14 | } 15 | 16 | # Some assert helpers, inspired by Dokku: https://github.com/dokku/dokku/blob/master/tests/unit/test_helper.bash 17 | flunk() { 18 | { if [[ "$#" -eq 0 ]]; then cat - 19 | else echo "$*" 20 | fi 21 | } 22 | return 1 23 | } 24 | 25 | assert_equal() { 26 | if [[ "$1" != "$2" ]]; then 27 | { echo "expected: $1" 28 | echo "actual: $2" 29 | } | flunk 30 | fi 31 | } 32 | 33 | assert_output_contains() { 34 | local input="$output"; local expected="$1"; local count="${2:-1}"; local found=0 35 | until [ "${input/$expected/}" = "$input" ]; do 36 | input="${input/$expected/}" 37 | let found+=1 38 | done 39 | assert_equal "$count" "$found" 40 | } 41 | -------------------------------------------------------------------------------- /test/cookie_stickyness_test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "common" 4 | 5 | 6 | setup() { 7 | etcdctl rm --recursive /kontena/haproxy/lb/services/service-a || true 8 | etcdctl rm --recursive /kontena/haproxy/lb/services/service-b || true 9 | etcdctl rm --recursive /kontena/haproxy/lb/services/service-c || true 10 | } 11 | 12 | 13 | @test "supports cookie stickyness" { 14 | etcdctl set /kontena/haproxy/lb/services/service-b/cookie "" 15 | etcdctl set /kontena/haproxy/lb/services/service-b/virtual_hosts www.foo.com 16 | etcdctl set /kontena/haproxy/lb/services/service-b/upstreams/server service-b:9292 17 | sleep 1 18 | run curl -c - -s -H "Host: www.foo.com" http://localhost:8180/ 19 | 20 | [ "${lines[0]}" = "service-b# Netscape HTTP Cookie File" ] 21 | # cookie in format: www.foo.com FALSE / FALSE 0 KONTENA_SERVERID server 22 | [ $(expr "${lines[3]}" : ".*KONTENA_SERVERID.*") -ne 0 ] 23 | [ $(expr "${lines[3]}" : ".*server.*") -ne 0 ] 24 | 25 | } 26 | 27 | @test "supports cookie stickyness with custom cookie config" { 28 | etcdctl set /kontena/haproxy/lb/services/service-b/cookie "cookie LB_COOKIE_TEST insert indirect nocache" 29 | etcdctl set /kontena/haproxy/lb/services/service-b/virtual_hosts www.foo.com 30 | etcdctl set /kontena/haproxy/lb/services/service-b/upstreams/server service-b:9292 31 | sleep 1 32 | run curl -c - -s -H "Host: www.foo.com" http://localhost:8180/ 33 | [ "${lines[0]}" = "service-b# Netscape HTTP Cookie File" ] 34 | # cookie in format: www.foo.com FALSE / FALSE 0 KONTENA_SERVERID server 35 | [ $(expr "${lines[3]}" : ".*LB_COOKIE_TEST.*") -ne 0 ] 36 | [ $(expr "${lines[3]}" : ".*server.*") -ne 0 ] 37 | 38 | } 39 | 40 | @test "supports cookie stickyness with custom cookie prefix" { 41 | etcdctl set /kontena/haproxy/lb/services/service-b/cookie "cookie JSESSIONID prefix nocache" 42 | etcdctl set /kontena/haproxy/lb/services/service-b/virtual_hosts www.foo.com 43 | etcdctl set /kontena/haproxy/lb/services/service-b/upstreams/server-1 service-b:9292 44 | sleep 1 45 | run curl -c - -s -H "Host: www.foo.com" http://localhost:8180/cookie 46 | [ "${lines[0]}" = "service-b" ] 47 | # cookie in format: www.foo.com FALSE / FALSE 0 KONTENA_SERVERID server 48 | [ $(expr "${lines[4]}" : ".*JSESSIONID.*") -ne 0 ] 49 | [ $(expr "${lines[4]}" : ".*server-1~12345.*") -ne 0 ] 50 | 51 | } 52 | -------------------------------------------------------------------------------- /test/custom_settings_test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "common" 4 | 5 | @test "supports custom common settings via env" { 6 | 7 | run config 8 | assert_output_contains "option dontlognull" 9 | 10 | } 11 | -------------------------------------------------------------------------------- /test/empty_upstreams_test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "common" 4 | 5 | 6 | setup() { 7 | etcdctl rm --recursive /kontena/haproxy/lb/services || true 8 | } 9 | 10 | @test "if no upstreams, service frontend retained in config" { 11 | etcdctl set /kontena/haproxy/lb/services/service-b/virtual_hosts www.foo.com 12 | etcdctl set /kontena/haproxy/lb/services/service-a/virtual_path / 13 | etcdctl set /kontena/haproxy/lb/services/service-a/upstreams/server service-a:9292 14 | 15 | sleep 1 16 | run curl -sL -w "%{http_code}" -H "Host: www.foo.com" http://localhost:8180/ -o /dev/null 17 | [ "${lines[0]}" = "503" ] 18 | 19 | run config 20 | assert_output_contains "use_backend service-b" 1 21 | } 22 | -------------------------------------------------------------------------------- /test/error_page_test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "common" 4 | 5 | 6 | setup() { 7 | etcdctl rm --recursive /kontena/haproxy/lb/services/service-a || true 8 | etcdctl rm --recursive /kontena/haproxy/lb/services/service-b || true 9 | etcdctl rm --recursive /kontena/haproxy/lb/services/service-c || true 10 | } 11 | 12 | @test "returns custom error page" { 13 | etcdctl set /kontena/haproxy/lb/services/service-b/virtual_hosts www.foo.com 14 | sleep 1 15 | run curl -s http://localhost:8180/invalid/ 16 | [ $(expr "$output" : ".*Kontena Load Balancer.*") -ne 0 ] 17 | } 18 | -------------------------------------------------------------------------------- /test/global_settings_test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "common" 4 | 5 | @test "supports custom global settings via env" { 6 | 7 | run config 8 | assert_output_contains "ssl-default-bind-options force-tlsv12" 9 | 10 | } 11 | -------------------------------------------------------------------------------- /test/health_checks_test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "common" 4 | 5 | 6 | setup() { 7 | etcdctl rm --recursive /kontena/haproxy/lb/services/service-a || true 8 | etcdctl rm --recursive /kontena/haproxy/lb/services/service-b || true 9 | etcdctl rm --recursive /kontena/haproxy/lb/services/service-c || true 10 | } 11 | 12 | 13 | @test "returns health check page if configured in env" { 14 | etcdctl set /kontena/haproxy/lb/services/service-a/virtual_hosts www.foo.com 15 | sleep 1 16 | run curl -s http://localhost:8180/health 17 | [ $(expr "$output" : ".*Everything seems to be 200 - OK.*") -ne 0 ] 18 | } 19 | 20 | @test "returns error if health not configured in env" { 21 | etcdctl set /kontena/haproxy/lb_no_health/services/service-a/virtual_hosts www.foo.com 22 | sleep 1 23 | run curl -s http://localhost:8181/health/ 24 | [ $(expr "$output" : ".*503 — Service Unavailable.*") -ne 0 ] 25 | } 26 | 27 | @test "supports health check uri setting for balanced service" { 28 | etcdctl set /kontena/haproxy/lb/services/service-a/virtual_path /a/ 29 | etcdctl set /kontena/haproxy/lb/services/service-a/health_check_uri /health 30 | etcdctl set /kontena/haproxy/lb/services/service-a/upstreams/server service-a:9292 31 | sleep 1 32 | run curl -k -s https://localhost:8443/a/ 33 | [ "${lines[0]}" = "service-a" ] 34 | 35 | run config 36 | assert_output_contains "option httpchk GET /health" 37 | 38 | } 39 | 40 | 41 | @test "supports health check port setting for balanced service" { 42 | etcdctl set /kontena/haproxy/lb/services/service-a/virtual_path /a/ 43 | etcdctl set /kontena/haproxy/lb/services/service-a/health_check_uri /health 44 | etcdctl set /kontena/haproxy/lb/services/service-a/health_check_port 9292 45 | etcdctl set /kontena/haproxy/lb/services/service-a/upstreams/server service-a:9292 46 | sleep 1 47 | run curl -k -s https://localhost:8443/a/ 48 | [ "${lines[0]}" = "service-a" ] 49 | 50 | run config 51 | assert_output_contains "option httpchk GET /health" 52 | assert_output_contains "server server service-a:9292 check port 9292" 53 | 54 | } 55 | -------------------------------------------------------------------------------- /test/redirect_test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "common" 4 | 5 | 6 | setup() { 7 | etcdctl rm --recursive /kontena/haproxy/lb/services/service-b || true 8 | } 9 | 10 | 11 | @test "redirects *.foo.com -> foo.com" { 12 | etcdctl set /kontena/haproxy/lb/services/service-b/virtual_hosts *.foo.com 13 | etcdctl set /kontena/haproxy/lb/services/service-b/upstreams/server service-b:9292 14 | 15 | etcdctl set /kontena/haproxy/lb/services/service-b/custom_settings " 16 | acl wildcard hdr_reg(host) ^\w*\.foo.com\w* 17 | redirect code 301 location foo.com%[capture.req.uri] if wildcard 18 | " 19 | sleep 1 20 | run curl -sL -w "%{http_code}" -H "Host: www.foo.com" http://localhost:8180/ -o /dev/null 21 | [ "${lines[0]}" = "301" ] 22 | 23 | run curl -sL -w "%{http_code}" -H "Host: bar.foo.com" http://localhost:8180/ -o /dev/null 24 | [ "${lines[0]}" = "301" ] 25 | 26 | 27 | run curl -ksL -w "%{http_code}" -H "Host: www.foo.com" https://localhost:8443/ -o /dev/null 28 | [ "${lines[0]}" = "301" ] 29 | 30 | run curl -sL -w "%{http_code}" -H "Host: foo.bar.foo.com" http://localhost:8180/ 31 | [ "${lines[0]}" = "service-b200" ] 32 | } 33 | -------------------------------------------------------------------------------- /test/server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gliderlabs/alpine:3.2 2 | MAINTAINER jari@kontena.io 3 | 4 | RUN apk update && apk --update add ruby ruby-dev ca-certificates \ 5 | libssl1.0 openssl libstdc++ tzdata 6 | 7 | ADD Gemfile /app/ 8 | ADD Gemfile.lock /app/ 9 | 10 | RUN apk --update add --virtual build-dependencies build-base openssl-dev && \ 11 | gem install bundler && \ 12 | cd /app ; bundle install --without development test && \ 13 | apk del build-dependencies 14 | 15 | ADD . /app 16 | RUN chown -R nobody:nogroup /app 17 | USER nobody 18 | 19 | ENV RACK_ENV production 20 | EXPOSE 9292 21 | 22 | WORKDIR /app 23 | 24 | CMD ["bundle", "exec", "rackup"] 25 | -------------------------------------------------------------------------------- /test/server/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "roda" 4 | gem "puma" 5 | -------------------------------------------------------------------------------- /test/server/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | cuba (3.4.0) 5 | rack 6 | rack (1.6.4) 7 | 8 | PLATFORMS 9 | ruby 10 | 11 | DEPENDENCIES 12 | cuba 13 | 14 | BUNDLED WITH 15 | 1.10.6 16 | -------------------------------------------------------------------------------- /test/server/config.ru: -------------------------------------------------------------------------------- 1 | require "roda" 2 | require "socket" 3 | require "logger" 4 | 5 | Logger.class_eval { alias :write :'<<' } 6 | 7 | class App < Roda 8 | 9 | 10 | logger = Logger.new(STDOUT) 11 | use Rack::CommonLogger, logger 12 | plugin :cookies 13 | 14 | route do |r| 15 | 16 | r.root do 17 | Socket.gethostname 18 | end 19 | 20 | r.on "path" do 21 | r.is do 22 | r.get do 23 | output = Socket.gethostname 24 | output << "\n" 25 | output << request.path 26 | end 27 | end 28 | end 29 | 30 | r.on "virtual_path" do 31 | r.is do 32 | r.get do 33 | output = Socket.gethostname 34 | output << "\n" 35 | output << request.path 36 | end 37 | end 38 | end 39 | 40 | r.on "cookie" do 41 | r.is do 42 | r.get do 43 | response.set_cookie('JSESSIONID', '12345') 44 | output = Socket.gethostname 45 | output << "\n" 46 | output << request.path 47 | end 48 | end 49 | end 50 | 51 | r.on "health" do 52 | r.is do 53 | r.get do 54 | output = Socket.gethostname 55 | output << "\n" 56 | output << request.path 57 | end 58 | end 59 | end 60 | end 61 | end 62 | 63 | run App.freeze.app 64 | -------------------------------------------------------------------------------- /test/ssl/localhost/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC+zCCAeOgAwIBAgIJAJXVkdCXj6aqMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV 3 | BAMMCWxvY2FsaG9zdDAeFw0xNzEwMjYwOTA1MjdaFw0yMDA3MjIwOTA1MjdaMBQx 4 | EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 5 | ggEBAKzwoA0yPoG+VqJr4F69Iaif0SumXfgt16zfaJJYKYqCOoq9qVjX89jA6CQ5 6 | q+zn7066ihK1pFHA2T+Be7rMRo76p1VG2YW1NQ1Dj6LENVqgJqqVaVJbe67pvTLz 7 | dzZRLbjjpRV8M83CiSYTm+p53qBAsvl5DWQBRIJwK45LIi5kz7UxqI+R+SPLQJqC 8 | 4+U8TDDJ1pEdZJ4qlb9eJuP8l6TXtLrSuaLzhTPg08JGigAag39Mg1F2nkNSCI6w 9 | R7qGw3EhWgHjhsNCS/jP/w+JKQbQyuDuO5bYCVI9zBGbMtpP6xeDKDhmDGTguvYN 10 | +opSq1Aija93kk6N9ueFxlY+y4UCAwEAAaNQME4wHQYDVR0OBBYEFMZSiX50Bls8 11 | j8j66O0FXr/ZWtYFMB8GA1UdIwQYMBaAFMZSiX50Bls8j8j66O0FXr/ZWtYFMAwG 12 | A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBABhw+3Pm0nLhwEAtmUscH0bx 13 | XenjMxP5F3GxIj8eJfIWO4fXS/LIXTcM0ghKbm6q0zdBVra/nLMvrjyxYJlOqn04 14 | WyvBLH2EXjvIl8Bl/JLvLlddJ4gCfu5zW4d7Bs2+MBPVvfzzfFHKlmua5GbNxPoM 15 | hbVhZfWWqvN493mBOLE1j/1Bgch4zZGwJefP1+YuI2QHTDk8XmtBkBWymRAD4hBa 16 | 8Yg+XLBHfcGeqpIiCRXnTlQuxabmZlDmwh/M+Cxiac676z76EN62+zcAgd1NpAO6 17 | BRkJQadwDC4A9ogEkBYXwG0X9/+ZB6CzG4Vx+vdSupkFMp9b0z2vcodfO39KONg= 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /test/ssl/localhost/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCs8KANMj6Bvlai 3 | a+BevSGon9Erpl34Ldes32iSWCmKgjqKvalY1/PYwOgkOavs5+9OuooStaRRwNk/ 4 | gXu6zEaO+qdVRtmFtTUNQ4+ixDVaoCaqlWlSW3uu6b0y83c2US2446UVfDPNwokm 5 | E5vqed6gQLL5eQ1kAUSCcCuOSyIuZM+1MaiPkfkjy0CaguPlPEwwydaRHWSeKpW/ 6 | Xibj/Jek17S60rmi84Uz4NPCRooAGoN/TINRdp5DUgiOsEe6hsNxIVoB44bDQkv4 7 | z/8PiSkG0Mrg7juW2AlSPcwRmzLaT+sXgyg4Zgxk4Lr2DfqKUqtQIo2vd5JOjfbn 8 | hcZWPsuFAgMBAAECggEAF2tikUbrlhBblRU8xjeglkBGSD34XcJ/gYajl6XewkYO 9 | fXlftItSF1lQVo+Ey8lA7A1w40W74eJWyTXUtqAxMe2ZuX+lt2iprYknq2gcvZAQ 10 | jGs9XwzKfA5lM8Aqta1anr4dPgKa1VNx1Tk0lRU548O/OO9+s7tENtHP5C4ii9uc 11 | EPTq8V0vm2VbZOW17Yfw6a7RlxEvHP3JorECUvQwgYUSwgjQ2LLBEsCVpWo9w3z6 12 | ffCbz9X6JJ8DeqKS524o1RMyifdGgynnB8zVYsLwnEPTp5mFDec5+jWLjTCO/m+e 13 | IiGuaeeUBSG+oLREDjhw+bkb3wY/MGZsC546tOsKRQKBgQDW4XhAA5ES5ZNG5yMC 14 | rvBF1qtHFXxd060kn7Ndz6oGkGuZ99G0+NXHuaSP3CqEuOYdg2NS2LX32EHhwnFF 15 | /uRR4UKHLKJL/Ai1jk8/L+7sQSezzzW/Mek5v4lxH9hflfR2zH99Qb5Zemld5I1f 16 | rBuEGvLUTfv1nGCD+5PPZWnwCwKBgQDOCJGiSV4gozTO+Uq9yIuWJ9JB5JpM6IVF 17 | CNxmBseFLNZcnKb/Bl/NTH87Y7uk4r6nQ0VMfwv+dwd+wa/y0eau92T+PXo2sspp 18 | vlwGPP5RrcsDpOl5jiT022/MI9ybgnjNWFE3bA4DuCNua49LRTxW8Q1Leiu740EU 19 | XcnrFMecrwKBgBX5bMCvHLDgBVWk4XGuzid2MoHMcrFtqjEqm78mM28EadyO+UUW 20 | hVYtZ+TGURrNhcrS2t9oBgPYe7RInCjaTiMJdDI6oEZA+esHKJd/oWFLsHG06Pwq 21 | cH1VVwrYhNoRjbRwaUE37e1clVXiv4pfIVk7IEYRy4hse3pDyfPVnSXNAoGBAKX4 22 | 7SiopbTxBId+9yCvPxM0/QGr4Ej4PvN/0dw2td+oYP62CykBv4coio4TJ4QKTL99 23 | R4P6DHVu+ZC5Ar4/LO/hx2+vopYRrVFF0egMlmrB7/r9jD8prMe7RfJTKVH05s+0 24 | x6g32YpReelnqEVgft0izizxO+3dgf2gGBrR4INtAoGAIC3t93txnUNlRu0He6mL 25 | alFBjeDBl80/bhM74Aqo0veAHHwNZw5xg4dSwe5ydv5Ei6fLl5A4mqInPfRJhEVe 26 | WpuPlhWSprWv6eunCzG1huWrda4/0aXiOc/n5mdHAZ6v7sBrKEWWSiZlZ6+jay5M 27 | adNVFeXMFuwC0t7u0nUTQss= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /test/ssl/test-1/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC9TCCAd2gAwIBAgIJAK94fUzfHt1pMA0GCSqGSIb3DQEBCwUAMBExDzANBgNV 3 | BAMMBnRlc3QtMTAeFw0xNzEwMjYxMDA5MDNaFw0yMDA3MjIxMDA5MDNaMBExDzAN 4 | BgNVBAMMBnRlc3QtMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMP9 5 | 6ym6ptYHFh0o2aoGqTUL+Di+BYNMTDA2CciHtMPL7/SqdtS8Dej0hguyiee37D07 6 | b+Lo9I4/wBYVtCIUrDqqQvYwkAsUbZKD+nXWalfpSGtt3iFT4nVCg833yK6b6/JN 7 | TqpMmruMjn9sadzEAxabHU9at/j9ZCLrQBdgrGRhGJcCgPSc3jTLAEz5gf45F3DO 8 | vD/4aZYCsjS2qFHvtzFBP8pVqRP9CiXvCw/tuwN7wA7wBeJ185JPax8FfSELaDbZ 9 | Yd63GEVVCmM87HS8faCCbffmaciPoHPl8TyM3+FaGSot3HT6VLnkVFeQR470nKxT 10 | 0PX/iSD++b05l8GGjKECAwEAAaNQME4wHQYDVR0OBBYEFLJn8DEv3K4ISyzh8Du5 11 | GnOeT6WIMB8GA1UdIwQYMBaAFLJn8DEv3K4ISyzh8Du5GnOeT6WIMAwGA1UdEwQF 12 | MAMBAf8wDQYJKoZIhvcNAQELBQADggEBAL/KI42JqasFCofB+ii4L6qodHohhaw1 13 | tso6YQTSwvwYDG/tmjUdIRB3W2Vi4eUUHjhH0/CJk/jZaTOpQrC/Kd89gcQr+UzM 14 | uXx2bDGMp6VszE9wFPBpczoPndmc9ExPijbtOD4Q5NRpOgxs55JoVLyOEijvSfEn 15 | dQetBiLeudiYkQT89APysveqkj2WojK6H3Obvns9bZ5HlnGcWP71ttY1/HyIMd1P 16 | 2d10zFA66rriEwlcsOTKuusJ0y2OUOA6Fzi3v1vInDvx21QzLX3NMUtqn3t5qtto 17 | UQCkJrk2TE6mrL92igBV/+ckB5cGvJ5L8viyvijezX3xjOzn9JGhFhA= 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /test/ssl/test-1/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDD/espuqbWBxYd 3 | KNmqBqk1C/g4vgWDTEwwNgnIh7TDy+/0qnbUvA3o9IYLsonnt+w9O2/i6PSOP8AW 4 | FbQiFKw6qkL2MJALFG2Sg/p11mpX6Uhrbd4hU+J1QoPN98ium+vyTU6qTJq7jI5/ 5 | bGncxAMWmx1PWrf4/WQi60AXYKxkYRiXAoD0nN40ywBM+YH+ORdwzrw/+GmWArI0 6 | tqhR77cxQT/KVakT/Qol7wsP7bsDe8AO8AXidfOST2sfBX0hC2g22WHetxhFVQpj 7 | POx0vH2ggm335mnIj6Bz5fE8jN/hWhkqLdx0+lS55FRXkEeO9JysU9D1/4kg/vm9 8 | OZfBhoyhAgMBAAECggEBAIm3y93npUHxes2EneZGhfGbdpFQnQkUvNiHsDozeYa3 9 | r+YpPhTgC5os8GAZ1aN4bszcDhPRA79M9onOOGRWSGt0plbd6umOMixpBr50qwcZ 10 | CmVKr3KVwiQJWBqLyX1AXPxG7EboSzYMXzkUkhKpvU3OMztGkM2qKAoNalzC9oAV 11 | KdxTp52bG2iekBNj1b839jl57eRmJsGcAcRXMtxblvak84LEfPhnXSUp0nhhTHD2 12 | Dr+jvxF+q8TbIyf+StKppi3KmHliSr1qqLKC3+9qsHRCw0REnqOTJ04QHjo3QjUU 13 | 3Rlpal/b9yFyxT5o9+doQKKU7EISRwERfn2POLOO+pECgYEA916+BKBF8jq4Yghu 14 | G+b/zEZpeVeRYbPnxCgqrqI83+tistMbJxI1cHch7VLl6hVngD+U4BOnXRUGwHmG 15 | UxH7mzurX7ULJfoTSpN+4yMsd9kuBvlzoqgzaxg6Cl1adRQJUcaf1Ki3DN7ToFMU 16 | WRQdFNaVrEvEA5VefPeAk+utMLUCgYEAytRRhDMafqpFzVj/LSvYDVuMQCHMqRpJ 17 | S71HyW+qunyhzlYSUqk9IHWLDIET0bIZhVa4yThvnv3nekJ+OGPsci1Yi3uOziJK 18 | Oedv+UMu83p/Lo7pgMlfjj8EjOtAd9d3Sn35YIH8hQDF2iyCFABaVFCFwMkVEE8A 19 | rZu8naT8m70CgYEAz5kbLxaynM7a3qrkfVYnZm/RJJxwzeYFo4FyEIznOaR5eEni 20 | h6+oWXIhbuIbQZAlBGRXtJXJ5zw2JmHWcPCuj2BMOk3dxUlR10xhOI3US+Bf2EqQ 21 | 2Pj/7eivDPO7bnYaPB7NE9Nji9GVGP+gHAHdRhewFKChJ8C7Q3US2xD2j+ECgYEA 22 | k/aZVOh23opWi3vuA0TlwrDTOoGtrHrpl2AIe3GDybFb1ItDqJufZQt6mW+cRrA3 23 | H+dovBn4i7LL54uUSozSk2RzIKXNQqEPJvin3d3d5W6qUwucWgANPlbIegiwKfy8 24 | IFKP1pBc56Xtr8AiUHcFblajjETkodYQN5XR3erbAL0CgYEA6KfaCQkIe3NbTN6H 25 | o1hmwFgF2p4gjM202GwR6A4f4uvgrmnd76YDTmZidFbGfFw5igB3Gfisjd9nthtZ 26 | +NUq8hr7lx+ZJ413LyF/9CQAQn1l5BuIpcDGDirm8od4nZvg53qN1Q8AqEzZFcMw 27 | IJepvjbc/iTWfYqrWuod1wcBU5A= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /test/ssl_config_tests.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "common" 4 | 5 | @test "configures haproxy to load certs when configured with SSL_CERTS" { 6 | run config 7 | assert_output_contains "bind *:443 ssl crt /etc/haproxy/certs/" 1 8 | } 9 | 10 | @test "loads certs from both SSL_CERTS" { 11 | run docker exec kontenaloadbalancer_lb_1 ls /etc/haproxy/certs 12 | assert_output_contains "cert1_gen.pem" 1 13 | assert_output_contains "SSL_CERT_test1.pem" 1 14 | } 15 | -------------------------------------------------------------------------------- /test/ssl_disabled_test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "common" 4 | 5 | @test "does not configure haproxy to load certs when not configured with SSL_CERTS" { 6 | run docker exec kontenaloadbalancer_lb_no_health_1 cat /etc/haproxy/haproxy.cfg 7 | assert_output_contains "bind *:443 ssl crt /etc/haproxy/certs/" 0 8 | } 9 | -------------------------------------------------------------------------------- /test/ssl_test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "common" 4 | 5 | setup() { 6 | etcdctl rm --recursive /kontena/haproxy/lb/services || true 7 | etcdctl set /kontena/haproxy/lb/services/service-a/virtual_path /a/ 8 | etcdctl set /kontena/haproxy/lb/services/service-a/upstreams/server service-a:9292 9 | 10 | sleep 1 11 | } 12 | 13 | @test "uses the certificate from SSL_CERTS" { 14 | run curl -s --cacert /test/ssl/localhost/cert.pem https://localhost:8443/a/ 15 | 16 | [ "$status" -eq 0 ] 17 | [ "$output" = "service-a" ] 18 | } 19 | 20 | @test "uses the certificate from SSL_CERT_test1" { 21 | run docker run --rm --link kontenaloadbalancer_lb_1:test-1 -v $BATS_TEST_DIRNAME:/test --entrypoint=/usr/bin/curl lbtesthelper \ 22 | -s --cacert /test/ssl/test-1/cert.pem https://test-1/a/ 23 | 24 | [ "$status" -eq 0 ] 25 | [ "$output" = "service-a" ] 26 | } 27 | -------------------------------------------------------------------------------- /test/virtual_host_test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "common" 4 | 5 | 6 | setup() { 7 | etcdctl rm --recursive /kontena/haproxy/lb/services/service-a || true 8 | etcdctl rm --recursive /kontena/haproxy/lb/services/service-b || true 9 | etcdctl rm --recursive /kontena/haproxy/lb/services/service-c || true 10 | } 11 | 12 | 13 | @test "supports virtual_hosts" { 14 | etcdctl set /kontena/haproxy/lb/services/service-b/virtual_hosts www.foo.com,api.foo.com 15 | etcdctl set /kontena/haproxy/lb/services/service-b/upstreams/server service-b:9292 16 | sleep 1 17 | run curl -s -H "Host: www.foo.com" http://localhost:8180/ 18 | [ "${lines[0]}" = "service-b" ] 19 | run curl -s -H "Host: api.foo.com" http://localhost:8180/ 20 | [ "${lines[0]}" = "service-b" ] 21 | } 22 | 23 | @test "supports wildcard virtual_hosts" { 24 | etcdctl set /kontena/haproxy/lb/services/service-b/virtual_hosts *.foo.com 25 | etcdctl set /kontena/haproxy/lb/services/service-b/upstreams/server service-b:9292 26 | etcdctl set /kontena/haproxy/lb/services/service-c/virtual_hosts www.bar.com 27 | etcdctl set /kontena/haproxy/lb/services/service-c/upstreams/server service-c:9292 28 | sleep 1 29 | run curl -s -H "Host: www.foo.com" http://localhost:8180/ 30 | [ "${lines[0]}" = "service-b" ] 31 | run curl -s -H "Host: api.foo.com" http://localhost:8180/ 32 | [ "${lines[0]}" = "service-b" ] 33 | run curl -s -H "Host: www.bar.com" http://localhost:8180/ 34 | [ "${lines[0]}" = "service-c" ] 35 | } 36 | 37 | @test "supports virtual_hosts + virtual_path" { 38 | etcdctl set /kontena/haproxy/lb/services/service-b/virtual_path /b 39 | etcdctl set /kontena/haproxy/lb/services/service-b/upstreams/server service-b:9292 40 | 41 | etcdctl set /kontena/haproxy/lb/services/service-c/virtual_hosts www.bar.com,api.bar.com 42 | etcdctl set /kontena/haproxy/lb/services/service-c/virtual_path /c 43 | etcdctl set /kontena/haproxy/lb/services/service-c/upstreams/server service-c:9292 44 | 45 | sleep 1 46 | run curl -s http://localhost:8180 47 | [ "$status" -eq 0 ] 48 | [ $(expr "$output" : ".*Service Unavailable.*") -ne 0 ] 49 | 50 | run curl -s http://localhost:8180/b 51 | [ "$status" -eq 0 ] 52 | [ "${lines[0]}" = "service-b" ] 53 | 54 | run curl -s -H "Host: www.bar.com" http://localhost:8180/c 55 | [ "$status" -eq 0 ] 56 | [ "${lines[0]}" = "service-c" ] 57 | 58 | run curl -s -H "Host: api.bar.com" http://localhost:8180/c 59 | [ "$status" -eq 0 ] 60 | [ "${lines[0]}" = "service-c" ] 61 | } 62 | 63 | @test "supports virtual_hosts + virtual_path + keep_virtual_path" { 64 | etcdctl set /kontena/haproxy/lb/services/service-c/virtual_hosts www.foo.com 65 | etcdctl set /kontena/haproxy/lb/services/service-a/virtual_path /virtual_path 66 | etcdctl set /kontena/haproxy/lb/services/service-a/keep_virtual_path "true" 67 | etcdctl set /kontena/haproxy/lb/services/service-a/upstreams/server service-a:9292 68 | 69 | etcdctl set /kontena/haproxy/lb/services/service-b/virtual_path /b 70 | etcdctl set /kontena/haproxy/lb/services/service-b/upstreams/server service-b:9292 71 | 72 | etcdctl set /kontena/haproxy/lb/services/service-c/virtual_hosts www.bar.com,api.bar.com 73 | etcdctl set /kontena/haproxy/lb/services/service-c/virtual_path /c 74 | etcdctl set /kontena/haproxy/lb/services/service-c/upstreams/server service-c:9292 75 | 76 | sleep 1 77 | run curl -s http://localhost:8180 78 | [ "$status" -eq 0 ] 79 | [ $(expr "$output" : ".*Service Unavailable.*") -ne 0 ] 80 | 81 | run curl -s -H "Host: www.foo.com" http://localhost:8180/virtual_path 82 | [ "$status" -eq 0 ] 83 | [ "${lines[0]}" = "service-a" ] 84 | [ "${lines[1]}" = "/virtual_path" ] 85 | 86 | run curl -s http://localhost:8180/b 87 | [ "$status" -eq 0 ] 88 | [ "${lines[0]}" = "service-b" ] 89 | [ "${lines[1]}" = "" ] 90 | 91 | run curl -s -H "Host: www.bar.com" http://localhost:8180/c 92 | [ "$status" -eq 0 ] 93 | [ "${lines[0]}" = "service-c" ] 94 | 95 | run curl -s -H "Host: api.bar.com" http://localhost:8180/c 96 | [ "$status" -eq 0 ] 97 | [ "${lines[0]}" = "service-c" ] 98 | } 99 | 100 | @test "handles empty upstreams" { 101 | etcdctl set /kontena/haproxy/lb/services/service-b/virtual_hosts www.bar.com 102 | 103 | etcdctl set /kontena/haproxy/lb/services/service-c/virtual_hosts api.bar.com 104 | etcdctl set /kontena/haproxy/lb/services/service-c/upstreams/server service-c:9292 105 | 106 | sleep 1 107 | run curl -s -H "Host: www.bar.com" http://localhost:8180 108 | [ "$status" -eq 0 ] 109 | [ $(expr "$output" : ".*Service Unavailable.*") -ne 0 ] 110 | 111 | run curl -s -H "Host: api.bar.com" http://localhost:8180/ 112 | [ "$status" -eq 0 ] 113 | [ "${lines[0]}" = "service-c" ] 114 | } 115 | 116 | @test "on duplicate virtual_hosts first one in alphabets wins" { 117 | etcdctl set /kontena/haproxy/lb/services/service-b/virtual_hosts www.bar.com 118 | etcdctl set /kontena/haproxy/lb/services/service-b/upstreams/server service-b:9292 119 | 120 | etcdctl set /kontena/haproxy/lb/services/service-c/virtual_hosts www.bar.com 121 | etcdctl set /kontena/haproxy/lb/services/service-c/upstreams/server service-c:9292 122 | 123 | sleep 1 124 | run curl -s http://localhost:8180 125 | [ "$status" -eq 0 ] 126 | [ $(expr "$output" : ".*Service Unavailable.*") -ne 0 ] 127 | 128 | run curl -s -H "Host: www.bar.com" http://localhost:8180/ 129 | [ "$status" -eq 0 ] 130 | [ "${lines[0]}" = "service-b" ] 131 | } 132 | 133 | @test "prioritizes first vhost+vpath, then vhost and finally vpath" { 134 | etcdctl set /kontena/haproxy/lb/services/service-a/virtual_path / 135 | etcdctl set /kontena/haproxy/lb/services/service-a/upstreams/server service-a:9292 136 | 137 | etcdctl set /kontena/haproxy/lb/services/service-b/virtual_hosts www.bar.com 138 | etcdctl set /kontena/haproxy/lb/services/service-b/upstreams/server service-b:9292 139 | 140 | etcdctl set /kontena/haproxy/lb/services/service-c/virtual_hosts api.bar.com 141 | etcdctl set /kontena/haproxy/lb/services/service-c/virtual_path /c 142 | etcdctl set /kontena/haproxy/lb/services/service-c/upstreams/server service-c:9292 143 | 144 | sleep 1 145 | run curl -s http://localhost:8180 146 | [ "$status" -eq 0 ] 147 | [ "${lines[0]}" = "service-a" ] 148 | 149 | run curl -s -H "Host: www.bar.com" http://localhost:8180/ 150 | [ "$status" -eq 0 ] 151 | [ "${lines[0]}" = "service-b" ] 152 | 153 | run curl -s -H "Host: api.bar.com" http://localhost:8180/c 154 | [ "$status" -eq 0 ] 155 | [ "${lines[0]}" = "service-c" ] 156 | } 157 | 158 | @test "works with domain:port host header" { 159 | etcdctl set /kontena/haproxy/lb/services/service-b/virtual_hosts www.bar.com 160 | etcdctl set /kontena/haproxy/lb/services/service-b/upstreams/server service-b:9292 161 | 162 | sleep 1 163 | 164 | run curl -s -H "Host: www.bar.com" http://localhost:8180/ 165 | [ "$status" -eq 0 ] 166 | [ "${lines[0]}" = "service-b" ] 167 | 168 | run curl -s -H "Host: www.bar.com:80" http://localhost:8180/ 169 | [ "$status" -eq 0 ] 170 | [ "${lines[0]}" = "service-b" ] 171 | } 172 | -------------------------------------------------------------------------------- /test/virtual_path_test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "common" 4 | 5 | 6 | setup() { 7 | etcdctl rm --recursive /kontena/haproxy/lb/services/service-a || true 8 | etcdctl rm --recursive /kontena/haproxy/lb/services/service-b || true 9 | etcdctl rm --recursive /kontena/haproxy/lb/services/service-c || true 10 | } 11 | 12 | 13 | @test "supports virtual_path" { 14 | etcdctl set /kontena/haproxy/lb/services/service-a/virtual_path /a/ 15 | etcdctl set /kontena/haproxy/lb/services/service-a/upstreams/server service-a:9292 16 | sleep 1 17 | run curl -s http://localhost:8180/a/ 18 | [ "${lines[0]}" = "service-a" ] 19 | run curl -s http://localhost:8180/a/virtual_path 20 | [ "${lines[0]}" = "service-a" ] 21 | [ "${lines[1]}" = "/virtual_path" ] 22 | } 23 | 24 | @test "supports virtual_path + keep_virtual_path" { 25 | etcdctl set /kontena/haproxy/lb/services/service-a/virtual_path /virtual_path 26 | etcdctl set /kontena/haproxy/lb/services/service-a/keep_virtual_path "true" 27 | etcdctl set /kontena/haproxy/lb/services/service-a/upstreams/server service-a:9292 28 | etcdctl set /kontena/haproxy/lb/services/service-b/virtual_path /b/ 29 | etcdctl set /kontena/haproxy/lb/services/service-b/upstreams/server service-b:9292 30 | sleep 1 31 | run curl -s http://localhost:8180/virtual_path 32 | [ "${lines[0]}" = "service-a" ] 33 | [ "${lines[1]}" = "/virtual_path" ] 34 | run curl -s http://localhost:8180/b/virtual_path 35 | [ "${lines[0]}" = "service-b" ] 36 | [ "${lines[1]}" = "/virtual_path" ] 37 | } 38 | 39 | @test "supports multiple virtual_paths" { 40 | etcdctl set /kontena/haproxy/lb/services/service-a/virtual_path "/a/,/b/" 41 | etcdctl set /kontena/haproxy/lb/services/service-a/upstreams/server service-a:9292 42 | sleep 1 43 | run curl -s http://localhost:8180/a/ 44 | [ "${lines[0]}" = "service-a" ] 45 | run curl -s http://localhost:8180/b/ 46 | [ "${lines[0]}" = "service-a" ] 47 | run curl -s http://localhost:8180/a/virtual_path 48 | [ "${lines[0]}" = "service-a" ] 49 | [ "${lines[1]}" = "/virtual_path" ] 50 | run curl -s http://localhost:8180/b/virtual_path 51 | [ "${lines[0]}" = "service-a" ] 52 | [ "${lines[1]}" = "/virtual_path" ] 53 | } 54 | --------------------------------------------------------------------------------