diff --git a/lib/kamal/cli/app.rb b/lib/kamal/cli/app.rb index ec7c019a..188d5e9b 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -37,7 +37,7 @@ class Kamal::Cli::App < Kamal::Cli::Base roles.each do |role| execute *KAMAL.auditor.record("Started app version #{KAMAL.config.version}"), verbosity: :debug - execute *KAMAL.app(role: role).start, raise_on_non_zero_exit: false + execute *KAMAL.app(role: role, host: host).start, raise_on_non_zero_exit: false end end end @@ -51,7 +51,7 @@ class Kamal::Cli::App < Kamal::Cli::Base roles.each do |role| execute *KAMAL.auditor.record("Stopped app", role: role), verbosity: :debug - execute *KAMAL.app(role: role).stop, raise_on_non_zero_exit: false + execute *KAMAL.app(role: role, host: host).stop, raise_on_non_zero_exit: false end end end @@ -64,7 +64,7 @@ class Kamal::Cli::App < Kamal::Cli::Base roles = KAMAL.roles_on(host) roles.each do |role| - puts_by_host host, capture_with_info(*KAMAL.app(role: role).info) + puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).info) end end end @@ -80,7 +80,7 @@ class Kamal::Cli::App < Kamal::Cli::Base say "Get current version of running container...", :magenta unless options[:version] using_version(options[:version] || current_running_version) do |version| say "Launching interactive command with version #{version} via SSH from existing container on #{KAMAL.primary_host}...", :magenta - run_locally { exec KAMAL.app(role: KAMAL.primary_role).execute_in_existing_container_over_ssh(cmd, host: KAMAL.primary_host, env: env) } + run_locally { exec KAMAL.app(role: KAMAL.primary_role, host: KAMAL.primary_host).execute_in_existing_container_over_ssh(cmd, env: env) } end when options[:interactive] @@ -88,7 +88,7 @@ class Kamal::Cli::App < Kamal::Cli::Base using_version(version_or_latest) do |version| say "Launching interactive command with version #{version} via SSH from new container on #{KAMAL.primary_host}...", :magenta run_locally do - exec KAMAL.app(role: KAMAL.primary_role).execute_in_new_container_over_ssh(cmd, host: KAMAL.primary_host, env: env) + exec KAMAL.app(role: KAMAL.primary_role, host: KAMAL.primary_host).execute_in_new_container_over_ssh(cmd, env: env) end end @@ -102,7 +102,7 @@ class Kamal::Cli::App < Kamal::Cli::Base roles.each do |role| execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}", role: role), verbosity: :debug - puts_by_host host, capture_with_info(*KAMAL.app(role: role).execute_in_existing_container(cmd, env: env)) + puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_existing_container(cmd, env: env)) end end end @@ -116,7 +116,7 @@ class Kamal::Cli::App < Kamal::Cli::Base roles.each do |role| execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug - puts_by_host host, capture_with_info(*KAMAL.app(role: role).execute_in_new_container(cmd, env: env)) + puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_new_container(cmd, env: env)) end end end @@ -140,13 +140,14 @@ class Kamal::Cli::App < Kamal::Cli::Base roles = KAMAL.roles_on(host) roles.each do |role| - versions = capture_with_info(*KAMAL.app(role: role).list_versions, raise_on_non_zero_exit: false).split("\n") - versions -= [ capture_with_info(*KAMAL.app(role: role).current_running_version, raise_on_non_zero_exit: false).strip ] + app = KAMAL.app(role: role, host: host) + versions = capture_with_info(*app.list_versions, raise_on_non_zero_exit: false).split("\n") + versions -= [ capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip ] versions.each do |version| if stop puts_by_host host, "Stopping stale container for role #{role} with version #{version}" - execute *KAMAL.app(role: role).stop(version: version), raise_on_non_zero_exit: false + execute *app.stop(version: version), raise_on_non_zero_exit: false else puts_by_host host, "Detected stale container for role #{role} with version #{version} (use `kamal app stale_containers --stop` to stop)" end @@ -180,8 +181,9 @@ class Kamal::Cli::App < Kamal::Cli::Base KAMAL.specific_roles ||= [ "web" ] role = KAMAL.roles_on(KAMAL.primary_host).first - info KAMAL.app(role: role).follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep) - exec KAMAL.app(role: role).follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep) + app = KAMAL.app(role: role, host: host) + info app.follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep) + exec app.follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep) end else lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set @@ -191,7 +193,7 @@ class Kamal::Cli::App < Kamal::Cli::Base roles.each do |role| begin - puts_by_host host, capture_with_info(*KAMAL.app(role: role).logs(since: since, lines: lines, grep: grep)) + puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).logs(since: since, lines: lines, grep: grep)) rescue SSHKit::Command::Failed puts_by_host host, "Nothing found" end @@ -217,7 +219,7 @@ class Kamal::Cli::App < Kamal::Cli::Base roles.each do |role| execute *KAMAL.auditor.record("Removed app container with version #{version}", role: role), verbosity: :debug - execute *KAMAL.app(role: role).remove_container(version: version) + execute *KAMAL.app(role: role, host: host).remove_container(version: version) end end end @@ -231,7 +233,7 @@ class Kamal::Cli::App < Kamal::Cli::Base roles.each do |role| execute *KAMAL.auditor.record("Removed all app containers", role: role), verbosity: :debug - execute *KAMAL.app(role: role).remove_containers + execute *KAMAL.app(role: role, host: host).remove_containers end end end @@ -251,7 +253,7 @@ class Kamal::Cli::App < Kamal::Cli::Base def version on(KAMAL.hosts) do |host| role = KAMAL.roles_on(host).first - puts_by_host host, capture_with_info(*KAMAL.app(role: role).current_running_version).strip + puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).current_running_version).strip end end @@ -274,7 +276,7 @@ class Kamal::Cli::App < Kamal::Cli::Base version = nil on(host) do role = KAMAL.roles_on(host).first - version = capture_with_info(*KAMAL.app(role: role).current_running_version).strip + version = capture_with_info(*KAMAL.app(role: role, host: host).current_running_version).strip end version.presence end diff --git a/lib/kamal/cli/app/boot.rb b/lib/kamal/cli/app/boot.rb index ed7e2ed6..6098e654 100644 --- a/lib/kamal/cli/app/boot.rb +++ b/lib/kamal/cli/app/boot.rb @@ -22,7 +22,7 @@ class Kamal::Cli::App::Boot private def app - @app ||= KAMAL.app(role: role) + @app ||= KAMAL.app(role: role, host: host) end def auditor diff --git a/lib/kamal/cli/app/prepare_assets.rb b/lib/kamal/cli/app/prepare_assets.rb index f7f44269..dd28fa41 100644 --- a/lib/kamal/cli/app/prepare_assets.rb +++ b/lib/kamal/cli/app/prepare_assets.rb @@ -19,6 +19,6 @@ class Kamal::Cli::App::PrepareAssets private def app - @app ||= KAMAL.app(role: role) + @app ||= KAMAL.app(role: role, host: host) end end diff --git a/lib/kamal/cli/env.rb b/lib/kamal/cli/env.rb index 3cfb1741..56ba505a 100644 --- a/lib/kamal/cli/env.rb +++ b/lib/kamal/cli/env.rb @@ -8,8 +8,8 @@ class Kamal::Cli::Env < Kamal::Cli::Base execute *KAMAL.auditor.record("Pushed env files"), verbosity: :debug KAMAL.roles_on(host).each do |role| - execute *KAMAL.app(role: role).make_env_directory - upload! role.env.secrets_io, role.env.secrets_file, mode: 400 + execute *KAMAL.app(role: role, host: host).make_env_directory + upload! role.env(host).secrets_io, role.env(host).secrets_file, mode: 400 end end @@ -35,7 +35,7 @@ class Kamal::Cli::Env < Kamal::Cli::Base execute *KAMAL.auditor.record("Deleted env files"), verbosity: :debug KAMAL.roles_on(host).each do |role| - execute *KAMAL.app(role: role).remove_env_file + execute *KAMAL.app(role: role, host: host).remove_env_file end end diff --git a/lib/kamal/cli/main.rb b/lib/kamal/cli/main.rb index 00ffbfe3..d972da6b 100644 --- a/lib/kamal/cli/main.rb +++ b/lib/kamal/cli/main.rb @@ -246,7 +246,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base begin on(KAMAL.hosts) do KAMAL.roles_on(host).each do |role| - container_id = capture_with_info(*KAMAL.app(role: role).container_id_for_version(version)) + container_id = capture_with_info(*KAMAL.app(role: role, host: host).container_id_for_version(version)) raise "Container not found" unless container_id.present? end end diff --git a/lib/kamal/commander.rb b/lib/kamal/commander.rb index 042e8429..e7c5d21f 100644 --- a/lib/kamal/commander.rb +++ b/lib/kamal/commander.rb @@ -65,8 +65,8 @@ class Kamal::Commander end - def app(role: nil) - Kamal::Commands::App.new(config, role: role) + def app(role: nil, host: nil) + Kamal::Commands::App.new(config, role: role, host: host) end def accessory(name) diff --git a/lib/kamal/commands/app.rb b/lib/kamal/commands/app.rb index 15992640..37fa86ab 100644 --- a/lib/kamal/commands/app.rb +++ b/lib/kamal/commands/app.rb @@ -3,11 +3,12 @@ class Kamal::Commands::App < Kamal::Commands::Base ACTIVE_DOCKER_STATUSES = [ :running, :restarting ] - attr_reader :role, :role + attr_reader :role, :host - def initialize(config, role: nil) + def initialize(config, role: nil, host: nil) super(config) @role = role + @host = host end def run(hostname: nil) @@ -18,7 +19,7 @@ class Kamal::Commands::App < Kamal::Commands::Base *([ "--hostname", hostname ] if hostname), "-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"", "-e", "KAMAL_VERSION=\"#{config.version}\"", - *role.env_args, + *role.env_args(host), *role.health_check_args, *role.logging_args, *config.volume_args, @@ -70,11 +71,11 @@ class Kamal::Commands::App < Kamal::Commands::Base def make_env_directory - make_directory role.env.secrets_directory + make_directory role.env(host).secrets_directory end def remove_env_file - [ :rm, "-f", role.env.secrets_file ] + [ :rm, "-f", role.env(host).secrets_file ] end diff --git a/lib/kamal/commands/app/execution.rb b/lib/kamal/commands/app/execution.rb index 6d32b0c8..215821dc 100644 --- a/lib/kamal/commands/app/execution.rb +++ b/lib/kamal/commands/app/execution.rb @@ -11,7 +11,7 @@ module Kamal::Commands::App::Execution docker :run, ("-it" if interactive), "--rm", - *role&.env_args, + *role&.env_args(host), *argumentize("--env", env), *config.volume_args, *role&.option_args, @@ -19,11 +19,11 @@ module Kamal::Commands::App::Execution *command end - def execute_in_existing_container_over_ssh(*command, host:, env:) + def execute_in_existing_container_over_ssh(*command, env:) run_over_ssh execute_in_existing_container(*command, interactive: true, env: env), host: host end - def execute_in_new_container_over_ssh(*command, host:, env:) + def execute_in_new_container_over_ssh(*command, env:) run_over_ssh execute_in_new_container(*command, interactive: true, env: env), host: host end end diff --git a/lib/kamal/commands/healthcheck.rb b/lib/kamal/commands/healthcheck.rb index 70868101..2517a5e2 100644 --- a/lib/kamal/commands/healthcheck.rb +++ b/lib/kamal/commands/healthcheck.rb @@ -8,7 +8,7 @@ class Kamal::Commands::Healthcheck < Kamal::Commands::Base "--publish", "#{exposed_port}:#{config.healthcheck["port"]}", "--label", "service=#{config.healthcheck_service}", "-e", "KAMAL_CONTAINER_NAME=\"#{config.healthcheck_service}\"", - *primary.env_args, + *primary.env_args(config.primary_host), *primary.health_check_args(cord: false), *config.volume_args, *primary.option_args, diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index ec2c3f66..dd7970cd 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -233,6 +233,14 @@ class Kamal::Configuration raw_config.env || {} end + def env_tags + raw_config.env_tags.collect { |name, config| Kamal::Configuration::Env::Tag.new(name, config: config) } + end + + def env_tag(name) + env_tags.detect { |t| t.name == name.to_s } + end + def valid? ensure_destination_if_required && ensure_required_keys_present && ensure_valid_kamal_version && ensure_retain_containers_valid && ensure_valid_service_name diff --git a/lib/kamal/configuration/env/tag.rb b/lib/kamal/configuration/env/tag.rb new file mode 100644 index 00000000..2a6a1306 --- /dev/null +++ b/lib/kamal/configuration/env/tag.rb @@ -0,0 +1,12 @@ +class Kamal::Configuration::Env::Tag + attr_reader :name, :config + + def initialize(name, config:) + @name = name + @config = config + end + + def env + Kamal::Configuration::Env.from_config(config: config) + end +end diff --git a/lib/kamal/configuration/role.rb b/lib/kamal/configuration/role.rb index c726c7b8..f0df5924 100644 --- a/lib/kamal/configuration/role.rb +++ b/lib/kamal/configuration/role.rb @@ -7,6 +7,7 @@ class Kamal::Configuration::Role def initialize(name, config:) @name, @config = name.inquiry, config + @tagged_hosts ||= extract_tagged_hosts_from_config end def primary_host @@ -14,7 +15,11 @@ class Kamal::Configuration::Role end def hosts - @hosts ||= extract_hosts_from_config + tagged_hosts.keys + end + + def env_tags(host) + tagged_hosts.fetch(host).collect { |tag| config.env_tag(tag) } end def cmd @@ -50,12 +55,13 @@ class Kamal::Configuration::Role end - def env - @env ||= base_env.merge(specialized_env) + def env(host) + @envs ||= {} + @envs[host] ||= [ base_env, specialized_env, *env_tags(host).map(&:env) ].reduce(:merge) end - def env_args - env.args + def env_args(host) + env(host).args end def asset_volume_args @@ -164,7 +170,24 @@ class Kamal::Configuration::Role end private - attr_accessor :config + attr_accessor :config, :tagged_hosts + + def extract_tagged_hosts_from_config + {}.tap do |tagged_hosts| + extract_hosts_from_config.map do |host_config| + if host_config.is_a?(Hash) + raise ArgumentError, "Multiple hosts found: #{host_config.inspect}" unless host_config.size == 1 + + host, tags = host_config.first + tagged_hosts[host] = Array(tags) + elsif host_config.is_a?(String) || host_config.is_a?(Symbol) + tagged_hosts[host_config] = [] + else + raise ArgumentError, "Invalid host config: #{host_config.inspect}" + end + end + end + end def extract_hosts_from_config if config.servers.is_a?(Array) diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index dabd2ccd..2b44ddf6 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -92,6 +92,32 @@ class CliAppTest < CliTestCase end end + test "boot with host tags" do + Object.any_instance.stubs(:sleep) + + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", raise_on_non_zero_exit: false) + .returns("12345678") # running version + + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'") + .returns("running") # health check + + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false) + .returns("123") # old version + + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :inspect, "-f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}'", "app-web-123", "|", :awk, "'$2 == \"/tmp/kamal-cord\" {print $1}'", raise_on_non_zero_exit: false) + .returns("") # old version + + run_command("boot", config: :with_env_tags).tap do |output| + assert_match "docker tag dhh/app:latest dhh/app:latest", output + assert_match %r{docker run --detach --restart unless-stopped --name app-web-latest --hostname 1.1.1.1-[0-9a-f]{12} -e KAMAL_CONTAINER_NAME="app-web-latest" -e KAMAL_VERSION="latest" --env-file .kamal/env/roles/app-web.env --env TEST="root" --env EXPERIMENT="disabled" --env SITE="site1"}, output + assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output + end + end + test "start" do run_command("start").tap do |output| assert_match "docker start app-web-999", output diff --git a/test/commands/app_test.rb b/test/commands/app_test.rb index 3fe97d97..af7c6251 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -60,7 +60,7 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:servers] = { "web" => [ "1.1.1.1" ], "jobs" => { "hosts" => [ "1.1.1.2" ], "cmd" => "bin/jobs", "options" => { "mount" => "somewhere", "cap-add" => true } } } assert_equal \ "docker run --detach --restart unless-stopped --name app-jobs-999 -e KAMAL_CONTAINER_NAME=\"app-jobs-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-jobs.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"jobs\" --label destination --mount \"somewhere\" --cap-add dhh/app:999 bin/jobs", - new_command(role: "jobs").run.join(" ") + new_command(role: "jobs", host: "1.1.1.2").run.join(" ") end test "run with logging config" do @@ -80,6 +80,15 @@ class CommandsAppTest < ActiveSupport::TestCase new_command.run.join(" ") end + test "run with tags" do + @config[:servers] = [ { "1.1.1.1" => "tag1" } ] + @config[:env_tags] = { "tag1" => { "ENV1" => "value1" } } + + assert_equal \ + "docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --env ENV1=\"value1\" --health-cmd \"(curl -f http://localhost:3000/up || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999", + new_command.run.join(" ") + end + test "start" do assert_equal \ "docker start app-web-999", @@ -183,6 +192,15 @@ class CommandsAppTest < ActiveSupport::TestCase new_command.execute_in_new_container("bin/rails", "db:setup", env: { "foo" => "bar" }).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 --env-file .kamal/env/roles/app-web.env --env ENV1=\"value1\" 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 \ @@ -204,18 +222,26 @@ class CommandsAppTest < ActiveSupport::TestCase test "execute in new container over ssh" do assert_match %r{docker run -it --rm --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails c}, - new_command.execute_in_new_container_over_ssh("bin/rails", "c", host: "app-1", env: {}) + new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {}) + end + + test "execute in new container over ssh with tags" do + @config[:servers] = [ { "1.1.1.1" => "tag1" } ] + @config[:env_tags] = { "tag1" => { "ENV1" => "value1" } } + + assert_equal "ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --env-file .kamal/env/roles/app-web.env --env ENV1=\"value1\" dhh/app:999 bin/rails c'", + new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {}) end test "execute in new container with custom options over ssh" do @config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } } assert_match %r{docker run -it --rm --env-file .kamal/env/roles/app-web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c}, - new_command.execute_in_new_container_over_ssh("bin/rails", "c", host: "app-1", env: {}) + new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {}) end test "execute in existing container over ssh" do assert_match %r{docker exec -it app-web-999 bin/rails c}, - new_command.execute_in_existing_container_over_ssh("bin/rails", "c", host: "app-1", env: {}) + new_command.execute_in_existing_container_over_ssh("bin/rails", "c", env: {}) end test "run over ssh" do @@ -418,8 +444,8 @@ class CommandsAppTest < ActiveSupport::TestCase end private - def new_command(role: "web", **additional_config) + def new_command(role: "web", host: "1.1.1.1", **additional_config) config = Kamal::Configuration.new(@config.merge(additional_config), destination: @destination, version: "999") - Kamal::Commands::App.new(config, role: config.role(role)) + Kamal::Commands::App.new(config, role: config.role(role), host: host) end end diff --git a/test/commands/healthcheck_test.rb b/test/commands/healthcheck_test.rb index 66cdda6b..5c605175 100644 --- a/test/commands/healthcheck_test.rb +++ b/test/commands/healthcheck_test.rb @@ -46,6 +46,15 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase new_command.run.join(" ") end + test "run with tags" do + @config[:servers] = [ { "1.1.1.1" => "tag1" } ] + @config[:env_tags] = { "tag1" => { "ENV1" => "value1" } } + + assert_equal \ + "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 --env ENV1=\"value1\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" dhh/app:123", + new_command.run.join(" ") + end + test "status" do assert_equal \ "docker container ls --all --filter name=^healthcheck-app-123$ --quiet | xargs docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'", diff --git a/test/configuration/env/tags_test.rb b/test/configuration/env/tags_test.rb new file mode 100644 index 00000000..380cb5cd --- /dev/null +++ b/test/configuration/env/tags_test.rb @@ -0,0 +1,102 @@ +require "test_helper" + +class ConfigurationEnvTagsTest < ActiveSupport::TestCase + setup do + @deploy = { + service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, + servers: [ { "1.1.1.1" => "odd" }, { "1.1.1.2" => "even" }, { "1.1.1.3" => [ "odd", "three" ] } ], + env: { "REDIS_URL" => "redis://x/y", "THREE" => "false" }, + env_tags: { + "odd" => { "TYPE" => "odd" }, + "even" => { "TYPE" => "even" }, + "three" => { "THREE" => "true" } + } + } + + @config = Kamal::Configuration.new(@deploy) + + @deploy_with_roles = @deploy.dup.merge({ + servers: { + "web" => [ { "1.1.1.1" => "odd" }, "1.1.1.2" ], + "workers" => { + "hosts" => [ { "1.1.1.3" => [ "odd", "oddjob" ] }, "1.1.1.4" ], + "cmd" => "bin/jobs", + "env" => { + "REDIS_URL" => "redis://a/b", + "WEB_CONCURRENCY" => 4 + } + } + }, + env_tags: { + "odd" => { "TYPE" => "odd" }, + "oddjob" => { "TYPE" => "oddjob" } + } + }) + + @config_with_roles = Kamal::Configuration.new(@deploy_with_roles) + end + + test "tags" do + assert_equal 3, @config.env_tags.size + assert_equal %w[ odd even three ], @config.env_tags.map(&:name) + assert_equal({ "TYPE" => "odd" }, @config.env_tag("odd").env.clear) + assert_equal({ "TYPE" => "even" }, @config.env_tag("even").env.clear) + assert_equal({ "THREE" => "true" }, @config.env_tag("three").env.clear) + end + + test "tags with roles" do + assert_equal 2, @config_with_roles.env_tags.size + assert_equal %w[ odd oddjob ], @config_with_roles.env_tags.map(&:name) + assert_equal({ "TYPE" => "odd" }, @config_with_roles.env_tag("odd").env.clear) + assert_equal({ "TYPE" => "oddjob" }, @config_with_roles.env_tag("oddjob").env.clear) + end + + test "tag overrides env" do + assert_equal "false", @config.role("web").env("1.1.1.1").clear["THREE"] + assert_equal "true", @config.role("web").env("1.1.1.3").clear["THREE"] + end + + test "later tag wins" do + deploy = { + service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, + servers: [ { "1.1.1.1" => [ "first", "second" ] } ], + env_tags: { + "first" => { "TYPE" => "first" }, + "second" => { "TYPE" => "second" } + } + } + + config = Kamal::Configuration.new(deploy) + assert_equal "second", config.role("web").env("1.1.1.1").clear["TYPE"] + end + + test "tag secret env" do + ENV["PASSWORD"] = "hello" + + deploy = { + service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, + servers: [ { "1.1.1.1" => "secrets" } ], + env_tags: { + "secrets" => { "secret" => [ "PASSWORD" ] } + } + } + + config = Kamal::Configuration.new(deploy) + assert_equal "hello", config.role("web").env("1.1.1.1").secrets["PASSWORD"] + ensure + ENV.delete "PASSWORD" + end + + test "tag clear env" do + deploy = { + service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, + servers: [ { "1.1.1.1" => "clearly" } ], + env_tags: { + "clearly" => { "clear" => { "FOO" => "bar" } } + } + } + + config = Kamal::Configuration.new(deploy) + assert_equal "bar", config.role("web").env("1.1.1.1").clear["FOO"] + end +end diff --git a/test/configuration/role_test.rb b/test/configuration/role_test.rb index 0952f1e8..84fdfe6b 100644 --- a/test/configuration/role_test.rb +++ b/test/configuration/role_test.rb @@ -70,10 +70,10 @@ class ConfigurationRoleTest < ActiveSupport::TestCase end test "env overwritten by role" do - assert_equal "redis://a/b", @config_with_roles.role(:workers).env.clear["REDIS_URL"] + assert_equal "redis://a/b", @config_with_roles.role(:workers).env("1.1.1.3").clear["REDIS_URL"] - assert_equal "\n", @config_with_roles.role(:workers).env.secrets_io.string - assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], @config_with_roles.role(:workers).env_args + assert_equal "\n", @config_with_roles.role(:workers).env("1.1.1.3").secrets_io.string + assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], @config_with_roles.role(:workers).env_args("1.1.1.3") end test "container name" do @@ -86,7 +86,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase end test "env args" do - assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], @config_with_roles.role(:workers).env_args + assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], @config_with_roles.role(:workers).env_args("1.1.1.3") end test "env secret overwritten by role" do @@ -117,8 +117,8 @@ class ConfigurationRoleTest < ActiveSupport::TestCase DB_PASSWORD=secret&\"123 ENV - assert_equal expected_secrets_file, @config_with_roles.role(:workers).env.secrets_io.string - assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], @config_with_roles.role(:workers).env_args + assert_equal expected_secrets_file, @config_with_roles.role(:workers).env("1.1.1.3").secrets_io.string + assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], @config_with_roles.role(:workers).env_args("1.1.1.3") ensure ENV["REDIS_PASSWORD"] = nil ENV["DB_PASSWORD"] = nil @@ -141,8 +141,8 @@ class ConfigurationRoleTest < ActiveSupport::TestCase DB_PASSWORD=secret123 ENV - assert_equal expected_secrets_file, @config_with_roles.role(:workers).env.secrets_io.string - assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], @config_with_roles.role(:workers).env_args + assert_equal expected_secrets_file, @config_with_roles.role(:workers).env("1.1.1.3").secrets_io.string + assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], @config_with_roles.role(:workers).env_args("1.1.1.3") ensure ENV["DB_PASSWORD"] = nil end @@ -163,8 +163,8 @@ class ConfigurationRoleTest < ActiveSupport::TestCase REDIS_PASSWORD=secret456 ENV - assert_equal expected_secrets_file, @config_with_roles.role(:workers).env.secrets_io.string - assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], @config_with_roles.role(:workers).env_args + assert_equal expected_secrets_file, @config_with_roles.role(:workers).env("1.1.1.3").secrets_io.string + assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], @config_with_roles.role(:workers).env_args("1.1.1.3") ensure ENV["REDIS_PASSWORD"] = nil end @@ -191,14 +191,14 @@ class ConfigurationRoleTest < ActiveSupport::TestCase REDIS_PASSWORD=secret456 ENV - assert_equal expected_secrets_file, @config_with_roles.role(:workers).env.secrets_io.string - assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://c/d\"" ], @config_with_roles.role(:workers).env_args + assert_equal expected_secrets_file, @config_with_roles.role(:workers).env("1.1.1.3").secrets_io.string + assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://c/d\"" ], @config_with_roles.role(:workers).env_args("1.1.1.3") ensure ENV["REDIS_PASSWORD"] = nil end test "env secrets_file" do - assert_equal ".kamal/env/roles/app-workers.env", @config_with_roles.role(:workers).env.secrets_file + assert_equal ".kamal/env/roles/app-workers.env", @config_with_roles.role(:workers).env("1.1.1.3").secrets_file end test "uses cord" do diff --git a/test/fixtures/deploy_with_env_tags.yml b/test/fixtures/deploy_with_env_tags.yml new file mode 100644 index 00000000..4acc839c --- /dev/null +++ b/test/fixtures/deploy_with_env_tags.yml @@ -0,0 +1,29 @@ +service: app +image: dhh/app +servers: + web: + - 1.1.1.1: site1 + - 1.1.1.2: [ site1 experimental ] + - 1.2.1.1: site2 + - 1.2.1.2: site2 + workers: + - 1.1.1.3: site1 + - 1.1.1.4: site1 + - 1.2.1.3: site2 + - 1.2.1.4: [ site2 experimental ] +env: + clear: + TEST: "root" + EXPERIMENT: "disabled" +env_tags: + site1: + SITE: site1 + site2: + SITE: site2 + experimental: + env: + EXPERIMENT: "enabled" + +registry: + username: user + password: pw diff --git a/test/integration/docker/deployer/app/.env.erb b/test/integration/docker/deployer/app/.env.erb index cb2988d6..ea15ab06 100644 --- a/test/integration/docker/deployer/app/.env.erb +++ b/test/integration/docker/deployer/app/.env.erb @@ -1 +1,2 @@ SECRET_TOKEN='1234 with "中文"' +SECRET_TAG='TAGME' diff --git a/test/integration/docker/deployer/app/config/deploy.yml b/test/integration/docker/deployer/app/config/deploy.yml index 11e2cf04..aed676cc 100644 --- a/test/integration/docker/deployer/app/config/deploy.yml +++ b/test/integration/docker/deployer/app/config/deploy.yml @@ -2,13 +2,20 @@ service: app image: app servers: - vm1 - - vm2 + - vm2: [ tag1, tag2 ] env: clear: CLEAR_TOKEN: 4321 + CLEAR_TAG: "" HOST_TOKEN: "${HOST_TOKEN}" secret: - SECRET_TOKEN +env_tags: + tag1: + CLEAR_TAG: tagged + tag2: + secret: + - SECRET_TAG asset_path: /usr/share/nginx/html/versions registry: diff --git a/test/integration/main_test.rb b/test/integration/main_test.rb index ac549b97..a2dc8a0c 100644 --- a/test/integration/main_test.rb +++ b/test/integration/main_test.rb @@ -3,8 +3,7 @@ require_relative "integration_test" class MainTest < IntegrationTest test "envify, deploy, redeploy, rollback, details and audit" do kamal :envify - assert_local_env_file "SECRET_TOKEN='1234 with \"中文\"'" - assert_remote_env_file "SECRET_TOKEN=1234 with \"中文\"" + assert_env_files remove_local_env_file first_version = latest_app_version @@ -14,9 +13,7 @@ class MainTest < IntegrationTest kamal :deploy assert_app_is_up version: first_version assert_hooks_ran "pre-connect", "pre-build", "pre-deploy", "post-deploy" - assert_env :CLEAR_TOKEN, "4321", version: first_version - assert_env :HOST_TOKEN, "abcd", version: first_version - assert_env :SECRET_TOKEN, "1234 with \"中文\"", version: first_version + assert_envs version: first_version second_version = update_app_rev @@ -97,16 +94,38 @@ class MainTest < IntegrationTest assert_equal contents, deployer_exec("cat .env", capture: true) end - def assert_env(key, value, version:) - assert_equal "#{key}=#{value}", docker_compose("exec vm1 docker exec app-web-#{version} env | grep #{key}", capture: true) + def assert_envs(version:) + assert_env :CLEAR_TOKEN, "4321", version: version, vm: :vm1 + assert_env :HOST_TOKEN, "abcd", version: version, vm: :vm1 + assert_env :SECRET_TOKEN, "1234 with \"中文\"", version: version, vm: :vm1 + assert_no_env :CLEAR_TAG, version: version, vm: :vm1 + assert_no_env :SECRET_TAG, version: version, vm: :vm11 + assert_env :CLEAR_TAG, "tagged", version: version, vm: :vm2 + assert_env :SECRET_TAG, "TAGME", version: version, vm: :vm2 + end + + def assert_env(key, value, vm:, version:) + assert_equal "#{key}=#{value}", docker_compose("exec #{vm} docker exec app-web-#{version} env | grep #{key}", capture: true) + end + + def assert_no_env(key, vm:, version:) + assert_raises(RuntimeError, /exit 1/) do + docker_compose("exec #{vm} docker exec app-web-#{version} env | grep #{key}", capture: true) + end + end + + def assert_env_files + assert_local_env_file "SECRET_TOKEN='1234 with \"中文\"'\nSECRET_TAG='TAGME'" + assert_remote_env_file "SECRET_TOKEN=1234 with \"中文\"", vm: :vm1 + assert_remote_env_file "SECRET_TOKEN=1234 with \"中文\"\nSECRET_TAG=TAGME", vm: :vm2 end def remove_local_env_file deployer_exec("rm .env") end - def assert_remote_env_file(contents) - assert_equal contents, docker_compose("exec vm1 cat /root/.kamal/env/roles/app-web.env", capture: true) + def assert_remote_env_file(contents, vm:) + assert_equal contents, docker_compose("exec #{vm} cat /root/.kamal/env/roles/app-web.env", capture: true) end def assert_no_remote_env_file