From eab717e0cf88ef06bcdb821f65affce6d855653b Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 17 Jul 2024 16:04:46 +0100 Subject: [PATCH 01/43] Add kamal-proxy in experimental mode The proxy can be enabled via the config: ``` proxy: enabled: true hosts: - 10.0.0.1 - 10.0.0.2 ``` This will enable the proxy and cause it to be run on the hosts listed under `hosts`, after running `kamal proxy reboot`. Enabling the proxy disables `kamal traefik` commands and replaces them with `kamal proxy` ones. However only the marked hosts will run the kamal-proxy container, the rest will run Traefik as before. --- bin/docs | 1 + lib/kamal/cli.rb | 1 + lib/kamal/cli/app.rb | 23 ++- lib/kamal/cli/app/boot.rb | 24 ++- lib/kamal/cli/main.rb | 24 ++- lib/kamal/cli/proxy.rb | 160 ++++++++++++++++++ lib/kamal/cli/traefik.rb | 17 ++ lib/kamal/commander.rb | 10 +- lib/kamal/commander/specifics.rb | 9 + lib/kamal/commands/app.rb | 18 ++ lib/kamal/commands/app/containers.rb | 7 + lib/kamal/commands/proxy.rb | 69 ++++++++ lib/kamal/configuration.rb | 7 +- .../configuration/docs/configuration.yml | 6 + lib/kamal/configuration/docs/proxy.yml | 83 +++++++++ lib/kamal/configuration/proxy.rb | 57 +++++++ lib/kamal/configuration/role.rb | 8 + test/cli/app_test.rb | 12 ++ test/cli/proxy_test.rb | 141 +++++++++++++++ test/commands/proxy_test.rb | 126 ++++++++++++++ test/fixtures/deploy_with_proxy.yml | 42 +++++ test/integration/broken_deploy_test.rb | 1 - .../docker/deployer/app/Dockerfile | 2 +- .../docker/deployer/app_with_roles/Dockerfile | 2 +- .../deployer/app_with_roles/config/deploy.yml | 5 + test/integration/integration_test.rb | 2 +- test/integration/proxy_test.rb | 84 +++++++++ 27 files changed, 924 insertions(+), 17 deletions(-) create mode 100644 lib/kamal/cli/proxy.rb create mode 100644 lib/kamal/commands/proxy.rb create mode 100644 lib/kamal/configuration/docs/proxy.yml create mode 100644 lib/kamal/configuration/proxy.rb create mode 100644 test/cli/proxy_test.rb create mode 100644 test/commands/proxy_test.rb create mode 100644 test/fixtures/deploy_with_proxy.yml create mode 100644 test/integration/proxy_test.rb diff --git a/bin/docs b/bin/docs index a8731ce27..437c5ce71 100755 --- a/bin/docs +++ b/bin/docs @@ -24,6 +24,7 @@ DOCS = { "env" => "Environment variables", "healthcheck" => "Healthchecks", "logging" => "Logging", + "proxy" => "Proxy (Experimental)", "registry" => "Docker Registry", "role" => "Roles", "servers" => "Servers", diff --git a/lib/kamal/cli.rb b/lib/kamal/cli.rb index 6772556e3..dc35c4031 100644 --- a/lib/kamal/cli.rb +++ b/lib/kamal/cli.rb @@ -1,4 +1,5 @@ module Kamal::Cli + class BootError < StandardError; end class HookError < StandardError; end class LockError < StandardError; end end diff --git a/lib/kamal/cli/app.rb b/lib/kamal/cli/app.rb index ce21b2cf2..732addae9 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -38,8 +38,17 @@ def start roles = KAMAL.roles_on(host) roles.each do |role| + app = KAMAL.app(role: role, host: host) execute *KAMAL.auditor.record("Started app version #{KAMAL.config.version}"), verbosity: :debug - execute *KAMAL.app(role: role, host: host).start, raise_on_non_zero_exit: false + execute *app.start, raise_on_non_zero_exit: false + + if role.running_traefik? && KAMAL.proxy_host?(host) + version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip + endpoint = capture_with_info(*app.container_endpoint(version: version)).strip + raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, did the container boot?" if endpoint.empty? + + execute *KAMAL.proxy.deploy(role.container_prefix, target: endpoint) + end end end end @@ -52,8 +61,18 @@ def stop roles = KAMAL.roles_on(host) roles.each do |role| + app = KAMAL.app(role: role, host: host) execute *KAMAL.auditor.record("Stopped app", role: role), verbosity: :debug - execute *KAMAL.app(role: role, host: host).stop, raise_on_non_zero_exit: false + + if role.running_traefik? && KAMAL.proxy_host?(host) + version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip + endpoint = capture_with_info(*app.container_endpoint(version: version)).strip + if endpoint.present? + execute *KAMAL.proxy.remove(role.container_prefix, target: endpoint), raise_on_non_zero_exit: false + end + end + + execute *app.stop, raise_on_non_zero_exit: false end end end diff --git a/lib/kamal/cli/app/boot.rb b/lib/kamal/cli/app/boot.rb index df3e69254..b6a094773 100644 --- a/lib/kamal/cli/app/boot.rb +++ b/lib/kamal/cli/app/boot.rb @@ -45,15 +45,25 @@ def old_version_renamed_if_clashing def start_new_version audit "Booted app version #{version}" - - execute *app.tie_cord(role.cord_host_file) if uses_cord? hostname = "#{host.to_s[0...51].gsub(/\.+$/, '')}-#{SecureRandom.hex(6)}" execute *app.ensure_env_directory upload! role.secrets_io(host), role.secrets_path, mode: "0600" - execute *app.run(hostname: hostname) - Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) } + if proxy_host? + execute *app.run_for_proxy(hostname: hostname) + if running_traefik? + endpoint = capture_with_info(*app.container_endpoint(version: version)).strip + raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, did the container boot?" if endpoint.empty? + execute *KAMAL.proxy.deploy(role.container_prefix, target: endpoint) + else + Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) } + end + else + execute *app.tie_cord(role.cord_host_file) if uses_cord? + execute *app.run(hostname: hostname) + Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) } + end end def stop_new_version @@ -61,7 +71,7 @@ def stop_new_version end def stop_old_version(version) - if uses_cord? + if uses_cord? && !proxy_host? cord = capture_with_info(*app.cord(version: version), raise_on_non_zero_exit: false).strip if cord.present? execute *app.cut_cord(cord) @@ -124,4 +134,8 @@ def gatekeeper? def queuer? barrier && !barrier_role? end + + def proxy_host? + KAMAL.proxy_host?(host) + end end diff --git a/lib/kamal/cli/main.rb b/lib/kamal/cli/main.rb index 3bd6dc24f..fdbc25597 100644 --- a/lib/kamal/cli/main.rb +++ b/lib/kamal/cli/main.rb @@ -35,8 +35,13 @@ def deploy with_lock do run_hook "pre-deploy", secrets: true - say "Ensure Traefik is running...", :magenta - invoke "kamal:cli:traefik:boot", [], invoke_options + if KAMAL.config.proxy.enabled? + say "Ensure Traefik/kamal-proxy is running...", :magenta + invoke "kamal:cli:proxy:boot", [], invoke_options + else + say "Ensure Traefik is running...", :magenta + invoke "kamal:cli:traefik:boot", [], invoke_options + end say "Detect stale containers...", :magenta invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true) @@ -104,7 +109,11 @@ def rollback(version) desc "details", "Show details about all containers" def details - invoke "kamal:cli:traefik:details" + if KAMAL.config.proxy.enabled? + invoke "kamal:cli:proxy:details" + else + invoke "kamal:cli:traefik:details" + end invoke "kamal:cli:app:details" invoke "kamal:cli:accessory:details", [ "all" ] end @@ -181,7 +190,11 @@ def init def remove confirming "This will remove all containers and images. Are you sure?" do with_lock do - invoke "kamal:cli:traefik:remove", [], options.without(:confirmed) + if KAMAL.config.proxy.enabled? + invoke "kamal:cli:proxy:remove", [], options.without(:confirmed) + else + invoke "kamal:cli:traefik:remove", [], options.without(:confirmed) + end invoke "kamal:cli:app:remove", [], options.without(:confirmed) invoke "kamal:cli:accessory:remove", [ "all" ], options invoke "kamal:cli:registry:logout", [], options.without(:confirmed).merge(skip_local: true) @@ -206,6 +219,9 @@ def version desc "lock", "Manage the deploy lock" subcommand "lock", Kamal::Cli::Lock + desc "proxy", "Prune old application images and containers" + subcommand "proxy", Kamal::Cli::Proxy + desc "prune", "Prune old application images and containers" subcommand "prune", Kamal::Cli::Prune diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb new file mode 100644 index 000000000..d546439cb --- /dev/null +++ b/lib/kamal/cli/proxy.rb @@ -0,0 +1,160 @@ +class Kamal::Cli::Proxy < Kamal::Cli::Base + desc "boot", "Boot proxy on servers" + def boot + raise_unless_kamal_proxy_enabled! + with_lock do + on(KAMAL.traefik_hosts) do |host| + execute *KAMAL.registry.login + execute *KAMAL.traefik_or_proxy(host).start_or_run + end + end + end + + desc "reboot", "Reboot proxy on servers (stop container, remove container, start new container)" + option :rolling, type: :boolean, default: false, desc: "Reboot proxy on hosts in sequence, rather than in parallel" + option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question" + def reboot + raise_unless_kamal_proxy_enabled! + confirming "This will cause a brief outage on each host. Are you sure?" do + with_lock do + host_groups = options[:rolling] ? KAMAL.traefik_hosts : [ KAMAL.traefik_hosts ] + host_groups.each do |hosts| + host_list = Array(hosts).join(",") + run_hook "pre-traefik-reboot", hosts: host_list + on(hosts) do |host| + execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug + execute *KAMAL.registry.login + + "Stopping and removing Traefik on #{host}, if running..." + execute *KAMAL.traefik.stop, raise_on_non_zero_exit: false + execute *KAMAL.traefik.remove_container + + "Stopping and removing kamal-proxy on #{host}, if running..." + execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false + execute *KAMAL.proxy.remove_container + + execute *KAMAL.traefik_or_proxy(host).run + + if KAMAL.proxy_host?(host) + KAMAL.roles_on(host).select(&:running_traefik?).each do |role| + app = KAMAL.app(role: role, host: host) + + version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip + endpoint = capture_with_info(*app.container_endpoint(version: version)).strip + + if endpoint.present? + info "Deploying #{endpoint} for role `#{role}` on #{host}..." + execute *KAMAL.proxy.deploy(role.container_prefix, target: endpoint) + end + end + end + end + run_hook "post-traefik-reboot", hosts: host_list + end + end + end + end + + desc "start", "Start existing proxy container on servers" + def start + raise_unless_kamal_proxy_enabled! + with_lock do + on(KAMAL.traefik_hosts) do |host| + execute *KAMAL.auditor.record("Started proxy"), verbosity: :debug + execute *KAMAL.traefik_or_proxy(host).start + end + end + end + + desc "stop", "Stop existing proxy container on servers" + def stop + raise_unless_kamal_proxy_enabled! + with_lock do + on(KAMAL.traefik_hosts) do |host| + execute *KAMAL.auditor.record("Stopped proxy"), verbosity: :debug + execute *KAMAL.traefik_or_proxy(host).stop, raise_on_non_zero_exit: false + end + end + end + + desc "restart", "Restart existing proxy container on servers" + def restart + raise_unless_kamal_proxy_enabled! + with_lock do + stop + start + end + end + + desc "details", "Show details about proxy container from servers" + def details + raise_unless_kamal_proxy_enabled! + on(KAMAL.traefik_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.traefik_or_proxy(host).info), type: "Proxy" } + end + + desc "logs", "Show log lines from proxy on servers" + option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)" + option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server" + option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)" + option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)" + def logs + raise_unless_kamal_proxy_enabled! + grep = options[:grep] + + if options[:follow] + run_locally do + info "Following logs on #{KAMAL.primary_host}..." + info KAMAL.traefik_or_proxy(KAMAL.primary_host).follow_logs(host: KAMAL.primary_host, grep: grep) + exec KAMAL.traefik_or_proxy(KAMAL.primary_host).follow_logs(host: KAMAL.primary_host, grep: grep) + end + else + since = options[:since] + lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set + + on(KAMAL.traefik_hosts) do |host| + puts_by_host host, capture(*KAMAL.traefik_or_proxy(host).logs(since: since, lines: lines, grep: grep)), type: "Proxy" + end + end + end + + desc "remove", "Remove proxy container and image from servers" + def remove + raise_unless_kamal_proxy_enabled! + with_lock do + stop + remove_container + remove_image + end + end + + desc "remove_container", "Remove proxy container from servers", hide: true + def remove_container + raise_unless_kamal_proxy_enabled! + with_lock do + on(KAMAL.traefik_hosts) do + execute *KAMAL.auditor.record("Removed proxy container"), verbosity: :debug + execute *KAMAL.proxy.remove_container + execute *KAMAL.traefik.remove_container + end + end + end + + desc "remove_image", "Remove proxy image from servers", hide: true + def remove_image + raise_unless_kamal_proxy_enabled! + with_lock do + on(KAMAL.traefik_hosts) do + execute *KAMAL.auditor.record("Removed proxy image"), verbosity: :debug + execute *KAMAL.proxy.remove_image + execute *KAMAL.traefik.remove_image + end + end + end + + private + def raise_unless_kamal_proxy_enabled! + unless KAMAL.config.proxy.enabled? + raise "kamal proxy commands are disabled unless experimental proxy support is enabled. Use `kamal traefik` commands instead." + end + end +end diff --git a/lib/kamal/cli/traefik.rb b/lib/kamal/cli/traefik.rb index 41ffbc045..0dc25955b 100644 --- a/lib/kamal/cli/traefik.rb +++ b/lib/kamal/cli/traefik.rb @@ -1,6 +1,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base desc "boot", "Boot Traefik on servers" def boot + raise_if_kamal_proxy_enabled! with_lock do on(KAMAL.traefik_hosts) do execute *KAMAL.registry.login @@ -15,6 +16,7 @@ def boot option :rolling, type: :boolean, default: false, desc: "Reboot traefik on hosts in sequence, rather than in parallel" option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question" def reboot + raise_if_kamal_proxy_enabled! confirming "This will cause a brief outage on each host. Are you sure?" do with_lock do host_groups = options[:rolling] ? KAMAL.traefik_hosts : [ KAMAL.traefik_hosts ] @@ -36,6 +38,7 @@ def reboot desc "start", "Start existing Traefik container on servers" def start + raise_if_kamal_proxy_enabled! with_lock do on(KAMAL.traefik_hosts) do execute *KAMAL.auditor.record("Started traefik"), verbosity: :debug @@ -46,6 +49,7 @@ def start desc "stop", "Stop existing Traefik container on servers" def stop + raise_if_kamal_proxy_enabled! with_lock do on(KAMAL.traefik_hosts) do execute *KAMAL.auditor.record("Stopped traefik"), verbosity: :debug @@ -56,6 +60,7 @@ def stop desc "restart", "Restart existing Traefik container on servers" def restart + raise_if_kamal_proxy_enabled! with_lock do stop start @@ -64,6 +69,7 @@ def restart desc "details", "Show details about Traefik container from servers" def details + raise_if_kamal_proxy_enabled! on(KAMAL.traefik_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.traefik.info), type: "Traefik" } end @@ -74,6 +80,7 @@ def details option :grep_options, aliases: "-o", desc: "Additional options supplied to grep" option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)" def logs + raise_if_kamal_proxy_enabled! grep = options[:grep] grep_options = options[:grep_options] @@ -95,6 +102,7 @@ def logs desc "remove", "Remove Traefik container and image from servers" def remove + raise_if_kamal_proxy_enabled! with_lock do stop remove_container @@ -104,6 +112,7 @@ def remove desc "remove_container", "Remove Traefik container from servers", hide: true def remove_container + raise_if_kamal_proxy_enabled! with_lock do on(KAMAL.traefik_hosts) do execute *KAMAL.auditor.record("Removed traefik container"), verbosity: :debug @@ -114,6 +123,7 @@ def remove_container desc "remove_image", "Remove Traefik image from servers", hide: true def remove_image + raise_if_kamal_proxy_enabled! with_lock do on(KAMAL.traefik_hosts) do execute *KAMAL.auditor.record("Removed traefik image"), verbosity: :debug @@ -121,4 +131,11 @@ def remove_image end end end + + private + def raise_if_kamal_proxy_enabled! + if KAMAL.config.proxy.enabled? + raise "kamal traefik commands are disabled when experimental proxy support is enabled. Use `kamal proxy` commands instead." + end + end end diff --git a/lib/kamal/commander.rb b/lib/kamal/commander.rb index 11914a676..994debb54 100644 --- a/lib/kamal/commander.rb +++ b/lib/kamal/commander.rb @@ -4,7 +4,7 @@ class Kamal::Commander attr_accessor :verbosity, :holding_lock, :connected - delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :traefik_hosts, :accessory_hosts, to: :specifics + delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :traefik_hosts, :proxy_hosts, :proxy_host?, :accessory_hosts, to: :specifics def initialize self.verbosity = :info @@ -106,6 +106,10 @@ def lock @lock ||= Kamal::Commands::Lock.new(config) end + def proxy + @proxy ||= Kamal::Commands::Proxy.new(config) + end + def prune @prune ||= Kamal::Commands::Prune.new(config) end @@ -127,6 +131,10 @@ def alias(name) end + def traefik_or_proxy(host) + proxy_host?(host) ? proxy : traefik + end + def with_verbosity(level) old_level = self.verbosity diff --git a/lib/kamal/commander/specifics.rb b/lib/kamal/commander/specifics.rb index 127bd40e5..12a710d8e 100644 --- a/lib/kamal/commander/specifics.rb +++ b/lib/kamal/commander/specifics.rb @@ -22,6 +22,15 @@ def traefik_hosts config.traefik_hosts & specified_hosts end + def proxy_hosts + traefik_hosts & config.proxy_hosts + end + + def proxy_host?(host) + host = host.hostname if host.is_a?(SSHKit::Host) + proxy_hosts.include?(host) + end + def accessory_hosts specific_hosts || config.accessories.flat_map(&:hosts) end diff --git a/lib/kamal/commands/app.rb b/lib/kamal/commands/app.rb index f1991e481..ad6e0613a 100644 --- a/lib/kamal/commands/app.rb +++ b/lib/kamal/commands/app.rb @@ -30,6 +30,24 @@ def run(hostname: nil) role.cmd end + def run_for_proxy(hostname: nil) + docker :run, + "--detach", + "--restart unless-stopped", + "--name", container_name, + *([ "--hostname", hostname ] if hostname), + "-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"", + "-e", "KAMAL_VERSION=\"#{config.version}\"", + *role.env_args(host), + *role.logging_args, + *config.volume_args, + *role.asset_volume_args, + *role.label_args_for_proxy, + *role.option_args, + config.absolute_image, + role.cmd + end + def start docker :start, container_name end diff --git a/lib/kamal/commands/app/containers.rb b/lib/kamal/commands/app/containers.rb index 0bab388b8..629fa47fb 100644 --- a/lib/kamal/commands/app/containers.rb +++ b/lib/kamal/commands/app/containers.rb @@ -28,4 +28,11 @@ def container_health_log(version:) container_id_for(container_name: container_name(version)), xargs(docker(:inspect, "--format", DOCKER_HEALTH_LOG_FORMAT)) end + + def container_endpoint(version:) + pipe \ + container_id_for(container_name: container_name(version)), + xargs(docker(:inspect, "--format", "'{{.NetworkSettings.IPAddress}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'")), + [ :sed, "-e", "'s/\\/tcp$//'" ] + end end diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb new file mode 100644 index 000000000..89808d729 --- /dev/null +++ b/lib/kamal/commands/proxy.rb @@ -0,0 +1,69 @@ +class Kamal::Commands::Proxy < Kamal::Commands::Base + delegate :argumentize, :optionize, to: Kamal::Utils + delegate :container_name, to: :proxy_config + + attr_reader :proxy_config + + def initialize(config) + super + @proxy_config = config.proxy + end + + def run + docker :run, + "--name", container_name, + "--detach", + "--restart", "unless-stopped", + *proxy_config.publish_args, + "--volume", "/var/run/docker.sock:/var/run/docker.sock", + "--volume", "#{container_name}:/root/.config/kamal-proxy", + *config.logging_args, + proxy_config.image + end + + def start + docker :container, :start, container_name + end + + def stop(name: container_name) + docker :container, :stop, name + end + + def start_or_run + combine start, run, by: "||" + end + + def deploy(service, target:) + optionize({ target: target }) + docker :exec, container_name, "kamal-proxy", :deploy, service, *optionize({ target: target }), *proxy_config.deploy_command_args + end + + def remove(service, target:) + docker :exec, container_name, "kamal-proxy", :remove, service, *optionize({ target: target }) + end + + def info + docker :ps, "--filter", "name=^#{container_name}$" + end + + def logs(since: nil, lines: nil, grep: nil, grep_options: nil) + pipe \ + docker(:logs, container_name, (" --since #{since}" if since), (" --tail #{lines}" if lines), "--timestamps", "2>&1"), + ("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep) + end + + def follow_logs(host:, grep: nil, grep_options: nil) + run_over_ssh pipe( + docker(:logs, container_name, "--timestamps", "--tail", "10", "--follow", "2>&1"), + (%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep) + ).join(" "), host: host + end + + def remove_container + docker :container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=kamal-proxy" + end + + def remove_image + docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=kamal-proxy" + end +end diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index 2758d15ab..40c9e4fed 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -10,7 +10,7 @@ class Kamal::Configuration delegate :argumentize, :optionize, to: Kamal::Utils attr_reader :destination, :raw_config, :secrets - attr_reader :accessories, :aliases, :boot, :builder, :env, :healthcheck, :logging, :traefik, :servers, :ssh, :sshkit, :registry + attr_reader :accessories, :aliases :boot, :builder, :env, :healthcheck, :logging, :proxy, :traefik, :servers, :ssh, :sshkit, :registry include Validation @@ -60,6 +60,7 @@ def initialize(raw_config, destination: nil, version: nil, validate: true) @healthcheck = Healthcheck.new(healthcheck_config: @raw_config.healthcheck) @logging = Logging.new(logging_config: @raw_config.logging) + @proxy = Proxy.new(config: self) @traefik = Traefik.new(config: self) @ssh = Ssh.new(config: self) @sshkit = Sshkit.new(config: self) @@ -143,6 +144,10 @@ def traefik_hosts traefik_roles.flat_map(&:hosts).uniq end + def proxy_hosts + proxy.hosts + end + def repository [ registry.server, image ].compact.join("/") end diff --git a/lib/kamal/configuration/docs/configuration.yml b/lib/kamal/configuration/docs/configuration.yml index f1045dd69..8ebaaa0d2 100644 --- a/lib/kamal/configuration/docs/configuration.yml +++ b/lib/kamal/configuration/docs/configuration.yml @@ -143,6 +143,12 @@ accessories: traefik: ... +# Proxy +# +# **Experimental** Configuration for kamal-proxy the replacement for Traefik, see kamal docs proxy +proxy: + ... + # SSHKit # # See kamal docs sshkit diff --git a/lib/kamal/configuration/docs/proxy.yml b/lib/kamal/configuration/docs/proxy.yml new file mode 100644 index 000000000..f61ea2295 --- /dev/null +++ b/lib/kamal/configuration/docs/proxy.yml @@ -0,0 +1,83 @@ +# Proxy +# +# **Experimental** [kamal-proxy](http://github.com/basecamp/kamal-proxy) is a +# custom built specifically for Kamal. It will replace Traefik in Kamal v2.0, +# but currently is available as an experimental feature. +# +# When this is enabled, the proxy will be started on the hosts listed under the hosts key. +# In addition, the kamal traefik command will be disabled and replaced by kamal proxy. +# +# The kamal proxy command works identically to kamal traefik on hosts that have not +# been included. It will also handle switching between Traefik and kamal-proxy when you +# run kamal proxy reboot. + +# Limitations +# +# Currently the proxy will run on ports 80 and 443 and will bind to those +# ports on the host. +# +# There is no way to set custom options for `docker run` when booting the proxy. +# +# If you have custom Traefik configuration via labels or boot arguments they may +# not have an equivalent in kamal-proxy. + +# Proxy settings +# +# The proxy is configured in the root configuration under `traefik`. These are +# options that are set when deploying the application, not when booting the proxy +# +# They are application specific, so are not shared when multiple applications +# with the same proxy. +proxy: + + # Enabled + # + # Whether to enable experimental proxy support. Defaults to false + enabled: true + + # Hosts + # + # The hosts to run the proxy on, instead of Traefik + # This is a temporary setting and will be removed when we full switch to kamal-proxy + # + # If you run `kamal traefik reboot`, then the proxy will be started on these hosts + # in place of traefik. + hosts: + - 10.0.0.1 + - 10.0.0.2 + + # Host + # + # This is the host that will be used to serve the app. By setting this you can run + # multiple apps on the same server sharing the same instance of the proxy. + # + # If this is set only requests that match this host will be forwarded by the proxy. + # if this is not set, then all requests will be forwarded, except for matching + # requests for other apps that do have a host set. + host: foo.example.com + + # Deploy timeout + # + # How long to wait for the app to boot when deploying, defaults to 30 seconds + deploy_timeout: 10s + + # Response timeout + # + # How long to wait for requests to complete before timing out, defaults to 30 seconds + response_timeout: 10 + + # Healthcheck + # + # When deploying, the proxy will by default hit /up once every second until we hit + # the deploy timeout, with a 5 second timeout for each request. + # + # Once the app is up, the proxy will stop hitting the healthcheck endpoint. + healthcheck: + interval: 3 + path: /health + timeout: 3 + + # Max Request Body Size + # + # The maximum request size in bytes that the proxy will accept, defaults to 1GB + max_request_body_size: 40_000_000 diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb new file mode 100644 index 000000000..5dc3546b5 --- /dev/null +++ b/lib/kamal/configuration/proxy.rb @@ -0,0 +1,57 @@ +class Kamal::Configuration::Proxy + include Kamal::Configuration::Validation + + DEFAULT_HTTP_PORT = 80 + DEFAULT_HTTPS_PORT = 443 + DEFAULT_IMAGE = "basecamp/kamal-proxy:latest" + + delegate :argumentize, :optionize, to: Kamal::Utils + + def initialize(config:) + @proxy_config = config.raw_config.proxy || {} + validate! proxy_config + end + + def enabled? + !!proxy_config.fetch("enabled", false) + end + + def hosts + if enabled? + proxy_config.fetch("hosts", []) + else + [] + end + end + + def image + proxy_config.fetch("image", DEFAULT_IMAGE) + end + + def container_name + "kamal-proxy" + end + + def publish_args + argumentize "--publish", [ "#{DEFAULT_HTTP_PORT}:#{DEFAULT_HTTP_PORT}", "#{DEFAULT_HTTPS_PORT}:#{DEFAULT_HTTPS_PORT}" ] + end + + def deploy_options + { + host: proxy_config["host"], + "deploy-timeout": proxy_config["deploy_timeout"], + "drain-timeout": proxy_config["drain_timeout"], + "health-check-interval": proxy_config.dig("health_check", "interval"), + "health-check-timeout": proxy_config.dig("health_check", "timeout"), + "health-check-path": proxy_config.dig("health_check", "path"), + "target-timeout": proxy_config["response_timeout"] + }.compact + end + + def deploy_command_args + optionize deploy_options + end + + private + attr_accessor :proxy_config +end diff --git a/lib/kamal/configuration/role.rb b/lib/kamal/configuration/role.rb index ef651898f..6579b9d07 100644 --- a/lib/kamal/configuration/role.rb +++ b/lib/kamal/configuration/role.rb @@ -58,10 +58,18 @@ def labels default_labels.merge(traefik_labels).merge(custom_labels) end + def labels_for_proxy + default_labels.merge(custom_labels) + end + def label_args argumentize "--label", labels end + def label_args_for_proxy + argumentize "--label", labels_for_proxy + end + def logging_args logging.args end diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index 85a966fd6..81ac8b16e 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -356,6 +356,18 @@ class CliAppTest < CliTestCase end end + test "boot proxy" do + SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version + + run_command("boot", config: :with_proxy).tap do |output| + assert_match /Renaming container .* to .* as already deployed on 1.1.1.1/, output # Rename + assert_match /docker rename app-web-latest app-web-latest_replaced_[0-9a-f]{16}/, output + assert_match /docker run --detach --restart unless-stopped --name app-web-latest --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 --log-opt max-size="10m" --label service="app" --label role="web" --label destination dhh\/app:latest/, output + assert_match /docker exec kamal-proxy kamal-proxy deploy app-web --target "123"/, output + assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output + end + end + private def run_command(*command, config: :with_accessories, host: "1.1.1.1", allow_execute_error: false) stdouted do diff --git a/test/cli/proxy_test.rb b/test/cli/proxy_test.rb new file mode 100644 index 000000000..7d08c62f3 --- /dev/null +++ b/test/cli/proxy_test.rb @@ -0,0 +1,141 @@ +require_relative "cli_test_case" + +class CliProxyTest < CliTestCase + test "boot" do + run_command("boot").tap do |output| + assert_match "docker login", output + assert_match "docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", output + end + end + + test "reboot" do + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{.NetworkSettings.IPAddress}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'", "|", :sed, "-e", "'s/\\/tcp$//'") + .returns("172.1.0.2:80") + .at_least_once + + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with { |*args| args[0..1] == [ :sh, "-c" ] } + .returns("123") + .at_least_once + + run_command("reboot", "-y").tap do |output| + assert_match "docker container stop kamal-proxy on 1.1.1.1", output + assert_match "docker container stop traefik on 1.1.1.1", output + assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output + assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output + assert_match "docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE} on 1.1.1.1", output + assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"172.1.0.2:80\" --deploy-timeout \"6s\" on 1.1.1.1", output + + assert_match "docker container stop kamal-proxy on 1.1.1.2", output + assert_match "docker container stop traefik on 1.1.1.2", output + assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.2", output + assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.2", 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\" traefik:v2.10 --providers.docker --log.level=\"DEBUG\" on 1.1.1.2", output + end + end + + test "reboot --rolling" do + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{.NetworkSettings.IPAddress}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'", "|", :sed, "-e", "'s/\\/tcp$//'") + .returns("172.1.0.2:80") + .at_least_once + + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with { |*args| args[0..1] == [ :sh, "-c" ] } + .returns("123") + .at_least_once + + run_command("reboot", "--rolling", "-y").tap do |output| + assert_match "Running docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output + end + end + + test "start" do + run_command("start").tap do |output| + assert_match "docker container start kamal-proxy", output + end + end + + test "stop" do + run_command("stop").tap do |output| + assert_match "docker container stop kamal-proxy", output + end + end + + test "restart" do + Kamal::Cli::Proxy.any_instance.expects(:stop) + Kamal::Cli::Proxy.any_instance.expects(:start) + + run_command("restart") + end + + test "details" do + run_command("details").tap do |output| + assert_match "docker ps --filter name=^kamal-proxy$", output + end + end + + test "logs" do + SSHKit::Backend::Abstract.any_instance.stubs(:capture) + .with(:docker, :logs, "kamal-proxy", " --tail 100", "--timestamps", "2>&1") + .returns("Log entry") + + SSHKit::Backend::Abstract.any_instance.stubs(:capture) + .with(:docker, :logs, "traefik", " --tail 100", "--timestamps", "2>&1") + .returns("Log entry") + + run_command("logs").tap do |output| + assert_match "Proxy Host: 1.1.1.1", output + assert_match "Log entry", output + end + end + + test "logs with follow" do + SSHKit::Backend::Abstract.any_instance.stubs(:exec) + .with("ssh -t root@1.1.1.1 -p 22 'docker logs kamal-proxy --timestamps --tail 10 --follow 2>&1'") + + assert_match "docker logs kamal-proxy --timestamps --tail 10 --follow", run_command("logs", "--follow") + end + + test "remove" do + Kamal::Cli::Proxy.any_instance.expects(:stop) + Kamal::Cli::Proxy.any_instance.expects(:remove_container) + Kamal::Cli::Proxy.any_instance.expects(:remove_image) + + run_command("remove") + end + + test "remove_container" do + run_command("remove_container").tap do |output| + assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy", output + end + end + + test "remove_image" do + run_command("remove_image").tap do |output| + assert_match "docker image prune --all --force --filter label=org.opencontainers.image.title=kamal-proxy", output + end + end + + test "commands disallowed when proxy is disabled" do + assert_raises_when_disabled "boot" + assert_raises_when_disabled "reboot" + assert_raises_when_disabled "start" + assert_raises_when_disabled "stop" + assert_raises_when_disabled "details" + assert_raises_when_disabled "logs" + assert_raises_when_disabled "remove" + end + + private + def run_command(*command, fixture: :with_proxy) + stdouted { Kamal::Cli::Proxy.start([ *command, "-c", "test/fixtures/deploy_#{fixture}.yml" ]) } + end + + def assert_raises_when_disabled(command) + assert_raises "kamal proxy commands are disabled unless experimental proxy support is enabled. Use `kamal traefik` commands instead." do + run_command(command, fixture: :with_accessories) + end + end +end diff --git a/test/commands/proxy_test.rb b/test/commands/proxy_test.rb new file mode 100644 index 000000000..73e7bfbf2 --- /dev/null +++ b/test/commands/proxy_test.rb @@ -0,0 +1,126 @@ +require "test_helper" + +class CommandsProxyTest < ActiveSupport::TestCase + setup do + @config = { + service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ] + } + + ENV["EXAMPLE_API_KEY"] = "456" + end + + teardown do + ENV.delete("EXAMPLE_API_KEY") + end + + test "run" do + assert_equal \ + "docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", + new_command.run.join(" ") + end + + test "run with ports configured" do + assert_equal \ + "docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", + new_command.run.join(" ") + end + + test "run without configuration" do + @config.delete(:proxy) + + assert_equal \ + "docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", + new_command.run.join(" ") + end + + test "run with logging config" do + @config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } } + + assert_equal \ + "docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", + new_command.run.join(" ") + end + + test "proxy start" do + assert_equal \ + "docker container start kamal-proxy", + new_command.start.join(" ") + end + + test "proxy stop" do + assert_equal \ + "docker container stop kamal-proxy", + new_command.stop.join(" ") + end + + test "proxy info" do + assert_equal \ + "docker ps --filter name=^kamal-proxy$", + new_command.info.join(" ") + end + + test "proxy logs" do + assert_equal \ + "docker logs kamal-proxy --timestamps 2>&1", + new_command.logs.join(" ") + end + + test "proxy logs since 2h" do + assert_equal \ + "docker logs kamal-proxy --since 2h --timestamps 2>&1", + new_command.logs(since: "2h").join(" ") + end + + test "proxy logs last 10 lines" do + assert_equal \ + "docker logs kamal-proxy --tail 10 --timestamps 2>&1", + new_command.logs(lines: 10).join(" ") + end + + test "proxy logs with grep hello!" do + assert_equal \ + "docker logs kamal-proxy --timestamps 2>&1 | grep 'hello!'", + new_command.logs(grep: "hello!").join(" ") + end + + test "proxy remove container" do + assert_equal \ + "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy", + new_command.remove_container.join(" ") + end + + test "proxy remove image" do + assert_equal \ + "docker image prune --all --force --filter label=org.opencontainers.image.title=kamal-proxy", + new_command.remove_image.join(" ") + end + + test "proxy follow logs" do + assert_equal \ + "ssh -t root@1.1.1.1 -p 22 'docker logs kamal-proxy --timestamps --tail 10 --follow 2>&1'", + new_command.follow_logs(host: @config[:servers].first) + end + + test "proxy follow logs with grep hello!" do + assert_equal \ + "ssh -t root@1.1.1.1 -p 22 'docker logs kamal-proxy --timestamps --tail 10 --follow 2>&1 | grep \"hello!\"'", + new_command.follow_logs(host: @config[:servers].first, grep: "hello!") + end + + test "deploy" do + assert_equal \ + "docker exec kamal-proxy kamal-proxy deploy service --target \"172.1.0.2:80\"", + new_command.deploy("service", target: "172.1.0.2:80").join(" ") + end + + test "remove" do + assert_equal \ + "docker exec kamal-proxy kamal-proxy remove service --target \"172.1.0.2:80\"", + new_command.remove("service", target: "172.1.0.2:80").join(" ") + end + + private + def new_command + Kamal::Commands::Proxy.new(Kamal::Configuration.new(@config, version: "123")) + end +end diff --git a/test/fixtures/deploy_with_proxy.yml b/test/fixtures/deploy_with_proxy.yml new file mode 100644 index 000000000..bfe8c505c --- /dev/null +++ b/test/fixtures/deploy_with_proxy.yml @@ -0,0 +1,42 @@ +service: app +image: dhh/app +servers: + web: + - "1.1.1.1" + - "1.1.1.2" + workers: + - "1.1.1.3" + - "1.1.1.4" +registry: + username: user + password: pw + +proxy: + enabled: true + hosts: + - "1.1.1.1" + deploy_timeout: 6s + +accessories: + mysql: + image: mysql:5.7 + host: 1.1.1.3 + port: 3306 + env: + clear: + MYSQL_ROOT_HOST: '%' + secret: + - MYSQL_ROOT_PASSWORD + files: + - test/fixtures/files/my.cnf:/etc/mysql/my.cnf + directories: + - data:/var/lib/mysql + redis: + image: redis:latest + roles: + - web + port: 6379 + directories: + - data:/data + +readiness_delay: 0 diff --git a/test/integration/broken_deploy_test.rb b/test/integration/broken_deploy_test.rb index 77f0ff96a..964f1d0bb 100644 --- a/test/integration/broken_deploy_test.rb +++ b/test/integration/broken_deploy_test.rb @@ -27,6 +27,5 @@ def assert_failed_deploy(output) assert_match /First web container is unhealthy on vm[12], not booting any other roles/, output assert_match "First web container is unhealthy, not booting workers on vm3", output assert_match "nginx: [emerg] unexpected end of file, expecting \";\" or \"}\" in /etc/nginx/conf.d/default.conf:2", output - assert_match 'ERROR {"Status":"unhealthy","FailingStreak":0,"Log":[]}', output end end diff --git a/test/integration/docker/deployer/app/Dockerfile b/test/integration/docker/deployer/app/Dockerfile index dc270aa9a..0e6237df4 100644 --- a/test/integration/docker/deployer/app/Dockerfile +++ b/test/integration/docker/deployer/app/Dockerfile @@ -6,4 +6,4 @@ ARG COMMIT_SHA RUN echo $COMMIT_SHA > /usr/share/nginx/html/version RUN mkdir -p /usr/share/nginx/html/versions && echo "version" > /usr/share/nginx/html/versions/$COMMIT_SHA RUN mkdir -p /usr/share/nginx/html/versions && echo "hidden" > /usr/share/nginx/html/versions/.hidden - +RUN echo "Up!" > /usr/share/nginx/html/up diff --git a/test/integration/docker/deployer/app_with_roles/Dockerfile b/test/integration/docker/deployer/app_with_roles/Dockerfile index dc270aa9a..0e6237df4 100644 --- a/test/integration/docker/deployer/app_with_roles/Dockerfile +++ b/test/integration/docker/deployer/app_with_roles/Dockerfile @@ -6,4 +6,4 @@ ARG COMMIT_SHA RUN echo $COMMIT_SHA > /usr/share/nginx/html/version RUN mkdir -p /usr/share/nginx/html/versions && echo "version" > /usr/share/nginx/html/versions/$COMMIT_SHA RUN mkdir -p /usr/share/nginx/html/versions && echo "hidden" > /usr/share/nginx/html/versions/.hidden - +RUN echo "Up!" > /usr/share/nginx/html/up diff --git a/test/integration/docker/deployer/app_with_roles/config/deploy.yml b/test/integration/docker/deployer/app_with_roles/config/deploy.yml index 3b9426654..c15af55b1 100644 --- a/test/integration/docker/deployer/app_with_roles/config/deploy.yml +++ b/test/integration/docker/deployer/app_with_roles/config/deploy.yml @@ -9,6 +9,11 @@ servers: hosts: - vm3 cmd: sleep infinity +proxy: + enabled: true + hosts: + - vm2 + deploy_timeout: 2s asset_path: /usr/share/nginx/html/versions diff --git a/test/integration/integration_test.rb b/test/integration/integration_test.rb index ea445d9e7..fd23e579f 100644 --- a/test/integration/integration_test.rb +++ b/test/integration/integration_test.rb @@ -31,7 +31,7 @@ def docker_compose(*commands, capture: false, raise_on_error: true) succeeded = system("cd test/integration && #{command}") end - raise "Command `#{command}` failed with error code `#{$?}`" if !succeeded && raise_on_error + raise "Command `#{command}` failed with error code `#{$?}`, and output:\n#{result}" if !succeeded && raise_on_error result end diff --git a/test/integration/proxy_test.rb b/test/integration/proxy_test.rb new file mode 100644 index 000000000..1f4442661 --- /dev/null +++ b/test/integration/proxy_test.rb @@ -0,0 +1,84 @@ +require_relative "integration_test" + +class ProxyTest < IntegrationTest + setup do + @app = "app_with_roles" + end + + test "boot, reboot, stop, start, restart, logs, remove" do + kamal :envify + + kamal :proxy, :boot + assert_proxy_running + + output = kamal :proxy, :reboot, "-y", "--verbose", capture: true + assert_proxy_running + assert_hooks_ran "pre-traefik-reboot", "post-traefik-reboot" + assert_match /Rebooting Traefik on vm1,vm2.../, output + assert_match /Rebooted Traefik on vm1,vm2/, output + + output = kamal :proxy, :reboot, "--rolling", "-y", "--verbose", capture: true + assert_proxy_running + assert_hooks_ran "pre-traefik-reboot", "post-traefik-reboot" + assert_match /Rebooting Traefik on vm1.../, output + assert_match /Rebooted Traefik on vm1/, output + assert_match /Rebooting Traefik on vm2.../, output + assert_match /Rebooted Traefik on vm2/, output + + kamal :proxy, :boot + assert_proxy_running + assert_traefik_running + + # Check booting when booted doesn't raise an error + kamal :proxy, :stop + assert_proxy_not_running + assert_traefik_not_running + + # Check booting when stopped works + kamal :proxy, :boot + assert_proxy_running + assert_traefik_running + + kamal :proxy, :stop + assert_proxy_not_running + assert_traefik_not_running + + kamal :proxy, :start + assert_proxy_running + assert_traefik_running + + kamal :proxy, :restart + assert_proxy_running + assert_traefik_running + + logs = kamal :proxy, :logs, capture: true + assert_match /Traefik version [\d.]+ built on/, logs + + kamal :proxy, :remove + assert_proxy_not_running + assert_traefik_not_running + + kamal :env, :delete + end + + private + def assert_proxy_running + assert_match /basecamp\/kamal-proxy:latest \"kamal-proxy run\"/, proxy_details + end + + def assert_proxy_not_running + assert_no_match /basecamp\/kamal-proxy:latest \"kamal-proxy run\"/, proxy_details + end + + def assert_traefik_running + assert_match /traefik:v2.10 "\/entrypoint.sh/, proxy_details + end + + def assert_traefik_not_running + assert_no_match /traefik:v2.10 "\/entrypoint.sh/, proxy_details + end + + def proxy_details + kamal :proxy, :details, capture: true + end +end From d63ff8f2513dfc4d5dae4b9d412b2fc79a8786e5 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Mon, 29 Jul 2024 14:32:04 +0100 Subject: [PATCH 02/43] Set extra fields --- lib/kamal/configuration/docs/proxy.yml | 16 +++++++++++++--- lib/kamal/configuration/proxy.rb | 6 +++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/kamal/configuration/docs/proxy.yml b/lib/kamal/configuration/docs/proxy.yml index f61ea2295..dbf47a2b1 100644 --- a/lib/kamal/configuration/docs/proxy.yml +++ b/lib/kamal/configuration/docs/proxy.yml @@ -77,7 +77,17 @@ proxy: path: /health timeout: 3 - # Max Request Body Size + # Buffering # - # The maximum request size in bytes that the proxy will accept, defaults to 1GB - max_request_body_size: 40_000_000 + # Whether to buffer request and response bodies in the proxy + # + # By default buffering is enabled with a max request body size of 1GB and no limit + # for response size. + # + # You can also set the memory limit for buffering, which defaults to 1MB, anything + # larger than that is written to disk. + buffering: + enabled: true + max_request_body: 40_000_000 + max_response_body: 0 + memory: 2_000_000 diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb index 5dc3546b5..732195d32 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -44,7 +44,11 @@ def deploy_options "health-check-interval": proxy_config.dig("health_check", "interval"), "health-check-timeout": proxy_config.dig("health_check", "timeout"), "health-check-path": proxy_config.dig("health_check", "path"), - "target-timeout": proxy_config["response_timeout"] + "target-timeout": proxy_config["response_timeout"], + "buffer": proxy_config.fetch("buffer", { enabled: true }).fetch("enabled", true), + "buffer-memory": proxy_config.dig("buffer", "memory"), + "max-request-body": proxy_config.dig("buffer", "max_request_body"), + "max-response-body": proxy_config.dig("buffer", "max_response_body") }.compact end From 418d8045d82ce315d4b530708a4c33ceac723341 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 31 Jul 2024 10:44:48 +0100 Subject: [PATCH 03/43] Add forward headers support --- lib/kamal/configuration/docs/proxy.yml | 7 +++++++ lib/kamal/configuration/proxy.rb | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/kamal/configuration/docs/proxy.yml b/lib/kamal/configuration/docs/proxy.yml index dbf47a2b1..0c541c511 100644 --- a/lib/kamal/configuration/docs/proxy.yml +++ b/lib/kamal/configuration/docs/proxy.yml @@ -91,3 +91,10 @@ proxy: max_request_body: 40_000_000 max_response_body: 0 memory: 2_000_000 + + # Forward headers + # + # Whether to forward the X-Forwarded-For and X-Forwarded-Proto headers (defaults to false) + # + # If you are behind a trusted proxy, you can set this to true to forward the headers. + forward_headers: true diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb index 732195d32..df409d04d 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -48,7 +48,8 @@ def deploy_options "buffer": proxy_config.fetch("buffer", { enabled: true }).fetch("enabled", true), "buffer-memory": proxy_config.dig("buffer", "memory"), "max-request-body": proxy_config.dig("buffer", "max_request_body"), - "max-response-body": proxy_config.dig("buffer", "max_response_body") + "max-response-body": proxy_config.dig("buffer", "max_response_body"), + "forward-headers": proxy_config.dig("forward_headers") }.compact end From fe0c656de5be246ad61c98699fb103213914b9e0 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 31 Jul 2024 11:04:09 +0100 Subject: [PATCH 04/43] Split buffer requests/responses --- lib/kamal/configuration/docs/proxy.yml | 3 ++- lib/kamal/configuration/proxy.rb | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/kamal/configuration/docs/proxy.yml b/lib/kamal/configuration/docs/proxy.yml index 0c541c511..db5b96741 100644 --- a/lib/kamal/configuration/docs/proxy.yml +++ b/lib/kamal/configuration/docs/proxy.yml @@ -87,7 +87,8 @@ proxy: # You can also set the memory limit for buffering, which defaults to 1MB, anything # larger than that is written to disk. buffering: - enabled: true + requests: true + responses: true max_request_body: 40_000_000 max_response_body: 0 memory: 2_000_000 diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb index df409d04d..ee0321d49 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -45,7 +45,8 @@ def deploy_options "health-check-timeout": proxy_config.dig("health_check", "timeout"), "health-check-path": proxy_config.dig("health_check", "path"), "target-timeout": proxy_config["response_timeout"], - "buffer": proxy_config.fetch("buffer", { enabled: true }).fetch("enabled", true), + "buffer-requests": proxy_config.fetch("buffer", { "requests": true }).fetch("requests", true), + "buffer-responses": proxy_config.fetch("buffer", { "responses": true }).fetch("responses", true), "buffer-memory": proxy_config.dig("buffer", "memory"), "max-request-body": proxy_config.dig("buffer", "max_request_body"), "max-response-body": proxy_config.dig("buffer", "max_response_body"), From 55756fa6f3eb3947765fdd618952458f90f2779d Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 31 Jul 2024 13:55:58 +0100 Subject: [PATCH 05/43] Set request and response headers --- lib/kamal/commands/proxy.rb | 1 - lib/kamal/configuration/docs/proxy.yml | 13 +++++++++++++ lib/kamal/configuration/proxy.rb | 5 ++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb index 89808d729..9ae029f76 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -34,7 +34,6 @@ def start_or_run end def deploy(service, target:) - optionize({ target: target }) docker :exec, container_name, "kamal-proxy", :deploy, service, *optionize({ target: target }), *proxy_config.deploy_command_args end diff --git a/lib/kamal/configuration/docs/proxy.yml b/lib/kamal/configuration/docs/proxy.yml index db5b96741..8d5b201fe 100644 --- a/lib/kamal/configuration/docs/proxy.yml +++ b/lib/kamal/configuration/docs/proxy.yml @@ -93,6 +93,19 @@ proxy: max_response_body: 0 memory: 2_000_000 + # Logging + # + # Configure request logging for the proxy + # You can specify request and response headers to log. + # By default, Cache-Control and Last-Modified request headers are logged + logging: + request_headers: + - Cache-Control + - X-Forwarded-Proto + response_headers: + - X-Request-ID + - X-Request-Start + # Forward headers # # Whether to forward the X-Forwarded-For and X-Forwarded-Proto headers (defaults to false) diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb index ee0321d49..84a343b85 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -4,6 +4,7 @@ class Kamal::Configuration::Proxy DEFAULT_HTTP_PORT = 80 DEFAULT_HTTPS_PORT = 443 DEFAULT_IMAGE = "basecamp/kamal-proxy:latest" + DEFAULT_LOG_REQUEST_HEADERS = [ "Cache-Control", "Last-Modified" ] delegate :argumentize, :optionize, to: Kamal::Utils @@ -50,7 +51,9 @@ def deploy_options "buffer-memory": proxy_config.dig("buffer", "memory"), "max-request-body": proxy_config.dig("buffer", "max_request_body"), "max-response-body": proxy_config.dig("buffer", "max_response_body"), - "forward-headers": proxy_config.dig("forward_headers") + "forward-headers": proxy_config.dig("forward_headers"), + "log-request-header": proxy_config.dig("logging", "request_headers") || DEFAULT_LOG_REQUEST_HEADERS, + "log-response-header": proxy_config.dig("logging", "response_headers") }.compact end From 53903ddcd2023e72e2dc98bd5fa85b6168c8df54 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Mon, 5 Aug 2024 15:41:57 +0100 Subject: [PATCH 06/43] Read buffer not buffering --- lib/kamal/configuration/proxy.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb index 84a343b85..dd5aac1d0 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -46,11 +46,11 @@ def deploy_options "health-check-timeout": proxy_config.dig("health_check", "timeout"), "health-check-path": proxy_config.dig("health_check", "path"), "target-timeout": proxy_config["response_timeout"], - "buffer-requests": proxy_config.fetch("buffer", { "requests": true }).fetch("requests", true), - "buffer-responses": proxy_config.fetch("buffer", { "responses": true }).fetch("responses", true), - "buffer-memory": proxy_config.dig("buffer", "memory"), - "max-request-body": proxy_config.dig("buffer", "max_request_body"), - "max-response-body": proxy_config.dig("buffer", "max_response_body"), + "buffer-requests": proxy_config.fetch("buffering", { "requests": true }).fetch("requests", true), + "buffer-responses": proxy_config.fetch("buffering", { "responses": true }).fetch("responses", true), + "buffer-memory": proxy_config.dig("buffering", "memory"), + "max-request-body": proxy_config.dig("buffering", "max_request_body"), + "max-response-body": proxy_config.dig("buffering", "max_response_body"), "forward-headers": proxy_config.dig("forward_headers"), "log-request-header": proxy_config.dig("logging", "request_headers") || DEFAULT_LOG_REQUEST_HEADERS, "log-response-header": proxy_config.dig("logging", "response_headers") From bd6558630f93d2476f382e042834074708cf44da Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 29 Aug 2024 09:13:09 +0100 Subject: [PATCH 07/43] Fix merge error --- lib/kamal/configuration.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index 40c9e4fed..116cf2322 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -10,7 +10,7 @@ class Kamal::Configuration delegate :argumentize, :optionize, to: Kamal::Utils attr_reader :destination, :raw_config, :secrets - attr_reader :accessories, :aliases :boot, :builder, :env, :healthcheck, :logging, :proxy, :traefik, :servers, :ssh, :sshkit, :registry + attr_reader :accessories, :aliases, :boot, :builder, :env, :healthcheck, :logging, :proxy, :traefik, :servers, :ssh, :sshkit, :registry include Validation From 13bdf50cebbf6594f09a58f3c5d706a5efaefd7f Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 5 Sep 2024 14:28:44 +0100 Subject: [PATCH 08/43] Fix tests for proxy defaults and required builder arch --- test/cli/proxy_test.rb | 2 +- test/commands/proxy_test.rb | 4 ++-- test/fixtures/deploy_with_proxy.yml | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/test/cli/proxy_test.rb b/test/cli/proxy_test.rb index 7d08c62f3..92ab30030 100644 --- a/test/cli/proxy_test.rb +++ b/test/cli/proxy_test.rb @@ -25,7 +25,7 @@ class CliProxyTest < CliTestCase assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output assert_match "docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE} on 1.1.1.1", output - assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"172.1.0.2:80\" --deploy-timeout \"6s\" on 1.1.1.1", output + assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"172.1.0.2:80\" --deploy-timeout \"6s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" on 1.1.1.1", output assert_match "docker container stop kamal-proxy on 1.1.1.2", output assert_match "docker container stop traefik on 1.1.1.2", output diff --git a/test/commands/proxy_test.rb b/test/commands/proxy_test.rb index 73e7bfbf2..7e7a60cb4 100644 --- a/test/commands/proxy_test.rb +++ b/test/commands/proxy_test.rb @@ -3,7 +3,7 @@ class CommandsProxyTest < ActiveSupport::TestCase setup do @config = { - service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ] + service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], builder: { "arch" => "amd64" } } ENV["EXAMPLE_API_KEY"] = "456" @@ -109,7 +109,7 @@ class CommandsProxyTest < ActiveSupport::TestCase test "deploy" do assert_equal \ - "docker exec kamal-proxy kamal-proxy deploy service --target \"172.1.0.2:80\"", + "docker exec kamal-proxy kamal-proxy deploy service --target \"172.1.0.2:80\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\"", new_command.deploy("service", target: "172.1.0.2:80").join(" ") end diff --git a/test/fixtures/deploy_with_proxy.yml b/test/fixtures/deploy_with_proxy.yml index bfe8c505c..2912c6454 100644 --- a/test/fixtures/deploy_with_proxy.yml +++ b/test/fixtures/deploy_with_proxy.yml @@ -10,6 +10,8 @@ servers: registry: username: user password: pw +builder: + arch: amd64 proxy: enabled: true From 63ebeda4897db28b3e53be8b607d31856520af21 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 5 Sep 2024 14:31:37 +0100 Subject: [PATCH 09/43] Create proxy and app containers in a kamal network --- lib/kamal/cli/proxy.rb | 6 ++++++ lib/kamal/commands/app.rb | 7 +++---- lib/kamal/commands/app/containers.rb | 2 +- lib/kamal/commands/docker.rb | 4 ++++ lib/kamal/commands/proxy.rb | 1 + test/cli/app_test.rb | 2 +- test/cli/proxy_test.rb | 8 ++++---- test/commands/proxy_test.rb | 8 ++++---- 8 files changed, 24 insertions(+), 14 deletions(-) diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index d546439cb..87fee4b4f 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -3,6 +3,12 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base def boot raise_unless_kamal_proxy_enabled! with_lock do + on(KAMAL.hosts) do |host| + execute *KAMAL.docker.create_network + rescue SSHKit::Command::Failed => e + raise unless e.message.include?("already exists") + end + on(KAMAL.traefik_hosts) do |host| execute *KAMAL.registry.login execute *KAMAL.traefik_or_proxy(host).start_or_run diff --git a/lib/kamal/commands/app.rb b/lib/kamal/commands/app.rb index ad6e0613a..94ef29e98 100644 --- a/lib/kamal/commands/app.rb +++ b/lib/kamal/commands/app.rb @@ -5,6 +5,8 @@ class Kamal::Commands::App < Kamal::Commands::Base attr_reader :role, :host + delegate :container_name, to: :role + def initialize(config, role: nil, host: nil) super(config) @role = role @@ -35,6 +37,7 @@ def run_for_proxy(hostname: nil) "--detach", "--restart unless-stopped", "--name", container_name, + "--network", "kamal", *([ "--hostname", hostname ] if hostname), "-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"", "-e", "KAMAL_VERSION=\"#{config.version}\"", @@ -92,10 +95,6 @@ def ensure_env_directory end private - def container_name(version = nil) - [ role.container_prefix, version || config.version ].compact.join("-") - end - def latest_image_id docker :image, :ls, *argumentize("--filter", "reference=#{config.latest_image}"), "--format", "'{{.ID}}'" end diff --git a/lib/kamal/commands/app/containers.rb b/lib/kamal/commands/app/containers.rb index 629fa47fb..2d8289625 100644 --- a/lib/kamal/commands/app/containers.rb +++ b/lib/kamal/commands/app/containers.rb @@ -32,7 +32,7 @@ def container_health_log(version:) def container_endpoint(version:) pipe \ container_id_for(container_name: container_name(version)), - xargs(docker(:inspect, "--format", "'{{.NetworkSettings.IPAddress}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'")), + xargs(docker(:inspect, "--format", "'{{index .NetworkSettings.Networks.kamal.Aliases 0}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'")), [ :sed, "-e", "'s/\\/tcp$//'" ] end end diff --git a/lib/kamal/commands/docker.rb b/lib/kamal/commands/docker.rb index 2966e095d..bc9345dfc 100644 --- a/lib/kamal/commands/docker.rb +++ b/lib/kamal/commands/docker.rb @@ -19,6 +19,10 @@ def superuser? [ '[ "${EUID:-$(id -u)}" -eq 0 ] || command -v sudo >/dev/null || command -v su >/dev/null' ] end + def create_network + docker :network, :create, :kamal + end + private def get_docker shell \ diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb index 9ae029f76..3a17afaee 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -12,6 +12,7 @@ def initialize(config) def run docker :run, "--name", container_name, + "--network", "kamal", "--detach", "--restart", "unless-stopped", *proxy_config.publish_args, diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index 81ac8b16e..225bf21d4 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -362,7 +362,7 @@ class CliAppTest < CliTestCase run_command("boot", config: :with_proxy).tap do |output| assert_match /Renaming container .* to .* as already deployed on 1.1.1.1/, output # Rename assert_match /docker rename app-web-latest app-web-latest_replaced_[0-9a-f]{16}/, output - assert_match /docker run --detach --restart unless-stopped --name app-web-latest --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 --log-opt max-size="10m" --label service="app" --label role="web" --label destination dhh\/app:latest/, output + assert_match /docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-[0-9a-f]{12} -e KAMAL_CONTAINER_NAME="app-web-latest" -e KAMAL_VERSION="latest" --env-file .kamal\/env\/roles\/app-web.env --log-opt max-size="10m" --label service="app" --label role="web" --label destination dhh\/app:latest/, output assert_match /docker exec kamal-proxy kamal-proxy deploy app-web --target "123"/, output assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output end diff --git a/test/cli/proxy_test.rb b/test/cli/proxy_test.rb index 92ab30030..e35c3c063 100644 --- a/test/cli/proxy_test.rb +++ b/test/cli/proxy_test.rb @@ -4,13 +4,13 @@ class CliProxyTest < CliTestCase test "boot" do run_command("boot").tap do |output| assert_match "docker login", output - assert_match "docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", output + assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", output end end test "reboot" do SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{.NetworkSettings.IPAddress}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'", "|", :sed, "-e", "'s/\\/tcp$//'") + .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{index .NetworkSettings.Networks.kamal.Aliases 0}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'", "|", :sed, "-e", "'s/\\/tcp$//'") .returns("172.1.0.2:80") .at_least_once @@ -24,7 +24,7 @@ class CliProxyTest < CliTestCase assert_match "docker container stop traefik on 1.1.1.1", output assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output - assert_match "docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE} on 1.1.1.1", output + assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE} on 1.1.1.1", output assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"172.1.0.2:80\" --deploy-timeout \"6s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" on 1.1.1.1", output assert_match "docker container stop kamal-proxy on 1.1.1.2", output @@ -37,7 +37,7 @@ class CliProxyTest < CliTestCase test "reboot --rolling" do SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{.NetworkSettings.IPAddress}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'", "|", :sed, "-e", "'s/\\/tcp$//'") + .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{index .NetworkSettings.Networks.kamal.Aliases 0}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'", "|", :sed, "-e", "'s/\\/tcp$//'") .returns("172.1.0.2:80") .at_least_once diff --git a/test/commands/proxy_test.rb b/test/commands/proxy_test.rb index 7e7a60cb4..0a2cc6c02 100644 --- a/test/commands/proxy_test.rb +++ b/test/commands/proxy_test.rb @@ -15,13 +15,13 @@ class CommandsProxyTest < ActiveSupport::TestCase test "run" do assert_equal \ - "docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", + "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", new_command.run.join(" ") end test "run with ports configured" do assert_equal \ - "docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", + "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", new_command.run.join(" ") end @@ -29,7 +29,7 @@ class CommandsProxyTest < ActiveSupport::TestCase @config.delete(:proxy) assert_equal \ - "docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", + "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", new_command.run.join(" ") end @@ -37,7 +37,7 @@ class CommandsProxyTest < ActiveSupport::TestCase @config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } } assert_equal \ - "docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", + "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", new_command.run.join(" ") end From f347ef7e447b06b8f8c1f9a7f5ea3576dafcbfec Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 5 Sep 2024 16:02:43 +0100 Subject: [PATCH 10/43] Add proxy upgrade command --- lib/kamal/cli/proxy.rb | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index 87fee4b4f..ef7ae6463 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -61,6 +61,43 @@ def reboot end end + desc "upgrade", "Upgrade to correct proxy on servers (stop container, remove container, start new container)" + option :rolling, type: :boolean, default: false, desc: "Reboot proxy on hosts in sequence, rather than in parallel" + option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question" + def upgrade + invoke_options = { "version" => KAMAL.config.version }.merge(options) + + raise_unless_kamal_proxy_enabled! + confirming "This will cause a brief outage on each host. Are you sure?" do + host_groups = options[:rolling] ? KAMAL.hosts : [ KAMAL.hosts ] + host_groups.each do |hosts| + host_list = Array(hosts).join(",") + run_hook "pre-traefik-reboot", hosts: host_list + on(hosts) do |host| + execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug + execute *KAMAL.registry.login + + "Stopping and removing Traefik on #{host}, if running..." + execute *KAMAL.traefik.stop, raise_on_non_zero_exit: false + execute *KAMAL.traefik.remove_container + + "Stopping and removing kamal-proxy on #{host}, if running..." + execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false + execute *KAMAL.proxy.remove_container + end + + invoke "kamal:cli:proxy:boot", [], invoke_options.merge("hosts" => host_list) + reset_invocation(Kamal::Cli::Proxy) + invoke "kamal:cli:app:boot", [], invoke_options.merge("hosts" => host_list) + reset_invocation(Kamal::Cli::App) + invoke "kamal:cli:prune:all", [], invoke_options.merge("hosts" => host_list) + reset_invocation(Kamal::Cli::Prune) + + run_hook "post-traefik-reboot", hosts: host_list + end + end + end + desc "start", "Start existing proxy container on servers" def start raise_unless_kamal_proxy_enabled! @@ -163,4 +200,8 @@ def raise_unless_kamal_proxy_enabled! raise "kamal proxy commands are disabled unless experimental proxy support is enabled. Use `kamal traefik` commands instead." end end + + def reset_invocation(cli_class) + instance_variable_get("@_invocations")[cli_class].pop + end end From 9c2d5f83f772609d45cafc6b42fd7b594d72b872 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Fri, 6 Sep 2024 12:22:57 +0100 Subject: [PATCH 11/43] Boot latest version when upgrading proxy --- lib/kamal/cli/proxy.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index ef7ae6463..1092d39c1 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -88,7 +88,7 @@ def upgrade invoke "kamal:cli:proxy:boot", [], invoke_options.merge("hosts" => host_list) reset_invocation(Kamal::Cli::Proxy) - invoke "kamal:cli:app:boot", [], invoke_options.merge("hosts" => host_list) + invoke "kamal:cli:app:boot", [], invoke_options.merge("hosts" => host_list, version: KAMAL.config.latest_tag) reset_invocation(Kamal::Cli::App) invoke "kamal:cli:prune:all", [], invoke_options.merge("hosts" => host_list) reset_invocation(Kamal::Cli::Prune) From 2056351c384011843454c681b4c1821edf3676b1 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Fri, 6 Sep 2024 15:06:01 +0100 Subject: [PATCH 12/43] Use kamal network for accessories --- lib/kamal/cli/accessory.rb | 43 +++++++++++++++-------------- lib/kamal/commands/accessory.rb | 2 ++ lib/kamal/commands/app/execution.rb | 1 + test/cli/accessory_test.rb | 25 +++++++++-------- test/cli/app_test.rb | 6 ++-- test/commands/accessory_test.rb | 12 ++++---- test/commands/app_test.rb | 14 +++++----- 7 files changed, 56 insertions(+), 47 deletions(-) diff --git a/lib/kamal/cli/accessory.rb b/lib/kamal/cli/accessory.rb index 2bf9a7860..bc84fe522 100644 --- a/lib/kamal/cli/accessory.rb +++ b/lib/kamal/cli/accessory.rb @@ -1,16 +1,17 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)" - def boot(name, login: true) + def boot(name, prepare: true) with_lock do if name == "all" KAMAL.accessory_names.each { |accessory_name| boot(accessory_name) } else + prepare(name) if prepare + with_accessory(name) do |accessory, hosts| directories(name) upload(name) on(hosts) do - execute *KAMAL.registry.login if login execute *KAMAL.auditor.record("Booted #{name} accessory"), verbosity: :debug execute *accessory.ensure_env_directory upload! accessory.secrets_io, accessory.secrets_path, mode: "0600" @@ -57,15 +58,10 @@ def reboot(name) if name == "all" KAMAL.accessory_names.each { |accessory_name| reboot(accessory_name) } else - with_accessory(name) do |accessory, hosts| - on(hosts) do - execute *KAMAL.registry.login - end - - stop(name) - remove_container(name) - boot(name, login: false) - end + prepare(name) + stop(name) + remove_container(name) + boot(name, prepare: false) end end end @@ -97,10 +93,8 @@ def stop(name) desc "restart [NAME]", "Restart existing accessory container on host" def restart(name) with_lock do - with_accessory(name) do - stop(name) - start(name) - end + stop(name) + start(name) end end @@ -251,11 +245,20 @@ def accessory_hosts(accessory) end def remove_accessory(name) - with_accessory(name) do - stop(name) - remove_container(name) - remove_image(name) - remove_service_directory(name) + stop(name) + remove_container(name) + remove_image(name) + remove_service_directory(name) + end + + def prepare(name) + with_accessory(name) do |accessory, hosts| + on(hosts) do + execute *KAMAL.registry.login + execute *KAMAL.docker.create_network + rescue SSHKit::Command::Failed => e + raise unless e.message.include?("already exists") + end end end end diff --git a/lib/kamal/commands/accessory.rb b/lib/kamal/commands/accessory.rb index f3b676d14..787f7d43a 100644 --- a/lib/kamal/commands/accessory.rb +++ b/lib/kamal/commands/accessory.rb @@ -15,6 +15,7 @@ def run "--name", service_name, "--detach", "--restart", "unless-stopped", + "--network", "kamal", *config.logging_args, *publish_args, *env_args, @@ -63,6 +64,7 @@ def execute_in_new_container(*command, interactive: false) docker :run, ("-it" if interactive), "--rm", + "--network", "kamal", *env_args, *volume_args, image, diff --git a/lib/kamal/commands/app/execution.rb b/lib/kamal/commands/app/execution.rb index 215821dcc..4434c26aa 100644 --- a/lib/kamal/commands/app/execution.rb +++ b/lib/kamal/commands/app/execution.rb @@ -11,6 +11,7 @@ def execute_in_new_container(*command, interactive: false, env:) docker :run, ("-it" if interactive), "--rm", + "--network", "kamal", *role&.env_args(host), *argumentize("--env", env), *config.volume_args, diff --git a/test/cli/accessory_test.rb b/test/cli/accessory_test.rb index 0e3abc467..c9016f56d 100644 --- a/test/cli/accessory_test.rb +++ b/test/cli/accessory_test.rb @@ -15,7 +15,7 @@ class CliAccessoryTest < CliTestCase run_command("boot", "mysql").tap do |output| assert_match /docker login.*on 1.1.1.3/, output - assert_match "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/env/accessories/app-mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output + assert_match "docker run --name app-mysql --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/env/accessories/app-mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output end end @@ -29,9 +29,12 @@ class CliAccessoryTest < CliTestCase assert_match /docker login.*on 1.1.1.3/, output assert_match /docker login.*on 1.1.1.1/, output assert_match /docker login.*on 1.1.1.2/, output - assert_match "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/env/accessories/app-mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output - assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output - assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", output + assert_match /docker network create kamal.*on 1.1.1.1/, output + assert_match /docker network create kamal.*on 1.1.1.2/, output + assert_match /docker network create kamal.*on 1.1.1.3/, output + assert_match "docker run --name app-mysql --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/env/accessories/app-mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output + assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output + assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", output end end @@ -51,7 +54,7 @@ class CliAccessoryTest < CliTestCase Kamal::Commands::Registry.any_instance.expects(:login) Kamal::Cli::Accessory.any_instance.expects(:stop).with("mysql") Kamal::Cli::Accessory.any_instance.expects(:remove_container).with("mysql") - Kamal::Cli::Accessory.any_instance.expects(:boot).with("mysql", login: false) + Kamal::Cli::Accessory.any_instance.expects(:boot).with("mysql", prepare: false) run_command("reboot", "mysql") end @@ -60,10 +63,10 @@ class CliAccessoryTest < CliTestCase Kamal::Commands::Registry.any_instance.expects(:login).times(3) Kamal::Cli::Accessory.any_instance.expects(:stop).with("mysql") Kamal::Cli::Accessory.any_instance.expects(:remove_container).with("mysql") - Kamal::Cli::Accessory.any_instance.expects(:boot).with("mysql", login: false) + Kamal::Cli::Accessory.any_instance.expects(:boot).with("mysql", prepare: false) Kamal::Cli::Accessory.any_instance.expects(:stop).with("redis") Kamal::Cli::Accessory.any_instance.expects(:remove_container).with("redis") - Kamal::Cli::Accessory.any_instance.expects(:boot).with("redis", login: false) + Kamal::Cli::Accessory.any_instance.expects(:boot).with("redis", prepare: false) run_command("reboot", "all") end @@ -200,8 +203,8 @@ class CliAccessoryTest < CliTestCase run_command("boot", "redis", "--hosts", "1.1.1.1").tap do |output| assert_match /docker login.*on 1.1.1.1/, output assert_no_match /docker login.*on 1.1.1.2/, output - assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output - assert_no_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", output + assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output + assert_no_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", output end end @@ -212,8 +215,8 @@ class CliAccessoryTest < CliTestCase run_command("boot", "redis", "--hosts", "1.1.1.1,1.1.1.3").tap do |output| assert_match /docker login.*on 1.1.1.1/, output assert_no_match /docker login.*on 1.1.1.3/, output - assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output - assert_no_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.3", output + assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output + assert_no_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.3", output end end diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index 225bf21d4..2968a7a32 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -243,13 +243,13 @@ class CliAppTest < CliTestCase test "exec" do run_command("exec", "ruby -v").tap do |output| - assert_match "docker run --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v", output + assert_match "docker run --rm --network kamal --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v", output end end test "exec separate arguments" do run_command("exec", "ruby", " -v").tap do |output| - assert_match "docker run --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v", output + assert_match "docker run --rm --network kamal --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v", output end end @@ -262,7 +262,7 @@ class CliAppTest < CliTestCase test "exec interactive" do SSHKit::Backend::Abstract.any_instance.expects(:exec) - .with("ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v'") + .with("ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --network kamal --env-file .kamal/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 diff --git a/test/commands/accessory_test.rb b/test/commands/accessory_test.rb index 23d304dac..bc3df9ce5 100644 --- a/test/commands/accessory_test.rb +++ b/test/commands/accessory_test.rb @@ -51,15 +51,15 @@ class CommandsAccessoryTest < ActiveSupport::TestCase test "run" do assert_equal \ - "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/env/accessories/app-mysql.env --label service=\"app-mysql\" private.registry/mysql:8.0", + "docker run --name app-mysql --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/env/accessories/app-mysql.env --label service=\"app-mysql\" private.registry/mysql:8.0", new_command(:mysql).run.join(" ") assert_equal \ - "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env SOMETHING=\"else\" --env-file .kamal/env/accessories/app-redis.env --volume /var/lib/redis:/data --label service=\"app-redis\" --label cache=\"true\" redis:latest", + "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env SOMETHING=\"else\" --env-file .kamal/env/accessories/app-redis.env --volume /var/lib/redis:/data --label service=\"app-redis\" --label cache=\"true\" redis:latest", new_command(:redis).run.join(" ") assert_equal \ - "docker run --name custom-busybox --detach --restart unless-stopped --log-opt max-size=\"10m\" --env-file .kamal/env/accessories/custom-busybox.env --label service=\"custom-busybox\" busybox:latest", + "docker run --name custom-busybox --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --env-file .kamal/env/accessories/custom-busybox.env --label service=\"custom-busybox\" busybox:latest", new_command(:busybox).run.join(" ") end @@ -67,7 +67,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase @config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } } assert_equal \ - "docker run --name custom-busybox --detach --restart unless-stopped --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --env-file .kamal/env/accessories/custom-busybox.env --label service=\"custom-busybox\" busybox:latest", + "docker run --name custom-busybox --detach --restart unless-stopped --network kamal --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --env-file .kamal/env/accessories/custom-busybox.env --label service=\"custom-busybox\" busybox:latest", new_command(:busybox).run.join(" ") end @@ -92,7 +92,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase test "execute in new container" do assert_equal \ - "docker run --rm --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/env/accessories/app-mysql.env private.registry/mysql:8.0 mysql -u root", + "docker run --rm --network kamal --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/env/accessories/app-mysql.env private.registry/mysql:8.0 mysql -u root", new_command(:mysql).execute_in_new_container("mysql", "-u", "root").join(" ") end @@ -104,7 +104,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase test "execute in new container over ssh" do new_command(:mysql).stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) do - assert_match %r{docker run -it --rm --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/env/accessories/app-mysql.env private.registry/mysql:8.0 mysql -u root}, + assert_match %r{docker run -it --rm --network kamal --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/env/accessories/app-mysql.env private.registry/mysql:8.0 mysql -u root}, new_command(:mysql).execute_in_new_container_over_ssh("mysql", "-u", "root") end end diff --git a/test/commands/app_test.rb b/test/commands/app_test.rb index e385764e0..caecd2551 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -204,13 +204,13 @@ class CommandsAppTest < ActiveSupport::TestCase test "execute in new container" do assert_equal \ - "docker run --rm --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails db:setup", + "docker run --rm --network kamal --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails db:setup", new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ") end test "execute in new container with env" do assert_equal \ - "docker run --rm --env-file .kamal/env/roles/app-web.env --env foo=\"bar\" dhh/app:999 bin/rails db:setup", + "docker run --rm --network kamal --env-file .kamal/env/roles/app-web.env --env foo=\"bar\" dhh/app:999 bin/rails db:setup", new_command.execute_in_new_container("bin/rails", "db:setup", env: { "foo" => "bar" }).join(" ") end @@ -219,14 +219,14 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } } assert_equal \ - "docker run --rm --env ENV1=\"value1\" --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails db:setup", + "docker run --rm --network kamal --env ENV1=\"value1\" --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails db:setup", new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ") end test "execute in new container with custom options" do @config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } } assert_equal \ - "docker run --rm --env-file .kamal/env/roles/app-web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails db:setup", + "docker run --rm --network kamal --env-file .kamal/env/roles/app-web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails db:setup", new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ") end @@ -243,7 +243,7 @@ class CommandsAppTest < ActiveSupport::TestCase end test "execute in new container over ssh" do - assert_match %r{docker run -it --rm --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails c}, + assert_match %r{docker run -it --rm --network kamal --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails c}, new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {}) end @@ -251,13 +251,13 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:servers] = [ { "1.1.1.1" => "tag1" } ] @config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } } - assert_equal "ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --env ENV1=\"value1\" --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails c'", + assert_equal "ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --network kamal --env ENV1=\"value1\" --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails c'", new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {}) end test "execute in new container with custom options over ssh" do @config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } } - assert_match %r{docker run -it --rm --env-file .kamal/env/roles/app-web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c}, + assert_match %r{docker run -it --rm --network kamal --env-file .kamal/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", env: {}) end From b33c999125c79d4b1079e6d94d7a5666691d46a7 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Mon, 9 Sep 2024 16:11:35 +0100 Subject: [PATCH 13/43] Remove envify, make proxy booting work with env files --- lib/kamal/cli/proxy.rb | 8 +++++++- lib/kamal/commands/app/execution.rb | 1 - lib/kamal/configuration/docs/env.yml | 2 -- test/integration/main_test.rb | 1 - test/integration/proxy_test.rb | 2 -- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index 1092d39c1..81b734f78 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -11,7 +11,13 @@ def boot on(KAMAL.traefik_hosts) do |host| execute *KAMAL.registry.login - execute *KAMAL.traefik_or_proxy(host).start_or_run + if KAMAL.proxy_host?(host) + execute *KAMAL.proxy.start_or_run + else + execute *KAMAL.traefik.ensure_env_directory + upload! KAMAL.traefik.secrets_io, KAMAL.traefik.secrets_path, mode: "0600" + execute *KAMAL.traefik.start_or_run + end end end end diff --git a/lib/kamal/commands/app/execution.rb b/lib/kamal/commands/app/execution.rb index 4434c26aa..215821dcc 100644 --- a/lib/kamal/commands/app/execution.rb +++ b/lib/kamal/commands/app/execution.rb @@ -11,7 +11,6 @@ def execute_in_new_container(*command, interactive: false, env:) docker :run, ("-it" if interactive), "--rm", - "--network", "kamal", *role&.env_args(host), *argumentize("--env", env), *config.volume_args, diff --git a/lib/kamal/configuration/docs/env.yml b/lib/kamal/configuration/docs/env.yml index 0ca2bfa9e..c2cf0ed0c 100644 --- a/lib/kamal/configuration/docs/env.yml +++ b/lib/kamal/configuration/docs/env.yml @@ -24,14 +24,12 @@ env: # KAMAL_REGISTRY_PASSWORD=pw # DB_PASSWORD=secret123 # ``` -# See https://kamal-deploy.org/docs/commands/envify/ for how to use generated .env files. # # To pass the secrets you should list them under the `secret` key. When you do this the # other variables need to be moved under the `clear` key. # # Unlike clear values, secrets are not passed directly to the container, # but are stored in an env file on the host -# The file is not updated when deploying, only when running `kamal envify` or `kamal env push`. env: clear: DB_USER: app diff --git a/test/integration/main_test.rb b/test/integration/main_test.rb index b58aeeb9b..1eb05eff8 100644 --- a/test/integration/main_test.rb +++ b/test/integration/main_test.rb @@ -76,7 +76,6 @@ class MainTest < IntegrationTest test "aliases" do @app = "app_with_roles" - kamal :envify kamal :deploy output = kamal :whome, capture: true diff --git a/test/integration/proxy_test.rb b/test/integration/proxy_test.rb index 1f4442661..f5698592b 100644 --- a/test/integration/proxy_test.rb +++ b/test/integration/proxy_test.rb @@ -6,8 +6,6 @@ class ProxyTest < IntegrationTest end test "boot, reboot, stop, start, restart, logs, remove" do - kamal :envify - kamal :proxy, :boot assert_proxy_running From 2fdc59a3aa300ec800c0e938cc0995a4eb779ca7 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Tue, 10 Sep 2024 09:22:43 +0100 Subject: [PATCH 14/43] Fix tests --- test/cli/app_test.rb | 6 +++--- test/commands/app_test.rb | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index 2968a7a32..225bf21d4 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -243,13 +243,13 @@ class CliAppTest < CliTestCase test "exec" do run_command("exec", "ruby -v").tap do |output| - assert_match "docker run --rm --network kamal --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v", output + assert_match "docker run --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v", output end end test "exec separate arguments" do run_command("exec", "ruby", " -v").tap do |output| - assert_match "docker run --rm --network kamal --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v", output + assert_match "docker run --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v", output end end @@ -262,7 +262,7 @@ class CliAppTest < CliTestCase test "exec interactive" do SSHKit::Backend::Abstract.any_instance.expects(:exec) - .with("ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --network kamal --env-file .kamal/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 diff --git a/test/commands/app_test.rb b/test/commands/app_test.rb index caecd2551..e385764e0 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -204,13 +204,13 @@ class CommandsAppTest < ActiveSupport::TestCase test "execute in new container" do assert_equal \ - "docker run --rm --network kamal --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails db:setup", + "docker run --rm --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails db:setup", new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ") end test "execute in new container with env" do assert_equal \ - "docker run --rm --network kamal --env-file .kamal/env/roles/app-web.env --env foo=\"bar\" dhh/app:999 bin/rails db:setup", + "docker run --rm --env-file .kamal/env/roles/app-web.env --env foo=\"bar\" dhh/app:999 bin/rails db:setup", new_command.execute_in_new_container("bin/rails", "db:setup", env: { "foo" => "bar" }).join(" ") end @@ -219,14 +219,14 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } } assert_equal \ - "docker run --rm --network kamal --env ENV1=\"value1\" --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails db:setup", + "docker run --rm --env ENV1=\"value1\" --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails db:setup", new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ") end test "execute in new container with custom options" do @config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } } assert_equal \ - "docker run --rm --network kamal --env-file .kamal/env/roles/app-web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails db:setup", + "docker run --rm --env-file .kamal/env/roles/app-web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails db:setup", new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ") end @@ -243,7 +243,7 @@ class CommandsAppTest < ActiveSupport::TestCase end test "execute in new container over ssh" do - assert_match %r{docker run -it --rm --network kamal --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails c}, + assert_match %r{docker run -it --rm --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails c}, new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {}) end @@ -251,13 +251,13 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:servers] = [ { "1.1.1.1" => "tag1" } ] @config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } } - assert_equal "ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --network kamal --env ENV1=\"value1\" --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails c'", + assert_equal "ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --env ENV1=\"value1\" --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", env: {}) end test "execute in new container with custom options over ssh" do @config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } } - assert_match %r{docker run -it --rm --network kamal --env-file .kamal/env/roles/app-web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c}, + assert_match %r{docker run -it --rm --env-file .kamal/env/roles/app-web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c}, new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {}) end From e9d480b514ea150f5eadaa8db2db700ca7a43aa3 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Tue, 10 Sep 2024 14:47:33 +0100 Subject: [PATCH 15/43] Add the proxy/ssl config and pass on to kamal-proxy --- lib/kamal/configuration/docs/proxy.yml | 7 ++++++ lib/kamal/configuration/proxy.rb | 7 +++++- lib/kamal/configuration/validator/proxy.rb | 9 ++++++++ test/configuration/proxy_test.rb | 25 ++++++++++++++++++++++ 4 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 lib/kamal/configuration/validator/proxy.rb create mode 100644 test/configuration/proxy_test.rb diff --git a/lib/kamal/configuration/docs/proxy.yml b/lib/kamal/configuration/docs/proxy.yml index 8d5b201fe..2d101aefe 100644 --- a/lib/kamal/configuration/docs/proxy.yml +++ b/lib/kamal/configuration/docs/proxy.yml @@ -56,6 +56,13 @@ proxy: # requests for other apps that do have a host set. host: foo.example.com + # SSL + # + # Kamal Proxy can automatically obtain and renew TLS certificates for your applications. + # To ensure this set, the ssl flag. This only works if we are deploying to one server and + # the host flag is set. + ssl: true + # Deploy timeout # # How long to wait for the app to boot when deploying, defaults to 30 seconds diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb index dd5aac1d0..69b79be7c 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -10,7 +10,7 @@ class Kamal::Configuration::Proxy def initialize(config:) @proxy_config = config.raw_config.proxy || {} - validate! proxy_config + validate! proxy_config, with: Kamal::Configuration::Validator::Proxy end def enabled? @@ -37,9 +37,14 @@ def publish_args argumentize "--publish", [ "#{DEFAULT_HTTP_PORT}:#{DEFAULT_HTTP_PORT}", "#{DEFAULT_HTTPS_PORT}:#{DEFAULT_HTTPS_PORT}" ] end + def ssl? + proxy_config.fetch("ssl", false) + end + def deploy_options { host: proxy_config["host"], + tls: proxy_config["ssl"], "deploy-timeout": proxy_config["deploy_timeout"], "drain-timeout": proxy_config["drain_timeout"], "health-check-interval": proxy_config.dig("health_check", "interval"), diff --git a/lib/kamal/configuration/validator/proxy.rb b/lib/kamal/configuration/validator/proxy.rb new file mode 100644 index 000000000..a4ee19bf5 --- /dev/null +++ b/lib/kamal/configuration/validator/proxy.rb @@ -0,0 +1,9 @@ +class Kamal::Configuration::Validator::Proxy < Kamal::Configuration::Validator + def validate! + super + + if config["host"].blank? && config["ssl"] + error "Must set a host to enable automatic SSL" + end + end +end diff --git a/test/configuration/proxy_test.rb b/test/configuration/proxy_test.rb new file mode 100644 index 000000000..3aa3f85e3 --- /dev/null +++ b/test/configuration/proxy_test.rb @@ -0,0 +1,25 @@ +require "test_helper" + +class ConfigurationEnvTest < ActiveSupport::TestCase + setup do + @deploy = { + service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, + builder: { "arch" => "amd64" }, servers: [ "1.1.1.1" ] + } + end + + test "ssl with host" do + @deploy[:proxy] = { "ssl" => true, "host" => "example.com" } + assert_equal true, config.proxy.ssl? + end + + test "ssl with no host" do + @deploy[:proxy] = { "ssl" => true } + assert_raises(Kamal::ConfigurationError) { config.proxy.ssl? } + end + + private + def config + Kamal::Configuration.new(@deploy) + end +end From 6f2eaed398654096f2d4a9c36d843f3af0742212 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Tue, 10 Sep 2024 17:47:38 +0100 Subject: [PATCH 16/43] Work out the host and port for the container Avoid docker inspect: 1. Use the container ID as the host 2. Configure the port, default to 3000 --- lib/kamal/cli/app.rb | 4 ++-- lib/kamal/cli/app/boot.rb | 2 +- lib/kamal/cli/proxy.rb | 2 +- lib/kamal/commands/app/containers.rb | 7 ------- lib/kamal/commands/proxy.rb | 6 +++--- lib/kamal/configuration/docs/proxy.yml | 6 ++++++ lib/kamal/configuration/proxy.rb | 4 ++++ test/cli/app_test.rb | 2 +- test/cli/proxy_test.rb | 10 +++++----- test/commands/proxy_test.rb | 8 ++++---- .../docker/deployer/app_with_roles/config/deploy.yml | 1 + 11 files changed, 28 insertions(+), 24 deletions(-) diff --git a/lib/kamal/cli/app.rb b/lib/kamal/cli/app.rb index 732addae9..aaa8cd506 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -44,7 +44,7 @@ def start if role.running_traefik? && KAMAL.proxy_host?(host) version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip - endpoint = capture_with_info(*app.container_endpoint(version: version)).strip + endpoint = capture_with_info(*app.container_id_for_version(version)).strip raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, did the container boot?" if endpoint.empty? execute *KAMAL.proxy.deploy(role.container_prefix, target: endpoint) @@ -66,7 +66,7 @@ def stop if role.running_traefik? && KAMAL.proxy_host?(host) version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip - endpoint = capture_with_info(*app.container_endpoint(version: version)).strip + endpoint = capture_with_info(*app.container_id_for_version(version)).strip if endpoint.present? execute *KAMAL.proxy.remove(role.container_prefix, target: endpoint), raise_on_non_zero_exit: false end diff --git a/lib/kamal/cli/app/boot.rb b/lib/kamal/cli/app/boot.rb index b6a094773..00e0fb732 100644 --- a/lib/kamal/cli/app/boot.rb +++ b/lib/kamal/cli/app/boot.rb @@ -53,7 +53,7 @@ def start_new_version if proxy_host? execute *app.run_for_proxy(hostname: hostname) if running_traefik? - endpoint = capture_with_info(*app.container_endpoint(version: version)).strip + endpoint = capture_with_info(*app.container_id_for_version(version)).strip raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, did the container boot?" if endpoint.empty? execute *KAMAL.proxy.deploy(role.container_prefix, target: endpoint) else diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index 81b734f78..b0bb33a4c 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -52,7 +52,7 @@ def reboot app = KAMAL.app(role: role, host: host) version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip - endpoint = capture_with_info(*app.container_endpoint(version: version)).strip + endpoint = capture_with_info(*app.container_id_for_version(version)).strip if endpoint.present? info "Deploying #{endpoint} for role `#{role}` on #{host}..." diff --git a/lib/kamal/commands/app/containers.rb b/lib/kamal/commands/app/containers.rb index 2d8289625..0bab388b8 100644 --- a/lib/kamal/commands/app/containers.rb +++ b/lib/kamal/commands/app/containers.rb @@ -28,11 +28,4 @@ def container_health_log(version:) container_id_for(container_name: container_name(version)), xargs(docker(:inspect, "--format", DOCKER_HEALTH_LOG_FORMAT)) end - - def container_endpoint(version:) - pipe \ - container_id_for(container_name: container_name(version)), - xargs(docker(:inspect, "--format", "'{{index .NetworkSettings.Networks.kamal.Aliases 0}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'")), - [ :sed, "-e", "'s/\\/tcp$//'" ] - end end diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb index 3a17afaee..b50cd0f25 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -1,6 +1,6 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base delegate :argumentize, :optionize, to: Kamal::Utils - delegate :container_name, to: :proxy_config + delegate :container_name, :port, to: :proxy_config attr_reader :proxy_config @@ -35,11 +35,11 @@ def start_or_run end def deploy(service, target:) - docker :exec, container_name, "kamal-proxy", :deploy, service, *optionize({ target: target }), *proxy_config.deploy_command_args + docker :exec, container_name, "kamal-proxy", :deploy, service, *optionize({ target: "#{target}:#{port}" }), *proxy_config.deploy_command_args end def remove(service, target:) - docker :exec, container_name, "kamal-proxy", :remove, service, *optionize({ target: target }) + docker :exec, container_name, "kamal-proxy", :remove, service, *optionize({ target: "#{target}:#{port}" }) end def info diff --git a/lib/kamal/configuration/docs/proxy.yml b/lib/kamal/configuration/docs/proxy.yml index 2d101aefe..c30117fbf 100644 --- a/lib/kamal/configuration/docs/proxy.yml +++ b/lib/kamal/configuration/docs/proxy.yml @@ -56,6 +56,12 @@ proxy: # requests for other apps that do have a host set. host: foo.example.com + # Port + # + # The port the application is exposed on + # Defaults to 80 + port: 3000 + # SSL # # Kamal Proxy can automatically obtain and renew TLS certificates for your applications. diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb index 69b79be7c..db3dcd96e 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -25,6 +25,10 @@ def hosts end end + def port + proxy_config.fetch("port", 80) + end + def image proxy_config.fetch("image", DEFAULT_IMAGE) end diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index 225bf21d4..17ae3debd 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -363,7 +363,7 @@ class CliAppTest < CliTestCase assert_match /Renaming container .* to .* as already deployed on 1.1.1.1/, output # Rename assert_match /docker rename app-web-latest app-web-latest_replaced_[0-9a-f]{16}/, output assert_match /docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-[0-9a-f]{12} -e KAMAL_CONTAINER_NAME="app-web-latest" -e KAMAL_VERSION="latest" --env-file .kamal\/env\/roles\/app-web.env --log-opt max-size="10m" --label service="app" --label role="web" --label destination dhh\/app:latest/, output - assert_match /docker exec kamal-proxy kamal-proxy deploy app-web --target "123"/, output + assert_match /docker exec kamal-proxy kamal-proxy deploy app-web --target "123:3000"/, output assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output end end diff --git a/test/cli/proxy_test.rb b/test/cli/proxy_test.rb index e35c3c063..3d65207c8 100644 --- a/test/cli/proxy_test.rb +++ b/test/cli/proxy_test.rb @@ -10,8 +10,8 @@ class CliProxyTest < CliTestCase test "reboot" do SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{index .NetworkSettings.Networks.kamal.Aliases 0}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'", "|", :sed, "-e", "'s/\\/tcp$//'") - .returns("172.1.0.2:80") + .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet") + .returns("abcdefabcdef") .at_least_once SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) @@ -25,7 +25,7 @@ class CliProxyTest < CliTestCase assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE} on 1.1.1.1", output - assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"172.1.0.2:80\" --deploy-timeout \"6s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" on 1.1.1.1", output + assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:3000\" --deploy-timeout \"6s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" on 1.1.1.1", output assert_match "docker container stop kamal-proxy on 1.1.1.2", output assert_match "docker container stop traefik on 1.1.1.2", output @@ -37,8 +37,8 @@ class CliProxyTest < CliTestCase test "reboot --rolling" do SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{index .NetworkSettings.Networks.kamal.Aliases 0}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'", "|", :sed, "-e", "'s/\\/tcp$//'") - .returns("172.1.0.2:80") + .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet") + .returns("abcdefabcdef") .at_least_once SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) diff --git a/test/commands/proxy_test.rb b/test/commands/proxy_test.rb index 0a2cc6c02..71bb4f1cd 100644 --- a/test/commands/proxy_test.rb +++ b/test/commands/proxy_test.rb @@ -109,14 +109,14 @@ class CommandsProxyTest < ActiveSupport::TestCase test "deploy" do assert_equal \ - "docker exec kamal-proxy kamal-proxy deploy service --target \"172.1.0.2:80\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\"", - new_command.deploy("service", target: "172.1.0.2:80").join(" ") + "docker exec kamal-proxy kamal-proxy deploy service --target \"172.1.0.2:3000\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\"", + new_command.deploy("service", target: "172.1.0.2").join(" ") end test "remove" do assert_equal \ - "docker exec kamal-proxy kamal-proxy remove service --target \"172.1.0.2:80\"", - new_command.remove("service", target: "172.1.0.2:80").join(" ") + "docker exec kamal-proxy kamal-proxy remove service --target \"172.1.0.2:3000\"", + new_command.remove("service", target: "172.1.0.2").join(" ") end private diff --git a/test/integration/docker/deployer/app_with_roles/config/deploy.yml b/test/integration/docker/deployer/app_with_roles/config/deploy.yml index c15af55b1..2f5c6fc33 100644 --- a/test/integration/docker/deployer/app_with_roles/config/deploy.yml +++ b/test/integration/docker/deployer/app_with_roles/config/deploy.yml @@ -13,6 +13,7 @@ proxy: enabled: true hosts: - vm2 + port: 80 deploy_timeout: 2s asset_path: /usr/share/nginx/html/versions From dcd4778dd9470e34cb856c0d0509cadde71c403f Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 11 Sep 2024 14:22:37 +0100 Subject: [PATCH 17/43] Port -> app_port --- lib/kamal/commands/proxy.rb | 6 +++--- lib/kamal/configuration/docs/proxy.yml | 6 +++--- lib/kamal/configuration/proxy.rb | 4 ++-- test/cli/app_test.rb | 2 +- test/cli/proxy_test.rb | 2 +- test/commands/proxy_test.rb | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb index b50cd0f25..42fe35244 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -1,6 +1,6 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base delegate :argumentize, :optionize, to: Kamal::Utils - delegate :container_name, :port, to: :proxy_config + delegate :container_name, :app_port, to: :proxy_config attr_reader :proxy_config @@ -35,11 +35,11 @@ def start_or_run end def deploy(service, target:) - docker :exec, container_name, "kamal-proxy", :deploy, service, *optionize({ target: "#{target}:#{port}" }), *proxy_config.deploy_command_args + docker :exec, container_name, "kamal-proxy", :deploy, service, *optionize({ target: "#{target}:#{app_port}" }), *proxy_config.deploy_command_args end def remove(service, target:) - docker :exec, container_name, "kamal-proxy", :remove, service, *optionize({ target: "#{target}:#{port}" }) + docker :exec, container_name, "kamal-proxy", :remove, service, *optionize({ target: "#{target}:#{app_port}" }) end def info diff --git a/lib/kamal/configuration/docs/proxy.yml b/lib/kamal/configuration/docs/proxy.yml index c30117fbf..906d40323 100644 --- a/lib/kamal/configuration/docs/proxy.yml +++ b/lib/kamal/configuration/docs/proxy.yml @@ -56,11 +56,11 @@ proxy: # requests for other apps that do have a host set. host: foo.example.com - # Port + # App port # - # The port the application is exposed on + # The port the application container is exposed on # Defaults to 80 - port: 3000 + app_port: 3000 # SSL # diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb index db3dcd96e..32abc3826 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -25,8 +25,8 @@ def hosts end end - def port - proxy_config.fetch("port", 80) + def app_port + proxy_config.fetch("app_port", 80) end def image diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index 17ae3debd..4460390e9 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -363,7 +363,7 @@ class CliAppTest < CliTestCase assert_match /Renaming container .* to .* as already deployed on 1.1.1.1/, output # Rename assert_match /docker rename app-web-latest app-web-latest_replaced_[0-9a-f]{16}/, output assert_match /docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-[0-9a-f]{12} -e KAMAL_CONTAINER_NAME="app-web-latest" -e KAMAL_VERSION="latest" --env-file .kamal\/env\/roles\/app-web.env --log-opt max-size="10m" --label service="app" --label role="web" --label destination dhh\/app:latest/, output - assert_match /docker exec kamal-proxy kamal-proxy deploy app-web --target "123:3000"/, output + assert_match /docker exec kamal-proxy kamal-proxy deploy app-web --target "123:80"/, output assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output end end diff --git a/test/cli/proxy_test.rb b/test/cli/proxy_test.rb index 3d65207c8..b4d5aa780 100644 --- a/test/cli/proxy_test.rb +++ b/test/cli/proxy_test.rb @@ -25,7 +25,7 @@ class CliProxyTest < CliTestCase assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE} on 1.1.1.1", output - assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:3000\" --deploy-timeout \"6s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" on 1.1.1.1", output + assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" on 1.1.1.1", output assert_match "docker container stop kamal-proxy on 1.1.1.2", output assert_match "docker container stop traefik on 1.1.1.2", output diff --git a/test/commands/proxy_test.rb b/test/commands/proxy_test.rb index 71bb4f1cd..f5e82a342 100644 --- a/test/commands/proxy_test.rb +++ b/test/commands/proxy_test.rb @@ -109,13 +109,13 @@ class CommandsProxyTest < ActiveSupport::TestCase test "deploy" do assert_equal \ - "docker exec kamal-proxy kamal-proxy deploy service --target \"172.1.0.2:3000\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\"", + "docker exec kamal-proxy kamal-proxy deploy service --target \"172.1.0.2:80\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\"", new_command.deploy("service", target: "172.1.0.2").join(" ") end test "remove" do assert_equal \ - "docker exec kamal-proxy kamal-proxy remove service --target \"172.1.0.2:3000\"", + "docker exec kamal-proxy kamal-proxy remove service --target \"172.1.0.2:80\"", new_command.remove("service", target: "172.1.0.2").join(" ") end From 27a7b339a6fadda1614fd2a8c3903d3c1d54038c Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 11 Sep 2024 14:30:14 +0100 Subject: [PATCH 18/43] Drop run_directory configuration option We need to drop to be fixed so multiple applications put the config in the same place. --- lib/kamal/configuration.rb | 8 ++------ test/commands/server_test.rb | 4 ---- test/configuration_test.rb | 6 ------ 3 files changed, 2 insertions(+), 16 deletions(-) diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index 116cf2322..1b96467b4 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -204,15 +204,11 @@ def run_id def run_directory - raw_config.run_directory || ".kamal" + ".kamal" end def run_directory_as_docker_volume - if Pathname.new(run_directory).absolute? - run_directory - else - File.join "$(pwd)", run_directory - end + File.join "$(pwd)", run_directory end def hooks_path diff --git a/test/commands/server_test.rb b/test/commands/server_test.rb index 8c465fd9d..aa13fc048 100644 --- a/test/commands/server_test.rb +++ b/test/commands/server_test.rb @@ -12,10 +12,6 @@ class CommandsServerTest < ActiveSupport::TestCase assert_equal "mkdir -p .kamal", new_command.ensure_run_directory.join(" ") end - test "ensure non default run directory" do - assert_equal "mkdir -p /var/run/kamal", new_command(run_directory: "/var/run/kamal").ensure_run_directory.join(" ") - end - private def new_command(extra_config = {}) Kamal::Commands::Server.new(Kamal::Configuration.new(@config.merge(extra_config))) diff --git a/test/configuration_test.rb b/test/configuration_test.rb index aa78dda98..eaa881d98 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -294,17 +294,11 @@ class ConfigurationTest < ActiveSupport::TestCase test "run directory" do config = Kamal::Configuration.new(@deploy) assert_equal ".kamal", config.run_directory - - config = Kamal::Configuration.new(@deploy.merge!(run_directory: "/root/kamal")) - assert_equal "/root/kamal", config.run_directory end test "run directory as docker volume" do config = Kamal::Configuration.new(@deploy) assert_equal "$(pwd)/.kamal", config.run_directory_as_docker_volume - - config = Kamal::Configuration.new(@deploy.merge!(run_directory: "/root/kamal")) - assert_equal "/root/kamal", config.run_directory_as_docker_volume end test "run id" do From 5bca8015bc99b55ed2440836e256140d5d841be5 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 11 Sep 2024 14:46:56 +0100 Subject: [PATCH 19/43] Map kamal proxy config into .kamal/proxy/config This will allow us to share files with the proxy via the host. --- lib/kamal/commands/proxy.rb | 2 +- lib/kamal/configuration/proxy.rb | 7 ++++++- test/cli/proxy_test.rb | 4 ++-- test/commands/proxy_test.rb | 8 ++++---- .../docker/deployer/app_with_roles/config/deploy.yml | 1 - 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb index 42fe35244..e13bc9665 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -17,7 +17,7 @@ def run "--restart", "unless-stopped", *proxy_config.publish_args, "--volume", "/var/run/docker.sock:/var/run/docker.sock", - "--volume", "#{container_name}:/root/.config/kamal-proxy", + "--volume", "#{proxy_config.config_directory_as_docker_volume}:/root/.config/kamal-proxy", *config.logging_args, proxy_config.image end diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb index 32abc3826..7dff0bbd2 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -9,6 +9,7 @@ class Kamal::Configuration::Proxy delegate :argumentize, :optionize, to: Kamal::Utils def initialize(config:) + @config = config @proxy_config = config.raw_config.proxy || {} validate! proxy_config, with: Kamal::Configuration::Validator::Proxy end @@ -70,6 +71,10 @@ def deploy_command_args optionize deploy_options end + def config_directory_as_docker_volume + File.join config.run_directory_as_docker_volume, "proxy", "config" + end + private - attr_accessor :proxy_config + attr_reader :config, :proxy_config end diff --git a/test/cli/proxy_test.rb b/test/cli/proxy_test.rb index b4d5aa780..20c773cb2 100644 --- a/test/cli/proxy_test.rb +++ b/test/cli/proxy_test.rb @@ -4,7 +4,7 @@ class CliProxyTest < CliTestCase test "boot" do run_command("boot").tap do |output| assert_match "docker login", output - assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", output + assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", output end end @@ -24,7 +24,7 @@ class CliProxyTest < CliTestCase assert_match "docker container stop traefik on 1.1.1.1", output assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output - assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE} on 1.1.1.1", output + assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE} on 1.1.1.1", output assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" on 1.1.1.1", output assert_match "docker container stop kamal-proxy on 1.1.1.2", output diff --git a/test/commands/proxy_test.rb b/test/commands/proxy_test.rb index f5e82a342..3b7c7191e 100644 --- a/test/commands/proxy_test.rb +++ b/test/commands/proxy_test.rb @@ -15,13 +15,13 @@ class CommandsProxyTest < ActiveSupport::TestCase test "run" do assert_equal \ - "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", + "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", new_command.run.join(" ") end test "run with ports configured" do assert_equal \ - "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", + "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", new_command.run.join(" ") end @@ -29,7 +29,7 @@ class CommandsProxyTest < ActiveSupport::TestCase @config.delete(:proxy) assert_equal \ - "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", + "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", new_command.run.join(" ") end @@ -37,7 +37,7 @@ class CommandsProxyTest < ActiveSupport::TestCase @config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } } assert_equal \ - "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", + "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/root/.config/kamal-proxy --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", new_command.run.join(" ") end diff --git a/test/integration/docker/deployer/app_with_roles/config/deploy.yml b/test/integration/docker/deployer/app_with_roles/config/deploy.yml index 2f5c6fc33..c15af55b1 100644 --- a/test/integration/docker/deployer/app_with_roles/config/deploy.yml +++ b/test/integration/docker/deployer/app_with_roles/config/deploy.yml @@ -13,7 +13,6 @@ proxy: enabled: true hosts: - vm2 - port: 80 deploy_timeout: 2s asset_path: /usr/share/nginx/html/versions From f4d309c5ccad06b19f36f970ea1b623dd3fa75c4 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 12 Sep 2024 10:52:05 +0100 Subject: [PATCH 20/43] Rip out Traefik --- README.md | 2 +- bin/docs | 4 +- lib/kamal/cli/app.rb | 4 +- lib/kamal/cli/app/boot.rb | 31 +-- lib/kamal/cli/healthcheck/poller.rb | 4 +- lib/kamal/cli/main.rb | 28 +-- lib/kamal/cli/proxy.rb | 86 +++----- lib/kamal/cli/prune.rb | 1 - lib/kamal/cli/templates/deploy.yml | 11 - .../sample_hooks/post-proxy-reboot.sample | 3 + .../sample_hooks/post-traefik-reboot.sample | 3 - ...-reboot.sample => pre-proxy-reboot.sample} | 0 lib/kamal/cli/traefik.rb | 141 ------------- lib/kamal/commander.rb | 14 +- lib/kamal/commander/specifics.rb | 11 +- lib/kamal/commands/app.rb | 23 +-- lib/kamal/commands/app/cord.rb | 22 -- lib/kamal/commands/proxy.rb | 9 + lib/kamal/commands/prune.rb | 8 - lib/kamal/commands/traefik.rb | 77 ------- lib/kamal/configuration.rb | 25 +-- .../configuration/docs/configuration.yml | 14 +- lib/kamal/configuration/docs/healthcheck.yml | 59 ------ lib/kamal/configuration/docs/proxy.yml | 43 +--- lib/kamal/configuration/docs/role.yml | 6 +- lib/kamal/configuration/docs/traefik.yml | 62 ------ lib/kamal/configuration/healthcheck.rb | 63 ------ lib/kamal/configuration/proxy.rb | 18 +- lib/kamal/configuration/role.rb | 100 +-------- lib/kamal/configuration/traefik.rb | 78 ------- test/cli/app_test.rb | 73 ++----- test/cli/main_test.rb | 60 ++---- test/cli/proxy_test.rb | 27 +-- test/cli/prune_test.rb | 2 - test/cli/traefik_test.rb | 110 ---------- test/commander_test.rb | 12 +- test/commands/app_test.rb | 52 +---- test/commands/hook_test.rb | 2 +- test/commands/lock_test.rb | 2 +- test/commands/prune_test.rb | 8 +- test/commands/server_test.rb | 2 +- test/commands/traefik_test.rb | 195 ------------------ test/configuration/role_test.rb | 33 +-- test/configuration/validation_test.rb | 2 +- test/configuration_test.rb | 29 ++- .../deploy_primary_web_role_override.yml | 4 +- test/fixtures/deploy_with_extensions.yml | 2 +- ...l => deploy_with_multiple_proxy_roles.yml} | 4 +- test/fixtures/deploy_with_proxy.yml | 3 - test/fixtures/deploy_workers_only.yml | 2 +- test/integration/app_test.rb | 1 + .../app/.kamal/hooks/post-proxy-reboot | 3 + .../app/.kamal/hooks/post-traefik-reboot | 3 - .../app/.kamal/hooks/pre-proxy-reboot | 3 + .../app/.kamal/hooks/pre-traefik-reboot | 3 - .../docker/deployer/app/config/deploy.yml | 10 +- .../.kamal/hooks/post-proxy-reboot | 3 + .../.kamal/hooks/post-traefik-reboot | 3 - .../.kamal/hooks/pre-proxy-reboot | 3 + .../.kamal/hooks/pre-traefik-reboot | 3 - .../deployer/app_with_roles/config/deploy.yml | 11 - test/integration/docker/deployer/setup.sh | 1 - test/integration/integration_test.rb | 8 +- test/integration/main_test.rb | 7 +- test/integration/proxy_test.rb | 33 +-- test/integration/traefik_test.rb | 61 ------ 66 files changed, 199 insertions(+), 1531 deletions(-) create mode 100755 lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample delete mode 100755 lib/kamal/cli/templates/sample_hooks/post-traefik-reboot.sample rename lib/kamal/cli/templates/sample_hooks/{pre-traefik-reboot.sample => pre-proxy-reboot.sample} (100%) delete mode 100644 lib/kamal/cli/traefik.rb delete mode 100644 lib/kamal/commands/app/cord.rb delete mode 100644 lib/kamal/commands/traefik.rb delete mode 100644 lib/kamal/configuration/docs/healthcheck.yml delete mode 100644 lib/kamal/configuration/docs/traefik.yml delete mode 100644 lib/kamal/configuration/healthcheck.rb delete mode 100644 lib/kamal/configuration/traefik.rb delete mode 100644 test/cli/traefik_test.rb delete mode 100644 test/commands/traefik_test.rb rename test/fixtures/{deploy_with_multiple_traefik_roles.yml => deploy_with_multiple_proxy_roles.yml} (93%) create mode 100755 test/integration/docker/deployer/app/.kamal/hooks/post-proxy-reboot delete mode 100755 test/integration/docker/deployer/app/.kamal/hooks/post-traefik-reboot create mode 100755 test/integration/docker/deployer/app/.kamal/hooks/pre-proxy-reboot delete mode 100755 test/integration/docker/deployer/app/.kamal/hooks/pre-traefik-reboot create mode 100755 test/integration/docker/deployer/app_with_roles/.kamal/hooks/post-proxy-reboot delete mode 100755 test/integration/docker/deployer/app_with_roles/.kamal/hooks/post-traefik-reboot create mode 100755 test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-proxy-reboot delete mode 100755 test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-traefik-reboot delete mode 100644 test/integration/traefik_test.rb diff --git a/README.md b/README.md index 9b95066f9..924e1e278 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Kamal: Deploy web apps anywhere -From bare metal to cloud VMs, deploy web apps anywhere with zero downtime. Kamal has the dynamic reverse-proxy Traefik hold requests while a new app container is started and the old one is stopped. Works seamlessly across multiple hosts, using SSHKit to execute commands. Originally built for Rails apps, Kamal will work with any type of web app that can be containerized with Docker. +From bare metal to cloud VMs, deploy web apps anywhere with zero downtime. Kamal uses [kamal-proxy](https://github.com/basecamp/kamal-proxy) to seamlessly switch requests between containers. Works seamlessly across multiple servers, using SSHKit to execute commands. Originally built for Rails apps, Kamal will work with any type of web app that can be containerized with Docker. ➡️ See [kamal-deploy.org](https://kamal-deploy.org) for documentation on [installation](https://kamal-deploy.org/docs/installation), [configuration](https://kamal-deploy.org/docs/configuration), and [commands](https://kamal-deploy.org/docs/commands). diff --git a/bin/docs b/bin/docs index 437c5ce71..9947a04cb 100755 --- a/bin/docs +++ b/bin/docs @@ -22,15 +22,13 @@ DOCS = { "builder" => "Builders", "configuration" => "Configuration overview", "env" => "Environment variables", - "healthcheck" => "Healthchecks", "logging" => "Logging", "proxy" => "Proxy (Experimental)", "registry" => "Docker Registry", "role" => "Roles", "servers" => "Servers", "ssh" => "SSH", - "sshkit" => "SSHKit", - "traefik" => "Traefik" + "sshkit" => "SSHKit" } class DocWriter diff --git a/lib/kamal/cli/app.rb b/lib/kamal/cli/app.rb index aaa8cd506..5b0535da2 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -42,7 +42,7 @@ def start execute *KAMAL.auditor.record("Started app version #{KAMAL.config.version}"), verbosity: :debug execute *app.start, raise_on_non_zero_exit: false - if role.running_traefik? && KAMAL.proxy_host?(host) + if role.running_proxy? version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip endpoint = capture_with_info(*app.container_id_for_version(version)).strip raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, did the container boot?" if endpoint.empty? @@ -64,7 +64,7 @@ def stop app = KAMAL.app(role: role, host: host) execute *KAMAL.auditor.record("Stopped app", role: role), verbosity: :debug - if role.running_traefik? && KAMAL.proxy_host?(host) + if role.running_proxy? version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip endpoint = capture_with_info(*app.container_id_for_version(version)).strip if endpoint.present? diff --git a/lib/kamal/cli/app/boot.rb b/lib/kamal/cli/app/boot.rb index 00e0fb732..74da5a56f 100644 --- a/lib/kamal/cli/app/boot.rb +++ b/lib/kamal/cli/app/boot.rb @@ -1,7 +1,7 @@ class Kamal::Cli::App::Boot attr_reader :host, :role, :version, :barrier, :sshkit delegate :execute, :capture_with_info, :capture_with_pretty_json, :info, :error, :upload!, to: :sshkit - delegate :uses_cord?, :assets?, :running_traefik?, to: :role + delegate :assets?, :running_proxy?, to: :role def initialize(host, role, sshkit, version, barrier) @host = host @@ -50,18 +50,12 @@ def start_new_version execute *app.ensure_env_directory upload! role.secrets_io(host), role.secrets_path, mode: "0600" - if proxy_host? - execute *app.run_for_proxy(hostname: hostname) - if running_traefik? - endpoint = capture_with_info(*app.container_id_for_version(version)).strip - raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, did the container boot?" if endpoint.empty? - execute *KAMAL.proxy.deploy(role.container_prefix, target: endpoint) - else - Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) } - end + execute *app.run(hostname: hostname) + if running_proxy? + endpoint = capture_with_info(*app.container_id_for_version(version)).strip + raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, did the container boot?" if endpoint.empty? + execute *KAMAL.proxy.deploy(role.container_prefix, target: endpoint) else - execute *app.tie_cord(role.cord_host_file) if uses_cord? - execute *app.run(hostname: hostname) Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) } end end @@ -71,16 +65,7 @@ def stop_new_version end def stop_old_version(version) - if uses_cord? && !proxy_host? - cord = capture_with_info(*app.cord(version: version), raise_on_non_zero_exit: false).strip - if cord.present? - execute *app.cut_cord(cord) - Kamal::Cli::Healthcheck::Poller.wait_for_unhealthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) } - end - end - execute *app.stop(version: version), raise_on_non_zero_exit: false - execute *app.clean_up_assets if assets? end @@ -134,8 +119,4 @@ def gatekeeper? def queuer? barrier && !barrier_role? end - - def proxy_host? - KAMAL.proxy_host?(host) - end end diff --git a/lib/kamal/cli/healthcheck/poller.rb b/lib/kamal/cli/healthcheck/poller.rb index 249a1f6b5..0643c1573 100644 --- a/lib/kamal/cli/healthcheck/poller.rb +++ b/lib/kamal/cli/healthcheck/poller.rb @@ -6,7 +6,7 @@ module Kamal::Cli::Healthcheck::Poller def wait_for_healthy(pause_after_ready: false, &block) attempt = 1 - max_attempts = KAMAL.config.healthcheck.max_attempts + max_attempts = 7 begin case status = block.call @@ -33,7 +33,7 @@ def wait_for_healthy(pause_after_ready: false, &block) def wait_for_unhealthy(pause_after_ready: false, &block) attempt = 1 - max_attempts = KAMAL.config.healthcheck.max_attempts + max_attempts = 7 begin case status = block.call diff --git a/lib/kamal/cli/main.rb b/lib/kamal/cli/main.rb index fdbc25597..c0f5f1a78 100644 --- a/lib/kamal/cli/main.rb +++ b/lib/kamal/cli/main.rb @@ -35,13 +35,8 @@ def deploy with_lock do run_hook "pre-deploy", secrets: true - if KAMAL.config.proxy.enabled? - say "Ensure Traefik/kamal-proxy is running...", :magenta - invoke "kamal:cli:proxy:boot", [], invoke_options - else - say "Ensure Traefik is running...", :magenta - invoke "kamal:cli:traefik:boot", [], invoke_options - end + say "Ensure kamal-proxy is running...", :magenta + invoke "kamal:cli:proxy:boot", [], invoke_options say "Detect stale containers...", :magenta invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true) @@ -56,7 +51,7 @@ def deploy run_hook "post-deploy", secrets: true, runtime: runtime.round end - desc "redeploy", "Deploy app to servers without bootstrapping servers, starting Traefik, pruning, and registry login" + desc "redeploy", "Deploy app to servers without bootstrapping servers, starting kamal-proxy, pruning, and registry login" option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push" def redeploy runtime = print_runtime do @@ -109,11 +104,7 @@ def rollback(version) desc "details", "Show details about all containers" def details - if KAMAL.config.proxy.enabled? - invoke "kamal:cli:proxy:details" - else - invoke "kamal:cli:traefik:details" - end + invoke "kamal:cli:proxy:details" invoke "kamal:cli:app:details" invoke "kamal:cli:accessory:details", [ "all" ] end @@ -185,16 +176,12 @@ def init end end - desc "remove", "Remove Traefik, app, accessories, and registry session from servers" + desc "remove", "Remove kamal-proxy, app, accessories, and registry session from servers" option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question" def remove confirming "This will remove all containers and images. Are you sure?" do with_lock do - if KAMAL.config.proxy.enabled? - invoke "kamal:cli:proxy:remove", [], options.without(:confirmed) - else - invoke "kamal:cli:traefik:remove", [], options.without(:confirmed) - end + invoke "kamal:cli:proxy:remove", [], options.without(:confirmed) invoke "kamal:cli:app:remove", [], options.without(:confirmed) invoke "kamal:cli:accessory:remove", [ "all" ], options invoke "kamal:cli:registry:logout", [], options.without(:confirmed).merge(skip_local: true) @@ -234,9 +221,6 @@ def version desc "server", "Bootstrap servers with curl and Docker" subcommand "server", Kamal::Cli::Server - desc "traefik", "Manage Traefik load balancer" - subcommand "traefik", Kamal::Cli::Traefik - private def container_available?(version) begin diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index b0bb33a4c..94d219aab 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -1,7 +1,6 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base desc "boot", "Boot proxy on servers" def boot - raise_unless_kamal_proxy_enabled! with_lock do on(KAMAL.hosts) do |host| execute *KAMAL.docker.create_network @@ -9,15 +8,9 @@ def boot raise unless e.message.include?("already exists") end - on(KAMAL.traefik_hosts) do |host| + on(KAMAL.proxy_hosts) do |host| execute *KAMAL.registry.login - if KAMAL.proxy_host?(host) - execute *KAMAL.proxy.start_or_run - else - execute *KAMAL.traefik.ensure_env_directory - upload! KAMAL.traefik.secrets_io, KAMAL.traefik.secrets_path, mode: "0600" - execute *KAMAL.traefik.start_or_run - end + execute *KAMAL.proxy.start_or_run end end end @@ -26,42 +19,38 @@ def boot option :rolling, type: :boolean, default: false, desc: "Reboot proxy on hosts in sequence, rather than in parallel" option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question" def reboot - raise_unless_kamal_proxy_enabled! confirming "This will cause a brief outage on each host. Are you sure?" do with_lock do - host_groups = options[:rolling] ? KAMAL.traefik_hosts : [ KAMAL.traefik_hosts ] + host_groups = options[:rolling] ? KAMAL.proxy_hosts : [ KAMAL.proxy_hosts ] host_groups.each do |hosts| host_list = Array(hosts).join(",") - run_hook "pre-traefik-reboot", hosts: host_list + run_hook "pre-proxy-reboot", hosts: host_list on(hosts) do |host| execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug execute *KAMAL.registry.login "Stopping and removing Traefik on #{host}, if running..." - execute *KAMAL.traefik.stop, raise_on_non_zero_exit: false - execute *KAMAL.traefik.remove_container + execute *KAMAL.proxy.cleanup_traefik "Stopping and removing kamal-proxy on #{host}, if running..." execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false execute *KAMAL.proxy.remove_container - execute *KAMAL.traefik_or_proxy(host).run + execute *KAMAL.proxy.run - if KAMAL.proxy_host?(host) - KAMAL.roles_on(host).select(&:running_traefik?).each do |role| - app = KAMAL.app(role: role, host: host) + KAMAL.roles_on(host).select(&:running_proxy?).each do |role| + app = KAMAL.app(role: role, host: host) - version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip - endpoint = capture_with_info(*app.container_id_for_version(version)).strip + version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip + endpoint = capture_with_info(*app.container_id_for_version(version)).strip - if endpoint.present? - info "Deploying #{endpoint} for role `#{role}` on #{host}..." - execute *KAMAL.proxy.deploy(role.container_prefix, target: endpoint) - end + if endpoint.present? + info "Deploying #{endpoint} for role `#{role}` on #{host}..." + execute *KAMAL.proxy.deploy(role.container_prefix, target: endpoint) end end end - run_hook "post-traefik-reboot", hosts: host_list + run_hook "post-proxy-reboot", hosts: host_list end end end @@ -73,19 +62,18 @@ def reboot def upgrade invoke_options = { "version" => KAMAL.config.version }.merge(options) - raise_unless_kamal_proxy_enabled! confirming "This will cause a brief outage on each host. Are you sure?" do host_groups = options[:rolling] ? KAMAL.hosts : [ KAMAL.hosts ] host_groups.each do |hosts| host_list = Array(hosts).join(",") - run_hook "pre-traefik-reboot", hosts: host_list + run_hook "pre-proxy-reboot", hosts: host_list on(hosts) do |host| execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug execute *KAMAL.registry.login "Stopping and removing Traefik on #{host}, if running..." - execute *KAMAL.traefik.stop, raise_on_non_zero_exit: false - execute *KAMAL.traefik.remove_container + execute *KAMAL.proxy.stop_traefik, raise_on_non_zero_exit: false + execute *KAMAL.proxy.cleanup_traefik "Stopping and removing kamal-proxy on #{host}, if running..." execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false @@ -99,36 +87,33 @@ def upgrade invoke "kamal:cli:prune:all", [], invoke_options.merge("hosts" => host_list) reset_invocation(Kamal::Cli::Prune) - run_hook "post-traefik-reboot", hosts: host_list + run_hook "post-proxy-reboot", hosts: host_list end end end desc "start", "Start existing proxy container on servers" def start - raise_unless_kamal_proxy_enabled! with_lock do - on(KAMAL.traefik_hosts) do |host| + on(KAMAL.proxy_hosts) do |host| execute *KAMAL.auditor.record("Started proxy"), verbosity: :debug - execute *KAMAL.traefik_or_proxy(host).start + execute *KAMAL.proxy.start end end end desc "stop", "Stop existing proxy container on servers" def stop - raise_unless_kamal_proxy_enabled! with_lock do - on(KAMAL.traefik_hosts) do |host| + on(KAMAL.proxy_hosts) do |host| execute *KAMAL.auditor.record("Stopped proxy"), verbosity: :debug - execute *KAMAL.traefik_or_proxy(host).stop, raise_on_non_zero_exit: false + execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false end end end desc "restart", "Restart existing proxy container on servers" def restart - raise_unless_kamal_proxy_enabled! with_lock do stop start @@ -137,8 +122,7 @@ def restart desc "details", "Show details about proxy container from servers" def details - raise_unless_kamal_proxy_enabled! - on(KAMAL.traefik_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.traefik_or_proxy(host).info), type: "Proxy" } + on(KAMAL.proxy_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.proxy.info), type: "Proxy" } end desc "logs", "Show log lines from proxy on servers" @@ -147,28 +131,26 @@ def details option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)" option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)" def logs - raise_unless_kamal_proxy_enabled! grep = options[:grep] if options[:follow] run_locally do info "Following logs on #{KAMAL.primary_host}..." - info KAMAL.traefik_or_proxy(KAMAL.primary_host).follow_logs(host: KAMAL.primary_host, grep: grep) - exec KAMAL.traefik_or_proxy(KAMAL.primary_host).follow_logs(host: KAMAL.primary_host, grep: grep) + info KAMAL.proxy.follow_logs(host: KAMAL.primary_host, grep: grep) + exec KAMAL.proxy.follow_logs(host: KAMAL.primary_host, grep: grep) end else since = options[:since] lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set - on(KAMAL.traefik_hosts) do |host| - puts_by_host host, capture(*KAMAL.traefik_or_proxy(host).logs(since: since, lines: lines, grep: grep)), type: "Proxy" + on(KAMAL.proxy_hosts) do |host| + puts_by_host host, capture(*KAMAL.proxy.logs(since: since, lines: lines, grep: grep)), type: "Proxy" end end end desc "remove", "Remove proxy container and image from servers" def remove - raise_unless_kamal_proxy_enabled! with_lock do stop remove_container @@ -178,35 +160,25 @@ def remove desc "remove_container", "Remove proxy container from servers", hide: true def remove_container - raise_unless_kamal_proxy_enabled! with_lock do - on(KAMAL.traefik_hosts) do + on(KAMAL.proxy_hosts) do execute *KAMAL.auditor.record("Removed proxy container"), verbosity: :debug execute *KAMAL.proxy.remove_container - execute *KAMAL.traefik.remove_container end end end desc "remove_image", "Remove proxy image from servers", hide: true def remove_image - raise_unless_kamal_proxy_enabled! with_lock do - on(KAMAL.traefik_hosts) do + on(KAMAL.proxy_hosts) do execute *KAMAL.auditor.record("Removed proxy image"), verbosity: :debug execute *KAMAL.proxy.remove_image - execute *KAMAL.traefik.remove_image end end end private - def raise_unless_kamal_proxy_enabled! - unless KAMAL.config.proxy.enabled? - raise "kamal proxy commands are disabled unless experimental proxy support is enabled. Use `kamal traefik` commands instead." - end - end - def reset_invocation(cli_class) instance_variable_get("@_invocations")[cli_class].pop end diff --git a/lib/kamal/cli/prune.rb b/lib/kamal/cli/prune.rb index 7635e97da..aaf799803 100644 --- a/lib/kamal/cli/prune.rb +++ b/lib/kamal/cli/prune.rb @@ -28,7 +28,6 @@ def containers on(KAMAL.hosts) do execute *KAMAL.auditor.record("Pruned containers"), verbosity: :debug execute *KAMAL.prune.app_containers(retain: retain) - execute *KAMAL.prune.healthcheck_containers end end end diff --git a/lib/kamal/cli/templates/deploy.yml b/lib/kamal/cli/templates/deploy.yml index 119961b9d..8ecf5d044 100644 --- a/lib/kamal/cli/templates/deploy.yml +++ b/lib/kamal/cli/templates/deploy.yml @@ -57,17 +57,6 @@ builder: # directories: # - data:/data -# Configure custom arguments for Traefik. Be sure to reboot traefik when you modify it. -# traefik: -# args: -# accesslog: true -# accesslog.format: json - -# Configure a custom healthcheck (default is /up on port 3000) -# healthcheck: -# path: /healthz -# port: 4000 - # Bridge fingerprinted assets, like JS and CSS, between versions to avoid # hitting 404 on in-flight requests. Combines all files from new and old # version inside the asset_path. diff --git a/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample b/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample new file mode 100755 index 000000000..1435a677f --- /dev/null +++ b/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooted kamal-proxy on $KAMAL_HOSTS" diff --git a/lib/kamal/cli/templates/sample_hooks/post-traefik-reboot.sample b/lib/kamal/cli/templates/sample_hooks/post-traefik-reboot.sample deleted file mode 100755 index e3d9e3ccb..000000000 --- a/lib/kamal/cli/templates/sample_hooks/post-traefik-reboot.sample +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -echo "Rebooted Traefik on $KAMAL_HOSTS" diff --git a/lib/kamal/cli/templates/sample_hooks/pre-traefik-reboot.sample b/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample similarity index 100% rename from lib/kamal/cli/templates/sample_hooks/pre-traefik-reboot.sample rename to lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample diff --git a/lib/kamal/cli/traefik.rb b/lib/kamal/cli/traefik.rb deleted file mode 100644 index 0dc25955b..000000000 --- a/lib/kamal/cli/traefik.rb +++ /dev/null @@ -1,141 +0,0 @@ -class Kamal::Cli::Traefik < Kamal::Cli::Base - desc "boot", "Boot Traefik on servers" - def boot - raise_if_kamal_proxy_enabled! - with_lock do - on(KAMAL.traefik_hosts) do - execute *KAMAL.registry.login - execute *KAMAL.traefik.ensure_env_directory - upload! KAMAL.traefik.secrets_io, KAMAL.traefik.secrets_path, mode: "0600" - execute *KAMAL.traefik.start_or_run - end - end - end - - desc "reboot", "Reboot Traefik on servers (stop container, remove container, start new container)" - option :rolling, type: :boolean, default: false, desc: "Reboot traefik on hosts in sequence, rather than in parallel" - option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question" - def reboot - raise_if_kamal_proxy_enabled! - confirming "This will cause a brief outage on each host. Are you sure?" do - with_lock do - host_groups = options[:rolling] ? KAMAL.traefik_hosts : [ KAMAL.traefik_hosts ] - host_groups.each do |hosts| - host_list = Array(hosts).join(",") - run_hook "pre-traefik-reboot", hosts: host_list - on(hosts) do - execute *KAMAL.auditor.record("Rebooted traefik"), verbosity: :debug - execute *KAMAL.registry.login - execute *KAMAL.traefik.stop, raise_on_non_zero_exit: false - execute *KAMAL.traefik.remove_container - execute *KAMAL.traefik.run - end - run_hook "post-traefik-reboot", hosts: host_list - end - end - end - end - - desc "start", "Start existing Traefik container on servers" - def start - raise_if_kamal_proxy_enabled! - with_lock do - on(KAMAL.traefik_hosts) do - execute *KAMAL.auditor.record("Started traefik"), verbosity: :debug - execute *KAMAL.traefik.start - end - end - end - - desc "stop", "Stop existing Traefik container on servers" - def stop - raise_if_kamal_proxy_enabled! - with_lock do - on(KAMAL.traefik_hosts) do - execute *KAMAL.auditor.record("Stopped traefik"), verbosity: :debug - execute *KAMAL.traefik.stop, raise_on_non_zero_exit: false - end - end - end - - desc "restart", "Restart existing Traefik container on servers" - def restart - raise_if_kamal_proxy_enabled! - with_lock do - stop - start - end - end - - desc "details", "Show details about Traefik container from servers" - def details - raise_if_kamal_proxy_enabled! - on(KAMAL.traefik_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.traefik.info), type: "Traefik" } - end - - desc "logs", "Show log lines from Traefik on servers" - option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)" - option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server" - option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)" - option :grep_options, aliases: "-o", desc: "Additional options supplied to grep" - option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)" - def logs - raise_if_kamal_proxy_enabled! - grep = options[:grep] - grep_options = options[:grep_options] - - if options[:follow] - run_locally do - info "Following logs on #{KAMAL.primary_host}..." - info KAMAL.traefik.follow_logs(host: KAMAL.primary_host, grep: grep, grep_options: grep_options) - exec KAMAL.traefik.follow_logs(host: KAMAL.primary_host, grep: grep, grep_options: grep_options) - end - else - since = options[:since] - lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set - - on(KAMAL.traefik_hosts) do |host| - puts_by_host host, capture(*KAMAL.traefik.logs(since: since, lines: lines, grep: grep, grep_options: grep_options)), type: "Traefik" - end - end - end - - desc "remove", "Remove Traefik container and image from servers" - def remove - raise_if_kamal_proxy_enabled! - with_lock do - stop - remove_container - remove_image - end - end - - desc "remove_container", "Remove Traefik container from servers", hide: true - def remove_container - raise_if_kamal_proxy_enabled! - with_lock do - on(KAMAL.traefik_hosts) do - execute *KAMAL.auditor.record("Removed traefik container"), verbosity: :debug - execute *KAMAL.traefik.remove_container - end - end - end - - desc "remove_image", "Remove Traefik image from servers", hide: true - def remove_image - raise_if_kamal_proxy_enabled! - with_lock do - on(KAMAL.traefik_hosts) do - execute *KAMAL.auditor.record("Removed traefik image"), verbosity: :debug - execute *KAMAL.traefik.remove_image - end - end - end - - private - def raise_if_kamal_proxy_enabled! - if KAMAL.config.proxy.enabled? - raise "kamal traefik commands are disabled when experimental proxy support is enabled. Use `kamal proxy` commands instead." - end - end -end diff --git a/lib/kamal/commander.rb b/lib/kamal/commander.rb index 994debb54..c07e89331 100644 --- a/lib/kamal/commander.rb +++ b/lib/kamal/commander.rb @@ -4,7 +4,7 @@ class Kamal::Commander attr_accessor :verbosity, :holding_lock, :connected - delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :traefik_hosts, :proxy_hosts, :proxy_host?, :accessory_hosts, to: :specifics + delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :proxy_hosts, :accessory_hosts, to: :specifics def initialize self.verbosity = :info @@ -94,10 +94,6 @@ def docker @docker ||= Kamal::Commands::Docker.new(config) end - def healthcheck - @healthcheck ||= Kamal::Commands::Healthcheck.new(config) - end - def hook @hook ||= Kamal::Commands::Hook.new(config) end @@ -122,19 +118,11 @@ def server @server ||= Kamal::Commands::Server.new(config) end - def traefik - @traefik ||= Kamal::Commands::Traefik.new(config) - end - def alias(name) config.aliases[name] end - def traefik_or_proxy(host) - proxy_host?(host) ? proxy : traefik - end - def with_verbosity(level) old_level = self.verbosity diff --git a/lib/kamal/commander/specifics.rb b/lib/kamal/commander/specifics.rb index 12a710d8e..288fd9b55 100644 --- a/lib/kamal/commander/specifics.rb +++ b/lib/kamal/commander/specifics.rb @@ -18,17 +18,8 @@ def roles_on(host) roles.select { |role| role.hosts.include?(host.to_s) } end - def traefik_hosts - config.traefik_hosts & specified_hosts - end - def proxy_hosts - traefik_hosts & config.proxy_hosts - end - - def proxy_host?(host) - host = host.hostname if host.is_a?(SSHKit::Host) - proxy_hosts.include?(host) + config.proxy_hosts & specified_hosts end def accessory_hosts diff --git a/lib/kamal/commands/app.rb b/lib/kamal/commands/app.rb index 94ef29e98..417ad4637 100644 --- a/lib/kamal/commands/app.rb +++ b/lib/kamal/commands/app.rb @@ -1,5 +1,5 @@ class Kamal::Commands::App < Kamal::Commands::Base - include Assets, Containers, Cord, Execution, Images, Logging + include Assets, Containers, Execution, Images, Logging ACTIVE_DOCKER_STATUSES = [ :running, :restarting ] @@ -14,25 +14,6 @@ def initialize(config, role: nil, host: nil) end def run(hostname: nil) - docker :run, - "--detach", - "--restart unless-stopped", - "--name", container_name, - *([ "--hostname", hostname ] if hostname), - "-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"", - "-e", "KAMAL_VERSION=\"#{config.version}\"", - *role.env_args(host), - *role.health_check_args, - *role.logging_args, - *config.volume_args, - *role.asset_volume_args, - *role.label_args, - *role.option_args, - config.absolute_image, - role.cmd - end - - def run_for_proxy(hostname: nil) docker :run, "--detach", "--restart unless-stopped", @@ -45,7 +26,7 @@ def run_for_proxy(hostname: nil) *role.logging_args, *config.volume_args, *role.asset_volume_args, - *role.label_args_for_proxy, + *role.label_args, *role.option_args, config.absolute_image, role.cmd diff --git a/lib/kamal/commands/app/cord.rb b/lib/kamal/commands/app/cord.rb deleted file mode 100644 index 4912992e1..000000000 --- a/lib/kamal/commands/app/cord.rb +++ /dev/null @@ -1,22 +0,0 @@ -module Kamal::Commands::App::Cord - def cord(version:) - pipe \ - docker(:inspect, "-f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}'", container_name(version)), - [ :awk, "'$2 == \"#{role.cord_volume.container_path}\" {print $1}'" ] - end - - def tie_cord(cord) - create_empty_file(cord) - end - - def cut_cord(cord) - remove_directory(cord) - end - - private - def create_empty_file(file) - chain \ - make_directory_for(file), - [ :touch, file ] - end -end diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb index e13bc9665..8a9535f31 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -66,4 +66,13 @@ def remove_container def remove_image docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=kamal-proxy" end + + def cleanup_traefik + chain \ + docker(:container, :stop, "traefik"), + combine( + docker(:container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=Traefik"), + docker(:image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=Traefik") + ) + end end diff --git a/lib/kamal/commands/prune.rb b/lib/kamal/commands/prune.rb index b820b5af7..30b6eafd3 100644 --- a/lib/kamal/commands/prune.rb +++ b/lib/kamal/commands/prune.rb @@ -20,10 +20,6 @@ def app_containers(retain:) "while read container_id; do docker rm $container_id; done" end - def healthcheck_containers - docker :container, :prune, "--force", *healthcheck_service_filter - end - private def stopped_containers_filters [ "created", "exited", "dead" ].flat_map { |status| [ "--filter", "status=#{status}" ] } @@ -39,8 +35,4 @@ def active_image_list def service_filter [ "--filter", "label=service=#{config.service}" ] end - - def healthcheck_service_filter - [ "--filter", "label=service=#{config.healthcheck_service}" ] - end end diff --git a/lib/kamal/commands/traefik.rb b/lib/kamal/commands/traefik.rb deleted file mode 100644 index 964ef3eb9..000000000 --- a/lib/kamal/commands/traefik.rb +++ /dev/null @@ -1,77 +0,0 @@ -class Kamal::Commands::Traefik < Kamal::Commands::Base - delegate :argumentize, :optionize, to: Kamal::Utils - delegate :port, :publish?, :labels, :env, :image, :options, :args, :env_args, :secrets_io, :env_directory, :secrets_path, to: :"config.traefik" - - def run - docker :run, "--name traefik", - "--detach", - "--restart", "unless-stopped", - *publish_args, - "--volume", "/var/run/docker.sock:/var/run/docker.sock", - *env_args, - *config.logging_args, - *label_args, - *docker_options_args, - image, - "--providers.docker", - *cmd_option_args - end - - def start - docker :container, :start, "traefik" - end - - def stop - docker :container, :stop, "traefik" - end - - def start_or_run - any start, run - end - - def info - docker :ps, "--filter", "name=^traefik$" - end - - def logs(since: nil, lines: nil, grep: nil, grep_options: nil) - pipe \ - docker(:logs, "traefik", (" --since #{since}" if since), (" --tail #{lines}" if lines), "--timestamps", "2>&1"), - ("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep) - end - - def follow_logs(host:, grep: nil, grep_options: nil) - run_over_ssh pipe( - docker(:logs, "traefik", "--timestamps", "--tail", "10", "--follow", "2>&1"), - (%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep) - ).join(" "), host: host - end - - def remove_container - docker :container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=Traefik" - end - - def remove_image - docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=Traefik" - end - - def ensure_env_directory - make_directory env_directory - end - - private - def publish_args - argumentize "--publish", port if publish? - end - - def label_args - argumentize "--label", labels - end - - def docker_options_args - optionize(options) - end - - def cmd_option_args - optionize args, with: "=" - end -end diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index 1b96467b4..38dc21905 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -10,7 +10,7 @@ class Kamal::Configuration delegate :argumentize, :optionize, to: Kamal::Utils attr_reader :destination, :raw_config, :secrets - attr_reader :accessories, :aliases, :boot, :builder, :env, :healthcheck, :logging, :proxy, :traefik, :servers, :ssh, :sshkit, :registry + attr_reader :accessories, :aliases, :boot, :builder, :env, :logging, :proxy, :servers, :ssh, :sshkit, :registry include Validation @@ -58,10 +58,8 @@ def initialize(raw_config, destination: nil, version: nil, validate: true) @builder = Builder.new(config: self) @env = Env.new(config: @raw_config.env || {}, secrets: secrets) - @healthcheck = Healthcheck.new(healthcheck_config: @raw_config.healthcheck) @logging = Logging.new(logging_config: @raw_config.logging) @proxy = Proxy.new(config: self) - @traefik = Traefik.new(config: self) @ssh = Ssh.new(config: self) @sshkit = Sshkit.new(config: self) @@ -132,20 +130,16 @@ def allow_empty_roles? raw_config.allow_empty_roles end - def traefik_roles - roles.select(&:running_traefik?) + def proxy_roles + roles.select(&:running_proxy?) end - def traefik_role_names - traefik_roles.flat_map(&:name) - end - - def traefik_hosts - traefik_roles.flat_map(&:hosts).uniq + def proxy_role_names + proxy_roles.flat_map(&:name) end def proxy_hosts - proxy.hosts + proxy_roles.flat_map(&:hosts).uniq end def repository @@ -190,10 +184,6 @@ def logging_args end - def healthcheck_service - [ "healthcheck", service, destination ].compact.join("-") - end - def readiness_delay raw_config.readiness_delay || 7 end @@ -251,8 +241,7 @@ def to_h sshkit: sshkit.to_h, builder: builder.to_h, accessories: raw_config.accessories, - logging: logging_args, - healthcheck: healthcheck.to_h + logging: logging_args }.compact end diff --git a/lib/kamal/configuration/docs/configuration.yml b/lib/kamal/configuration/docs/configuration.yml index 8ebaaa0d2..44910fda9 100644 --- a/lib/kamal/configuration/docs/configuration.yml +++ b/lib/kamal/configuration/docs/configuration.yml @@ -137,15 +137,9 @@ builder: accessories: ... -# Traefik -# -# The Traefik proxy is used for zero-downtime deployments, see kamal docs traefik -traefik: - ... - # Proxy # -# **Experimental** Configuration for kamal-proxy the replacement for Traefik, see kamal docs proxy +# Configuration for kamal-proxy, see kamal docs proxy proxy: ... @@ -161,12 +155,6 @@ sshkit: boot: ... -# Healthcheck -# -# Configuring healthcheck commands, intervals and timeouts, see kamal docs healthcheck -healthcheck: - ... - # Logging # # Docker logging configuration, see kamal docs logging diff --git a/lib/kamal/configuration/docs/healthcheck.yml b/lib/kamal/configuration/docs/healthcheck.yml deleted file mode 100644 index 3c55d5ddf..000000000 --- a/lib/kamal/configuration/docs/healthcheck.yml +++ /dev/null @@ -1,59 +0,0 @@ -# Healthcheck configuration -# -# On roles that are running Traefik, Kamal will supply a default healthcheck to `docker run`. -# For other roles, by default no healthcheck is supplied. -# -# If no healthcheck is supplied and the image does not define one, then we wait for the container -# to reach a running state and then pause for the readiness delay. -# -# The default healthcheck is `curl -f http://localhost:/`, so it assumes that `curl` -# is available within the container. - -# Healthcheck options -# -# These go under the `healthcheck` key in the root or role configuration. -healthcheck: - - # Command - # - # The command to run, defaults to `curl -f http://localhost:/` on roles running Traefik - cmd: "curl -f http://localhost" - - # Interval - # - # The Docker healthcheck interval, defaults to `1s` - interval: 10s - - # Max attempts - # - # The maximum number of times we poll the container to see if it is healthy, defaults to `7` - # Each check is separated by an increasing interval starting with 1 second. - max_attempts: 3 - - # Port - # - # The port to use in the healthcheck, defaults to `3000` - port: "80" - - # Path - # - # The path to use in the healthcheck, defaults to `/up` - path: /health - - # Cords for zero-downtime deployments - # - # The cord file is used for zero-downtime deployments. The healthcheck is augmented with a check - # for the existance of the file. This allows us to delete the file and force the container to - # become unhealthy, causing Traefik to stop routing traffic to it. - # - # Kamal mounts a volume at this location and creates the file before starting the container. - # You can set the value to `false` to disable the cord file, but this loses the zero-downtime - # guarantee. - # - # The default value is `/tmp/kamal-cord` - cord: /cord - - # Log lines - # - # Number of lines to log from the container when the healthcheck fails, defaults to `50` - log_lines: 100 diff --git a/lib/kamal/configuration/docs/proxy.yml b/lib/kamal/configuration/docs/proxy.yml index 906d40323..82d009780 100644 --- a/lib/kamal/configuration/docs/proxy.yml +++ b/lib/kamal/configuration/docs/proxy.yml @@ -1,51 +1,12 @@ # Proxy # -# **Experimental** [kamal-proxy](http://github.com/basecamp/kamal-proxy) is a -# custom built specifically for Kamal. It will replace Traefik in Kamal v2.0, -# but currently is available as an experimental feature. -# -# When this is enabled, the proxy will be started on the hosts listed under the hosts key. -# In addition, the kamal traefik command will be disabled and replaced by kamal proxy. -# -# The kamal proxy command works identically to kamal traefik on hosts that have not -# been included. It will also handle switching between Traefik and kamal-proxy when you -# run kamal proxy reboot. - -# Limitations -# -# Currently the proxy will run on ports 80 and 443 and will bind to those -# ports on the host. -# -# There is no way to set custom options for `docker run` when booting the proxy. -# -# If you have custom Traefik configuration via labels or boot arguments they may -# not have an equivalent in kamal-proxy. - -# Proxy settings -# -# The proxy is configured in the root configuration under `traefik`. These are +# The proxy is configured in the root configuration under `proxy`. These are # options that are set when deploying the application, not when booting the proxy # # They are application specific, so are not shared when multiple applications -# with the same proxy. +# run on the same proxy. proxy: - # Enabled - # - # Whether to enable experimental proxy support. Defaults to false - enabled: true - - # Hosts - # - # The hosts to run the proxy on, instead of Traefik - # This is a temporary setting and will be removed when we full switch to kamal-proxy - # - # If you run `kamal traefik reboot`, then the proxy will be started on these hosts - # in place of traefik. - hosts: - - 10.0.0.1 - - 10.0.0.2 - # Host # # This is the host that will be used to serve the app. By setting this you can run diff --git a/lib/kamal/configuration/docs/role.yml b/lib/kamal/configuration/docs/role.yml index 9f6962a58..8ed6e46c7 100644 --- a/lib/kamal/configuration/docs/role.yml +++ b/lib/kamal/configuration/docs/role.yml @@ -26,7 +26,7 @@ servers: # # When there are other options to set, the list of hosts goes under the `hosts` key # - # By default only the primary role uses Traefik, but you can set `traefik` to change + # By default only the primary role uses a proxy, but you can set `proxy` to change # it. # # You can also set a custom cmd to run in the container, and overwrite other settings @@ -35,13 +35,11 @@ servers: hosts: - 172.1.0.3 - 172.1.0.4: experiment1 - traefik: true + proxy: true cmd: "bin/jobs" options: memory: 2g cpus: 4 - healthcheck: - ... logging: ... labels: diff --git a/lib/kamal/configuration/docs/traefik.yml b/lib/kamal/configuration/docs/traefik.yml deleted file mode 100644 index 756afa9e2..000000000 --- a/lib/kamal/configuration/docs/traefik.yml +++ /dev/null @@ -1,62 +0,0 @@ -# Traefik -# -# Traefik is a reverse proxy, used by Kamal for zero-downtime deployments. -# -# We start an instance on the hosts in it's own container. -# -# During a deployment: -# 1. We start a new container which Traefik automatically detects due to the labels we have applied -# 2. Traefik starts routing traffic to the new container -# 3. We force the old container to fail it's healthcheck, causing Traefik to stop routing traffic to it -# 4. We stop the old container - -# Traefik settings -# -# Traekik is configured in the root configuration under `traefik`. -traefik: - - # Image - # - # The Traefik image to use, defaults to `traefik:v2.10` - image: traefik:v2.9 - - # Host port - # - # The host port to publish the Traefik container on, defaults to `80` - host_port: "8080" - - # Disabling publishing - # - # To avoid publishing the Traefik container, set this to `false` - publish: false - - # Labels - # - # Additional labels to apply to the Traefik container - labels: - 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" - - # Arguments - # - # Additional arguments to pass to the Traefik container - args: - entryPoints.http.address: ":80" - entryPoints.http.forwardedHeaders.insecure: true - accesslog: true - accesslog.format: json - - # Options - # - # Additional options to pass to `docker run` - options: - cpus: 2 - - # Environment variables - # - # See kamal docs env - env: - ... diff --git a/lib/kamal/configuration/healthcheck.rb b/lib/kamal/configuration/healthcheck.rb deleted file mode 100644 index 888068a44..000000000 --- a/lib/kamal/configuration/healthcheck.rb +++ /dev/null @@ -1,63 +0,0 @@ -class Kamal::Configuration::Healthcheck - include Kamal::Configuration::Validation - - attr_reader :healthcheck_config - - def initialize(healthcheck_config:, context: "healthcheck") - @healthcheck_config = healthcheck_config || {} - validate! @healthcheck_config, context: context - end - - def merge(other) - self.class.new healthcheck_config: healthcheck_config.deep_merge(other.healthcheck_config) - end - - def cmd - healthcheck_config.fetch("cmd", http_health_check) - end - - def port - healthcheck_config.fetch("port", 3000) - end - - def path - healthcheck_config.fetch("path", "/up") - end - - def max_attempts - healthcheck_config.fetch("max_attempts", 7) - end - - def interval - healthcheck_config.fetch("interval", "1s") - end - - def cord - healthcheck_config.fetch("cord", "/tmp/kamal-cord") - end - - def log_lines - healthcheck_config.fetch("log_lines", 50) - end - - def set_port_or_path? - healthcheck_config["port"].present? || healthcheck_config["path"].present? - end - - def to_h - { - "cmd" => cmd, - "interval" => interval, - "max_attempts" => max_attempts, - "port" => port, - "path" => path, - "cord" => cord, - "log_lines" => log_lines - } - end - - private - def http_health_check - "curl -f #{URI.join("http://localhost:#{port}", path)} || exit 1" if path.present? || port.present? - end -end diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb index 7dff0bbd2..4232e8506 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -14,18 +14,6 @@ def initialize(config:) validate! proxy_config, with: Kamal::Configuration::Validator::Proxy end - def enabled? - !!proxy_config.fetch("enabled", false) - end - - def hosts - if enabled? - proxy_config.fetch("hosts", []) - else - [] - end - end - def app_port proxy_config.fetch("app_port", 80) end @@ -52,9 +40,9 @@ def deploy_options tls: proxy_config["ssl"], "deploy-timeout": proxy_config["deploy_timeout"], "drain-timeout": proxy_config["drain_timeout"], - "health-check-interval": proxy_config.dig("health_check", "interval"), - "health-check-timeout": proxy_config.dig("health_check", "timeout"), - "health-check-path": proxy_config.dig("health_check", "path"), + "health-check-interval": proxy_config.dig("healthcheck", "interval"), + "health-check-timeout": proxy_config.dig("healthcheck", "timeout"), + "health-check-path": proxy_config.dig("healthcheck", "path"), "target-timeout": proxy_config["response_timeout"], "buffer-requests": proxy_config.fetch("buffering", { "requests": true }).fetch("requests", true), "buffer-responses": proxy_config.fetch("buffering", { "responses": true }).fetch("responses", true), diff --git a/lib/kamal/configuration/role.rb b/lib/kamal/configuration/role.rb index 6579b9d07..7e5768ec2 100644 --- a/lib/kamal/configuration/role.rb +++ b/lib/kamal/configuration/role.rb @@ -1,10 +1,9 @@ class Kamal::Configuration::Role include Kamal::Configuration::Validation - CORD_FILE = "cord" delegate :argumentize, :optionize, to: Kamal::Utils - attr_reader :name, :config, :specialized_env, :specialized_logging, :specialized_healthcheck + attr_reader :name, :config, :specialized_env, :specialized_logging alias to_s name @@ -24,10 +23,6 @@ def initialize(name, config:) @specialized_logging = Kamal::Configuration::Logging.new \ logging_config: specializations.fetch("logging", {}), context: "servers/#{name}/logging" - - @specialized_healthcheck = Kamal::Configuration::Healthcheck.new \ - healthcheck_config: specializations.fetch("healthcheck", {}), - context: "servers/#{name}/healthcheck" end def primary_host @@ -55,10 +50,6 @@ def option_args end def labels - default_labels.merge(traefik_labels).merge(custom_labels) - end - - def labels_for_proxy default_labels.merge(custom_labels) end @@ -66,10 +57,6 @@ def label_args argumentize "--label", labels end - def label_args_for_proxy - argumentize "--label", labels_for_proxy - end - def logging_args logging.args end @@ -105,38 +92,11 @@ def asset_volume_args end - def health_check_args(cord: true) - if running_traefik? || healthcheck.set_port_or_path? - if cord && uses_cord? - optionize({ "health-cmd" => health_check_cmd_with_cord, "health-interval" => healthcheck.interval }) - .concat(cord_volume.docker_args) - else - optionize({ "health-cmd" => healthcheck.cmd, "health-interval" => healthcheck.interval }) - end - else - [] - end - end - - def healthcheck - @healthcheck ||= - if running_traefik? - config.healthcheck.merge(specialized_healthcheck) - else - specialized_healthcheck - end - end - - def health_check_cmd_with_cord - "(#{healthcheck.cmd}) && (stat #{cord_container_file} > /dev/null || exit 1)" - end - - - def running_traefik? - if specializations["traefik"].nil? + def running_proxy? + if specializations["proxy"].nil? primary? else - specializations["traefik"] + specializations["proxy"] end end @@ -145,35 +105,6 @@ def primary? end - def uses_cord? - running_traefik? && cord_volume && healthcheck.cmd.present? - end - - def cord_host_directory - File.join config.run_directory_as_docker_volume, "cords", [ container_prefix, config.run_id ].join("-") - end - - def cord_volume - if (cord = healthcheck.cord) - @cord_volume ||= Kamal::Configuration::Volume.new \ - host_path: File.join(config.run_directory, "cords", [ container_prefix, config.run_id ].join("-")), - container_path: cord - end - end - - def cord_host_file - File.join cord_volume.host_path, CORD_FILE - end - - def cord_container_directory - health_check_options.fetch("cord", nil) - end - - def cord_container_file - File.join cord_volume.container_path, CORD_FILE - end - - def container_name(version = nil) [ container_prefix, version || config.version ].compact.join("-") end @@ -188,7 +119,7 @@ def asset_path end def assets? - asset_path.present? && running_traefik? + asset_path.present? && running_proxy? end def asset_volume(version = nil) @@ -241,27 +172,6 @@ def specializations end end - def traefik_labels - if running_traefik? - { - # Setting a service property ensures that the generated service name will be consistent between versions - "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" - } - else - {} - end - end - - def traefik_service - container_prefix - end - def custom_labels Hash.new.tap do |labels| labels.merge!(config.labels) if config.labels.present? diff --git a/lib/kamal/configuration/traefik.rb b/lib/kamal/configuration/traefik.rb deleted file mode 100644 index 45d8bac54..000000000 --- a/lib/kamal/configuration/traefik.rb +++ /dev/null @@ -1,78 +0,0 @@ -class Kamal::Configuration::Traefik - delegate :argumentize, to: Kamal::Utils - - DEFAULT_IMAGE = "traefik:v2.10" - CONTAINER_PORT = 80 - 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" - } - - include Kamal::Configuration::Validation - - attr_reader :config, :traefik_config - - def initialize(config:) - @config = config - @traefik_config = config.raw_config.traefik || {} - validate! traefik_config - end - - def publish? - traefik_config["publish"] != false - end - - def labels - DEFAULT_LABELS.merge(traefik_config["labels"] || {}) - end - - def env - Kamal::Configuration::Env.new \ - config: traefik_config.fetch("env", {}), - secrets: config.secrets, - context: "traefik/env" - end - - def host_port - traefik_config.fetch("host_port", CONTAINER_PORT) - end - - def options - traefik_config.fetch("options", {}) - end - - def port - "#{host_port}:#{CONTAINER_PORT}" - end - - def args - DEFAULT_ARGS.merge(traefik_config.fetch("args", {})) - end - - def image - traefik_config.fetch("image", DEFAULT_IMAGE) - end - - def env_args - [ *env.clear_args, *argumentize("--env-file", secrets_path) ] - end - - def env_directory - File.join(config.env_directory, "traefik") - end - - def secrets_io - env.secrets_io - end - - def secrets_path - File.join(config.env_directory, "traefik", "traefik.env") - end -end diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index 4460390e9..149fa3f03 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -5,7 +5,7 @@ class CliAppTest < CliTestCase stub_running run_command("boot").tap do |output| assert_match "docker tag dhh/app:latest dhh/app:latest", output - assert_match /docker run --detach --restart unless-stopped --name app-web-latest --hostname 1.1.1.1-[0-9a-f]{12} /, output + assert_match /docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-[0-9a-f]{12} /, output assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output end end @@ -18,26 +18,18 @@ class CliAppTest < CliTestCase .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", raise_on_non_zero_exit: false) .returns("12345678") # running version - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'") - .returns("running") # health check - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false) .returns("123") # old version SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :inspect, "-f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}'", "app-web-123", "|", :awk, "'$2 == \"/tmp/kamal-cord\" {print $1}'", raise_on_non_zero_exit: false) - .returns("cordfile") # old version - - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'") - .returns("unhealthy") # old version unhealthy + .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet") + .returns("12345678") # running version run_command("boot").tap do |output| assert_match /Renaming container .* to .* as already deployed on 1.1.1.1/, output # Rename assert_match /docker rename app-web-latest app-web-latest_replaced_[0-9a-f]{16}/, output - assert_match /docker run --detach --restart unless-stopped --name app-web-latest --hostname 1.1.1.1-[0-9a-f]{12} /, output + assert_match /docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-[0-9a-f]{12} /, output assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output end ensure @@ -70,23 +62,19 @@ class CliAppTest < CliTestCase .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", raise_on_non_zero_exit: false) .returns("12345678") # running version - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'") - .returns("running") # health check - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false) .returns("123").twice # old version SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :inspect, "-f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}'", "app-web-123", "|", :awk, "'$2 == \"/tmp/kamal-cord\" {print $1}'", raise_on_non_zero_exit: false) - .returns("") # old version + .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet") + .returns("12345678") # running version run_command("boot", config: :with_assets).tap do |output| assert_match "docker tag dhh/app:latest dhh/app:latest", output assert_match "/usr/bin/env mkdir -p .kamal/assets/volumes/app-web-latest ; cp -rnT .kamal/assets/extracted/app-web-latest .kamal/assets/volumes/app-web-latest ; cp -rnT .kamal/assets/extracted/app-web-latest .kamal/assets/volumes/app-web-123 || true ; cp -rnT .kamal/assets/extracted/app-web-123 .kamal/assets/volumes/app-web-latest || true", output assert_match "/usr/bin/env mkdir -p .kamal/assets/extracted/app-web-latest && docker stop -t 1 app-web-assets 2> /dev/null || true && docker run --name app-web-assets --detach --rm --entrypoint sleep dhh/app:latest 1000000 && docker cp -L app-web-assets:/public/assets/. .kamal/assets/extracted/app-web-latest && docker stop -t 1 app-web-assets", output - assert_match /docker run --detach --restart unless-stopped --name app-web-latest --hostname 1.1.1.1-[0-9a-f]{12} /, output + assert_match /docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-[0-9a-f]{12} /, output assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output assert_match "/usr/bin/env find .kamal/assets/extracted -maxdepth 1 -name 'app-web-*' ! -name app-web-latest -exec rm -rf \"{}\" + ; find .kamal/assets/volumes -maxdepth 1 -name 'app-web-*' ! -name app-web-latest -exec rm -rf \"{}\" +", output end @@ -96,24 +84,20 @@ class CliAppTest < CliTestCase Object.any_instance.stubs(:sleep) SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", raise_on_non_zero_exit: false) - .returns("12345678") # running version + .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", raise_on_non_zero_exit: false) + .returns("12345678") # running version SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'") - .returns("running") # health check + .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet") + .returns("12345678") # running version SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false) .returns("123") # old version - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :inspect, "-f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}'", "app-web-123", "|", :awk, "'$2 == \"/tmp/kamal-cord\" {print $1}'", raise_on_non_zero_exit: false) - .returns("") # old version - run_command("boot", config: :with_env_tags).tap do |output| assert_match "docker tag dhh/app:latest dhh/app:latest", output - assert_match %r{docker run --detach --restart unless-stopped --name app-web-latest --hostname 1.1.1.1-[0-9a-f]{12} -e KAMAL_CONTAINER_NAME="app-web-latest" -e KAMAL_VERSION="latest" --env TEST="root" --env EXPERIMENT="disabled" --env SITE="site1"}, output + assert_match %r{docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-[0-9a-f]{12} -e KAMAL_CONTAINER_NAME="app-web-latest" -e KAMAL_VERSION="latest" --env TEST="root" --env EXPERIMENT="disabled" --env SITE="site1"}, output assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output end end @@ -123,14 +107,6 @@ class CliAppTest < CliTestCase SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'") - .returns("running").at_least_once # web health check passing - - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'") - .returns("unhealthy").at_least_once # web health check failing - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:docker, :container, :ls, "--all", "--filter", "name=^app-workers-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'") .returns("running").at_least_once # workers health check @@ -150,9 +126,11 @@ class CliAppTest < CliTestCase SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'") - .returns("unhealthy").at_least_once # web health check failing + SSHKit::Backend::Abstract.any_instance.stubs(:execute).returns("") + SSHKit::Backend::Abstract.any_instance.stubs(:execute) + .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :stop, raise_on_non_zero_exit: false).twice + SSHKit::Backend::Abstract.any_instance.stubs(:execute) + .with(:docker, :exec, "kamal-proxy", "kamal-proxy", :deploy, "app-web", "--target", "\"123:80\"", "--buffer-requests", "--buffer-responses", "--log-request-header", "\"Cache-Control\"", "--log-request-header", "\"Last-Modified\"").raises(SSHKit::Command::Failed.new("Failed to deploy")) stderred do run_command("boot", config: :with_roles, host: nil, allow_execute_error: true).tap do |output| @@ -160,8 +138,6 @@ class CliAppTest < CliTestCase assert_match "Waiting for the first healthy web container before booting workers on 1.1.1.4...", output assert_match "First web container is unhealthy, not booting workers on 1.1.1.3", output assert_match "First web container is unhealthy, not booting workers on 1.1.1.4", output - assert_match "Running docker container ls --all --filter name=^app-web-latest$ --quiet | xargs docker stop on 1.1.1.1", output - assert_match "Running docker container ls --all --filter name=^app-web-latest$ --quiet | xargs docker stop on 1.1.1.2", output end end ensure @@ -169,8 +145,11 @@ class CliAppTest < CliTestCase end test "start" do + SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("999") # old version + run_command("start").tap do |output| assert_match "docker start app-web-999", output + assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"999:80\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\"", output end end @@ -342,7 +321,7 @@ class CliAppTest < CliTestCase hostname = "this-hostname-is-really-unacceptably-long-to-be-honest.example.com" stdouted { Kamal::Cli::App.start([ "boot", "-c", "test/fixtures/deploy_with_uncommon_hostnames.yml", "--hosts", hostname ]) }.tap do |output| - assert_match /docker run --detach --restart unless-stopped --name app-web-latest --hostname this-hostname-is-really-unacceptably-long-to-be-hon-[0-9a-f]{12} /, output + assert_match /docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname this-hostname-is-really-unacceptably-long-to-be-hon-[0-9a-f]{12} /, output end end @@ -352,7 +331,7 @@ class CliAppTest < CliTestCase hostname = "this-hostname-with-random-part-is-too-long.example.com" stdouted { Kamal::Cli::App.start([ "boot", "-c", "test/fixtures/deploy_with_uncommon_hostnames.yml", "--hosts", hostname ]) }.tap do |output| - assert_match /docker run --detach --restart unless-stopped --name app-web-latest --hostname this-hostname-with-random-part-is-too-long.example-[0-9a-f]{12} /, output + assert_match /docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname this-hostname-with-random-part-is-too-long.example-[0-9a-f]{12} /, output end end @@ -381,13 +360,5 @@ def stub_running Object.any_instance.stubs(:sleep) SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version - - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'") - .returns("running") # health check - - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'") - .returns("unhealthy") # health check end end diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index 2115f4180..f82eef3e2 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -24,7 +24,7 @@ class CliMainTest < CliTestCase # deploy Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options.merge(skip_local: true)) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:pull", [], invoke_options) - Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:boot", [], invoke_options) + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], invoke_options) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options) @@ -35,7 +35,7 @@ class CliMainTest < CliTestCase assert_match /Acquiring the deploy lock/, output assert_match /Log into image registry/, output assert_match /Pull app image/, output - assert_match /Ensure Traefik is running/, output + assert_match /Ensure kamal-proxy is running/, output assert_match /Detect stale containers/, output assert_match /Prune old containers and images/, output assert_match /Releasing the deploy lock/, output @@ -48,7 +48,7 @@ class CliMainTest < CliTestCase Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options.merge(skip_local: false)) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:deliver", [], invoke_options) - Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:boot", [], invoke_options) + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], invoke_options) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options) @@ -61,7 +61,7 @@ class CliMainTest < CliTestCase assert_match /Log into image registry/, output assert_match /Build and push app image/, output assert_hook_ran "pre-deploy", output, **hook_variables, secrets: true - assert_match /Ensure Traefik is running/, output + assert_match /Ensure kamal-proxy is running/, output assert_match /Detect stale containers/, output assert_match /Prune old containers and images/, output assert_hook_ran "post-deploy", output, **hook_variables, runtime: true, secrets: true @@ -74,7 +74,7 @@ class CliMainTest < CliTestCase Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options.merge(skip_local: true)) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:pull", [], invoke_options) - Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:boot", [], invoke_options) + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], invoke_options) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options) @@ -83,7 +83,7 @@ class CliMainTest < CliTestCase assert_match /Acquiring the deploy lock/, output assert_match /Log into image registry/, output assert_match /Pull app image/, output - assert_match /Ensure Traefik is running/, output + assert_match /Ensure kamal-proxy is running/, output assert_match /Detect stale containers/, output assert_match /Prune old containers and images/, output assert_match /Releasing the deploy lock/, output @@ -180,7 +180,7 @@ class CliMainTest < CliTestCase Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options.merge(skip_local: false)) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:deliver", [], invoke_options) - Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:boot", [], invoke_options) + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], invoke_options) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options) @@ -190,27 +190,12 @@ class CliMainTest < CliTestCase end end - test "deploy without healthcheck if primary host doesn't have traefik" do - invoke_options = { "config_file" => "test/fixtures/deploy_workers_only.yml", "version" => "999", "skip_hooks" => false } - - Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:healthcheck:perform", [], invoke_options).never - - Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options.merge(skip_local: false)) - Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:deliver", [], invoke_options) - Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:boot", [], invoke_options) - Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)) - Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options) - Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options) - - run_command("deploy", config_file: "deploy_workers_only") - end - test "deploy with missing secrets" do invoke_options = { "config_file" => "test/fixtures/deploy_with_secrets.yml", "version" => "999", "skip_hooks" => false } Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options.merge(skip_local: false)) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:deliver", [], invoke_options) - Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:boot", [], invoke_options) + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], invoke_options) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options) @@ -273,18 +258,11 @@ class CliMainTest < CliTestCase SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=#{role} --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=#{role} --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-#{role}-}; done", raise_on_non_zero_exit: false) .returns("version-to-rollback\n").at_least_once - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :container, :ls, "--all", "--filter", "name=^app-#{role}-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'") - .returns("running").at_least_once # health check end SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :inspect, "-f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}'", "app-web-version-to-rollback", "|", :awk, "'$2 == \"/tmp/kamal-cord\" {print $1}'", raise_on_non_zero_exit: false) - .returns("corddirectory").at_least_once # health check - - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-version-to-rollback$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'") - .returns("unhealthy").at_least_once # health check + .with(:docker, :container, :ls, "--all", "--filter", "name=^app-workers-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'") + .returns("running").at_least_once # health check Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true) hook_variables = { version: 123, service_version: "app@123", hosts: "1.1.1.1,1.1.1.2,1.1.1.3,1.1.1.4", command: "rollback" } @@ -301,17 +279,15 @@ class CliMainTest < CliTestCase test "rollback without old version" do Kamal::Cli::Main.any_instance.stubs(:container_available?).returns(true) - Kamal::Cli::Healthcheck::Poller.stubs(:sleep) - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", raise_on_non_zero_exit: false) .returns("").at_least_once + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet") + .returns("123").at_least_once SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false) .returns("").at_least_once - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'") - .returns("running").at_least_once # health check run_command("rollback", "123").tap do |output| assert_match "docker run --detach --restart unless-stopped --name app-web-123", output @@ -320,7 +296,7 @@ class CliMainTest < CliTestCase end test "details" do - Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:details") + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:details") Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:details") Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:details", [ "all" ]) @@ -434,9 +410,9 @@ class CliMainTest < CliTestCase test "remove with confirmation" do run_command("remove", "-y", config_file: "deploy_with_accessories").tap do |output| - assert_match /docker container stop traefik/, output - assert_match /docker container prune --force --filter label=org.opencontainers.image.title=Traefik/, output - assert_match /docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik/, output + assert_match /docker container stop kamal-proxy/, output + assert_match /docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy/, output + assert_match /docker image prune --all --force --filter label=org.opencontainers.image.title=kamal-proxy/, output assert_match /docker ps --quiet --filter label=service=app | xargs docker stop/, output assert_match /docker container prune --force --filter label=service=app/, output @@ -480,7 +456,7 @@ class CliMainTest < CliTestCase end test "run an alias for details" do - Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:details") + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:details") Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:details") Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:details", [ "all" ]) diff --git a/test/cli/proxy_test.rb b/test/cli/proxy_test.rb index 20c773cb2..1d98395f1 100644 --- a/test/cli/proxy_test.rb +++ b/test/cli/proxy_test.rb @@ -21,17 +21,16 @@ class CliProxyTest < CliTestCase run_command("reboot", "-y").tap do |output| assert_match "docker container stop kamal-proxy on 1.1.1.1", output - assert_match "docker container stop traefik on 1.1.1.1", output + assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output - assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE} on 1.1.1.1", output assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" on 1.1.1.1", output assert_match "docker container stop kamal-proxy on 1.1.1.2", output - assert_match "docker container stop traefik on 1.1.1.2", output + assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.2", output assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.2", output - assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.2", 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\" traefik:v2.10 --providers.docker --log.level=\"DEBUG\" on 1.1.1.2", output + assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE} on 1.1.1.2", output + assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" on 1.1.1.2", output end end @@ -82,7 +81,7 @@ class CliProxyTest < CliTestCase .returns("Log entry") SSHKit::Backend::Abstract.any_instance.stubs(:capture) - .with(:docker, :logs, "traefik", " --tail 100", "--timestamps", "2>&1") + .with(:docker, :logs, "proxy", " --tail 100", "--timestamps", "2>&1") .returns("Log entry") run_command("logs").tap do |output| @@ -118,24 +117,8 @@ class CliProxyTest < CliTestCase end end - test "commands disallowed when proxy is disabled" do - assert_raises_when_disabled "boot" - assert_raises_when_disabled "reboot" - assert_raises_when_disabled "start" - assert_raises_when_disabled "stop" - assert_raises_when_disabled "details" - assert_raises_when_disabled "logs" - assert_raises_when_disabled "remove" - end - private def run_command(*command, fixture: :with_proxy) stdouted { Kamal::Cli::Proxy.start([ *command, "-c", "test/fixtures/deploy_#{fixture}.yml" ]) } end - - def assert_raises_when_disabled(command) - assert_raises "kamal proxy commands are disabled unless experimental proxy support is enabled. Use `kamal traefik` commands instead." do - run_command(command, fixture: :with_accessories) - end - end end diff --git a/test/cli/prune_test.rb b/test/cli/prune_test.rb index 33723f13d..bb4ced51a 100644 --- a/test/cli/prune_test.rb +++ b/test/cli/prune_test.rb @@ -18,12 +18,10 @@ class CliPruneTest < CliTestCase test "containers" do run_command("containers").tap do |output| assert_match /docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +6 | while read container_id; do docker rm $container_id; done on 1.1.1.\d/, output - assert_match /docker container prune --force --filter label=service=healthcheck-app on 1.1.1.\d/, output end run_command("containers", "--retain", "10").tap do |output| assert_match /docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +11 | while read container_id; do docker rm $container_id; done on 1.1.1.\d/, output - assert_match /docker container prune --force --filter label=service=healthcheck-app on 1.1.1.\d/, output end assert_raises(RuntimeError, "retain must be at least 1") do diff --git a/test/cli/traefik_test.rb b/test/cli/traefik_test.rb deleted file mode 100644 index 291711509..000000000 --- a/test/cli/traefik_test.rb +++ /dev/null @@ -1,110 +0,0 @@ -require_relative "cli_test_case" - -class CliTraefikTest < CliTestCase - test "boot" do - run_command("boot").tap do |output| - assert_match "docker login", output - assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Configuration::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output - end - end - - test "reboot" do - Kamal::Commands::Registry.any_instance.expects(:login).twice - - run_command("reboot", "-y").tap do |output| - assert_match "docker container stop traefik", output - assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik", output - assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Configuration::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output - end - end - - test "reboot --rolling" do - Object.any_instance.stubs(:sleep) - - run_command("reboot", "--rolling", "-y").tap do |output| - assert_match "Running docker container prune --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output - end - end - - test "start" do - run_command("start").tap do |output| - assert_match "docker container start traefik", output - end - end - - test "stop" do - run_command("stop").tap do |output| - assert_match "docker container stop traefik", output - end - end - - test "restart" do - Kamal::Cli::Traefik.any_instance.expects(:stop) - Kamal::Cli::Traefik.any_instance.expects(:start) - - run_command("restart") - end - - test "details" do - run_command("details").tap do |output| - assert_match "docker ps --filter name=^traefik$", output - end - end - - test "logs" do - SSHKit::Backend::Abstract.any_instance.stubs(:capture) - .with(:docker, :logs, "traefik", " --tail 100", "--timestamps", "2>&1") - .returns("Log entry") - - run_command("logs").tap do |output| - assert_match "Traefik Host: 1.1.1.1", output - assert_match "Log entry", output - end - end - - test "logs with follow" do - SSHKit::Backend::Abstract.any_instance.stubs(:exec) - .with("ssh -t root@1.1.1.1 -p 22 'docker logs traefik --timestamps --tail 10 --follow 2>&1'") - - assert_match "docker logs traefik --timestamps --tail 10 --follow", run_command("logs", "--follow") - end - - test "logs with follow and grep" do - SSHKit::Backend::Abstract.any_instance.stubs(:exec) - .with("ssh -t root@1.1.1.1 -p 22 'docker logs traefik --timestamps --tail 10 --follow 2>&1 | grep \"hey\"'") - - assert_match "docker logs traefik --timestamps --tail 10 --follow 2>&1 | grep \"hey\"", run_command("logs", "--follow", "--grep", "hey") - end - - test "logs with follow, grep, and grep options" do - SSHKit::Backend::Abstract.any_instance.stubs(:exec) - .with("ssh -t root@1.1.1.1 -p 22 'docker logs traefik --timestamps --tail 10 --follow 2>&1 | grep \"hey\" -C 2'") - - assert_match "docker logs traefik --timestamps --tail 10 --follow 2>&1 | grep \"hey\" -C 2", run_command("logs", "--follow", "--grep", "hey", "--grep-options", "-C 2") - end - - test "remove" do - Kamal::Cli::Traefik.any_instance.expects(:stop) - Kamal::Cli::Traefik.any_instance.expects(:remove_container) - Kamal::Cli::Traefik.any_instance.expects(:remove_image) - - run_command("remove") - end - - test "remove_container" do - run_command("remove_container").tap do |output| - assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik", output - end - end - - test "remove_image" do - run_command("remove_image").tap do |output| - assert_match "docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik", output - end - end - - private - def run_command(*command) - stdouted { Kamal::Cli::Traefik.start([ *command, "-c", "test/fixtures/deploy_with_accessories.yml" ]) } - end -end diff --git a/test/commander_test.rb b/test/commander_test.rb index c2b78b213..54031e803 100644 --- a/test/commander_test.rb +++ b/test/commander_test.rb @@ -136,18 +136,18 @@ class CommanderTest < ActiveSupport::TestCase assert_equal [ "1.1.1.3", "1.1.1.4", "1.1.1.1", "1.1.1.2" ], @kamal.hosts end - test "traefik hosts should observe filtered roles" do - configure_with(:deploy_with_multiple_traefik_roles) + test "proxy hosts should observe filtered roles" do + configure_with(:deploy_with_multiple_proxy_roles) @kamal.specific_roles = [ "web_tokyo" ] - assert_equal [ "1.1.1.3", "1.1.1.4" ], @kamal.traefik_hosts + assert_equal [ "1.1.1.3", "1.1.1.4" ], @kamal.proxy_hosts end - test "traefik hosts should observe filtered hosts" do - configure_with(:deploy_with_multiple_traefik_roles) + test "proxy hosts should observe filtered hosts" do + configure_with(:deploy_with_multiple_proxy_roles) @kamal.specific_hosts = [ "1.1.1.2" ] - assert_equal [ "1.1.1.2" ], @kamal.traefik_hosts + assert_equal [ "1.1.1.2" ], @kamal.proxy_hosts end private diff --git a/test/commands/app_test.rb b/test/commands/app_test.rb index e385764e0..699319016 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 destination --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999", + "docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999", new_command.run.join(" ") end test "run with hostname" do assert_equal \ - "docker run --detach --restart unless-stopped --name app-web-999 --hostname myhost -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/up || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999", + "docker run --detach --restart unless-stopped --name app-web-999 --network kamal --hostname myhost -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999", new_command.run(hostname: "myhost").join(" ") end @@ -28,38 +28,14 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:volumes] = [ "/local/path:/container/path" ] assert_equal \ - "docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/up || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label destination --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999", - new_command.run.join(" ") - end - - test "run with custom healthcheck path" do - @config[:healthcheck] = { "path" => "/healthz" } - - assert_equal \ - "docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/healthz || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999", - new_command.run.join(" ") - end - - test "run with custom healthcheck command" do - @config[:healthcheck] = { "cmd" => "/bin/up" } - - assert_equal \ - "docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(/bin/up) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999", - new_command.run.join(" ") - end - - test "run with role-specific healthcheck options" do - @config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "healthcheck" => { "cmd" => "/bin/healthy" } } } - - assert_equal \ - "docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(/bin/healthy) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999", + "docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --log-opt max-size=\"10m\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label destination dhh/app:999", new_command.run.join(" ") end test "run with custom options" do @config[:servers] = { "web" => [ "1.1.1.1" ], "jobs" => { "hosts" => [ "1.1.1.2" ], "cmd" => "bin/jobs", "options" => { "mount" => "somewhere", "cap-add" => true } } } assert_equal \ - "docker run --detach --restart unless-stopped --name app-jobs-999 -e KAMAL_CONTAINER_NAME=\"app-jobs-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-jobs.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"jobs\" --label destination --mount \"somewhere\" --cap-add dhh/app:999 bin/jobs", + "docker run --detach --restart unless-stopped --name app-jobs-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-jobs-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/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", host: "1.1.1.2").run.join(" ") end @@ -67,7 +43,7 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } } assert_equal \ - "docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/up || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label destination --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999", + "docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999", new_command.run.join(" ") end @@ -76,7 +52,7 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "logging" => { "driver" => "local", "options" => { "max-size" => "100m" } } } } assert_equal \ - "docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/up || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label destination --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999", + "docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999", new_command.run.join(" ") end @@ -85,7 +61,7 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } } assert_equal \ - "docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env ENV1=\"value1\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/up || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999", + "docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env ENV1=\"value1\" --env-file .kamal/env/roles/app-web.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999", new_command.run.join(" ") end @@ -412,20 +388,6 @@ class CommandsAppTest < ActiveSupport::TestCase new_command.tag_latest_image.join(" ") end - test "cord" do - assert_equal "docker inspect -f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}' app-web-123 | awk '$2 == \"/tmp/kamal-cord\" {print $1}'", new_command.cord(version: 123).join(" ") - end - - test "tie cord" do - assert_equal "mkdir -p . ; touch cordfile", new_command.tie_cord("cordfile").join(" ") - assert_equal "mkdir -p corddir ; touch corddir/cordfile", new_command.tie_cord("corddir/cordfile").join(" ") - assert_equal "mkdir -p /corddir ; touch /corddir/cordfile", new_command.tie_cord("/corddir/cordfile").join(" ") - end - - test "cut cord" do - assert_equal "rm -r corddir", new_command.cut_cord("corddir").join(" ") - end - test "extract assets" do assert_equal [ :mkdir, "-p", ".kamal/assets/extracted/app-web-999", "&&", diff --git a/test/commands/hook_test.rb b/test/commands/hook_test.rb index f6234d6a1..dc2afc5ac 100644 --- a/test/commands/hook_test.rb +++ b/test/commands/hook_test.rb @@ -8,7 +8,7 @@ class CommandsHookTest < ActiveSupport::TestCase @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], - builder: { "arch" => "amd64" }, traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } + builder: { "arch" => "amd64" } } @performer = Kamal::Git.email.presence || `whoami`.chomp diff --git a/test/commands/lock_test.rb b/test/commands/lock_test.rb index 028719220..a87572513 100644 --- a/test/commands/lock_test.rb +++ b/test/commands/lock_test.rb @@ -4,7 +4,7 @@ class CommandsLockTest < ActiveSupport::TestCase setup do @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], - builder: { "arch" => "amd64" }, traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } + builder: { "arch" => "amd64" } } end diff --git a/test/commands/prune_test.rb b/test/commands/prune_test.rb index 430a13db6..50e852bcb 100644 --- a/test/commands/prune_test.rb +++ b/test/commands/prune_test.rb @@ -4,7 +4,7 @@ class CommandsPruneTest < ActiveSupport::TestCase setup do @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], - builder: { "arch" => "amd64" }, traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } + builder: { "arch" => "amd64" } } end @@ -30,12 +30,6 @@ class CommandsPruneTest < ActiveSupport::TestCase new_command.app_containers(retain: 3).join(" ") end - test "healthcheck containers" do - assert_equal \ - "docker container prune --force --filter label=service=healthcheck-app", - new_command.healthcheck_containers.join(" ") - end - private def new_command Kamal::Commands::Prune.new(Kamal::Configuration.new(@config, version: "123")) diff --git a/test/commands/server_test.rb b/test/commands/server_test.rb index aa13fc048..648821b4a 100644 --- a/test/commands/server_test.rb +++ b/test/commands/server_test.rb @@ -4,7 +4,7 @@ class CommandsServerTest < ActiveSupport::TestCase setup do @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], - builder: { "arch" => "amd64" }, traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } + builder: { "arch" => "amd64" } } end diff --git a/test/commands/traefik_test.rb b/test/commands/traefik_test.rb deleted file mode 100644 index b13e37005..000000000 --- a/test/commands/traefik_test.rb +++ /dev/null @@ -1,195 +0,0 @@ -require "test_helper" - -class CommandsTraefikTest < ActiveSupport::TestCase - setup do - @image = "traefik:test" - - @config = { - service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], builder: { "arch" => "amd64" }, - traefik: { "image" => @image, "args" => { "accesslog.format" => "json", "api.insecure" => true, "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } - } - - setup_test_secrets("secrets" => "EXAMPLE_API_KEY=456") - end - - teardown do - teardown_test_secrets - end - - test "run" do - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - - @config[:traefik]["host_port"] = "8080" - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 8080:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - - @config[:traefik]["publish"] = false - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - end - - test "run with ports configured" do - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - - @config[:traefik]["options"] = { "publish" => %w[9000:9000 9001:9001] } - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --publish \"9000:9000\" --publish \"9001:9001\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - end - - test "run with volumes configured" do - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - - @config[:traefik]["options"] = { "volume" => %w[./letsencrypt/acme.json:/letsencrypt/acme.json] } - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - end - - test "run with several options configured" do - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - - @config[:traefik]["options"] = { "volume" => %w[./letsencrypt/acme.json:/letsencrypt/acme.json], "publish" => %w[8080:8080], "memory" => "512m" } - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" --publish \"8080:8080\" --memory \"512m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - end - - test "run with labels configured" do - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - - @config[:traefik]["labels"] = { "traefik.http.routers.dashboard.service" => "api@internal", "traefik.http.routers.dashboard.middlewares" => "auth" } - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --label traefik.http.routers.dashboard.service=\"api@internal\" --label traefik.http.routers.dashboard.middlewares=\"auth\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - end - - test "run with env configured" do - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - - @config[:traefik]["env"] = { "EXAMPLE_API_KEY" => "456" } - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env EXAMPLE_API_KEY=\"456\" --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - end - - test "run without configuration" do - @config.delete(:traefik) - - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Configuration::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", - new_command.run.join(" ") - end - - test "run with logging config" do - @config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } } - - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - end - - test "run with default args overriden" do - @config[:traefik]["args"]["log.level"] = "ERROR" - - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"ERROR\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - end - - test "run with args array" do - @config[:traefik]["args"] = { "entrypoints.web.forwardedheaders.trustedips" => %w[ 127.0.0.1 127.0.0.2 ] } - assert_equal "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" traefik:test --providers.docker --log.level=\"DEBUG\" --entrypoints.web.forwardedheaders.trustedips=\"127.0.0.1\" --entrypoints.web.forwardedheaders.trustedips=\"127.0.0.2\"", new_command.run.join(" ") - end - - test "traefik start" do - assert_equal \ - "docker container start traefik", - new_command.start.join(" ") - end - - test "traefik stop" do - assert_equal \ - "docker container stop traefik", - new_command.stop.join(" ") - end - - test "traefik info" do - assert_equal \ - "docker ps --filter name=^traefik$", - new_command.info.join(" ") - end - - test "traefik logs" do - assert_equal \ - "docker logs traefik --timestamps 2>&1", - new_command.logs.join(" ") - end - - test "traefik logs since 2h" do - assert_equal \ - "docker logs traefik --since 2h --timestamps 2>&1", - new_command.logs(since: "2h").join(" ") - end - - test "traefik logs last 10 lines" do - assert_equal \ - "docker logs traefik --tail 10 --timestamps 2>&1", - new_command.logs(lines: 10).join(" ") - end - - test "traefik logs with grep hello!" do - assert_equal \ - "docker logs traefik --timestamps 2>&1 | grep 'hello!'", - new_command.logs(grep: "hello!").join(" ") - end - - test "traefik logs with grep hello! and grep options" do - assert_equal \ - "docker logs traefik --timestamps 2>&1 | grep 'hello!' -C 2", - new_command.logs(grep: "hello!", grep_options: "-C 2").join(" ") - end - - test "traefik remove container" do - assert_equal \ - "docker container prune --force --filter label=org.opencontainers.image.title=Traefik", - new_command.remove_container.join(" ") - end - - test "traefik remove image" do - assert_equal \ - "docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik", - new_command.remove_image.join(" ") - end - - test "traefik follow logs" do - assert_equal \ - "ssh -t root@1.1.1.1 -p 22 'docker logs traefik --timestamps --tail 10 --follow 2>&1'", - new_command.follow_logs(host: @config[:servers].first) - end - - test "traefik follow logs with grep hello!" do - assert_equal \ - "ssh -t root@1.1.1.1 -p 22 'docker logs traefik --timestamps --tail 10 --follow 2>&1 | grep \"hello!\"'", - new_command.follow_logs(host: @config[:servers].first, grep: "hello!") - end - - private - def new_command - Kamal::Commands::Traefik.new(Kamal::Configuration.new(@config, version: "123")) - end -end diff --git a/test/configuration/role_test.rb b/test/configuration/role_test.rb index c0b643bfa..b9938a777 100644 --- a/test/configuration/role_test.rb +++ b/test/configuration/role_test.rb @@ -39,7 +39,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase end test "special label args for web" do - assert_equal [ "--label", "service=\"app\"", "--label", "role=\"web\"", "--label", "destination", "--label", "traefik.http.services.app-web.loadbalancer.server.scheme=\"http\"", "--label", "traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.routers.app-web.priority=\"2\"", "--label", "traefik.http.middlewares.app-web-retry.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\"", "--label", "traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\"" ], config.role(:web).label_args + assert_equal [ "--label", "service=\"app\"", "--label", "role=\"web\"", "--label", "destination" ], config.role(:web).label_args end test "custom labels" do @@ -53,17 +53,12 @@ class ConfigurationRoleTest < ActiveSupport::TestCase assert_equal "70", Kamal::Configuration.new(@deploy_with_roles).role(:workers).labels["my.custom.label"] end - test "overwriting default traefik label" do - @deploy[:labels] = { "traefik.http.routers.app-web.rule" => "\"Host(\\`example.com\\`) || (Host(\\`example.org\\`) && Path(\\`/traefik\\`))\"" } - assert_equal "\"Host(\\`example.com\\`) || (Host(\\`example.org\\`) && Path(\\`/traefik\\`))\"", config.role(:web).labels["traefik.http.routers.app-web.rule"] - end - - test "default traefik label on non-web role" do + test "default proxy label on non-web role" do config = Kamal::Configuration.new(@deploy_with_roles.tap { |c| - c[:servers]["beta"] = { "traefik" => true, "hosts" => [ "1.1.1.5" ] } + c[:servers]["beta"] = { "proxy" => true, "hosts" => [ "1.1.1.5" ] } }) - assert_equal [ "--label", "service=\"app\"", "--label", "role=\"beta\"", "--label", "destination", "--label", "traefik.http.services.app-beta.loadbalancer.server.scheme=\"http\"", "--label", "traefik.http.routers.app-beta.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.routers.app-beta.priority=\"2\"", "--label", "traefik.http.middlewares.app-beta-retry.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app-beta-retry.retry.initialinterval=\"500ms\"", "--label", "traefik.http.routers.app-beta.middlewares=\"app-beta-retry@docker\"" ], config.role(:beta).label_args + assert_equal [ "--label", "service=\"app\"", "--label", "role=\"beta\"", "--label", "destination" ], config.role(:beta).label_args end test "env overwritten by role" do @@ -198,26 +193,6 @@ class ConfigurationRoleTest < ActiveSupport::TestCase end end - test "uses cord" do - assert config_with_roles.role(:web).uses_cord? - assert_not config_with_roles.role(:workers).uses_cord? - end - - test "cord host file" do - assert_match %r{.kamal/cords/app-web-[0-9a-f]{32}/cord}, config_with_roles.role(:web).cord_host_file - end - - test "cord volume" do - assert_equal "/tmp/kamal-cord", config_with_roles.role(:web).cord_volume.container_path - assert_match %r{.kamal/cords/app-web-[0-9a-f]{32}}, config_with_roles.role(:web).cord_volume.host_path - assert_equal "--volume", config_with_roles.role(:web).cord_volume.docker_args[0] - assert_match %r{\$\(pwd\)/.kamal/cords/app-web-[0-9a-f]{32}:/tmp/kamal-cord}, config_with_roles.role(:web).cord_volume.docker_args[1] - end - - test "cord container file" do - assert_equal "/tmp/kamal-cord/cord", config_with_roles.role(:web).cord_container_file - end - test "asset path and volume args" do ENV["VERSION"] = "12345" assert_nil config_with_roles.role(:web).asset_volume_args diff --git a/test/configuration/validation_test.rb b/test/configuration/validation_test.rb index ac409e845..d8ac3e3bf 100644 --- a/test/configuration/validation_test.rb +++ b/test/configuration/validation_test.rb @@ -22,7 +22,7 @@ class ConfigurationValidationTest < ActiveSupport::TestCase assert_error "servers: should be an array or a hash", servers: "foo" - [ :labels, :registry, :accessories, :env, :ssh, :sshkit, :builder, :traefik, :boot, :healthcheck, :logging ].each do |key| + [ :labels, :registry, :accessories, :env, :ssh, :sshkit, :builder, :proxy, :boot, :logging ].each do |key| assert_error "#{key}: should be a hash", **{ key =>[] } end end diff --git a/test/configuration_test.rb b/test/configuration_test.rb index eaa881d98..c64070eb7 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -77,22 +77,22 @@ class ConfigurationTest < ActiveSupport::TestCase assert_equal "1.1.1.1", @config_with_roles.primary_host end - test "traefik hosts" do - assert_equal [ "1.1.1.1", "1.1.1.2" ], @config_with_roles.traefik_hosts + test "proxy hosts" do + assert_equal [ "1.1.1.1", "1.1.1.2" ], @config_with_roles.proxy_hosts - @deploy_with_roles[:servers]["workers"]["traefik"] = true + @deploy_with_roles[:servers]["workers"]["proxy"] = true config = Kamal::Configuration.new(@deploy_with_roles) - assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3" ], config.traefik_hosts + assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3" ], config.proxy_hosts end - test "filtered traefik hosts" do - assert_equal [ "1.1.1.1", "1.1.1.2" ], @config_with_roles.traefik_hosts + test "filtered proxy hosts" do + assert_equal [ "1.1.1.1", "1.1.1.2" ], @config_with_roles.proxy_hosts - @deploy_with_roles[:servers]["workers"]["traefik"] = true + @deploy_with_roles[:servers]["workers"]["proxy"] = true config = Kamal::Configuration.new(@deploy_with_roles) - assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3" ], config.traefik_hosts + assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3" ], config.proxy_hosts end test "version no git repo" do @@ -157,10 +157,6 @@ class ConfigurationTest < ActiveSupport::TestCase assert_equal "app-missing", @config.service_with_version end - test "healthcheck service" do - assert_equal "healthcheck-app", @config.healthcheck_service - end - test "hosts required for all roles" do # Empty server list for implied web role assert_raises(Kamal::ConfigurationError) do @@ -269,8 +265,7 @@ class ConfigurationTest < ActiveSupport::TestCase sshkit: {}, volume_args: [ "--volume", "/local/path:/container/path" ], builder: { "arch" => "amd64" }, - logging: [ "--log-opt", "max-size=\"10m\"" ], - healthcheck: { "cmd"=>"curl -f http://localhost:3000/up || exit 1", "interval" => "1s", "path"=>"/up", "port"=>3000, "max_attempts" => 7, "cord" => "/tmp/kamal-cord", "log_lines" => 50 } } + logging: [ "--log-opt", "max-size=\"10m\"" ] } assert_equal expected_config, @config.to_h end @@ -322,7 +317,7 @@ class ConfigurationTest < ActiveSupport::TestCase assert_equal "alternate_web", config.primary_role.name assert_equal "1.1.1.4", config.primary_host assert config.role(:alternate_web).primary? - assert config.role(:alternate_web).running_traefik? + assert config.role(:alternate_web).running_proxy? end test "primary role missing" do @@ -344,7 +339,7 @@ class ConfigurationTest < ActiveSupport::TestCase dest_config_file = Pathname.new(File.expand_path("fixtures/deploy_with_extensions.yml", __dir__)) config = Kamal::Configuration.create_from config_file: dest_config_file - assert_equal config.role(:web_tokyo).running_traefik?, true - assert_equal config.role(:web_chicago).running_traefik?, true + assert_equal config.role(:web_tokyo).running_proxy?, true + assert_equal config.role(:web_chicago).running_proxy?, true end end diff --git a/test/fixtures/deploy_primary_web_role_override.yml b/test/fixtures/deploy_primary_web_role_override.yml index aa52f5edd..6bc2f837d 100644 --- a/test/fixtures/deploy_primary_web_role_override.yml +++ b/test/fixtures/deploy_primary_web_role_override.yml @@ -2,12 +2,12 @@ service: app image: dhh/app servers: web_chicago: - traefik: true + proxy: true hosts: - 1.1.1.1 - 1.1.1.2 web_tokyo: - traefik: true + proxy: true hosts: - 1.1.1.3 - 1.1.1.4 diff --git a/test/fixtures/deploy_with_extensions.yml b/test/fixtures/deploy_with_extensions.yml index 4a5f934a5..c45104de3 100644 --- a/test/fixtures/deploy_with_extensions.yml +++ b/test/fixtures/deploy_with_extensions.yml @@ -1,6 +1,6 @@ x-web: &web - traefik: true + proxy: true service: app image: dhh/app diff --git a/test/fixtures/deploy_with_multiple_traefik_roles.yml b/test/fixtures/deploy_with_multiple_proxy_roles.yml similarity index 93% rename from test/fixtures/deploy_with_multiple_traefik_roles.yml rename to test/fixtures/deploy_with_multiple_proxy_roles.yml index a12705831..6b785a6a4 100644 --- a/test/fixtures/deploy_with_multiple_traefik_roles.yml +++ b/test/fixtures/deploy_with_multiple_proxy_roles.yml @@ -8,14 +8,14 @@ servers: - 1.1.1.2 env: ROLE: "web" - traefik: true + proxy: true web_tokyo: hosts: - 1.1.1.3 - 1.1.1.4 env: ROLE: "web" - traefik: true + proxy: true workers: cmd: bin/jobs hosts: diff --git a/test/fixtures/deploy_with_proxy.yml b/test/fixtures/deploy_with_proxy.yml index 2912c6454..e91e9657a 100644 --- a/test/fixtures/deploy_with_proxy.yml +++ b/test/fixtures/deploy_with_proxy.yml @@ -14,9 +14,6 @@ builder: arch: amd64 proxy: - enabled: true - hosts: - - "1.1.1.1" deploy_timeout: 6s accessories: diff --git a/test/fixtures/deploy_workers_only.yml b/test/fixtures/deploy_workers_only.yml index d83adeeb7..6f6568c3a 100644 --- a/test/fixtures/deploy_workers_only.yml +++ b/test/fixtures/deploy_workers_only.yml @@ -2,7 +2,7 @@ service: app image: dhh/app servers: workers: - traefik: false + proxy: false hosts: - 1.1.1.1 - 1.1.1.2 diff --git a/test/integration/app_test.rb b/test/integration/app_test.rb index b7dcdc343..60e1cf6e9 100644 --- a/test/integration/app_test.rb +++ b/test/integration/app_test.rb @@ -8,6 +8,7 @@ class AppTest < IntegrationTest kamal :app, :stop + exit! assert_app_is_down kamal :app, :start diff --git a/test/integration/docker/deployer/app/.kamal/hooks/post-proxy-reboot b/test/integration/docker/deployer/app/.kamal/hooks/post-proxy-reboot new file mode 100755 index 000000000..fb47d3cc6 --- /dev/null +++ b/test/integration/docker/deployer/app/.kamal/hooks/post-proxy-reboot @@ -0,0 +1,3 @@ +#!/bin/sh +echo "Rebooted kamal-proxy on ${KAMAL_HOSTS}" +mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/post-proxy-reboot diff --git a/test/integration/docker/deployer/app/.kamal/hooks/post-traefik-reboot b/test/integration/docker/deployer/app/.kamal/hooks/post-traefik-reboot deleted file mode 100755 index 598ddb633..000000000 --- a/test/integration/docker/deployer/app/.kamal/hooks/post-traefik-reboot +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -echo "Rebooted Traefik on ${KAMAL_HOSTS}" -mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/post-traefik-reboot diff --git a/test/integration/docker/deployer/app/.kamal/hooks/pre-proxy-reboot b/test/integration/docker/deployer/app/.kamal/hooks/pre-proxy-reboot new file mode 100755 index 000000000..65a70a001 --- /dev/null +++ b/test/integration/docker/deployer/app/.kamal/hooks/pre-proxy-reboot @@ -0,0 +1,3 @@ +#!/bin/sh +echo "Rebooting Traefik on ${KAMAL_HOSTS}..." +mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-proxy-reboot diff --git a/test/integration/docker/deployer/app/.kamal/hooks/pre-traefik-reboot b/test/integration/docker/deployer/app/.kamal/hooks/pre-traefik-reboot deleted file mode 100755 index 81269d246..000000000 --- a/test/integration/docker/deployer/app/.kamal/hooks/pre-traefik-reboot +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -echo "Rebooting Traefik on ${KAMAL_HOSTS}..." -mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-traefik-reboot diff --git a/test/integration/docker/deployer/app/config/deploy.yml b/test/integration/docker/deployer/app/config/deploy.yml index a85412ad2..5e9994026 100644 --- a/test/integration/docker/deployer/app/config/deploy.yml +++ b/test/integration/docker/deployer/app/config/deploy.yml @@ -20,6 +20,8 @@ env: secret: - SECRET_TAG asset_path: /usr/share/nginx/html/versions +proxy: + deploy_timeout: 2s registry: server: registry:4443 @@ -30,14 +32,6 @@ builder: arch: <%= Kamal::Utils.docker_arch %> args: COMMIT_SHA: <%= `git rev-parse HEAD` %> -healthcheck: - cmd: wget -qO- http://localhost > /dev/null || exit 1 - max_attempts: 3 -traefik: - args: - accesslog: true - accesslog.format: json - image: registry:4443/traefik:v2.10 accessories: busybox: service: custom-busybox diff --git a/test/integration/docker/deployer/app_with_roles/.kamal/hooks/post-proxy-reboot b/test/integration/docker/deployer/app_with_roles/.kamal/hooks/post-proxy-reboot new file mode 100755 index 000000000..fb47d3cc6 --- /dev/null +++ b/test/integration/docker/deployer/app_with_roles/.kamal/hooks/post-proxy-reboot @@ -0,0 +1,3 @@ +#!/bin/sh +echo "Rebooted kamal-proxy on ${KAMAL_HOSTS}" +mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/post-proxy-reboot diff --git a/test/integration/docker/deployer/app_with_roles/.kamal/hooks/post-traefik-reboot b/test/integration/docker/deployer/app_with_roles/.kamal/hooks/post-traefik-reboot deleted file mode 100755 index 598ddb633..000000000 --- a/test/integration/docker/deployer/app_with_roles/.kamal/hooks/post-traefik-reboot +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -echo "Rebooted Traefik on ${KAMAL_HOSTS}" -mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/post-traefik-reboot diff --git a/test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-proxy-reboot b/test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-proxy-reboot new file mode 100755 index 000000000..6413b6ef2 --- /dev/null +++ b/test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-proxy-reboot @@ -0,0 +1,3 @@ +#!/bin/sh +echo "Rebooting kamal-proxy on ${KAMAL_HOSTS}..." +mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-proxy-reboot diff --git a/test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-traefik-reboot b/test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-traefik-reboot deleted file mode 100755 index 81269d246..000000000 --- a/test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-traefik-reboot +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -echo "Rebooting Traefik on ${KAMAL_HOSTS}..." -mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-traefik-reboot diff --git a/test/integration/docker/deployer/app_with_roles/config/deploy.yml b/test/integration/docker/deployer/app_with_roles/config/deploy.yml index c15af55b1..835b4ffda 100644 --- a/test/integration/docker/deployer/app_with_roles/config/deploy.yml +++ b/test/integration/docker/deployer/app_with_roles/config/deploy.yml @@ -10,9 +10,6 @@ servers: - vm3 cmd: sleep infinity proxy: - enabled: true - hosts: - - vm2 deploy_timeout: 2s asset_path: /usr/share/nginx/html/versions @@ -26,14 +23,6 @@ builder: arch: <%= Kamal::Utils.docker_arch %> args: COMMIT_SHA: <%= `git rev-parse HEAD` %> -healthcheck: - cmd: wget -qO- http://localhost > /dev/null || exit 1 - max_attempts: 3 -traefik: - args: - accesslog: true - accesslog.format: json - image: registry:4443/traefik:v2.10 accessories: busybox: service: custom-busybox diff --git a/test/integration/docker/deployer/setup.sh b/test/integration/docker/deployer/setup.sh index 0cd511d92..4867519ee 100755 --- a/test/integration/docker/deployer/setup.sh +++ b/test/integration/docker/deployer/setup.sh @@ -19,7 +19,6 @@ push_image_to_registry_4443() { install_kamal push_image_to_registry_4443 nginx 1-alpine-slim -push_image_to_registry_4443 traefik v2.10 push_image_to_registry_4443 busybox 1.36.0 # .ssh is on a shared volume that persists between runs. Clean it up as the diff --git a/test/integration/integration_test.rb b/test/integration/integration_test.rb index fd23e579f..a8e149c1d 100644 --- a/test/integration/integration_test.rb +++ b/test/integration/integration_test.rb @@ -101,8 +101,8 @@ def assert_hooks_ran(*hooks) def assert_200(response) code = response.code if code != "200" - puts "Got response code #{code}, here are the traefik logs:" - kamal :traefik, :logs + puts "Got response code #{code}, here are the proxy logs:" + kamal :proxy, :logs puts "And here are the load balancer logs" docker_compose :logs, :load_balancer puts "Tried to get the response code again and got #{app_response.code}" @@ -129,8 +129,8 @@ def setup_deployer def debug_response_code(app_response, expected_code) code = app_response.code if code != expected_code - puts "Got response code #{code}, here are the traefik logs:" - kamal :traefik, :logs + puts "Got response code #{code}, here are the proxy logs:" + kamal :proxy, :logs puts "And here are the load balancer logs" docker_compose :logs, :load_balancer puts "Tried to get the response code again and got #{app_response.code}" diff --git a/test/integration/main_test.rb b/test/integration/main_test.rb index 1eb05eff8..fbcd09274 100644 --- a/test/integration/main_test.rb +++ b/test/integration/main_test.rb @@ -24,11 +24,11 @@ class MainTest < IntegrationTest assert_app_is_up version: first_version details = kamal :details, capture: true - assert_match /Traefik Host: vm1/, details - assert_match /Traefik Host: vm2/, details + assert_match /Proxy Host: vm1/, details + assert_match /Proxy Host: vm2/, details assert_match /App Host: vm1/, details assert_match /App Host: vm2/, details - assert_match /traefik:v2.10/, details + assert_match /basecamp\/kamal-proxy:latest/, details assert_match /registry:4443\/app:#{first_version}/, details audit = kamal :audit, capture: true @@ -70,7 +70,6 @@ class MainTest < IntegrationTest assert_equal({ user: "root", port: 22, keepalive: true, keepalive_interval: 30, log_level: :fatal }, config[:ssh_options]) assert_equal({ "driver" => "docker", "arch" => "#{Kamal::Utils.docker_arch}", "args" => { "COMMIT_SHA" => version } }, config[:builder]) assert_equal [ "--log-opt", "max-size=\"10m\"" ], config[:logging] - assert_equal({ "cmd"=>"wget -qO- http://localhost > /dev/null || exit 1", "interval"=>"1s", "max_attempts"=>3, "port"=>3000, "path"=>"/up", "cord"=>"/tmp/kamal-cord", "log_lines"=>50 }, config[:healthcheck]) end test "aliases" do diff --git a/test/integration/proxy_test.rb b/test/integration/proxy_test.rb index f5698592b..752ec41b7 100644 --- a/test/integration/proxy_test.rb +++ b/test/integration/proxy_test.rb @@ -11,50 +11,43 @@ class ProxyTest < IntegrationTest output = kamal :proxy, :reboot, "-y", "--verbose", capture: true assert_proxy_running - assert_hooks_ran "pre-traefik-reboot", "post-traefik-reboot" - assert_match /Rebooting Traefik on vm1,vm2.../, output - assert_match /Rebooted Traefik on vm1,vm2/, output + assert_hooks_ran "pre-proxy-reboot", "post-proxy-reboot" + assert_match /Rebooting kamal-proxy on vm1,vm2.../, output + assert_match /Rebooted kamal-proxy on vm1,vm2/, output output = kamal :proxy, :reboot, "--rolling", "-y", "--verbose", capture: true assert_proxy_running - assert_hooks_ran "pre-traefik-reboot", "post-traefik-reboot" - assert_match /Rebooting Traefik on vm1.../, output - assert_match /Rebooted Traefik on vm1/, output - assert_match /Rebooting Traefik on vm2.../, output - assert_match /Rebooted Traefik on vm2/, output + assert_hooks_ran "pre-proxy-reboot", "post-proxy-reboot" + assert_match /Rebooting kamal-proxy on vm1.../, output + assert_match /Rebooted kamal-proxy on vm1/, output + assert_match /Rebooting kamal-proxy on vm2.../, output + assert_match /Rebooted kamal-proxy on vm2/, output kamal :proxy, :boot assert_proxy_running - assert_traefik_running # Check booting when booted doesn't raise an error kamal :proxy, :stop assert_proxy_not_running - assert_traefik_not_running # Check booting when stopped works kamal :proxy, :boot assert_proxy_running - assert_traefik_running kamal :proxy, :stop assert_proxy_not_running - assert_traefik_not_running kamal :proxy, :start assert_proxy_running - assert_traefik_running kamal :proxy, :restart assert_proxy_running - assert_traefik_running logs = kamal :proxy, :logs, capture: true - assert_match /Traefik version [\d.]+ built on/, logs + assert_match /No previous state to restore/, logs kamal :proxy, :remove assert_proxy_not_running - assert_traefik_not_running kamal :env, :delete end @@ -68,14 +61,6 @@ def assert_proxy_not_running assert_no_match /basecamp\/kamal-proxy:latest \"kamal-proxy run\"/, proxy_details end - def assert_traefik_running - assert_match /traefik:v2.10 "\/entrypoint.sh/, proxy_details - end - - def assert_traefik_not_running - assert_no_match /traefik:v2.10 "\/entrypoint.sh/, proxy_details - end - def proxy_details kamal :proxy, :details, capture: true end diff --git a/test/integration/traefik_test.rb b/test/integration/traefik_test.rb deleted file mode 100644 index 48f9ea024..000000000 --- a/test/integration/traefik_test.rb +++ /dev/null @@ -1,61 +0,0 @@ -require_relative "integration_test" - -class TraefikTest < IntegrationTest - test "boot, reboot, stop, start, restart, logs, remove" do - kamal :traefik, :boot - assert_traefik_running - - output = kamal :traefik, :reboot, "-y", "--verbose", capture: true - assert_traefik_running - assert_hooks_ran "pre-traefik-reboot", "post-traefik-reboot" - assert_match /Rebooting Traefik on vm1,vm2.../, output - assert_match /Rebooted Traefik on vm1,vm2/, output - - output = kamal :traefik, :reboot, "--rolling", "-y", "--verbose", capture: true - assert_traefik_running - assert_hooks_ran "pre-traefik-reboot", "post-traefik-reboot" - assert_match /Rebooting Traefik on vm1.../, output - assert_match /Rebooted Traefik on vm1/, output - assert_match /Rebooting Traefik on vm2.../, output - assert_match /Rebooted Traefik on vm2/, output - - kamal :traefik, :boot - assert_traefik_running - - # Check booting when booted doesn't raise an error - kamal :traefik, :stop - assert_traefik_not_running - - # Check booting when stopped works - kamal :traefik, :boot - assert_traefik_running - - kamal :traefik, :stop - assert_traefik_not_running - - kamal :traefik, :start - assert_traefik_running - - kamal :traefik, :restart - assert_traefik_running - - logs = kamal :traefik, :logs, capture: true - assert_match /Traefik version [\d.]+ built on/, logs - - kamal :traefik, :remove - assert_traefik_not_running - end - - private - def assert_traefik_running - assert_match /traefik:v2.10 "\/entrypoint.sh/, traefik_details - end - - def assert_traefik_not_running - assert_no_match /traefik:v2.10 "\/entrypoint.sh/, traefik_details - end - - def traefik_details - kamal :traefik, :details, capture: true - end -end From 2125327d545d0a95e53d6675c9d784b6c06a6385 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 12 Sep 2024 12:32:32 +0100 Subject: [PATCH 21/43] proxy/host -> proxy/hosts --- lib/kamal/configuration/docs/proxy.yml | 13 +++++++------ lib/kamal/configuration/proxy.rb | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/kamal/configuration/docs/proxy.yml b/lib/kamal/configuration/docs/proxy.yml index 82d009780..0ca6befd9 100644 --- a/lib/kamal/configuration/docs/proxy.yml +++ b/lib/kamal/configuration/docs/proxy.yml @@ -7,15 +7,16 @@ # run on the same proxy. proxy: - # Host + # Hosts # - # This is the host that will be used to serve the app. By setting this you can run - # multiple apps on the same server sharing the same instance of the proxy. + # These are the hosts that will be used to serve the app. If you deploy more + # than one application to a single host, the proxy will route requests based + # these hosts # - # If this is set only requests that match this host will be forwarded by the proxy. - # if this is not set, then all requests will be forwarded, except for matching + # If no hosts are set, then all requests will be forwarded, except for matching # requests for other apps that do have a host set. - host: foo.example.com + hosts: + - foo.example.com # App port # diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb index 4232e8506..a294208da 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -36,7 +36,7 @@ def ssl? def deploy_options { - host: proxy_config["host"], + host: proxy_config.fetch("hosts", []).first, tls: proxy_config["ssl"], "deploy-timeout": proxy_config["deploy_timeout"], "drain-timeout": proxy_config["drain_timeout"], From ccb742419776aa15617428672eb8c32ac58fbca0 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 12 Sep 2024 12:57:45 +0100 Subject: [PATCH 22/43] Remove stray exit! --- test/integration/app_test.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/test/integration/app_test.rb b/test/integration/app_test.rb index 60e1cf6e9..b7dcdc343 100644 --- a/test/integration/app_test.rb +++ b/test/integration/app_test.rb @@ -8,7 +8,6 @@ class AppTest < IntegrationTest kamal :app, :stop - exit! assert_app_is_down kamal :app, :start From a40b644145b7fe484b00bd2314b148df9abc7f3c Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 12 Sep 2024 12:59:34 +0100 Subject: [PATCH 23/43] Check that there's no traefik hooks left behind --- lib/kamal/configuration.rb | 11 +++++++++++ lib/kamal/configuration/validator/proxy.rb | 2 +- test/configuration/proxy_test.rb | 2 +- test/configuration_test.rb | 14 ++++++++++++++ 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index 38dc21905..96784e646 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -70,6 +70,7 @@ def initialize(raw_config, destination: nil, version: nil, validate: true) ensure_valid_kamal_version ensure_retain_containers_valid ensure_valid_service_name + ensure_no_traefik_reboot_hooks end @@ -303,6 +304,16 @@ def ensure_retain_containers_valid true end + def ensure_no_traefik_reboot_hooks + hooks = %w[ pre-traefik-reboot post-traefik-reboot ].select { |hook_file| File.exist?(File.join(hooks_path, hook_file)) } + + if hooks.any? + raise Kamal::ConfigurationError, "Found #{hooks.join(", ")}, these should be renamed to (pre|post)-proxy-reboot" + end + + true + end + def role_names raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort diff --git a/lib/kamal/configuration/validator/proxy.rb b/lib/kamal/configuration/validator/proxy.rb index a4ee19bf5..2b0555706 100644 --- a/lib/kamal/configuration/validator/proxy.rb +++ b/lib/kamal/configuration/validator/proxy.rb @@ -2,7 +2,7 @@ class Kamal::Configuration::Validator::Proxy < Kamal::Configuration::Validator def validate! super - if config["host"].blank? && config["ssl"] + if config["hosts"].blank? && config["ssl"] error "Must set a host to enable automatic SSL" end end diff --git a/test/configuration/proxy_test.rb b/test/configuration/proxy_test.rb index 3aa3f85e3..6dac27ff3 100644 --- a/test/configuration/proxy_test.rb +++ b/test/configuration/proxy_test.rb @@ -9,7 +9,7 @@ class ConfigurationEnvTest < ActiveSupport::TestCase end test "ssl with host" do - @deploy[:proxy] = { "ssl" => true, "host" => "example.com" } + @deploy[:proxy] = { "ssl" => true, "hosts" => [ "example.com" ] } assert_equal true, config.proxy.ssl? end diff --git a/test/configuration_test.rb b/test/configuration_test.rb index c64070eb7..c10b85086 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -342,4 +342,18 @@ class ConfigurationTest < ActiveSupport::TestCase assert_equal config.role(:web_tokyo).running_proxy?, true assert_equal config.role(:web_chicago).running_proxy?, true end + + test "traefik hooks raise error" do + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + FileUtils.mkdir_p ".kamal/hooks" + FileUtils.touch ".kamal/hooks/post-traefik-reboot" + FileUtils.touch ".kamal/hooks/pre-traefik-reboot" + exception = assert_raises(Kamal::ConfigurationError) do + Kamal::Configuration.new(@deploy) + end + assert_equal "Found pre-traefik-reboot, post-traefik-reboot, these should be renamed to (pre|post)-proxy-reboot", exception.message + end + end + end end From e1016b246910aac411d4d346de40b858123dab86 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 12 Sep 2024 13:06:35 +0100 Subject: [PATCH 24/43] No need to wait_for_healthy --- lib/kamal/cli/healthcheck/poller.rb | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/lib/kamal/cli/healthcheck/poller.rb b/lib/kamal/cli/healthcheck/poller.rb index 0643c1573..00646b623 100644 --- a/lib/kamal/cli/healthcheck/poller.rb +++ b/lib/kamal/cli/healthcheck/poller.rb @@ -31,31 +31,6 @@ def wait_for_healthy(pause_after_ready: false, &block) info "Container is healthy!" end - def wait_for_unhealthy(pause_after_ready: false, &block) - attempt = 1 - max_attempts = 7 - - begin - case status = block.call - when "unhealthy" - sleep TRAEFIK_UPDATE_DELAY if pause_after_ready - else - raise Kamal::Cli::Healthcheck::Error, "container not unhealthy (#{status})" - end - rescue Kamal::Cli::Healthcheck::Error => e - if attempt <= max_attempts - info "#{e.message}, retrying in #{attempt}s (attempt #{attempt}/#{max_attempts})..." - sleep attempt - attempt += 1 - retry - else - raise - end - end - - info "Container is unhealthy!" - end - private def info(message) SSHKit.config.output.info(message) From 33834a266aa700e2319512a274759160db20034b Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 12 Sep 2024 13:08:31 +0100 Subject: [PATCH 25/43] Drop sleep after container healthy --- lib/kamal/cli/healthcheck/poller.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/kamal/cli/healthcheck/poller.rb b/lib/kamal/cli/healthcheck/poller.rb index 00646b623..fd8714ce5 100644 --- a/lib/kamal/cli/healthcheck/poller.rb +++ b/lib/kamal/cli/healthcheck/poller.rb @@ -1,9 +1,6 @@ module Kamal::Cli::Healthcheck::Poller extend self - TRAEFIK_UPDATE_DELAY = 5 - - def wait_for_healthy(pause_after_ready: false, &block) attempt = 1 max_attempts = 7 @@ -11,7 +8,6 @@ def wait_for_healthy(pause_after_ready: false, &block) begin case status = block.call when "healthy" - sleep TRAEFIK_UPDATE_DELAY if pause_after_ready when "running" # No health check configured sleep KAMAL.config.readiness_delay if pause_after_ready else From 109339189aa79be82fe2452ffca0afc14e7a2a20 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 12 Sep 2024 14:18:13 +0100 Subject: [PATCH 26/43] Fix up integration app_test.rb --- test/integration/app_test.rb | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/test/integration/app_test.rb b/test/integration/app_test.rb index b7dcdc343..121a96a51 100644 --- a/test/integration/app_test.rb +++ b/test/integration/app_test.rb @@ -20,30 +20,30 @@ class AppTest < IntegrationTest wait_for_app_to_be_up logs = kamal :app, :logs, capture: true - assert_match /App Host: vm1/, logs - assert_match /App Host: vm2/, logs - assert_match /GET \/ HTTP\/1.1/, logs + assert_match "App Host: vm1", logs + assert_match "App Host: vm2", logs + assert_match "GET /version HTTP/1.1", logs images = kamal :app, :images, capture: true - assert_match /App Host: vm1/, images - assert_match /App Host: vm2/, images + assert_match "App Host: vm1", images + assert_match "App Host: vm2", images assert_match /registry:4443\/app\s+#{latest_app_version}/, images assert_match /registry:4443\/app\s+latest/, images containers = kamal :app, :containers, capture: true - assert_match /App Host: vm1/, containers - assert_match /App Host: vm2/, containers - assert_match /registry:4443\/app:#{latest_app_version}/, containers - assert_match /registry:4443\/app:latest/, containers + assert_match "App Host: vm1", containers + assert_match "App Host: vm2", containers + assert_match "registry:4443/app:#{latest_app_version}", containers + assert_match "registry:4443/app:latest", containers exec_output = kamal :app, :exec, :ps, capture: true - assert_match /App Host: vm1/, exec_output - assert_match /App Host: vm2/, exec_output + assert_match "App Host: vm1", exec_output + assert_match "App Host: vm2", exec_output assert_match /1 root 0:\d\d ps/, exec_output exec_output = kamal :app, :exec, "--reuse", :ps, capture: true - assert_match /App Host: vm1/, exec_output - assert_match /App Host: vm2/, exec_output + assert_match "App Host: vm2", exec_output + assert_match "App Host: vm1", exec_output assert_match /1 root 0:\d\d nginx/, exec_output kamal :app, :remove From cb73c730f91390bcefaad14536e9f021e5b3a807 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 12 Sep 2024 14:31:21 +0100 Subject: [PATCH 27/43] No need for run_id --- lib/kamal/configuration.rb | 4 ---- test/commands/app_test.rb | 1 - test/configuration_test.rb | 5 ----- 3 files changed, 10 deletions(-) diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index 96784e646..76e6d9ee6 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -189,10 +189,6 @@ def readiness_delay raw_config.readiness_delay || 7 end - def run_id - @run_id ||= SecureRandom.hex(16) - end - def run_directory ".kamal" diff --git a/test/commands/app_test.rb b/test/commands/app_test.rb index 699319016..9c1593651 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -3,7 +3,6 @@ class CommandsAppTest < ActiveSupport::TestCase setup do setup_test_secrets("secrets" => "RAILS_MASTER_KEY=456") - Kamal::Configuration.any_instance.stubs(:run_id).returns("12345678901234567890123456789012") @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], env: { "secret" => [ "RAILS_MASTER_KEY" ] }, builder: { "arch" => "amd64" } } end diff --git a/test/configuration_test.rb b/test/configuration_test.rb index c10b85086..1df3fc3b9 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -296,11 +296,6 @@ class ConfigurationTest < ActiveSupport::TestCase assert_equal "$(pwd)/.kamal", config.run_directory_as_docker_volume end - test "run id" do - SecureRandom.expects(:hex).with(16).returns("09876543211234567890098765432112") - assert_equal "09876543211234567890098765432112", @config.run_id - end - test "asset path" do assert_nil @config.asset_path assert_equal "foo", Kamal::Configuration.new(@deploy.merge!(asset_path: "foo")).asset_path From c21757f747260027ed4eb0931d9690fe6b43f2ef Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 12 Sep 2024 15:40:32 +0100 Subject: [PATCH 28/43] Move all files on the host under a common directory This will make running kamal remove simpler, we can just clean up that directory. --- lib/kamal/cli/base.rb | 6 ++-- lib/kamal/cli/lock.rb | 2 +- lib/kamal/cli/server.rb | 2 -- lib/kamal/commands/app/assets.rb | 14 ++++---- lib/kamal/commands/auditor.rb | 6 ++-- lib/kamal/commands/server.rb | 4 +-- lib/kamal/configuration.rb | 17 +++++++--- lib/kamal/configuration/accessory.rb | 2 +- lib/kamal/configuration/role.rb | 14 ++++---- test/cli/accessory_test.rb | 16 +++++----- test/cli/app_test.rb | 14 ++++---- test/cli/cli_test_case.rb | 2 +- test/cli/main_test.rb | 6 ++-- test/cli/server_test.rb | 6 ++-- test/commands/accessory_test.rb | 12 +++---- test/commands/app_test.rb | 48 ++++++++++++++-------------- test/commands/auditor_test.rb | 16 +++++----- test/commands/server_test.rb | 4 +-- test/configuration/accessory_test.rb | 4 +-- test/configuration/role_test.rb | 24 +++++++------- 20 files changed, 112 insertions(+), 107 deletions(-) diff --git a/lib/kamal/cli/base.rb b/lib/kamal/cli/base.rb index 85815506b..37a9e0462 100644 --- a/lib/kamal/cli/base.rb +++ b/lib/kamal/cli/base.rb @@ -101,7 +101,7 @@ def confirming(question) end def acquire_lock - ensure_run_and_locks_directory + ensure_service_and_locks_directory raise_if_locked do say "Acquiring the deploy lock...", :magenta @@ -174,9 +174,9 @@ def first_invocation instance_variable_get("@_invocations").first end - def ensure_run_and_locks_directory + def ensure_service_and_locks_directory on(KAMAL.hosts) do - execute(*KAMAL.server.ensure_run_directory) + execute(*KAMAL.server.ensure_service_directory) end on(KAMAL.primary_host) do diff --git a/lib/kamal/cli/lock.rb b/lib/kamal/cli/lock.rb index 306c8a078..abbea71fb 100644 --- a/lib/kamal/cli/lock.rb +++ b/lib/kamal/cli/lock.rb @@ -12,7 +12,7 @@ def status option :message, aliases: "-m", type: :string, desc: "A lock message", required: true def acquire message = options[:message] - ensure_run_and_locks_directory + ensure_service_and_locks_directory raise_if_locked do on(KAMAL.primary_host) do diff --git a/lib/kamal/cli/server.rb b/lib/kamal/cli/server.rb index 5b1b0cc7e..452ab089e 100644 --- a/lib/kamal/cli/server.rb +++ b/lib/kamal/cli/server.rb @@ -36,8 +36,6 @@ def bootstrap missing << host end end - - execute(*KAMAL.server.ensure_run_directory) end if missing.any? diff --git a/lib/kamal/commands/app/assets.rb b/lib/kamal/commands/app/assets.rb index 9841f4fbb..c1e65d188 100644 --- a/lib/kamal/commands/app/assets.rb +++ b/lib/kamal/commands/app/assets.rb @@ -3,18 +3,18 @@ def extract_assets asset_container = "#{role.container_prefix}-assets" combine \ - make_directory(role.asset_extracted_path), + make_directory(role.asset_extracted_directory), [ *docker(:stop, "-t 1", asset_container, "2> /dev/null"), "|| true" ], docker(:run, "--name", asset_container, "--detach", "--rm", "--entrypoint", "sleep", config.absolute_image, "1000000"), - docker(:cp, "-L", "#{asset_container}:#{role.asset_path}/.", role.asset_extracted_path), + docker(:cp, "-L", "#{asset_container}:#{role.asset_path}/.", role.asset_extracted_directory), docker(:stop, "-t 1", asset_container), by: "&&" end def sync_asset_volumes(old_version: nil) - new_extracted_path, new_volume_path = role.asset_extracted_path(config.version), role.asset_volume.host_path + new_extracted_path, new_volume_path = role.asset_extracted_directory(config.version), role.asset_volume.host_path if old_version.present? - old_extracted_path, old_volume_path = role.asset_extracted_path(old_version), role.asset_volume(old_version).host_path + old_extracted_path, old_volume_path = role.asset_extracted_directory(old_version), role.asset_volume(old_version).host_path end commands = [ make_directory(new_volume_path), copy_contents(new_extracted_path, new_volume_path) ] @@ -29,8 +29,8 @@ def sync_asset_volumes(old_version: nil) def clean_up_assets chain \ - find_and_remove_older_siblings(role.asset_extracted_path), - find_and_remove_older_siblings(role.asset_volume_path) + find_and_remove_older_siblings(role.asset_extracted_directory), + find_and_remove_older_siblings(role.asset_volume_directory) end private @@ -39,7 +39,7 @@ def find_and_remove_older_siblings(path) :find, Pathname.new(path).dirname.to_s, "-maxdepth 1", - "-name", "'#{role.container_prefix}-*'", + "-name", "'#{role.name}-*'", "!", "-name", Pathname.new(path).basename.to_s, "-exec rm -rf \"{}\" +" ] diff --git a/lib/kamal/commands/auditor.rb b/lib/kamal/commands/auditor.rb index 9846d8e2e..3589643d9 100644 --- a/lib/kamal/commands/auditor.rb +++ b/lib/kamal/commands/auditor.rb @@ -9,7 +9,7 @@ def initialize(config, **details) # Runs remotely def record(line, **details) combine \ - [ :mkdir, "-p", config.run_directory ], + [ :mkdir, "-p", config.service_directory ], append( [ :echo, audit_tags(**details).except(:version, :service_version, :service).to_s, line ], audit_log_file @@ -22,9 +22,7 @@ def reveal private def audit_log_file - file = [ config.service, config.destination, "audit.log" ].compact.join("-") - - File.join(config.run_directory, file) + File.join config.service_directory, "audit.log" end def audit_tags(**details) diff --git a/lib/kamal/commands/server.rb b/lib/kamal/commands/server.rb index fb781fe8e..3bcb5eff6 100644 --- a/lib/kamal/commands/server.rb +++ b/lib/kamal/commands/server.rb @@ -1,5 +1,5 @@ class Kamal::Commands::Server < Kamal::Commands::Base - def ensure_run_directory - [ :mkdir, "-p", config.run_directory ] + def ensure_service_directory + [ :mkdir, "-p", config.service_directory ] end end diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index 76e6d9ee6..9d88bb66f 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -194,6 +194,19 @@ def run_directory ".kamal" end + def service_directory + File.join run_directory, "apps", [ service, destination ].compact.join("-") + end + + def env_directory + File.join service_directory, "env" + end + + def assets_directory + File.join service_directory, "assets" + end + + def run_directory_as_docker_volume File.join "$(pwd)", run_directory end @@ -207,10 +220,6 @@ def asset_path end - def env_directory - File.join(run_directory, "env") - end - def env_tags @env_tags ||= if (tags = raw_config.env["tags"]) tags.collect { |name, config| Env::Tag.new(name, config: config, secrets: secrets) } diff --git a/lib/kamal/configuration/accessory.rb b/lib/kamal/configuration/accessory.rb index 57489f17e..804a15029 100644 --- a/lib/kamal/configuration/accessory.rb +++ b/lib/kamal/configuration/accessory.rb @@ -63,7 +63,7 @@ def secrets_io end def secrets_path - File.join(config.env_directory, "accessories", "#{service_name}.env") + File.join(config.env_directory, "accessories", "#{name}.env") end def files diff --git a/lib/kamal/configuration/role.rb b/lib/kamal/configuration/role.rb index 7e5768ec2..3f1e6d74b 100644 --- a/lib/kamal/configuration/role.rb +++ b/lib/kamal/configuration/role.rb @@ -84,7 +84,7 @@ def secrets_io(host) end def secrets_path - File.join(config.env_directory, "roles", "#{container_prefix}.env") + File.join(config.env_directory, "roles", "#{name}.env") end def asset_volume_args @@ -122,19 +122,19 @@ def assets? asset_path.present? && running_proxy? end - def asset_volume(version = nil) + def asset_volume(version = config.version) if assets? Kamal::Configuration::Volume.new \ - host_path: asset_volume_path(version), container_path: asset_path + host_path: asset_volume_directory(version), container_path: asset_path end end - def asset_extracted_path(version = nil) - File.join config.run_directory, "assets", "extracted", container_name(version) + def asset_extracted_directory(version = config.version) + File.join config.assets_directory, "extracted", [ name, version ].join("-") end - def asset_volume_path(version = nil) - File.join config.run_directory, "assets", "volumes", container_name(version) + def asset_volume_directory(version = config.version) + File.join config.assets_directory, "volumes", [ name, version ].join("-") end private diff --git a/test/cli/accessory_test.rb b/test/cli/accessory_test.rb index c9016f56d..5bb8762a8 100644 --- a/test/cli/accessory_test.rb +++ b/test/cli/accessory_test.rb @@ -15,7 +15,7 @@ class CliAccessoryTest < CliTestCase run_command("boot", "mysql").tap do |output| assert_match /docker login.*on 1.1.1.3/, output - assert_match "docker run --name app-mysql --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/env/accessories/app-mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output + assert_match "docker run --name app-mysql --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/apps/app/env/accessories/mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output end end @@ -32,9 +32,9 @@ class CliAccessoryTest < CliTestCase assert_match /docker network create kamal.*on 1.1.1.1/, output assert_match /docker network create kamal.*on 1.1.1.2/, output assert_match /docker network create kamal.*on 1.1.1.3/, output - assert_match "docker run --name app-mysql --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/env/accessories/app-mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output - assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output - assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", output + assert_match "docker run --name app-mysql --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/apps/app/env/accessories/mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output + assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/apps/app/env/accessories/redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output + assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/apps/app/env/accessories/redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", output end end @@ -203,8 +203,8 @@ class CliAccessoryTest < CliTestCase run_command("boot", "redis", "--hosts", "1.1.1.1").tap do |output| assert_match /docker login.*on 1.1.1.1/, output assert_no_match /docker login.*on 1.1.1.2/, output - assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output - assert_no_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", output + assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/apps/app/env/accessories/redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output + assert_no_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/apps/app/env/accessories/redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", output end end @@ -215,8 +215,8 @@ class CliAccessoryTest < CliTestCase run_command("boot", "redis", "--hosts", "1.1.1.1,1.1.1.3").tap do |output| assert_match /docker login.*on 1.1.1.1/, output assert_no_match /docker login.*on 1.1.1.3/, output - assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output - assert_no_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.3", output + assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/apps/app/env/accessories/redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output + assert_no_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/apps/app/env/accessories/redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.3", output end end diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index 149fa3f03..6c37d6206 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -72,11 +72,11 @@ class CliAppTest < CliTestCase run_command("boot", config: :with_assets).tap do |output| assert_match "docker tag dhh/app:latest dhh/app:latest", output - assert_match "/usr/bin/env mkdir -p .kamal/assets/volumes/app-web-latest ; cp -rnT .kamal/assets/extracted/app-web-latest .kamal/assets/volumes/app-web-latest ; cp -rnT .kamal/assets/extracted/app-web-latest .kamal/assets/volumes/app-web-123 || true ; cp -rnT .kamal/assets/extracted/app-web-123 .kamal/assets/volumes/app-web-latest || true", output - assert_match "/usr/bin/env mkdir -p .kamal/assets/extracted/app-web-latest && docker stop -t 1 app-web-assets 2> /dev/null || true && docker run --name app-web-assets --detach --rm --entrypoint sleep dhh/app:latest 1000000 && docker cp -L app-web-assets:/public/assets/. .kamal/assets/extracted/app-web-latest && docker stop -t 1 app-web-assets", output + assert_match "/usr/bin/env mkdir -p .kamal/apps/app/assets/volumes/web-latest ; cp -rnT .kamal/apps/app/assets/extracted/web-latest .kamal/apps/app/assets/volumes/web-latest ; cp -rnT .kamal/apps/app/assets/extracted/web-latest .kamal/apps/app/assets/volumes/web-123 || true ; cp -rnT .kamal/apps/app/assets/extracted/web-123 .kamal/apps/app/assets/volumes/web-latest || true", output + assert_match "/usr/bin/env mkdir -p .kamal/apps/app/assets/extracted/web-latest && docker stop -t 1 app-web-assets 2> /dev/null || true && docker run --name app-web-assets --detach --rm --entrypoint sleep dhh/app:latest 1000000 && docker cp -L app-web-assets:/public/assets/. .kamal/apps/app/assets/extracted/web-latest && docker stop -t 1 app-web-assets", output assert_match /docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-[0-9a-f]{12} /, output assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output - assert_match "/usr/bin/env find .kamal/assets/extracted -maxdepth 1 -name 'app-web-*' ! -name app-web-latest -exec rm -rf \"{}\" + ; find .kamal/assets/volumes -maxdepth 1 -name 'app-web-*' ! -name app-web-latest -exec rm -rf \"{}\" +", output + assert_match "/usr/bin/env find .kamal/apps/app/assets/extracted -maxdepth 1 -name 'web-*' ! -name web-latest -exec rm -rf \"{}\" + ; find .kamal/apps/app/assets/volumes -maxdepth 1 -name 'web-*' ! -name web-latest -exec rm -rf \"{}\" +", output end end @@ -222,13 +222,13 @@ class CliAppTest < CliTestCase test "exec" do run_command("exec", "ruby -v").tap do |output| - assert_match "docker run --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v", output + assert_match "docker run --rm --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v", output end end test "exec separate arguments" do run_command("exec", "ruby", " -v").tap do |output| - assert_match "docker run --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v", output + assert_match "docker run --rm --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v", output end end @@ -241,7 +241,7 @@ class CliAppTest < CliTestCase test "exec interactive" do SSHKit::Backend::Abstract.any_instance.expects(:exec) - .with("ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v'") + .with("ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v'") run_command("exec", "-i", "ruby -v").tap do |output| assert_match "Get most recent version available as an image...", output assert_match "Launching interactive command with version latest via SSH from new container on 1.1.1.1...", output @@ -341,7 +341,7 @@ class CliAppTest < CliTestCase run_command("boot", config: :with_proxy).tap do |output| assert_match /Renaming container .* to .* as already deployed on 1.1.1.1/, output # Rename assert_match /docker rename app-web-latest app-web-latest_replaced_[0-9a-f]{16}/, output - assert_match /docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-[0-9a-f]{12} -e KAMAL_CONTAINER_NAME="app-web-latest" -e KAMAL_VERSION="latest" --env-file .kamal\/env\/roles\/app-web.env --log-opt max-size="10m" --label service="app" --label role="web" --label destination dhh\/app:latest/, output + assert_match /docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-[0-9a-f]{12} -e KAMAL_CONTAINER_NAME="app-web-latest" -e KAMAL_VERSION="latest" --env-file .kamal\/apps\/app\/env\/roles\/web.env --log-opt max-size="10m" --label service="app" --label role="web" --label destination dhh\/app:latest/, output assert_match /docker exec kamal-proxy kamal-proxy deploy app-web --target "123:80"/, output assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output end diff --git a/test/cli/cli_test_case.rb b/test/cli/cli_test_case.rb index 3f3e9294e..2ffebd9cb 100644 --- a/test/cli/cli_test_case.rb +++ b/test/cli/cli_test_case.rb @@ -29,7 +29,7 @@ def fail_hook(hook) def stub_setup SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |*args| args == [ :mkdir, "-p", ".kamal" ] } + .with { |*args| args == [ :mkdir, "-p", ".kamal/apps/app" ] } SSHKit::Backend::Abstract.any_instance.stubs(:execute) .with { |arg1, arg2, arg3| arg1 == :mkdir && arg2 == "-p" && arg3 == ".kamal/locks" } SSHKit::Backend::Abstract.any_instance.stubs(:execute) diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index f82eef3e2..6600765ba 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -97,7 +97,7 @@ class CliMainTest < CliTestCase Dir.stubs(:chdir) SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |*args| args == [ :mkdir, "-p", ".kamal" ] } + .with { |*args| args == [ :mkdir, "-p", ".kamal/apps/app" ] } SSHKit::Backend::Abstract.any_instance.stubs(:execute) .with { |*args| args == [ :mkdir, "-p", ".kamal/locks" ] } @@ -134,7 +134,7 @@ class CliMainTest < CliTestCase Dir.stubs(:chdir) SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |*args| args == [ :mkdir, "-p", ".kamal" ] } + .with { |*args| args == [ :mkdir, "-p", ".kamal/apps/app" ] } SSHKit::Backend::Abstract.any_instance.stubs(:execute) .with { |*args| args == [ :mkdir, "-p", ".kamal/locks" ] } @@ -305,7 +305,7 @@ class CliMainTest < CliTestCase test "audit" do run_command("audit").tap do |output| - assert_match %r{tail -n 50 \.kamal/app-audit.log on 1.1.1.1}, output + assert_match %r{tail -n 50 \.kamal/apps/app/audit.log on 1.1.1.1}, output assert_match /App Host: 1.1.1.1/, output end end diff --git a/test/cli/server_test.rb b/test/cli/server_test.rb index 110e217dd..0436a3eef 100644 --- a/test/cli/server_test.rb +++ b/test/cli/server_test.rb @@ -32,7 +32,7 @@ class CliServerTest < CliTestCase test "bootstrap already installed" do stub_setup SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(true).at_least_once - SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:mkdir, "-p", ".kamal").returns("").at_least_once + SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:mkdir, "-p", ".kamal/apps/app").returns("").at_least_once assert_equal "Acquiring the deploy lock...\nReleasing the deploy lock...", run_command("bootstrap") end @@ -41,7 +41,7 @@ class CliServerTest < CliTestCase stub_setup SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(false).at_least_once SSHKit::Backend::Abstract.any_instance.expects(:execute).with('[ "${EUID:-$(id -u)}" -eq 0 ] || command -v sudo >/dev/null || command -v su >/dev/null', raise_on_non_zero_exit: false).returns(false).at_least_once - SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:mkdir, "-p", ".kamal").returns("").at_least_once + SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:mkdir, "-p", ".kamal/apps/app").returns("").at_least_once assert_raise RuntimeError, "Docker is not installed on 1.1.1.1, 1.1.1.3, 1.1.1.4, 1.1.1.2 and can't be automatically installed without having root access and the `curl` command available. Install Docker manually: https://docs.docker.com/engine/install/" do run_command("bootstrap") @@ -53,7 +53,7 @@ class CliServerTest < CliTestCase SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(false).at_least_once SSHKit::Backend::Abstract.any_instance.expects(:execute).with('[ "${EUID:-$(id -u)}" -eq 0 ] || command -v sudo >/dev/null || command -v su >/dev/null', raise_on_non_zero_exit: false).returns(true).at_least_once SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:sh, "-c", "'curl -fsSL https://get.docker.com || wget -O - https://get.docker.com || echo \"exit 1\"'", "|", :sh).at_least_once - SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:mkdir, "-p", ".kamal").returns("").at_least_once + SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:mkdir, "-p", ".kamal/apps/app").returns("").at_least_once Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true) SSHKit::Backend::Abstract.any_instance.expects(:execute).with(".kamal/hooks/pre-connect", anything).at_least_once SSHKit::Backend::Abstract.any_instance.expects(:execute).with(".kamal/hooks/docker-setup", anything).at_least_once diff --git a/test/commands/accessory_test.rb b/test/commands/accessory_test.rb index bc3df9ce5..19fe745d8 100644 --- a/test/commands/accessory_test.rb +++ b/test/commands/accessory_test.rb @@ -51,15 +51,15 @@ class CommandsAccessoryTest < ActiveSupport::TestCase test "run" do assert_equal \ - "docker run --name app-mysql --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/env/accessories/app-mysql.env --label service=\"app-mysql\" private.registry/mysql:8.0", + "docker run --name app-mysql --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/apps/app/env/accessories/mysql.env --label service=\"app-mysql\" private.registry/mysql:8.0", new_command(:mysql).run.join(" ") assert_equal \ - "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env SOMETHING=\"else\" --env-file .kamal/env/accessories/app-redis.env --volume /var/lib/redis:/data --label service=\"app-redis\" --label cache=\"true\" redis:latest", + "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env SOMETHING=\"else\" --env-file .kamal/apps/app/env/accessories/redis.env --volume /var/lib/redis:/data --label service=\"app-redis\" --label cache=\"true\" redis:latest", new_command(:redis).run.join(" ") assert_equal \ - "docker run --name custom-busybox --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --env-file .kamal/env/accessories/custom-busybox.env --label service=\"custom-busybox\" busybox:latest", + "docker run --name custom-busybox --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --env-file .kamal/apps/app/env/accessories/busybox.env --label service=\"custom-busybox\" busybox:latest", new_command(:busybox).run.join(" ") end @@ -67,7 +67,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase @config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } } assert_equal \ - "docker run --name custom-busybox --detach --restart unless-stopped --network kamal --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --env-file .kamal/env/accessories/custom-busybox.env --label service=\"custom-busybox\" busybox:latest", + "docker run --name custom-busybox --detach --restart unless-stopped --network kamal --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --env-file .kamal/apps/app/env/accessories/busybox.env --label service=\"custom-busybox\" busybox:latest", new_command(:busybox).run.join(" ") end @@ -92,7 +92,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase test "execute in new container" do assert_equal \ - "docker run --rm --network kamal --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/env/accessories/app-mysql.env private.registry/mysql:8.0 mysql -u root", + "docker run --rm --network kamal --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/apps/app/env/accessories/mysql.env private.registry/mysql:8.0 mysql -u root", new_command(:mysql).execute_in_new_container("mysql", "-u", "root").join(" ") end @@ -104,7 +104,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase test "execute in new container over ssh" do new_command(:mysql).stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) do - assert_match %r{docker run -it --rm --network kamal --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/env/accessories/app-mysql.env private.registry/mysql:8.0 mysql -u root}, + assert_match %r{docker run -it --rm --network kamal --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/apps/app/env/accessories/mysql.env private.registry/mysql:8.0 mysql -u root}, new_command(:mysql).execute_in_new_container_over_ssh("mysql", "-u", "root") end end diff --git a/test/commands/app_test.rb b/test/commands/app_test.rb index 9c1593651..440626243 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -13,13 +13,13 @@ class CommandsAppTest < ActiveSupport::TestCase test "run" do assert_equal \ - "docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999", + "docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999", new_command.run.join(" ") end test "run with hostname" do assert_equal \ - "docker run --detach --restart unless-stopped --name app-web-999 --network kamal --hostname myhost -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999", + "docker run --detach --restart unless-stopped --name app-web-999 --network kamal --hostname myhost -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999", new_command.run(hostname: "myhost").join(" ") end @@ -27,14 +27,14 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:volumes] = [ "/local/path:/container/path" ] assert_equal \ - "docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --log-opt max-size=\"10m\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label destination dhh/app:999", + "docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label destination dhh/app:999", new_command.run.join(" ") end test "run with custom options" do @config[:servers] = { "web" => [ "1.1.1.1" ], "jobs" => { "hosts" => [ "1.1.1.2" ], "cmd" => "bin/jobs", "options" => { "mount" => "somewhere", "cap-add" => true } } } assert_equal \ - "docker run --detach --restart unless-stopped --name app-jobs-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-jobs-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-jobs.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"jobs\" --label destination --mount \"somewhere\" --cap-add dhh/app:999 bin/jobs", + "docker run --detach --restart unless-stopped --name app-jobs-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-jobs-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/apps/app/env/roles/jobs.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"jobs\" --label destination --mount \"somewhere\" --cap-add dhh/app:999 bin/jobs", new_command(role: "jobs", host: "1.1.1.2").run.join(" ") end @@ -42,7 +42,7 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } } assert_equal \ - "docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999", + "docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/apps/app/env/roles/web.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999", new_command.run.join(" ") end @@ -51,7 +51,7 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "logging" => { "driver" => "local", "options" => { "max-size" => "100m" } } } } assert_equal \ - "docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999", + "docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/apps/app/env/roles/web.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999", new_command.run.join(" ") end @@ -60,7 +60,7 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } } assert_equal \ - "docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env ENV1=\"value1\" --env-file .kamal/env/roles/app-web.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999", + "docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999", new_command.run.join(" ") end @@ -179,13 +179,13 @@ class CommandsAppTest < ActiveSupport::TestCase test "execute in new container" do assert_equal \ - "docker run --rm --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails db:setup", + "docker run --rm --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails db:setup", new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ") end test "execute in new container with env" do assert_equal \ - "docker run --rm --env-file .kamal/env/roles/app-web.env --env foo=\"bar\" dhh/app:999 bin/rails db:setup", + "docker run --rm --env-file .kamal/apps/app/env/roles/web.env --env foo=\"bar\" dhh/app:999 bin/rails db:setup", new_command.execute_in_new_container("bin/rails", "db:setup", env: { "foo" => "bar" }).join(" ") end @@ -194,14 +194,14 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } } assert_equal \ - "docker run --rm --env ENV1=\"value1\" --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails db:setup", + "docker run --rm --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails db:setup", new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ") end test "execute in new container with custom options" do @config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } } assert_equal \ - "docker run --rm --env-file .kamal/env/roles/app-web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails db:setup", + "docker run --rm --env-file .kamal/apps/app/env/roles/web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails db:setup", new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ") end @@ -218,7 +218,7 @@ class CommandsAppTest < ActiveSupport::TestCase end test "execute in new container over ssh" do - assert_match %r{docker run -it --rm --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails c}, + assert_match %r{docker run -it --rm --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails c}, new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {}) end @@ -226,13 +226,13 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:servers] = [ { "1.1.1.1" => "tag1" } ] @config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } } - assert_equal "ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --env ENV1=\"value1\" --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails c'", + assert_equal "ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails c'", new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {}) end test "execute in new container with custom options over ssh" do @config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } } - assert_match %r{docker run -it --rm --env-file .kamal/env/roles/app-web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c}, + assert_match %r{docker run -it --rm --env-file .kamal/apps/app/env/roles/web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c}, new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {}) end @@ -389,32 +389,32 @@ class CommandsAppTest < ActiveSupport::TestCase test "extract assets" do assert_equal [ - :mkdir, "-p", ".kamal/assets/extracted/app-web-999", "&&", + :mkdir, "-p", ".kamal/apps/app/assets/extracted/web-999", "&&", :docker, :stop, "-t 1", "app-web-assets", "2> /dev/null", "|| true", "&&", :docker, :run, "--name", "app-web-assets", "--detach", "--rm", "--entrypoint", "sleep", "dhh/app:999", "1000000", "&&", - :docker, :cp, "-L", "app-web-assets:/public/assets/.", ".kamal/assets/extracted/app-web-999", "&&", + :docker, :cp, "-L", "app-web-assets:/public/assets/.", ".kamal/apps/app/assets/extracted/web-999", "&&", :docker, :stop, "-t 1", "app-web-assets" ], new_command(asset_path: "/public/assets").extract_assets end test "sync asset volumes" do assert_equal [ - :mkdir, "-p", ".kamal/assets/volumes/app-web-999", ";", - :cp, "-rnT", ".kamal/assets/extracted/app-web-999", ".kamal/assets/volumes/app-web-999" + :mkdir, "-p", ".kamal/apps/app/assets/volumes/web-999", ";", + :cp, "-rnT", ".kamal/apps/app/assets/extracted/web-999", ".kamal/apps/app/assets/volumes/web-999" ], new_command(asset_path: "/public/assets").sync_asset_volumes assert_equal [ - :mkdir, "-p", ".kamal/assets/volumes/app-web-999", ";", - :cp, "-rnT", ".kamal/assets/extracted/app-web-999", ".kamal/assets/volumes/app-web-999", ";", - :cp, "-rnT", ".kamal/assets/extracted/app-web-999", ".kamal/assets/volumes/app-web-998", "|| true", ";", - :cp, "-rnT", ".kamal/assets/extracted/app-web-998", ".kamal/assets/volumes/app-web-999", "|| true" + :mkdir, "-p", ".kamal/apps/app/assets/volumes/web-999", ";", + :cp, "-rnT", ".kamal/apps/app/assets/extracted/web-999", ".kamal/apps/app/assets/volumes/web-999", ";", + :cp, "-rnT", ".kamal/apps/app/assets/extracted/web-999", ".kamal/apps/app/assets/volumes/web-998", "|| true", ";", + :cp, "-rnT", ".kamal/apps/app/assets/extracted/web-998", ".kamal/apps/app/assets/volumes/web-999", "|| true" ], new_command(asset_path: "/public/assets").sync_asset_volumes(old_version: 998) end test "clean up assets" do assert_equal [ - :find, ".kamal/assets/extracted", "-maxdepth 1", "-name", "'app-web-*'", "!", "-name", "app-web-999", "-exec rm -rf \"{}\" +", ";", - :find, ".kamal/assets/volumes", "-maxdepth 1", "-name", "'app-web-*'", "!", "-name", "app-web-999", "-exec rm -rf \"{}\" +" + :find, ".kamal/apps/app/assets/extracted", "-maxdepth 1", "-name", "'web-*'", "!", "-name", "web-999", "-exec rm -rf \"{}\" +", ";", + :find, ".kamal/apps/app/assets/volumes", "-maxdepth 1", "-name", "'web-*'", "!", "-name", "web-999", "-exec rm -rf \"{}\" +" ], new_command(asset_path: "/public/assets").clean_up_assets end diff --git a/test/commands/auditor_test.rb b/test/commands/auditor_test.rb index 2abc8d815..d0f666108 100644 --- a/test/commands/auditor_test.rb +++ b/test/commands/auditor_test.rb @@ -18,22 +18,22 @@ class CommandsAuditorTest < ActiveSupport::TestCase test "record" do assert_equal [ - :mkdir, "-p", ".kamal", "&&", + :mkdir, "-p", ".kamal/apps/app", "&&", :echo, "[#{@recorded_at}] [#{@performer}]", "app removed container", - ">>", ".kamal/app-audit.log" + ">>", ".kamal/apps/app/audit.log" ], @auditor.record("app removed container") end test "record with destination" do new_command(destination: "staging").tap do |auditor| assert_equal [ - :mkdir, "-p", ".kamal", "&&", + :mkdir, "-p", ".kamal/apps/app-staging", "&&", :echo, "[#{@recorded_at}] [#{@performer}] [staging]", "app removed container", - ">>", ".kamal/app-staging-audit.log" + ">>", ".kamal/apps/app-staging/audit.log" ], auditor.record("app removed container") end end @@ -41,22 +41,22 @@ class CommandsAuditorTest < ActiveSupport::TestCase test "record with command details" do new_command(role: "web").tap do |auditor| assert_equal [ - :mkdir, "-p", ".kamal", "&&", + :mkdir, "-p", ".kamal/apps/app", "&&", :echo, "[#{@recorded_at}] [#{@performer}] [web]", "app removed container", - ">>", ".kamal/app-audit.log" + ">>", ".kamal/apps/app/audit.log" ], auditor.record("app removed container") end end test "record with arg details" do assert_equal [ - :mkdir, "-p", ".kamal", "&&", + :mkdir, "-p", ".kamal/apps/app", "&&", :echo, "[#{@recorded_at}] [#{@performer}] [value]", "app removed container", - ">>", ".kamal/app-audit.log" + ">>", ".kamal/apps/app/audit.log" ], @auditor.record("app removed container", detail: "value") end diff --git a/test/commands/server_test.rb b/test/commands/server_test.rb index 648821b4a..46cf43093 100644 --- a/test/commands/server_test.rb +++ b/test/commands/server_test.rb @@ -8,8 +8,8 @@ class CommandsServerTest < ActiveSupport::TestCase } end - test "ensure run directory" do - assert_equal "mkdir -p .kamal", new_command.ensure_run_directory.join(" ") + test "ensure service directory" do + assert_equal "mkdir -p .kamal/apps/app", new_command.ensure_service_directory.join(" ") end private diff --git a/test/configuration/accessory_test.rb b/test/configuration/accessory_test.rb index acfe991fc..2615dab60 100644 --- a/test/configuration/accessory_test.rb +++ b/test/configuration/accessory_test.rb @@ -119,9 +119,9 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase with_test_secrets("secrets" => "MYSQL_ROOT_PASSWORD=secret123") do config = Kamal::Configuration.new(@deploy) - assert_equal [ "--env", "MYSQL_ROOT_HOST=\"%\"", "--env-file", ".kamal/env/accessories/app-mysql.env" ], config.accessory(:mysql).env_args.map(&:to_s) + assert_equal [ "--env", "MYSQL_ROOT_HOST=\"%\"", "--env-file", ".kamal/apps/app/env/accessories/mysql.env" ], config.accessory(:mysql).env_args.map(&:to_s) assert_equal "MYSQL_ROOT_PASSWORD=secret123\n", config.accessory(:mysql).secrets_io.string - assert_equal [ "--env", "SOMETHING=\"else\"", "--env-file", ".kamal/env/accessories/app-redis.env" ], @config.accessory(:redis).env_args + assert_equal [ "--env", "SOMETHING=\"else\"", "--env-file", ".kamal/apps/app/env/accessories/redis.env" ], @config.accessory(:redis).env_args assert_equal "\n", config.accessory(:redis).secrets_io.string end end diff --git a/test/configuration/role_test.rb b/test/configuration/role_test.rb index b9938a777..577190775 100644 --- a/test/configuration/role_test.rb +++ b/test/configuration/role_test.rb @@ -65,7 +65,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase assert_equal "redis://a/b", config_with_roles.role(:workers).env("1.1.1.3").clear["REDIS_URL"] assert_equal \ - [ "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"", "--env-file", ".kamal/env/roles/app-workers.env" ], + [ "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"", "--env-file", ".kamal/apps/app/env/roles/workers.env" ], config_with_roles.role(:workers).env_args("1.1.1.3").map(&:to_s) assert_equal \ @@ -84,7 +84,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase test "env args" do assert_equal \ - [ "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"", "--env-file", ".kamal/env/roles/app-workers.env" ], + [ "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"", "--env-file", ".kamal/apps/app/env/roles/workers.env" ], config_with_roles.role(:workers).env_args("1.1.1.3").map(&:to_s) assert_equal \ @@ -114,7 +114,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase } assert_equal \ - [ "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"", "--env-file", ".kamal/env/roles/app-workers.env" ], + [ "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"", "--env-file", ".kamal/apps/app/env/roles/workers.env" ], config_with_roles.role(:workers).env_args("1.1.1.3").map(&:to_s) assert_equal \ @@ -136,7 +136,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase } assert_equal \ - [ "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"", "--env-file", ".kamal/env/roles/app-workers.env" ], + [ "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"", "--env-file", ".kamal/apps/app/env/roles/workers.env" ], config_with_roles.role(:workers).env_args("1.1.1.3").map(&:to_s) assert_equal \ @@ -157,7 +157,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase } assert_equal \ - [ "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"", "--env-file", ".kamal/env/roles/app-workers.env" ], + [ "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"", "--env-file", ".kamal/apps/app/env/roles/workers.env" ], config_with_roles.role(:workers).env_args("1.1.1.3").map(&:to_s) assert_equal \ @@ -184,7 +184,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase } assert_equal \ - [ "--env", "REDIS_URL=\"redis://c/d\"", "--env-file", ".kamal/env/roles/app-workers.env" ], + [ "--env", "REDIS_URL=\"redis://c/d\"", "--env-file", ".kamal/apps/app/env/roles/workers.env" ], config_with_roles.role(:workers).env_args("1.1.1.3").map(&:to_s) assert_equal \ @@ -207,7 +207,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase }) assert_equal "foo", config_with_assets.role(:web).asset_path assert_equal "foo", config_with_assets.role(:workers).asset_path - assert_equal [ "--volume", "$(pwd)/.kamal/assets/volumes/app-web-12345:foo" ], config_with_assets.role(:web).asset_volume_args + assert_equal [ "--volume", "$(pwd)/.kamal/apps/app/assets/volumes/web-12345:foo" ], config_with_assets.role(:web).asset_volume_args assert_nil config_with_assets.role(:workers).asset_volume_args assert config_with_assets.role(:web).assets? assert_not config_with_assets.role(:workers).assets? @@ -217,7 +217,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase }) assert_equal "bar", config_with_assets.role(:web).asset_path assert_nil config_with_assets.role(:workers).asset_path - assert_equal [ "--volume", "$(pwd)/.kamal/assets/volumes/app-web-12345:bar" ], config_with_assets.role(:web).asset_volume_args + assert_equal [ "--volume", "$(pwd)/.kamal/apps/app/assets/volumes/web-12345:bar" ], config_with_assets.role(:web).asset_volume_args assert_nil config_with_assets.role(:workers).asset_volume_args assert config_with_assets.role(:web).assets? assert_not config_with_assets.role(:workers).assets? @@ -228,16 +228,16 @@ class ConfigurationRoleTest < ActiveSupport::TestCase test "asset extracted path" do ENV["VERSION"] = "12345" - assert_equal ".kamal/assets/extracted/app-web-12345", config_with_roles.role(:web).asset_extracted_path - assert_equal ".kamal/assets/extracted/app-workers-12345", config_with_roles.role(:workers).asset_extracted_path + assert_equal ".kamal/apps/app/assets/extracted/web-12345", config_with_roles.role(:web).asset_extracted_directory + assert_equal ".kamal/apps/app/assets/extracted/workers-12345", config_with_roles.role(:workers).asset_extracted_directory ensure ENV.delete("VERSION") end test "asset volume path" do ENV["VERSION"] = "12345" - assert_equal ".kamal/assets/volumes/app-web-12345", config_with_roles.role(:web).asset_volume_path - assert_equal ".kamal/assets/volumes/app-workers-12345", config_with_roles.role(:workers).asset_volume_path + assert_equal ".kamal/apps/app/assets/volumes/web-12345", config_with_roles.role(:web).asset_volume_directory + assert_equal ".kamal/apps/app/assets/volumes/workers-12345", config_with_roles.role(:workers).asset_volume_directory ensure ENV.delete("VERSION") end From d7d6fa34b030cf27efb9806c0284528f5a256bba Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 12 Sep 2024 15:48:10 +0100 Subject: [PATCH 29/43] Use Volume for kamal proxy config volume --- lib/kamal/commands/proxy.rb | 2 +- lib/kamal/configuration.rb | 4 ---- lib/kamal/configuration/proxy.rb | 6 ++++-- test/configuration_test.rb | 5 ----- 4 files changed, 5 insertions(+), 12 deletions(-) diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb index 8a9535f31..cf01fa194 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -17,7 +17,7 @@ def run "--restart", "unless-stopped", *proxy_config.publish_args, "--volume", "/var/run/docker.sock:/var/run/docker.sock", - "--volume", "#{proxy_config.config_directory_as_docker_volume}:/root/.config/kamal-proxy", + *proxy_config.config_volume.docker_args, *config.logging_args, proxy_config.image end diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index 9d88bb66f..264b83e95 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -207,10 +207,6 @@ def assets_directory end - def run_directory_as_docker_volume - File.join "$(pwd)", run_directory - end - def hooks_path raw_config.hooks_path || ".kamal/hooks" end diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb index a294208da..2d2efdfb9 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -59,8 +59,10 @@ def deploy_command_args optionize deploy_options end - def config_directory_as_docker_volume - File.join config.run_directory_as_docker_volume, "proxy", "config" + def config_volume + Kamal::Configuration::Volume.new \ + host_path: File.join(config.run_directory, "proxy", "config"), + container_path: "/root/.config/kamal-proxy" end private diff --git a/test/configuration_test.rb b/test/configuration_test.rb index 1df3fc3b9..81dbb5acb 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -291,11 +291,6 @@ class ConfigurationTest < ActiveSupport::TestCase assert_equal ".kamal", config.run_directory end - test "run directory as docker volume" do - config = Kamal::Configuration.new(@deploy) - assert_equal "$(pwd)/.kamal", config.run_directory_as_docker_volume - end - test "asset path" do assert_nil @config.asset_path assert_equal "foo", Kamal::Configuration.new(@deploy.merge!(asset_path: "foo")).asset_path From b8972a6833dcbe6f4e842403436b9fbc2e3e456c Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 12 Sep 2024 16:01:49 +0100 Subject: [PATCH 30/43] Remove service directory on kamal remove --- lib/kamal/cli/app.rb | 15 +++++++++++++++ lib/kamal/cli/main.rb | 2 +- lib/kamal/commands/server.rb | 6 +++++- test/cli/main_test.rb | 1 + 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/lib/kamal/cli/app.rb b/lib/kamal/cli/app.rb index 5b0535da2..21a5ffddc 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -231,6 +231,7 @@ def remove stop remove_containers remove_images + remove_service_directory end end @@ -272,6 +273,20 @@ def remove_images end end + desc "remove_service_directory", "Remove the service directory from servers", hide: true + def remove_service_directory + with_lock do + on(KAMAL.hosts) do |host| + roles = KAMAL.roles_on(host) + + roles.each do |role| + execute *KAMAL.auditor.record("Removed #{KAMAL.config.service_directory} on all servers", role: role), verbosity: :debug + execute *KAMAL.server.remove_service_directory + end + end + end + end + desc "version", "Show app version currently running on servers" def version on(KAMAL.hosts) do |host| diff --git a/lib/kamal/cli/main.rb b/lib/kamal/cli/main.rb index c0f5f1a78..833910622 100644 --- a/lib/kamal/cli/main.rb +++ b/lib/kamal/cli/main.rb @@ -181,8 +181,8 @@ def init def remove confirming "This will remove all containers and images. Are you sure?" do with_lock do - invoke "kamal:cli:proxy:remove", [], options.without(:confirmed) invoke "kamal:cli:app:remove", [], options.without(:confirmed) + invoke "kamal:cli:proxy:remove", [], options.without(:confirmed) invoke "kamal:cli:accessory:remove", [ "all" ], options invoke "kamal:cli:registry:logout", [], options.without(:confirmed).merge(skip_local: true) end diff --git a/lib/kamal/commands/server.rb b/lib/kamal/commands/server.rb index 3bcb5eff6..a619a82dd 100644 --- a/lib/kamal/commands/server.rb +++ b/lib/kamal/commands/server.rb @@ -1,5 +1,9 @@ class Kamal::Commands::Server < Kamal::Commands::Base def ensure_service_directory - [ :mkdir, "-p", config.service_directory ] + make_directory config.service_directory + end + + def remove_service_directory + remove_directory config.service_directory end end diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index 6600765ba..0956ba514 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -417,6 +417,7 @@ class CliMainTest < CliTestCase assert_match /docker ps --quiet --filter label=service=app | xargs docker stop/, output assert_match /docker container prune --force --filter label=service=app/, output assert_match /docker image prune --all --force --filter label=service=app/, output + assert_match "/usr/bin/env rm -r .kamal/apps/app", output assert_match /docker container stop app-mysql/, output assert_match /docker container prune --force --filter label=service=app-mysql/, output From 35fe9c154decf5cf2431641379c9a6b92cd10991 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 12 Sep 2024 16:05:56 +0100 Subject: [PATCH 31/43] Move audits back to run dir so they survive kamal remove --- lib/kamal/commands/auditor.rb | 6 ++++-- test/cli/main_test.rb | 2 +- test/commands/auditor_test.rb | 16 ++++++++-------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/kamal/commands/auditor.rb b/lib/kamal/commands/auditor.rb index 3589643d9..9846d8e2e 100644 --- a/lib/kamal/commands/auditor.rb +++ b/lib/kamal/commands/auditor.rb @@ -9,7 +9,7 @@ def initialize(config, **details) # Runs remotely def record(line, **details) combine \ - [ :mkdir, "-p", config.service_directory ], + [ :mkdir, "-p", config.run_directory ], append( [ :echo, audit_tags(**details).except(:version, :service_version, :service).to_s, line ], audit_log_file @@ -22,7 +22,9 @@ def reveal private def audit_log_file - File.join config.service_directory, "audit.log" + file = [ config.service, config.destination, "audit.log" ].compact.join("-") + + File.join(config.run_directory, file) end def audit_tags(**details) diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index 0956ba514..7f878e5b0 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -305,7 +305,7 @@ class CliMainTest < CliTestCase test "audit" do run_command("audit").tap do |output| - assert_match %r{tail -n 50 \.kamal/apps/app/audit.log on 1.1.1.1}, output + assert_match %r{tail -n 50 \.kamal/app-audit.log on 1.1.1.1}, output assert_match /App Host: 1.1.1.1/, output end end diff --git a/test/commands/auditor_test.rb b/test/commands/auditor_test.rb index d0f666108..2abc8d815 100644 --- a/test/commands/auditor_test.rb +++ b/test/commands/auditor_test.rb @@ -18,22 +18,22 @@ class CommandsAuditorTest < ActiveSupport::TestCase test "record" do assert_equal [ - :mkdir, "-p", ".kamal/apps/app", "&&", + :mkdir, "-p", ".kamal", "&&", :echo, "[#{@recorded_at}] [#{@performer}]", "app removed container", - ">>", ".kamal/apps/app/audit.log" + ">>", ".kamal/app-audit.log" ], @auditor.record("app removed container") end test "record with destination" do new_command(destination: "staging").tap do |auditor| assert_equal [ - :mkdir, "-p", ".kamal/apps/app-staging", "&&", + :mkdir, "-p", ".kamal", "&&", :echo, "[#{@recorded_at}] [#{@performer}] [staging]", "app removed container", - ">>", ".kamal/apps/app-staging/audit.log" + ">>", ".kamal/app-staging-audit.log" ], auditor.record("app removed container") end end @@ -41,22 +41,22 @@ class CommandsAuditorTest < ActiveSupport::TestCase test "record with command details" do new_command(role: "web").tap do |auditor| assert_equal [ - :mkdir, "-p", ".kamal/apps/app", "&&", + :mkdir, "-p", ".kamal", "&&", :echo, "[#{@recorded_at}] [#{@performer}] [web]", "app removed container", - ">>", ".kamal/apps/app/audit.log" + ">>", ".kamal/app-audit.log" ], auditor.record("app removed container") end end test "record with arg details" do assert_equal [ - :mkdir, "-p", ".kamal/apps/app", "&&", + :mkdir, "-p", ".kamal", "&&", :echo, "[#{@recorded_at}] [#{@performer}] [value]", "app removed container", - ">>", ".kamal/apps/app/audit.log" + ">>", ".kamal/app-audit.log" ], @auditor.record("app removed container", detail: "value") end From 24031fefb0d4658bb33bc3fa25a72424b3eca239 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 12 Sep 2024 16:47:03 +0100 Subject: [PATCH 32/43] Remove proxy only if no apps are installed --- lib/kamal/cli/accessory.rb | 8 +++--- lib/kamal/cli/app.rb | 10 +++---- lib/kamal/cli/base.rb | 2 +- lib/kamal/cli/proxy.rb | 39 +++++++++++++++++++++++++--- lib/kamal/commands/accessory.rb | 2 +- lib/kamal/commands/proxy.rb | 4 +++ lib/kamal/commands/server.rb | 14 +++++++--- lib/kamal/configuration.rb | 16 +++++++++--- lib/kamal/configuration/proxy.rb | 2 +- test/cli/accessory_test.rb | 10 +++---- test/cli/proxy_test.rb | 39 +++++++++++++++++++++++++--- test/commands/server_test.rb | 2 +- test/integration/app_test.rb | 1 + test/integration/integration_test.rb | 12 +++++++++ test/integration/main_test.rb | 2 ++ test/integration/proxy_test.rb | 1 + 16 files changed, 131 insertions(+), 33 deletions(-) diff --git a/lib/kamal/cli/accessory.rb b/lib/kamal/cli/accessory.rb index bc84fe522..e73c6f392 100644 --- a/lib/kamal/cli/accessory.rb +++ b/lib/kamal/cli/accessory.rb @@ -207,12 +207,12 @@ def remove_image(name) end end - desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host", hide: true - def remove_service_directory(name) + desc "remove_app_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host", hide: true + def remove_app_directory(name) with_lock do with_accessory(name) do |accessory, hosts| on(hosts) do - execute *accessory.remove_service_directory + execute *accessory.remove_app_directory end end end @@ -248,7 +248,7 @@ def remove_accessory(name) stop(name) remove_container(name) remove_image(name) - remove_service_directory(name) + remove_app_directory(name) end def prepare(name) diff --git a/lib/kamal/cli/app.rb b/lib/kamal/cli/app.rb index 21a5ffddc..7359fdde9 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -231,7 +231,7 @@ def remove stop remove_containers remove_images - remove_service_directory + remove_app_directory end end @@ -273,15 +273,15 @@ def remove_images end end - desc "remove_service_directory", "Remove the service directory from servers", hide: true - def remove_service_directory + desc "remove_app_directory", "Remove the service directory from servers", hide: true + def remove_app_directory with_lock do on(KAMAL.hosts) do |host| roles = KAMAL.roles_on(host) roles.each do |role| - execute *KAMAL.auditor.record("Removed #{KAMAL.config.service_directory} on all servers", role: role), verbosity: :debug - execute *KAMAL.server.remove_service_directory + execute *KAMAL.auditor.record("Removed #{KAMAL.config.app_directory} on all servers", role: role), verbosity: :debug + execute *KAMAL.server.remove_app_directory end end end diff --git a/lib/kamal/cli/base.rb b/lib/kamal/cli/base.rb index 37a9e0462..0164765d1 100644 --- a/lib/kamal/cli/base.rb +++ b/lib/kamal/cli/base.rb @@ -176,7 +176,7 @@ def first_invocation def ensure_service_and_locks_directory on(KAMAL.hosts) do - execute(*KAMAL.server.ensure_service_directory) + execute(*KAMAL.server.ensure_app_directory) end on(KAMAL.primary_host) do diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index 94d219aab..4b02800f8 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -150,11 +150,15 @@ def logs end desc "remove", "Remove proxy container and image from servers" + option :force, type: :boolean, default: false, desc: "Force removing proxy when apps are still installed" def remove with_lock do - stop - remove_container - remove_image + if removal_allowed?(options[:force]) + stop + remove_container + remove_image + remove_host_directory + end end end @@ -178,8 +182,37 @@ def remove_image end end + desc "remove_host_directory", "Remove proxy directory from servers", hide: true + def remove_host_directory + with_lock do + on(KAMAL.proxy_hosts) do + execute *KAMAL.auditor.record("Removed #{KAMAL.config.proxy_directory}"), verbosity: :debug + execute *KAMAL.proxy.remove_host_directory + end + end + end + private def reset_invocation(cli_class) instance_variable_get("@_invocations")[cli_class].pop end + + def removal_allowed?(force) + on(KAMAL.proxy_hosts) do |host| + app_count = capture_with_info(*KAMAL.server.app_directory_count).chomp.to_i + raise "The are other applications installed on #{host}" if app_count > 0 + end + + true + rescue SSHKit::Runner::ExecuteError => e + raise unless e.message.include?("The are other applications installed on") + + if force + say "Forcing, so removing the proxy, even though other apps are installed", :magenta + else + say "Not removing the proxy, as other apps are installed, ignore this check with kamal proxy remove --force", :magenta + end + + force + end end diff --git a/lib/kamal/commands/accessory.rb b/lib/kamal/commands/accessory.rb index 787f7d43a..22220d3f4 100644 --- a/lib/kamal/commands/accessory.rb +++ b/lib/kamal/commands/accessory.rb @@ -90,7 +90,7 @@ def ensure_local_file_present(local_file) end end - def remove_service_directory + def remove_app_directory [ :rm, "-rf", service_name ] end diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb index cf01fa194..f5ea8cf41 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -67,6 +67,10 @@ def remove_image docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=kamal-proxy" end + def remove_host_directory + remove_directory config.proxy_directory + end + def cleanup_traefik chain \ docker(:container, :stop, "traefik"), diff --git a/lib/kamal/commands/server.rb b/lib/kamal/commands/server.rb index a619a82dd..e8bf97e31 100644 --- a/lib/kamal/commands/server.rb +++ b/lib/kamal/commands/server.rb @@ -1,9 +1,15 @@ class Kamal::Commands::Server < Kamal::Commands::Base - def ensure_service_directory - make_directory config.service_directory + def ensure_app_directory + make_directory config.app_directory end - def remove_service_directory - remove_directory config.service_directory + def remove_app_directory + remove_directory config.app_directory + end + + def app_directory_count + pipe \ + [ :ls, config.apps_directory ], + [ :wc, "-l" ] end end diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index 264b83e95..1d6a445e3 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -194,16 +194,24 @@ def run_directory ".kamal" end - def service_directory - File.join run_directory, "apps", [ service, destination ].compact.join("-") + def apps_directory + File.join run_directory, "apps" + end + + def app_directory + File.join apps_directory, [ service, destination ].compact.join("-") + end + + def proxy_directory + File.join run_directory, "proxy" end def env_directory - File.join service_directory, "env" + File.join app_directory, "env" end def assets_directory - File.join service_directory, "assets" + File.join app_directory, "assets" end diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb index 2d2efdfb9..b7bd7ba94 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -61,7 +61,7 @@ def deploy_command_args def config_volume Kamal::Configuration::Volume.new \ - host_path: File.join(config.run_directory, "proxy", "config"), + host_path: File.join(config.proxy_directory, "config"), container_path: "/root/.config/kamal-proxy" end diff --git a/test/cli/accessory_test.rb b/test/cli/accessory_test.rb index 5bb8762a8..2dbff62d9 100644 --- a/test/cli/accessory_test.rb +++ b/test/cli/accessory_test.rb @@ -166,7 +166,7 @@ class CliAccessoryTest < CliTestCase Kamal::Cli::Accessory.any_instance.expects(:stop).with("mysql") Kamal::Cli::Accessory.any_instance.expects(:remove_container).with("mysql") Kamal::Cli::Accessory.any_instance.expects(:remove_image).with("mysql") - Kamal::Cli::Accessory.any_instance.expects(:remove_service_directory).with("mysql") + Kamal::Cli::Accessory.any_instance.expects(:remove_app_directory).with("mysql") run_command("remove", "mysql", "-y") end @@ -175,11 +175,11 @@ class CliAccessoryTest < CliTestCase Kamal::Cli::Accessory.any_instance.expects(:stop).with("mysql") Kamal::Cli::Accessory.any_instance.expects(:remove_container).with("mysql") Kamal::Cli::Accessory.any_instance.expects(:remove_image).with("mysql") - Kamal::Cli::Accessory.any_instance.expects(:remove_service_directory).with("mysql") + Kamal::Cli::Accessory.any_instance.expects(:remove_app_directory).with("mysql") Kamal::Cli::Accessory.any_instance.expects(:stop).with("redis") Kamal::Cli::Accessory.any_instance.expects(:remove_container).with("redis") Kamal::Cli::Accessory.any_instance.expects(:remove_image).with("redis") - Kamal::Cli::Accessory.any_instance.expects(:remove_service_directory).with("redis") + Kamal::Cli::Accessory.any_instance.expects(:remove_app_directory).with("redis") run_command("remove", "all", "-y") end @@ -192,8 +192,8 @@ class CliAccessoryTest < CliTestCase assert_match "docker image rm --force mysql", run_command("remove_image", "mysql") end - test "remove_service_directory" do - assert_match "rm -rf app-mysql", run_command("remove_service_directory", "mysql") + test "remove_app_directory" do + assert_match "rm -rf app-mysql", run_command("remove_app_directory", "mysql") end test "hosts param respected" do diff --git a/test/cli/proxy_test.rb b/test/cli/proxy_test.rb index 1d98395f1..0589e525a 100644 --- a/test/cli/proxy_test.rb +++ b/test/cli/proxy_test.rb @@ -98,11 +98,36 @@ class CliProxyTest < CliTestCase end test "remove" do - Kamal::Cli::Proxy.any_instance.expects(:stop) - Kamal::Cli::Proxy.any_instance.expects(:remove_container) - Kamal::Cli::Proxy.any_instance.expects(:remove_image) + run_command("remove").tap do |output| + assert_match "/usr/bin/env ls .kamal/apps | wc -l", output + assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy", output + assert_match "docker image prune --all --force --filter label=org.opencontainers.image.title=kamal-proxy", output + assert_match "/usr/bin/env rm -r .kamal/proxy", output + end + end + + test "remove with other apps" do + Thread.report_on_exception = false + + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info).with(:ls, ".kamal/apps", "|", :wc, "-l").returns("1\n").twice + + run_command("remove").tap do |output| + assert_match "Not removing the proxy, as other apps are installed, ignore this check with kamal proxy remove --force", output + end + ensure + Thread.report_on_exception = true + end + + test "force remove with other apps" do + Thread.report_on_exception = false + + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info).with(:ls, ".kamal/apps", "|", :wc, "-l").returns("1\n").twice - run_command("remove") + run_command("remove").tap do |output| + assert_match "Not removing the proxy, as other apps are installed, ignore this check with kamal proxy remove --force", output + end + ensure + Thread.report_on_exception = true end test "remove_container" do @@ -117,6 +142,12 @@ class CliProxyTest < CliTestCase end end + test "remove_host_directory" do + run_command("remove_host_directory").tap do |output| + assert_match "/usr/bin/env rm -r .kamal/proxy", output + end + end + private def run_command(*command, fixture: :with_proxy) stdouted { Kamal::Cli::Proxy.start([ *command, "-c", "test/fixtures/deploy_#{fixture}.yml" ]) } diff --git a/test/commands/server_test.rb b/test/commands/server_test.rb index 46cf43093..5db2ac59a 100644 --- a/test/commands/server_test.rb +++ b/test/commands/server_test.rb @@ -9,7 +9,7 @@ class CommandsServerTest < ActiveSupport::TestCase end test "ensure service directory" do - assert_equal "mkdir -p .kamal/apps/app", new_command.ensure_service_directory.join(" ") + assert_equal "mkdir -p .kamal/apps/app", new_command.ensure_app_directory.join(" ") end private diff --git a/test/integration/app_test.rb b/test/integration/app_test.rb index 121a96a51..40a896b1e 100644 --- a/test/integration/app_test.rb +++ b/test/integration/app_test.rb @@ -49,5 +49,6 @@ class AppTest < IntegrationTest kamal :app, :remove assert_app_is_down + assert_app_directory_removed end end diff --git a/test/integration/integration_test.rb b/test/integration/integration_test.rb index a8e149c1d..5c675da69 100644 --- a/test/integration/integration_test.rb +++ b/test/integration/integration_test.rb @@ -148,4 +148,16 @@ def assert_container_not_running(host:, name:) def container_running?(host:, name:) docker_compose("exec #{host} docker ps --filter=name=#{name} | tail -n+2", capture: true).strip.present? end + + def assert_app_directory_removed + assert_directory_removed("./kamal/apps/#{@app}") + end + + def assert_proxy_directory_removed + assert_directory_removed("./kamal/proxy") + end + + def assert_directory_removed(directory) + assert docker_compose("exec vm1 ls #{directory} | wc -l", capture: true).strip == "0" + end end diff --git a/test/integration/main_test.rb b/test/integration/main_test.rb index fbcd09274..cefe2100c 100644 --- a/test/integration/main_test.rb +++ b/test/integration/main_test.rb @@ -97,6 +97,8 @@ class MainTest < IntegrationTest kamal :remove, "-y" assert_no_images_or_containers + assert_app_directory_removed + assert_proxy_directory_removed end private diff --git a/test/integration/proxy_test.rb b/test/integration/proxy_test.rb index 752ec41b7..d796eb6d3 100644 --- a/test/integration/proxy_test.rb +++ b/test/integration/proxy_test.rb @@ -48,6 +48,7 @@ class ProxyTest < IntegrationTest kamal :proxy, :remove assert_proxy_not_running + assert_proxy_directory_removed kamal :env, :delete end From d2672c771ea97e3380302378f3163359d649e6f6 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 12 Sep 2024 16:57:41 +0100 Subject: [PATCH 33/43] Remove redundant call to env remove --- test/integration/proxy_test.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/integration/proxy_test.rb b/test/integration/proxy_test.rb index d796eb6d3..efab25835 100644 --- a/test/integration/proxy_test.rb +++ b/test/integration/proxy_test.rb @@ -49,8 +49,6 @@ class ProxyTest < IntegrationTest kamal :proxy, :remove assert_proxy_not_running assert_proxy_directory_removed - - kamal :env, :delete end private From 8b965b0a31f500fbe2a091b94644bab2c85cff63 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 12 Sep 2024 19:27:59 +0100 Subject: [PATCH 34/43] Handle polling without the healthcheck config --- lib/kamal/cli/app.rb | 2 +- lib/kamal/cli/app/boot.rb | 3 ++ lib/kamal/cli/healthcheck/poller.rb | 30 ++++++++----- lib/kamal/configuration.rb | 4 ++ .../configuration/docs/configuration.yml | 8 +++- test/cli/app_test.rb | 43 ++++++++++++++++++- test/fixtures/deploy_with_roles.yml | 1 + 7 files changed, 77 insertions(+), 14 deletions(-) diff --git a/lib/kamal/cli/app.rb b/lib/kamal/cli/app.rb index 7359fdde9..195b5ec7a 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -4,7 +4,7 @@ def boot with_lock do say "Get most recent version available as an image...", :magenta unless options[:version] using_version(version_or_latest) do |version| - say "Start container with version #{version} using a #{KAMAL.config.readiness_delay}s readiness delay (or reboot if already running)...", :magenta + say "Start container with version #{version} (or reboot if already running)...", :magenta # Assets are prepared in a separate step to ensure they are on all hosts before booting on(KAMAL.hosts) do diff --git a/lib/kamal/cli/app/boot.rb b/lib/kamal/cli/app/boot.rb index 74da5a56f..6939825a2 100644 --- a/lib/kamal/cli/app/boot.rb +++ b/lib/kamal/cli/app/boot.rb @@ -58,6 +58,9 @@ def start_new_version else Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) } end + rescue => e + error "Failed to boot #{role} on #{host}" + raise e end def stop_new_version diff --git a/lib/kamal/cli/healthcheck/poller.rb b/lib/kamal/cli/healthcheck/poller.rb index fd8714ce5..fad518454 100644 --- a/lib/kamal/cli/healthcheck/poller.rb +++ b/lib/kamal/cli/healthcheck/poller.rb @@ -1,22 +1,30 @@ module Kamal::Cli::Healthcheck::Poller extend self - def wait_for_healthy(pause_after_ready: false, &block) + def wait_for_healthy(role, &block) attempt = 1 - max_attempts = 7 + timeout_at = Time.now + KAMAL.config.readiness_timeout + readiness_delay = KAMAL.config.readiness_delay begin - case status = block.call - when "healthy" - when "running" # No health check configured - sleep KAMAL.config.readiness_delay if pause_after_ready - else - raise Kamal::Cli::Healthcheck::Error, "container not ready (#{status})" + status = block.call + + if status == "running" + # Wait for the readiness delay and confirm it is still running + if readiness_delay > 0 + info "Container is running, waiting for readiness delay of #{readiness_delay} seconds" + sleep readiness_delay + status = block.call + end + end + + unless %w[ running healthy ].include?(status) + raise Kamal::Cli::Healthcheck::Error, "container not ready after #{KAMAL.config.readiness_timeout} seconds (#{status})" end rescue Kamal::Cli::Healthcheck::Error => e - if attempt <= max_attempts - info "#{e.message}, retrying in #{attempt}s (attempt #{attempt}/#{max_attempts})..." - sleep attempt + time_left = timeout_at - Time.now + if time_left > 0 + sleep [ attempt, time_left ].min attempt += 1 retry else diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index 1d6a445e3..bd33b08c7 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -189,6 +189,10 @@ def readiness_delay raw_config.readiness_delay || 7 end + def readiness_timeout + raw_config.readiness_timeout || 30 + end + def run_directory ".kamal" diff --git a/lib/kamal/configuration/docs/configuration.yml b/lib/kamal/configuration/docs/configuration.yml index 44910fda9..2e7b618d1 100644 --- a/lib/kamal/configuration/docs/configuration.yml +++ b/lib/kamal/configuration/docs/configuration.yml @@ -111,9 +111,15 @@ minimum_version: 1.3.0 # Readiness delay # # Seconds to wait for a container to boot after is running, default 7 -# This only applies to containers that do not specify a healthcheck +# This only applies to containers that do not run a proxy or specify a healthcheck readiness_delay: 4 +# Readiness timeout +# +# How long to wait for a container to become ready, default 30 +# This only applies to containers that do not run a proxy +readiness_timeout: 4 + # Run directory # # Directory to store kamal runtime files in on the host, default `.kamal` diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index 6c37d6206..5eaf77a67 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -116,7 +116,7 @@ class CliAppTest < CliTestCase assert_match "Waiting for the first healthy web container before booting workers on 1.1.1.4...", output assert_match "First web container is healthy, booting workers on 1.1.1.3", output assert_match "First web container is healthy, booting workers on 1.1.1.4", output - end + end end test "boot with web barrier closed" do @@ -144,6 +144,47 @@ class CliAppTest < CliTestCase Thread.report_on_exception = true end + test "boot with worker errors" do + Thread.report_on_exception = false + + Object.any_instance.stubs(:sleep) + + SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version + + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :container, :ls, "--all", "--filter", "name=^app-workers-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'") + .returns("unhealthy").at_least_once # workers health check + + run_command("boot", config: :with_roles, host: nil, allow_execute_error: true).tap do |output| + assert_match "Waiting for the first healthy web container before booting workers on 1.1.1.3...", output + assert_match "Waiting for the first healthy web container before booting workers on 1.1.1.4...", output + assert_match "First web container is healthy, booting workers on 1.1.1.3", output + assert_match "First web container is healthy, booting workers on 1.1.1.4", output + assert_match "ERROR Failed to boot workers on 1.1.1.3", output + assert_match "ERROR Failed to boot workers on 1.1.1.4", output + end + ensure + Thread.report_on_exception = true + end + + test "boot with worker ready then not" do + Thread.report_on_exception = false + + Object.any_instance.stubs(:sleep) + + SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version + + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :container, :ls, "--all", "--filter", "name=^app-workers-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'") + .returns("running", "stopped").at_least_once # workers health check + + run_command("boot", config: :with_roles, host: "1.1.1.3", allow_execute_error: true).tap do |output| + assert_match "ERROR Failed to boot workers on 1.1.1.3", output + end + ensure + Thread.report_on_exception = true + end + test "start" do SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("999") # old version diff --git a/test/fixtures/deploy_with_roles.yml b/test/fixtures/deploy_with_roles.yml index 1eb8cc5c0..d831a5b47 100644 --- a/test/fixtures/deploy_with_roles.yml +++ b/test/fixtures/deploy_with_roles.yml @@ -16,3 +16,4 @@ registry: password: pw builder: arch: amd64 +readiness_timeout: 1 From 3c39086613c17d1f9d46d037ed555338abd8dcce Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 12 Sep 2024 19:35:09 +0100 Subject: [PATCH 35/43] Not experimental --- bin/docs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/docs b/bin/docs index 9947a04cb..b76872d3b 100755 --- a/bin/docs +++ b/bin/docs @@ -23,7 +23,7 @@ DOCS = { "configuration" => "Configuration overview", "env" => "Environment variables", "logging" => "Logging", - "proxy" => "Proxy (Experimental)", + "proxy" => "Proxy", "registry" => "Docker Registry", "role" => "Roles", "servers" => "Servers", From a84ee6315f4a4af5e9c8e31434cd8666624d58ee Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 12 Sep 2024 19:37:07 +0100 Subject: [PATCH 36/43] Rename service -> app directory --- lib/kamal/cli/base.rb | 4 ++-- lib/kamal/cli/lock.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/kamal/cli/base.rb b/lib/kamal/cli/base.rb index 0164765d1..c5a3e376a 100644 --- a/lib/kamal/cli/base.rb +++ b/lib/kamal/cli/base.rb @@ -101,7 +101,7 @@ def confirming(question) end def acquire_lock - ensure_service_and_locks_directory + ensure_app_and_locks_directory raise_if_locked do say "Acquiring the deploy lock...", :magenta @@ -174,7 +174,7 @@ def first_invocation instance_variable_get("@_invocations").first end - def ensure_service_and_locks_directory + def ensure_app_and_locks_directory on(KAMAL.hosts) do execute(*KAMAL.server.ensure_app_directory) end diff --git a/lib/kamal/cli/lock.rb b/lib/kamal/cli/lock.rb index abbea71fb..08afb1f46 100644 --- a/lib/kamal/cli/lock.rb +++ b/lib/kamal/cli/lock.rb @@ -12,7 +12,7 @@ def status option :message, aliases: "-m", type: :string, desc: "A lock message", required: true def acquire message = options[:message] - ensure_service_and_locks_directory + ensure_app_and_locks_directory raise_if_locked do on(KAMAL.primary_host) do From bf91d6c1cad5f80ad3190aac1b300dc50c943126 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Fri, 13 Sep 2024 09:45:29 +0100 Subject: [PATCH 37/43] Fix command description --- lib/kamal/cli/main.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/kamal/cli/main.rb b/lib/kamal/cli/main.rb index 833910622..46ce1583c 100644 --- a/lib/kamal/cli/main.rb +++ b/lib/kamal/cli/main.rb @@ -206,7 +206,7 @@ def version desc "lock", "Manage the deploy lock" subcommand "lock", Kamal::Cli::Lock - desc "proxy", "Prune old application images and containers" + desc "proxy", "Manage kamal-proxy" subcommand "proxy", Kamal::Cli::Proxy desc "prune", "Prune old application images and containers" From a316e51eda2142d5571269d739109e8c2ba791ae Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Mon, 16 Sep 2024 10:01:08 +0100 Subject: [PATCH 38/43] Add user agent to default headers --- lib/kamal/cli/proxy.rb | 1 - lib/kamal/configuration/proxy.rb | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index 4b02800f8..bfeb80734 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -72,7 +72,6 @@ def upgrade execute *KAMAL.registry.login "Stopping and removing Traefik on #{host}, if running..." - execute *KAMAL.proxy.stop_traefik, raise_on_non_zero_exit: false execute *KAMAL.proxy.cleanup_traefik "Stopping and removing kamal-proxy on #{host}, if running..." diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb index b7bd7ba94..810f75171 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -4,7 +4,7 @@ class Kamal::Configuration::Proxy DEFAULT_HTTP_PORT = 80 DEFAULT_HTTPS_PORT = 443 DEFAULT_IMAGE = "basecamp/kamal-proxy:latest" - DEFAULT_LOG_REQUEST_HEADERS = [ "Cache-Control", "Last-Modified" ] + DEFAULT_LOG_REQUEST_HEADERS = [ "Cache-Control", "Last-Modified", "User-Agent" ] delegate :argumentize, :optionize, to: Kamal::Utils From e8ff233e813da5450e5c5fe582a6fa1d43ff077e Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Mon, 16 Sep 2024 10:08:40 +0100 Subject: [PATCH 39/43] Fix default log header tests --- test/cli/app_test.rb | 2 +- test/cli/proxy_test.rb | 4 ++-- test/commands/proxy_test.rb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index 5eaf77a67..860078cff 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -130,7 +130,7 @@ class CliAppTest < CliTestCase SSHKit::Backend::Abstract.any_instance.stubs(:execute) .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :stop, raise_on_non_zero_exit: false).twice SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with(:docker, :exec, "kamal-proxy", "kamal-proxy", :deploy, "app-web", "--target", "\"123:80\"", "--buffer-requests", "--buffer-responses", "--log-request-header", "\"Cache-Control\"", "--log-request-header", "\"Last-Modified\"").raises(SSHKit::Command::Failed.new("Failed to deploy")) + .with(:docker, :exec, "kamal-proxy", "kamal-proxy", :deploy, "app-web", "--target", "\"123:80\"", "--buffer-requests", "--buffer-responses", "--log-request-header", "\"Cache-Control\"", "--log-request-header", "\"Last-Modified\"", "--log-request-header", "\"User-Agent\"").raises(SSHKit::Command::Failed.new("Failed to deploy")) stderred do run_command("boot", config: :with_roles, host: nil, allow_execute_error: true).tap do |output| diff --git a/test/cli/proxy_test.rb b/test/cli/proxy_test.rb index 0589e525a..8bc0f895f 100644 --- a/test/cli/proxy_test.rb +++ b/test/cli/proxy_test.rb @@ -24,13 +24,13 @@ class CliProxyTest < CliTestCase assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE} on 1.1.1.1", output - assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" on 1.1.1.1", output + assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\" on 1.1.1.1", output assert_match "docker container stop kamal-proxy on 1.1.1.2", output assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.2", output assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.2", output assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE} on 1.1.1.2", output - assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" on 1.1.1.2", output + assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\" on 1.1.1.2", output end end diff --git a/test/commands/proxy_test.rb b/test/commands/proxy_test.rb index 3b7c7191e..36e04c7b6 100644 --- a/test/commands/proxy_test.rb +++ b/test/commands/proxy_test.rb @@ -109,7 +109,7 @@ class CommandsProxyTest < ActiveSupport::TestCase test "deploy" do assert_equal \ - "docker exec kamal-proxy kamal-proxy deploy service --target \"172.1.0.2:80\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\"", + "docker exec kamal-proxy kamal-proxy deploy service --target \"172.1.0.2:80\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\"", new_command.deploy("service", target: "172.1.0.2").join(" ") end From 7f31510aec2c6ad52239e52bd45bb384d8d01e5d Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Mon, 16 Sep 2024 11:09:22 +0100 Subject: [PATCH 40/43] Set hosts via config rather than options --- lib/kamal/cli/proxy.rb | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index bfeb80734..b3c47a494 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -60,7 +60,7 @@ def reboot option :rolling, type: :boolean, default: false, desc: "Reboot proxy on hosts in sequence, rather than in parallel" option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question" def upgrade - invoke_options = { "version" => KAMAL.config.version }.merge(options) + invoke_options = { "version" => KAMAL.config.latest_tag }.merge(options) confirming "This will cause a brief outage on each host. Are you sure?" do host_groups = options[:rolling] ? KAMAL.hosts : [ KAMAL.hosts ] @@ -77,14 +77,20 @@ def upgrade "Stopping and removing kamal-proxy on #{host}, if running..." execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false execute *KAMAL.proxy.remove_container + execute *KAMAL.proxy.remove_image end - invoke "kamal:cli:proxy:boot", [], invoke_options.merge("hosts" => host_list) - reset_invocation(Kamal::Cli::Proxy) - invoke "kamal:cli:app:boot", [], invoke_options.merge("hosts" => host_list, version: KAMAL.config.latest_tag) - reset_invocation(Kamal::Cli::App) - invoke "kamal:cli:prune:all", [], invoke_options.merge("hosts" => host_list) - reset_invocation(Kamal::Cli::Prune) + begin + old_hosts, KAMAL.specific_hosts = KAMAL.specific_hosts, hosts + invoke "kamal:cli:proxy:boot", [], invoke_options + reset_invocation(Kamal::Cli::Proxy) + invoke "kamal:cli:app:boot", [], invoke_options + reset_invocation(Kamal::Cli::App) + invoke "kamal:cli:prune:all", [], invoke_options + reset_invocation(Kamal::Cli::Prune) + ensure + KAMAL.specific_hosts = old_hosts + end run_hook "post-proxy-reboot", hosts: host_list end From 6c51e596ae8c6674d4a8d53a168cc55dbffd459e Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Mon, 16 Sep 2024 12:40:07 +0100 Subject: [PATCH 41/43] Put locks directories in .kamal so they leave no trace when deleted --- lib/kamal/cli/accessory.rb | 8 ++++---- lib/kamal/cli/app.rb | 2 +- lib/kamal/cli/base.rb | 10 +++------- lib/kamal/cli/lock.rb | 2 +- lib/kamal/cli/proxy.rb | 2 +- lib/kamal/commands/accessory.rb | 2 +- lib/kamal/commands/lock.rb | 8 ++------ lib/kamal/commands/server.rb | 4 ++-- test/cli/accessory_test.rb | 10 +++++----- test/cli/app_test.rb | 2 +- test/cli/cli_test_case.rb | 6 +++--- test/cli/lock_test.rb | 2 +- test/cli/main_test.rb | 14 ++++---------- test/cli/server_test.rb | 6 +++--- test/commands/lock_test.rb | 6 +++--- test/commands/server_test.rb | 4 ++-- 16 files changed, 37 insertions(+), 51 deletions(-) diff --git a/lib/kamal/cli/accessory.rb b/lib/kamal/cli/accessory.rb index e73c6f392..bc84fe522 100644 --- a/lib/kamal/cli/accessory.rb +++ b/lib/kamal/cli/accessory.rb @@ -207,12 +207,12 @@ def remove_image(name) end end - desc "remove_app_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host", hide: true - def remove_app_directory(name) + desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host", hide: true + def remove_service_directory(name) with_lock do with_accessory(name) do |accessory, hosts| on(hosts) do - execute *accessory.remove_app_directory + execute *accessory.remove_service_directory end end end @@ -248,7 +248,7 @@ def remove_accessory(name) stop(name) remove_container(name) remove_image(name) - remove_app_directory(name) + remove_service_directory(name) end def prepare(name) diff --git a/lib/kamal/cli/app.rb b/lib/kamal/cli/app.rb index 195b5ec7a..2a02ed652 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -281,7 +281,7 @@ def remove_app_directory roles.each do |role| execute *KAMAL.auditor.record("Removed #{KAMAL.config.app_directory} on all servers", role: role), verbosity: :debug - execute *KAMAL.server.remove_app_directory + execute *KAMAL.server.remove_app_directory, raise_on_non_zero_exit: false end end end diff --git a/lib/kamal/cli/base.rb b/lib/kamal/cli/base.rb index c5a3e376a..b3af23cce 100644 --- a/lib/kamal/cli/base.rb +++ b/lib/kamal/cli/base.rb @@ -101,7 +101,7 @@ def confirming(question) end def acquire_lock - ensure_app_and_locks_directory + ensure_run_directory raise_if_locked do say "Acquiring the deploy lock...", :magenta @@ -174,13 +174,9 @@ def first_invocation instance_variable_get("@_invocations").first end - def ensure_app_and_locks_directory + def ensure_run_directory on(KAMAL.hosts) do - execute(*KAMAL.server.ensure_app_directory) - end - - on(KAMAL.primary_host) do - execute(*KAMAL.lock.ensure_locks_directory) + execute(*KAMAL.server.ensure_run_directory) end end end diff --git a/lib/kamal/cli/lock.rb b/lib/kamal/cli/lock.rb index 08afb1f46..ceab1f27b 100644 --- a/lib/kamal/cli/lock.rb +++ b/lib/kamal/cli/lock.rb @@ -12,7 +12,7 @@ def status option :message, aliases: "-m", type: :string, desc: "A lock message", required: true def acquire message = options[:message] - ensure_app_and_locks_directory + ensure_run_directory raise_if_locked do on(KAMAL.primary_host) do diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index b3c47a494..3ac02c763 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -192,7 +192,7 @@ def remove_host_directory with_lock do on(KAMAL.proxy_hosts) do execute *KAMAL.auditor.record("Removed #{KAMAL.config.proxy_directory}"), verbosity: :debug - execute *KAMAL.proxy.remove_host_directory + execute *KAMAL.proxy.remove_host_directory, raise_on_non_zero_exit: false end end end diff --git a/lib/kamal/commands/accessory.rb b/lib/kamal/commands/accessory.rb index 22220d3f4..787f7d43a 100644 --- a/lib/kamal/commands/accessory.rb +++ b/lib/kamal/commands/accessory.rb @@ -90,7 +90,7 @@ def ensure_local_file_present(local_file) end end - def remove_app_directory + def remove_service_directory [ :rm, "-rf", service_name ] end diff --git a/lib/kamal/commands/lock.rb b/lib/kamal/commands/lock.rb index 395b6f3a4..aafaec708 100644 --- a/lib/kamal/commands/lock.rb +++ b/lib/kamal/commands/lock.rb @@ -44,14 +44,10 @@ def stat_lock_dir "/dev/null" end - def locks_dir - File.join(config.run_directory, "locks") - end - def lock_dir - dir_name = [ config.service, config.destination ].compact.join("-") + dir_name = [ "lock", config.service, config.destination ].compact.join("-") - File.join(locks_dir, dir_name) + File.join(config.run_directory, dir_name) end def lock_details_file diff --git a/lib/kamal/commands/server.rb b/lib/kamal/commands/server.rb index e8bf97e31..305903f6f 100644 --- a/lib/kamal/commands/server.rb +++ b/lib/kamal/commands/server.rb @@ -1,6 +1,6 @@ class Kamal::Commands::Server < Kamal::Commands::Base - def ensure_app_directory - make_directory config.app_directory + def ensure_run_directory + make_directory config.run_directory end def remove_app_directory diff --git a/test/cli/accessory_test.rb b/test/cli/accessory_test.rb index 2dbff62d9..5bb8762a8 100644 --- a/test/cli/accessory_test.rb +++ b/test/cli/accessory_test.rb @@ -166,7 +166,7 @@ class CliAccessoryTest < CliTestCase Kamal::Cli::Accessory.any_instance.expects(:stop).with("mysql") Kamal::Cli::Accessory.any_instance.expects(:remove_container).with("mysql") Kamal::Cli::Accessory.any_instance.expects(:remove_image).with("mysql") - Kamal::Cli::Accessory.any_instance.expects(:remove_app_directory).with("mysql") + Kamal::Cli::Accessory.any_instance.expects(:remove_service_directory).with("mysql") run_command("remove", "mysql", "-y") end @@ -175,11 +175,11 @@ class CliAccessoryTest < CliTestCase Kamal::Cli::Accessory.any_instance.expects(:stop).with("mysql") Kamal::Cli::Accessory.any_instance.expects(:remove_container).with("mysql") Kamal::Cli::Accessory.any_instance.expects(:remove_image).with("mysql") - Kamal::Cli::Accessory.any_instance.expects(:remove_app_directory).with("mysql") + Kamal::Cli::Accessory.any_instance.expects(:remove_service_directory).with("mysql") Kamal::Cli::Accessory.any_instance.expects(:stop).with("redis") Kamal::Cli::Accessory.any_instance.expects(:remove_container).with("redis") Kamal::Cli::Accessory.any_instance.expects(:remove_image).with("redis") - Kamal::Cli::Accessory.any_instance.expects(:remove_app_directory).with("redis") + Kamal::Cli::Accessory.any_instance.expects(:remove_service_directory).with("redis") run_command("remove", "all", "-y") end @@ -192,8 +192,8 @@ class CliAccessoryTest < CliTestCase assert_match "docker image rm --force mysql", run_command("remove_image", "mysql") end - test "remove_app_directory" do - assert_match "rm -rf app-mysql", run_command("remove_app_directory", "mysql") + test "remove_service_directory" do + assert_match "rm -rf app-mysql", run_command("remove_service_directory", "mysql") end test "hosts param respected" do diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index 860078cff..f3bd9fe5f 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -37,7 +37,7 @@ class CliAppTest < CliTestCase end test "boot uses group strategy when specified" do - Kamal::Cli::App.any_instance.stubs(:on).with("1.1.1.1").times(3) # ensure locks dir, acquire & release lock + Kamal::Cli::App.any_instance.stubs(:on).with("1.1.1.1").times(2) # ensure locks dir, acquire & release lock Kamal::Cli::App.any_instance.stubs(:on).with([ "1.1.1.1" ]) # tag container # Strategy is used when booting the containers diff --git a/test/cli/cli_test_case.rb b/test/cli/cli_test_case.rb index 2ffebd9cb..5a2bb76f7 100644 --- a/test/cli/cli_test_case.rb +++ b/test/cli/cli_test_case.rb @@ -31,11 +31,11 @@ def stub_setup SSHKit::Backend::Abstract.any_instance.stubs(:execute) .with { |*args| args == [ :mkdir, "-p", ".kamal/apps/app" ] } SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |arg1, arg2, arg3| arg1 == :mkdir && arg2 == "-p" && arg3 == ".kamal/locks" } + .with { |arg1, arg2, arg3| arg1 == :mkdir && arg2 == "-p" && arg3 == ".kamal/lock-app" } SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |arg1, arg2| arg1 == :mkdir && arg2 == ".kamal/locks/app" } + .with { |arg1, arg2| arg1 == :mkdir && arg2 == ".kamal/lock-app" } SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |arg1, arg2| arg1 == :rm && arg2 == ".kamal/locks/app/details" } + .with { |arg1, arg2| arg1 == :rm && arg2 == ".kamal/lock-app/details" } SSHKit::Backend::Abstract.any_instance.stubs(:execute) .with(:docker, :buildx, :inspect, "kamal-local-docker-container") end diff --git a/test/cli/lock_test.rb b/test/cli/lock_test.rb index f6874d49e..66482153e 100644 --- a/test/cli/lock_test.rb +++ b/test/cli/lock_test.rb @@ -3,7 +3,7 @@ class CliLockTest < CliTestCase test "status" do run_command("status").tap do |output| - assert_match "Running /usr/bin/env stat .kamal/locks/app > /dev/null && cat .kamal/locks/app/details | base64 -d on 1.1.1.1", output + assert_match "Running /usr/bin/env stat .kamal/lock-app > /dev/null && cat .kamal/lock-app/details | base64 -d on 1.1.1.1", output end end diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index 7f878e5b0..607fee21b 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -100,14 +100,11 @@ class CliMainTest < CliTestCase .with { |*args| args == [ :mkdir, "-p", ".kamal/apps/app" ] } SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |*args| args == [ :mkdir, "-p", ".kamal/locks" ] } - - SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |*arg| arg[0..1] == [ :mkdir, ".kamal/locks/app" ] } - .raises(RuntimeError, "mkdir: cannot create directory ‘kamal/locks/app’: File exists") + .with { |*arg| arg[0..1] == [ :mkdir, ".kamal/lock-app" ] } + .raises(RuntimeError, "mkdir: cannot create directory ‘kamal/lock-app’: File exists") SSHKit::Backend::Abstract.any_instance.expects(:capture_with_debug) - .with(:stat, ".kamal/locks/app", ">", "/dev/null", "&&", :cat, ".kamal/locks/app/details", "|", :base64, "-d") + .with(:stat, ".kamal/lock-app", ">", "/dev/null", "&&", :cat, ".kamal/lock-app/details", "|", :base64, "-d") SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:git, "-C", anything, :"rev-parse", :HEAD) @@ -137,10 +134,7 @@ class CliMainTest < CliTestCase .with { |*args| args == [ :mkdir, "-p", ".kamal/apps/app" ] } SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |*args| args == [ :mkdir, "-p", ".kamal/locks" ] } - - SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |*arg| arg[0..1] == [ :mkdir, ".kamal/locks/app" ] } + .with { |*arg| arg[0..1] == [ :mkdir, ".kamal/lock-app" ] } .raises(SocketError, "getaddrinfo: nodename nor servname provided, or not known") SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) diff --git a/test/cli/server_test.rb b/test/cli/server_test.rb index 0436a3eef..110e217dd 100644 --- a/test/cli/server_test.rb +++ b/test/cli/server_test.rb @@ -32,7 +32,7 @@ class CliServerTest < CliTestCase test "bootstrap already installed" do stub_setup SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(true).at_least_once - SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:mkdir, "-p", ".kamal/apps/app").returns("").at_least_once + SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:mkdir, "-p", ".kamal").returns("").at_least_once assert_equal "Acquiring the deploy lock...\nReleasing the deploy lock...", run_command("bootstrap") end @@ -41,7 +41,7 @@ class CliServerTest < CliTestCase stub_setup SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(false).at_least_once SSHKit::Backend::Abstract.any_instance.expects(:execute).with('[ "${EUID:-$(id -u)}" -eq 0 ] || command -v sudo >/dev/null || command -v su >/dev/null', raise_on_non_zero_exit: false).returns(false).at_least_once - SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:mkdir, "-p", ".kamal/apps/app").returns("").at_least_once + SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:mkdir, "-p", ".kamal").returns("").at_least_once assert_raise RuntimeError, "Docker is not installed on 1.1.1.1, 1.1.1.3, 1.1.1.4, 1.1.1.2 and can't be automatically installed without having root access and the `curl` command available. Install Docker manually: https://docs.docker.com/engine/install/" do run_command("bootstrap") @@ -53,7 +53,7 @@ class CliServerTest < CliTestCase SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(false).at_least_once SSHKit::Backend::Abstract.any_instance.expects(:execute).with('[ "${EUID:-$(id -u)}" -eq 0 ] || command -v sudo >/dev/null || command -v su >/dev/null', raise_on_non_zero_exit: false).returns(true).at_least_once SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:sh, "-c", "'curl -fsSL https://get.docker.com || wget -O - https://get.docker.com || echo \"exit 1\"'", "|", :sh).at_least_once - SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:mkdir, "-p", ".kamal/apps/app").returns("").at_least_once + SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:mkdir, "-p", ".kamal").returns("").at_least_once Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true) SSHKit::Backend::Abstract.any_instance.expects(:execute).with(".kamal/hooks/pre-connect", anything).at_least_once SSHKit::Backend::Abstract.any_instance.expects(:execute).with(".kamal/hooks/docker-setup", anything).at_least_once diff --git a/test/commands/lock_test.rb b/test/commands/lock_test.rb index a87572513..0ad0ba46f 100644 --- a/test/commands/lock_test.rb +++ b/test/commands/lock_test.rb @@ -10,19 +10,19 @@ class CommandsLockTest < ActiveSupport::TestCase test "status" do assert_equal \ - "stat .kamal/locks/app-production > /dev/null && cat .kamal/locks/app-production/details | base64 -d", + "stat .kamal/lock-app-production > /dev/null && cat .kamal/lock-app-production/details | base64 -d", new_command.status.join(" ") end test "acquire" do assert_match \ - %r{mkdir \.kamal/locks/app-production && echo ".*" > \.kamal/locks/app-production/details}m, + %r{mkdir \.kamal/lock-app-production && echo ".*" > \.kamal/lock-app-production/details}m, new_command.acquire("Hello", "123").join(" ") end test "release" do assert_match \ - "rm .kamal/locks/app-production/details && rm -r .kamal/locks/app-production", + "rm .kamal/lock-app-production/details && rm -r .kamal/lock-app-production", new_command.release.join(" ") end diff --git a/test/commands/server_test.rb b/test/commands/server_test.rb index 5db2ac59a..648821b4a 100644 --- a/test/commands/server_test.rb +++ b/test/commands/server_test.rb @@ -8,8 +8,8 @@ class CommandsServerTest < ActiveSupport::TestCase } end - test "ensure service directory" do - assert_equal "mkdir -p .kamal/apps/app", new_command.ensure_app_directory.join(" ") + test "ensure run directory" do + assert_equal "mkdir -p .kamal", new_command.ensure_run_directory.join(" ") end private From 1f721739d662c7a6fa1dd9dbec85265c21a1224e Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Mon, 16 Sep 2024 15:51:58 +0100 Subject: [PATCH 42/43] Use version 0.1.0 of kamal-proxy and add minimum version check --- lib/kamal/cli/proxy.rb | 6 +++++ lib/kamal/commands/proxy.rb | 6 +++++ lib/kamal/configuration/proxy.rb | 5 ++-- lib/kamal/utils.rb | 4 ++++ test/cli/proxy_test.rb | 40 +++++++++++++++++++++++++++++--- test/commands/proxy_test.rb | 14 +++++++---- test/integration/main_test.rb | 2 +- test/integration/proxy_test.rb | 4 ++-- 8 files changed, 69 insertions(+), 12 deletions(-) diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index 3ac02c763..108d78552 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -10,6 +10,12 @@ def boot on(KAMAL.proxy_hosts) do |host| execute *KAMAL.registry.login + + version = capture_with_info(*KAMAL.proxy.version).strip.presence + + if version && Kamal::Utils.older_version?(version, Kamal::Configuration::Proxy::MINIMUM_VERSION) + raise "kamal-proxy version #{version} is too old, please reboot to update to at least #{Kamal::Configuration::Proxy::MINIMUM_VERSION}" + end execute *KAMAL.proxy.start_or_run end end diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb index f5ea8cf41..354493db2 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -46,6 +46,12 @@ def info docker :ps, "--filter", "name=^#{container_name}$" end + def version + pipe \ + docker(:inspect, container_name, "--format '{{.Config.Image}}'"), + [ :cut, "-d:", "-f2" ] + end + def logs(since: nil, lines: nil, grep: nil, grep_options: nil) pipe \ docker(:logs, container_name, (" --since #{since}" if since), (" --tail #{lines}" if lines), "--timestamps", "2>&1"), diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb index 810f75171..a3161e1b2 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -1,9 +1,10 @@ class Kamal::Configuration::Proxy include Kamal::Configuration::Validation + MINIMUM_VERSION = "v0.1.0" DEFAULT_HTTP_PORT = 80 DEFAULT_HTTPS_PORT = 443 - DEFAULT_IMAGE = "basecamp/kamal-proxy:latest" + DEFAULT_IMAGE = "basecamp/kamal-proxy:#{MINIMUM_VERSION}" DEFAULT_LOG_REQUEST_HEADERS = [ "Cache-Control", "Last-Modified", "User-Agent" ] delegate :argumentize, :optionize, to: Kamal::Utils @@ -62,7 +63,7 @@ def deploy_command_args def config_volume Kamal::Configuration::Volume.new \ host_path: File.join(config.proxy_directory, "config"), - container_path: "/root/.config/kamal-proxy" + container_path: "/home/kamal-proxy/.config/kamal-proxy" end private diff --git a/lib/kamal/utils.rb b/lib/kamal/utils.rb index 266d6a96b..ab8dd50e7 100644 --- a/lib/kamal/utils.rb +++ b/lib/kamal/utils.rb @@ -101,4 +101,8 @@ def docker_arch arch end end + + def older_version?(version, other_version) + Gem::Version.new(version.delete_prefix("v")) < Gem::Version.new(other_version.delete_prefix("v")) + end end diff --git a/test/cli/proxy_test.rb b/test/cli/proxy_test.rb index 8bc0f895f..2a0834fc9 100644 --- a/test/cli/proxy_test.rb +++ b/test/cli/proxy_test.rb @@ -4,10 +4,44 @@ class CliProxyTest < CliTestCase test "boot" do run_command("boot").tap do |output| assert_match "docker login", output - assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", output + assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", output end end + test "boot old version" do + Thread.report_on_exception = false + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :inspect, "kamal-proxy", "--format '{{.Config.Image}}'", "|", :cut, "-d:", "-f2") + .returns("v0.0.1") + .at_least_once + + exception = assert_raises do + run_command("boot").tap do |output| + assert_match "docker login", output + assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", output + end + end + + assert_includes exception.message, "kamal-proxy version v0.0.1 is too old, please reboot to update to at least v0.1.0" + ensure + Thread.report_on_exception = false + end + + test "boot correct version" do + Thread.report_on_exception = false + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :inspect, "kamal-proxy", "--format '{{.Config.Image}}'", "|", :cut, "-d:", "-f2") + .returns("v0.1.0") + .at_least_once + + run_command("boot").tap do |output| + assert_match "docker login", output + assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", output + end + ensure + Thread.report_on_exception = false + end + test "reboot" do SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet") @@ -23,13 +57,13 @@ class CliProxyTest < CliTestCase assert_match "docker container stop kamal-proxy on 1.1.1.1", output assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output - assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE} on 1.1.1.1", output + assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE} on 1.1.1.1", output assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\" on 1.1.1.1", output assert_match "docker container stop kamal-proxy on 1.1.1.2", output assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.2", output assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.2", output - assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE} on 1.1.1.2", output + assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE} on 1.1.1.2", output assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\" on 1.1.1.2", output end end diff --git a/test/commands/proxy_test.rb b/test/commands/proxy_test.rb index 36e04c7b6..349e1a1be 100644 --- a/test/commands/proxy_test.rb +++ b/test/commands/proxy_test.rb @@ -15,13 +15,13 @@ class CommandsProxyTest < ActiveSupport::TestCase test "run" do assert_equal \ - "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", + "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", new_command.run.join(" ") end test "run with ports configured" do assert_equal \ - "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", + "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", new_command.run.join(" ") end @@ -29,7 +29,7 @@ class CommandsProxyTest < ActiveSupport::TestCase @config.delete(:proxy) assert_equal \ - "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", + "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", new_command.run.join(" ") end @@ -37,7 +37,7 @@ class CommandsProxyTest < ActiveSupport::TestCase @config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } } assert_equal \ - "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/root/.config/kamal-proxy --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", + "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", new_command.run.join(" ") end @@ -119,6 +119,12 @@ class CommandsProxyTest < ActiveSupport::TestCase new_command.remove("service", target: "172.1.0.2").join(" ") end + test "version" do + assert_equal \ + "docker inspect kamal-proxy --format '{{.Config.Image}}' | cut -d: -f2", + new_command.version.join(" ") + end + private def new_command Kamal::Commands::Proxy.new(Kamal::Configuration.new(@config, version: "123")) diff --git a/test/integration/main_test.rb b/test/integration/main_test.rb index cefe2100c..5a4eb91c8 100644 --- a/test/integration/main_test.rb +++ b/test/integration/main_test.rb @@ -28,7 +28,7 @@ class MainTest < IntegrationTest assert_match /Proxy Host: vm2/, details assert_match /App Host: vm1/, details assert_match /App Host: vm2/, details - assert_match /basecamp\/kamal-proxy:latest/, details + assert_match /basecamp\/kamal-proxy:v0.1.0/, details assert_match /registry:4443\/app:#{first_version}/, details audit = kamal :audit, capture: true diff --git a/test/integration/proxy_test.rb b/test/integration/proxy_test.rb index efab25835..f9c7133b3 100644 --- a/test/integration/proxy_test.rb +++ b/test/integration/proxy_test.rb @@ -53,11 +53,11 @@ class ProxyTest < IntegrationTest private def assert_proxy_running - assert_match /basecamp\/kamal-proxy:latest \"kamal-proxy run\"/, proxy_details + assert_match /basecamp\/kamal-proxy:v0.1.0 \"kamal-proxy run\"/, proxy_details end def assert_proxy_not_running - assert_no_match /basecamp\/kamal-proxy:latest \"kamal-proxy run\"/, proxy_details + assert_no_match /basecamp\/kamal-proxy:v0.1.0 \"kamal-proxy run\"/, proxy_details end def proxy_details From 267b52643893ae644f30f09469b283ecb187f3c5 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Mon, 16 Sep 2024 20:45:09 +0100 Subject: [PATCH 43/43] Switch proxy/hosts to proxy/host The proxy only supports a single host per app for nowm so make the config match that. --- lib/kamal/configuration/docs/proxy.yml | 10 ++++------ lib/kamal/configuration/proxy.rb | 2 +- lib/kamal/configuration/validator/proxy.rb | 2 +- test/configuration/proxy_test.rb | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/kamal/configuration/docs/proxy.yml b/lib/kamal/configuration/docs/proxy.yml index 0ca6befd9..e754eb36d 100644 --- a/lib/kamal/configuration/docs/proxy.yml +++ b/lib/kamal/configuration/docs/proxy.yml @@ -7,16 +7,14 @@ # run on the same proxy. proxy: - # Hosts + # Host # - # These are the hosts that will be used to serve the app. If you deploy more - # than one application to a single host, the proxy will route requests based - # these hosts + # The hosts that will be used to serve the app. The proxy will only route requests + # to this host to your app. # # If no hosts are set, then all requests will be forwarded, except for matching # requests for other apps that do have a host set. - hosts: - - foo.example.com + host: foo.example.com # App port # diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb index a3161e1b2..2671b4c84 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -37,7 +37,7 @@ def ssl? def deploy_options { - host: proxy_config.fetch("hosts", []).first, + host: proxy_config["host"], tls: proxy_config["ssl"], "deploy-timeout": proxy_config["deploy_timeout"], "drain-timeout": proxy_config["drain_timeout"], diff --git a/lib/kamal/configuration/validator/proxy.rb b/lib/kamal/configuration/validator/proxy.rb index 2b0555706..a4ee19bf5 100644 --- a/lib/kamal/configuration/validator/proxy.rb +++ b/lib/kamal/configuration/validator/proxy.rb @@ -2,7 +2,7 @@ class Kamal::Configuration::Validator::Proxy < Kamal::Configuration::Validator def validate! super - if config["hosts"].blank? && config["ssl"] + if config["host"].blank? && config["ssl"] error "Must set a host to enable automatic SSL" end end diff --git a/test/configuration/proxy_test.rb b/test/configuration/proxy_test.rb index 6dac27ff3..3aa3f85e3 100644 --- a/test/configuration/proxy_test.rb +++ b/test/configuration/proxy_test.rb @@ -9,7 +9,7 @@ class ConfigurationEnvTest < ActiveSupport::TestCase end test "ssl with host" do - @deploy[:proxy] = { "ssl" => true, "hosts" => [ "example.com" ] } + @deploy[:proxy] = { "ssl" => true, "host" => "example.com" } assert_equal true, config.proxy.ssl? end