diff --git a/lib/kamal/cli/base.rb b/lib/kamal/cli/base.rb index 9a38d97e..7cd69fc6 100644 --- a/lib/kamal/cli/base.rb +++ b/lib/kamal/cli/base.rb @@ -14,8 +14,8 @@ module Kamal::Cli class_option :version, desc: "Run commands against a specific app version" class_option :primary, type: :boolean, aliases: "-p", desc: "Run commands only on primary host instead of all" - class_option :hosts, aliases: "-h", desc: "Run commands on these hosts instead of all (separate by comma)" - class_option :roles, aliases: "-r", desc: "Run commands on these roles instead of all (separate by comma)" + class_option :hosts, aliases: "-h", desc: "Run commands on these hosts instead of all (separate by comma, supports wildcards with *)" + class_option :roles, aliases: "-r", desc: "Run commands on these roles instead of all (separate by comma, supports wildcards with *)" class_option :config_file, aliases: "-c", default: "config/deploy.yml", desc: "Path to config file" class_option :destination, aliases: "-d", desc: "Specify destination to be used for config file (staging -> deploy.staging.yml)" diff --git a/lib/kamal/cli/healthcheck.rb b/lib/kamal/cli/healthcheck.rb index 54d01c03..b3dbf6bd 100644 --- a/lib/kamal/cli/healthcheck.rb +++ b/lib/kamal/cli/healthcheck.rb @@ -3,7 +3,7 @@ class Kamal::Cli::Healthcheck < Kamal::Cli::Base desc "perform", "Health check current app version" def perform - raise "The primary host is not configured to run Traefik" unless KAMAL.config.primary_role.running_traefik? + raise "The primary host is not configured to run Traefik" unless KAMAL.config.role(KAMAL.config.primary_role).running_traefik? on(KAMAL.primary_host) do begin execute *KAMAL.healthcheck.run diff --git a/lib/kamal/cli/main.rb b/lib/kamal/cli/main.rb index c8f0411d..6adc36fe 100644 --- a/lib/kamal/cli/main.rb +++ b/lib/kamal/cli/main.rb @@ -38,7 +38,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base say "Ensure Traefik is running...", :magenta invoke "kamal:cli:traefik:boot", [], invoke_options - if KAMAL.config.primary_role.running_traefik? + if KAMAL.config.role(KAMAL.config.primary_role).running_traefik? say "Ensure app can pass healthcheck...", :magenta invoke "kamal:cli:healthcheck:perform", [], invoke_options end diff --git a/lib/kamal/cli/templates/deploy.yml b/lib/kamal/cli/templates/deploy.yml index 65b3062d..20bbe71a 100644 --- a/lib/kamal/cli/templates/deploy.yml +++ b/lib/kamal/cli/templates/deploy.yml @@ -83,3 +83,15 @@ registry: # boot: # limit: 10 # Can also specify as a percentage of total hosts, such as "25%" # wait: 2 + +# Configure the role used to determine the primary_host. This host takes +# deploy locks, runs health checks during the deploy, and follow logs, etc. +# +# Caution: there's no support for role renaming yet, so be careful to cleanup +# the previous role on the deployed hosts. +# primary_role: web + +# Controls if we abort when see a role with no hosts. Disabling this may be +# useful for more complex deploy configurations. +# +# allow_empty_roles: false diff --git a/lib/kamal/commander.rb b/lib/kamal/commander.rb index 869568ae..35939fe7 100644 --- a/lib/kamal/commander.rb +++ b/lib/kamal/commander.rb @@ -28,11 +28,11 @@ class Kamal::Commander end def specific_roles=(role_names) - @specific_roles = config.roles.select { |r| role_names.include?(r.name) } if role_names.present? + @specific_roles = Kamal::Utils.filter_specific_items(role_names, config.roles) if role_names.present? end def specific_hosts=(hosts) - @specific_hosts = config.all_hosts & hosts if hosts.present? + @specific_hosts = Kamal::Utils.filter_specific_items(hosts, config.all_hosts) if hosts.present? end def primary_host diff --git a/lib/kamal/commands/base.rb b/lib/kamal/commands/base.rb index 7552a92d..3ce9b325 100644 --- a/lib/kamal/commands/base.rb +++ b/lib/kamal/commands/base.rb @@ -18,7 +18,7 @@ module Kamal::Commands elsif config.ssh.proxy && config.ssh.proxy.is_a?(Net::SSH::Proxy::Command) cmd << " -o ProxyCommand='#{config.ssh.proxy.command_line_template}'" end - cmd << " -t #{config.ssh.user}@#{host} '#{command.join(" ")}'" + cmd << " -t #{config.ssh.user}@#{host} -p #{config.ssh.port} '#{command.join(" ")}'" end end diff --git a/lib/kamal/commands/healthcheck.rb b/lib/kamal/commands/healthcheck.rb index 5adc0244..35bf9562 100644 --- a/lib/kamal/commands/healthcheck.rb +++ b/lib/kamal/commands/healthcheck.rb @@ -1,7 +1,7 @@ class Kamal::Commands::Healthcheck < Kamal::Commands::Base def run - web = config.role(:web) + primary = config.role(config.primary_role) docker :run, "--detach", @@ -9,12 +9,12 @@ 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}\"", - *web.env_args, - *web.health_check_args(cord: false), + *primary.env_args, + *primary.health_check_args(cord: false), *config.volume_args, - *web.option_args, + *primary.option_args, config.absolute_image, - web.cmd + primary.cmd end def status diff --git a/lib/kamal/commands/traefik.rb b/lib/kamal/commands/traefik.rb index 39ba796d..c6ec8c8d 100644 --- a/lib/kamal/commands/traefik.rb +++ b/lib/kamal/commands/traefik.rb @@ -6,6 +6,14 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base DEFAULT_ARGS = { 'log.level' => 'DEBUG' } + DEFAULT_LABELS = { + # These ensure we serve a 502 rather than a 404 if no containers are available + "traefik.http.routers.catchall.entryPoints" => "http", + "traefik.http.routers.catchall.rule" => "PathPrefix(`/`)", + "traefik.http.routers.catchall.service" => "unavailable", + "traefik.http.routers.catchall.priority" => 1, + "traefik.http.services.unavailable.loadbalancer.server.port" => "0" + } def run docker :run, "--name traefik", @@ -97,7 +105,7 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base end def labels - config.traefik["labels"] || [] + DEFAULT_LABELS.merge(config.traefik["labels"] || {}) end def image diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index b4a0ecd5..e5ecbb53 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -25,7 +25,9 @@ class Kamal::Configuration def load_config_file(file) if file.exist? - YAML.load(ERB.new(IO.read(file)).result).symbolize_keys + # Newer Psych doesn't load aliases by default + load_method = YAML.respond_to?(:unsafe_load) ? :unsafe_load : :load + YAML.send(load_method, ERB.new(IO.read(file)).result).symbolize_keys else raise "Configuration file not found in #{file}" end @@ -90,18 +92,21 @@ class Kamal::Configuration end def primary_host - primary_role.primary_host + role(primary_role)&.primary_host end - def primary_role - role(:web) || roles.first + def traefik_roles + roles.select(&:running_traefik?) + end + + def traefik_role_names + traefik_roles.flat_map(&:name) end def traefik_hosts - roles.select(&:running_traefik?).flat_map(&:hosts).uniq + traefik_roles.flat_map(&:hosts).uniq end - def repository [ raw_config.registry["server"], image ].compact.join("/") end @@ -203,6 +208,14 @@ class Kamal::Configuration raw_config.asset_path end + def primary_role + raw_config.primary_role || "web" + end + + def allow_empty_roles? + raw_config.allow_empty_roles + end + def valid? ensure_destination_if_required && ensure_required_keys_present && ensure_valid_kamal_version @@ -251,9 +264,19 @@ class Kamal::Configuration raise ArgumentError, "You must specify a password for the registry in config/deploy.yml (or set the ENV variable if that's used)" end - roles.each do |role| - if role.hosts.empty? - raise ArgumentError, "No servers specified for the #{role.name} role" + unless role_names.include?(primary_role) + raise ArgumentError, "The primary_role #{primary_role} isn't defined" + end + + if role(primary_role).hosts.empty? + raise ArgumentError, "No servers specified for the #{primary_role} primary_role" + end + + unless allow_empty_roles? + roles.each do |role| + if role.hosts.empty? + raise ArgumentError, "No servers specified for the #{role.name} role. You can ignore this with allow_empty_roles: true" + end end end diff --git a/lib/kamal/configuration/role.rb b/lib/kamal/configuration/role.rb index 2945f87b..da88d406 100644 --- a/lib/kamal/configuration/role.rb +++ b/lib/kamal/configuration/role.rb @@ -93,7 +93,15 @@ class Kamal::Configuration::Role def running_traefik? - name.web? || specializations["traefik"] + if specializations["traefik"].nil? + primary? + else + specializations["traefik"] + end + end + + def primary? + @config.primary_role == name end @@ -185,6 +193,7 @@ class Kamal::Configuration::Role "traefik.http.services.#{traefik_service}.loadbalancer.server.scheme" => "http", "traefik.http.routers.#{traefik_service}.rule" => "PathPrefix(`/`)", + "traefik.http.routers.#{traefik_service}.priority" => "2", "traefik.http.middlewares.#{traefik_service}-retry.retry.attempts" => "5", "traefik.http.middlewares.#{traefik_service}-retry.retry.initialinterval" => "500ms", "traefik.http.routers.#{traefik_service}.middlewares" => "#{traefik_service}-retry@docker" diff --git a/lib/kamal/configuration/ssh.rb b/lib/kamal/configuration/ssh.rb index 57a740b7..b9630102 100644 --- a/lib/kamal/configuration/ssh.rb +++ b/lib/kamal/configuration/ssh.rb @@ -9,6 +9,10 @@ class Kamal::Configuration::Ssh config.fetch("user", "root") end + def port + config.fetch("port", 22) + end + def proxy if (proxy = config["proxy"]) Net::SSH::Proxy::Jump.new(proxy.include?("@") ? proxy : "root@#{proxy}") @@ -18,7 +22,7 @@ class Kamal::Configuration::Ssh end def options - { user: user, proxy: proxy, logger: logger, keepalive: true, keepalive_interval: 30 }.compact + { user: user, port: port, proxy: proxy, logger: logger, keepalive: true, keepalive_interval: 30 }.compact end def to_h diff --git a/lib/kamal/utils.rb b/lib/kamal/utils.rb index da16633e..54185cf4 100644 --- a/lib/kamal/utils.rb +++ b/lib/kamal/utils.rb @@ -58,4 +58,20 @@ module Kamal::Utils .gsub(/`/, '\\\\`') .gsub(DOLLAR_SIGN_WITHOUT_SHELL_EXPANSION_REGEX, '\$') end + + # Apply a list of host or role filters, including wildcard matches + def filter_specific_items(filters, items) + matches = [] + + Array(filters).select do |filter| + matches += Array(items).select do |item| + # Only allow * for a wildcard + pattern = Regexp.escape(filter).gsub('\*', '.*') + # items are roles or hosts + (item.respond_to?(:name) ? item.name : item).match(/^#{pattern}$/) + end + end + + matches + end end diff --git a/test/cli/accessory_test.rb b/test/cli/accessory_test.rb index 5ecf8038..78623808 100644 --- a/test/cli/accessory_test.rb +++ b/test/cli/accessory_test.rb @@ -97,7 +97,7 @@ class CliAccessoryTest < CliTestCase test "logs with follow" do SSHKit::Backend::Abstract.any_instance.stubs(:exec) - .with("ssh -t root@1.1.1.3 'docker logs app-mysql --timestamps --tail 10 --follow 2>&1'") + .with("ssh -t root@1.1.1.3 -p 22 'docker logs app-mysql --timestamps --tail 10 --follow 2>&1'") assert_match "docker logs app-mysql --timestamps --tail 10 --follow 2>&1", run_command("logs", "mysql", "--follow") end diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index fd106421..206d8524 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -172,7 +172,7 @@ class CliAppTest < CliTestCase test "exec interactive" do SSHKit::Backend::Abstract.any_instance.expects(:exec) - .with("ssh -t root@1.1.1.1 'docker run -it --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v'") + .with("ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v'") run_command("exec", "-i", "ruby -v").tap do |output| assert_match "Get most recent version available as an image...", output assert_match "Launching interactive command with version latest via SSH from new container on 1.1.1.1...", output @@ -181,7 +181,7 @@ class CliAppTest < CliTestCase test "exec interactive with reuse" do SSHKit::Backend::Abstract.any_instance.expects(:exec) - .with("ssh -t root@1.1.1.1 'docker exec -it app-web-999 ruby -v'") + .with("ssh -t root@1.1.1.1 -p 22 'docker exec -it app-web-999 ruby -v'") run_command("exec", "-i", "--reuse", "ruby -v").tap do |output| assert_match "Get current version of running container...", output assert_match "Running docker ps --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest --format \"{{.Names}}\" | while read line; do echo ${line#app-web-}; done on 1.1.1.1", output @@ -210,7 +210,7 @@ class CliAppTest < CliTestCase test "logs with follow" do SSHKit::Backend::Abstract.any_instance.stubs(:exec) - .with("ssh -t root@1.1.1.1 'docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --timestamps --tail 10 --follow 2>&1'") + .with("ssh -t root@1.1.1.1 -p 22 'docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --timestamps --tail 10 --follow 2>&1'") assert_match "docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --timestamps --tail 10 --follow 2>&1", run_command("logs", "--follow") end diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index 852602c6..8b152d52 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -298,6 +298,16 @@ class CliMainTest < CliTestCase end end + test "config with primary web role override" do + run_command("config", config_file: "deploy_primary_web_role_override").tap do |output| + config = YAML.load(output) + + assert_equal ["web_chicago", "web_tokyo"], config[:roles] + assert_equal ["1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4"], config[:hosts] + assert_equal "1.1.1.3", config[:primary_host] + end + end + test "config with destination" do run_command("config", "-d", "world", config_file: "deploy_for_dest").tap do |output| config = YAML.load(output) @@ -311,6 +321,19 @@ class CliMainTest < CliTestCase end end + test "config with aliases" do + run_command("config", config_file: "deploy_with_aliases").tap do |output| + config = YAML.load(output) + + assert_equal ["web", "web_tokyo", "workers", "workers_tokyo"], config[:roles] + assert_equal ["1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4"], config[:hosts] + assert_equal "999", config[:version] + assert_equal "registry.digitalocean.com/dhh/app", config[:repository] + assert_equal "registry.digitalocean.com/dhh/app:999", config[:absolute_image] + assert_equal "app-999", config[:service_with_version] + end + end + test "init" do Pathname.any_instance.expects(:exist?).returns(false).times(3) Pathname.any_instance.stubs(:mkpath) diff --git a/test/cli/traefik_test.rb b/test/cli/traefik_test.rb index 6c6fbf64..5e11df92 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 --env-file .kamal/env/traefik/traefik.env --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\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::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 --env-file .kamal/env/traefik/traefik.env --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\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output end end @@ -64,7 +64,7 @@ class CliTraefikTest < CliTestCase test "logs with follow" do SSHKit::Backend::Abstract.any_instance.stubs(:exec) - .with("ssh -t root@1.1.1.1 'docker logs traefik --timestamps --tail 10 --follow 2>&1'") + .with("ssh -t root@1.1.1.1 -p 22 'docker logs traefik --timestamps --tail 10 --follow 2>&1'") assert_match "docker logs traefik --timestamps --tail 10 --follow", run_command("logs", "--follow") end diff --git a/test/commander_test.rb b/test/commander_test.rb index 2290e0f0..06cc5bbb 100644 --- a/test/commander_test.rb +++ b/test/commander_test.rb @@ -14,6 +14,18 @@ class CommanderTest < ActiveSupport::TestCase @kamal.specific_hosts = [ "1.1.1.1", "1.1.1.2" ] assert_equal [ "1.1.1.1", "1.1.1.2" ], @kamal.hosts + + @kamal.specific_hosts = [ "1.1.1.1*" ] + assert_equal [ "1.1.1.1" ], @kamal.hosts + + @kamal.specific_hosts = [ "1.1.1.*", "*.1.2.*" ] + assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], @kamal.hosts + + @kamal.specific_hosts = [ "*" ] + assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], @kamal.hosts + + @kamal.specific_hosts = [ "*miss" ] + assert_equal [], @kamal.hosts end test "filtering hosts by filtering roles" do @@ -28,6 +40,18 @@ class CommanderTest < ActiveSupport::TestCase @kamal.specific_roles = [ "workers" ] assert_equal [ "workers" ], @kamal.roles.map(&:name) + + @kamal.specific_roles = [ "w*" ] + assert_equal [ "web", "workers" ], @kamal.roles.map(&:name) + + @kamal.specific_roles = [ "we*", "*orkers" ] + assert_equal [ "web", "workers" ], @kamal.roles.map(&:name) + + @kamal.specific_roles = [ "*" ] + assert_equal [ "web", "workers" ], @kamal.roles.map(&:name) + + @kamal.specific_roles = [ "*miss" ] + assert_equal [], @kamal.roles.map(&:name) end test "filtering roles by filtering hosts" do diff --git a/test/commands/accessory_test.rb b/test/commands/accessory_test.rb index 2825f3de..687c7fe5 100644 --- a/test/commands/accessory_test.rb +++ b/test/commands/accessory_test.rb @@ -128,7 +128,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase test "follow logs" do assert_equal \ - "ssh -t root@1.1.1.5 'docker logs app-mysql --timestamps --tail 10 --follow 2>&1'", + "ssh -t root@1.1.1.5 -p 22 'docker logs app-mysql --timestamps --tail 10 --follow 2>&1'", new_command(:mysql).follow_logs end diff --git a/test/commands/app_test.rb b/test/commands/app_test.rb index 13abad78..c2e335a3 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -14,13 +14,13 @@ class CommandsAppTest < ActiveSupport::TestCase test "run" do assert_equal \ - "docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/up || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label 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\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/up || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999", new_command.run.join(" ") end test "run with hostname" do assert_equal \ - "docker run --detach --restart unless-stopped --name app-web-999 --hostname myhost -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/up || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label 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\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/up || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label 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(hostname: "myhost").join(" ") end @@ -28,7 +28,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 KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/up || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label 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\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/up || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label 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 @@ -36,7 +36,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 KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/healthz || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label 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\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/healthz || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label 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 @@ -44,7 +44,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 KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(/bin/up) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label 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\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(/bin/up) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label 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 @@ -52,7 +52,7 @@ 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 KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(/bin/healthy) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label 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\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(/bin/healthy) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label 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 @@ -67,7 +67,7 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } } assert_equal \ - "docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/up || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label 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\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/up || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label 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 @@ -190,32 +190,37 @@ class CommandsAppTest < ActiveSupport::TestCase end test "run over ssh" do - assert_equal "ssh -t root@1.1.1.1 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1") + assert_equal "ssh -t root@1.1.1.1 -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1") end test "run over ssh with custom user" do @config[:ssh] = { "user" => "app" } - assert_equal "ssh -t app@1.1.1.1 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1") + assert_equal "ssh -t app@1.1.1.1 -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1") + end + + test "run over ssh with custom port" do + @config[:ssh] = { "port" => "2222" } + assert_equal "ssh -t root@1.1.1.1 -p 2222 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1") end test "run over ssh with proxy" do @config[:ssh] = { "proxy" => "2.2.2.2" } - assert_equal "ssh -J root@2.2.2.2 -t root@1.1.1.1 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1") + assert_equal "ssh -J root@2.2.2.2 -t root@1.1.1.1 -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1") end test "run over ssh with proxy user" do @config[:ssh] = { "proxy" => "app@2.2.2.2" } - assert_equal "ssh -J app@2.2.2.2 -t root@1.1.1.1 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1") + assert_equal "ssh -J app@2.2.2.2 -t root@1.1.1.1 -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1") end test "run over ssh with custom user with proxy" do @config[:ssh] = { "user" => "app", "proxy" => "2.2.2.2" } - assert_equal "ssh -J root@2.2.2.2 -t app@1.1.1.1 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1") + assert_equal "ssh -J root@2.2.2.2 -t app@1.1.1.1 -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1") end test "run over ssh with proxy_command" do @config[:ssh] = { "proxy_command" => "ssh -W %h:%p user@proxy-server" } - assert_equal "ssh -o ProxyCommand='ssh -W %h:%p user@proxy-server' -t root@1.1.1.1 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1") + assert_equal "ssh -o ProxyCommand='ssh -W %h:%p user@proxy-server' -t root@1.1.1.1 -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1") end test "current_running_container_id" do diff --git a/test/commands/traefik_test.rb b/test/commands/traefik_test.rb index 7dfcff97..53fb3cd3 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 --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\"", + "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") @config[:traefik]["host_port"] = "8080" assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 8080:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{@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\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") @config[:traefik]["publish"] = false assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{@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\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") end test "run with ports configured" do assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{@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.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") @config[:traefik]["options"] = {"publish" => %w[9000:9000 9001:9001]} assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --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\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --publish \"9000:9000\" --publish \"9001:9001\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") end test "run with volumes configured" do assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{@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.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") @config[:traefik]["options"] = {"volume" => %w[./letsencrypt/acme.json:/letsencrypt/acme.json] } assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --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\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") end test "run with several options configured" do assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{@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.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") @config[:traefik]["options"] = {"volume" => %w[./letsencrypt/acme.json:/letsencrypt/acme.json], "publish" => %w[8080:8080], "memory" => "512m"} assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --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\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" --publish \"8080:8080\" --memory \"512m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") end test "run with labels configured" do assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{@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.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") @config[:traefik]["labels"] = { "traefik.http.routers.dashboard.service" => "api@internal", "traefik.http.routers.dashboard.middlewares" => "auth" } assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.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.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --label traefik.http.routers.dashboard.service=\"api@internal\" --label traefik.http.routers.dashboard.middlewares=\"auth\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") end test "run with env configured" do assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{@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.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") @config[:traefik]["env"] = { "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 --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\"", + "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") 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 --env-file .kamal/env/traefik/traefik.env --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\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::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 --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\"", + "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") end @@ -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 --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\"", + "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"ERROR\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") end @@ -167,13 +167,13 @@ class CommandsTraefikTest < ActiveSupport::TestCase test "traefik follow logs" do assert_equal \ - "ssh -t root@1.1.1.1 'docker logs traefik --timestamps --tail 10 --follow 2>&1'", + "ssh -t root@1.1.1.1 -p 22 'docker logs traefik --timestamps --tail 10 --follow 2>&1'", new_command.follow_logs(host: @config[:servers].first) end test "traefik follow logs with grep hello!" do assert_equal \ - "ssh -t root@1.1.1.1 'docker logs traefik --timestamps --tail 10 --follow 2>&1 | grep \"hello!\"'", + "ssh -t root@1.1.1.1 -p 22 'docker logs traefik --timestamps --tail 10 --follow 2>&1 | grep \"hello!\"'", new_command.follow_logs(host: @config[:servers].first, grep: 'hello!') end diff --git a/test/configuration/role_test.rb b/test/configuration/role_test.rb index 6966e893..6aa4b343 100644 --- a/test/configuration/role_test.rb +++ b/test/configuration/role_test.rb @@ -42,7 +42,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase end test "special label args for web" do - assert_equal [ "--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\"" ], @config.role(:web).label_args + assert_equal [ "--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.routers.app-web.priority=\"2\"", "--label", "traefik.http.middlewares.app-web-retry.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\"", "--label", "traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\"" ], @config.role(:web).label_args end test "custom labels" do @@ -66,7 +66,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase c[:servers]["beta"] = { "traefik" => "true", "hosts" => [ "1.1.1.5" ] } }) - assert_equal [ "--label", "service=\"app\"", "--label", "role=\"beta\"", "--label", "traefik.http.services.app-beta.loadbalancer.server.scheme=\"http\"", "--label", "traefik.http.routers.app-beta.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.middlewares.app-beta-retry.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app-beta-retry.retry.initialinterval=\"500ms\"", "--label", "traefik.http.routers.app-beta.middlewares=\"app-beta-retry@docker\"" ], config.role(:beta).label_args + assert_equal [ "--label", "service=\"app\"", "--label", "role=\"beta\"", "--label", "traefik.http.services.app-beta.loadbalancer.server.scheme=\"http\"", "--label", "traefik.http.routers.app-beta.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.routers.app-beta.priority=\"2\"", "--label", "traefik.http.middlewares.app-beta-retry.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app-beta-retry.retry.initialinterval=\"500ms\"", "--label", "traefik.http.routers.app-beta.middlewares=\"app-beta-retry@docker\"" ], config.role(:beta).label_args end test "env overwritten by role" do diff --git a/test/configuration/ssh_test.rb b/test/configuration/ssh_test.rb index cb3d5f5c..438a0f45 100644 --- a/test/configuration/ssh_test.rb +++ b/test/configuration/ssh_test.rb @@ -22,6 +22,9 @@ class ConfigurationSshTest < ActiveSupport::TestCase config = Kamal::Configuration.new(@deploy.tap { |c| c.merge!(ssh: { "log_level" => "debug" }) }) assert_equal 0, config.ssh.options[:logger].level + + config = Kamal::Configuration.new(@deploy.tap { |c| c.merge!(ssh: { "port" => 2222 }) }) + assert_equal 2222, config.ssh.options[:port] end test "ssh options with proxy host" do diff --git a/test/configuration_test.rb b/test/configuration_test.rb index 62693b2a..392b6afb 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -165,6 +165,16 @@ class ConfigurationTest < ActiveSupport::TestCase end end + test "allow_empty_roles" do + assert_silent do + Kamal::Configuration.new @deploy.merge(servers: { "web" => %w[ web ], "workers" => { "hosts" => %w[ ] } }, allow_empty_roles: true) + end + + assert_raises(ArgumentError) do + Kamal::Configuration.new @deploy.merge(servers: { "web" => %w[], "workers" => { "hosts" => %w[] } }, allow_empty_roles: true) + end + end + test "volume_args" do assert_equal ["--volume", "/local/path:/container/path"], @config.volume_args end @@ -227,7 +237,7 @@ class ConfigurationTest < ActiveSupport::TestCase :repository=>"dhh/app", :absolute_image=>"dhh/app:missing", :service_with_version=>"app-missing", - :ssh_options=>{ :user=>"root", log_level: :fatal, keepalive: true, keepalive_interval: 30 }, + :ssh_options=>{ :user=>"root", port: 22, log_level: :fatal, keepalive: true, keepalive_interval: 30 }, :sshkit=>{}, :volume_args=>["--volume", "/local/path:/container/path"], :builder=>{}, @@ -278,4 +288,25 @@ class ConfigurationTest < ActiveSupport::TestCase assert_nil @config.asset_path assert_equal "foo", Kamal::Configuration.new(@deploy.merge!(asset_path: "foo")).asset_path end + + test "primary role" do + assert_equal "web", @config.primary_role + + config = Kamal::Configuration.new(@deploy_with_roles.deep_merge({ + servers: { "alternate_web" => { "hosts" => [ "1.1.1.4", "1.1.1.5" ] } }, + primary_role: "alternate_web" } )) + + + assert_equal "alternate_web", config.primary_role + assert_equal "1.1.1.4", config.primary_host + assert config.role(:alternate_web).primary? + assert config.role(:alternate_web).running_traefik? + end + + test "primary role missing" do + error = assert_raises(ArgumentError) do + Kamal::Configuration.new(@deploy.merge(primary_role: "bar")) + end + assert_match /bar isn't defined/, error.message + end end diff --git a/test/fixtures/deploy_primary_web_role_override.yml b/test/fixtures/deploy_primary_web_role_override.yml new file mode 100644 index 00000000..e372c602 --- /dev/null +++ b/test/fixtures/deploy_primary_web_role_override.yml @@ -0,0 +1,20 @@ +service: app +image: dhh/app +servers: + web_chicago: + traefik: enabled + hosts: + - 1.1.1.1 + - 1.1.1.2 + web_tokyo: + traefik: enabled + hosts: + - 1.1.1.3 + - 1.1.1.4 +env: + REDIS_URL: redis://x/y +registry: + server: registry.digitalocean.com + username: user + password: pw +primary_role: web_tokyo diff --git a/test/fixtures/deploy_with_aliases.yml b/test/fixtures/deploy_with_aliases.yml new file mode 100644 index 00000000..90c18b57 --- /dev/null +++ b/test/fixtures/deploy_with_aliases.yml @@ -0,0 +1,36 @@ +# helper aliases +chicago_hosts: &chicago_hosts + hosts: + - 1.1.1.1 + - 1.1.1.2 +tokyo_hosts: &tokyo_hosts + hosts: + - 1.1.1.3 + - 1.1.1.4 +web_common: &web_common + env: + ROLE: "web" + traefik: true + +# actual config +service: app +image: dhh/app +servers: + web: + <<: *chicago_hosts + <<: *web_common + web_tokyo: + <<: *tokyo_hosts + <<: *web_common + workers: + cmd: bin/jobs + <<: *chicago_hosts + workers_tokyo: + cmd: bin/jobs + <<: *tokyo_hosts +env: + REDIS_URL: redis://x/y +registry: + server: registry.digitalocean.com + username: user + password: pw diff --git a/test/fixtures/deploy_workers_only.yml b/test/fixtures/deploy_workers_only.yml index f12e650e..75cbbde1 100644 --- a/test/fixtures/deploy_workers_only.yml +++ b/test/fixtures/deploy_workers_only.yml @@ -2,9 +2,11 @@ service: app image: dhh/app servers: workers: + traefik: false hosts: - 1.1.1.1 - 1.1.1.2 +primary_role: workers registry: username: user password: pw diff --git a/test/integration/app_test.rb b/test/integration/app_test.rb index 37a4dd71..9824ce0d 100644 --- a/test/integration/app_test.rb +++ b/test/integration/app_test.rb @@ -10,8 +10,7 @@ class AppTest < IntegrationTest kamal :app, :stop - # traefik is up and returns 404s when it can't match a route - assert_app_not_found + assert_app_is_down kamal :app, :start @@ -51,7 +50,6 @@ class AppTest < IntegrationTest kamal :app, :remove - # traefik is up and returns 404s when it can't match a route - assert_app_not_found + assert_app_is_down end end diff --git a/test/integration/integration_test.rb b/test/integration/integration_test.rb index b352b714..950e41ed 100644 --- a/test/integration/integration_test.rb +++ b/test/integration/integration_test.rb @@ -55,12 +55,6 @@ class IntegrationTest < ActiveSupport::TestCase assert_app_version(version, response) if version end - def assert_app_not_found - response = app_response - debug_response_code(response, "404") - assert_equal "404", response.code - end - def wait_for_app_to_be_up(timeout: 20, up_count: 3) timeout_at = Time.now + timeout up_times = 0 diff --git a/test/integration/main_test.rb b/test/integration/main_test.rb index 1171c3de..a0364fb2 100644 --- a/test/integration/main_test.rb +++ b/test/integration/main_test.rb @@ -54,7 +54,7 @@ class MainTest < IntegrationTest assert_equal "registry:4443/app:#{version}", config[:absolute_image] assert_equal "app-#{version}", config[:service_with_version] assert_equal [], config[:volume_args] - assert_equal({ user: "root", keepalive: true, keepalive_interval: 30, log_level: :fatal }, config[:ssh_options]) + assert_equal({ user: "root", port: 22, 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, "cord"=>"/tmp/kamal-cord", "log_lines" => 50, "cmd"=>"wget -qO- http://localhost > /dev/null || exit 1" }, config[:healthcheck])