Catch up with main
This commit is contained in:
@@ -14,8 +14,8 @@ class CliAccessoryTest < CliTestCase
|
||||
Kamal::Cli::Accessory.any_instance.expects(:upload).with("mysql")
|
||||
|
||||
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 --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 login private.registry -u [REDACTED] -p [REDACTED] 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\" private.registry/mysql:5.7 on 1.1.1.3", output
|
||||
end
|
||||
end
|
||||
|
||||
@@ -24,17 +24,21 @@ class CliAccessoryTest < CliTestCase
|
||||
Kamal::Cli::Accessory.any_instance.expects(:upload).with("mysql")
|
||||
Kamal::Cli::Accessory.any_instance.expects(:directories).with("redis")
|
||||
Kamal::Cli::Accessory.any_instance.expects(:upload).with("redis")
|
||||
Kamal::Cli::Accessory.any_instance.expects(:directories).with("busybox")
|
||||
Kamal::Cli::Accessory.any_instance.expects(:upload).with("busybox")
|
||||
|
||||
run_command("boot", "all").tap do |output|
|
||||
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 login private.registry -u [REDACTED] -p [REDACTED] on 1.1.1.3", output
|
||||
assert_match "docker login private.registry -u [REDACTED] -p [REDACTED] on 1.1.1.1", output
|
||||
assert_match "docker login private.registry -u [REDACTED] -p [REDACTED] on 1.1.1.2", output
|
||||
assert_match "docker login other.registry -u [REDACTED] -p [REDACTED] on 1.1.1.3", 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-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\" private.registry/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
|
||||
assert_match "docker run --name custom-box --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --env-file .kamal/apps/app/env/accessories/busybox.env --label service=\"custom-box\" other.registry/busybox:latest on 1.1.1.3", output
|
||||
end
|
||||
end
|
||||
|
||||
@@ -60,13 +64,16 @@ class CliAccessoryTest < CliTestCase
|
||||
end
|
||||
|
||||
test "reboot all" do
|
||||
Kamal::Commands::Registry.any_instance.expects(:login).times(3)
|
||||
Kamal::Commands::Registry.any_instance.expects(:login).times(4)
|
||||
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", 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", prepare: false)
|
||||
Kamal::Cli::Accessory.any_instance.expects(:stop).with("busybox")
|
||||
Kamal::Cli::Accessory.any_instance.expects(:remove_container).with("busybox")
|
||||
Kamal::Cli::Accessory.any_instance.expects(:boot).with("busybox", prepare: false)
|
||||
|
||||
run_command("reboot", "all")
|
||||
end
|
||||
@@ -94,7 +101,7 @@ class CliAccessoryTest < CliTestCase
|
||||
end
|
||||
|
||||
test "details with non-existent accessory" do
|
||||
assert_equal "No accessory by the name of 'hello' (options: mysql and redis)", stderred { run_command("details", "hello") }
|
||||
assert_equal "No accessory by the name of 'hello' (options: mysql, redis, and busybox)", stderred { run_command("details", "hello") }
|
||||
end
|
||||
|
||||
test "details with all" do
|
||||
@@ -180,6 +187,10 @@ class CliAccessoryTest < CliTestCase
|
||||
Kamal::Cli::Accessory.any_instance.expects(:remove_container).with("redis")
|
||||
Kamal::Cli::Accessory.any_instance.expects(:remove_image).with("redis")
|
||||
Kamal::Cli::Accessory.any_instance.expects(:remove_service_directory).with("redis")
|
||||
Kamal::Cli::Accessory.any_instance.expects(:stop).with("busybox")
|
||||
Kamal::Cli::Accessory.any_instance.expects(:remove_container).with("busybox")
|
||||
Kamal::Cli::Accessory.any_instance.expects(:remove_image).with("busybox")
|
||||
Kamal::Cli::Accessory.any_instance.expects(:remove_service_directory).with("busybox")
|
||||
|
||||
run_command("remove", "all", "-y")
|
||||
end
|
||||
@@ -189,7 +200,7 @@ class CliAccessoryTest < CliTestCase
|
||||
end
|
||||
|
||||
test "remove_image" do
|
||||
assert_match "docker image rm --force mysql", run_command("remove_image", "mysql")
|
||||
assert_match "docker image rm --force private.registry/mysql:5.7", run_command("remove_image", "mysql")
|
||||
end
|
||||
|
||||
test "remove_service_directory" do
|
||||
@@ -201,8 +212,8 @@ class CliAccessoryTest < CliTestCase
|
||||
Kamal::Cli::Accessory.any_instance.expects(:upload).with("redis")
|
||||
|
||||
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 login private.registry -u [REDACTED] -p [REDACTED] on 1.1.1.1", output
|
||||
assert_no_match "docker login private.registry -u [REDACTED] -p [REDACTED] 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
|
||||
@@ -213,8 +224,8 @@ class CliAccessoryTest < CliTestCase
|
||||
Kamal::Cli::Accessory.any_instance.expects(:upload).with("redis")
|
||||
|
||||
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 login private.registry -u [REDACTED] -p [REDACTED] on 1.1.1.1", output
|
||||
assert_no_match "docker login private.registry -u [REDACTED] -p [REDACTED] 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
|
||||
@@ -225,7 +236,7 @@ class CliAccessoryTest < CliTestCase
|
||||
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 "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\" private.registry/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
|
||||
@@ -235,14 +246,13 @@ class CliAccessoryTest < CliTestCase
|
||||
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 "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\" private.registry/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" ]) }
|
||||
stdouted { Kamal::Cli::Accessory.start([ *command, "-c", "test/fixtures/deploy_with_accessories_with_different_registries.yml" ]) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -73,7 +73,7 @@ class CliAppTest < CliTestCase
|
||||
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/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 "/usr/bin/env mkdir -p .kamal/apps/app/assets/extracted/web-latest && docker container rm app-web-assets 2> /dev/null || true && docker container create --name app-web-assets dhh/app:latest && docker container cp -L app-web-assets:/public/assets/. .kamal/apps/app/assets/extracted/web-latest && docker container rm 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/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
|
||||
@@ -263,13 +263,37 @@ class CliAppTest < CliTestCase
|
||||
|
||||
test "exec" do
|
||||
run_command("exec", "ruby -v").tap do |output|
|
||||
assert_match "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v", output
|
||||
assert_match "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" 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 --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v", output
|
||||
assert_match "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:latest ruby -v", output
|
||||
end
|
||||
end
|
||||
|
||||
test "exec detach" do
|
||||
run_command("exec", "--detach", "ruby -v").tap do |output|
|
||||
assert_match "docker run --detach --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:latest ruby -v", output
|
||||
end
|
||||
end
|
||||
|
||||
test "exec detach with reuse" do
|
||||
assert_raises(ArgumentError, "Detach is not compatible with reuse") do
|
||||
run_command("exec", "--detach", "--reuse", "ruby -v")
|
||||
end
|
||||
end
|
||||
|
||||
test "exec detach with interactive" do
|
||||
assert_raises(ArgumentError, "Detach is not compatible with interactive") do
|
||||
run_command("exec", "--interactive", "--detach", "ruby -v")
|
||||
end
|
||||
end
|
||||
|
||||
test "exec detach with interactive and reuse" do
|
||||
assert_raises(ArgumentError, "Detach is not compatible with interactive or reuse") do
|
||||
run_command("exec", "--interactive", "--detach", "--reuse", "ruby -v")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -282,7 +306,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 --network kamal --env-file .kamal/apps/app/env/roles/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 --log-opt max-size=\"10m\" 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
|
||||
@@ -329,6 +353,13 @@ class CliAppTest < CliTestCase
|
||||
assert_match "sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --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=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --tail 10 --follow 2>&1", run_command("logs", "--follow")
|
||||
end
|
||||
|
||||
test "logs with follow and container_id" do
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
|
||||
.with("ssh -t root@1.1.1.1 -p 22 'echo ID123 | xargs docker logs --timestamps --tail 10 --follow 2>&1'")
|
||||
|
||||
assert_match "echo ID123 | xargs docker logs --timestamps --tail 10 --follow 2>&1", run_command("logs", "--follow", "--container-id", "ID123")
|
||||
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 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --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=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1 | grep \"hey\"'")
|
||||
@@ -351,8 +382,10 @@ class CliAppTest < CliTestCase
|
||||
|
||||
|
||||
test "version through main" do
|
||||
stdouted { Kamal::Cli::Main.start([ "app", "version", "-c", "test/fixtures/deploy_with_accessories.yml", "--hosts", "1.1.1.1" ]) }.tap do |output|
|
||||
assert_match "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --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=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", output
|
||||
with_argv([ "app", "version", "-c", "test/fixtures/deploy_with_accessories.yml", "--hosts", "1.1.1.1" ]) do
|
||||
stdouted { Kamal::Cli::Main.start }.tap do |output|
|
||||
assert_match "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --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=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", output
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ class CliBuildTest < CliTestCase
|
||||
.raises(SSHKit::Command::Failed.new("no buildx"))
|
||||
|
||||
Kamal::Commands::Builder.any_instance.stubs(:native_and_local?).returns(false)
|
||||
assert_raises(Kamal::Cli::Build::BuildError) { run_command("push") }
|
||||
assert_raises(Kamal::Cli::DependencyError) { run_command("push") }
|
||||
end
|
||||
|
||||
test "push pre-build hook failure" do
|
||||
@@ -235,6 +235,12 @@ class CliBuildTest < CliTestCase
|
||||
end
|
||||
end
|
||||
|
||||
test "create cloud" do
|
||||
run_command("create", fixture: :with_cloud_builder).tap do |output|
|
||||
assert_match /docker buildx create --driver cloud example_org\/cloud_builder/, output
|
||||
end
|
||||
end
|
||||
|
||||
test "create with error" do
|
||||
stub_setup
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
@@ -252,6 +258,12 @@ class CliBuildTest < CliTestCase
|
||||
end
|
||||
end
|
||||
|
||||
test "remove cloud" do
|
||||
run_command("remove", fixture: :with_cloud_builder).tap do |output|
|
||||
assert_match /docker buildx rm cloud-example_org-cloud_builder/, output
|
||||
end
|
||||
end
|
||||
|
||||
test "details" do
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:capture)
|
||||
.with(:docker, :context, :ls, "&&", :docker, :buildx, :ls)
|
||||
@@ -274,17 +286,4 @@ class CliBuildTest < CliTestCase
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.with { |*args| args[0..1] == [ :docker, :buildx ] }
|
||||
end
|
||||
|
||||
def with_build_directory
|
||||
build_directory = File.join Dir.tmpdir, "kamal-clones", "app-#{pwd_sha}", "kamal"
|
||||
FileUtils.mkdir_p build_directory
|
||||
FileUtils.touch File.join build_directory, "Dockerfile"
|
||||
yield build_directory + "/"
|
||||
ensure
|
||||
FileUtils.rm_rf build_directory
|
||||
end
|
||||
|
||||
def pwd_sha
|
||||
Digest::SHA256.hexdigest(Dir.pwd)[0..12]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -51,4 +51,17 @@ class CliTestCase < ActiveSupport::TestCase
|
||||
ensure
|
||||
ARGV.replace(old_argv)
|
||||
end
|
||||
|
||||
def with_build_directory
|
||||
build_directory = File.join Dir.tmpdir, "kamal-clones", "app-#{pwd_sha}", "kamal"
|
||||
FileUtils.mkdir_p build_directory
|
||||
FileUtils.touch File.join build_directory, "Dockerfile"
|
||||
yield build_directory + "/"
|
||||
ensure
|
||||
FileUtils.rm_rf build_directory
|
||||
end
|
||||
|
||||
def pwd_sha
|
||||
Digest::SHA256.hexdigest(Dir.pwd)[0..12]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,8 +8,7 @@ class CliMainTest < CliTestCase
|
||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
|
||||
|
||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:server:bootstrap", [], invoke_options)
|
||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:boot", [ "all" ], invoke_options)
|
||||
Kamal::Cli::Main.any_instance.expects(:deploy)
|
||||
Kamal::Cli::Main.any_instance.expects(:deploy).with(boot_accessories: true)
|
||||
|
||||
run_command("setup").tap do |output|
|
||||
assert_match /Ensure Docker is installed.../, output
|
||||
@@ -460,6 +459,7 @@ class CliMainTest < CliTestCase
|
||||
|
||||
test "run an alias for a console" do
|
||||
run_command("console", config_file: "deploy_with_aliases").tap do |output|
|
||||
assert_no_match "App Host: 1.1.1.4", output
|
||||
assert_match "docker exec app-console-999 bin/console on 1.1.1.5", output
|
||||
assert_match "App Host: 1.1.1.5", output
|
||||
end
|
||||
@@ -486,6 +486,33 @@ class CliMainTest < CliTestCase
|
||||
end
|
||||
end
|
||||
|
||||
test "switch config file with an alias" do
|
||||
with_config_files do
|
||||
with_argv([ "other_config" ]) do
|
||||
stdouted { Kamal::Cli::Main.start }.tap do |output|
|
||||
assert_match ":service_with_version: app2-999", output
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "switch destination with an alias" do
|
||||
with_config_files do
|
||||
with_argv([ "other_destination_config" ]) do
|
||||
stdouted { Kamal::Cli::Main.start }.tap do |output|
|
||||
assert_match ":service_with_version: app3-999", output
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "run on primary via alias" do
|
||||
run_command("primary_details", config_file: "deploy_with_aliases").tap do |output|
|
||||
assert_match "App Host: 1.1.1.1", output
|
||||
assert_no_match "App Host: 1.1.1.2", output
|
||||
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)
|
||||
@@ -530,6 +557,20 @@ class CliMainTest < CliTestCase
|
||||
end
|
||||
end
|
||||
|
||||
def with_config_files
|
||||
Dir.mktmpdir do |tmpdir|
|
||||
config_dir = File.join(tmpdir, "config")
|
||||
FileUtils.mkdir_p(config_dir)
|
||||
FileUtils.cp "test/fixtures/deploy.yml", config_dir
|
||||
FileUtils.cp "test/fixtures/deploy2.yml", config_dir
|
||||
FileUtils.cp "test/fixtures/deploy.elsewhere.yml", config_dir
|
||||
|
||||
Dir.chdir(tmpdir) do
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def assert_file(file, content)
|
||||
assert_match content, File.read(file)
|
||||
end
|
||||
|
||||
@@ -55,13 +55,11 @@ class CliProxyTest < CliTestCase
|
||||
|
||||
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 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --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 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --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
|
||||
@@ -281,6 +279,32 @@ class CliProxyTest < CliTestCase
|
||||
end
|
||||
end
|
||||
|
||||
test "boot_config set bind IP" do
|
||||
run_command("boot_config", "set", "--publish-host-ip", "127.0.0.1").tap do |output|
|
||||
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
|
||||
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
|
||||
assert_match "Uploading \"--publish 127.0.0.1:80:80 --publish 127.0.0.1:443:443 --log-opt max-size=10m\" to .kamal/proxy/options on #{host}", output
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "boot_config set multiple bind IPs" do
|
||||
run_command("boot_config", "set", "--publish-host-ip", "127.0.0.1", "--publish-host-ip", "::1").tap do |output|
|
||||
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
|
||||
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
|
||||
assert_match "Uploading \"--publish 127.0.0.1:80:80 --publish 127.0.0.1:443:443 --publish [::1]:80:80 --publish [::1]:443:443 --log-opt max-size=10m\" to .kamal/proxy/options on #{host}", output
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "boot_config set invalid bind IPs" do
|
||||
exception = assert_raises do
|
||||
run_command("boot_config", "set", "--publish-host-ip", "1.2.3.invalidIP", "--publish-host-ip", "::1")
|
||||
end
|
||||
|
||||
assert_includes exception.message, "Invalid publish IP address: 1.2.3.invalidIP"
|
||||
end
|
||||
|
||||
test "boot_config set docker options" do
|
||||
run_command("boot_config", "set", "--docker_options", "label=foo=bar", "add_host=thishost:thathost").tap do |output|
|
||||
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
|
||||
|
||||
@@ -43,6 +43,16 @@ class CliRegistryTest < CliTestCase
|
||||
end
|
||||
end
|
||||
|
||||
test "login with no docker" do
|
||||
stub_setup
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.with(:docker, "--version", "&&", :docker, :buildx, "version")
|
||||
.raises(SSHKit::Command::Failed.new("command not found"))
|
||||
|
||||
assert_raises(Kamal::Cli::DependencyError) { run_command("login") }
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
def run_command(*command)
|
||||
stdouted { Kamal::Cli::Registry.start([ *command, "-c", "test/fixtures/deploy_with_accessories.yml" ]) }
|
||||
|
||||
@@ -13,12 +13,6 @@ class CliSecretsTest < CliTestCase
|
||||
run_command("fetch", "foo", "bar", "baz", "--adapter", "test")
|
||||
end
|
||||
|
||||
test "fetch without required --account" do
|
||||
assert_equal \
|
||||
"\\{\\\"foo\\\":\\\"oof\\\",\\\"bar\\\":\\\"rab\\\",\\\"baz\\\":\\\"zab\\\"\\}",
|
||||
run_command("fetch", "foo", "bar", "baz", "--adapter", "test_optional_account")
|
||||
end
|
||||
|
||||
test "extract" do
|
||||
assert_equal "oof", run_command("extract", "foo", "{\"foo\":\"oof\", \"bar\":\"rab\", \"baz\":\"zab\"}")
|
||||
end
|
||||
|
||||
@@ -5,7 +5,9 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
||||
setup_test_secrets("secrets" => "MYSQL_ROOT_PASSWORD=secret123")
|
||||
|
||||
@config = {
|
||||
service: "app", image: "dhh/app", registry: { "server" => "private.registry", "username" => "dhh", "password" => "secret" },
|
||||
service: "app",
|
||||
image: "dhh/app",
|
||||
registry: { "server" => "private.registry", "username" => "dhh", "password" => "secret" },
|
||||
servers: [ "1.1.1.1" ],
|
||||
builder: { "arch" => "amd64" },
|
||||
accessories: {
|
||||
@@ -39,6 +41,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
||||
"busybox" => {
|
||||
"service" => "custom-busybox",
|
||||
"image" => "busybox:latest",
|
||||
"registry" => { "server" => "other.registry", "username" => "user", "password" => "pw" },
|
||||
"host" => "1.1.1.7",
|
||||
"proxy" => {
|
||||
"host" => "busybox.example.com"
|
||||
@@ -62,7 +65,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
||||
new_command(:redis).run.join(" ")
|
||||
|
||||
assert_equal \
|
||||
"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",
|
||||
"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\" other.registry/busybox:latest",
|
||||
new_command(:busybox).run.join(" ")
|
||||
end
|
||||
|
||||
@@ -70,7 +73,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 --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",
|
||||
"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\" other.registry/busybox:latest",
|
||||
new_command(:busybox).run.join(" ")
|
||||
end
|
||||
|
||||
@@ -100,7 +103,6 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
||||
new_command(:mysql).info.join(" ")
|
||||
end
|
||||
|
||||
|
||||
test "execute in new container" do
|
||||
assert_equal \
|
||||
"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",
|
||||
@@ -127,8 +129,6 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
test "logs" do
|
||||
assert_equal \
|
||||
"docker logs app-mysql --timestamps 2>&1",
|
||||
|
||||
@@ -157,6 +157,12 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
new_command.logs.join(" ")
|
||||
end
|
||||
|
||||
test "logs with container_id" do
|
||||
assert_equal \
|
||||
"echo C137 | xargs docker logs --timestamps 2>&1",
|
||||
new_command.logs(container_id: "C137").join(" ")
|
||||
end
|
||||
|
||||
test "logs with since" do
|
||||
assert_equal \
|
||||
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --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=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1",
|
||||
@@ -208,6 +214,10 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
"ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --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=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1 | grep \"Completed\"'",
|
||||
new_command.follow_logs(host: "app-1", grep: "Completed")
|
||||
|
||||
assert_equal \
|
||||
"ssh -t root@app-1 -p 22 'echo ID321 | xargs docker logs --timestamps --follow 2>&1'",
|
||||
new_command.follow_logs(host: "app-1", container_id: "ID321")
|
||||
|
||||
assert_equal \
|
||||
"ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --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=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --tail 123 --follow 2>&1'",
|
||||
new_command.follow_logs(host: "app-1", lines: 123)
|
||||
@@ -224,29 +234,43 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
|
||||
test "execute in new container" do
|
||||
assert_equal \
|
||||
"docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails db:setup",
|
||||
"docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" 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 logging" do
|
||||
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
|
||||
|
||||
assert_equal \
|
||||
"docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" 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 --network kamal --env-file .kamal/apps/app/env/roles/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\" --log-opt max-size=\"10m\" dhh/app:999 bin/rails db:setup",
|
||||
new_command.execute_in_new_container("bin/rails", "db:setup", env: { "foo" => "bar" }).join(" ")
|
||||
end
|
||||
|
||||
test "execute in new detached container" do
|
||||
assert_equal \
|
||||
"docker run --detach --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:999 bin/rails db:setup",
|
||||
new_command.execute_in_new_container("bin/rails", "db:setup", detach: true, env: {}).join(" ")
|
||||
end
|
||||
|
||||
test "execute in new container with tags" do
|
||||
@config[:servers] = [ { "1.1.1.1" => "tag1" } ]
|
||||
@config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } }
|
||||
|
||||
assert_equal \
|
||||
"docker run --rm --network kamal --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/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 --log-opt max-size=\"10m\" 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 --network kamal --env-file .kamal/apps/app/env/roles/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 --log-opt max-size=\"10m\" --mount \"somewhere\" --cap-add dhh/app:999 bin/rails db:setup",
|
||||
new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ")
|
||||
end
|
||||
|
||||
@@ -263,7 +287,7 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
test "execute in new container over ssh" do
|
||||
assert_match %r{docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/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 --log-opt max-size="10m" dhh/app:999 bin/rails c},
|
||||
new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
|
||||
end
|
||||
|
||||
@@ -271,13 +295,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 --network kamal --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/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 --log-opt max-size=\"10m\" 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 --network kamal --env-file .kamal/apps/app/env/roles/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 --log-opt max-size=\"10m\" --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c},
|
||||
new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
|
||||
end
|
||||
|
||||
@@ -315,6 +339,16 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
assert_equal "ssh -J root@2.2.2.2 -t app@1.1.1.1 -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
|
||||
end
|
||||
|
||||
test "run over ssh with keys config" do
|
||||
@config[:ssh] = { "keys" => [ "path_to_key.pem" ] }
|
||||
assert_equal "ssh -i path_to_key.pem -t root@1.1.1.1 -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
|
||||
end
|
||||
|
||||
test "run over ssh with keys config with keys_only" do
|
||||
@config[:ssh] = { "keys" => [ "path_to_key.pem" ], "keys_only" => true }
|
||||
assert_equal "ssh -i path_to_key.pem -o IdentitiesOnly=yes -t root@1.1.1.1 -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
|
||||
end
|
||||
|
||||
test "run over ssh with proxy_command" do
|
||||
@config[:ssh] = { "proxy_command" => "ssh -W %h:%p user@proxy-server" }
|
||||
assert_equal "ssh -o ProxyCommand='ssh -W %h:%p user@proxy-server' -t root@1.1.1.1 -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
|
||||
@@ -435,10 +469,10 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
test "extract assets" do
|
||||
assert_equal [
|
||||
: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/apps/app/assets/extracted/web-999", "&&",
|
||||
:docker, :stop, "-t 1", "app-web-assets"
|
||||
:docker, :container, :rm, "app-web-assets", "2> /dev/null", "|| true", "&&",
|
||||
:docker, :container, :create, "--name", "app-web-assets", "dhh/app:999", "&&",
|
||||
:docker, :container, :cp, "-L", "app-web-assets:/public/assets/.", ".kamal/apps/app/assets/extracted/web-999", "&&",
|
||||
:docker, :container, :rm, "app-web-assets"
|
||||
], new_command(asset_path: "/public/assets").extract_assets
|
||||
end
|
||||
|
||||
|
||||
@@ -87,6 +87,14 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
||||
end
|
||||
end
|
||||
|
||||
test "cloud builder" do
|
||||
builder = new_builder_command(builder: { "arch" => [ "#{local_arch}" ], "driver" => "cloud docker-org-name/builder-name" })
|
||||
assert_equal "cloud", builder.name
|
||||
assert_equal \
|
||||
"docker buildx build --push --platform linux/#{local_arch} --builder cloud-docker-org-name-builder-name -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile .",
|
||||
builder.push.join(" ")
|
||||
end
|
||||
|
||||
test "build args" do
|
||||
builder = new_builder_command(builder: { "args" => { "a" => 1, "b" => 2 } })
|
||||
assert_equal \
|
||||
|
||||
@@ -2,14 +2,27 @@ require "test_helper"
|
||||
|
||||
class CommandsRegistryTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@config = { service: "app",
|
||||
@config = {
|
||||
service: "app",
|
||||
image: "dhh/app",
|
||||
registry: { "username" => "dhh",
|
||||
registry: {
|
||||
"username" => "dhh",
|
||||
"password" => "secret",
|
||||
"server" => "hub.docker.com"
|
||||
},
|
||||
builder: { "arch" => "amd64" },
|
||||
servers: [ "1.1.1.1" ]
|
||||
servers: [ "1.1.1.1" ],
|
||||
accessories: {
|
||||
"db" => {
|
||||
"image" => "mysql:8.0",
|
||||
"hosts" => [ "1.1.1.1" ],
|
||||
"registry" => {
|
||||
"username" => "user",
|
||||
"password" => "pw",
|
||||
"server" => "other.hub.docker.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
@@ -19,13 +32,24 @@ class CommandsRegistryTest < ActiveSupport::TestCase
|
||||
registry.login.join(" ")
|
||||
end
|
||||
|
||||
test "given registry login" do
|
||||
assert_equal \
|
||||
"docker login other.hub.docker.com -u \"user\" -p \"pw\"",
|
||||
registry.login(registry_config: accessory_registry_config).join(" ")
|
||||
end
|
||||
|
||||
test "registry login with ENV password" do
|
||||
with_test_secrets("secrets" => "KAMAL_REGISTRY_PASSWORD=more-secret") do
|
||||
with_test_secrets("secrets" => "KAMAL_REGISTRY_PASSWORD=more-secret\nKAMAL_MYSQL_REGISTRY_PASSWORD=secret-pw") do
|
||||
@config[:registry]["password"] = [ "KAMAL_REGISTRY_PASSWORD" ]
|
||||
@config[:accessories]["db"]["registry"]["password"] = [ "KAMAL_MYSQL_REGISTRY_PASSWORD" ]
|
||||
|
||||
assert_equal \
|
||||
"docker login hub.docker.com -u \"dhh\" -p \"more-secret\"",
|
||||
registry.login.join(" ")
|
||||
|
||||
assert_equal \
|
||||
"docker login other.hub.docker.com -u \"user\" -p \"secret-pw\"",
|
||||
registry.login(registry_config: accessory_registry_config).join(" ")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -55,8 +79,22 @@ class CommandsRegistryTest < ActiveSupport::TestCase
|
||||
registry.logout.join(" ")
|
||||
end
|
||||
|
||||
test "given registry logout" do
|
||||
assert_equal \
|
||||
"docker logout other.hub.docker.com",
|
||||
registry.logout(registry_config: accessory_registry_config).join(" ")
|
||||
end
|
||||
|
||||
private
|
||||
def registry
|
||||
Kamal::Commands::Registry.new Kamal::Configuration.new(@config)
|
||||
Kamal::Commands::Registry.new main_config
|
||||
end
|
||||
|
||||
def main_config
|
||||
Kamal::Configuration.new(@config)
|
||||
end
|
||||
|
||||
def accessory_registry_config
|
||||
main_config.accessory("db").registry
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,7 +3,9 @@ require "test_helper"
|
||||
class ConfigurationAccessoryTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@deploy = {
|
||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" },
|
||||
service: "app",
|
||||
image: "dhh/app",
|
||||
registry: { "username" => "dhh", "password" => "secret" },
|
||||
servers: {
|
||||
"web" => [ "1.1.1.1", "1.1.1.2" ],
|
||||
"workers" => [ "1.1.1.3", "1.1.1.4" ]
|
||||
@@ -12,7 +14,7 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
|
||||
env: { "REDIS_URL" => "redis://x/y" },
|
||||
accessories: {
|
||||
"mysql" => {
|
||||
"image" => "mysql:8.0",
|
||||
"image" => "public.registry/mysql:8.0",
|
||||
"host" => "1.1.1.5",
|
||||
"port" => "3306",
|
||||
"env" => {
|
||||
@@ -52,6 +54,7 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
|
||||
"monitoring" => {
|
||||
"service" => "custom-monitoring",
|
||||
"image" => "monitoring:latest",
|
||||
"registry" => { "server" => "other.registry", "username" => "user", "password" => "pw" },
|
||||
"roles" => [ "web" ],
|
||||
"port" => "4321:4321",
|
||||
"labels" => {
|
||||
@@ -80,6 +83,21 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
|
||||
assert_equal "custom-monitoring", @config.accessory(:monitoring).service_name
|
||||
end
|
||||
|
||||
test "image" do
|
||||
assert_equal "public.registry/mysql:8.0", @config.accessory(:mysql).image
|
||||
assert_equal "redis:latest", @config.accessory(:redis).image
|
||||
assert_equal "other.registry/monitoring:latest", @config.accessory(:monitoring).image
|
||||
end
|
||||
|
||||
test "registry" do
|
||||
assert_nil @config.accessory(:mysql).registry
|
||||
assert_nil @config.accessory(:redis).registry
|
||||
monitoring_registry = @config.accessory(:monitoring).registry
|
||||
assert_equal "other.registry", monitoring_registry.server
|
||||
assert_equal "user", monitoring_registry.username
|
||||
assert_equal "pw", monitoring_registry.password
|
||||
end
|
||||
|
||||
test "port" do
|
||||
assert_equal "3306:3306", @config.accessory(:mysql).port
|
||||
assert_equal "6379:6379", @config.accessory(:redis).port
|
||||
|
||||
12
test/fixtures/deploy.elsewhere.yml
vendored
Normal file
12
test/fixtures/deploy.elsewhere.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
service: app3
|
||||
image: dhh/app3
|
||||
servers:
|
||||
- "1.1.1.3"
|
||||
- "1.1.1.4"
|
||||
registry:
|
||||
username: user
|
||||
password: pw
|
||||
builder:
|
||||
arch: amd64
|
||||
aliases:
|
||||
other_config: config -c config/deploy2.yml
|
||||
13
test/fixtures/deploy.yml
vendored
Normal file
13
test/fixtures/deploy.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
service: app
|
||||
image: dhh/app
|
||||
servers:
|
||||
- "1.1.1.1"
|
||||
- "1.1.1.2"
|
||||
registry:
|
||||
username: user
|
||||
password: pw
|
||||
builder:
|
||||
arch: amd64
|
||||
aliases:
|
||||
other_config: config -c config/deploy2.yml
|
||||
other_destination_config: config -d elsewhere
|
||||
12
test/fixtures/deploy2.yml
vendored
Normal file
12
test/fixtures/deploy2.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
service: app2
|
||||
image: dhh/app2
|
||||
servers:
|
||||
- "1.1.1.1"
|
||||
- "1.1.1.2"
|
||||
registry:
|
||||
username: user2
|
||||
password: pw2
|
||||
builder:
|
||||
arch: amd64
|
||||
aliases:
|
||||
other_config: config -c config/deploy2.yml
|
||||
47
test/fixtures/deploy_with_accessories_with_different_registries.yml
vendored
Normal file
47
test/fixtures/deploy_with_accessories_with_different_registries.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
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:
|
||||
server: private.registry
|
||||
username: user
|
||||
password: pw
|
||||
builder:
|
||||
arch: amd64
|
||||
|
||||
accessories:
|
||||
mysql:
|
||||
image: private.registry/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
|
||||
busybox:
|
||||
service: custom-box
|
||||
image: busybox:latest
|
||||
host: 1.1.1.3
|
||||
registry:
|
||||
server: other.registry
|
||||
username: other_user
|
||||
password: other_pw
|
||||
|
||||
readiness_delay: 0
|
||||
3
test/fixtures/deploy_with_aliases.yml
vendored
3
test/fixtures/deploy_with_aliases.yml
vendored
@@ -21,3 +21,6 @@ aliases:
|
||||
console: app exec --reuse -p -r console "bin/console"
|
||||
exec: app exec --reuse -p -r console
|
||||
rails: app exec --reuse -p -r console rails
|
||||
primary_details: details -p
|
||||
deploy_secondary: deploy -d secondary
|
||||
|
||||
|
||||
40
test/fixtures/deploy_with_cloud_builder.yml
vendored
Normal file
40
test/fixtures/deploy_with_cloud_builder.yml
vendored
Normal 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
|
||||
|
||||
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
|
||||
|
||||
builder:
|
||||
arch: <%= Kamal::Utils.docker_arch == "arm64" ? "amd64" : "arm64" %>
|
||||
driver: cloud example_org/cloud_builder
|
||||
@@ -90,9 +90,9 @@ class MainTest < IntegrationTest
|
||||
test "setup and remove" do
|
||||
@app = "app_with_roles"
|
||||
|
||||
kamal :proxy, :set_config,
|
||||
kamal :proxy, :boot_config, "set",
|
||||
"--publish=false",
|
||||
"--options=label=traefik.http.services.kamal_proxy.loadbalancer.server.scheme=http",
|
||||
"--docker-options=label=traefik.http.services.kamal_proxy.loadbalancer.server.scheme=http",
|
||||
"label=traefik.http.routers.kamal_proxy.rule=PathPrefix\\\(\\\`/\\\`\\\)",
|
||||
"label=traefik.http.routers.kamal_proxy.priority=2"
|
||||
|
||||
|
||||
@@ -1,6 +1,35 @@
|
||||
require "test_helper"
|
||||
|
||||
class AwsSecretsManagerAdapterTest < SecretAdapterTestCase
|
||||
test "fails when errors are present" do
|
||||
stub_ticks.with("aws --version 2> /dev/null")
|
||||
stub_ticks
|
||||
.with("aws secretsmanager batch-get-secret-value --secret-id-list unknown1 unknown2 --profile default")
|
||||
.returns(<<~JSON)
|
||||
{
|
||||
"SecretValues": [],
|
||||
"Errors": [
|
||||
{
|
||||
"SecretId": "unknown1",
|
||||
"ErrorCode": "ResourceNotFoundException",
|
||||
"Message": "Secrets Manager can't find the specified secret."
|
||||
},
|
||||
{
|
||||
"SecretId": "unknown2",
|
||||
"ErrorCode": "ResourceNotFoundException",
|
||||
"Message": "Secrets Manager can't find the specified secret."
|
||||
}
|
||||
]
|
||||
}
|
||||
JSON
|
||||
|
||||
error = assert_raises RuntimeError do
|
||||
JSON.parse(shellunescape(run_command("fetch", "unknown1", "unknown2")))
|
||||
end
|
||||
|
||||
assert_equal [ "unknown1: Secrets Manager can't find the specified secret.", "unknown2: Secrets Manager can't find the specified secret." ].join(" "), error.message
|
||||
end
|
||||
|
||||
test "fetch" do
|
||||
stub_ticks.with("aws --version 2> /dev/null")
|
||||
stub_ticks
|
||||
@@ -44,6 +73,48 @@ class AwsSecretsManagerAdapterTest < SecretAdapterTestCase
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch with string value" do
|
||||
stub_ticks.with("aws --version 2> /dev/null")
|
||||
stub_ticks
|
||||
.with("aws secretsmanager batch-get-secret-value --secret-id-list secret secret2/KEY1 --profile default")
|
||||
.returns(<<~JSON)
|
||||
{
|
||||
"SecretValues": [
|
||||
{
|
||||
"ARN": "arn:aws:secretsmanager:us-east-1:aaaaaaaaaaaa:secret:secret",
|
||||
"Name": "secret",
|
||||
"VersionId": "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv",
|
||||
"SecretString": "a-string-secret",
|
||||
"VersionStages": [
|
||||
"AWSCURRENT"
|
||||
],
|
||||
"CreatedDate": "2024-01-01T00:00:00.000000"
|
||||
},
|
||||
{
|
||||
"ARN": "arn:aws:secretsmanager:us-east-1:aaaaaaaaaaaa:secret:secret2",
|
||||
"Name": "secret2",
|
||||
"VersionId": "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv",
|
||||
"SecretString": "{\\"KEY2\\":\\"VALUE2\\"}",
|
||||
"VersionStages": [
|
||||
"AWSCURRENT"
|
||||
],
|
||||
"CreatedDate": "2024-01-01T00:00:00.000000"
|
||||
}
|
||||
],
|
||||
"Errors": []
|
||||
}
|
||||
JSON
|
||||
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "secret", "secret2/KEY1")))
|
||||
|
||||
expected_json = {
|
||||
"secret"=>"a-string-secret",
|
||||
"secret2/KEY2"=>"VALUE2"
|
||||
}
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch with secret names" do
|
||||
stub_ticks.with("aws --version 2> /dev/null")
|
||||
stub_ticks
|
||||
|
||||
119
test/secrets/bitwarden_secrets_manager_adapter_test.rb
Normal file
119
test/secrets/bitwarden_secrets_manager_adapter_test.rb
Normal file
@@ -0,0 +1,119 @@
|
||||
require "test_helper"
|
||||
|
||||
class BitwardenSecretsManagerAdapterTest < SecretAdapterTestCase
|
||||
test "fetch with no parameters" do
|
||||
stub_ticks.with("bws --version 2> /dev/null")
|
||||
stub_login
|
||||
|
||||
error = assert_raises RuntimeError do
|
||||
(shellunescape(run_command("fetch")))
|
||||
end
|
||||
assert_equal("You must specify what to retrieve from Bitwarden Secrets Manager", error.message)
|
||||
end
|
||||
|
||||
test "fetch all" do
|
||||
stub_ticks.with("bws --version 2> /dev/null")
|
||||
stub_login
|
||||
stub_ticks
|
||||
.with("bws secret list -o env")
|
||||
.returns("KAMAL_REGISTRY_PASSWORD=\"some_password\"\nMY_OTHER_SECRET=\"my=weird\"secret\"")
|
||||
|
||||
expected = '{"KAMAL_REGISTRY_PASSWORD":"some_password","MY_OTHER_SECRET":"my\=weird\"secret"}'
|
||||
actual = shellunescape(run_command("fetch", "all"))
|
||||
assert_equal expected, actual
|
||||
end
|
||||
|
||||
test "fetch all with from" do
|
||||
stub_ticks.with("bws --version 2> /dev/null")
|
||||
stub_login
|
||||
stub_ticks
|
||||
.with("bws secret list -o env 82aeb5bd-6958-4a89-8197-eacab758acce")
|
||||
.returns("KAMAL_REGISTRY_PASSWORD=\"some_password\"\nMY_OTHER_SECRET=\"my=weird\"secret\"")
|
||||
|
||||
expected = '{"KAMAL_REGISTRY_PASSWORD":"some_password","MY_OTHER_SECRET":"my\=weird\"secret"}'
|
||||
actual = shellunescape(run_command("fetch", "all", "--from", "82aeb5bd-6958-4a89-8197-eacab758acce"))
|
||||
assert_equal expected, actual
|
||||
end
|
||||
|
||||
test "fetch item" do
|
||||
stub_ticks.with("bws --version 2> /dev/null")
|
||||
stub_login
|
||||
stub_ticks
|
||||
.with("bws secret get -o env 82aeb5bd-6958-4a89-8197-eacab758acce")
|
||||
.returns("KAMAL_REGISTRY_PASSWORD=\"some_password\"")
|
||||
|
||||
expected = '{"KAMAL_REGISTRY_PASSWORD":"some_password"}'
|
||||
actual = shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce"))
|
||||
assert_equal expected, actual
|
||||
end
|
||||
|
||||
test "fetch with multiple items" do
|
||||
stub_ticks.with("bws --version 2> /dev/null")
|
||||
stub_login
|
||||
stub_ticks
|
||||
.with("bws secret get -o env 82aeb5bd-6958-4a89-8197-eacab758acce")
|
||||
.returns("KAMAL_REGISTRY_PASSWORD=\"some_password\"")
|
||||
stub_ticks
|
||||
.with("bws secret get -o env 6f8cdf27-de2b-4c77-a35d-07df8050e332")
|
||||
.returns("MY_OTHER_SECRET=\"my=weird\"secret\"")
|
||||
|
||||
expected = '{"KAMAL_REGISTRY_PASSWORD":"some_password","MY_OTHER_SECRET":"my\=weird\"secret"}'
|
||||
actual = shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce", "6f8cdf27-de2b-4c77-a35d-07df8050e332"))
|
||||
assert_equal expected, actual
|
||||
end
|
||||
|
||||
test "fetch all empty" do
|
||||
stub_ticks.with("bws --version 2> /dev/null")
|
||||
stub_login
|
||||
stub_ticks_with("bws secret list -o env", succeed: false).returns("Error:\n0: Received error message from server")
|
||||
|
||||
error = assert_raises RuntimeError do
|
||||
(shellunescape(run_command("fetch", "all")))
|
||||
end
|
||||
assert_equal("Could not read secrets from Bitwarden Secrets Manager", error.message)
|
||||
end
|
||||
|
||||
test "fetch nonexistent item" do
|
||||
stub_ticks.with("bws --version 2> /dev/null")
|
||||
stub_login
|
||||
stub_ticks_with("bws secret get -o env 82aeb5bd-6958-4a89-8197-eacab758acce", succeed: false)
|
||||
.returns("ERROR (RuntimeError): Could not read 82aeb5bd-6958-4a89-8197-eacab758acce from Bitwarden Secrets Manager")
|
||||
|
||||
error = assert_raises RuntimeError do
|
||||
(shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce")))
|
||||
end
|
||||
assert_equal("Could not read 82aeb5bd-6958-4a89-8197-eacab758acce from Bitwarden Secrets Manager", error.message)
|
||||
end
|
||||
|
||||
test "fetch with no access token" do
|
||||
stub_ticks.with("bws --version 2> /dev/null")
|
||||
stub_ticks_with("bws run 'echo OK'", succeed: false)
|
||||
|
||||
error = assert_raises RuntimeError do
|
||||
(shellunescape(run_command("fetch", "all")))
|
||||
end
|
||||
assert_equal("Could not authenticate to Bitwarden Secrets Manager. Did you set a valid access token?", error.message)
|
||||
end
|
||||
|
||||
test "fetch without CLI installed" do
|
||||
stub_ticks_with("bws --version 2> /dev/null", succeed: false)
|
||||
|
||||
error = assert_raises RuntimeError do
|
||||
shellunescape(run_command("fetch"))
|
||||
end
|
||||
assert_equal "Bitwarden Secrets Manager CLI is not installed", error.message
|
||||
end
|
||||
|
||||
private
|
||||
def stub_login
|
||||
stub_ticks.with("bws run 'echo OK'").returns("OK")
|
||||
end
|
||||
|
||||
def run_command(*command)
|
||||
stdouted do
|
||||
Kamal::Cli::Secrets.start \
|
||||
[ *command,
|
||||
"--adapter", "bitwarden-sm" ]
|
||||
end
|
||||
end
|
||||
end
|
||||
81
test/secrets/enpass_adapter_test.rb
Normal file
81
test/secrets/enpass_adapter_test.rb
Normal file
@@ -0,0 +1,81 @@
|
||||
require "test_helper"
|
||||
|
||||
class EnpassAdapterTest < SecretAdapterTestCase
|
||||
test "fetch without CLI installed" do
|
||||
stub_ticks_with("enpass-cli version 2> /dev/null", succeed: false)
|
||||
|
||||
error = assert_raises RuntimeError do
|
||||
JSON.parse(shellunescape(run_command("fetch", "mynote")))
|
||||
end
|
||||
|
||||
assert_equal "Enpass CLI is not installed", error.message
|
||||
end
|
||||
|
||||
test "fetch one item" do
|
||||
stub_ticks_with("enpass-cli version 2> /dev/null")
|
||||
|
||||
stub_ticks
|
||||
.with("enpass-cli -json -vault vault-path show FooBar")
|
||||
.returns(<<~JSON)
|
||||
[{"category":"computer","label":"SECRET_1","login":"","password":"my-password-1","title":"FooBar","type":"password"}]
|
||||
JSON
|
||||
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "FooBar/SECRET_1")))
|
||||
|
||||
expected_json = { "FooBar/SECRET_1" => "my-password-1" }
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch multiple items" do
|
||||
stub_ticks_with("enpass-cli version 2> /dev/null")
|
||||
|
||||
stub_ticks
|
||||
.with("enpass-cli -json -vault vault-path show FooBar")
|
||||
.returns(<<~JSON)
|
||||
[
|
||||
{"category":"computer","label":"SECRET_1","login":"","password":"my-password-1","title":"FooBar","type":"password"},
|
||||
{"category":"computer","label":"SECRET_2","login":"","password":"my-password-2","title":"FooBar","type":"password"},
|
||||
{"category":"computer","label":"SECRET_3","login":"","password":"my-password-1","title":"Hello","type":"password"}
|
||||
]
|
||||
JSON
|
||||
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "FooBar/SECRET_1", "FooBar/SECRET_2")))
|
||||
|
||||
expected_json = { "FooBar/SECRET_1" => "my-password-1", "FooBar/SECRET_2" => "my-password-2" }
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch all with from" do
|
||||
stub_ticks_with("enpass-cli version 2> /dev/null")
|
||||
|
||||
stub_ticks
|
||||
.with("enpass-cli -json -vault vault-path show FooBar")
|
||||
.returns(<<~JSON)
|
||||
[
|
||||
{"category":"computer","label":"SECRET_1","login":"","password":"my-password-1","title":"FooBar","type":"password"},
|
||||
{"category":"computer","label":"SECRET_2","login":"","password":"my-password-2","title":"FooBar","type":"password"},
|
||||
{"category":"computer","label":"SECRET_3","login":"","password":"my-password-1","title":"Hello","type":"password"},
|
||||
{"category":"computer","label":"","login":"","password":"my-password-3","title":"FooBar","type":"password"}
|
||||
]
|
||||
JSON
|
||||
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "FooBar")))
|
||||
|
||||
expected_json = { "FooBar/SECRET_1" => "my-password-1", "FooBar/SECRET_2" => "my-password-2", "FooBar" => "my-password-3" }
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
private
|
||||
def run_command(*command)
|
||||
stdouted do
|
||||
Kamal::Cli::Secrets.start \
|
||||
[ *command,
|
||||
"-c", "test/fixtures/deploy_with_accessories.yml",
|
||||
"--adapter", "enpass",
|
||||
"--from", "vault-path" ]
|
||||
end
|
||||
end
|
||||
end
|
||||
220
test/secrets/gcp_secret_manager_adapter_test.rb
Normal file
220
test/secrets/gcp_secret_manager_adapter_test.rb
Normal file
@@ -0,0 +1,220 @@
|
||||
require "test_helper"
|
||||
|
||||
class GcpSecretManagerAdapterTest < SecretAdapterTestCase
|
||||
test "fetch" do
|
||||
stub_gcloud_version
|
||||
stub_authenticated
|
||||
stub_mypassword
|
||||
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "mypassword")))
|
||||
|
||||
expected_json = { "default/mypassword"=>"secret123" }
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch unauthenticated" do
|
||||
stub_ticks.with("gcloud --version 2> /dev/null")
|
||||
|
||||
stub_mypassword
|
||||
stub_unauthenticated
|
||||
|
||||
error = assert_raises RuntimeError do
|
||||
JSON.parse(shellunescape(run_command("fetch", "mypassword")))
|
||||
end
|
||||
|
||||
assert_match(/could not login to gcloud/, error.message)
|
||||
end
|
||||
|
||||
test "fetch with from" do
|
||||
stub_gcloud_version
|
||||
stub_authenticated
|
||||
stub_items(0, project: "other-project")
|
||||
stub_items(1, project: "other-project")
|
||||
stub_items(2, project: "other-project")
|
||||
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "--from", "other-project", "item1", "item2", "item3")))
|
||||
|
||||
expected_json = {
|
||||
"other-project/item1"=>"secret1", "other-project/item2"=>"secret2", "other-project/item3"=>"secret3"
|
||||
}
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch with multiple projects" do
|
||||
stub_gcloud_version
|
||||
stub_authenticated
|
||||
stub_items(0, project: "some-project")
|
||||
stub_items(1, project: "project-confidence")
|
||||
stub_items(2, project: "manhattan-project")
|
||||
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "some-project/item1", "project-confidence/item2", "manhattan-project/item3")))
|
||||
|
||||
expected_json = {
|
||||
"some-project/item1"=>"secret1", "project-confidence/item2"=>"secret2", "manhattan-project/item3"=>"secret3"
|
||||
}
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch with specific version" do
|
||||
stub_gcloud_version
|
||||
stub_authenticated
|
||||
stub_items(0, project: "some-project", version: "123")
|
||||
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "some-project/item1/123")))
|
||||
|
||||
expected_json = {
|
||||
"some-project/item1"=>"secret1"
|
||||
}
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch with non-default account" do
|
||||
stub_gcloud_version
|
||||
stub_authenticated
|
||||
stub_items(0, project: "some-project", version: "123", account: "email@example.com")
|
||||
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "some-project/item1/123", account: "email@example.com")))
|
||||
|
||||
expected_json = {
|
||||
"some-project/item1"=>"secret1"
|
||||
}
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch with service account impersonation" do
|
||||
stub_gcloud_version
|
||||
stub_authenticated
|
||||
stub_items(0, project: "some-project", version: "123", impersonate_service_account: "service-user@example.com")
|
||||
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "some-project/item1/123", account: "default|service-user@example.com")))
|
||||
|
||||
expected_json = {
|
||||
"some-project/item1"=>"secret1"
|
||||
}
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch with delegation chain and specific user" do
|
||||
stub_gcloud_version
|
||||
stub_authenticated
|
||||
stub_items(0, project: "some-project", version: "123", account: "user@example.com", impersonate_service_account: "service-user@example.com,service-user2@example.com")
|
||||
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "some-project/item1/123", account: "user@example.com|service-user@example.com,service-user2@example.com")))
|
||||
|
||||
expected_json = {
|
||||
"some-project/item1"=>"secret1"
|
||||
}
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch with non-default account and service account impersonation" do
|
||||
stub_gcloud_version
|
||||
stub_authenticated
|
||||
stub_items(0, project: "some-project", version: "123", account: "email@example.com", impersonate_service_account: "service-user@example.com")
|
||||
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "some-project/item1/123", account: "email@example.com|service-user@example.com")))
|
||||
|
||||
expected_json = {
|
||||
"some-project/item1"=>"secret1"
|
||||
}
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch without CLI installed" do
|
||||
stub_gcloud_version(succeed: false)
|
||||
|
||||
error = assert_raises RuntimeError do
|
||||
JSON.parse(shellunescape(run_command("fetch", "item1")))
|
||||
end
|
||||
assert_equal "gcloud CLI is not installed", error.message
|
||||
end
|
||||
|
||||
private
|
||||
def run_command(*command, account: "default")
|
||||
stdouted do
|
||||
Kamal::Cli::Secrets.start \
|
||||
[ *command,
|
||||
"-c", "test/fixtures/deploy_with_accessories.yml",
|
||||
"--adapter", "gcp_secret_manager",
|
||||
"--account", account ]
|
||||
end
|
||||
end
|
||||
|
||||
def stub_gcloud_version(succeed: true)
|
||||
stub_ticks_with("gcloud --version 2> /dev/null", succeed: succeed)
|
||||
end
|
||||
|
||||
def stub_authenticated
|
||||
stub_ticks
|
||||
.with("gcloud auth list --format=json")
|
||||
.returns(<<~JSON)
|
||||
[
|
||||
{
|
||||
"account": "email@example.com",
|
||||
"status": "ACTIVE"
|
||||
}
|
||||
]
|
||||
JSON
|
||||
end
|
||||
|
||||
def stub_unauthenticated
|
||||
stub_ticks
|
||||
.with("gcloud auth list --format=json")
|
||||
.returns("[]")
|
||||
|
||||
stub_ticks
|
||||
.with("gcloud auth login")
|
||||
.returns(<<~JSON)
|
||||
{
|
||||
"expired": false,
|
||||
"valid": true
|
||||
}
|
||||
JSON
|
||||
end
|
||||
|
||||
def stub_mypassword
|
||||
stub_ticks
|
||||
.with("gcloud secrets versions access latest --secret=mypassword --format=json")
|
||||
.returns(<<~JSON)
|
||||
{
|
||||
"name": "projects/000000000/secrets/mypassword/versions/1",
|
||||
"payload": {
|
||||
"data": "c2VjcmV0MTIz",
|
||||
"dataCrc32c": "2522602764"
|
||||
}
|
||||
}
|
||||
JSON
|
||||
end
|
||||
|
||||
def stub_items(n, project: nil, account: nil, version: "latest", impersonate_service_account: nil)
|
||||
payloads = [
|
||||
{ data: "c2VjcmV0MQ==", checksum: 1846998209 },
|
||||
{ data: "c2VjcmV0Mg==", checksum: 2101741365 },
|
||||
{ data: "c2VjcmV0Mw==", checksum: 2402124854 }
|
||||
]
|
||||
stub_ticks
|
||||
.with("gcloud secrets versions access #{version} " \
|
||||
"--secret=item#{n + 1}" \
|
||||
"#{" --project=#{project}" if project}" \
|
||||
"#{" --account=#{account}" if account}" \
|
||||
"#{" --impersonate-service-account=#{impersonate_service_account}" if impersonate_service_account} " \
|
||||
"--format=json")
|
||||
.returns(<<~JSON)
|
||||
{
|
||||
"name": "projects/000000001/secrets/item1/versions/1",
|
||||
"payload": {
|
||||
"data": "#{payloads[n][:data]}",
|
||||
"dataCrc32c": "#{payloads[n][:checksum]}"
|
||||
}
|
||||
}
|
||||
JSON
|
||||
end
|
||||
end
|
||||
@@ -20,6 +20,20 @@ class SecretsTest < ActiveSupport::TestCase
|
||||
end
|
||||
end
|
||||
|
||||
test "env references" do
|
||||
with_test_secrets("secrets" => "SECRET1=$SECRET1") do
|
||||
ENV["SECRET1"] = "ABC"
|
||||
assert_equal "ABC", Kamal::Secrets.new["SECRET1"]
|
||||
end
|
||||
end
|
||||
|
||||
test "secrets file value overrides env" do
|
||||
with_test_secrets("secrets" => "SECRET1=DEF") do
|
||||
ENV["SECRET1"] = "ABC"
|
||||
assert_equal "DEF", Kamal::Secrets.new["SECRET1"]
|
||||
end
|
||||
end
|
||||
|
||||
test "destinations" do
|
||||
with_test_secrets("secrets.dest" => "SECRET=DEF", "secrets" => "SECRET=ABC", "secrets-common" => "SECRET=GHI\nSECRET2=JKL") do
|
||||
assert_equal "ABC", Kamal::Secrets.new["SECRET"]
|
||||
|
||||
Reference in New Issue
Block a user