Merge branch 'basecamp:main' into buildpacks

This commit is contained in:
Nick Hammond
2024-09-22 08:47:08 -07:00
committed by GitHub
104 changed files with 2027 additions and 1682 deletions

View File

@@ -15,7 +15,7 @@ class CliAccessoryTest < CliTestCase
run_command("boot", "mysql").tap do |output|
assert_match /docker login.*on 1.1.1.3/, output
assert_match "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/env/accessories/app-mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output
assert_match "docker run --name app-mysql --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/apps/app/env/accessories/mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output
end
end
@@ -29,9 +29,12 @@ class CliAccessoryTest < CliTestCase
assert_match /docker login.*on 1.1.1.3/, output
assert_match /docker login.*on 1.1.1.1/, output
assert_match /docker login.*on 1.1.1.2/, output
assert_match "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/env/accessories/app-mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output
assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output
assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", output
assert_match /docker network create kamal.*on 1.1.1.1/, output
assert_match /docker network create kamal.*on 1.1.1.2/, output
assert_match /docker network create kamal.*on 1.1.1.3/, output
assert_match "docker run --name app-mysql --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/apps/app/env/accessories/mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output
assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/apps/app/env/accessories/redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output
assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/apps/app/env/accessories/redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", output
end
end
@@ -51,7 +54,7 @@ class CliAccessoryTest < CliTestCase
Kamal::Commands::Registry.any_instance.expects(:login)
Kamal::Cli::Accessory.any_instance.expects(:stop).with("mysql")
Kamal::Cli::Accessory.any_instance.expects(:remove_container).with("mysql")
Kamal::Cli::Accessory.any_instance.expects(:boot).with("mysql", login: false)
Kamal::Cli::Accessory.any_instance.expects(:boot).with("mysql", prepare: false)
run_command("reboot", "mysql")
end
@@ -60,10 +63,10 @@ class CliAccessoryTest < CliTestCase
Kamal::Commands::Registry.any_instance.expects(:login).times(3)
Kamal::Cli::Accessory.any_instance.expects(:stop).with("mysql")
Kamal::Cli::Accessory.any_instance.expects(:remove_container).with("mysql")
Kamal::Cli::Accessory.any_instance.expects(:boot).with("mysql", login: false)
Kamal::Cli::Accessory.any_instance.expects(:boot).with("mysql", prepare: false)
Kamal::Cli::Accessory.any_instance.expects(:stop).with("redis")
Kamal::Cli::Accessory.any_instance.expects(:remove_container).with("redis")
Kamal::Cli::Accessory.any_instance.expects(:boot).with("redis", login: false)
Kamal::Cli::Accessory.any_instance.expects(:boot).with("redis", prepare: false)
run_command("reboot", "all")
end
@@ -200,8 +203,8 @@ class CliAccessoryTest < CliTestCase
run_command("boot", "redis", "--hosts", "1.1.1.1").tap do |output|
assert_match /docker login.*on 1.1.1.1/, output
assert_no_match /docker login.*on 1.1.1.2/, output
assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output
assert_no_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", output
assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/apps/app/env/accessories/redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output
assert_no_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/apps/app/env/accessories/redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", output
end
end
@@ -212,11 +215,32 @@ class CliAccessoryTest < CliTestCase
run_command("boot", "redis", "--hosts", "1.1.1.1,1.1.1.3").tap do |output|
assert_match /docker login.*on 1.1.1.1/, output
assert_no_match /docker login.*on 1.1.1.3/, output
assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output
assert_no_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.3", output
assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/apps/app/env/accessories/redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output
assert_no_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/apps/app/env/accessories/redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.3", output
end
end
test "upgrade" do
run_command("upgrade", "-y", "all").tap do |output|
assert_match "Upgrading all accessories on 1.1.1.3,1.1.1.1,1.1.1.2...", output
assert_match "docker network create kamal on 1.1.1.3", output
assert_match "docker container stop app-mysql on 1.1.1.3", output
assert_match "docker run --name app-mysql --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST="%" --env-file .kamal/apps/app/env/accessories/mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output
assert_match "Upgraded all accessories on 1.1.1.3,1.1.1.1,1.1.1.2...", output
end
end
test "upgrade rolling" do
run_command("upgrade", "--rolling", "-y", "all").tap do |output|
assert_match "Upgrading all accessories on 1.1.1.3...", output
assert_match "docker network create kamal on 1.1.1.3", output
assert_match "docker container stop app-mysql on 1.1.1.3", output
assert_match "docker run --name app-mysql --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST="%" --env-file .kamal/apps/app/env/accessories/mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output
assert_match "Upgraded all accessories on 1.1.1.3", output
end
end
private
def run_command(*command)
stdouted { Kamal::Cli::Accessory.start([ *command, "-c", "test/fixtures/deploy_with_accessories.yml" ]) }

View File

@@ -5,7 +5,7 @@ class CliAppTest < CliTestCase
stub_running
run_command("boot").tap do |output|
assert_match "docker tag dhh/app:latest dhh/app:latest", output
assert_match /docker run --detach --restart unless-stopped --name app-web-latest --hostname 1.1.1.1-[0-9a-f]{12} /, output
assert_match /docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-[0-9a-f]{12} /, output
assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output
end
end
@@ -18,26 +18,18 @@ class CliAppTest < CliTestCase
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", raise_on_non_zero_exit: false)
.returns("12345678") # running version
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
.returns("running") # health check
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
.returns("123") # old version
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :inspect, "-f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}'", "app-web-123", "|", :awk, "'$2 == \"/tmp/kamal-cord\" {print $1}'", raise_on_non_zero_exit: false)
.returns("cordfile") # old version
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
.returns("unhealthy") # old version unhealthy
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet")
.returns("12345678") # running version
run_command("boot").tap do |output|
assert_match /Renaming container .* to .* as already deployed on 1.1.1.1/, output # Rename
assert_match /docker rename app-web-latest app-web-latest_replaced_[0-9a-f]{16}/, output
assert_match /docker run --detach --restart unless-stopped --name app-web-latest --hostname 1.1.1.1-[0-9a-f]{12} /, output
assert_match /docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-[0-9a-f]{12} /, output
assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output
end
ensure
@@ -45,7 +37,7 @@ class CliAppTest < CliTestCase
end
test "boot uses group strategy when specified" do
Kamal::Cli::App.any_instance.stubs(:on).with("1.1.1.1").times(3) # ensure locks dir, acquire & release lock
Kamal::Cli::App.any_instance.stubs(:on).with("1.1.1.1").times(2) # ensure locks dir, acquire & release lock
Kamal::Cli::App.any_instance.stubs(:on).with([ "1.1.1.1" ]) # tag container
# Strategy is used when booting the containers
@@ -70,25 +62,21 @@ class CliAppTest < CliTestCase
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", raise_on_non_zero_exit: false)
.returns("12345678") # running version
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
.returns("running") # health check
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
.returns("123").twice # old version
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :inspect, "-f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}'", "app-web-123", "|", :awk, "'$2 == \"/tmp/kamal-cord\" {print $1}'", raise_on_non_zero_exit: false)
.returns("") # old version
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet")
.returns("12345678") # running version
run_command("boot", config: :with_assets).tap do |output|
assert_match "docker tag dhh/app:latest dhh/app:latest", output
assert_match "/usr/bin/env mkdir -p .kamal/assets/volumes/app-web-latest ; cp -rnT .kamal/assets/extracted/app-web-latest .kamal/assets/volumes/app-web-latest ; cp -rnT .kamal/assets/extracted/app-web-latest .kamal/assets/volumes/app-web-123 || true ; cp -rnT .kamal/assets/extracted/app-web-123 .kamal/assets/volumes/app-web-latest || true", output
assert_match "/usr/bin/env mkdir -p .kamal/assets/extracted/app-web-latest && docker stop -t 1 app-web-assets 2> /dev/null || true && docker run --name app-web-assets --detach --rm --entrypoint sleep dhh/app:latest 1000000 && docker cp -L app-web-assets:/public/assets/. .kamal/assets/extracted/app-web-latest && docker stop -t 1 app-web-assets", output
assert_match /docker run --detach --restart unless-stopped --name app-web-latest --hostname 1.1.1.1-[0-9a-f]{12} /, output
assert_match "/usr/bin/env mkdir -p .kamal/apps/app/assets/volumes/web-latest ; cp -rnT .kamal/apps/app/assets/extracted/web-latest .kamal/apps/app/assets/volumes/web-latest ; cp -rnT .kamal/apps/app/assets/extracted/web-latest .kamal/apps/app/assets/volumes/web-123 || true ; cp -rnT .kamal/apps/app/assets/extracted/web-123 .kamal/apps/app/assets/volumes/web-latest || true", output
assert_match "/usr/bin/env mkdir -p .kamal/apps/app/assets/extracted/web-latest && docker stop -t 1 app-web-assets 2> /dev/null || true && docker run --name app-web-assets --detach --rm --entrypoint sleep dhh/app:latest 1000000 && docker cp -L app-web-assets:/public/assets/. .kamal/apps/app/assets/extracted/web-latest && docker stop -t 1 app-web-assets", output
assert_match /docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-[0-9a-f]{12} /, output
assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output
assert_match "/usr/bin/env find .kamal/assets/extracted -maxdepth 1 -name 'app-web-*' ! -name app-web-latest -exec rm -rf \"{}\" + ; find .kamal/assets/volumes -maxdepth 1 -name 'app-web-*' ! -name app-web-latest -exec rm -rf \"{}\" +", output
assert_match "/usr/bin/env find .kamal/apps/app/assets/extracted -maxdepth 1 -name 'web-*' ! -name web-latest -exec rm -rf \"{}\" + ; find .kamal/apps/app/assets/volumes -maxdepth 1 -name 'web-*' ! -name web-latest -exec rm -rf \"{}\" +", output
end
end
@@ -96,24 +84,20 @@ class CliAppTest < CliTestCase
Object.any_instance.stubs(:sleep)
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", raise_on_non_zero_exit: false)
.returns("12345678") # running version
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", raise_on_non_zero_exit: false)
.returns("12345678") # running version
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
.returns("running") # health check
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet")
.returns("12345678") # running version
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
.returns("123") # old version
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :inspect, "-f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}'", "app-web-123", "|", :awk, "'$2 == \"/tmp/kamal-cord\" {print $1}'", raise_on_non_zero_exit: false)
.returns("") # old version
run_command("boot", config: :with_env_tags).tap do |output|
assert_match "docker tag dhh/app:latest dhh/app:latest", output
assert_match %r{docker run --detach --restart unless-stopped --name app-web-latest --hostname 1.1.1.1-[0-9a-f]{12} -e KAMAL_CONTAINER_NAME="app-web-latest" -e KAMAL_VERSION="latest" --env TEST="root" --env EXPERIMENT="disabled" --env SITE="site1"}, output
assert_match %r{docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-[0-9a-f]{12} -e KAMAL_CONTAINER_NAME="app-web-latest" -e KAMAL_VERSION="latest" --env TEST="root" --env EXPERIMENT="disabled" --env SITE="site1"}, output
assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output
end
end
@@ -123,14 +107,6 @@ class CliAppTest < CliTestCase
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
.returns("running").at_least_once # web health check passing
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
.returns("unhealthy").at_least_once # web health check failing
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-workers-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
.returns("running").at_least_once # workers health check
@@ -140,7 +116,7 @@ class CliAppTest < CliTestCase
assert_match "Waiting for the first healthy web container before booting workers on 1.1.1.4...", output
assert_match "First web container is healthy, booting workers on 1.1.1.3", output
assert_match "First web container is healthy, booting workers on 1.1.1.4", output
end
end
end
test "boot with web barrier closed" do
@@ -150,9 +126,11 @@ class CliAppTest < CliTestCase
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
.returns("unhealthy").at_least_once # web health check failing
SSHKit::Backend::Abstract.any_instance.stubs(:execute).returns("")
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :stop, raise_on_non_zero_exit: false)
SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:docker, :exec, "kamal-proxy", "kamal-proxy", :deploy, "app-web", "--target", "\"123:80\"", "--deploy-timeout", "\"1s\"", "--drain-timeout", "\"30s\"", "--buffer-requests", "--buffer-responses", "--log-request-header", "\"Cache-Control\"", "--log-request-header", "\"Last-Modified\"", "--log-request-header", "\"User-Agent\"").raises(SSHKit::Command::Failed.new("Failed to deploy"))
stderred do
run_command("boot", config: :with_roles, host: nil, allow_execute_error: true).tap do |output|
@@ -160,17 +138,59 @@ class CliAppTest < CliTestCase
assert_match "Waiting for the first healthy web container before booting workers on 1.1.1.4...", output
assert_match "First web container is unhealthy, not booting workers on 1.1.1.3", output
assert_match "First web container is unhealthy, not booting workers on 1.1.1.4", output
assert_match "Running docker container ls --all --filter name=^app-web-latest$ --quiet | xargs docker stop on 1.1.1.1", output
assert_match "Running docker container ls --all --filter name=^app-web-latest$ --quiet | xargs docker stop on 1.1.1.2", output
end
end
ensure
Thread.report_on_exception = true
end
test "boot with worker errors" do
Thread.report_on_exception = false
Object.any_instance.stubs(:sleep)
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-workers-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
.returns("unhealthy").at_least_once # workers health check
run_command("boot", config: :with_roles, host: nil, allow_execute_error: true).tap do |output|
assert_match "Waiting for the first healthy web container before booting workers on 1.1.1.3...", output
assert_match "Waiting for the first healthy web container before booting workers on 1.1.1.4...", output
assert_match "First web container is healthy, booting workers on 1.1.1.3", output
assert_match "First web container is healthy, booting workers on 1.1.1.4", output
assert_match "ERROR Failed to boot workers on 1.1.1.3", output
assert_match "ERROR Failed to boot workers on 1.1.1.4", output
end
ensure
Thread.report_on_exception = true
end
test "boot with worker ready then not" do
Thread.report_on_exception = false
Object.any_instance.stubs(:sleep)
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-workers-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
.returns("running", "stopped").at_least_once # workers health check
run_command("boot", config: :with_roles, host: "1.1.1.3", allow_execute_error: true).tap do |output|
assert_match "ERROR Failed to boot workers on 1.1.1.3", output
end
ensure
Thread.report_on_exception = true
end
test "start" do
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("999") # old version
run_command("start").tap do |output|
assert_match "docker start app-web-999", output
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"999:80\" --deploy-timeout \"30s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\"", output
end
end
@@ -243,13 +263,13 @@ class CliAppTest < CliTestCase
test "exec" do
run_command("exec", "ruby -v").tap do |output|
assert_match "docker run --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v", output
assert_match "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v", output
end
end
test "exec separate arguments" do
run_command("exec", "ruby", " -v").tap do |output|
assert_match "docker run --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v", output
assert_match "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v", output
end
end
@@ -262,7 +282,7 @@ class CliAppTest < CliTestCase
test "exec interactive" do
SSHKit::Backend::Abstract.any_instance.expects(:exec)
.with("ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v'")
.with("ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v'")
run_command("exec", "-i", "ruby -v").tap do |output|
assert_match "Get most recent version available as an image...", output
assert_match "Launching interactive command with version latest via SSH from new container on 1.1.1.1...", output
@@ -295,11 +315,11 @@ class CliAppTest < CliTestCase
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
.with("ssh -t root@1.1.1.1 'sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1| xargs docker logs --timestamps --tail 10 2>&1'")
assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --tail 100 2>&1", run_command("logs")
assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --tail 100 2>&1", run_command("logs")
assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs 2>&1 | grep 'hey'", run_command("logs", "--grep", "hey")
assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'hey'", run_command("logs", "--grep", "hey")
assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs 2>&1 | grep 'hey' -C 2", run_command("logs", "--grep", "hey", "--grep-options", "-C 2")
assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'hey' -C 2", run_command("logs", "--grep", "hey", "--grep-options", "-C 2")
end
test "logs with follow" do
@@ -342,7 +362,7 @@ class CliAppTest < CliTestCase
hostname = "this-hostname-is-really-unacceptably-long-to-be-honest.example.com"
stdouted { Kamal::Cli::App.start([ "boot", "-c", "test/fixtures/deploy_with_uncommon_hostnames.yml", "--hosts", hostname ]) }.tap do |output|
assert_match /docker run --detach --restart unless-stopped --name app-web-latest --hostname this-hostname-is-really-unacceptably-long-to-be-hon-[0-9a-f]{12} /, output
assert_match /docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname this-hostname-is-really-unacceptably-long-to-be-hon-[0-9a-f]{12} /, output
end
end
@@ -352,7 +372,28 @@ class CliAppTest < CliTestCase
hostname = "this-hostname-with-random-part-is-too-long.example.com"
stdouted { Kamal::Cli::App.start([ "boot", "-c", "test/fixtures/deploy_with_uncommon_hostnames.yml", "--hosts", hostname ]) }.tap do |output|
assert_match /docker run --detach --restart unless-stopped --name app-web-latest --hostname this-hostname-with-random-part-is-too-long.example-[0-9a-f]{12} /, output
assert_match /docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname this-hostname-with-random-part-is-too-long.example-[0-9a-f]{12} /, output
end
end
test "boot proxy" do
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version
run_command("boot", config: :with_proxy).tap do |output|
assert_match /Renaming container .* to .* as already deployed on 1.1.1.1/, output # Rename
assert_match /docker rename app-web-latest app-web-latest_replaced_[0-9a-f]{16}/, output
assert_match /docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-[0-9a-f]{12} -e KAMAL_CONTAINER_NAME="app-web-latest" -e KAMAL_VERSION="latest" --env-file .kamal\/apps\/app\/env\/roles\/web.env --log-opt max-size="10m" --label service="app" --label role="web" --label destination dhh\/app:latest/, output
assert_match /docker exec kamal-proxy kamal-proxy deploy app-web --target "123:80"/, output
assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output
end
end
test "boot proxy with role specific config" do
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version
run_command("boot", config: :with_proxy_roles, host: nil).tap do |output|
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"123:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --target-timeout \"10s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\"", output
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web2 --target \"123:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --target-timeout \"15s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\"", output
end
end
@@ -369,13 +410,5 @@ class CliAppTest < CliTestCase
Object.any_instance.stubs(:sleep)
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
.returns("running") # health check
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
.returns("unhealthy") # health check
end
end

View File

@@ -49,7 +49,7 @@ class CliBuildTest < CliTestCase
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :submodule, :update, "--init")
SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".", env: {})
.with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".")
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:git, "-C", anything, :"rev-parse", :HEAD)
@@ -140,7 +140,7 @@ class CliBuildTest < CliTestCase
.returns("")
SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".", env: {})
.with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".")
run_command("push").tap do |output|
assert_match /WARN Missing compatible builder, so creating a new one first/, output

View File

@@ -29,39 +29,19 @@ class CliTestCase < ActiveSupport::TestCase
def stub_setup
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |*args| args == [ :mkdir, "-p", ".kamal" ] }
.with { |*args| args == [ :mkdir, "-p", ".kamal/apps/app" ] }
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |arg1, arg2, arg3| arg1 == :mkdir && arg2 == "-p" && arg3 == ".kamal/locks" }
.with { |arg1, arg2, arg3| arg1 == :mkdir && arg2 == "-p" && arg3 == ".kamal/lock-app" }
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |arg1, arg2| arg1 == :mkdir && arg2 == ".kamal/locks/app" }
.with { |arg1, arg2| arg1 == :mkdir && arg2 == ".kamal/lock-app" }
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |arg1, arg2| arg1 == :rm && arg2 == ".kamal/locks/app/details" }
.with { |arg1, arg2| arg1 == :rm && arg2 == ".kamal/lock-app/details" }
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with(:docker, :buildx, :inspect, "kamal-local-docker-container")
end
def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: false, secrets: false)
whoami = `whoami`.chomp
performer = Kamal::Git.email.presence || whoami
service = service_version.split("@").first
assert_match "Running the #{hook} hook...\n", output
expected = %r{Running\s/usr/bin/env\s\.kamal/hooks/#{hook}\sas\s#{whoami}@localhost\n\s
DEBUG\s\[[0-9a-f]*\]\sCommand:\s\(\sexport\s
KAMAL_RECORDED_AT=\"\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ\"\s
KAMAL_PERFORMER=\"#{performer}\"\s
KAMAL_VERSION=\"#{version}\"\s
KAMAL_SERVICE_VERSION=\"#{service_version}\"\s
KAMAL_SERVICE=\"#{service}\"\s
KAMAL_HOSTS=\"#{hosts}\"\s
KAMAL_COMMAND=\"#{command}\"\s
#{"KAMAL_SUBCOMMAND=\\\"#{subcommand}\\\"\\s" if subcommand}
#{"KAMAL_RUNTIME=\\\"\\d+\\\"\\s" if runtime}
#{"DB_PASSWORD=\"secret\"\\s" if secrets}
;\s/usr/bin/env\s\.kamal/hooks/#{hook} }x
assert_match expected, output
assert_match %r{usr/bin/env\s\.kamal/hooks/#{hook}}, output
end
def with_argv(*argv)

View File

@@ -3,7 +3,7 @@ require_relative "cli_test_case"
class CliLockTest < CliTestCase
test "status" do
run_command("status").tap do |output|
assert_match "Running /usr/bin/env stat .kamal/locks/app > /dev/null && cat .kamal/locks/app/details | base64 -d on 1.1.1.1", output
assert_match "Running /usr/bin/env stat .kamal/lock-app > /dev/null && cat .kamal/lock-app/details | base64 -d on 1.1.1.1", output
end
end

View File

@@ -24,7 +24,7 @@ class CliMainTest < CliTestCase
# deploy
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options.merge(skip_local: true))
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:pull", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:boot", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true))
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options)
@@ -35,7 +35,7 @@ class CliMainTest < CliTestCase
assert_match /Acquiring the deploy lock/, output
assert_match /Log into image registry/, output
assert_match /Pull app image/, output
assert_match /Ensure Traefik is running/, output
assert_match /Ensure kamal-proxy is running/, output
assert_match /Detect stale containers/, output
assert_match /Prune old containers and images/, output
assert_match /Releasing the deploy lock/, output
@@ -48,7 +48,7 @@ class CliMainTest < CliTestCase
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options.merge(skip_local: false))
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:deliver", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:boot", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true))
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options)
@@ -61,7 +61,7 @@ class CliMainTest < CliTestCase
assert_match /Log into image registry/, output
assert_match /Build and push app image/, output
assert_hook_ran "pre-deploy", output, **hook_variables, secrets: true
assert_match /Ensure Traefik is running/, output
assert_match /Ensure kamal-proxy is running/, output
assert_match /Detect stale containers/, output
assert_match /Prune old containers and images/, output
assert_hook_ran "post-deploy", output, **hook_variables, runtime: true, secrets: true
@@ -74,7 +74,7 @@ class CliMainTest < CliTestCase
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options.merge(skip_local: true))
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:pull", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:boot", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true))
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options)
@@ -83,7 +83,7 @@ class CliMainTest < CliTestCase
assert_match /Acquiring the deploy lock/, output
assert_match /Log into image registry/, output
assert_match /Pull app image/, output
assert_match /Ensure Traefik is running/, output
assert_match /Ensure kamal-proxy is running/, output
assert_match /Detect stale containers/, output
assert_match /Prune old containers and images/, output
assert_match /Releasing the deploy lock/, output
@@ -97,17 +97,14 @@ class CliMainTest < CliTestCase
Dir.stubs(:chdir)
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |*args| args == [ :mkdir, "-p", ".kamal" ] }
.with { |*args| args == [ :mkdir, "-p", ".kamal/apps/app" ] }
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |*args| args == [ :mkdir, "-p", ".kamal/locks" ] }
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |*arg| arg[0..1] == [ :mkdir, ".kamal/locks/app" ] }
.raises(RuntimeError, "mkdir: cannot create directory kamal/locks/app: File exists")
.with { |*arg| arg[0..1] == [ :mkdir, ".kamal/lock-app" ] }
.raises(RuntimeError, "mkdir: cannot create directory kamal/lock-app: File exists")
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_debug)
.with(:stat, ".kamal/locks/app", ">", "/dev/null", "&&", :cat, ".kamal/locks/app/details", "|", :base64, "-d")
.with(:stat, ".kamal/lock-app", ">", "/dev/null", "&&", :cat, ".kamal/lock-app/details", "|", :base64, "-d")
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:git, "-C", anything, :"rev-parse", :HEAD)
@@ -134,13 +131,10 @@ class CliMainTest < CliTestCase
Dir.stubs(:chdir)
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |*args| args == [ :mkdir, "-p", ".kamal" ] }
.with { |*args| args == [ :mkdir, "-p", ".kamal/apps/app" ] }
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |*args| args == [ :mkdir, "-p", ".kamal/locks" ] }
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |*arg| arg[0..1] == [ :mkdir, ".kamal/locks/app" ] }
.with { |*arg| arg[0..1] == [ :mkdir, ".kamal/lock-app" ] }
.raises(SocketError, "getaddrinfo: nodename nor servname provided, or not known")
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
@@ -180,7 +174,7 @@ class CliMainTest < CliTestCase
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options.merge(skip_local: false))
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:deliver", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:boot", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true))
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options)
@@ -190,27 +184,12 @@ class CliMainTest < CliTestCase
end
end
test "deploy without healthcheck if primary host doesn't have traefik" do
invoke_options = { "config_file" => "test/fixtures/deploy_workers_only.yml", "version" => "999", "skip_hooks" => false }
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:healthcheck:perform", [], invoke_options).never
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options.merge(skip_local: false))
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:deliver", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:boot", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true))
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options)
run_command("deploy", config_file: "deploy_workers_only")
end
test "deploy with missing secrets" do
invoke_options = { "config_file" => "test/fixtures/deploy_with_secrets.yml", "version" => "999", "skip_hooks" => false }
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options.merge(skip_local: false))
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:deliver", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:boot", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true))
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options)
@@ -273,18 +252,11 @@ class CliMainTest < CliTestCase
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=#{role} --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=#{role} --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-#{role}-}; done", raise_on_non_zero_exit: false)
.returns("version-to-rollback\n").at_least_once
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-#{role}-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
.returns("running").at_least_once # health check
end
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :inspect, "-f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}'", "app-web-version-to-rollback", "|", :awk, "'$2 == \"/tmp/kamal-cord\" {print $1}'", raise_on_non_zero_exit: false)
.returns("corddirectory").at_least_once # health check
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-version-to-rollback$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
.returns("unhealthy").at_least_once # health check
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-workers-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
.returns("running").at_least_once # health check
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
hook_variables = { version: 123, service_version: "app@123", hosts: "1.1.1.1,1.1.1.2,1.1.1.3,1.1.1.4", command: "rollback" }
@@ -301,17 +273,15 @@ class CliMainTest < CliTestCase
test "rollback without old version" do
Kamal::Cli::Main.any_instance.stubs(:container_available?).returns(true)
Kamal::Cli::Healthcheck::Poller.stubs(:sleep)
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", raise_on_non_zero_exit: false)
.returns("").at_least_once
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet")
.returns("123").at_least_once
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
.returns("").at_least_once
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
.returns("running").at_least_once # health check
run_command("rollback", "123").tap do |output|
assert_match "docker run --detach --restart unless-stopped --name app-web-123", output
@@ -320,7 +290,7 @@ class CliMainTest < CliTestCase
end
test "details" do
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:details")
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:details")
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:details")
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:details", [ "all" ])
@@ -434,13 +404,14 @@ class CliMainTest < CliTestCase
test "remove with confirmation" do
run_command("remove", "-y", config_file: "deploy_with_accessories").tap do |output|
assert_match /docker container stop traefik/, output
assert_match /docker container prune --force --filter label=org.opencontainers.image.title=Traefik/, output
assert_match /docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik/, output
assert_match /docker container stop kamal-proxy/, output
assert_match /docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy/, output
assert_match /docker image prune --all --force --filter label=org.opencontainers.image.title=kamal-proxy/, output
assert_match /docker ps --quiet --filter label=service=app | xargs docker stop/, output
assert_match /docker container prune --force --filter label=service=app/, output
assert_match /docker image prune --all --force --filter label=service=app/, output
assert_match "/usr/bin/env rm -r .kamal/apps/app", output
assert_match /docker container stop app-mysql/, output
assert_match /docker container prune --force --filter label=service=app-mysql/, output
@@ -480,7 +451,7 @@ class CliMainTest < CliTestCase
end
test "run an alias for details" do
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:details")
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:details")
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:details")
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:details", [ "all" ])
@@ -515,6 +486,34 @@ class CliMainTest < CliTestCase
end
end
test "upgrade" do
invoke_options = { "config_file" => "test/fixtures/deploy_with_accessories.yml", "skip_hooks" => false, "confirmed" => true, "rolling" => false }
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:upgrade", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:upgrade", [ "all" ], invoke_options)
run_command("upgrade", "-y", config_file: "deploy_with_accessories").tap do |output|
assert_match "Upgrading all hosts...", output
assert_match "Upgraded all hosts", output
end
end
test "upgrade rolling" do
invoke_options = { "config_file" => "test/fixtures/deploy_with_accessories.yml", "skip_hooks" => false, "confirmed" => true, "rolling" => false }
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:upgrade", [], invoke_options).times(4)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:upgrade", [ "all" ], invoke_options).times(3)
run_command("upgrade", "--rolling", "-y", config_file: "deploy_with_accessories").tap do |output|
assert_match "Upgrading 1.1.1.1...", output
assert_match "Upgraded 1.1.1.1", output
assert_match "Upgrading 1.1.1.2...", output
assert_match "Upgraded 1.1.1.2", output
assert_match "Upgrading 1.1.1.3...", output
assert_match "Upgraded 1.1.1.3", output
assert_match "Upgrading 1.1.1.4...", output
assert_match "Upgraded 1.1.1.4", output
end
end
private
def run_command(*command, config_file: "deploy_simple")
with_argv([ *command, "-c", "test/fixtures/#{config_file}.yml" ]) do

243
test/cli/proxy_test.rb Normal file
View File

@@ -0,0 +1,243 @@
require_relative "cli_test_case"
class CliProxyTest < CliTestCase
test "boot" do
run_command("boot").tap do |output|
assert_match "docker login", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}", output
end
end
test "boot old version" do
Thread.report_on_exception = false
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :inspect, "kamal-proxy", "--format '{{.Config.Image}}'", "|", :cut, "-d:", "-f2")
.returns("v0.0.1")
.at_least_once
exception = assert_raises do
run_command("boot").tap do |output|
assert_match "docker login", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}", output
end
end
assert_includes exception.message, "kamal-proxy version v0.0.1 is too old, please reboot to update to at least #{Kamal::Configuration::PROXY_MINIMUM_VERSION}"
ensure
Thread.report_on_exception = false
end
test "boot correct version" do
Thread.report_on_exception = false
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :inspect, "kamal-proxy", "--format '{{.Config.Image}}'", "|", :cut, "-d:", "-f2")
.returns(Kamal::Configuration::PROXY_MINIMUM_VERSION)
.at_least_once
run_command("boot").tap do |output|
assert_match "docker login", output
assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}", output
end
ensure
Thread.report_on_exception = false
end
test "reboot" do
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet")
.returns("abcdefabcdef")
.at_least_once
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with { |*args| args[0..1] == [ :sh, "-c" ] }
.returns("123")
.at_least_once
run_command("reboot", "-y").tap do |output|
assert_match "docker container stop kamal-proxy on 1.1.1.1", output
assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image} on 1.1.1.1", output
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\" on 1.1.1.1", output
assert_match "docker container stop kamal-proxy on 1.1.1.2", output
assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.2", output
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.2", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image} on 1.1.1.2", output
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\" on 1.1.1.2", output
end
end
test "reboot --rolling" do
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet")
.returns("abcdefabcdef")
.at_least_once
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with { |*args| args[0..1] == [ :sh, "-c" ] }
.returns("123")
.at_least_once
run_command("reboot", "--rolling", "-y").tap do |output|
assert_match "Running docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output
end
end
test "start" do
run_command("start").tap do |output|
assert_match "docker container start kamal-proxy", output
end
end
test "stop" do
run_command("stop").tap do |output|
assert_match "docker container stop kamal-proxy", output
end
end
test "restart" do
Kamal::Cli::Proxy.any_instance.expects(:stop)
Kamal::Cli::Proxy.any_instance.expects(:start)
run_command("restart")
end
test "details" do
run_command("details").tap do |output|
assert_match "docker ps --filter name=^kamal-proxy$", output
end
end
test "logs" do
SSHKit::Backend::Abstract.any_instance.stubs(:capture)
.with(:docker, :logs, "kamal-proxy", "--tail 100", "--timestamps", "2>&1")
.returns("Log entry")
SSHKit::Backend::Abstract.any_instance.stubs(:capture)
.with(:docker, :logs, "proxy", "--tail 100", "--timestamps", "2>&1")
.returns("Log entry")
run_command("logs").tap do |output|
assert_match "Proxy Host: 1.1.1.1", output
assert_match "Log entry", output
end
end
test "logs with follow" do
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
.with("ssh -t root@1.1.1.1 -p 22 'docker logs kamal-proxy --timestamps --tail 10 --follow 2>&1'")
assert_match "docker logs kamal-proxy --timestamps --tail 10 --follow", run_command("logs", "--follow")
end
test "remove" do
run_command("remove").tap do |output|
assert_match "/usr/bin/env ls .kamal/apps | wc -l", output
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy", output
assert_match "docker image prune --all --force --filter label=org.opencontainers.image.title=kamal-proxy", output
end
end
test "remove with other apps" do
Thread.report_on_exception = false
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info).with(:ls, ".kamal/apps", "|", :wc, "-l").returns("1\n").twice
run_command("remove").tap do |output|
assert_match "Not removing the proxy, as other apps are installed, ignore this check with kamal proxy remove --force", output
end
ensure
Thread.report_on_exception = true
end
test "force remove with other apps" do
Thread.report_on_exception = false
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info).with(:ls, ".kamal/apps", "|", :wc, "-l").returns("1\n").twice
run_command("remove").tap do |output|
assert_match "Not removing the proxy, as other apps are installed, ignore this check with kamal proxy remove --force", output
end
ensure
Thread.report_on_exception = true
end
test "remove_container" do
run_command("remove_container").tap do |output|
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy", output
end
end
test "remove_image" do
run_command("remove_image").tap do |output|
assert_match "docker image prune --all --force --filter label=org.opencontainers.image.title=kamal-proxy", output
end
end
test "upgrade" do
Object.any_instance.stubs(:sleep)
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("12345678")
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :inspect, "kamal-proxy", "--format '{{.Config.Image}}'", "|", :cut, "-d:", "-f2")
.returns(Kamal::Configuration::PROXY_MINIMUM_VERSION)
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-workers-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
.returns("running").at_least_once # workers health check
run_command("upgrade", "-y").tap do |output|
assert_match "Upgrading proxy on 1.1.1.1,1.1.1.2,1.1.1.3,1.1.1.4...", output
assert_match "docker login -u [REDACTED] -p [REDACTED]", output
assert_match "docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik", output
assert_match "docker container stop kamal-proxy", output
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy", output
assert_match "docker image prune --all --force --filter label=org.opencontainers.image.title=kamal-proxy", output
assert_match "/usr/bin/env mkdir -p .kamal", output
assert_match "docker network create kamal", output
assert_match "docker login -u [REDACTED] -p [REDACTED]", output
assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", output
assert_match "/usr/bin/env mkdir -p .kamal", output
assert_match %r{docker rename app-web-latest app-web-latest_replaced_.*}, output
assert_match "/usr/bin/env mkdir -p .kamal/apps/app/env/roles", output
assert_match %r{/usr/bin/env .* .kamal/apps/app/env/roles/web.env}, output
assert_match %r{docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-.* -e KAMAL_CONTAINER_NAME="app-web-latest" -e KAMAL_VERSION="latest" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size="10m" --label service="app" --label role="web" --label destination dhh/app:latest}, output
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"12345678:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\"", output
assert_match "docker container ls --all --filter name=^app-web-12345678$ --quiet | xargs docker stop", output
assert_match "docker tag dhh/app:latest dhh/app:latest", output
assert_match "/usr/bin/env mkdir -p .kamal", output
assert_match "docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +6 | while read container_id; do docker rm $container_id; done", output
assert_match "docker image prune --force --filter label=service=app", output
assert_match "Upgraded proxy on 1.1.1.1,1.1.1.2,1.1.1.3,1.1.1.4", output
end
end
test "upgrade rolling" do
Object.any_instance.stubs(:sleep)
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("12345678")
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :inspect, "kamal-proxy", "--format '{{.Config.Image}}'", "|", :cut, "-d:", "-f2")
.returns(Kamal::Configuration::PROXY_MINIMUM_VERSION)
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-workers-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
.returns("running").at_least_once # workers health check
run_command("upgrade", "--rolling", "-y",).tap do |output|
%w[1.1.1.1 1.1.1.2 1.1.1.3 1.1.1.4].each do |host|
assert_match "Upgrading proxy on #{host}...", output
assert_match "docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on #{host}", output
assert_match "Upgraded proxy on #{host}", output
end
end
end
private
def run_command(*command, fixture: :with_proxy)
stdouted { Kamal::Cli::Proxy.start([ *command, "-c", "test/fixtures/deploy_#{fixture}.yml" ]) }
end
end

View File

@@ -18,12 +18,10 @@ class CliPruneTest < CliTestCase
test "containers" do
run_command("containers").tap do |output|
assert_match /docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +6 | while read container_id; do docker rm $container_id; done on 1.1.1.\d/, output
assert_match /docker container prune --force --filter label=service=healthcheck-app on 1.1.1.\d/, output
end
run_command("containers", "--retain", "10").tap do |output|
assert_match /docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +11 | while read container_id; do docker rm $container_id; done on 1.1.1.\d/, output
assert_match /docker container prune --force --filter label=service=healthcheck-app on 1.1.1.\d/, output
end
assert_raises(RuntimeError, "retain must be at least 1") do

View File

@@ -1,110 +0,0 @@
require_relative "cli_test_case"
class CliTraefikTest < CliTestCase
test "boot" do
run_command("boot").tap do |output|
assert_match "docker login", output
assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Configuration::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output
end
end
test "reboot" do
Kamal::Commands::Registry.any_instance.expects(:login).twice
run_command("reboot", "-y").tap do |output|
assert_match "docker container stop traefik", output
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik", output
assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Configuration::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output
end
end
test "reboot --rolling" do
Object.any_instance.stubs(:sleep)
run_command("reboot", "--rolling", "-y").tap do |output|
assert_match "Running docker container prune --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output
end
end
test "start" do
run_command("start").tap do |output|
assert_match "docker container start traefik", output
end
end
test "stop" do
run_command("stop").tap do |output|
assert_match "docker container stop traefik", output
end
end
test "restart" do
Kamal::Cli::Traefik.any_instance.expects(:stop)
Kamal::Cli::Traefik.any_instance.expects(:start)
run_command("restart")
end
test "details" do
run_command("details").tap do |output|
assert_match "docker ps --filter name=^traefik$", output
end
end
test "logs" do
SSHKit::Backend::Abstract.any_instance.stubs(:capture)
.with(:docker, :logs, "traefik", " --tail 100", "--timestamps", "2>&1")
.returns("Log entry")
run_command("logs").tap do |output|
assert_match "Traefik Host: 1.1.1.1", output
assert_match "Log entry", output
end
end
test "logs with follow" do
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
.with("ssh -t root@1.1.1.1 -p 22 'docker logs traefik --timestamps --tail 10 --follow 2>&1'")
assert_match "docker logs traefik --timestamps --tail 10 --follow", run_command("logs", "--follow")
end
test "logs with follow and grep" do
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
.with("ssh -t root@1.1.1.1 -p 22 'docker logs traefik --timestamps --tail 10 --follow 2>&1 | grep \"hey\"'")
assert_match "docker logs traefik --timestamps --tail 10 --follow 2>&1 | grep \"hey\"", run_command("logs", "--follow", "--grep", "hey")
end
test "logs with follow, grep, and grep options" do
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
.with("ssh -t root@1.1.1.1 -p 22 'docker logs traefik --timestamps --tail 10 --follow 2>&1 | grep \"hey\" -C 2'")
assert_match "docker logs traefik --timestamps --tail 10 --follow 2>&1 | grep \"hey\" -C 2", run_command("logs", "--follow", "--grep", "hey", "--grep-options", "-C 2")
end
test "remove" do
Kamal::Cli::Traefik.any_instance.expects(:stop)
Kamal::Cli::Traefik.any_instance.expects(:remove_container)
Kamal::Cli::Traefik.any_instance.expects(:remove_image)
run_command("remove")
end
test "remove_container" do
run_command("remove_container").tap do |output|
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik", output
end
end
test "remove_image" do
run_command("remove_image").tap do |output|
assert_match "docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik", output
end
end
private
def run_command(*command)
stdouted { Kamal::Cli::Traefik.start([ *command, "-c", "test/fixtures/deploy_with_accessories.yml" ]) }
end
end

View File

@@ -136,18 +136,18 @@ class CommanderTest < ActiveSupport::TestCase
assert_equal [ "1.1.1.3", "1.1.1.4", "1.1.1.1", "1.1.1.2" ], @kamal.hosts
end
test "traefik hosts should observe filtered roles" do
configure_with(:deploy_with_multiple_traefik_roles)
test "proxy hosts should observe filtered roles" do
configure_with(:deploy_with_multiple_proxy_roles)
@kamal.specific_roles = [ "web_tokyo" ]
assert_equal [ "1.1.1.3", "1.1.1.4" ], @kamal.traefik_hosts
assert_equal [ "1.1.1.3", "1.1.1.4" ], @kamal.proxy_hosts
end
test "traefik hosts should observe filtered hosts" do
configure_with(:deploy_with_multiple_traefik_roles)
test "proxy hosts should observe filtered hosts" do
configure_with(:deploy_with_multiple_proxy_roles)
@kamal.specific_hosts = [ "1.1.1.2" ]
assert_equal [ "1.1.1.2" ], @kamal.traefik_hosts
assert_equal [ "1.1.1.2" ], @kamal.proxy_hosts
end
private

View File

@@ -51,15 +51,15 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
test "run" do
assert_equal \
"docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/env/accessories/app-mysql.env --label service=\"app-mysql\" private.registry/mysql:8.0",
"docker run --name app-mysql --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/apps/app/env/accessories/mysql.env --label service=\"app-mysql\" private.registry/mysql:8.0",
new_command(:mysql).run.join(" ")
assert_equal \
"docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env SOMETHING=\"else\" --env-file .kamal/env/accessories/app-redis.env --volume /var/lib/redis:/data --label service=\"app-redis\" --label cache=\"true\" redis:latest",
"docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env SOMETHING=\"else\" --env-file .kamal/apps/app/env/accessories/redis.env --volume /var/lib/redis:/data --label service=\"app-redis\" --label cache=\"true\" redis:latest",
new_command(:redis).run.join(" ")
assert_equal \
"docker run --name custom-busybox --detach --restart unless-stopped --log-opt max-size=\"10m\" --env-file .kamal/env/accessories/custom-busybox.env --label service=\"custom-busybox\" busybox:latest",
"docker run --name custom-busybox --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --env-file .kamal/apps/app/env/accessories/busybox.env --label service=\"custom-busybox\" busybox:latest",
new_command(:busybox).run.join(" ")
end
@@ -67,7 +67,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
assert_equal \
"docker run --name custom-busybox --detach --restart unless-stopped --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --env-file .kamal/env/accessories/custom-busybox.env --label service=\"custom-busybox\" busybox:latest",
"docker run --name custom-busybox --detach --restart unless-stopped --network kamal --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --env-file .kamal/apps/app/env/accessories/busybox.env --label service=\"custom-busybox\" busybox:latest",
new_command(:busybox).run.join(" ")
end
@@ -92,7 +92,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
test "execute in new container" do
assert_equal \
"docker run --rm --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/env/accessories/app-mysql.env private.registry/mysql:8.0 mysql -u root",
"docker run --rm --network kamal --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/apps/app/env/accessories/mysql.env private.registry/mysql:8.0 mysql -u root",
new_command(:mysql).execute_in_new_container("mysql", "-u", "root").join(" ")
end
@@ -104,7 +104,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
test "execute in new container over ssh" do
new_command(:mysql).stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) do
assert_match %r{docker run -it --rm --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/env/accessories/app-mysql.env private.registry/mysql:8.0 mysql -u root},
assert_match %r{docker run -it --rm --network kamal --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/apps/app/env/accessories/mysql.env private.registry/mysql:8.0 mysql -u root},
new_command(:mysql).execute_in_new_container_over_ssh("mysql", "-u", "root")
end
end
@@ -130,12 +130,20 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
assert_equal \
"docker logs app-mysql --since 5m --tail 100 --timestamps 2>&1 | grep 'thing' -C 2",
new_command(:mysql).logs(since: "5m", lines: 100, grep: "thing", grep_options: "-C 2").join(" ")
assert_equal \
"docker logs app-mysql --since 5m --tail 100 2>&1 | grep 'thing' -C 2",
new_command(:mysql).logs(timestamps: false, since: "5m", lines: 100, grep: "thing", grep_options: "-C 2").join(" ")
end
test "follow logs" do
assert_equal \
"ssh -t root@1.1.1.5 -p 22 'docker logs app-mysql --timestamps --tail 10 --follow 2>&1'",
new_command(:mysql).follow_logs
assert_equal \
"ssh -t root@1.1.1.5 -p 22 'docker logs app-mysql --tail 10 --follow 2>&1'",
new_command(:mysql).follow_logs(timestamps: false)
end
test "remove container" do

View File

@@ -3,9 +3,8 @@ require "test_helper"
class CommandsAppTest < ActiveSupport::TestCase
setup do
setup_test_secrets("secrets" => "RAILS_MASTER_KEY=456")
Kamal::Configuration.any_instance.stubs(:run_id).returns("12345678901234567890123456789012")
@config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], env: { "secret" => [ "RAILS_MASTER_KEY" ] }, builder: { "arch" => "amd64" } }
@config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: { "web" => [ "1.1.1.1" ], "workers" => [ "1.1.1.2" ] }, env: { "secret" => [ "RAILS_MASTER_KEY" ] }, builder: { "arch" => "amd64" } }
end
teardown do
@@ -14,13 +13,13 @@ class CommandsAppTest < ActiveSupport::TestCase
test "run" do
assert_equal \
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/up || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
"docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
new_command.run.join(" ")
end
test "run with hostname" do
assert_equal \
"docker run --detach --restart unless-stopped --name app-web-999 --hostname myhost -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/up || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
"docker run --detach --restart unless-stopped --name app-web-999 --network kamal --hostname myhost -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
new_command.run(hostname: "myhost").join(" ")
end
@@ -28,38 +27,14 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:volumes] = [ "/local/path:/container/path" ]
assert_equal \
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/up || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label destination --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
new_command.run.join(" ")
end
test "run with custom healthcheck path" do
@config[:healthcheck] = { "path" => "/healthz" }
assert_equal \
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/healthz || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
new_command.run.join(" ")
end
test "run with custom healthcheck command" do
@config[:healthcheck] = { "cmd" => "/bin/up" }
assert_equal \
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(/bin/up) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
new_command.run.join(" ")
end
test "run with role-specific healthcheck options" do
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "healthcheck" => { "cmd" => "/bin/healthy" } } }
assert_equal \
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(/bin/healthy) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
"docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
new_command.run.join(" ")
end
test "run with custom options" do
@config[:servers] = { "web" => [ "1.1.1.1" ], "jobs" => { "hosts" => [ "1.1.1.2" ], "cmd" => "bin/jobs", "options" => { "mount" => "somewhere", "cap-add" => true } } }
assert_equal \
"docker run --detach --restart unless-stopped --name app-jobs-999 -e KAMAL_CONTAINER_NAME=\"app-jobs-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-jobs.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"jobs\" --label destination --mount \"somewhere\" --cap-add dhh/app:999 bin/jobs",
"docker run --detach --restart unless-stopped --name app-jobs-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-jobs-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/apps/app/env/roles/jobs.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"jobs\" --label destination --mount \"somewhere\" --cap-add dhh/app:999 bin/jobs",
new_command(role: "jobs", host: "1.1.1.2").run.join(" ")
end
@@ -67,7 +42,7 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
assert_equal \
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/up || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label destination --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
"docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/apps/app/env/roles/web.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
new_command.run.join(" ")
end
@@ -76,7 +51,7 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "logging" => { "driver" => "local", "options" => { "max-size" => "100m" } } } }
assert_equal \
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/up || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label destination --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
"docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/apps/app/env/roles/web.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
new_command.run.join(" ")
end
@@ -85,7 +60,7 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } }
assert_equal \
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env ENV1=\"value1\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/up || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
"docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
new_command.run.join(" ")
end
@@ -108,11 +83,15 @@ class CommandsAppTest < ActiveSupport::TestCase
new_command.stop.join(" ")
end
test "stop with custom stop wait time" do
@config[:stop_wait_time] = 30
test "stop with custom drain timeout" do
@config[:drain_timeout] = 20
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop -t 30",
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop",
new_command.stop.join(" ")
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=workers --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=workers --filter status=running --filter status=restarting' | head -1 | xargs docker stop -t 20",
new_command(role: "workers").stop.join(" ")
end
test "stop with version" do
@@ -134,52 +113,65 @@ class CommandsAppTest < ActiveSupport::TestCase
new_command.info.join(" ")
end
test "deploy" do
assert_equal \
"docker exec kamal-proxy kamal-proxy deploy app-web --target \"172.1.0.2:80\" --deploy-timeout \"30s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\"",
new_command.deploy(target: "172.1.0.2").join(" ")
end
test "remove" do
assert_equal \
"docker exec kamal-proxy kamal-proxy remove app-web --target \"172.1.0.2:80\"",
new_command.remove(target: "172.1.0.2").join(" ")
end
test "logs" do
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs 2>&1",
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1",
new_command.logs.join(" ")
end
test "logs with since" do
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --since 5m 2>&1",
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1",
new_command.logs(since: "5m").join(" ")
end
test "logs with lines" do
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --tail 100 2>&1",
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --tail 100 2>&1",
new_command.logs(lines: "100").join(" ")
end
test "logs with since and lines" do
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --since 5m --tail 100 2>&1",
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m --tail 100 2>&1",
new_command.logs(since: "5m", lines: "100").join(" ")
end
test "logs with grep" do
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs 2>&1 | grep 'my-id'",
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'my-id'",
new_command.logs(grep: "my-id").join(" ")
end
test "logs with grep and grep options" do
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs 2>&1 | grep 'my-id' -C 2",
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'my-id' -C 2",
new_command.logs(grep: "my-id", grep_options: "-C 2").join(" ")
end
test "logs with since, grep and grep options" do
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --since 5m 2>&1 | grep 'my-id' -C 2",
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1 | grep 'my-id' -C 2",
new_command.logs(since: "5m", grep: "my-id", grep_options: "-C 2").join(" ")
end
test "logs with since and grep" do
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --since 5m 2>&1 | grep 'my-id'",
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1 | grep 'my-id'",
new_command.logs(since: "5m", grep: "my-id").join(" ")
end
@@ -199,18 +191,22 @@ class CommandsAppTest < ActiveSupport::TestCase
assert_equal \
"ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --tail 123 --follow 2>&1 | grep \"Completed\"'",
new_command.follow_logs(host: "app-1", lines: 123, grep: "Completed")
assert_equal \
"ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --tail 123 --follow 2>&1 | grep \"Completed\"'",
new_command.follow_logs(host: "app-1", timestamps: false, lines: 123, grep: "Completed")
end
test "execute in new container" do
assert_equal \
"docker run --rm --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails db:setup",
"docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails db:setup",
new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ")
end
test "execute in new container with env" do
assert_equal \
"docker run --rm --env-file .kamal/env/roles/app-web.env --env foo=\"bar\" dhh/app:999 bin/rails db:setup",
"docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --env foo=\"bar\" dhh/app:999 bin/rails db:setup",
new_command.execute_in_new_container("bin/rails", "db:setup", env: { "foo" => "bar" }).join(" ")
end
@@ -219,14 +215,14 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } }
assert_equal \
"docker run --rm --env ENV1=\"value1\" --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails db:setup",
"docker run --rm --network kamal --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails db:setup",
new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ")
end
test "execute in new container with custom options" do
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } }
assert_equal \
"docker run --rm --env-file .kamal/env/roles/app-web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails db:setup",
"docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails db:setup",
new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ")
end
@@ -243,7 +239,7 @@ class CommandsAppTest < ActiveSupport::TestCase
end
test "execute in new container over ssh" do
assert_match %r{docker run -it --rm --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails c},
assert_match %r{docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails c},
new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
end
@@ -251,13 +247,13 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:servers] = [ { "1.1.1.1" => "tag1" } ]
@config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } }
assert_equal "ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --env ENV1=\"value1\" --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails c'",
assert_equal "ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --network kamal --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails c'",
new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
end
test "execute in new container with custom options over ssh" do
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } }
assert_match %r{docker run -it --rm --env-file .kamal/env/roles/app-web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c},
assert_match %r{docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c},
new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
end
@@ -412,48 +408,34 @@ class CommandsAppTest < ActiveSupport::TestCase
new_command.tag_latest_image.join(" ")
end
test "cord" do
assert_equal "docker inspect -f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}' app-web-123 | awk '$2 == \"/tmp/kamal-cord\" {print $1}'", new_command.cord(version: 123).join(" ")
end
test "tie cord" do
assert_equal "mkdir -p . ; touch cordfile", new_command.tie_cord("cordfile").join(" ")
assert_equal "mkdir -p corddir ; touch corddir/cordfile", new_command.tie_cord("corddir/cordfile").join(" ")
assert_equal "mkdir -p /corddir ; touch /corddir/cordfile", new_command.tie_cord("/corddir/cordfile").join(" ")
end
test "cut cord" do
assert_equal "rm -r corddir", new_command.cut_cord("corddir").join(" ")
end
test "extract assets" do
assert_equal [
:mkdir, "-p", ".kamal/assets/extracted/app-web-999", "&&",
:mkdir, "-p", ".kamal/apps/app/assets/extracted/web-999", "&&",
:docker, :stop, "-t 1", "app-web-assets", "2> /dev/null", "|| true", "&&",
:docker, :run, "--name", "app-web-assets", "--detach", "--rm", "--entrypoint", "sleep", "dhh/app:999", "1000000", "&&",
:docker, :cp, "-L", "app-web-assets:/public/assets/.", ".kamal/assets/extracted/app-web-999", "&&",
:docker, :cp, "-L", "app-web-assets:/public/assets/.", ".kamal/apps/app/assets/extracted/web-999", "&&",
:docker, :stop, "-t 1", "app-web-assets"
], new_command(asset_path: "/public/assets").extract_assets
end
test "sync asset volumes" do
assert_equal [
:mkdir, "-p", ".kamal/assets/volumes/app-web-999", ";",
:cp, "-rnT", ".kamal/assets/extracted/app-web-999", ".kamal/assets/volumes/app-web-999"
:mkdir, "-p", ".kamal/apps/app/assets/volumes/web-999", ";",
:cp, "-rnT", ".kamal/apps/app/assets/extracted/web-999", ".kamal/apps/app/assets/volumes/web-999"
], new_command(asset_path: "/public/assets").sync_asset_volumes
assert_equal [
:mkdir, "-p", ".kamal/assets/volumes/app-web-999", ";",
:cp, "-rnT", ".kamal/assets/extracted/app-web-999", ".kamal/assets/volumes/app-web-999", ";",
:cp, "-rnT", ".kamal/assets/extracted/app-web-999", ".kamal/assets/volumes/app-web-998", "|| true", ";",
:cp, "-rnT", ".kamal/assets/extracted/app-web-998", ".kamal/assets/volumes/app-web-999", "|| true"
:mkdir, "-p", ".kamal/apps/app/assets/volumes/web-999", ";",
:cp, "-rnT", ".kamal/apps/app/assets/extracted/web-999", ".kamal/apps/app/assets/volumes/web-999", ";",
:cp, "-rnT", ".kamal/apps/app/assets/extracted/web-999", ".kamal/apps/app/assets/volumes/web-998", "|| true", ";",
:cp, "-rnT", ".kamal/apps/app/assets/extracted/web-998", ".kamal/apps/app/assets/volumes/web-999", "|| true"
], new_command(asset_path: "/public/assets").sync_asset_volumes(old_version: 998)
end
test "clean up assets" do
assert_equal [
:find, ".kamal/assets/extracted", "-maxdepth 1", "-name", "'app-web-*'", "!", "-name", "app-web-999", "-exec rm -rf \"{}\" +", ";",
:find, ".kamal/assets/volumes", "-maxdepth 1", "-name", "'app-web-*'", "!", "-name", "app-web-999", "-exec rm -rf \"{}\" +"
:find, ".kamal/apps/app/assets/extracted", "-maxdepth 1", "-name", "'web-*'", "!", "-name", "web-999", "-exec rm -rf \"{}\" +", ";",
:find, ".kamal/apps/app/assets/volumes", "-maxdepth 1", "-name", "'web-*'", "!", "-name", "web-999", "-exec rm -rf \"{}\" +"
], new_command(asset_path: "/public/assets").clean_up_assets
end

View File

@@ -8,7 +8,7 @@ class CommandsHookTest < ActiveSupport::TestCase
@config = {
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ],
builder: { "arch" => "amd64" }, traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
builder: { "arch" => "amd64" }
}
@performer = Kamal::Git.email.presence || `whoami`.chomp
@@ -16,41 +16,34 @@ class CommandsHookTest < ActiveSupport::TestCase
end
test "run" do
assert_equal [
".kamal/hooks/foo",
{ env: {
"KAMAL_RECORDED_AT" => @recorded_at,
"KAMAL_PERFORMER" => @performer,
"KAMAL_VERSION" => "123",
"KAMAL_SERVICE_VERSION" => "app@123",
"KAMAL_SERVICE" => "app" } }
], new_command.run("foo")
assert_equal [ ".kamal/hooks/foo" ], new_command.run("foo")
end
test "env" do
assert_equal ({
"KAMAL_RECORDED_AT" => @recorded_at,
"KAMAL_PERFORMER" => @performer,
"KAMAL_VERSION" => "123",
"KAMAL_SERVICE_VERSION" => "app@123",
"KAMAL_SERVICE" => "app"
}), new_command.env
end
test "run with custom hooks_path" do
assert_equal [
"custom/hooks/path/foo",
{ env: {
"KAMAL_RECORDED_AT" => @recorded_at,
"KAMAL_PERFORMER" => @performer,
"KAMAL_VERSION" => "123",
"KAMAL_SERVICE_VERSION" => "app@123",
"KAMAL_SERVICE" => "app" } }
], new_command(hooks_path: "custom/hooks/path").run("foo")
assert_equal [ "custom/hooks/path/foo" ], new_command(hooks_path: "custom/hooks/path").run("foo")
end
test "hook with secrets" do
test "env with secrets" do
with_test_secrets("secrets" => "DB_PASSWORD=secret") do
assert_equal [
".kamal/hooks/foo",
{ env: {
assert_equal (
{
"KAMAL_RECORDED_AT" => @recorded_at,
"KAMAL_PERFORMER" => @performer,
"KAMAL_VERSION" => "123",
"KAMAL_SERVICE_VERSION" => "app@123",
"KAMAL_SERVICE" => "app",
"DB_PASSWORD" => "secret" } }
], new_command(env: { "secret" => [ "DB_PASSWORD" ] }).run("foo", secrets: true)
"DB_PASSWORD" => "secret" }
), new_command.env(secrets: true)
end
end

View File

@@ -4,25 +4,25 @@ class CommandsLockTest < ActiveSupport::TestCase
setup do
@config = {
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ],
builder: { "arch" => "amd64" }, traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
builder: { "arch" => "amd64" }
}
end
test "status" do
assert_equal \
"stat .kamal/locks/app-production > /dev/null && cat .kamal/locks/app-production/details | base64 -d",
"stat .kamal/lock-app-production > /dev/null && cat .kamal/lock-app-production/details | base64 -d",
new_command.status.join(" ")
end
test "acquire" do
assert_match \
%r{mkdir \.kamal/locks/app-production && echo ".*" > \.kamal/locks/app-production/details}m,
%r{mkdir \.kamal/lock-app-production && echo ".*" > \.kamal/lock-app-production/details}m,
new_command.acquire("Hello", "123").join(" ")
end
test "release" do
assert_match \
"rm .kamal/locks/app-production/details && rm -r .kamal/locks/app-production",
"rm .kamal/lock-app-production/details && rm -r .kamal/lock-app-production",
new_command.release.join(" ")
end

126
test/commands/proxy_test.rb Normal file
View File

@@ -0,0 +1,126 @@
require "test_helper"
class CommandsProxyTest < ActiveSupport::TestCase
setup do
@config = {
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], builder: { "arch" => "amd64" }
}
ENV["EXAMPLE_API_KEY"] = "456"
end
teardown do
ENV.delete("EXAMPLE_API_KEY")
end
test "run" do
assert_equal \
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}",
new_command.run.join(" ")
end
test "run with ports configured" do
assert_equal \
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}",
new_command.run.join(" ")
end
test "run without configuration" do
@config.delete(:proxy)
assert_equal \
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}",
new_command.run.join(" ")
end
test "run with logging config" do
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
assert_equal \
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}",
new_command.run.join(" ")
end
test "proxy start" do
assert_equal \
"docker container start kamal-proxy",
new_command.start.join(" ")
end
test "proxy stop" do
assert_equal \
"docker container stop kamal-proxy",
new_command.stop.join(" ")
end
test "proxy info" do
assert_equal \
"docker ps --filter name=^kamal-proxy$",
new_command.info.join(" ")
end
test "proxy logs" do
assert_equal \
"docker logs kamal-proxy --timestamps 2>&1",
new_command.logs.join(" ")
end
test "proxy logs since 2h" do
assert_equal \
"docker logs kamal-proxy --since 2h --timestamps 2>&1",
new_command.logs(since: "2h").join(" ")
end
test "proxy logs last 10 lines" do
assert_equal \
"docker logs kamal-proxy --tail 10 --timestamps 2>&1",
new_command.logs(lines: 10).join(" ")
end
test "proxy logs without timestamps" do
assert_equal \
"docker logs kamal-proxy 2>&1",
new_command.logs(timestamps: false).join(" ")
end
test "proxy logs with grep hello!" do
assert_equal \
"docker logs kamal-proxy --timestamps 2>&1 | grep 'hello!'",
new_command.logs(grep: "hello!").join(" ")
end
test "proxy remove container" do
assert_equal \
"docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy",
new_command.remove_container.join(" ")
end
test "proxy remove image" do
assert_equal \
"docker image prune --all --force --filter label=org.opencontainers.image.title=kamal-proxy",
new_command.remove_image.join(" ")
end
test "proxy follow logs" do
assert_equal \
"ssh -t root@1.1.1.1 -p 22 'docker logs kamal-proxy --timestamps --tail 10 --follow 2>&1'",
new_command.follow_logs(host: @config[:servers].first)
end
test "proxy follow logs with grep hello!" do
assert_equal \
"ssh -t root@1.1.1.1 -p 22 'docker logs kamal-proxy --timestamps --tail 10 --follow 2>&1 | grep \"hello!\"'",
new_command.follow_logs(host: @config[:servers].first, grep: "hello!")
end
test "version" do
assert_equal \
"docker inspect kamal-proxy --format '{{.Config.Image}}' | cut -d: -f2",
new_command.version.join(" ")
end
private
def new_command
Kamal::Commands::Proxy.new(Kamal::Configuration.new(@config, version: "123"))
end
end

View File

@@ -4,7 +4,7 @@ class CommandsPruneTest < ActiveSupport::TestCase
setup do
@config = {
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ],
builder: { "arch" => "amd64" }, traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
builder: { "arch" => "amd64" }
}
end
@@ -30,12 +30,6 @@ class CommandsPruneTest < ActiveSupport::TestCase
new_command.app_containers(retain: 3).join(" ")
end
test "healthcheck containers" do
assert_equal \
"docker container prune --force --filter label=service=healthcheck-app",
new_command.healthcheck_containers.join(" ")
end
private
def new_command
Kamal::Commands::Prune.new(Kamal::Configuration.new(@config, version: "123"))

View File

@@ -4,7 +4,7 @@ class CommandsServerTest < ActiveSupport::TestCase
setup do
@config = {
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ],
builder: { "arch" => "amd64" }, traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
builder: { "arch" => "amd64" }
}
end
@@ -12,10 +12,6 @@ class CommandsServerTest < ActiveSupport::TestCase
assert_equal "mkdir -p .kamal", new_command.ensure_run_directory.join(" ")
end
test "ensure non default run directory" do
assert_equal "mkdir -p /var/run/kamal", new_command(run_directory: "/var/run/kamal").ensure_run_directory.join(" ")
end
private
def new_command(extra_config = {})
Kamal::Commands::Server.new(Kamal::Configuration.new(@config.merge(extra_config)))

View File

@@ -1,195 +0,0 @@
require "test_helper"
class CommandsTraefikTest < ActiveSupport::TestCase
setup do
@image = "traefik:test"
@config = {
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], builder: { "arch" => "amd64" },
traefik: { "image" => @image, "args" => { "accesslog.format" => "json", "api.insecure" => true, "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
}
setup_test_secrets("secrets" => "EXAMPLE_API_KEY=456")
end
teardown do
teardown_test_secrets
end
test "run" do
assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ")
@config[:traefik]["host_port"] = "8080"
assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 8080:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ")
@config[:traefik]["publish"] = false
assert_equal \
"docker run --name traefik --detach --restart unless-stopped --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ")
end
test "run with ports configured" do
assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ")
@config[:traefik]["options"] = { "publish" => %w[9000:9000 9001:9001] }
assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --publish \"9000:9000\" --publish \"9001:9001\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ")
end
test "run with volumes configured" do
assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ")
@config[:traefik]["options"] = { "volume" => %w[./letsencrypt/acme.json:/letsencrypt/acme.json] }
assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ")
end
test "run with several options configured" do
assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ")
@config[:traefik]["options"] = { "volume" => %w[./letsencrypt/acme.json:/letsencrypt/acme.json], "publish" => %w[8080:8080], "memory" => "512m" }
assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" --publish \"8080:8080\" --memory \"512m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ")
end
test "run with labels configured" do
assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ")
@config[:traefik]["labels"] = { "traefik.http.routers.dashboard.service" => "api@internal", "traefik.http.routers.dashboard.middlewares" => "auth" }
assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --label traefik.http.routers.dashboard.service=\"api@internal\" --label traefik.http.routers.dashboard.middlewares=\"auth\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ")
end
test "run with env configured" do
assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ")
@config[:traefik]["env"] = { "EXAMPLE_API_KEY" => "456" }
assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env EXAMPLE_API_KEY=\"456\" --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ")
end
test "run without configuration" do
@config.delete(:traefik)
assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Configuration::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"",
new_command.run.join(" ")
end
test "run with logging config" do
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ")
end
test "run with default args overriden" do
@config[:traefik]["args"]["log.level"] = "ERROR"
assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"ERROR\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ")
end
test "run with args array" do
@config[:traefik]["args"] = { "entrypoints.web.forwardedheaders.trustedips" => %w[ 127.0.0.1 127.0.0.2 ] }
assert_equal "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" traefik:test --providers.docker --log.level=\"DEBUG\" --entrypoints.web.forwardedheaders.trustedips=\"127.0.0.1\" --entrypoints.web.forwardedheaders.trustedips=\"127.0.0.2\"", new_command.run.join(" ")
end
test "traefik start" do
assert_equal \
"docker container start traefik",
new_command.start.join(" ")
end
test "traefik stop" do
assert_equal \
"docker container stop traefik",
new_command.stop.join(" ")
end
test "traefik info" do
assert_equal \
"docker ps --filter name=^traefik$",
new_command.info.join(" ")
end
test "traefik logs" do
assert_equal \
"docker logs traefik --timestamps 2>&1",
new_command.logs.join(" ")
end
test "traefik logs since 2h" do
assert_equal \
"docker logs traefik --since 2h --timestamps 2>&1",
new_command.logs(since: "2h").join(" ")
end
test "traefik logs last 10 lines" do
assert_equal \
"docker logs traefik --tail 10 --timestamps 2>&1",
new_command.logs(lines: 10).join(" ")
end
test "traefik logs with grep hello!" do
assert_equal \
"docker logs traefik --timestamps 2>&1 | grep 'hello!'",
new_command.logs(grep: "hello!").join(" ")
end
test "traefik logs with grep hello! and grep options" do
assert_equal \
"docker logs traefik --timestamps 2>&1 | grep 'hello!' -C 2",
new_command.logs(grep: "hello!", grep_options: "-C 2").join(" ")
end
test "traefik remove container" do
assert_equal \
"docker container prune --force --filter label=org.opencontainers.image.title=Traefik",
new_command.remove_container.join(" ")
end
test "traefik remove image" do
assert_equal \
"docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik",
new_command.remove_image.join(" ")
end
test "traefik follow logs" do
assert_equal \
"ssh -t root@1.1.1.1 -p 22 'docker logs traefik --timestamps --tail 10 --follow 2>&1'",
new_command.follow_logs(host: @config[:servers].first)
end
test "traefik follow logs with grep hello!" do
assert_equal \
"ssh -t root@1.1.1.1 -p 22 'docker logs traefik --timestamps --tail 10 --follow 2>&1 | grep \"hello!\"'",
new_command.follow_logs(host: @config[:servers].first, grep: "hello!")
end
private
def new_command
Kamal::Commands::Traefik.new(Kamal::Configuration.new(@config, version: "123"))
end
end

View File

@@ -119,9 +119,9 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
with_test_secrets("secrets" => "MYSQL_ROOT_PASSWORD=secret123") do
config = Kamal::Configuration.new(@deploy)
assert_equal [ "--env", "MYSQL_ROOT_HOST=\"%\"", "--env-file", ".kamal/env/accessories/app-mysql.env" ], config.accessory(:mysql).env_args.map(&:to_s)
assert_equal [ "--env", "MYSQL_ROOT_HOST=\"%\"", "--env-file", ".kamal/apps/app/env/accessories/mysql.env" ], config.accessory(:mysql).env_args.map(&:to_s)
assert_equal "MYSQL_ROOT_PASSWORD=secret123\n", config.accessory(:mysql).secrets_io.string
assert_equal [ "--env", "SOMETHING=\"else\"", "--env-file", ".kamal/env/accessories/app-redis.env" ], @config.accessory(:redis).env_args
assert_equal [ "--env", "SOMETHING=\"else\"", "--env-file", ".kamal/apps/app/env/accessories/redis.env" ], @config.accessory(:redis).env_args
assert_equal "\n", config.accessory(:redis).secrets_io.string
end
end

View File

@@ -0,0 +1,25 @@
require "test_helper"
class ConfigurationEnvTest < ActiveSupport::TestCase
setup do
@deploy = {
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" },
builder: { "arch" => "amd64" }, servers: [ "1.1.1.1" ]
}
end
test "ssl with host" do
@deploy[:proxy] = { "ssl" => true, "host" => "example.com" }
assert_equal true, config.proxy.ssl?
end
test "ssl with no host" do
@deploy[:proxy] = { "ssl" => true }
assert_raises(Kamal::ConfigurationError) { config.proxy.ssl? }
end
private
def config
Kamal::Configuration.new(@deploy)
end
end

View File

@@ -39,7 +39,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
end
test "special label args for web" do
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"web\"", "--label", "destination", "--label", "traefik.http.services.app-web.loadbalancer.server.scheme=\"http\"", "--label", "traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.routers.app-web.priority=\"2\"", "--label", "traefik.http.middlewares.app-web-retry.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\"", "--label", "traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\"" ], config.role(:web).label_args
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"web\"", "--label", "destination" ], config.role(:web).label_args
end
test "custom labels" do
@@ -53,24 +53,19 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
assert_equal "70", Kamal::Configuration.new(@deploy_with_roles).role(:workers).labels["my.custom.label"]
end
test "overwriting default traefik label" do
@deploy[:labels] = { "traefik.http.routers.app-web.rule" => "\"Host(\\`example.com\\`) || (Host(\\`example.org\\`) && Path(\\`/traefik\\`))\"" }
assert_equal "\"Host(\\`example.com\\`) || (Host(\\`example.org\\`) && Path(\\`/traefik\\`))\"", config.role(:web).labels["traefik.http.routers.app-web.rule"]
end
test "default traefik label on non-web role" do
test "default proxy label on non-web role" do
config = Kamal::Configuration.new(@deploy_with_roles.tap { |c|
c[:servers]["beta"] = { "traefik" => true, "hosts" => [ "1.1.1.5" ] }
c[:servers]["beta"] = { "proxy" => true, "hosts" => [ "1.1.1.5" ] }
})
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"beta\"", "--label", "destination", "--label", "traefik.http.services.app-beta.loadbalancer.server.scheme=\"http\"", "--label", "traefik.http.routers.app-beta.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.routers.app-beta.priority=\"2\"", "--label", "traefik.http.middlewares.app-beta-retry.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app-beta-retry.retry.initialinterval=\"500ms\"", "--label", "traefik.http.routers.app-beta.middlewares=\"app-beta-retry@docker\"" ], config.role(:beta).label_args
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"beta\"", "--label", "destination" ], config.role(:beta).label_args
end
test "env overwritten by role" do
assert_equal "redis://a/b", config_with_roles.role(:workers).env("1.1.1.3").clear["REDIS_URL"]
assert_equal \
[ "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"", "--env-file", ".kamal/env/roles/app-workers.env" ],
[ "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"", "--env-file", ".kamal/apps/app/env/roles/workers.env" ],
config_with_roles.role(:workers).env_args("1.1.1.3").map(&:to_s)
assert_equal \
@@ -89,7 +84,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
test "env args" do
assert_equal \
[ "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"", "--env-file", ".kamal/env/roles/app-workers.env" ],
[ "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"", "--env-file", ".kamal/apps/app/env/roles/workers.env" ],
config_with_roles.role(:workers).env_args("1.1.1.3").map(&:to_s)
assert_equal \
@@ -119,7 +114,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
}
assert_equal \
[ "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"", "--env-file", ".kamal/env/roles/app-workers.env" ],
[ "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"", "--env-file", ".kamal/apps/app/env/roles/workers.env" ],
config_with_roles.role(:workers).env_args("1.1.1.3").map(&:to_s)
assert_equal \
@@ -141,7 +136,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
}
assert_equal \
[ "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"", "--env-file", ".kamal/env/roles/app-workers.env" ],
[ "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"", "--env-file", ".kamal/apps/app/env/roles/workers.env" ],
config_with_roles.role(:workers).env_args("1.1.1.3").map(&:to_s)
assert_equal \
@@ -162,7 +157,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
}
assert_equal \
[ "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"", "--env-file", ".kamal/env/roles/app-workers.env" ],
[ "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"", "--env-file", ".kamal/apps/app/env/roles/workers.env" ],
config_with_roles.role(:workers).env_args("1.1.1.3").map(&:to_s)
assert_equal \
@@ -189,7 +184,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
}
assert_equal \
[ "--env", "REDIS_URL=\"redis://c/d\"", "--env-file", ".kamal/env/roles/app-workers.env" ],
[ "--env", "REDIS_URL=\"redis://c/d\"", "--env-file", ".kamal/apps/app/env/roles/workers.env" ],
config_with_roles.role(:workers).env_args("1.1.1.3").map(&:to_s)
assert_equal \
@@ -198,26 +193,6 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
end
end
test "uses cord" do
assert config_with_roles.role(:web).uses_cord?
assert_not config_with_roles.role(:workers).uses_cord?
end
test "cord host file" do
assert_match %r{.kamal/cords/app-web-[0-9a-f]{32}/cord}, config_with_roles.role(:web).cord_host_file
end
test "cord volume" do
assert_equal "/tmp/kamal-cord", config_with_roles.role(:web).cord_volume.container_path
assert_match %r{.kamal/cords/app-web-[0-9a-f]{32}}, config_with_roles.role(:web).cord_volume.host_path
assert_equal "--volume", config_with_roles.role(:web).cord_volume.docker_args[0]
assert_match %r{\$\(pwd\)/.kamal/cords/app-web-[0-9a-f]{32}:/tmp/kamal-cord}, config_with_roles.role(:web).cord_volume.docker_args[1]
end
test "cord container file" do
assert_equal "/tmp/kamal-cord/cord", config_with_roles.role(:web).cord_container_file
end
test "asset path and volume args" do
ENV["VERSION"] = "12345"
assert_nil config_with_roles.role(:web).asset_volume_args
@@ -232,7 +207,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
})
assert_equal "foo", config_with_assets.role(:web).asset_path
assert_equal "foo", config_with_assets.role(:workers).asset_path
assert_equal [ "--volume", "$(pwd)/.kamal/assets/volumes/app-web-12345:foo" ], config_with_assets.role(:web).asset_volume_args
assert_equal [ "--volume", "$(pwd)/.kamal/apps/app/assets/volumes/web-12345:foo" ], config_with_assets.role(:web).asset_volume_args
assert_nil config_with_assets.role(:workers).asset_volume_args
assert config_with_assets.role(:web).assets?
assert_not config_with_assets.role(:workers).assets?
@@ -242,7 +217,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
})
assert_equal "bar", config_with_assets.role(:web).asset_path
assert_nil config_with_assets.role(:workers).asset_path
assert_equal [ "--volume", "$(pwd)/.kamal/assets/volumes/app-web-12345:bar" ], config_with_assets.role(:web).asset_volume_args
assert_equal [ "--volume", "$(pwd)/.kamal/apps/app/assets/volumes/web-12345:bar" ], config_with_assets.role(:web).asset_volume_args
assert_nil config_with_assets.role(:workers).asset_volume_args
assert config_with_assets.role(:web).assets?
assert_not config_with_assets.role(:workers).assets?
@@ -253,20 +228,36 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
test "asset extracted path" do
ENV["VERSION"] = "12345"
assert_equal ".kamal/assets/extracted/app-web-12345", config_with_roles.role(:web).asset_extracted_path
assert_equal ".kamal/assets/extracted/app-workers-12345", config_with_roles.role(:workers).asset_extracted_path
assert_equal ".kamal/apps/app/assets/extracted/web-12345", config_with_roles.role(:web).asset_extracted_directory
assert_equal ".kamal/apps/app/assets/extracted/workers-12345", config_with_roles.role(:workers).asset_extracted_directory
ensure
ENV.delete("VERSION")
end
test "asset volume path" do
ENV["VERSION"] = "12345"
assert_equal ".kamal/assets/volumes/app-web-12345", config_with_roles.role(:web).asset_volume_path
assert_equal ".kamal/assets/volumes/app-workers-12345", config_with_roles.role(:workers).asset_volume_path
assert_equal ".kamal/apps/app/assets/volumes/web-12345", config_with_roles.role(:web).asset_volume_directory
assert_equal ".kamal/apps/app/assets/volumes/workers-12345", config_with_roles.role(:workers).asset_volume_directory
ensure
ENV.delete("VERSION")
end
test "stop args with proxy" do
assert_equal [], config_with_roles.role(:web).stop_args
end
test "stop args with no proxy" do
assert_equal [ "-t", 30 ], config_with_roles.role(:workers).stop_args
end
test "role specific proxy config" do
@deploy_with_roles[:proxy] = { "response_timeout" => 15 }
@deploy_with_roles[:servers]["workers"]["proxy"] = { "response_timeout" => 18 }
assert_equal "15s", config_with_roles.role(:web).proxy.deploy_options[:"target-timeout"]
assert_equal "18s", config_with_roles.role(:workers).proxy.deploy_options[:"target-timeout"]
end
private
def config
Kamal::Configuration.new(@deploy)

View File

@@ -14,7 +14,7 @@ class ConfigurationValidationTest < ActiveSupport::TestCase
assert_error "#{key}: should be a boolean", **{ key => "foo" }
end
[ :stop_wait_time, :retain_containers, :readiness_delay ].each do |key|
[ :deploy_timeout, :drain_timeout, :retain_containers, :readiness_delay ].each do |key|
assert_error "#{key}: should be an integer", **{ key => "foo" }
end
@@ -22,7 +22,7 @@ class ConfigurationValidationTest < ActiveSupport::TestCase
assert_error "servers: should be an array or a hash", servers: "foo"
[ :labels, :registry, :accessories, :env, :ssh, :sshkit, :builder, :traefik, :boot, :healthcheck, :logging ].each do |key|
[ :labels, :registry, :accessories, :env, :ssh, :sshkit, :builder, :proxy, :boot, :logging ].each do |key|
assert_error "#{key}: should be a hash", **{ key =>[] }
end
end

View File

@@ -77,22 +77,22 @@ class ConfigurationTest < ActiveSupport::TestCase
assert_equal "1.1.1.1", @config_with_roles.primary_host
end
test "traefik hosts" do
assert_equal [ "1.1.1.1", "1.1.1.2" ], @config_with_roles.traefik_hosts
test "proxy hosts" do
assert_equal [ "1.1.1.1", "1.1.1.2" ], @config_with_roles.proxy_hosts
@deploy_with_roles[:servers]["workers"]["traefik"] = true
@deploy_with_roles[:servers]["workers"]["proxy"] = true
config = Kamal::Configuration.new(@deploy_with_roles)
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3" ], config.traefik_hosts
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3" ], config.proxy_hosts
end
test "filtered traefik hosts" do
assert_equal [ "1.1.1.1", "1.1.1.2" ], @config_with_roles.traefik_hosts
test "filtered proxy hosts" do
assert_equal [ "1.1.1.1", "1.1.1.2" ], @config_with_roles.proxy_hosts
@deploy_with_roles[:servers]["workers"]["traefik"] = true
@deploy_with_roles[:servers]["workers"]["proxy"] = true
config = Kamal::Configuration.new(@deploy_with_roles)
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3" ], config.traefik_hosts
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3" ], config.proxy_hosts
end
test "version no git repo" do
@@ -157,10 +157,6 @@ class ConfigurationTest < ActiveSupport::TestCase
assert_equal "app-missing", @config.service_with_version
end
test "healthcheck service" do
assert_equal "healthcheck-app", @config.healthcheck_service
end
test "hosts required for all roles" do
# Empty server list for implied web role
assert_raises(Kamal::ConfigurationError) do
@@ -269,8 +265,7 @@ class ConfigurationTest < ActiveSupport::TestCase
sshkit: {},
volume_args: [ "--volume", "/local/path:/container/path" ],
builder: { "arch" => "amd64" },
logging: [ "--log-opt", "max-size=\"10m\"" ],
healthcheck: { "cmd"=>"curl -f http://localhost:3000/up || exit 1", "interval" => "1s", "path"=>"/up", "port"=>3000, "max_attempts" => 7, "cord" => "/tmp/kamal-cord", "log_lines" => 50 } }
logging: [ "--log-opt", "max-size=\"10m\"" ] }
assert_equal expected_config, @config.to_h
end
@@ -294,22 +289,6 @@ class ConfigurationTest < ActiveSupport::TestCase
test "run directory" do
config = Kamal::Configuration.new(@deploy)
assert_equal ".kamal", config.run_directory
config = Kamal::Configuration.new(@deploy.merge!(run_directory: "/root/kamal"))
assert_equal "/root/kamal", config.run_directory
end
test "run directory as docker volume" do
config = Kamal::Configuration.new(@deploy)
assert_equal "$(pwd)/.kamal", config.run_directory_as_docker_volume
config = Kamal::Configuration.new(@deploy.merge!(run_directory: "/root/kamal"))
assert_equal "/root/kamal", config.run_directory_as_docker_volume
end
test "run id" do
SecureRandom.expects(:hex).with(16).returns("09876543211234567890098765432112")
assert_equal "09876543211234567890098765432112", @config.run_id
end
test "asset path" do
@@ -328,7 +307,7 @@ class ConfigurationTest < ActiveSupport::TestCase
assert_equal "alternate_web", config.primary_role.name
assert_equal "1.1.1.4", config.primary_host
assert config.role(:alternate_web).primary?
assert config.role(:alternate_web).running_traefik?
assert config.role(:alternate_web).running_proxy?
end
test "primary role missing" do
@@ -350,7 +329,52 @@ class ConfigurationTest < ActiveSupport::TestCase
dest_config_file = Pathname.new(File.expand_path("fixtures/deploy_with_extensions.yml", __dir__))
config = Kamal::Configuration.create_from config_file: dest_config_file
assert_equal config.role(:web_tokyo).running_traefik?, true
assert_equal config.role(:web_chicago).running_traefik?, true
assert_equal config.role(:web_tokyo).running_proxy?, true
assert_equal config.role(:web_chicago).running_proxy?, true
end
test "traefik hooks raise error" do
Dir.mktmpdir do |dir|
Dir.chdir(dir) do
FileUtils.mkdir_p ".kamal/hooks"
FileUtils.touch ".kamal/hooks/post-traefik-reboot"
FileUtils.touch ".kamal/hooks/pre-traefik-reboot"
exception = assert_raises(Kamal::ConfigurationError) do
Kamal::Configuration.new(@deploy)
end
assert_equal "Found pre-traefik-reboot, post-traefik-reboot, these should be renamed to (pre|post)-proxy-reboot", exception.message
end
end
end
test "proxy ssl roles with no host" do
@deploy_with_roles[:servers]["workers"]["proxy"] = { "ssl" => true }
exception = assert_raises(Kamal::ConfigurationError) do
Kamal::Configuration.new(@deploy_with_roles)
end
assert_equal "servers/workers/proxy: Must set a host to enable automatic SSL", exception.message
end
test "proxy ssl roles with multiple servers" do
@deploy_with_roles[:servers]["workers"]["proxy"] = { "ssl" => true, "host" => "foo.example.com" }
exception = assert_raises(Kamal::ConfigurationError) do
Kamal::Configuration.new(@deploy_with_roles)
end
assert_equal "SSL is only supported on a single server, found 2 servers for role workers", exception.message
end
test "two proxy ssl roles with same host" do
@deploy_with_roles[:servers]["web"] = { "hosts" => [ "1.1.1.1" ], "proxy" => { "ssl" => true, "host" => "foo.example.com" } }
@deploy_with_roles[:servers]["workers"] = { "hosts" => [ "1.1.1.1" ], "proxy" => { "ssl" => true, "host" => "foo.example.com" } }
exception = assert_raises(Kamal::ConfigurationError) do
Kamal::Configuration.new(@deploy_with_roles)
end
assert_equal "Different roles can't share the same host for SSL: foo.example.com", exception.message
end
end

View File

@@ -2,12 +2,12 @@ service: app
image: dhh/app
servers:
web_chicago:
traefik: true
proxy: {}
hosts:
- 1.1.1.1
- 1.1.1.2
web_tokyo:
traefik: true
proxy: {}
hosts:
- 1.1.1.3
- 1.1.1.4

View File

@@ -1,6 +1,6 @@
x-web: &web
traefik: true
proxy: {}
service: app
image: dhh/app

View File

@@ -8,14 +8,14 @@ servers:
- 1.1.1.2
env:
ROLE: "web"
traefik: true
proxy: true
web_tokyo:
hosts:
- 1.1.1.3
- 1.1.1.4
env:
ROLE: "web"
traefik: true
proxy: true
workers:
cmd: bin/jobs
hosts:

40
test/fixtures/deploy_with_proxy.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
service: app
image: dhh/app
servers:
web:
- "1.1.1.1"
- "1.1.1.2"
workers:
- "1.1.1.3"
- "1.1.1.4"
registry:
username: user
password: pw
builder:
arch: amd64
accessories:
mysql:
image: mysql:5.7
host: 1.1.1.3
port: 3306
env:
clear:
MYSQL_ROOT_HOST: '%'
secret:
- MYSQL_ROOT_PASSWORD
files:
- test/fixtures/files/my.cnf:/etc/mysql/my.cnf
directories:
- data:/var/lib/mysql
redis:
image: redis:latest
roles:
- web
port: 6379
directories:
- data:/data
readiness_delay: 0
deploy_timeout: 6

View File

@@ -0,0 +1,46 @@
service: app
image: dhh/app
servers:
web:
hosts:
- "1.1.1.1"
- "1.1.1.2"
web2:
hosts:
- "1.1.1.3"
- "1.1.1.4"
proxy:
response_timeout: 15
registry:
username: user
password: pw
builder:
arch: amd64
proxy:
response_timeout: 10
accessories:
mysql:
image: mysql:5.7
host: 1.1.1.3
port: 3306
env:
clear:
MYSQL_ROOT_HOST: '%'
secret:
- MYSQL_ROOT_PASSWORD
files:
- test/fixtures/files/my.cnf:/etc/mysql/my.cnf
directories:
- data:/var/lib/mysql
redis:
image: redis:latest
roles:
- web
port: 6379
directories:
- data:/data
readiness_delay: 0
deploy_timeout: 6

View File

@@ -16,3 +16,4 @@ registry:
password: pw
builder:
arch: amd64
deploy_timeout: 1

View File

@@ -2,7 +2,7 @@ service: app
image: dhh/app
servers:
workers:
traefik: false
proxy: false
hosts:
- 1.1.1.1
- 1.1.1.2

View File

@@ -20,34 +20,35 @@ class AppTest < IntegrationTest
wait_for_app_to_be_up
logs = kamal :app, :logs, capture: true
assert_match /App Host: vm1/, logs
assert_match /App Host: vm2/, logs
assert_match /GET \/ HTTP\/1.1/, logs
assert_match "App Host: vm1", logs
assert_match "App Host: vm2", logs
assert_match "GET /version HTTP/1.1", logs
images = kamal :app, :images, capture: true
assert_match /App Host: vm1/, images
assert_match /App Host: vm2/, images
assert_match "App Host: vm1", images
assert_match "App Host: vm2", images
assert_match /registry:4443\/app\s+#{latest_app_version}/, images
assert_match /registry:4443\/app\s+latest/, images
containers = kamal :app, :containers, capture: true
assert_match /App Host: vm1/, containers
assert_match /App Host: vm2/, containers
assert_match /registry:4443\/app:#{latest_app_version}/, containers
assert_match /registry:4443\/app:latest/, containers
assert_match "App Host: vm1", containers
assert_match "App Host: vm2", containers
assert_match "registry:4443/app:#{latest_app_version}", containers
assert_match "registry:4443/app:latest", containers
exec_output = kamal :app, :exec, :ps, capture: true
assert_match /App Host: vm1/, exec_output
assert_match /App Host: vm2/, exec_output
assert_match "App Host: vm1", exec_output
assert_match "App Host: vm2", exec_output
assert_match /1 root 0:\d\d ps/, exec_output
exec_output = kamal :app, :exec, "--reuse", :ps, capture: true
assert_match /App Host: vm1/, exec_output
assert_match /App Host: vm2/, exec_output
assert_match "App Host: vm2", exec_output
assert_match "App Host: vm1", exec_output
assert_match /1 root 0:\d\d nginx/, exec_output
kamal :app, :remove
assert_app_is_down
assert_app_directory_removed
end
end

View File

@@ -9,7 +9,7 @@ class BrokenDeployTest < IntegrationTest
kamal :deploy
assert_app_is_up version: first_version
assert_container_running host: :vm3, name: "app-workers-#{first_version}"
assert_container_running host: :vm3, name: "app_with_roles-workers-#{first_version}"
second_version = break_app
@@ -17,8 +17,8 @@ class BrokenDeployTest < IntegrationTest
assert_failed_deploy output
assert_app_is_up version: first_version
assert_container_running host: :vm3, name: "app-workers-#{first_version}"
assert_container_not_running host: :vm3, name: "app-workers-#{second_version}"
assert_container_running host: :vm3, name: "app_with_roles-workers-#{first_version}"
assert_container_not_running host: :vm3, name: "app_with_roles-workers-#{second_version}"
end
private
@@ -27,6 +27,5 @@ class BrokenDeployTest < IntegrationTest
assert_match /First web container is unhealthy on vm[12], not booting any other roles/, output
assert_match "First web container is unhealthy, not booting workers on vm3", output
assert_match "nginx: [emerg] unexpected end of file, expecting \";\" or \"}\" in /etc/nginx/conf.d/default.conf:2", output
assert_match 'ERROR {"Status":"unhealthy","FailingStreak":0,"Log":[]}', output
end
end

View File

@@ -0,0 +1,3 @@
#!/bin/sh
echo "Rebooted kamal-proxy on ${KAMAL_HOSTS}"
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/post-proxy-reboot

View File

@@ -1,3 +0,0 @@
#!/bin/sh
echo "Rebooted Traefik on ${KAMAL_HOSTS}"
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/post-traefik-reboot

View File

@@ -0,0 +1,3 @@
#!/bin/sh
echo "Rebooting Traefik on ${KAMAL_HOSTS}..."
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-proxy-reboot

View File

@@ -1,3 +0,0 @@
#!/bin/sh
echo "Rebooting Traefik on ${KAMAL_HOSTS}..."
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-traefik-reboot

View File

@@ -6,4 +6,4 @@ ARG COMMIT_SHA
RUN echo $COMMIT_SHA > /usr/share/nginx/html/version
RUN mkdir -p /usr/share/nginx/html/versions && echo "version" > /usr/share/nginx/html/versions/$COMMIT_SHA
RUN mkdir -p /usr/share/nginx/html/versions && echo "hidden" > /usr/share/nginx/html/versions/.hidden
RUN echo "Up!" > /usr/share/nginx/html/up

View File

@@ -20,7 +20,11 @@ env:
secret:
- SECRET_TAG
asset_path: /usr/share/nginx/html/versions
deploy_timeout: 2
drain_timeout: 2
readiness_delay: 0
proxy:
host: 127.0.0.1
registry:
server: registry:4443
username: root
@@ -30,14 +34,6 @@ builder:
arch: <%= Kamal::Utils.docker_arch %>
args:
COMMIT_SHA: <%= `git rev-parse HEAD` %>
healthcheck:
cmd: wget -qO- http://localhost > /dev/null || exit 1
max_attempts: 3
traefik:
args:
accesslog: true
accesslog.format: json
image: registry:4443/traefik:v2.10
accessories:
busybox:
service: custom-busybox
@@ -45,5 +41,3 @@ accessories:
cmd: sh -c 'echo "Starting busybox..."; trap exit term; while true; do sleep 1; done'
roles:
- web
stop_wait_time: 1
readiness_delay: 0

View File

@@ -0,0 +1,3 @@
#!/bin/sh
echo "Rebooted kamal-proxy on ${KAMAL_HOSTS}"
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/post-proxy-reboot

View File

@@ -1,3 +0,0 @@
#!/bin/sh
echo "Rebooted Traefik on ${KAMAL_HOSTS}"
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/post-traefik-reboot

View File

@@ -0,0 +1,3 @@
#!/bin/sh
echo "Rebooting kamal-proxy on ${KAMAL_HOSTS}..."
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-proxy-reboot

View File

@@ -1,3 +0,0 @@
#!/bin/sh
echo "Rebooting Traefik on ${KAMAL_HOSTS}..."
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-traefik-reboot

View File

@@ -6,4 +6,4 @@ ARG COMMIT_SHA
RUN echo $COMMIT_SHA > /usr/share/nginx/html/version
RUN mkdir -p /usr/share/nginx/html/versions && echo "version" > /usr/share/nginx/html/versions/$COMMIT_SHA
RUN mkdir -p /usr/share/nginx/html/versions && echo "hidden" > /usr/share/nginx/html/versions/.hidden
RUN echo "Up!" > /usr/share/nginx/html/up

View File

@@ -1,5 +1,5 @@
service: app
image: app
service: app_with_roles
image: app_with_roles
servers:
web:
hosts:
@@ -9,6 +9,31 @@ servers:
hosts:
- vm3
cmd: sleep infinity
deploy_timeout: 2
drain_timeout: 2
readiness_delay: 0
proxy:
host: localhost
healthcheck:
interval: 1
timeout: 1
path: "/up"
response_timeout: 2
buffering:
requests: true
responses: true
memory: 400_000
max_request_body: 40_000_000
max_response_body: 40_000_000
forward_headers: true
logging:
request_headers:
- Cache-Control
- X-Forwarded-Proto
response_headers:
- X-Request-ID
- X-Request-Start
asset_path: /usr/share/nginx/html/versions
@@ -21,14 +46,6 @@ builder:
arch: <%= Kamal::Utils.docker_arch %>
args:
COMMIT_SHA: <%= `git rev-parse HEAD` %>
healthcheck:
cmd: wget -qO- http://localhost > /dev/null || exit 1
max_attempts: 3
traefik:
args:
accesslog: true
accesslog.format: json
image: registry:4443/traefik:v2.10
accessories:
busybox:
service: custom-busybox
@@ -36,8 +53,6 @@ accessories:
cmd: sh -c 'echo "Starting busybox..."; trap exit term; while true; do sleep 1; done'
roles:
- web
stop_wait_time: 1
readiness_delay: 0
aliases:
whome: version
worker_hostname: app exec -r workers -q --reuse hostname

View File

@@ -19,7 +19,6 @@ push_image_to_registry_4443() {
install_kamal
push_image_to_registry_4443 nginx 1-alpine-slim
push_image_to_registry_4443 traefik v2.10
push_image_to_registry_4443 busybox 1.36.0
# .ssh is on a shared volume that persists between runs. Clean it up as the

View File

@@ -8,6 +8,8 @@ server {
location / {
proxy_pass http://loadbalancer;
proxy_set_header Host $host;
proxy_connect_timeout 10;
proxy_send_timeout 10;
proxy_read_timeout 10;

View File

@@ -31,7 +31,7 @@ class IntegrationTest < ActiveSupport::TestCase
succeeded = system("cd test/integration && #{command}")
end
raise "Command `#{command}` failed with error code `#{$?}`" if !succeeded && raise_on_error
raise "Command `#{command}` failed with error code `#{$?}`, and output:\n#{result}" if !succeeded && raise_on_error
result
end
@@ -50,8 +50,8 @@ class IntegrationTest < ActiveSupport::TestCase
assert_equal "502", response.code
end
def assert_app_is_up(version: nil)
response = app_response
def assert_app_is_up(version: nil, app: @app)
response = app_response(app: app)
debug_response_code(response, "200")
assert_equal "200", response.code
assert_app_version(version, response) if version
@@ -69,8 +69,8 @@ class IntegrationTest < ActiveSupport::TestCase
assert_equal up_times, up_count
end
def app_response
Net::HTTP.get_response(URI.parse("http://localhost:12345/version"))
def app_response(app: @app)
Net::HTTP.get_response(URI.parse("http://#{app_host(app)}:12345/version"))
end
def update_app_rev
@@ -101,8 +101,8 @@ class IntegrationTest < ActiveSupport::TestCase
def assert_200(response)
code = response.code
if code != "200"
puts "Got response code #{code}, here are the traefik logs:"
kamal :traefik, :logs
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}"
@@ -129,8 +129,8 @@ class IntegrationTest < ActiveSupport::TestCase
def debug_response_code(app_response, expected_code)
code = app_response.code
if code != expected_code
puts "Got response code #{code}, here are the traefik logs:"
kamal :traefik, :logs
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}"
@@ -148,4 +148,31 @@ class IntegrationTest < ActiveSupport::TestCase
def container_running?(host:, name:)
docker_compose("exec #{host} docker ps --filter=name=#{name} | tail -n+2", capture: true).strip.present?
end
def assert_app_directory_removed
assert_directory_removed("./kamal/apps/#{@app}")
end
def assert_directory_removed(directory)
assert docker_compose("exec vm1 ls #{directory} | wc -l", capture: true).strip == "0"
end
def assert_proxy_running
assert_container_running(host: "vm1", name: "kamal-proxy")
end
def assert_proxy_not_running
assert_container_not_running(host: "vm1", name: "kamal-proxy")
end
def app_host(app = @app)
case app
when "app"
"127.0.0.1"
when "app_with_roles"
"localhost"
else
raise "Unknown app: #{app}"
end
end
end

View File

@@ -24,11 +24,11 @@ class MainTest < IntegrationTest
assert_app_is_up version: first_version
details = kamal :details, capture: true
assert_match /Traefik Host: vm1/, details
assert_match /Traefik Host: vm2/, details
assert_match /Proxy Host: vm1/, details
assert_match /Proxy Host: vm2/, details
assert_match /App Host: vm1/, details
assert_match /App Host: vm2/, details
assert_match /traefik:v2.10/, details
assert_match /basecamp\/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}/, details
assert_match /registry:4443\/app:#{first_version}/, details
audit = kamal :audit, capture: true
@@ -46,13 +46,13 @@ class MainTest < IntegrationTest
assert_app_is_up version: version
assert_hooks_ran "pre-connect", "pre-build", "pre-deploy", "post-deploy"
assert_container_running host: :vm3, name: "app-workers-#{version}"
assert_container_running host: :vm3, name: "app_with_roles-workers-#{version}"
second_version = update_app_rev
kamal :redeploy
assert_app_is_up version: second_version
assert_container_running host: :vm3, name: "app-workers-#{second_version}"
assert_container_running host: :vm3, name: "app_with_roles-workers-#{second_version}"
end
test "config" do
@@ -70,13 +70,11 @@ class MainTest < IntegrationTest
assert_equal({ user: "root", port: 22, keepalive: true, keepalive_interval: 30, log_level: :fatal }, config[:ssh_options])
assert_equal({ "driver" => "docker", "arch" => "#{Kamal::Utils.docker_arch}", "args" => { "COMMIT_SHA" => version } }, config[:builder])
assert_equal [ "--log-opt", "max-size=\"10m\"" ], config[:logging]
assert_equal({ "cmd"=>"wget -qO- http://localhost > /dev/null || exit 1", "interval"=>"1s", "max_attempts"=>3, "port"=>3000, "path"=>"/up", "cord"=>"/tmp/kamal-cord", "log_lines"=>50 }, config[:healthcheck])
end
test "aliases" do
@app = "app_with_roles"
kamal :envify
kamal :deploy
output = kamal :whome, capture: true
@@ -99,6 +97,30 @@ class MainTest < IntegrationTest
kamal :remove, "-y"
assert_no_images_or_containers
assert_app_directory_removed
end
test "two apps" do
@app = "app"
kamal :deploy
app1_version = latest_app_version
@app = "app_with_roles"
kamal :deploy
app2_version = latest_app_version
assert_app_is_up version: app1_version, app: "app"
assert_app_is_up version: app2_version, app: "app_with_roles"
@app = "app"
kamal :remove, "-y"
assert_app_directory_removed
assert_proxy_running
@app = "app_with_roles"
kamal :remove, "-y"
assert_app_directory_removed
assert_proxy_not_running
end
private
@@ -116,21 +138,21 @@ class MainTest < IntegrationTest
end
def assert_env(key, value, vm:, version:)
assert_equal "#{key}=#{value}", docker_compose("exec #{vm} docker exec app-web-#{version} env | grep #{key}", capture: true)
assert_equal "#{key}=#{value}", docker_compose("exec #{vm} docker exec #{@app}-web-#{version} env | grep #{key}", capture: true)
end
def assert_no_env(key, vm:, version:)
assert_raises(RuntimeError, /exit 1/) do
docker_compose("exec #{vm} docker exec app-web-#{version} env | grep #{key}", capture: true)
docker_compose("exec #{vm} docker exec #{@app}-web-#{version} env | grep #{key}", capture: true)
end
end
def assert_accumulated_assets(*versions)
versions.each do |version|
assert_equal "200", Net::HTTP.get_response(URI.parse("http://localhost:12345/versions/#{version}")).code
assert_equal "200", Net::HTTP.get_response(URI.parse("http://#{app_host}:12345/versions/#{version}")).code
end
assert_equal "200", Net::HTTP.get_response(URI.parse("http://localhost:12345/versions/.hidden")).code
assert_equal "200", Net::HTTP.get_response(URI.parse("http://#{app_host}:12345/versions/.hidden")).code
end
def vm1_image_ids

View File

@@ -0,0 +1,52 @@
require_relative "integration_test"
class ProxyTest < IntegrationTest
setup do
@app = "app_with_roles"
end
test "boot, reboot, stop, start, restart, logs, remove" do
kamal :proxy, :boot
assert_proxy_running
output = kamal :proxy, :reboot, "-y", "--verbose", capture: true
assert_proxy_running
assert_hooks_ran "pre-proxy-reboot", "post-proxy-reboot"
assert_match /Rebooting kamal-proxy on vm1,vm2.../, output
assert_match /Rebooted kamal-proxy on vm1,vm2/, output
output = kamal :proxy, :reboot, "--rolling", "-y", "--verbose", capture: true
assert_proxy_running
assert_hooks_ran "pre-proxy-reboot", "post-proxy-reboot"
assert_match /Rebooting kamal-proxy on vm1.../, output
assert_match /Rebooted kamal-proxy on vm1/, output
assert_match /Rebooting kamal-proxy on vm2.../, output
assert_match /Rebooted kamal-proxy on vm2/, output
kamal :proxy, :boot
assert_proxy_running
# Check booting when booted doesn't raise an error
kamal :proxy, :stop
assert_proxy_not_running
# Check booting when stopped works
kamal :proxy, :boot
assert_proxy_running
kamal :proxy, :stop
assert_proxy_not_running
kamal :proxy, :start
assert_proxy_running
kamal :proxy, :restart
assert_proxy_running
logs = kamal :proxy, :logs, capture: true
assert_match /No previous state to restore/, logs
kamal :proxy, :remove
assert_proxy_not_running
end
end

View File

@@ -1,61 +0,0 @@
require_relative "integration_test"
class TraefikTest < IntegrationTest
test "boot, reboot, stop, start, restart, logs, remove" do
kamal :traefik, :boot
assert_traefik_running
output = kamal :traefik, :reboot, "-y", "--verbose", capture: true
assert_traefik_running
assert_hooks_ran "pre-traefik-reboot", "post-traefik-reboot"
assert_match /Rebooting Traefik on vm1,vm2.../, output
assert_match /Rebooted Traefik on vm1,vm2/, output
output = kamal :traefik, :reboot, "--rolling", "-y", "--verbose", capture: true
assert_traefik_running
assert_hooks_ran "pre-traefik-reboot", "post-traefik-reboot"
assert_match /Rebooting Traefik on vm1.../, output
assert_match /Rebooted Traefik on vm1/, output
assert_match /Rebooting Traefik on vm2.../, output
assert_match /Rebooted Traefik on vm2/, output
kamal :traefik, :boot
assert_traefik_running
# Check booting when booted doesn't raise an error
kamal :traefik, :stop
assert_traefik_not_running
# Check booting when stopped works
kamal :traefik, :boot
assert_traefik_running
kamal :traefik, :stop
assert_traefik_not_running
kamal :traefik, :start
assert_traefik_running
kamal :traefik, :restart
assert_traefik_running
logs = kamal :traefik, :logs, capture: true
assert_match /Traefik version [\d.]+ built on/, logs
kamal :traefik, :remove
assert_traefik_not_running
end
private
def assert_traefik_running
assert_match /traefik:v2.10 "\/entrypoint.sh/, traefik_details
end
def assert_traefik_not_running
assert_no_match /traefik:v2.10 "\/entrypoint.sh/, traefik_details
end
def traefik_details
kamal :traefik, :details, capture: true
end
end