From 94bf0906574edd6d73037aad8d8f6e79dc92b3f6 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 30 Aug 2023 15:16:48 +0100 Subject: [PATCH] Copy env files to remote hosts Setting env variables in the docker arguments requires having them on the deploy host. Instead we'll add two new commands `kamal env push` and `kamal env delete` which will manage copying the environment as .env files to the remote host. Docker will pick up the file with `--env-file `. Env files will be stored under `/env`. Running `kamal env push` will create env files for each role and accessory, and traefik if required. `kamal envify` has been updated to also push the env files. By avoiding using `kamal envify` and creating the local and remote secrets manually, you can now avoid accessing secrets needed for the docker runtime environment locally. You will still need build secrets. One thing to note - the Docker doesn't parse the environment variables in the env file, one result of this is that you can't specify multi-line values - see https://github.com/moby/moby/issues/12997. We maybe need to look docker config or docker secrets longer term to get around this. Hattip to @kevinmcconnell - this was all his idea. --- lib/kamal/cli/env.rb | 52 ++++++++++++++++ lib/kamal/cli/main.rb | 6 ++ lib/kamal/commander.rb | 4 ++ lib/kamal/commands/accessory.rb | 16 ++--- lib/kamal/commands/app.rb | 9 ++- lib/kamal/commands/base.rb | 8 +++ lib/kamal/commands/traefik.rb | 28 ++++++--- lib/kamal/configuration.rb | 18 ++---- lib/kamal/configuration/accessory.rb | 16 ++++- lib/kamal/configuration/role.rb | 16 ++++- lib/kamal/utils.rb | 34 +++++++--- test/cli/accessory_test.rb | 8 +-- test/cli/env_test.rb | 38 ++++++++++++ test/cli/healthcheck_test.rb | 4 +- test/cli/lock_test.rb | 10 +-- test/cli/main_test.rb | 6 +- test/cli/traefik_test.rb | 4 +- test/commands/accessory_test.rb | 20 ++++-- test/commands/app_test.rb | 36 ++++++----- test/commands/healthcheck_test.rb | 10 +-- test/commands/traefik_test.rb | 50 ++++++++++----- test/configuration/accessory_test.rb | 25 +++++--- test/configuration/role_test.rb | 55 ++++++++++++---- test/configuration_test.rb | 41 +----------- test/integration/accessory_test.rb | 4 ++ test/integration/app_test.rb | 2 + test/integration/docker/deployer/Dockerfile | 2 +- .../docker/deployer/app/config/deploy.yml | 6 ++ test/integration/lock_test.rb | 2 + test/integration/main_test.rb | 27 +++++--- test/integration/traefik_test.rb | 4 ++ test/utils_test.rb | 62 +++++++++++++++++-- 32 files changed, 453 insertions(+), 170 deletions(-) create mode 100644 lib/kamal/cli/env.rb create mode 100644 test/cli/env_test.rb diff --git a/lib/kamal/cli/env.rb b/lib/kamal/cli/env.rb new file mode 100644 index 000000000..7d4dac102 --- /dev/null +++ b/lib/kamal/cli/env.rb @@ -0,0 +1,52 @@ +require "tempfile" + +class Kamal::Cli::Env < Kamal::Cli::Base + desc "push", "Push the env file to the remote hosts" + def push + mutating do + on(KAMAL.hosts) do + KAMAL.roles_on(host).each do |role| + role_config = KAMAL.config.role(role) + execute *KAMAL.app(role: role).make_env_directory + upload! StringIO.new(role_config.env_file), role_config.host_env_file_path, mode: 400 + end + end + + on(KAMAL.traefik_hosts) do + execute *KAMAL.traefik.make_env_directory + upload! StringIO.new(KAMAL.traefik.env_file), KAMAL.traefik.host_env_file_path, mode: 400 + end + + on(KAMAL.accessory_hosts) do + KAMAL.accessories_on(host).each do |accessory| + accessory_config = KAMAL.config.accessory(accessory) + execute *KAMAL.accessory(accessory).make_env_directory + upload! StringIO.new(accessory_config.env_file), accessory_config.host_env_file_path, mode: 400 + end + end + end + end + + desc "delete", "Delete the env file from the remote hosts" + def delete + mutating do + on(KAMAL.hosts) do + KAMAL.roles_on(host).each do |role| + role_config = KAMAL.config.role(role) + execute *KAMAL.app(role: role).remove_env_file + end + end + + on(KAMAL.traefik_hosts) do + execute *KAMAL.traefik.remove_env_file + end + + on(KAMAL.accessory_hosts) do + KAMAL.accessories_on(host).each do |accessory| + accessory_config = KAMAL.config.accessory(accessory) + execute *KAMAL.accessory(accessory).remove_env_file + end + end + end + end +end diff --git a/lib/kamal/cli/main.rb b/lib/kamal/cli/main.rb index 29218d9d3..c41543574 100644 --- a/lib/kamal/cli/main.rb +++ b/lib/kamal/cli/main.rb @@ -175,6 +175,9 @@ def envify end File.write(env_path, ERB.new(File.read(env_template_path)).result, perm: 0600) + + load_envs # reload new file + invoke "kamal:cli:env:push", options end desc "remove", "Remove Traefik, app, accessories, and registry session from servers" @@ -204,6 +207,9 @@ def version desc "build", "Build application image" subcommand "build", Kamal::Cli::Build + desc "env", "Manage environment files" + subcommand "env", Kamal::Cli::Env + desc "healthcheck", "Healthcheck application" subcommand "healthcheck", Kamal::Cli::Healthcheck diff --git a/lib/kamal/commander.rb b/lib/kamal/commander.rb index 47e0cf9fd..a98ac2b54 100644 --- a/lib/kamal/commander.rb +++ b/lib/kamal/commander.rb @@ -75,6 +75,10 @@ def accessory_names config.accessories&.collect(&:name) || [] end + def accessories_on(host) + config.accessories.select { |accessory| accessory.hosts.include?(host.to_s) }.map(&:name) + end + def app(role: nil) Kamal::Commands::App.new(config, role: role) diff --git a/lib/kamal/commands/accessory.rb b/lib/kamal/commands/accessory.rb index 9252e2a27..bd70ac5c7 100644 --- a/lib/kamal/commands/accessory.rb +++ b/lib/kamal/commands/accessory.rb @@ -86,14 +86,6 @@ def ensure_local_file_present(local_file) end end - def make_directory_for(remote_file) - make_directory Pathname.new(remote_file).dirname.to_s - end - - def make_directory(path) - [ :mkdir, "-p", path ] - end - def remove_service_directory [ :rm, "-rf", service_name ] end @@ -106,6 +98,14 @@ def remove_image docker :image, :rm, "--force", image end + def make_env_directory + make_directory accessory_config.host_env_directory + end + + def remove_env_file + [:rm, "-f", accessory_config.host_env_file_path] + end + private def service_filter [ "--filter", "label=service=#{service_name}" ] diff --git a/lib/kamal/commands/app.rb b/lib/kamal/commands/app.rb index 11d15fcc3..cb1f7091c 100644 --- a/lib/kamal/commands/app.rb +++ b/lib/kamal/commands/app.rb @@ -81,7 +81,7 @@ def execute_in_new_container(*command, interactive: false) docker :run, ("-it" if interactive), "--rm", - *config.env_args, + *role&.env_args, *config.volume_args, *role&.option_args, config.absolute_image, @@ -149,6 +149,13 @@ def tag_current_as_latest docker :tag, config.absolute_image, config.latest_image end + def make_env_directory + make_directory config.role(role).host_env_directory + end + + def remove_env_file + [:rm, "-f", config.role(role).host_env_file_path] + end private def container_name(version = nil) diff --git a/lib/kamal/commands/base.rb b/lib/kamal/commands/base.rb index 8a413b917..3058df162 100644 --- a/lib/kamal/commands/base.rb +++ b/lib/kamal/commands/base.rb @@ -26,6 +26,14 @@ def container_id_for(container_name:, only_running: false) docker :container, :ls, *("--all" unless only_running), "--filter", "name=^#{container_name}$", "--quiet" end + def make_directory_for(remote_file) + make_directory Pathname.new(remote_file).dirname.to_s + end + + def make_directory(path) + [ :mkdir, "-p", path ] + end + private def combine(*commands, by: "&&") commands diff --git a/lib/kamal/commands/traefik.rb b/lib/kamal/commands/traefik.rb index e77a81de9..fbbcea0fd 100644 --- a/lib/kamal/commands/traefik.rb +++ b/lib/kamal/commands/traefik.rb @@ -1,5 +1,5 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base - delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils + delegate :argumentize, :env_file_with_secrets, :optionize, to: Kamal::Utils DEFAULT_IMAGE = "traefik:v2.9" CONTAINER_PORT = 80 @@ -63,6 +63,22 @@ def port "#{host_port}:#{CONTAINER_PORT}" end + def env_file + env_file_with_secrets config.traefik.fetch("env", {}) + end + + def host_env_file_path + File.join host_env_directory, "traefik.env" + end + + def make_env_directory + make_directory(host_env_directory) + end + + def remove_env_file + [:rm, "-f", host_env_file_path] + end + private def publish_args argumentize "--publish", port unless config.traefik["publish"] == false @@ -73,13 +89,11 @@ def label_args end def env_args - env_config = config.traefik["env"] || {} + argumentize "--env-file", host_env_file_path + end - if env_config.present? - argumentize_env_with_secrets(env_config) - else - [] - end + def host_env_directory + File.join config.host_env_directory, "traefik" end def labels diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index 2bb8002bd..8db892fc5 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -7,7 +7,7 @@ class Kamal::Configuration delegate :service, :image, :servers, :env, :labels, :registry, :stop_wait_time, :hooks_path, to: :raw_config, allow_nil: true - delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils + delegate :argumentize, :optionize, to: Kamal::Utils attr_accessor :destination attr_accessor :raw_config @@ -113,14 +113,6 @@ def service_with_version end - def env_args - if raw_config.env.present? - argumentize_env_with_secrets(raw_config.env) - else - [] - end - end - def volume_args if raw_config.volumes.present? argumentize "--volume", raw_config.volumes @@ -174,7 +166,6 @@ def to_h repository: repository, absolute_image: absolute_image, service_with_version: service_with_version, - env_args: env_args, volume_args: volume_args, ssh_options: ssh.to_h, sshkit: sshkit.to_h, @@ -199,12 +190,15 @@ def builder # Will raise KeyError if any secret ENVs are missing def ensure_env_available - env_args - roles.each(&:env_args) + roles.each(&:env_file) true end + def host_env_directory + "#{run_directory}/env" + end + private # Will raise ArgumentError if any required config keys are missing def ensure_required_keys_present diff --git a/lib/kamal/configuration/accessory.rb b/lib/kamal/configuration/accessory.rb index 4ffcfc4b2..aa5ccfbdc 100644 --- a/lib/kamal/configuration/accessory.rb +++ b/lib/kamal/configuration/accessory.rb @@ -1,5 +1,5 @@ class Kamal::Configuration::Accessory - delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils + delegate :argumentize, :env_file_with_secrets, :optionize, to: Kamal::Utils attr_accessor :name, :specifics @@ -45,8 +45,20 @@ def env specifics["env"] || {} end + def env_file + env_file_with_secrets env + end + + def host_env_directory + File.join config.host_env_directory, "accessories" + end + + def host_env_file_path + File.join host_env_directory, "#{service_name}.env" + end + def env_args - argumentize_env_with_secrets env + argumentize "--env-file", host_env_file_path end def files diff --git a/lib/kamal/configuration/role.rb b/lib/kamal/configuration/role.rb index 4302430dd..f549d459b 100644 --- a/lib/kamal/configuration/role.rb +++ b/lib/kamal/configuration/role.rb @@ -1,5 +1,5 @@ class Kamal::Configuration::Role - delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils + delegate :argumentize, :env_file_with_secrets, :optionize, to: Kamal::Utils attr_accessor :name @@ -31,8 +31,20 @@ def env end end + def env_file + env_file_with_secrets env + end + + def host_env_directory + File.join config.host_env_directory, "roles" + end + + def host_env_file_path + File.join host_env_directory, "#{[config.service, name, config.destination].compact.join("-")}.env" + end + def env_args - argumentize_env_with_secrets env + argumentize "--env-file", host_env_file_path end def health_check_args diff --git a/lib/kamal/utils.rb b/lib/kamal/utils.rb index c2461373b..6ab1648b1 100644 --- a/lib/kamal/utils.rb +++ b/lib/kamal/utils.rb @@ -16,14 +16,24 @@ def argumentize(argument, attributes, sensitive: false) end end - # Return a list of shell arguments using the same named argument against the passed attributes, - # but redacts and expands secrets. - def argumentize_env_with_secrets(env) - if (secrets = env["secret"]).present? - argumentize("-e", secrets.to_h { |key| [ key, ENV.fetch(key) ] }, sensitive: true) + argumentize("-e", env["clear"]) - else - argumentize "-e", env.fetch("clear", env) - end + def env_file_with_secrets(env) + env_file = StringIO.new.tap do |contents| + if (secrets = env["secret"]).present? + env.fetch("secret", env)&.each do |key| + contents << docker_env_file_line(key, ENV.fetch(key)) + end + env["clear"]&.each do |key, value| + contents << docker_env_file_line(key, value) + end + else + env.fetch("clear", env)&.each do |key, value| + contents << docker_env_file_line(key, value) + end + end + end.string + + # Ensure the file has some contents to avoid the SSHKIT empty file warning + env_file || "\n" end # Returns a list of shell-dashed option arguments. If the value is true, it's treated like a value-less option. @@ -97,4 +107,12 @@ def abbreviate_version(version) def uncommitted_changes `git status --porcelain`.strip end + + def docker_env_file_line(key, value) + if key.include?("\n") || value.to_s.include?("\n") + raise ArgumentError, "docker env file format does not support newlines in keys or values, key: #{key}" + end + + "#{key.to_s}=#{value.to_s}\n" + end end diff --git a/test/cli/accessory_test.rb b/test/cli/accessory_test.rb index 99dc6e7c1..5ecf80389 100644 --- a/test/cli/accessory_test.rb +++ b/test/cli/accessory_test.rb @@ -7,7 +7,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 -e [REDACTED] -e MYSQL_ROOT_HOST=\"%\" --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 --log-opt max-size=\"10m\" --publish 3306:3306 --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 end end @@ -21,9 +21,9 @@ 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 -e [REDACTED] -e MYSQL_ROOT_HOST=\"%\" --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 --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 --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest 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-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 end end diff --git a/test/cli/env_test.rb b/test/cli/env_test.rb new file mode 100644 index 000000000..4d72f558a --- /dev/null +++ b/test/cli/env_test.rb @@ -0,0 +1,38 @@ +require_relative "cli_test_case" + +class CliEnvTest < CliTestCase + test "push" do + run_command("push").tap do |output| + assert_match "Running /usr/bin/env mkdir -p .kamal/env/roles on 1.1.1.1", output + assert_match "Running /usr/bin/env mkdir -p .kamal/env/traefik on 1.1.1.1", output + assert_match "Running /usr/bin/env mkdir -p .kamal/env/accessories on 1.1.1.1", output + assert_match "Running /usr/bin/env mkdir -p .kamal/env/roles on 1.1.1.1", output + assert_match "Running /usr/bin/env mkdir -p .kamal/env/traefik on 1.1.1.2", output + assert_match "Running /usr/bin/env mkdir -p .kamal/env/accessories on 1.1.1.1", output + assert_match ".kamal/env/roles/app-web.env", output + assert_match ".kamal/env/roles/app-workers.env", output + assert_match ".kamal/env/traefik/traefik.env", output + assert_match ".kamal/env/accessories/app-redis.env", output + + end + end + + test "delete" do + run_command("delete").tap do |output| + assert_match "Running /usr/bin/env rm -f .kamal/env/roles/app-web.env on 1.1.1.1", output + assert_match "Running /usr/bin/env rm -f .kamal/env/roles/app-web.env on 1.1.1.2", output + assert_match "Running /usr/bin/env rm -f .kamal/env/roles/app-workers.env on 1.1.1.3", output + assert_match "Running /usr/bin/env rm -f .kamal/env/roles/app-workers.env on 1.1.1.4", output + assert_match "Running /usr/bin/env rm -f .kamal/env/traefik/traefik.env on 1.1.1.1", output + assert_match "Running /usr/bin/env rm -f .kamal/env/traefik/traefik.env on 1.1.1.2", output + assert_match "Running /usr/bin/env rm -f .kamal/env/accessories/app-redis.env on 1.1.1.1", output + assert_match "Running /usr/bin/env rm -f .kamal/env/accessories/app-redis.env on 1.1.1.2", output + assert_match "Running /usr/bin/env rm -f .kamal/env/accessories/app-mysql.env on 1.1.1.3", output + end + end + + private + def run_command(*command) + stdouted { Kamal::Cli::Env.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) } + end +end diff --git a/test/cli/healthcheck_test.rb b/test/cli/healthcheck_test.rb index 9cef1c8f3..f9c3aa9c0 100644 --- a/test/cli/healthcheck_test.rb +++ b/test/cli/healthcheck_test.rb @@ -10,7 +10,7 @@ class CliHealthcheckTest < CliTestCase SSHKit::Backend::Abstract.any_instance.stubs(:execute) .with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :stop, raise_on_non_zero_exit: false) SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with(:docker, :run, "--detach", "--name", "healthcheck-app-999", "--publish", "3999:3000", "--label", "service=healthcheck-app", "-e", "KAMAL_CONTAINER_NAME=\"healthcheck-app\"", "--health-cmd", "\"curl -f http://localhost:3000/up || exit 1\"", "--health-interval", "\"1s\"", "dhh/app:999") + .with(:docker, :run, "--detach", "--name", "healthcheck-app-999", "--publish", "3999:3000", "--label", "service=healthcheck-app", "-e", "KAMAL_CONTAINER_NAME=\"healthcheck-app\"", "--env-file", ".kamal/env/roles/app-web.env", "--health-cmd", "\"curl -f http://localhost:3000/up || exit 1\"", "--health-interval", "\"1s\"", "dhh/app:999") SSHKit::Backend::Abstract.any_instance.stubs(:execute) .with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :container, :rm, raise_on_non_zero_exit: false) @@ -39,7 +39,7 @@ class CliHealthcheckTest < CliTestCase SSHKit::Backend::Abstract.any_instance.stubs(:execute) .with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :stop, raise_on_non_zero_exit: false) SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with(:docker, :run, "--detach", "--name", "healthcheck-app-999", "--publish", "3999:3000", "--label", "service=healthcheck-app", "-e", "KAMAL_CONTAINER_NAME=\"healthcheck-app\"", "--health-cmd", "\"curl -f http://localhost:3000/up || exit 1\"", "--health-interval", "\"1s\"", "dhh/app:999") + .with(:docker, :run, "--detach", "--name", "healthcheck-app-999", "--publish", "3999:3000", "--label", "service=healthcheck-app", "-e", "KAMAL_CONTAINER_NAME=\"healthcheck-app\"", "--env-file", ".kamal/env/roles/app-web.env", "--health-cmd", "\"curl -f http://localhost:3000/up || exit 1\"", "--health-interval", "\"1s\"", "dhh/app:999") SSHKit::Backend::Abstract.any_instance.stubs(:execute) .with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :container, :rm, raise_on_non_zero_exit: false) diff --git a/test/cli/lock_test.rb b/test/cli/lock_test.rb index 9521480f0..8972bdc54 100644 --- a/test/cli/lock_test.rb +++ b/test/cli/lock_test.rb @@ -2,19 +2,19 @@ class CliLockTest < CliTestCase test "status" do - run_command("status") do |output| - assert_match "stat lock", output + run_command("status").tap do |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 test "release" do - run_command("release") do |output| - assert_match "rm -rf lock", output + run_command("release").tap do |output| + assert_match "Released the deploy lock", output end end private def run_command(*command) - stdouted { Kamal::Cli::Lock.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) } + stdouted { Kamal::Cli::Lock.start([*command, "-v", "-c", "test/fixtures/deploy_with_accessories.yml"]) } end end diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index 18b313744..04f115750 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -339,10 +339,10 @@ class CliMainTest < CliTestCase end test "envify with destination" do - File.expects(:read).with(".env.staging.erb").returns("HELLO=<%= 'world' %>") - File.expects(:write).with(".env.staging", "HELLO=world", perm: 0600) + File.expects(:read).with(".env.world.erb").returns("HELLO=<%= 'world' %>") + File.expects(:write).with(".env.world", "HELLO=world", perm: 0600) - run_command("envify", "-d", "staging") + run_command("envify", "-d", "world", config_file: "deploy_for_dest") end test "remove with confirmation" do diff --git a/test/cli/traefik_test.rb b/test/cli/traefik_test.rb index 0edaf1f61..1b19b0331 100644 --- a/test/cli/traefik_test.rb +++ b/test/cli/traefik_test.rb @@ -4,7 +4,7 @@ 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 --log-opt max-size=\"10m\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", 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\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output end end @@ -14,7 +14,7 @@ class CliTraefikTest < CliTestCase run_command("reboot").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 --log-opt max-size=\"10m\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", 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\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output end end diff --git a/test/commands/accessory_test.rb b/test/commands/accessory_test.rb index 89279ccc7..2825f3de0 100644 --- a/test/commands/accessory_test.rb +++ b/test/commands/accessory_test.rb @@ -49,15 +49,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 -e MYSQL_ROOT_PASSWORD=\"secret123\" -e MYSQL_ROOT_HOST=\"%\" --label service=\"app-mysql\" private.registry/mysql:8.0", + "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 --env-file .kamal/env/accessories/app-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 -e SOMETHING=\"else\" --volume /var/lib/redis:/data --label service=\"app-redis\" --label cache=\"true\" redis:latest", + "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 /var/lib/redis:/data --label service=\"app-redis\" --label cache=\"true\" redis:latest", new_command(:redis).run.join(" ") assert_equal \ - "docker run --name app-busybox --detach --restart unless-stopped --log-opt max-size=\"10m\" --label service=\"app-busybox\" busybox:latest", + "docker run --name app-busybox --detach --restart unless-stopped --log-opt max-size=\"10m\" --env-file .kamal/env/accessories/app-busybox.env --label service=\"app-busybox\" busybox:latest", new_command(:busybox).run.join(" ") end @@ -65,7 +65,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase @config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } } assert_equal \ - "docker run --name app-busybox --detach --restart unless-stopped --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app-busybox\" busybox:latest", + "docker run --name app-busybox --detach --restart unless-stopped --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --env-file .kamal/env/accessories/app-busybox.env --label service=\"app-busybox\" busybox:latest", new_command(:busybox).run.join(" ") end @@ -90,7 +90,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase test "execute in new container" do assert_equal \ - "docker run --rm -e MYSQL_ROOT_PASSWORD=\"secret123\" -e MYSQL_ROOT_HOST=\"%\" private.registry/mysql:8.0 mysql -u root", + "docker run --rm --env-file .kamal/env/accessories/app-mysql.env private.registry/mysql:8.0 mysql -u root", new_command(:mysql).execute_in_new_container("mysql", "-u", "root").join(" ") end @@ -102,7 +102,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 -e MYSQL_ROOT_PASSWORD=\"secret123\" -e MYSQL_ROOT_HOST=\"%\" private.registry/mysql:8.0 mysql -u root|, + assert_match %r|docker run -it --rm --env-file .kamal/env/accessories/app-mysql.env private.registry/mysql:8.0 mysql -u root|, new_command(:mysql).execute_in_new_container_over_ssh("mysql", "-u", "root") end end @@ -144,6 +144,14 @@ class CommandsAccessoryTest < ActiveSupport::TestCase new_command(:mysql).remove_image.join(" ") end + test "make_env_directory" do + assert_equal "mkdir -p .kamal/env/accessories", new_command(:mysql).make_env_directory.join(" ") + end + + test "remove_env_file" do + assert_equal "rm -f .kamal/env/accessories/app-mysql.env", new_command(:mysql).remove_env_file.join(" ") + end + private def new_command(accessory) Kamal::Commands::Accessory.new(Kamal::Configuration.new(@config), name: accessory) diff --git a/test/commands/app_test.rb b/test/commands/app_test.rb index 69060afe9..ab3f6ace8 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -13,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 RAILS_MASTER_KEY=\"456\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --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 -e KAMAL_CONTAINER_NAME=\"app-web-999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --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 hostname" do assert_equal \ - "docker run --detach --restart unless-stopped --name app-web-999 --hostname myhost -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --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 --hostname myhost -e KAMAL_CONTAINER_NAME=\"app-web-999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --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(hostname: "myhost").join(" ") end @@ -27,7 +27,7 @@ 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 RAILS_MASTER_KEY=\"456\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --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 -e KAMAL_CONTAINER_NAME=\"app-web-999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --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 @@ -35,7 +35,7 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:healthcheck] = { "path" => "/healthz" } assert_equal \ - "docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --health-cmd \"curl -f http://localhost:3000/healthz || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --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 -e KAMAL_CONTAINER_NAME=\"app-web-999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3000/healthz || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --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 @@ -43,7 +43,7 @@ class CommandsAppTest < ActiveSupport::TestCase @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 RAILS_MASTER_KEY=\"456\" --health-cmd \"/bin/up\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --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 -e KAMAL_CONTAINER_NAME=\"app-web-999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"/bin/up\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --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 @@ -51,14 +51,14 @@ class CommandsAppTest < ActiveSupport::TestCase @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 RAILS_MASTER_KEY=\"456\" --health-cmd \"/bin/healthy\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --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 -e KAMAL_CONTAINER_NAME=\"app-web-999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"/bin/healthy\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --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 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 RAILS_MASTER_KEY=\"456\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"jobs\" --mount \"somewhere\" --cap-add dhh/app:999 bin/jobs", + "docker run --detach --restart unless-stopped --name app-jobs-999 -e KAMAL_CONTAINER_NAME=\"app-jobs-999\" --env-file .kamal/env/roles/app-jobs.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"jobs\" --mount \"somewhere\" --cap-add dhh/app:999 bin/jobs", new_command(role: "jobs").run.join(" ") end @@ -66,7 +66,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 RAILS_MASTER_KEY=\"456\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --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 -e KAMAL_CONTAINER_NAME=\"app-web-999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --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 @@ -85,13 +85,13 @@ class CommandsAppTest < ActiveSupport::TestCase test "start_or_run" do assert_equal \ - "docker start app-web-999 || docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --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 start app-web-999 || docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --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.start_or_run.join(" ") end test "start_or_run with hostname" do assert_equal \ - "docker start app-web-999 || docker run --detach --restart unless-stopped --name app-web-999 --hostname myhost -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --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 start app-web-999 || docker run --detach --restart unless-stopped --name app-web-999 --hostname myhost -e KAMAL_CONTAINER_NAME=\"app-web-999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --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.start_or_run(hostname: "myhost").join(" ") end @@ -167,14 +167,14 @@ class CommandsAppTest < ActiveSupport::TestCase test "execute in new container" do assert_equal \ - "docker run --rm -e RAILS_MASTER_KEY=\"456\" dhh/app:999 bin/rails db:setup", + "docker run --rm --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails db:setup", new_command.execute_in_new_container("bin/rails", "db:setup").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 -e RAILS_MASTER_KEY=\"456\" --mount \"somewhere\" --cap-add dhh/app:999 bin/rails db:setup", + "docker run --rm --env-file .kamal/env/roles/app-web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails db:setup", new_command.execute_in_new_container("bin/rails", "db:setup").join(" ") end @@ -185,13 +185,13 @@ class CommandsAppTest < ActiveSupport::TestCase end test "execute in new container over ssh" do - assert_match %r|docker run -it --rm -e RAILS_MASTER_KEY=\"456\" dhh/app:999 bin/rails c|, + assert_match %r|docker run -it --rm --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails c|, new_command.execute_in_new_container_over_ssh("bin/rails", "c", host: "app-1") 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 -e RAILS_MASTER_KEY=\"456\" --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c|, + 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|, new_command.execute_in_new_container_over_ssh("bin/rails", "c", host: "app-1") end @@ -334,6 +334,14 @@ class CommandsAppTest < ActiveSupport::TestCase new_command.tag_current_as_latest.join(" ") end + test "make_env_directory" do + assert_equal "mkdir -p .kamal/env/roles", new_command.make_env_directory.join(" ") + end + + test "remove_env_file" do + assert_equal "rm -f .kamal/env/roles/app-web.env", new_command.remove_env_file.join(" ") + end + private def new_command(role: "web") Kamal::Commands::App.new(Kamal::Configuration.new(@config, destination: @destination, version: "999"), role: role) diff --git a/test/commands/healthcheck_test.rb b/test/commands/healthcheck_test.rb index 9d47c3136..5ef7fc03d 100644 --- a/test/commands/healthcheck_test.rb +++ b/test/commands/healthcheck_test.rb @@ -10,7 +10,7 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase test "run" do assert_equal \ - "docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app -e KAMAL_CONTAINER_NAME=\"healthcheck-app\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" dhh/app:123", + "docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app -e KAMAL_CONTAINER_NAME=\"healthcheck-app\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" dhh/app:123", new_command.run.join(" ") end @@ -18,7 +18,7 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase @config[:healthcheck] = { "port" => 3001 } assert_equal \ - "docker run --detach --name healthcheck-app-123 --publish 3999:3001 --label service=healthcheck-app -e KAMAL_CONTAINER_NAME=\"healthcheck-app\" --health-cmd \"curl -f http://localhost:3001/up || exit 1\" --health-interval \"1s\" dhh/app:123", + "docker run --detach --name healthcheck-app-123 --publish 3999:3001 --label service=healthcheck-app -e KAMAL_CONTAINER_NAME=\"healthcheck-app\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3001/up || exit 1\" --health-interval \"1s\" dhh/app:123", new_command.run.join(" ") end @@ -26,7 +26,7 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase @destination = "staging" assert_equal \ - "docker run --detach --name healthcheck-app-staging-123 --publish 3999:3000 --label service=healthcheck-app-staging -e KAMAL_CONTAINER_NAME=\"healthcheck-app-staging\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" dhh/app:123", + "docker run --detach --name healthcheck-app-staging-123 --publish 3999:3000 --label service=healthcheck-app-staging -e KAMAL_CONTAINER_NAME=\"healthcheck-app-staging\" --env-file .kamal/env/roles/app-web-staging.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" dhh/app:123", new_command.run.join(" ") end @@ -34,7 +34,7 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase @config[:healthcheck] = { "cmd" => "/bin/up" } assert_equal \ - "docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app -e KAMAL_CONTAINER_NAME=\"healthcheck-app\" --health-cmd \"/bin/up\" --health-interval \"1s\" dhh/app:123", + "docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app -e KAMAL_CONTAINER_NAME=\"healthcheck-app\" --env-file .kamal/env/roles/app-web.env --health-cmd \"/bin/up\" --health-interval \"1s\" dhh/app:123", new_command.run.join(" ") end @@ -42,7 +42,7 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase @config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere" } } } @config[:healthcheck] = { "exposed_port" => 4999 } assert_equal \ - "docker run --detach --name healthcheck-app-123 --publish 4999:3000 --label service=healthcheck-app -e KAMAL_CONTAINER_NAME=\"healthcheck-app\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --mount \"somewhere\" dhh/app:123", + "docker run --detach --name healthcheck-app-123 --publish 4999:3000 --label service=healthcheck-app -e KAMAL_CONTAINER_NAME=\"healthcheck-app\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --mount \"somewhere\" dhh/app:123", new_command.run.join(" ") end diff --git a/test/commands/traefik_test.rb b/test/commands/traefik_test.rb index b6b47bfdd..5a651dee1 100644 --- a/test/commands/traefik_test.rb +++ b/test/commands/traefik_test.rb @@ -18,72 +18,72 @@ class CommandsTraefikTest < ActiveSupport::TestCase 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 --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", + "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\" #{@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 --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", + "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\" #{@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 --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", + "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\" #{@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 --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", + "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\" #{@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 --log-opt max-size=\"10m\" --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\"", + "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\" --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 --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", + "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\" #{@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 --log-opt max-size=\"10m\" --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\"", + "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\" --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 --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", + "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\" #{@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 --log-opt max-size=\"10m\" --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\"", + "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\" --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 --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", + "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\" #{@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 --log-opt max-size=\"10m\" --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\"", + "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.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 --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", + "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\" #{@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"] = { "secret" => %w[EXAMPLE_API_KEY] } assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock -e EXAMPLE_API_KEY=\"456\" --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", + "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\" #{@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 @@ -91,7 +91,7 @@ class CommandsTraefikTest < ActiveSupport::TestCase @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 --log-opt max-size=\"10m\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", + "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\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", new_command.run.join(" ") end @@ -99,7 +99,7 @@ class CommandsTraefikTest < ActiveSupport::TestCase @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 --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", + "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\" #{@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 @@ -107,7 +107,7 @@ class CommandsTraefikTest < ActiveSupport::TestCase @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 --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"ERROR\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", + "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\" #{@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 @@ -177,6 +177,24 @@ class CommandsTraefikTest < ActiveSupport::TestCase new_command.follow_logs(host: @config[:servers].first, grep: 'hello!') end + test "env_file" do + @config[:traefik]["env"] = { "secret" => %w[EXAMPLE_API_KEY] } + + assert_equal "EXAMPLE_API_KEY=456\n", new_command.env_file + end + + test "host_env_file_path" do + assert_equal ".kamal/env/traefik/traefik.env", new_command.host_env_file_path + end + + test "make_env_directory" do + assert_equal "mkdir -p .kamal/env/traefik", new_command.make_env_directory.join(" ") + end + + test "remove_env_file" do + assert_equal "rm -f .kamal/env/traefik/traefik.env", new_command.remove_env_file.join(" ") + end + private def new_command Kamal::Commands::Traefik.new(Kamal::Configuration.new(@config, version: "123")) diff --git a/test/configuration/accessory_test.rb b/test/configuration/accessory_test.rb index 4f9623044..73136239b 100644 --- a/test/configuration/accessory_test.rb +++ b/test/configuration/accessory_test.rb @@ -110,19 +110,30 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase assert_equal ["--label", "service=\"app-redis\"", "--label", "cache=\"true\""], @config.accessory(:redis).label_args end - test "env args with secret" do + test "env args" do + assert_equal ["--env-file", ".kamal/env/accessories/app-mysql.env"], @config.accessory(:mysql).env_args + assert_equal ["--env-file", ".kamal/env/accessories/app-redis.env"], @config.accessory(:redis).env_args + end + + test "env file with secret" do ENV["MYSQL_ROOT_PASSWORD"] = "secret123" - @config.accessory(:mysql).env_args.tap do |env_args| - assert_equal ["-e", "MYSQL_ROOT_PASSWORD=\"secret123\"", "-e", "MYSQL_ROOT_HOST=\"%\""], Kamal::Utils.unredacted(env_args) - assert_equal ["-e", "MYSQL_ROOT_PASSWORD=[REDACTED]", "-e", "MYSQL_ROOT_HOST=\"%\""], Kamal::Utils.redacted(env_args) - end + expected = <<~ENV + MYSQL_ROOT_PASSWORD=secret123 + MYSQL_ROOT_HOST=% + ENV + + assert_equal expected, @config.accessory(:mysql).env_file ensure ENV["MYSQL_ROOT_PASSWORD"] = nil end - test "env args without secret" do - assert_equal ["-e", "SOMETHING=\"else\""], @config.accessory(:redis).env_args + test "host_env_directory" do + assert_equal ".kamal/env/accessories", @config.accessory(:mysql).host_env_directory + end + + test "host_env_file_path" do + assert_equal ".kamal/env/accessories/app-mysql.env", @config.accessory(:mysql).host_env_file_path end test "volume args" do diff --git a/test/configuration/role_test.rb b/test/configuration/role_test.rb index c554965d9..b29ac2b53 100644 --- a/test/configuration/role_test.rb +++ b/test/configuration/role_test.rb @@ -71,7 +71,17 @@ class ConfigurationRoleTest < ActiveSupport::TestCase test "env overwritten by role" do assert_equal "redis://a/b", @config_with_roles.role(:workers).env["REDIS_URL"] - assert_equal ["-e", "REDIS_URL=\"redis://a/b\"", "-e", "WEB_CONCURRENCY=\"4\""], @config_with_roles.role(:workers).env_args + + expected_env = <<~ENV + REDIS_URL=redis://a/b + WEB_CONCURRENCY=4 + ENV + + assert_equal expected_env, @config_with_roles.role(:workers).env_file + end + + test "env args" do + assert_equal ["--env-file", ".kamal/env/roles/app-workers.env"], @config_with_roles.role(:workers).env_args end test "env secret overwritten by role" do @@ -97,10 +107,14 @@ class ConfigurationRoleTest < ActiveSupport::TestCase ENV["REDIS_PASSWORD"] = "secret456" ENV["DB_PASSWORD"] = "secret&\"123" - @config_with_roles.role(:workers).env_args.tap do |env_args| - assert_equal ["-e", "REDIS_PASSWORD=\"secret456\"", "-e", "DB_PASSWORD=\"secret&\\\"123\"", "-e", "REDIS_URL=\"redis://a/b\"", "-e", "WEB_CONCURRENCY=\"4\""], Kamal::Utils.unredacted(env_args) - assert_equal ["-e", "REDIS_PASSWORD=[REDACTED]", "-e", "DB_PASSWORD=[REDACTED]", "-e", "REDIS_URL=\"redis://a/b\"", "-e", "WEB_CONCURRENCY=\"4\""], Kamal::Utils.redacted(env_args) - end + expected = <<~ENV + REDIS_PASSWORD=secret456 + DB_PASSWORD=secret&\"123 + REDIS_URL=redis://a/b + WEB_CONCURRENCY=4 + ENV + + assert_equal expected, @config_with_roles.role(:workers).env_file ensure ENV["REDIS_PASSWORD"] = nil ENV["DB_PASSWORD"] = nil @@ -119,10 +133,13 @@ class ConfigurationRoleTest < ActiveSupport::TestCase ENV["DB_PASSWORD"] = "secret123" - @config_with_roles.role(:workers).env_args.tap do |env_args| - assert_equal ["-e", "DB_PASSWORD=\"secret123\"", "-e", "REDIS_URL=\"redis://a/b\"", "-e", "WEB_CONCURRENCY=\"4\""], Kamal::Utils.unredacted(env_args) - assert_equal ["-e", "DB_PASSWORD=[REDACTED]", "-e", "REDIS_URL=\"redis://a/b\"", "-e", "WEB_CONCURRENCY=\"4\""], Kamal::Utils.redacted(env_args) - end + expected = <<~ENV + DB_PASSWORD=secret123 + REDIS_URL=redis://a/b + WEB_CONCURRENCY=4 + ENV + + assert_equal expected, @config_with_roles.role(:workers).env_file ensure ENV["DB_PASSWORD"] = nil end @@ -139,11 +156,23 @@ class ConfigurationRoleTest < ActiveSupport::TestCase ENV["REDIS_PASSWORD"] = "secret456" - @config_with_roles.role(:workers).env_args.tap do |env_args| - assert_equal ["-e", "REDIS_PASSWORD=\"secret456\"", "-e", "REDIS_URL=\"redis://a/b\"", "-e", "WEB_CONCURRENCY=\"4\""], Kamal::Utils.unredacted(env_args) - assert_equal ["-e", "REDIS_PASSWORD=[REDACTED]", "-e", "REDIS_URL=\"redis://a/b\"", "-e", "WEB_CONCURRENCY=\"4\""], Kamal::Utils.redacted(env_args) - end + expected = <<~ENV + REDIS_PASSWORD=secret456 + REDIS_URL=redis://a/b + WEB_CONCURRENCY=4 + ENV + + assert_equal expected, @config_with_roles.role(:workers).env_file ensure ENV["REDIS_PASSWORD"] = nil end + + test "host_env_directory" do + assert_equal ".kamal/env/roles", @config_with_roles.role(:workers).host_env_directory + end + + test "host_env_file_path" do + assert_equal ".kamal/env/roles/app-workers.env", @config_with_roles.role(:workers).host_env_file_path + end + end diff --git a/test/configuration_test.rb b/test/configuration_test.rb index 615053129..d31e0dbdf 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -124,45 +124,7 @@ class ConfigurationTest < ActiveSupport::TestCase assert_equal "app-missing", @config.service_with_version end - test "env args" do - assert_equal [ "-e", "REDIS_URL=\"redis://x/y\"" ], @config.env_args - end - - test "env args with clear and secrets" do - ENV["PASSWORD"] = "secret123" - - config = Kamal::Configuration.new(@deploy.tap { |c| c.merge!({ - env: { "clear" => { "PORT" => "3000" }, "secret" => [ "PASSWORD" ] } - }) }) - - assert_equal [ "-e", "PASSWORD=\"secret123\"", "-e", "PORT=\"3000\"" ], Kamal::Utils.unredacted(config.env_args) - assert_equal [ "-e", "PASSWORD=[REDACTED]", "-e", "PORT=\"3000\"" ], Kamal::Utils.redacted(config.env_args) - ensure - ENV["PASSWORD"] = nil - end - - test "env args with only clear" do - config = Kamal::Configuration.new(@deploy.tap { |c| c.merge!({ - env: { "clear" => { "PORT" => "3000" } } - }) }) - - assert_equal [ "-e", "PORT=\"3000\"" ], config.env_args - end - - test "env args with only secrets" do - ENV["PASSWORD"] = "secret123" - - config = Kamal::Configuration.new(@deploy.tap { |c| c.merge!({ - env: { "secret" => [ "PASSWORD" ] } - }) }) - - assert_equal [ "-e", "PASSWORD=\"secret123\"" ], Kamal::Utils.unredacted(config.env_args) - assert_equal [ "-e", "PASSWORD=[REDACTED]" ], Kamal::Utils.redacted(config.env_args) - ensure - ENV["PASSWORD"] = nil - end - - test "env args with missing secret" do + test "env with missing secret" do assert_raises(KeyError) do config = Kamal::Configuration.new(@deploy.tap { |c| c.merge!({ env: { "secret" => [ "PASSWORD" ] } @@ -257,7 +219,6 @@ class ConfigurationTest < ActiveSupport::TestCase :repository=>"dhh/app", :absolute_image=>"dhh/app:missing", :service_with_version=>"app-missing", - :env_args=>["-e", "REDIS_URL=\"redis://x/y\""], :ssh_options=>{ :user=>"root", :auth_methods=>["publickey"], log_level: :fatal, keepalive: true, keepalive_interval: 30 }, :sshkit=>{}, :volume_args=>["--volume", "/local/path:/container/path"], diff --git a/test/integration/accessory_test.rb b/test/integration/accessory_test.rb index 102c02d39..87ae6de83 100644 --- a/test/integration/accessory_test.rb +++ b/test/integration/accessory_test.rb @@ -2,6 +2,8 @@ class AccessoryTest < IntegrationTest test "boot, stop, start, restart, logs, remove" do + kamal :envify + kamal :accessory, :boot, :busybox assert_accessory_running :busybox @@ -19,6 +21,8 @@ class AccessoryTest < IntegrationTest kamal :accessory, :remove, :busybox, "-y" assert_accessory_not_running :busybox + + kamal :env, :delete end private diff --git a/test/integration/app_test.rb b/test/integration/app_test.rb index 1f5c5b1ce..37a4dd71a 100644 --- a/test/integration/app_test.rb +++ b/test/integration/app_test.rb @@ -2,6 +2,8 @@ class AppTest < IntegrationTest test "stop, start, boot, logs, images, containers, exec, remove" do + kamal :envify + kamal :deploy assert_app_is_up diff --git a/test/integration/docker/deployer/Dockerfile b/test/integration/docker/deployer/Dockerfile index 119001395..b964e0e3d 100644 --- a/test/integration/docker/deployer/Dockerfile +++ b/test/integration/docker/deployer/Dockerfile @@ -23,7 +23,7 @@ RUN mkdir -p /etc/docker/certs.d/registry:4443 && ln -s /shared/certs/domain.crt RUN git config --global user.email "deployer@example.com" RUN git config --global user.name "Deployer" -RUN git init && git add . && git commit -am "Initial version" +RUN git init && echo ".env" >> .gitignore && git add . && git commit -am "Initial version" HEALTHCHECK --interval=1s CMD pgrep sleep diff --git a/test/integration/docker/deployer/app/config/deploy.yml b/test/integration/docker/deployer/app/config/deploy.yml index bc2cb28c0..6ecb94b31 100644 --- a/test/integration/docker/deployer/app/config/deploy.yml +++ b/test/integration/docker/deployer/app/config/deploy.yml @@ -3,6 +3,12 @@ image: app servers: - vm1 - vm2 +env: + clear: + CLEAR_TOKEN: '4321' + secret: + - SECRET_TOKEN + registry: server: registry:4443 username: root diff --git a/test/integration/lock_test.rb b/test/integration/lock_test.rb index 22795b378..c9d88a919 100644 --- a/test/integration/lock_test.rb +++ b/test/integration/lock_test.rb @@ -2,6 +2,8 @@ class LockTest < IntegrationTest test "acquire, release, status" do + kamal :envify + kamal :lock, :acquire, "-m 'Integration Tests'" status = kamal :lock, :status, capture: true diff --git a/test/integration/main_test.rb b/test/integration/main_test.rb index 3a6d55875..08e2567f6 100644 --- a/test/integration/main_test.rb +++ b/test/integration/main_test.rb @@ -1,7 +1,11 @@ require_relative "integration_test" class MainTest < IntegrationTest - test "deploy, redeploy, rollback, details and audit" do + test "envify, deploy, redeploy, rollback, details and audit" do + kamal :envify + assert_local_env_file "SECRET_TOKEN=1234" + assert_remote_env_file "SECRET_TOKEN=1234\nCLEAR_TOKEN=4321" + first_version = latest_app_version assert_app_is_down @@ -30,12 +34,9 @@ class MainTest < IntegrationTest audit = kamal :audit, capture: true assert_match /Booted app version #{first_version}.*Booted app version #{second_version}.*Booted app version #{first_version}.*/m, audit - end - - test "envify" do - kamal :envify - assert_equal "SECRET_TOKEN=1234", deployer_exec("cat .env", capture: true) + kamal :env, :delete + assert_no_remote_env_file end test "config" do @@ -49,11 +50,23 @@ class MainTest < IntegrationTest assert_equal "registry:4443/app", config[:repository] assert_equal "registry:4443/app:#{version}", config[:absolute_image] assert_equal "app-#{version}", config[:service_with_version] - assert_equal [], config[:env_args] assert_equal [], config[:volume_args] assert_equal({ user: "root", auth_methods: [ "publickey" ], keepalive: true, keepalive_interval: 30, log_level: :fatal }, config[:ssh_options]) assert_equal({ "multiarch" => false, "args" => { "COMMIT_SHA" => version } }, config[:builder]) assert_equal [ "--log-opt", "max-size=\"10m\"" ], config[:logging] assert_equal({ "path" => "/up", "port" => 3000, "max_attempts" => 7, "exposed_port" => 3999, "cmd" => "wget -qO- http://localhost > /dev/null" }, config[:healthcheck]) end + + private + def assert_local_env_file(contents) + assert_equal contents, deployer_exec("cat .env", capture: true) + end + + def assert_remote_env_file(contents) + assert_equal contents, docker_compose("exec vm1 cat /root/.kamal/env/roles/app-web.env", capture: true) + end + + def assert_no_remote_env_file + assert_equal "nofile", docker_compose("exec vm1 stat /root/.kamal/env/roles/app-web.env 2> /dev/null || echo nofile", capture: true) + end end diff --git a/test/integration/traefik_test.rb b/test/integration/traefik_test.rb index 45a08e228..92211c439 100644 --- a/test/integration/traefik_test.rb +++ b/test/integration/traefik_test.rb @@ -2,6 +2,8 @@ class TraefikTest < IntegrationTest test "boot, reboot, stop, start, restart, logs, remove" do + kamal :envify + kamal :traefik, :boot assert_traefik_running @@ -33,6 +35,8 @@ class TraefikTest < IntegrationTest kamal :traefik, :remove assert_traefik_not_running + + kamal :env, :delete end private diff --git a/test/utils_test.rb b/test/utils_test.rb index 3cc4af766..cef0a7fa2 100644 --- a/test/utils_test.rb +++ b/test/utils_test.rb @@ -11,13 +11,65 @@ class UtilsTest < ActiveSupport::TestCase Kamal::Utils.argumentize("--label", { foo: "bar" }, sensitive: true).last end - test "argumentize_env_with_secrets" do - ENV.expects(:fetch).with("FOO").returns("secret") + test "env file simple" do + env = { + "foo" => "bar", + "baz" => "haz" + } + + assert_equal "foo=bar\nbaz=haz\n", \ + Kamal::Utils.env_file_with_secrets(env) + end + + test "env file clear" do + env = { + "clear" => { + "foo" => "bar", + "baz" => "haz" + } + } + + assert_equal "foo=bar\nbaz=haz\n", \ + Kamal::Utils.env_file_with_secrets(env) + end + + test "env file secret" do + ENV["PASSWORD"] = "hello" + env = { + "secret" => [ "PASSWORD" ] + } - args = Kamal::Utils.argumentize_env_with_secrets({ "secret" => [ "FOO" ], "clear" => { BAZ: "qux" } }) + assert_equal "PASSWORD=hello\n", \ + Kamal::Utils.env_file_with_secrets(env) + ensure + ENV.delete "PASSWORD" + end + + test "env file missing secret" do + env = { + "secret" => [ "PASSWORD" ] + } + + assert_raises(KeyError) { Kamal::Utils.env_file_with_secrets(env) } + + ensure + ENV.delete "PASSWORD" + end - assert_equal [ "-e", "FOO=[REDACTED]", "-e", "BAZ=\"qux\"" ], Kamal::Utils.redacted(args) - assert_equal [ "-e", "FOO=\"secret\"", "-e", "BAZ=\"qux\"" ], Kamal::Utils.unredacted(args) + test "env file secret and clear" do + ENV["PASSWORD"] = "hello" + env = { + "secret" => [ "PASSWORD" ], + "clear" => { + "foo" => "bar", + "baz" => "haz" + } + } + + assert_equal "PASSWORD=hello\nfoo=bar\nbaz=haz\n", \ + Kamal::Utils.env_file_with_secrets(env) + ensure + ENV.delete "PASSWORD" end test "optionize" do