[mproxy](https://github.com/basecamp/parachute) is a custom minimal proxy designed specifically for Kamal. It has two big advantages over Traefik: 1. Imperative deployments - we tell it to switch from container A to container B, and it waits for container B to start then switches. No need to poll for health checks ourselves or mess around with forcing health checks to fail. 2. Support for multiple apps - as much as possible, configuration is supplied at runtime by the deploy command, allowing us to have multiple apps share an instance of mproxy without conflicting config.
152 lines
4.5 KiB
Ruby
152 lines
4.5 KiB
Ruby
require "net/http"
|
|
require "test_helper"
|
|
|
|
class IntegrationTest < ActiveSupport::TestCase
|
|
setup do
|
|
ENV["TEST_ID"] = SecureRandom.hex
|
|
docker_compose "up --build -d"
|
|
wait_for_healthy
|
|
setup_deployer
|
|
@app = "app"
|
|
end
|
|
|
|
teardown do
|
|
unless passed?
|
|
[ :deployer, :vm1, :vm2, :shared, :load_balancer, :registry ].each do |container|
|
|
puts
|
|
puts "Logs for #{container}:"
|
|
docker_compose :logs, container
|
|
end
|
|
end
|
|
docker_compose "down -t 1"
|
|
end
|
|
|
|
private
|
|
def docker_compose(*commands, capture: false, raise_on_error: true)
|
|
command = "TEST_ID=#{ENV["TEST_ID"]} docker compose #{commands.join(" ")}"
|
|
succeeded = false
|
|
if capture
|
|
result = stdouted { succeeded = system("cd test/integration && #{command}") }
|
|
else
|
|
succeeded = system("cd test/integration && #{command}")
|
|
end
|
|
|
|
raise "Command `#{command}` failed with error code `#{$?}`" if !succeeded && raise_on_error
|
|
result
|
|
end
|
|
|
|
def deployer_exec(*commands, workdir: nil, **options)
|
|
workdir ||= "/#{@app}"
|
|
docker_compose("exec --workdir #{workdir} deployer #{commands.join(" ")}", **options)
|
|
end
|
|
|
|
def kamal(*commands, **options)
|
|
deployer_exec(:kamal, *commands, **options)
|
|
end
|
|
|
|
def assert_app_is_down(response_code: "503")
|
|
response = app_response
|
|
debug_response_code(response, response_code)
|
|
assert_equal response_code, response.code
|
|
end
|
|
|
|
def assert_app_is_up(version: nil)
|
|
response = app_response
|
|
debug_response_code(response, "200")
|
|
assert_equal "200", response.code
|
|
assert_app_version(version, response) if version
|
|
end
|
|
|
|
def wait_for_app_to_be_up(timeout: 20, up_count: 3)
|
|
timeout_at = Time.now + timeout
|
|
up_times = 0
|
|
response = app_response
|
|
while up_times < up_count && timeout_at > Time.now
|
|
sleep 0.1
|
|
up_times += 1 if response.code == "200"
|
|
response = app_response
|
|
end
|
|
assert_equal up_times, up_count
|
|
end
|
|
|
|
def app_response
|
|
Net::HTTP.get_response(URI.parse("http://localhost:12345/version"))
|
|
end
|
|
|
|
def update_app_rev
|
|
deployer_exec "./update_app_rev.sh #{@app}", workdir: "/"
|
|
latest_app_version
|
|
end
|
|
|
|
def break_app
|
|
deployer_exec "./break_app.sh #{@app}", workdir: "/"
|
|
latest_app_version
|
|
end
|
|
|
|
def latest_app_version
|
|
deployer_exec("git rev-parse HEAD", capture: true)
|
|
end
|
|
|
|
def assert_app_version(version, response)
|
|
assert_equal version, response.body.strip
|
|
end
|
|
|
|
def assert_hooks_ran(*hooks)
|
|
hooks.each do |hook|
|
|
file = "/tmp/#{ENV["TEST_ID"]}/#{hook}"
|
|
assert_equal "removed '#{file}'", deployer_exec("rm -v #{file}", capture: true).strip
|
|
end
|
|
end
|
|
|
|
def assert_200(response)
|
|
code = response.code
|
|
if code != "200"
|
|
puts "Got response code #{code}, here are the proxy logs:"
|
|
kamal :proxy, :logs
|
|
puts "And here are the load balancer logs"
|
|
docker_compose :logs, :load_balancer
|
|
puts "Tried to get the response code again and got #{app_response.code}"
|
|
end
|
|
assert_equal "200", code
|
|
end
|
|
|
|
def wait_for_healthy(timeout: 30)
|
|
timeout_at = Time.now + timeout
|
|
while docker_compose("ps -a | tail -n +2 | grep -v '(healthy)' | wc -l", capture: true) != "0"
|
|
if timeout_at < Time.now
|
|
docker_compose("ps -a | tail -n +2 | grep -v '(healthy)'")
|
|
raise "Container not healthy after #{timeout} seconds" if timeout_at < Time.now
|
|
end
|
|
sleep 0.1
|
|
end
|
|
end
|
|
|
|
def setup_deployer
|
|
deployer_exec("./setup.sh", workdir: "/") unless $DEPLOYER_SETUP
|
|
$DEPLOYER_SETUP = true
|
|
end
|
|
|
|
def debug_response_code(app_response, expected_code)
|
|
code = app_response.code
|
|
if code != expected_code
|
|
puts "Got response code #{code}, here are the proxy logs:"
|
|
kamal :proxy, :logs, raise_on_error: false
|
|
puts "And here are the load balancer logs"
|
|
docker_compose :logs, :load_balancer, raise_on_error: false
|
|
puts "Tried to get the response code again and got #{app_response.code}"
|
|
end
|
|
end
|
|
|
|
def assert_container_running(host:, name:)
|
|
assert container_running?(host: host, name: name)
|
|
end
|
|
|
|
def assert_container_not_running(host:, name:)
|
|
assert_not container_running?(host: host, name: name)
|
|
end
|
|
|
|
def container_running?(host:, name:)
|
|
docker_compose("exec #{host} docker ps --filter=name=#{name} | tail -n+2", capture: true).tap { |x| p [ x, x.strip, x.strip.present? ] }.strip.present?
|
|
end
|
|
end
|