From eab717e0cf88ef06bcdb821f65affce6d855653b Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 17 Jul 2024 16:04:46 +0100 Subject: [PATCH 01/71] 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 a8731ce2..437c5ce7 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 6772556e..dc35c403 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 ce21b2cf..732addae 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -38,8 +38,17 @@ class Kamal::Cli::App < Kamal::Cli::Base 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 @@ class Kamal::Cli::App < Kamal::Cli::Base 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 df3e6925..b6a09477 100644 --- a/lib/kamal/cli/app/boot.rb +++ b/lib/kamal/cli/app/boot.rb @@ -45,15 +45,25 @@ class Kamal::Cli::App::Boot 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 @@ class Kamal::Cli::App::Boot 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 @@ class Kamal::Cli::App::Boot 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 3bd6dc24..fdbc2559 100644 --- a/lib/kamal/cli/main.rb +++ b/lib/kamal/cli/main.rb @@ -35,8 +35,13 @@ class Kamal::Cli::Main < Kamal::Cli::Base 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 @@ class Kamal::Cli::Main < Kamal::Cli::Base 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 @@ class Kamal::Cli::Main < Kamal::Cli::Base 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 @@ class Kamal::Cli::Main < Kamal::Cli::Base 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 00000000..d546439c --- /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 41ffbc04..0dc25955 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 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base 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 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base 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 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base 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 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base desc "restart", "Restart existing Traefik container on servers" def restart + raise_if_kamal_proxy_enabled! with_lock do stop start @@ -64,6 +69,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base 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 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base 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 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base 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 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base 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 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base 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 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base 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 11914a67..994debb5 100644 --- a/lib/kamal/commander.rb +++ b/lib/kamal/commander.rb @@ -4,7 +4,7 @@ require "active_support/core_ext/object/blank" 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 @@ class Kamal::Commander @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 @@ class Kamal::Commander 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 127bd40e..12a710d8 100644 --- a/lib/kamal/commander/specifics.rb +++ b/lib/kamal/commander/specifics.rb @@ -22,6 +22,15 @@ class Kamal::Commander::Specifics 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 f1991e48..ad6e0613 100644 --- a/lib/kamal/commands/app.rb +++ b/lib/kamal/commands/app.rb @@ -30,6 +30,24 @@ class Kamal::Commands::App < Kamal::Commands::Base 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 0bab388b..629fa47f 100644 --- a/lib/kamal/commands/app/containers.rb +++ b/lib/kamal/commands/app/containers.rb @@ -28,4 +28,11 @@ module Kamal::Commands::App::Containers 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 00000000..89808d72 --- /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 2758d15a..40c9e4fe 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 @@ class Kamal::Configuration @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 @@ class Kamal::Configuration 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 f1045dd6..8ebaaa0d 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 00000000..f61ea229 --- /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 00000000..5dc3546b --- /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 ef651898..6579b9d0 100644 --- a/lib/kamal/configuration/role.rb +++ b/lib/kamal/configuration/role.rb @@ -58,10 +58,18 @@ class Kamal::Configuration::Role 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 85a966fd..81ac8b16 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 00000000..7d08c62f --- /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 00000000..73e7bfbf --- /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 00000000..bfe8c505 --- /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 77f0ff96..964f1d0b 100644 --- a/test/integration/broken_deploy_test.rb +++ b/test/integration/broken_deploy_test.rb @@ -27,6 +27,5 @@ class BrokenDeployTest < IntegrationTest 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 dc270aa9..0e6237df 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 dc270aa9..0e6237df 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 3b942665..c15af55b 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 ea445d9e..fd23e579 100644 --- a/test/integration/integration_test.rb +++ b/test/integration/integration_test.rb @@ -31,7 +31,7 @@ class IntegrationTest < ActiveSupport::TestCase 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 00000000..1f444266 --- /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/71] 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 f61ea229..dbf47a2b 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 5dc3546b..732195d3 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -44,7 +44,11 @@ class Kamal::Configuration::Proxy "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/71] 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 dbf47a2b..0c541c51 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 732195d3..df409d04 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -48,7 +48,8 @@ class Kamal::Configuration::Proxy "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/71] 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 0c541c51..db5b9674 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 df409d04..ee0321d4 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -45,7 +45,8 @@ class Kamal::Configuration::Proxy "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/71] 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 89808d72..9ae029f7 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -34,7 +34,6 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base 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 db5b9674..8d5b201f 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 ee0321d4..84a343b8 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 @@ class Kamal::Configuration::Proxy "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/71] 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 84a343b8..dd5aac1d 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -46,11 +46,11 @@ class Kamal::Configuration::Proxy "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/71] 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 40c9e4fe..116cf232 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/71] 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 7d08c62f..92ab3003 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 73e7bfbf..7e7a60cb 100644 --- a/test/commands/proxy_test.rb +++ b/test/commands/proxy_test.rb @@ -3,7 +3,7 @@ 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" ] + 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 bfe8c505..2912c645 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/71] 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 d546439c..87fee4b4 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 ad6e0613..94ef29e9 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 @@ class Kamal::Commands::App < Kamal::Commands::Base "--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 @@ class Kamal::Commands::App < Kamal::Commands::Base 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 629fa47f..2d828962 100644 --- a/lib/kamal/commands/app/containers.rb +++ b/lib/kamal/commands/app/containers.rb @@ -32,7 +32,7 @@ module Kamal::Commands::App::Containers 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 2966e095..bc9345df 100644 --- a/lib/kamal/commands/docker.rb +++ b/lib/kamal/commands/docker.rb @@ -19,6 +19,10 @@ class Kamal::Commands::Docker < Kamal::Commands::Base [ '[ "${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 9ae029f7..3a17afae 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -12,6 +12,7 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base 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 81ac8b16..225bf21d 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 92ab3003..e35c3c06 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 7e7a60cb..0a2cc6c0 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/71] 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 87fee4b4..ef7ae646 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -61,6 +61,43 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base 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 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base 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/71] 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 ef7ae646..1092d39c 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -88,7 +88,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base 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/71] 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 2bf9a786..bc84fe52 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 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base 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 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base 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 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base 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 f3b676d1..787f7d43 100644 --- a/lib/kamal/commands/accessory.rb +++ b/lib/kamal/commands/accessory.rb @@ -15,6 +15,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base "--name", service_name, "--detach", "--restart", "unless-stopped", + "--network", "kamal", *config.logging_args, *publish_args, *env_args, @@ -63,6 +64,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base 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 215821dc..4434c26a 100644 --- a/lib/kamal/commands/app/execution.rb +++ b/lib/kamal/commands/app/execution.rb @@ -11,6 +11,7 @@ module Kamal::Commands::App::Execution 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 0e3abc46..c9016f56 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 225bf21d..2968a7a3 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 23d304da..bc3df9ce 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 e385764e..caecd255 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/71] 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 1092d39c..81b734f7 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -11,7 +11,13 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base 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 4434c26a..215821dc 100644 --- a/lib/kamal/commands/app/execution.rb +++ b/lib/kamal/commands/app/execution.rb @@ -11,7 +11,6 @@ module Kamal::Commands::App::Execution 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 0ca2bfa9..c2cf0ed0 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 b58aeeb9..1eb05eff 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 1f444266..f5698592 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/71] 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 2968a7a3..225bf21d 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 caecd255..e385764e 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/71] 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 8d5b201f..2d101aef 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 dd5aac1d..69b79be7 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 @@ class Kamal::Configuration::Proxy 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 00000000..a4ee19bf --- /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 00000000..3aa3f85e --- /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/71] 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 732addae..aaa8cd50 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -44,7 +44,7 @@ class Kamal::Cli::App < Kamal::Cli::Base 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 @@ class Kamal::Cli::App < Kamal::Cli::Base 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 b6a09477..00e0fb73 100644 --- a/lib/kamal/cli/app/boot.rb +++ b/lib/kamal/cli/app/boot.rb @@ -53,7 +53,7 @@ class Kamal::Cli::App::Boot 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 81b734f7..b0bb33a4 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -52,7 +52,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base 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 2d828962..0bab388b 100644 --- a/lib/kamal/commands/app/containers.rb +++ b/lib/kamal/commands/app/containers.rb @@ -28,11 +28,4 @@ module Kamal::Commands::App::Containers 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 3a17afae..b50cd0f2 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 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base 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 2d101aef..c30117fb 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 69b79be7..db3dcd96 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -25,6 +25,10 @@ class Kamal::Configuration::Proxy 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 225bf21d..17ae3deb 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 e35c3c06..3d65207c 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 0a2cc6c0..71bb4f1c 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 c15af55b..2f5c6fc3 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/71] 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 b50cd0f2..42fe3524 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 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base 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 c30117fb..906d4032 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 db3dcd96..32abc382 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -25,8 +25,8 @@ class Kamal::Configuration::Proxy 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 17ae3deb..4460390e 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 3d65207c..b4d5aa78 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 71bb4f1c..f5e82a34 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/71] 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 116cf232..1b96467b 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -204,15 +204,11 @@ class Kamal::Configuration 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 8c465fd9..aa13fc04 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 aa78dda9..eaa881d9 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/71] 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 42fe3524..e13bc966 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -17,7 +17,7 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base "--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 32abc382..7dff0bbd 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 @@ class Kamal::Configuration::Proxy 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 b4d5aa78..20c773cb 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 f5e82a34..3b7c7191 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 2f5c6fc3..c15af55b 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/71] 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 9b95066f..924e1e27 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 437c5ce7..9947a04c 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 aaa8cd50..5b0535da 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -42,7 +42,7 @@ class Kamal::Cli::App < Kamal::Cli::Base 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 @@ class Kamal::Cli::App < Kamal::Cli::Base 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 00e0fb73..74da5a56 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 @@ class Kamal::Cli::App::Boot 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 @@ class Kamal::Cli::App::Boot 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 @@ class Kamal::Cli::App::Boot 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 249a1f6b..0643c157 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 @@ module Kamal::Cli::Healthcheck::Poller 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 fdbc2559..c0f5f1a7 100644 --- a/lib/kamal/cli/main.rb +++ b/lib/kamal/cli/main.rb @@ -35,13 +35,8 @@ class Kamal::Cli::Main < Kamal::Cli::Base 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 @@ class Kamal::Cli::Main < Kamal::Cli::Base 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 @@ class Kamal::Cli::Main < Kamal::Cli::Base 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 @@ class Kamal::Cli::Main < Kamal::Cli::Base 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 @@ class Kamal::Cli::Main < Kamal::Cli::Base 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 b0bb33a4..94d219aa 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 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base 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 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base 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 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base 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 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base 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 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base 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 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base 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 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base 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 7635e97d..aaf79980 100644 --- a/lib/kamal/cli/prune.rb +++ b/lib/kamal/cli/prune.rb @@ -28,7 +28,6 @@ class Kamal::Cli::Prune < Kamal::Cli::Base 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 119961b9..8ecf5d04 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 00000000..1435a677 --- /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 e3d9e3cc..00000000 --- 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 0dc25955..00000000 --- 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 994debb5..c07e8933 100644 --- a/lib/kamal/commander.rb +++ b/lib/kamal/commander.rb @@ -4,7 +4,7 @@ require "active_support/core_ext/object/blank" 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 @@ class Kamal::Commander @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 @@ class Kamal::Commander @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 12a710d8..288fd9b5 100644 --- a/lib/kamal/commander/specifics.rb +++ b/lib/kamal/commander/specifics.rb @@ -18,17 +18,8 @@ class Kamal::Commander::Specifics 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 94ef29e9..417ad463 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 @@ class Kamal::Commands::App < Kamal::Commands::Base 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 @@ class Kamal::Commands::App < Kamal::Commands::Base *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 4912992e..00000000 --- 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 e13bc966..8a9535f3 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -66,4 +66,13 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base 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 b820b5af..30b6eafd 100644 --- a/lib/kamal/commands/prune.rb +++ b/lib/kamal/commands/prune.rb @@ -20,10 +20,6 @@ class Kamal::Commands::Prune < Kamal::Commands::Base "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 @@ class Kamal::Commands::Prune < Kamal::Commands::Base 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 964ef3eb..00000000 --- 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 1b96467b..38dc2190 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 @@ class Kamal::Configuration @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 @@ class Kamal::Configuration 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 @@ class Kamal::Configuration end - def healthcheck_service - [ "healthcheck", service, destination ].compact.join("-") - end - def readiness_delay raw_config.readiness_delay || 7 end @@ -251,8 +241,7 @@ class Kamal::Configuration 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 8ebaaa0d..44910fda 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 3c55d5dd..00000000 --- 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 906d4032..82d00978 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 9f6962a5..8ed6e46c 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 756afa9e..00000000 --- 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 888068a4..00000000 --- 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 7dff0bbd..4232e850 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -14,18 +14,6 @@ class Kamal::Configuration::Proxy 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 @@ class Kamal::Configuration::Proxy 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 6579b9d0..7e5768ec 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 @@ class Kamal::Configuration::Role @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 @@ class Kamal::Configuration::Role 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 @@ class Kamal::Configuration::Role 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 @@ class Kamal::Configuration::Role 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 @@ class Kamal::Configuration::Role 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 @@ class Kamal::Configuration::Role end def assets? - asset_path.present? && running_traefik? + asset_path.present? && running_proxy? end def asset_volume(version = nil) @@ -241,27 +172,6 @@ class Kamal::Configuration::Role 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 45d8bac5..00000000 --- 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 4460390e..149fa3f0 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 @@ class CliAppTest < CliTestCase 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 2115f418..f82eef3e 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 20c773cb..1d98395f 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 33723f13..bb4ced51 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 29171150..00000000 --- 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 c2b78b21..54031e80 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 e385764e..69931901 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 f6234d6a..dc2afc5a 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 02871922..a8757251 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 430a13db..50e852bc 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 aa13fc04..648821b4 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 b13e3700..00000000 --- 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 c0b643bf..b9938a77 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 ac409e84..d8ac3e3b 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 eaa881d9..c64070eb 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 aa52f5ed..6bc2f837 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 4a5f934a..c45104de 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 a1270583..6b785a6a 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 2912c645..e91e9657 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 d83adeeb..6f6568c3 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 b7dcdc34..60e1cf6e 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 00000000..fb47d3cc --- /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 598ddb63..00000000 --- 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 00000000..65a70a00 --- /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 81269d24..00000000 --- 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 a85412ad..5e999402 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 00000000..fb47d3cc --- /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 598ddb63..00000000 --- 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 00000000..6413b6ef --- /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 81269d24..00000000 --- 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 c15af55b..835b4ffd 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 0cd511d9..4867519e 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 fd23e579..a8e149c1 100644 --- a/test/integration/integration_test.rb +++ b/test/integration/integration_test.rb @@ -101,8 +101,8 @@ class IntegrationTest < ActiveSupport::TestCase 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 @@ class IntegrationTest < ActiveSupport::TestCase 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 1eb05eff..fbcd0927 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 f5698592..752ec41b 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 @@ class ProxyTest < IntegrationTest 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 48f9ea02..00000000 --- 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/71] 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 82d00978..0ca6befd 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 4232e850..a294208d 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -36,7 +36,7 @@ class Kamal::Configuration::Proxy 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/71] 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 60e1cf6e..b7dcdc34 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/71] 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 38dc2190..96784e64 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -70,6 +70,7 @@ class Kamal::Configuration ensure_valid_kamal_version ensure_retain_containers_valid ensure_valid_service_name + ensure_no_traefik_reboot_hooks end @@ -303,6 +304,16 @@ class Kamal::Configuration 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 a4ee19bf..2b055570 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 3aa3f85e..6dac27ff 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 c64070eb..c10b8508 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/71] 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 0643c157..00646b62 100644 --- a/lib/kamal/cli/healthcheck/poller.rb +++ b/lib/kamal/cli/healthcheck/poller.rb @@ -31,31 +31,6 @@ module Kamal::Cli::Healthcheck::Poller 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/71] 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 00646b62..fd8714ce 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 @@ module Kamal::Cli::Healthcheck::Poller 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/71] 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 b7dcdc34..121a96a5 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/71] 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 96784e64..76e6d9ee 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -189,10 +189,6 @@ class Kamal::Configuration 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 69931901..9c159365 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -3,7 +3,6 @@ require "test_helper" 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 c10b8508..1df3fc3b 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/71] 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 85815506..37a9e046 100644 --- a/lib/kamal/cli/base.rb +++ b/lib/kamal/cli/base.rb @@ -101,7 +101,7 @@ module Kamal::Cli 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 @@ module Kamal::Cli 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 306c8a07..abbea71f 100644 --- a/lib/kamal/cli/lock.rb +++ b/lib/kamal/cli/lock.rb @@ -12,7 +12,7 @@ class Kamal::Cli::Lock < Kamal::Cli::Base 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 5b1b0cc7..452ab089 100644 --- a/lib/kamal/cli/server.rb +++ b/lib/kamal/cli/server.rb @@ -36,8 +36,6 @@ class Kamal::Cli::Server < Kamal::Cli::Base 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 9841f4fb..c1e65d18 100644 --- a/lib/kamal/commands/app/assets.rb +++ b/lib/kamal/commands/app/assets.rb @@ -3,18 +3,18 @@ module Kamal::Commands::App::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 @@ module Kamal::Commands::App::Assets 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 @@ module Kamal::Commands::App::Assets :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 9846d8e2..3589643d 100644 --- a/lib/kamal/commands/auditor.rb +++ b/lib/kamal/commands/auditor.rb @@ -9,7 +9,7 @@ class Kamal::Commands::Auditor < Kamal::Commands::Base # 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 @@ class Kamal::Commands::Auditor < Kamal::Commands::Base 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 fb781fe8..3bcb5eff 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 76e6d9ee..9d88bb66 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -194,6 +194,19 @@ class Kamal::Configuration ".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 @@ class Kamal::Configuration 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 57489f17..804a1502 100644 --- a/lib/kamal/configuration/accessory.rb +++ b/lib/kamal/configuration/accessory.rb @@ -63,7 +63,7 @@ class Kamal::Configuration::Accessory 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 7e5768ec..3f1e6d74 100644 --- a/lib/kamal/configuration/role.rb +++ b/lib/kamal/configuration/role.rb @@ -84,7 +84,7 @@ class Kamal::Configuration::Role 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 @@ class Kamal::Configuration::Role 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 c9016f56..5bb8762a 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 149fa3f0..6c37d620 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 3f3e9294..2ffebd9c 100644 --- a/test/cli/cli_test_case.rb +++ b/test/cli/cli_test_case.rb @@ -29,7 +29,7 @@ class CliTestCase < ActiveSupport::TestCase 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 f82eef3e..6600765b 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 110e217d..0436a3ee 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 bc3df9ce..19fe745d 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 9c159365..44062624 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 2abc8d81..d0f66610 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 648821b4..46cf4309 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 acfe991f..2615dab6 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 b9938a77..57719077 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/71] 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 8a9535f3..cf01fa19 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -17,7 +17,7 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base "--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 9d88bb66..264b83e9 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -207,10 +207,6 @@ class Kamal::Configuration 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 a294208d..2d2efdfb 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -59,8 +59,10 @@ class Kamal::Configuration::Proxy 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 1df3fc3b..81dbb5ac 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/71] 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 5b0535da..21a5ffdd 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -231,6 +231,7 @@ class Kamal::Cli::App < Kamal::Cli::Base stop remove_containers remove_images + remove_service_directory end end @@ -272,6 +273,20 @@ class Kamal::Cli::App < Kamal::Cli::Base 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 c0f5f1a7..83391062 100644 --- a/lib/kamal/cli/main.rb +++ b/lib/kamal/cli/main.rb @@ -181,8 +181,8 @@ class Kamal::Cli::Main < Kamal::Cli::Base 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 3bcb5eff..a619a82d 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 6600765b..0956ba51 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/71] 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 3589643d..9846d8e2 100644 --- a/lib/kamal/commands/auditor.rb +++ b/lib/kamal/commands/auditor.rb @@ -9,7 +9,7 @@ class Kamal::Commands::Auditor < Kamal::Commands::Base # 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 @@ class Kamal::Commands::Auditor < Kamal::Commands::Base 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 0956ba51..7f878e5b 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 d0f66610..2abc8d81 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/71] 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 bc84fe52..e73c6f39 100644 --- a/lib/kamal/cli/accessory.rb +++ b/lib/kamal/cli/accessory.rb @@ -207,12 +207,12 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base 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 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base 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 21a5ffdd..7359fdde 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -231,7 +231,7 @@ class Kamal::Cli::App < Kamal::Cli::Base stop remove_containers remove_images - remove_service_directory + remove_app_directory end end @@ -273,15 +273,15 @@ class Kamal::Cli::App < Kamal::Cli::Base 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 37a9e046..0164765d 100644 --- a/lib/kamal/cli/base.rb +++ b/lib/kamal/cli/base.rb @@ -176,7 +176,7 @@ module Kamal::Cli 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 94d219aa..4b02800f 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -150,11 +150,15 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base 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 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base 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 787f7d43..22220d3f 100644 --- a/lib/kamal/commands/accessory.rb +++ b/lib/kamal/commands/accessory.rb @@ -90,7 +90,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base 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 cf01fa19..f5ea8cf4 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -67,6 +67,10 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base 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 a619a82d..e8bf97e3 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 264b83e9..1d6a445e 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -194,16 +194,24 @@ class Kamal::Configuration ".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 2d2efdfb..b7bd7ba9 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -61,7 +61,7 @@ class Kamal::Configuration::Proxy 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 5bb8762a..2dbff62d 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 1d98395f..0589e525 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 - run_command("remove") + 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").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 46cf4309..5db2ac59 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 121a96a5..40a896b1 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 a8e149c1..5c675da6 100644 --- a/test/integration/integration_test.rb +++ b/test/integration/integration_test.rb @@ -148,4 +148,16 @@ class IntegrationTest < ActiveSupport::TestCase 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 fbcd0927..cefe2100 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 752ec41b..d796eb6d 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/71] 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 d796eb6d..efab2583 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/71] 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 7359fdde..195b5ec7 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -4,7 +4,7 @@ class Kamal::Cli::App < Kamal::Cli::Base 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 74da5a56..6939825a 100644 --- a/lib/kamal/cli/app/boot.rb +++ b/lib/kamal/cli/app/boot.rb @@ -58,6 +58,9 @@ class Kamal::Cli::App::Boot 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 fd8714ce..fad51845 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 1d6a445e..bd33b08c 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -189,6 +189,10 @@ class Kamal::Configuration 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 44910fda..2e7b618d 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 6c37d620..5eaf77a6 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 1eb8cc5c..d831a5b4 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/71] Not experimental --- bin/docs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/docs b/bin/docs index 9947a04c..b76872d3 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/71] 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 0164765d..c5a3e376 100644 --- a/lib/kamal/cli/base.rb +++ b/lib/kamal/cli/base.rb @@ -101,7 +101,7 @@ module Kamal::Cli 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 @@ module Kamal::Cli 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 abbea71f..08afb1f4 100644 --- a/lib/kamal/cli/lock.rb +++ b/lib/kamal/cli/lock.rb @@ -12,7 +12,7 @@ class Kamal::Cli::Lock < Kamal::Cli::Base 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/71] 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 83391062..46ce1583 100644 --- a/lib/kamal/cli/main.rb +++ b/lib/kamal/cli/main.rb @@ -206,7 +206,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base 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/71] 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 4b02800f..bfeb8073 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -72,7 +72,6 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base 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 b7bd7ba9..810f7517 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/71] 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 5eaf77a6..860078cf 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 0589e525..8bc0f895 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 3b7c7191..36e04c7b 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/71] 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 bfeb8073..b3c47a49 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -60,7 +60,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base 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 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base "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/71] 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 e73c6f39..bc84fe52 100644 --- a/lib/kamal/cli/accessory.rb +++ b/lib/kamal/cli/accessory.rb @@ -207,12 +207,12 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base 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 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base 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 195b5ec7..2a02ed65 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -281,7 +281,7 @@ class Kamal::Cli::App < Kamal::Cli::Base 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 c5a3e376..b3af23cc 100644 --- a/lib/kamal/cli/base.rb +++ b/lib/kamal/cli/base.rb @@ -101,7 +101,7 @@ module Kamal::Cli 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 @@ module Kamal::Cli 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 08afb1f4..ceab1f27 100644 --- a/lib/kamal/cli/lock.rb +++ b/lib/kamal/cli/lock.rb @@ -12,7 +12,7 @@ class Kamal::Cli::Lock < Kamal::Cli::Base 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 b3c47a49..3ac02c76 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -192,7 +192,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base 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 22220d3f..787f7d43 100644 --- a/lib/kamal/commands/accessory.rb +++ b/lib/kamal/commands/accessory.rb @@ -90,7 +90,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base 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 395b6f3a..aafaec70 100644 --- a/lib/kamal/commands/lock.rb +++ b/lib/kamal/commands/lock.rb @@ -44,14 +44,10 @@ class Kamal::Commands::Lock < Kamal::Commands::Base "/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 e8bf97e3..305903f6 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 2dbff62d..5bb8762a 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 860078cf..f3bd9fe5 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 2ffebd9c..5a2bb76f 100644 --- a/test/cli/cli_test_case.rb +++ b/test/cli/cli_test_case.rb @@ -31,11 +31,11 @@ class CliTestCase < ActiveSupport::TestCase 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 f6874d49..66482153 100644 --- a/test/cli/lock_test.rb +++ b/test/cli/lock_test.rb @@ -3,7 +3,7 @@ require_relative "cli_test_case" 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 7f878e5b..607fee21 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 0436a3ee..110e217d 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 a8757251..0ad0ba46 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 5db2ac59..648821b4 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/71] 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 3ac02c76..108d7855 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -10,6 +10,12 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base 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 f5ea8cf4..354493db 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -46,6 +46,12 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base 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 810f7517..a3161e1b 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 @@ class Kamal::Configuration::Proxy 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 266d6a96..ab8dd50e 100644 --- a/lib/kamal/utils.rb +++ b/lib/kamal/utils.rb @@ -101,4 +101,8 @@ module Kamal::Utils 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 8bc0f895..2a0834fc 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 36e04c7b..349e1a1b 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 cefe2100..5a4eb91c 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 efab2583..f9c7133b 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/71] 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 0ca6befd..e754eb36 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 a3161e1b..2671b4c8 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -37,7 +37,7 @@ class Kamal::Configuration::Proxy 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 2b055570..a4ee19bf 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 6dac27ff..3aa3f85e 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 From 7f15fd143f7bae9298510d79ca9ca401fe4d7394 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Tue, 17 Sep 2024 12:55:11 +0100 Subject: [PATCH 44/71] Upgrade commands for Kamal 1.x -> 2.0 Adds: - `kamal upgrade` to upgrade all app hosts and accessory hosts - `kamal proxy upgrade` to upgrade the proxy on all hosts - `kamal accessory upgrade [name]` to upgrade accessories on all hosts Upgrade takes rolling and confirmed options and calls `proxy upgrade` and `accessory upgrade` in turn. To just upgrade a single host add -h [host] to the command. But the upgrade should run on all hosts, not just those running the proxy. Calling upgrade on a host that has already been upgraded should work ok. Upgrading hosts causes downtime but you can avoid if you run multiple hosts by: 1. Implementing the pre-proxy-reboot and post-proxy-reboot hooks to remove the host from external load balancers 2. Running the upgrade with the --rolling option **kamal proxy upgrade** 1. Creates a `kamal` network if required 2. Stops and removes the old proxy (whether Traefik or kamal-proxy) 3. Starts a kamal-proxy container in the `kamal` network 4. Reboots the app containers in the `kamal` network **kamal accessory upgrade [name]** 1. Creates a `kamal` network if required 2. Reboots the accessory containers in the `kamal` network A matching `downgrade` command will be added to Kamal 1.9. --- lib/kamal/cli/accessory.rb | 23 +++++++++++ lib/kamal/cli/base.rb | 4 ++ lib/kamal/cli/main.rb | 31 +++++++++++++++ lib/kamal/cli/proxy.rb | 13 ++---- lib/kamal/commander.rb | 7 ++++ test/cli/accessory_test.rb | 21 ++++++++++ test/cli/main_test.rb | 28 +++++++++++++ test/cli/proxy_test.rb | 61 +++++++++++++++++++++++++++++ test/fixtures/deploy_with_proxy.yml | 1 + 9 files changed, 180 insertions(+), 9 deletions(-) diff --git a/lib/kamal/cli/accessory.rb b/lib/kamal/cli/accessory.rb index bc84fe52..e6dc78cc 100644 --- a/lib/kamal/cli/accessory.rb +++ b/lib/kamal/cli/accessory.rb @@ -218,6 +218,29 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base end end + desc "upgrade", "Upgrade accessories from Kamal 1.x to 2.0 (restart them in 'kamal' network)" + option :rolling, type: :boolean, default: false, desc: "Upgrade one host at a time" + option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question" + def upgrade(name) + confirming "This will restart all accessories" do + with_lock do + if options[:rolling] + KAMAL.accessory_hosts.each do |host| + say "Upgrading accessories on #{host}...", :magenta + KAMAL.with_specific_hosts(host) do + reboot name + end + say "Upgraded accessories on #{host}...", :magenta + end + else + say "Upgrading accessories on all hosts...", :magenta + reboot name + say "Upgraded accessories on all hosts", :magenta + end + end + end + end + private def with_accessory(name) if KAMAL.config.accessory(name) diff --git a/lib/kamal/cli/base.rb b/lib/kamal/cli/base.rb index b3af23cc..f594c54e 100644 --- a/lib/kamal/cli/base.rb +++ b/lib/kamal/cli/base.rb @@ -174,6 +174,10 @@ module Kamal::Cli instance_variable_get("@_invocations").first end + def reset_invocation(cli_class) + instance_variable_get("@_invocations")[cli_class].pop + end + def ensure_run_directory on(KAMAL.hosts) do execute(*KAMAL.server.ensure_run_directory) diff --git a/lib/kamal/cli/main.rb b/lib/kamal/cli/main.rb index 46ce1583..3a56be4c 100644 --- a/lib/kamal/cli/main.rb +++ b/lib/kamal/cli/main.rb @@ -189,6 +189,37 @@ class Kamal::Cli::Main < Kamal::Cli::Base end end + desc "upgrade", "Upgrade from Kamal 1.x to 2.0" + option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question" + option :rolling, type: :boolean, default: false, desc: "Upgrade one host at a time" + def upgrade + confirming "This will replace Traefik with kamal-proxy and restart all accessories" do + with_lock do + if options[:rolling] + (KAMAL.hosts | KAMAL.accessory_hosts).each do |host| + KAMAL.with_specific_hosts(host) do + say "Upgrading #{host}...", :magenta + if KAMAL.hosts.include?(host) + invoke "kamal:cli:proxy:upgrade", [], options.merge(confirmed: true, rolling: false) + reset_invocation(Kamal::Cli::Proxy) + end + if KAMAL.accessory_hosts.include?(host) + invoke "kamal:cli:accessory:upgrade", [ "all" ], options.merge(confirmed: true, rolling: false) + reset_invocation(Kamal::Cli::Accessory) + end + say "Upgraded #{host}", :magenta + end + end + else + say "Upgrading all hosts...", :magenta + invoke "kamal:cli:proxy:upgrade", [], options.merge(confirmed: true) + invoke "kamal:cli:accessory:upgrade", [ "all" ], options.merge(confirmed: true) + say "Upgraded all hosts", :magenta + end + end + end + end + desc "version", "Show Kamal version" def version puts Kamal::VERSION diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index 108d7855..0e6a74ae 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -62,7 +62,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base end end - desc "upgrade", "Upgrade to correct proxy on servers (stop container, remove container, start new container)" + desc "upgrade", "Upgrade to kamal-proxy on servers (stop container, remove container, start new container, reboot app)" 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 @@ -72,6 +72,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base host_groups = options[:rolling] ? KAMAL.hosts : [ KAMAL.hosts ] host_groups.each do |hosts| host_list = Array(hosts).join(",") + say "Upgrading proxy on #{host_list}..." run_hook "pre-proxy-reboot", hosts: host_list on(hosts) do |host| execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug @@ -86,19 +87,17 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base execute *KAMAL.proxy.remove_image end - begin - old_hosts, KAMAL.specific_hosts = KAMAL.specific_hosts, hosts + KAMAL.with_specific_hosts(hosts) do 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 + say "Upgraded proxy on #{host_list}" end end end @@ -204,10 +203,6 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base 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 diff --git a/lib/kamal/commander.rb b/lib/kamal/commander.rb index c07e8933..1557df57 100644 --- a/lib/kamal/commander.rb +++ b/lib/kamal/commander.rb @@ -65,6 +65,13 @@ class Kamal::Commander end end + def with_specific_hosts(hosts) + original_hosts, self.specific_hosts = specific_hosts, hosts + yield + ensure + self.specific_hosts = original_hosts + end + def accessory_names config.accessories&.collect(&:name) || [] end diff --git a/test/cli/accessory_test.rb b/test/cli/accessory_test.rb index 5bb8762a..072962e1 100644 --- a/test/cli/accessory_test.rb +++ b/test/cli/accessory_test.rb @@ -220,6 +220,27 @@ class CliAccessoryTest < CliTestCase end end + test "upgrade" do + run_command("upgrade", "-y", "all").tap do |output| + assert_match "Upgrading accessories on all hosts...", output + assert_match "docker network create kamal on 1.1.1.3", output + assert_match "docker container stop app-mysql on 1.1.1.3", output + assert_match "docker run --name app-mysql --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST="%" --env-file .kamal/apps/app/env/accessories/mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output + assert_match "Upgraded accessories on all hosts", output + end + end + + test "upgrade rolling" do + run_command("upgrade", "--rolling", "-y", "all").tap do |output| + assert_match "Upgrading accessories on 1.1.1.3...", output + assert_match "docker network create kamal on 1.1.1.3", output + assert_match "docker container stop app-mysql on 1.1.1.3", output + assert_match "docker run --name app-mysql --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST="%" --env-file .kamal/apps/app/env/accessories/mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output + assert_match "Upgraded accessories on 1.1.1.3", output + end + end + + private def run_command(*command) stdouted { Kamal::Cli::Accessory.start([ *command, "-c", "test/fixtures/deploy_with_accessories.yml" ]) } diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index 607fee21..60fdf5c7 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -486,6 +486,34 @@ class CliMainTest < CliTestCase end end + test "upgrade" do + invoke_options = { "config_file" => "test/fixtures/deploy_with_accessories.yml", "skip_hooks" => false, "confirmed" => true, "rolling" => false } + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:upgrade", [], invoke_options) + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:upgrade", [ "all" ], invoke_options) + + run_command("upgrade", "-y", config_file: "deploy_with_accessories").tap do |output| + assert_match "Upgrading all hosts...", output + assert_match "Upgraded all hosts", output + end + end + + test "upgrade rolling" do + invoke_options = { "config_file" => "test/fixtures/deploy_with_accessories.yml", "skip_hooks" => false, "confirmed" => true, "rolling" => false } + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:upgrade", [], invoke_options).times(4) + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:upgrade", [ "all" ], invoke_options).times(4) + + run_command("upgrade", "--rolling", "-y", config_file: "deploy_with_accessories").tap do |output| + assert_match "Upgrading 1.1.1.1...", output + assert_match "Upgraded 1.1.1.1", output + assert_match "Upgrading 1.1.1.2...", output + assert_match "Upgraded 1.1.1.2", output + assert_match "Upgrading 1.1.1.3...", output + assert_match "Upgraded 1.1.1.3", output + assert_match "Upgrading 1.1.1.4...", output + assert_match "Upgraded 1.1.1.4", output + end + end + private def run_command(*command, config_file: "deploy_simple") with_argv([ *command, "-c", "test/fixtures/#{config_file}.yml" ]) do diff --git a/test/cli/proxy_test.rb b/test/cli/proxy_test.rb index 2a0834fc..97520071 100644 --- a/test/cli/proxy_test.rb +++ b/test/cli/proxy_test.rb @@ -182,6 +182,67 @@ class CliProxyTest < CliTestCase end end + test "upgrade" do + Object.any_instance.stubs(:sleep) + + SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("12345678") + + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :inspect, "kamal-proxy", "--format '{{.Config.Image}}'", "|", :cut, "-d:", "-f2") + .returns("v0.1.0") + + 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 + + run_command("upgrade", "-y").tap do |output| + assert_match "Upgrading proxy on 1.1.1.1,1.1.1.2,1.1.1.3,1.1.1.4...", output + assert_match "docker login -u [REDACTED] -p [REDACTED]", output + assert_match "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", 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 "/usr/bin/env mkdir -p .kamal", output + assert_match "docker network create kamal", output + assert_match "docker login -u [REDACTED] -p [REDACTED]", 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\" basecamp/kamal-proxy:v0.1.0", output + assert_match "/usr/bin/env mkdir -p .kamal", output + assert_match %r{docker rename app-web-latest app-web-latest_replaced_.*}, output + assert_match "/usr/bin/env mkdir -p .kamal/apps/app/env/roles", output + assert_match %r{/usr/bin/env .* .kamal/apps/app/env/roles/web.env}, output + assert_match %r{docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-.* -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 \"12345678:80\" --deploy-timeout \"6s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\"", output + assert_match "docker container ls --all --filter name=^app-web-12345678$ --quiet | xargs docker stop", output + assert_match "docker tag dhh/app:latest dhh/app:latest", output + assert_match "/usr/bin/env mkdir -p .kamal", 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", output + assert_match "docker image prune --force --filter label=service=app", output + assert_match "Upgraded proxy on 1.1.1.1,1.1.1.2,1.1.1.3,1.1.1.4", output + end + end + + test "upgrade rolling" do + Object.any_instance.stubs(:sleep) + + SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("12345678") + + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :inspect, "kamal-proxy", "--format '{{.Config.Image}}'", "|", :cut, "-d:", "-f2") + .returns("v0.1.0") + + 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 + + run_command("upgrade", "--rolling", "-y",).tap do |output| + %w[1.1.1.1 1.1.1.2 1.1.1.3 1.1.1.4].each do |host| + assert_match "Upgrading proxy on #{host}...", output + assert_match "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 #{host}", output + assert_match "Upgraded proxy on #{host}", output + end + 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/fixtures/deploy_with_proxy.yml b/test/fixtures/deploy_with_proxy.yml index e91e9657..a1f7a853 100644 --- a/test/fixtures/deploy_with_proxy.yml +++ b/test/fixtures/deploy_with_proxy.yml @@ -39,3 +39,4 @@ accessories: - data:/data readiness_delay: 0 +readiness_timeout: 1 From 0fe6a17a918fae595c5121c5c338be7990646be0 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Tue, 17 Sep 2024 13:23:15 +0100 Subject: [PATCH 45/71] Need a duration string for the response timeout Add `s` as the timeout is a duration. --- lib/kamal/configuration/docs/proxy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/kamal/configuration/docs/proxy.yml b/lib/kamal/configuration/docs/proxy.yml index e754eb36..8e80d0be 100644 --- a/lib/kamal/configuration/docs/proxy.yml +++ b/lib/kamal/configuration/docs/proxy.yml @@ -36,8 +36,8 @@ proxy: # Response timeout # - # How long to wait for requests to complete before timing out, defaults to 30 seconds - response_timeout: 10 + # How long to wait for requests to complete before timing out, defaults to 10 seconds + response_timeout: 30s # Healthcheck # From f183419f7ae250e5f926785927d31d16e06bdb29 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Tue, 17 Sep 2024 14:21:43 +0100 Subject: [PATCH 46/71] Ensure we don't load the secrets more than once Secrets were being created twice, which might require logging in twice. --- lib/kamal/configuration.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index bd33b08c..1d6dfd12 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -48,6 +48,8 @@ class Kamal::Configuration validate! raw_config, example: validation_yml.symbolize_keys, context: "", with: Kamal::Configuration::Validator::Configuration + @secrets = Kamal::Secrets.new(destination: destination) + # Eager load config to validate it, these are first as they have dependencies later on @servers = Servers.new(config: self) @registry = Registry.new(config: self) @@ -63,8 +65,6 @@ class Kamal::Configuration @ssh = Ssh.new(config: self) @sshkit = Sshkit.new(config: self) - @secrets = Kamal::Secrets.new(destination: destination) - ensure_destination_if_required ensure_required_keys_present ensure_valid_kamal_version @@ -259,10 +259,6 @@ class Kamal::Configuration }.compact end - def secrets - @secrets ||= Kamal::Secrets.new(destination: destination) - end - private # Will raise ArgumentError if any required config keys are missing def ensure_destination_if_required From 46c0836cd4f4e6d0dec5d177644680973796fb01 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 18 Sep 2024 09:18:28 +0100 Subject: [PATCH 47/71] Upgrade on accessory hosts only with correct messages --- lib/kamal/cli/accessory.rb | 18 +++++++----------- lib/kamal/cli/proxy.rb | 4 ++-- lib/kamal/commander/specifics.rb | 2 +- test/cli/accessory_test.rb | 8 ++++---- test/cli/main_test.rb | 2 +- 5 files changed, 15 insertions(+), 19 deletions(-) diff --git a/lib/kamal/cli/accessory.rb b/lib/kamal/cli/accessory.rb index e6dc78cc..dd56231e 100644 --- a/lib/kamal/cli/accessory.rb +++ b/lib/kamal/cli/accessory.rb @@ -224,18 +224,14 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base def upgrade(name) confirming "This will restart all accessories" do with_lock do - if options[:rolling] - KAMAL.accessory_hosts.each do |host| - say "Upgrading accessories on #{host}...", :magenta - KAMAL.with_specific_hosts(host) do - reboot name - end - say "Upgraded accessories on #{host}...", :magenta + host_groups = options[:rolling] ? KAMAL.accessory_hosts : [ KAMAL.accessory_hosts ] + host_groups.each do |hosts| + host_list = Array(hosts).join(",") + KAMAL.with_specific_hosts(hosts) do + say "Upgrading #{name} accessories on #{host_list}...", :magenta + reboot name + say "Upgraded #{name} accessories on #{host_list}...", :magenta end - else - say "Upgrading accessories on all hosts...", :magenta - reboot name - say "Upgraded accessories on all hosts", :magenta end end end diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index 0e6a74ae..545cee2a 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -72,7 +72,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base host_groups = options[:rolling] ? KAMAL.hosts : [ KAMAL.hosts ] host_groups.each do |hosts| host_list = Array(hosts).join(",") - say "Upgrading proxy on #{host_list}..." + say "Upgrading proxy on #{host_list}...", :magenta run_hook "pre-proxy-reboot", hosts: host_list on(hosts) do |host| execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug @@ -97,7 +97,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base end run_hook "post-proxy-reboot", hosts: host_list - say "Upgraded proxy on #{host_list}" + say "Upgraded proxy on #{host_list}", :magenta end end end diff --git a/lib/kamal/commander/specifics.rb b/lib/kamal/commander/specifics.rb index 288fd9b5..190d2b69 100644 --- a/lib/kamal/commander/specifics.rb +++ b/lib/kamal/commander/specifics.rb @@ -23,7 +23,7 @@ class Kamal::Commander::Specifics end def accessory_hosts - specific_hosts || config.accessories.flat_map(&:hosts) + config.accessories.flat_map(&:hosts) & specified_hosts end private diff --git a/test/cli/accessory_test.rb b/test/cli/accessory_test.rb index 072962e1..b8da2a7a 100644 --- a/test/cli/accessory_test.rb +++ b/test/cli/accessory_test.rb @@ -222,21 +222,21 @@ class CliAccessoryTest < CliTestCase test "upgrade" do run_command("upgrade", "-y", "all").tap do |output| - assert_match "Upgrading accessories on all hosts...", output + assert_match "Upgrading all accessories on 1.1.1.3,1.1.1.1,1.1.1.2...", output assert_match "docker network create kamal on 1.1.1.3", output assert_match "docker container stop app-mysql on 1.1.1.3", output assert_match "docker run --name app-mysql --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST="%" --env-file .kamal/apps/app/env/accessories/mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output - assert_match "Upgraded accessories on all hosts", output + assert_match "Upgraded all accessories on 1.1.1.3,1.1.1.1,1.1.1.2...", output end end test "upgrade rolling" do run_command("upgrade", "--rolling", "-y", "all").tap do |output| - assert_match "Upgrading accessories on 1.1.1.3...", output + assert_match "Upgrading all accessories on 1.1.1.3...", output assert_match "docker network create kamal on 1.1.1.3", output assert_match "docker container stop app-mysql on 1.1.1.3", output assert_match "docker run --name app-mysql --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST="%" --env-file .kamal/apps/app/env/accessories/mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output - assert_match "Upgraded accessories on 1.1.1.3", output + assert_match "Upgraded all accessories on 1.1.1.3", output end end diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index 60fdf5c7..4b111bad 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -500,7 +500,7 @@ class CliMainTest < CliTestCase test "upgrade rolling" do invoke_options = { "config_file" => "test/fixtures/deploy_with_accessories.yml", "skip_hooks" => false, "confirmed" => true, "rolling" => false } Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:upgrade", [], invoke_options).times(4) - Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:upgrade", [ "all" ], invoke_options).times(4) + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:upgrade", [ "all" ], invoke_options).times(3) run_command("upgrade", "--rolling", "-y", config_file: "deploy_with_accessories").tap do |output| assert_match "Upgrading 1.1.1.1...", output From 34effef70a2fb8c6f00e81f0c0de3ae36aafe453 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 18 Sep 2024 14:00:43 +0100 Subject: [PATCH 48/71] Update proxy and docs for Kamal 2.0/kamal-proxy 0.3.0 Update to kamal-proxy 0.3.0 and improve docs making sure they are in sync with that version. --- lib/kamal/cli/main.rb | 2 +- lib/kamal/cli/proxy.rb | 2 +- lib/kamal/configuration/docs/builder.yml | 27 ++++++++++++++--------- lib/kamal/configuration/docs/env.yml | 17 +++++++------- lib/kamal/configuration/docs/proxy.yml | 26 ++++++++++++++++------ lib/kamal/configuration/docs/registry.yml | 6 +++-- lib/kamal/configuration/docs/role.yml | 1 - lib/kamal/configuration/proxy.rb | 2 +- test/cli/proxy_test.rb | 10 ++++----- test/integration/main_test.rb | 2 +- test/integration/proxy_test.rb | 4 ++-- 11 files changed, 59 insertions(+), 40 deletions(-) diff --git a/lib/kamal/cli/main.rb b/lib/kamal/cli/main.rb index 3a56be4c..a47d8592 100644 --- a/lib/kamal/cli/main.rb +++ b/lib/kamal/cli/main.rb @@ -123,7 +123,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base end end - desc "docs", "Show Kamal documentation for configuration setting" + desc "docs [SECTION]", "Show Kamal configuration documentation" def docs(section = nil) case section when NilClass diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index 545cee2a..12d4c6b0 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -62,7 +62,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base end end - desc "upgrade", "Upgrade to kamal-proxy on servers (stop container, remove container, start new container, reboot app)" + desc "upgrade", "Upgrade to kamal-proxy on servers (stop container, remove container, start new container, reboot app)", hide: true 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 diff --git a/lib/kamal/configuration/docs/builder.yml b/lib/kamal/configuration/docs/builder.yml index 6209a4a6..1c41a386 100644 --- a/lib/kamal/configuration/docs/builder.yml +++ b/lib/kamal/configuration/docs/builder.yml @@ -12,25 +12,25 @@ # # Options go under the builder key in the root configuration. builder: - # Driver - # - # The build driver to use, defaults to `docker-container` - driver: docker # Arch # - # The architectures to build for, defaults to `[ amd64, arm64 ]` - # Unless you are using the docker driver, when it defaults to the local architecture - # You can set an array or just a single value + # The architectures to build for - you can set an array or just a single value. + # + # Allowed values are `amd64` and `arm64` arch: - amd64 - # Remote configuration + # Remote # - # If you have a remote builder, you can configure it here + # The connection string for a remote builder. If supplied Kamal will use this + # for builds that do not match the local architecture of the deployment host. remote: ssh://docker@docker-builder - # Whether to allow local builds + # Local + # + # If set to false, Kamal will always use the remote builder even when building + # the local architecture. # # Defaults to true local: true @@ -78,7 +78,7 @@ builder: # Build secrets # - # Values are read from the environment. + # Values are read from the .kamal/secrets. # secrets: - SECRET1 @@ -103,3 +103,8 @@ builder: # # SSH agent socket or keys to expose to the build ssh: default=$SSH_AUTH_SOCK + + # Driver + # + # The build driver to use, defaults to `docker-container` + driver: docker diff --git a/lib/kamal/configuration/docs/env.yml b/lib/kamal/configuration/docs/env.yml index c2cf0ed0..513799d4 100644 --- a/lib/kamal/configuration/docs/env.yml +++ b/lib/kamal/configuration/docs/env.yml @@ -1,7 +1,7 @@ # Environment variables # # Environment variables can be set directly in the Kamal configuration or -# loaded from a .env file, for secrets that should not be checked into Git. +# read from .kamal/secrets. # Reading environment variables from the configuration # @@ -12,19 +12,20 @@ env: DATABASE_HOST: mysql-db1 DATABASE_PORT: 3306 -# Using .env file to load required environment variables +# Using .kamal/secrets file to load required environment variables # -# Kamal uses dotenv to automatically load environment variables set in the .env file present -# in the application root. +# Kamal uses dotenv to automatically load environment variables set in the .kamal/secrets file. # # This file can be used to set variables like KAMAL_REGISTRY_PASSWORD or database passwords. -# But for this reason you must ensure that .env files are not checked into Git or included -# in your Dockerfile! The format is just key-value like: +# You can use variable or command substitution in the secrets file. +# # ``` -# KAMAL_REGISTRY_PASSWORD=pw -# DB_PASSWORD=secret123 +# KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD +# RAILS_MASTER_KEY=$(cat config/master.key) # ``` # +# If you store secrets directly in .kamal/secrets, ensure that it is not checked into version control. +# # 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. # diff --git a/lib/kamal/configuration/docs/proxy.yml b/lib/kamal/configuration/docs/proxy.yml index 8e80d0be..6840a75c 100644 --- a/lib/kamal/configuration/docs/proxy.yml +++ b/lib/kamal/configuration/docs/proxy.yml @@ -1,5 +1,9 @@ # Proxy # +# Kamal uses [kamal-proxy](https://github.com/basecamp/kamal-proxy) to provide +# gapless deployments. It runs on ports 80 and 443 and forwards requests to the +# application container. +# # 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 # @@ -13,20 +17,25 @@ proxy: # 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. + # requests for other apps deployed on that server that do have a host set. host: foo.example.com # App port # # The port the application container is exposed on + # # Defaults to 80 app_port: 3000 # 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. + # kamal-proxy can provide automatic HTTPS for your application via Let's Encrypt. + # + # This requires that we are deploying to a one server and the host option is set. + # The host value must point to the server we are deploying to and port 443 must be + # open for the Let's Encrypt challenge to succeed. + # + # Defaults to false ssl: true # Deploy timeout @@ -36,8 +45,8 @@ proxy: # Response timeout # - # How long to wait for requests to complete before timing out, defaults to 10 seconds - response_timeout: 30s + # How long to wait for requests to complete before timing out, defaults to 30 seconds + response_timeout: 10s # Healthcheck # @@ -70,7 +79,7 @@ proxy: # # 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 + # By default, Cache-Control, Last-Modified and User-Agent request headers are logged logging: request_headers: - Cache-Control @@ -84,4 +93,7 @@ proxy: # 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. + # + # By default kamal-proxy will not forward the headers the ssl option is set to true, and + # will forward them if it is set to false. forward_headers: true diff --git a/lib/kamal/configuration/docs/registry.yml b/lib/kamal/configuration/docs/registry.yml index 3254e454..3fea9ad6 100644 --- a/lib/kamal/configuration/docs/registry.yml +++ b/lib/kamal/configuration/docs/registry.yml @@ -27,11 +27,13 @@ registry: # and [set up roles and permissions](https://cloud.google.com/artifact-registry/docs/access-control#permissions). # Normally, assigning a roles/artifactregistry.writer role should be sufficient. # -# Once the service account is ready, you need to generate and download a JSON key, base64 encode it and add to .env: +# Once the service account is ready, you need to generate and download a JSON key and base64 encode it: # # ```shell -# echo "KAMAL_REGISTRY_PASSWORD=$(base64 -i /path/to/key.json)" | tr -d "\\n" >> .env +# base64 -i /path/to/key.json | tr -d "\\n") # ``` +# You'll then need to set the KAMAL_REGISTRY_PASSWORD secret to that value. +# # Use the env variable as password along with _json_key_base64 as username. # Here’s the final configuration: diff --git a/lib/kamal/configuration/docs/role.yml b/lib/kamal/configuration/docs/role.yml index 8ed6e46c..0f75c550 100644 --- a/lib/kamal/configuration/docs/role.yml +++ b/lib/kamal/configuration/docs/role.yml @@ -47,4 +47,3 @@ servers: env: ... asset_path: /public - diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb index 2671b4c8..8cce49b0 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -1,7 +1,7 @@ class Kamal::Configuration::Proxy include Kamal::Configuration::Validation - MINIMUM_VERSION = "v0.1.0" + MINIMUM_VERSION = "v0.3.0" DEFAULT_HTTP_PORT = 80 DEFAULT_HTTPS_PORT = 443 DEFAULT_IMAGE = "basecamp/kamal-proxy:#{MINIMUM_VERSION}" diff --git a/test/cli/proxy_test.rb b/test/cli/proxy_test.rb index 97520071..9466ec41 100644 --- a/test/cli/proxy_test.rb +++ b/test/cli/proxy_test.rb @@ -22,7 +22,7 @@ class CliProxyTest < CliTestCase 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" + assert_includes exception.message, "kamal-proxy version v0.0.1 is too old, please reboot to update to at least #{Kamal::Configuration::Proxy::MINIMUM_VERSION}" ensure Thread.report_on_exception = false end @@ -31,7 +31,7 @@ class CliProxyTest < CliTestCase 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") + .returns(Kamal::Configuration::Proxy::MINIMUM_VERSION) .at_least_once run_command("boot").tap do |output| @@ -189,7 +189,7 @@ class CliProxyTest < CliTestCase SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:docker, :inspect, "kamal-proxy", "--format '{{.Config.Image}}'", "|", :cut, "-d:", "-f2") - .returns("v0.1.0") + .returns(Kamal::Configuration::Proxy::MINIMUM_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}}'") @@ -205,7 +205,7 @@ class CliProxyTest < CliTestCase assert_match "/usr/bin/env mkdir -p .kamal", output assert_match "docker network create kamal", output assert_match "docker login -u [REDACTED] -p [REDACTED]", 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\" basecamp/kamal-proxy:v0.1.0", 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\" basecamp/kamal-proxy:#{Kamal::Configuration::Proxy::MINIMUM_VERSION}", output assert_match "/usr/bin/env mkdir -p .kamal", output assert_match %r{docker rename app-web-latest app-web-latest_replaced_.*}, output assert_match "/usr/bin/env mkdir -p .kamal/apps/app/env/roles", output @@ -228,7 +228,7 @@ class CliProxyTest < CliTestCase SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:docker, :inspect, "kamal-proxy", "--format '{{.Config.Image}}'", "|", :cut, "-d:", "-f2") - .returns("v0.1.0") + .returns(Kamal::Configuration::Proxy::MINIMUM_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}}'") diff --git a/test/integration/main_test.rb b/test/integration/main_test.rb index 5a4eb91c..11e2ac94 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:v0.1.0/, details + assert_match /basecamp\/kamal-proxy:#{Kamal::Configuration::Proxy::MINIMUM_VERSION}/, 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 f9c7133b..2888ebfd 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:v0.1.0 \"kamal-proxy run\"/, proxy_details + assert_match /basecamp\/kamal-proxy:#{Kamal::Configuration::Proxy::MINIMUM_VERSION} \"kamal-proxy run\"/, proxy_details end def assert_proxy_not_running - assert_no_match /basecamp\/kamal-proxy:v0.1.0 \"kamal-proxy run\"/, proxy_details + assert_no_match /basecamp\/kamal-proxy:#{Kamal::Configuration::Proxy::MINIMUM_VERSION} \"kamal-proxy run\"/, proxy_details end def proxy_details From 8bcd896242d191b24d04a2b9631bcf34b847873c Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 18 Sep 2024 14:42:01 +0100 Subject: [PATCH 49/71] Simplified deploy/drain timeouts Remove `stop_wait_time` and `readiness_timeout` from the root config and remove `deploy_timeout` and `drain_timeout` from the proxy config. Instead we'll just have `deploy_timeout` and `drain_timeout` in the root config. For roles that run the proxy, they are passed to the kamal-proxy deploy command. Once that returns we can assume the container is ready to shut down. For other roles, we'll use the `deploy_timeout` when polling the container to see if it is ready and the `drain_timeout` when stopping the container. --- lib/kamal/cli/healthcheck/poller.rb | 4 +-- lib/kamal/commands/app.rb | 2 +- lib/kamal/configuration.rb | 10 ++++--- .../configuration/docs/configuration.yml | 15 +++++------ lib/kamal/configuration/docs/proxy.yml | 5 ---- lib/kamal/configuration/proxy.rb | 14 ++++++---- lib/kamal/configuration/role.rb | 6 +++++ test/cli/app_test.rb | 8 +++--- test/cli/proxy_test.rb | 6 ++--- test/commands/app_test.rb | 12 ++++++--- test/commands/proxy_test.rb | 2 +- test/configuration/role_test.rb | 8 ++++++ test/configuration/validation_test.rb | 2 +- test/fixtures/deploy_with_proxy.yml | 4 +-- test/fixtures/deploy_with_roles.yml | 2 +- .../docker/deployer/app/config/deploy.yml | 7 +++-- .../deployer/app_with_roles/config/deploy.yml | 26 ++++++++++++++++--- 17 files changed, 85 insertions(+), 48 deletions(-) diff --git a/lib/kamal/cli/healthcheck/poller.rb b/lib/kamal/cli/healthcheck/poller.rb index fad51845..fc3d0dec 100644 --- a/lib/kamal/cli/healthcheck/poller.rb +++ b/lib/kamal/cli/healthcheck/poller.rb @@ -3,7 +3,7 @@ module Kamal::Cli::Healthcheck::Poller def wait_for_healthy(role, &block) attempt = 1 - timeout_at = Time.now + KAMAL.config.readiness_timeout + timeout_at = Time.now + KAMAL.config.deploy_timeout readiness_delay = KAMAL.config.readiness_delay begin @@ -19,7 +19,7 @@ module Kamal::Cli::Healthcheck::Poller end unless %w[ running healthy ].include?(status) - raise Kamal::Cli::Healthcheck::Error, "container not ready after #{KAMAL.config.readiness_timeout} seconds (#{status})" + raise Kamal::Cli::Healthcheck::Error, "container not ready after #{KAMAL.config.deploy_timeout} seconds (#{status})" end rescue Kamal::Cli::Healthcheck::Error => e time_left = timeout_at - Time.now diff --git a/lib/kamal/commands/app.rb b/lib/kamal/commands/app.rb index 417ad463..b6e35d66 100644 --- a/lib/kamal/commands/app.rb +++ b/lib/kamal/commands/app.rb @@ -43,7 +43,7 @@ class Kamal::Commands::App < Kamal::Commands::Base def stop(version: nil) pipe \ version ? container_id_for_version(version) : current_running_container_id, - xargs(config.stop_wait_time ? docker(:stop, "-t", config.stop_wait_time) : docker(:stop)) + xargs(docker(:stop, *role.stop_args)) end def info diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index 1d6dfd12..f6051ca7 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -6,7 +6,7 @@ require "erb" require "net/ssh/proxy/jump" class Kamal::Configuration - delegate :service, :image, :labels, :stop_wait_time, :hooks_path, to: :raw_config, allow_nil: true + delegate :service, :image, :labels, :hooks_path, to: :raw_config, allow_nil: true delegate :argumentize, :optionize, to: Kamal::Utils attr_reader :destination, :raw_config, :secrets @@ -189,8 +189,12 @@ class Kamal::Configuration raw_config.readiness_delay || 7 end - def readiness_timeout - raw_config.readiness_timeout || 30 + def deploy_timeout + raw_config.deploy_timeout || 30 + end + + def drain_timeout + raw_config.drain_timeout || 30 end diff --git a/lib/kamal/configuration/docs/configuration.yml b/lib/kamal/configuration/docs/configuration.yml index 2e7b618d..d3c1874d 100644 --- a/lib/kamal/configuration/docs/configuration.yml +++ b/lib/kamal/configuration/docs/configuration.yml @@ -93,11 +93,6 @@ primary_role: workers # Whether roles with no servers are allowed. Defaults to `false`. allow_empty_roles: false -# Stop wait time -# -# How long we wait for a container to stop before killing it, defaults to 30 seconds -stop_wait_time: 60 - # Retain containers # # How many old containers and images we retain, defaults to 5 @@ -114,11 +109,15 @@ minimum_version: 1.3.0 # This only applies to containers that do not run a proxy or specify a healthcheck readiness_delay: 4 -# Readiness timeout +# Deploy 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 +deploy_timeout: 10 + +# Drain timeout +# +# How long to wait for a containers to drain, default 30 +drain_timeout: 10 # Run directory # diff --git a/lib/kamal/configuration/docs/proxy.yml b/lib/kamal/configuration/docs/proxy.yml index 6840a75c..85870d14 100644 --- a/lib/kamal/configuration/docs/proxy.yml +++ b/lib/kamal/configuration/docs/proxy.yml @@ -38,11 +38,6 @@ proxy: # Defaults to false ssl: true - # 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 diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb index 8cce49b0..973442ab 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -39,12 +39,12 @@ class Kamal::Configuration::Proxy { 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("healthcheck", "interval"), - "health-check-timeout": proxy_config.dig("healthcheck", "timeout"), + "deploy-timeout": seconds_duration(config.deploy_timeout), + "drain-timeout": seconds_duration(config.drain_timeout), + "health-check-interval": seconds_duration(proxy_config.dig("healthcheck", "interval")), + "health-check-timeout": seconds_duration(proxy_config.dig("healthcheck", "timeout")), "health-check-path": proxy_config.dig("healthcheck", "path"), - "target-timeout": proxy_config["response_timeout"], + "target-timeout": seconds_duration(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), "buffer-memory": proxy_config.dig("buffering", "memory"), @@ -68,4 +68,8 @@ class Kamal::Configuration::Proxy private attr_reader :config, :proxy_config + + def seconds_duration(value) + value ? "#{value}s" : nil + end end diff --git a/lib/kamal/configuration/role.rb b/lib/kamal/configuration/role.rb index 3f1e6d74..9d559317 100644 --- a/lib/kamal/configuration/role.rb +++ b/lib/kamal/configuration/role.rb @@ -65,6 +65,12 @@ class Kamal::Configuration::Role @logging ||= config.logging.merge(specialized_logging) end + def stop_args + # When deploying with the proxy, kamal-proxy will drain request before returning so we don't need to wait. + timeout = running_proxy? ? nil : config.drain_timeout + + [ *argumentize("-t", timeout) ] + end def env(host) @envs ||= {} diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index f3bd9fe5..5fdf61c5 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -128,9 +128,9 @@ class CliAppTest < CliTestCase 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\"", "--log-request-header", "\"User-Agent\"").raises(SSHKit::Command::Failed.new("Failed to deploy")) + .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :stop, raise_on_non_zero_exit: false) + SSHKit::Backend::Abstract.any_instance.expects(:execute) + .with(:docker, :exec, "kamal-proxy", "kamal-proxy", :deploy, "app-web", "--target", "\"123:80\"", "--deploy-timeout", "\"1s\"", "--drain-timeout", "\"30s\"", "--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| @@ -190,7 +190,7 @@ class CliAppTest < CliTestCase 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 + assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"999:80\" --deploy-timeout \"30s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\"", output end end diff --git a/test/cli/proxy_test.rb b/test/cli/proxy_test.rb index 9466ec41..0296723b 100644 --- a/test/cli/proxy_test.rb +++ b/test/cli/proxy_test.rb @@ -58,13 +58,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:/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 exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\" on 1.1.1.1", output assert_match "docker container stop kamal-proxy on 1.1.1.2", output assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.2", output assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.2", output assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --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 + assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\" on 1.1.1.2", output end end @@ -211,7 +211,7 @@ class CliProxyTest < CliTestCase assert_match "/usr/bin/env mkdir -p .kamal/apps/app/env/roles", output assert_match %r{/usr/bin/env .* .kamal/apps/app/env/roles/web.env}, output assert_match %r{docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-.* -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 \"12345678:80\" --deploy-timeout \"6s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\"", output + assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"12345678:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\"", output assert_match "docker container ls --all --filter name=^app-web-12345678$ --quiet | xargs docker stop", output assert_match "docker tag dhh/app:latest dhh/app:latest", output assert_match "/usr/bin/env mkdir -p .kamal", output diff --git a/test/commands/app_test.rb b/test/commands/app_test.rb index 44062624..bb78d8fa 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -4,7 +4,7 @@ class CommandsAppTest < ActiveSupport::TestCase setup do setup_test_secrets("secrets" => "RAILS_MASTER_KEY=456") - @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], env: { "secret" => [ "RAILS_MASTER_KEY" ] }, builder: { "arch" => "amd64" } } + @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: { "web" => [ "1.1.1.1" ], "workers" => [ "1.1.1.2" ] }, env: { "secret" => [ "RAILS_MASTER_KEY" ] }, builder: { "arch" => "amd64" } } end teardown do @@ -83,11 +83,15 @@ class CommandsAppTest < ActiveSupport::TestCase new_command.stop.join(" ") end - test "stop with custom stop wait time" do - @config[:stop_wait_time] = 30 + test "stop with custom drain timeout" do + @config[:drain_timeout] = 20 assert_equal \ - "sh -c 'docker ps --latest --quiet --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 --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop -t 30", + "sh -c 'docker ps --latest --quiet --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 --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop", new_command.stop.join(" ") + + assert_equal \ + "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=workers --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=workers --filter status=running --filter status=restarting' | head -1 | xargs docker stop -t 20", + new_command(role: "workers").stop.join(" ") end test "stop with version" do diff --git a/test/commands/proxy_test.rb b/test/commands/proxy_test.rb index 349e1a1b..6f331719 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\" --log-request-header \"User-Agent\"", + "docker exec kamal-proxy kamal-proxy deploy service --target \"172.1.0.2:80\" --deploy-timeout \"30s\" --drain-timeout \"30s\" --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 diff --git a/test/configuration/role_test.rb b/test/configuration/role_test.rb index 57719077..a15fed7d 100644 --- a/test/configuration/role_test.rb +++ b/test/configuration/role_test.rb @@ -242,6 +242,14 @@ class ConfigurationRoleTest < ActiveSupport::TestCase ENV.delete("VERSION") end + test "stop args with proxy" do + assert_equal [], config_with_roles.role(:web).stop_args + end + + test "stop args with no proxy" do + assert_equal [ "-t", 30 ], config_with_roles.role(:workers).stop_args + end + private def config Kamal::Configuration.new(@deploy) diff --git a/test/configuration/validation_test.rb b/test/configuration/validation_test.rb index d8ac3e3b..179aacfe 100644 --- a/test/configuration/validation_test.rb +++ b/test/configuration/validation_test.rb @@ -14,7 +14,7 @@ class ConfigurationValidationTest < ActiveSupport::TestCase assert_error "#{key}: should be a boolean", **{ key => "foo" } end - [ :stop_wait_time, :retain_containers, :readiness_delay ].each do |key| + [ :deploy_timeout, :drain_timeout, :retain_containers, :readiness_delay ].each do |key| assert_error "#{key}: should be an integer", **{ key => "foo" } end diff --git a/test/fixtures/deploy_with_proxy.yml b/test/fixtures/deploy_with_proxy.yml index a1f7a853..77174ac0 100644 --- a/test/fixtures/deploy_with_proxy.yml +++ b/test/fixtures/deploy_with_proxy.yml @@ -13,8 +13,6 @@ registry: builder: arch: amd64 -proxy: - deploy_timeout: 6s accessories: mysql: @@ -39,4 +37,4 @@ accessories: - data:/data readiness_delay: 0 -readiness_timeout: 1 +deploy_timeout: 6 diff --git a/test/fixtures/deploy_with_roles.yml b/test/fixtures/deploy_with_roles.yml index d831a5b4..6f1656bd 100644 --- a/test/fixtures/deploy_with_roles.yml +++ b/test/fixtures/deploy_with_roles.yml @@ -16,4 +16,4 @@ registry: password: pw builder: arch: amd64 -readiness_timeout: 1 +deploy_timeout: 1 diff --git a/test/integration/docker/deployer/app/config/deploy.yml b/test/integration/docker/deployer/app/config/deploy.yml index 5e999402..13408ab7 100644 --- a/test/integration/docker/deployer/app/config/deploy.yml +++ b/test/integration/docker/deployer/app/config/deploy.yml @@ -20,8 +20,9 @@ env: secret: - SECRET_TAG asset_path: /usr/share/nginx/html/versions -proxy: - deploy_timeout: 2s +deploy_timeout: 2 +drain_timeout: 2 +readiness_delay: 0 registry: server: registry:4443 @@ -39,5 +40,3 @@ accessories: cmd: sh -c 'echo "Starting busybox..."; trap exit term; while true; do sleep 1; done' roles: - web -stop_wait_time: 1 -readiness_delay: 0 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 835b4ffd..3539185e 100644 --- a/test/integration/docker/deployer/app_with_roles/config/deploy.yml +++ b/test/integration/docker/deployer/app_with_roles/config/deploy.yml @@ -9,8 +9,30 @@ servers: hosts: - vm3 cmd: sleep infinity +deploy_timeout: 2 +drain_timeout: 2 +readiness_delay: 0 + proxy: - deploy_timeout: 2s + healthcheck: + interval: 1 + timeout: 1 + path: "/up" + response_timeout: 2 + buffering: + requests: true + responses: true + memory: 400_000 + max_request_body: 40_000_000 + max_response_body: 40_000_000 + forward_headers: true + logging: + request_headers: + - Cache-Control + - X-Forwarded-Proto + response_headers: + - X-Request-ID + - X-Request-Start asset_path: /usr/share/nginx/html/versions @@ -30,8 +52,6 @@ accessories: cmd: sh -c 'echo "Starting busybox..."; trap exit term; while true; do sleep 1; done' roles: - web -stop_wait_time: 1 -readiness_delay: 0 aliases: whome: version worker_hostname: app exec -r workers -q --reuse hostname From d218264b69682b1bc7f2e98125ff33bd9cbfdb3e Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 18 Sep 2024 15:28:26 +0100 Subject: [PATCH 50/71] Doc output fixes --- lib/kamal/configuration/docs/configuration.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/kamal/configuration/docs/configuration.yml b/lib/kamal/configuration/docs/configuration.yml index d3c1874d..597a8bea 100644 --- a/lib/kamal/configuration/docs/configuration.yml +++ b/lib/kamal/configuration/docs/configuration.yml @@ -70,7 +70,7 @@ env: # volume containing both sets of files. # This requires that file names change when the contents change # (e.g. by including a hash of the contents in the name). - +# # To configure this, set the path to the assets: asset_path: /path/to/assets @@ -106,6 +106,7 @@ 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 run a proxy or specify a healthcheck readiness_delay: 4 From fd0cdc1ca18986d4f9835bb6111062c8019f9160 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 18 Sep 2024 17:25:35 +0100 Subject: [PATCH 51/71] All role specific proxy configuration By default only the primary role runs the proxy. To disable the proxy for that role, you can set `proxy: false` under it. For other roles they default to not running the proxy, but you can enable it by setting `proxy: true` for the role, or alternatively setting a proxy configuration. The proxy configuration will be merged into the root proxy configuration. --- lib/kamal/cli/app.rb | 4 +- lib/kamal/cli/app/boot.rb | 2 +- lib/kamal/cli/proxy.rb | 6 +-- lib/kamal/commands/app.rb | 2 +- lib/kamal/commands/app/proxy.rb | 16 +++++++ lib/kamal/commands/proxy.rb | 27 ++++------- lib/kamal/configuration.rb | 24 +++++++++- lib/kamal/configuration/docs/proxy.yml | 6 +++ lib/kamal/configuration/docs/role.yml | 3 +- lib/kamal/configuration/proxy.rb | 41 ++++++----------- lib/kamal/configuration/role.rb | 43 +++++++++++++---- lib/kamal/configuration/validator.rb | 4 +- lib/kamal/configuration/validator/proxy.rb | 8 ++-- test/cli/app_test.rb | 9 ++++ test/cli/proxy_test.rb | 20 ++++---- test/commands/app_test.rb | 13 ++++++ test/commands/proxy_test.rb | 20 ++------ test/configuration/role_test.rb | 8 ++++ .../deploy_primary_web_role_override.yml | 4 +- test/fixtures/deploy_with_extensions.yml | 2 +- test/fixtures/deploy_with_proxy_roles.yml | 46 +++++++++++++++++++ 21 files changed, 210 insertions(+), 98 deletions(-) create mode 100644 lib/kamal/commands/app/proxy.rb create mode 100644 test/fixtures/deploy_with_proxy_roles.yml diff --git a/lib/kamal/cli/app.rb b/lib/kamal/cli/app.rb index 2a02ed65..d70d066b 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -47,7 +47,7 @@ class Kamal::Cli::App < Kamal::Cli::Base 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) + execute *app.deploy(target: endpoint) end end end @@ -68,7 +68,7 @@ class Kamal::Cli::App < Kamal::Cli::Base 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? - execute *KAMAL.proxy.remove(role.container_prefix, target: endpoint), raise_on_non_zero_exit: false + execute *app.remove(target: endpoint), raise_on_non_zero_exit: false end end diff --git a/lib/kamal/cli/app/boot.rb b/lib/kamal/cli/app/boot.rb index 6939825a..fd330c71 100644 --- a/lib/kamal/cli/app/boot.rb +++ b/lib/kamal/cli/app/boot.rb @@ -54,7 +54,7 @@ class Kamal::Cli::App::Boot 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) + execute *app.deploy(target: endpoint) else Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) } end diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index 12d4c6b0..f48faf0a 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -13,8 +13,8 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base 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}" + 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 @@ -52,7 +52,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base if endpoint.present? info "Deploying #{endpoint} for role `#{role}` on #{host}..." - execute *KAMAL.proxy.deploy(role.container_prefix, target: endpoint) + execute *app.deploy(target: endpoint) end end end diff --git a/lib/kamal/commands/app.rb b/lib/kamal/commands/app.rb index b6e35d66..6d8f44c6 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, Execution, Images, Logging + include Assets, Containers, Execution, Images, Logging, Proxy ACTIVE_DOCKER_STATUSES = [ :running, :restarting ] diff --git a/lib/kamal/commands/app/proxy.rb b/lib/kamal/commands/app/proxy.rb new file mode 100644 index 00000000..550e2e3b --- /dev/null +++ b/lib/kamal/commands/app/proxy.rb @@ -0,0 +1,16 @@ +module Kamal::Commands::App::Proxy + delegate :proxy_container_name, to: :config + + def deploy(target:) + proxy_exec :deploy, role.container_prefix, *role.proxy.deploy_command_args(target: target) + end + + def remove(target:) + proxy_exec :remove, role.container_prefix, *role.proxy.remove_command_args(target: target) + end + + private + def proxy_exec(*command) + docker :exec, proxy_container_name, "kamal-proxy", *command + end +end diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb index 354493db..aa347e2a 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -1,13 +1,5 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base delegate :argumentize, :optionize, to: Kamal::Utils - delegate :container_name, :app_port, to: :proxy_config - - attr_reader :proxy_config - - def initialize(config) - super - @proxy_config = config.proxy - end def run docker :run, @@ -15,11 +7,11 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base "--network", "kamal", "--detach", "--restart", "unless-stopped", - *proxy_config.publish_args, + *config.proxy_publish_args, "--volume", "/var/run/docker.sock:/var/run/docker.sock", - *proxy_config.config_volume.docker_args, + *config.proxy_config_volume.docker_args, *config.logging_args, - proxy_config.image + config.proxy_image end def start @@ -34,14 +26,6 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base combine start, run, by: "||" end - def deploy(service, target:) - 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}:#{app_port}" }) - end - def info docker :ps, "--filter", "name=^#{container_name}$" end @@ -85,4 +69,9 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base docker(:image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=Traefik") ) end + + private + def container_name + config.proxy_container_name + end end diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index f6051ca7..b28152bc 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -14,6 +14,10 @@ class Kamal::Configuration include Validation + PROXY_MINIMUM_VERSION = "v0.3.0" + PROXY_HTTP_PORT = 80 + PROXY_HTTPS_PORT = 443 + class << self def create_from(config_file:, destination: nil, version: nil) raw_config = load_config_files(config_file, *destination_config_file(config_file, destination)) @@ -61,7 +65,7 @@ class Kamal::Configuration @env = Env.new(config: @raw_config.env || {}, secrets: secrets) @logging = Logging.new(logging_config: @raw_config.logging) - @proxy = Proxy.new(config: self) + @proxy = Proxy.new(config: self, proxy_config: @raw_config.proxy || {}) @ssh = Ssh.new(config: self) @sshkit = Sshkit.new(config: self) @@ -244,6 +248,24 @@ class Kamal::Configuration env_tags.detect { |t| t.name == name.to_s } end + def proxy_publish_args + argumentize "--publish", [ "#{PROXY_HTTP_PORT}:#{PROXY_HTTP_PORT}", "#{PROXY_HTTPS_PORT}:#{PROXY_HTTPS_PORT}" ] + end + + def proxy_image + "basecamp/kamal-proxy:#{PROXY_MINIMUM_VERSION}" + end + + def proxy_container_name + "kamal-proxy" + end + + def proxy_config_volume + Kamal::Configuration::Volume.new \ + host_path: File.join(proxy_directory, "config"), + container_path: "/home/kamal-proxy/.config/kamal-proxy" + end + def to_h { diff --git a/lib/kamal/configuration/docs/proxy.yml b/lib/kamal/configuration/docs/proxy.yml index 85870d14..332a2c28 100644 --- a/lib/kamal/configuration/docs/proxy.yml +++ b/lib/kamal/configuration/docs/proxy.yml @@ -9,6 +9,12 @@ # # They are application specific, so are not shared when multiple applications # run on the same proxy. +# +# The proxy is enabled by default on the primary role, but can be disabled by +# setting `proxy: false`. +# +# It is disabled by default on all other roles, but can be enabled by setting +# `proxy: true`, or providing a proxy configuration. proxy: # Host diff --git a/lib/kamal/configuration/docs/role.yml b/lib/kamal/configuration/docs/role.yml index 0f75c550..fbc8a813 100644 --- a/lib/kamal/configuration/docs/role.yml +++ b/lib/kamal/configuration/docs/role.yml @@ -35,13 +35,14 @@ servers: hosts: - 172.1.0.3 - 172.1.0.4: experiment1 - proxy: true cmd: "bin/jobs" options: memory: 2g cpus: 4 logging: ... + proxy: + ... labels: my-label: workers env: diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb index 973442ab..551601c4 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -1,36 +1,23 @@ class Kamal::Configuration::Proxy include Kamal::Configuration::Validation - MINIMUM_VERSION = "v0.3.0" - DEFAULT_HTTP_PORT = 80 - DEFAULT_HTTPS_PORT = 443 - DEFAULT_IMAGE = "basecamp/kamal-proxy:#{MINIMUM_VERSION}" DEFAULT_LOG_REQUEST_HEADERS = [ "Cache-Control", "Last-Modified", "User-Agent" ] + CONTAINER_NAME = "kamal-proxy" delegate :argumentize, :optionize, to: Kamal::Utils - def initialize(config:) + attr_reader :config, :proxy_config + + def initialize(config:, proxy_config:, context: "proxy") @config = config - @proxy_config = config.raw_config.proxy || {} - validate! proxy_config, with: Kamal::Configuration::Validator::Proxy + @proxy_config = proxy_config + validate! @proxy_config, with: Kamal::Configuration::Validator::Proxy, context: context end def app_port proxy_config.fetch("app_port", 80) 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 ssl? proxy_config.fetch("ssl", false) end @@ -56,19 +43,19 @@ class Kamal::Configuration::Proxy }.compact end - def deploy_command_args - optionize deploy_options + def deploy_command_args(target:) + optionize ({ target: "#{target}:#{app_port}" }).merge(deploy_options) end - def config_volume - Kamal::Configuration::Volume.new \ - host_path: File.join(config.proxy_directory, "config"), - container_path: "/home/kamal-proxy/.config/kamal-proxy" + def remove_command_args(target:) + optionize({ target: "#{target}:#{app_port}" }) + end + + def merge(other) + self.class.new config: config, proxy_config: proxy_config.deep_merge(other.proxy_config) end private - attr_reader :config, :proxy_config - def seconds_duration(value) value ? "#{value}s" : nil end diff --git a/lib/kamal/configuration/role.rb b/lib/kamal/configuration/role.rb index 9d559317..76305312 100644 --- a/lib/kamal/configuration/role.rb +++ b/lib/kamal/configuration/role.rb @@ -3,7 +3,7 @@ class Kamal::Configuration::Role delegate :argumentize, :optionize, to: Kamal::Utils - attr_reader :name, :config, :specialized_env, :specialized_logging + attr_reader :name, :config, :specialized_env, :specialized_logging, :specialized_proxy alias to_s name @@ -23,6 +23,8 @@ class Kamal::Configuration::Role @specialized_logging = Kamal::Configuration::Logging.new \ logging_config: specializations.fetch("logging", {}), context: "servers/#{name}/logging" + + initialize_specialized_proxy end def primary_host @@ -65,6 +67,14 @@ class Kamal::Configuration::Role @logging ||= config.logging.merge(specialized_logging) end + def proxy + @proxy ||= config.proxy.merge(specialized_proxy) if running_proxy? + end + + def running_proxy? + @running_proxy + end + def stop_args # When deploying with the proxy, kamal-proxy will drain request before returning so we don't need to wait. timeout = running_proxy? ? nil : config.drain_timeout @@ -98,16 +108,8 @@ class Kamal::Configuration::Role end - def running_proxy? - if specializations["proxy"].nil? - primary? - else - specializations["proxy"] - end - end - def primary? - self == @config.primary_role + name == @config.primary_role_name end @@ -144,6 +146,27 @@ class Kamal::Configuration::Role end private + def initialize_specialized_proxy + proxy_specializations = specializations["proxy"] + + if primary? + # only false means no proxy for non-primary roles + @running_proxy = proxy_specializations != false + else + # false and nil both mean no proxy for non-primary roles + @running_proxy = !!proxy_specializations + end + + if running_proxy? + proxy_config = proxy_specializations == true || proxy_specializations.nil? ? {} : proxy_specializations + + @specialized_proxy = Kamal::Configuration::Proxy.new \ + config: config, + proxy_config: proxy_config, + context: "servers/#{name}/proxy" + end + end + def tagged_hosts {}.tap do |tagged_hosts| extract_hosts_from_config.map do |host_config| diff --git a/lib/kamal/configuration/validator.rb b/lib/kamal/configuration/validator.rb index 2ac8482d..0d46e4ae 100644 --- a/lib/kamal/configuration/validator.rb +++ b/lib/kamal/configuration/validator.rb @@ -24,7 +24,9 @@ class Kamal::Configuration::Validator example_value = example[key] if example_value == "..." - validate_type! value, *(Array if key == :servers), Hash + unless key.to_s == "proxy" && boolean?(value.class) + validate_type! value, *(Array if key == :servers), Hash + end elsif key == "hosts" validate_servers! value elsif example_value.is_a?(Array) diff --git a/lib/kamal/configuration/validator/proxy.rb b/lib/kamal/configuration/validator/proxy.rb index a4ee19bf..bf2e5e9e 100644 --- a/lib/kamal/configuration/validator/proxy.rb +++ b/lib/kamal/configuration/validator/proxy.rb @@ -1,9 +1,11 @@ class Kamal::Configuration::Validator::Proxy < Kamal::Configuration::Validator def validate! - super + unless config.nil? + super - if config["host"].blank? && config["ssl"] - error "Must set a host to enable automatic SSL" + if config["host"].blank? && config["ssl"] + error "Must set a host to enable automatic SSL" + end end end end diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index 5fdf61c5..3cb61bf0 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -388,6 +388,15 @@ class CliAppTest < CliTestCase end end + test "boot proxy with role specific config" do + SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version + + run_command("boot", config: :with_proxy_roles, host: nil).tap do |output| + assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"123:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --target-timeout \"10s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\"", output + assert_match "docker exec kamal-proxy kamal-proxy deploy app-web2 --target \"123:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --target-timeout \"15s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\"", 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 index 0296723b..dfc3aff9 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 $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.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.config.proxy_image}", output end end @@ -18,11 +18,11 @@ class CliProxyTest < CliTestCase 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 + 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.config.proxy_image}", output end end - assert_includes exception.message, "kamal-proxy version v0.0.1 is too old, please reboot to update to at least #{Kamal::Configuration::Proxy::MINIMUM_VERSION}" + assert_includes exception.message, "kamal-proxy version v0.0.1 is too old, please reboot to update to at least #{Kamal::Configuration::PROXY_MINIMUM_VERSION}" ensure Thread.report_on_exception = false end @@ -31,12 +31,12 @@ class CliProxyTest < CliTestCase 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(Kamal::Configuration::Proxy::MINIMUM_VERSION) + .returns(Kamal::Configuration::PROXY_MINIMUM_VERSION) .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 + 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.config.proxy_image}", output end ensure Thread.report_on_exception = false @@ -57,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:/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 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.config.proxy_image} on 1.1.1.1", output assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\" on 1.1.1.1", output assert_match "docker container stop kamal-proxy on 1.1.1.2", output assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.2", output assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.2", output - assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --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 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.config.proxy_image} on 1.1.1.2", output assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\" on 1.1.1.2", output end end @@ -189,7 +189,7 @@ class CliProxyTest < CliTestCase SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:docker, :inspect, "kamal-proxy", "--format '{{.Config.Image}}'", "|", :cut, "-d:", "-f2") - .returns(Kamal::Configuration::Proxy::MINIMUM_VERSION) + .returns(Kamal::Configuration::PROXY_MINIMUM_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}}'") @@ -205,7 +205,7 @@ class CliProxyTest < CliTestCase assert_match "/usr/bin/env mkdir -p .kamal", output assert_match "docker network create kamal", output assert_match "docker login -u [REDACTED] -p [REDACTED]", 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\" basecamp/kamal-proxy:#{Kamal::Configuration::Proxy::MINIMUM_VERSION}", 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\" basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", output assert_match "/usr/bin/env mkdir -p .kamal", output assert_match %r{docker rename app-web-latest app-web-latest_replaced_.*}, output assert_match "/usr/bin/env mkdir -p .kamal/apps/app/env/roles", output @@ -228,7 +228,7 @@ class CliProxyTest < CliTestCase SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:docker, :inspect, "kamal-proxy", "--format '{{.Config.Image}}'", "|", :cut, "-d:", "-f2") - .returns(Kamal::Configuration::Proxy::MINIMUM_VERSION) + .returns(Kamal::Configuration::PROXY_MINIMUM_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}}'") diff --git a/test/commands/app_test.rb b/test/commands/app_test.rb index bb78d8fa..67810a6e 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -113,6 +113,19 @@ class CommandsAppTest < ActiveSupport::TestCase new_command.info.join(" ") end + test "deploy" do + assert_equal \ + "docker exec kamal-proxy kamal-proxy deploy app-web --target \"172.1.0.2:80\" --deploy-timeout \"30s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\"", + new_command.deploy(target: "172.1.0.2").join(" ") + end + + test "remove" do + assert_equal \ + "docker exec kamal-proxy kamal-proxy remove app-web --target \"172.1.0.2:80\"", + new_command.remove(target: "172.1.0.2").join(" ") + end + + test "logs" do assert_equal \ diff --git a/test/commands/proxy_test.rb b/test/commands/proxy_test.rb index 6f331719..f0be198a 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:/home/kamal-proxy/.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.config.proxy_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:/home/kamal-proxy/.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.config.proxy_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:/home/kamal-proxy/.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.config.proxy_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:/home/kamal-proxy/.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.config.proxy_image}", new_command.run.join(" ") end @@ -107,18 +107,6 @@ class CommandsProxyTest < ActiveSupport::TestCase 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\" --deploy-timeout \"30s\" --drain-timeout \"30s\" --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 - - 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").join(" ") - end - test "version" do assert_equal \ "docker inspect kamal-proxy --format '{{.Config.Image}}' | cut -d: -f2", diff --git a/test/configuration/role_test.rb b/test/configuration/role_test.rb index a15fed7d..c7a94de0 100644 --- a/test/configuration/role_test.rb +++ b/test/configuration/role_test.rb @@ -250,6 +250,14 @@ class ConfigurationRoleTest < ActiveSupport::TestCase assert_equal [ "-t", 30 ], config_with_roles.role(:workers).stop_args end + test "role specific proxy config" do + @deploy_with_roles[:proxy] = { "response_timeout" => 15 } + @deploy_with_roles[:servers]["workers"]["proxy"] = { "response_timeout" => 18 } + + assert_equal "15s", config_with_roles.role(:web).proxy.deploy_options[:"target-timeout"] + assert_equal "18s", config_with_roles.role(:workers).proxy.deploy_options[:"target-timeout"] + end + private def config Kamal::Configuration.new(@deploy) diff --git a/test/fixtures/deploy_primary_web_role_override.yml b/test/fixtures/deploy_primary_web_role_override.yml index 6bc2f837..4d90ce58 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: - proxy: true + proxy: {} hosts: - 1.1.1.1 - 1.1.1.2 web_tokyo: - proxy: true + proxy: {} 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 c45104de..cbbc2e78 100644 --- a/test/fixtures/deploy_with_extensions.yml +++ b/test/fixtures/deploy_with_extensions.yml @@ -1,6 +1,6 @@ x-web: &web - proxy: true + proxy: {} service: app image: dhh/app diff --git a/test/fixtures/deploy_with_proxy_roles.yml b/test/fixtures/deploy_with_proxy_roles.yml new file mode 100644 index 00000000..73211213 --- /dev/null +++ b/test/fixtures/deploy_with_proxy_roles.yml @@ -0,0 +1,46 @@ +service: app +image: dhh/app +servers: + web: + hosts: + - "1.1.1.1" + - "1.1.1.2" + web2: + hosts: + - "1.1.1.3" + - "1.1.1.4" + proxy: + response_timeout: 15 +registry: + username: user + password: pw +builder: + arch: amd64 + +proxy: + response_timeout: 10 + +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 +deploy_timeout: 6 From 63f854ea18fa5dd501ac9f23e359187629a1337d Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 18 Sep 2024 17:42:45 +0100 Subject: [PATCH 52/71] Add validations for host/ssl roles Roles with SSL can only have one server. Two roles with SSL can't use the same host. --- lib/kamal/configuration.rb | 16 ++++++++++++++++ lib/kamal/configuration/proxy.rb | 4 ++++ lib/kamal/configuration/role.rb | 10 ++++++++++ test/configuration_test.rb | 31 +++++++++++++++++++++++++++++++ test/integration/main_test.rb | 2 +- test/integration/proxy_test.rb | 4 ++-- 6 files changed, 64 insertions(+), 3 deletions(-) diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index b28152bc..92d6b9d8 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -75,6 +75,8 @@ class Kamal::Configuration ensure_retain_containers_valid ensure_valid_service_name ensure_no_traefik_reboot_hooks + ensure_one_host_for_ssl_roles + ensure_unique_hosts_for_ssl_roles end @@ -349,6 +351,20 @@ class Kamal::Configuration true end + def ensure_one_host_for_ssl_roles + roles.each(&:ensure_one_host_for_ssl) + + true + end + + def ensure_unique_hosts_for_ssl_roles + hosts = roles.select(&:ssl?).map { |role| role.proxy.host } + duplicates = hosts.tally.filter_map { |host, count| host if count > 1 } + + raise Kamal::ConfigurationError, "Different roles can't share the same host for SSL: #{duplicates.join(", ")}" if duplicates.any? + + true + end def role_names raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb index 551601c4..af09e2c7 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -22,6 +22,10 @@ class Kamal::Configuration::Proxy proxy_config.fetch("ssl", false) end + def host + proxy_config["host"] + end + def deploy_options { host: proxy_config["host"], diff --git a/lib/kamal/configuration/role.rb b/lib/kamal/configuration/role.rb index 76305312..708e77fc 100644 --- a/lib/kamal/configuration/role.rb +++ b/lib/kamal/configuration/role.rb @@ -75,6 +75,10 @@ class Kamal::Configuration::Role @running_proxy end + def ssl? + running_proxy? && proxy.ssl? + end + def stop_args # When deploying with the proxy, kamal-proxy will drain request before returning so we don't need to wait. timeout = running_proxy? ? nil : config.drain_timeout @@ -145,6 +149,12 @@ class Kamal::Configuration::Role File.join config.assets_directory, "volumes", [ name, version ].join("-") end + def ensure_one_host_for_ssl + if running_proxy? && proxy.ssl? && hosts.size > 1 + raise Kamal::ConfigurationError, "SSL is only supported on a single server, found #{hosts.size} servers for role #{name}" + end + end + private def initialize_specialized_proxy proxy_specializations = specializations["proxy"] diff --git a/test/configuration_test.rb b/test/configuration_test.rb index 81dbb5ac..814aa26e 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -346,4 +346,35 @@ class ConfigurationTest < ActiveSupport::TestCase end end end + + test "proxy ssl roles with no host" do + @deploy_with_roles[:servers]["workers"]["proxy"] = { "ssl" => true } + + exception = assert_raises(Kamal::ConfigurationError) do + Kamal::Configuration.new(@deploy_with_roles) + end + + assert_equal "servers/workers/proxy: Must set a host to enable automatic SSL", exception.message + end + + test "proxy ssl roles with multiple servers" do + @deploy_with_roles[:servers]["workers"]["proxy"] = { "ssl" => true, "host" => "foo.example.com" } + + exception = assert_raises(Kamal::ConfigurationError) do + Kamal::Configuration.new(@deploy_with_roles) + end + + assert_equal "SSL is only supported on a single server, found 2 servers for role workers", exception.message + end + + test "two proxy ssl roles with same host" do + @deploy_with_roles[:servers]["web"] = { "hosts" => [ "1.1.1.1" ], "proxy" => { "ssl" => true, "host" => "foo.example.com" } } + @deploy_with_roles[:servers]["workers"] = { "hosts" => [ "1.1.1.1" ], "proxy" => { "ssl" => true, "host" => "foo.example.com" } } + + exception = assert_raises(Kamal::ConfigurationError) do + Kamal::Configuration.new(@deploy_with_roles) + end + + assert_equal "Different roles can't share the same host for SSL: foo.example.com", exception.message + end end diff --git a/test/integration/main_test.rb b/test/integration/main_test.rb index 11e2ac94..a015c1ce 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:#{Kamal::Configuration::Proxy::MINIMUM_VERSION}/, details + assert_match /basecamp\/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}/, 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 2888ebfd..0eb65aec 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:#{Kamal::Configuration::Proxy::MINIMUM_VERSION} \"kamal-proxy run\"/, proxy_details + assert_match /basecamp\/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION} \"kamal-proxy run\"/, proxy_details end def assert_proxy_not_running - assert_no_match /basecamp\/kamal-proxy:#{Kamal::Configuration::Proxy::MINIMUM_VERSION} \"kamal-proxy run\"/, proxy_details + assert_no_match /basecamp\/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION} \"kamal-proxy run\"/, proxy_details end def proxy_details From a6b983de060eb93b58f93e85fc41593457501203 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 18 Sep 2024 15:33:21 -0700 Subject: [PATCH 53/71] Bump version for 2.0.0.beta1 --- Gemfile.lock | 2 +- lib/kamal/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 8a8a1097..1d6096e0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - kamal (2.0.0.alpha) + kamal (2.0.0.beta1) activesupport (>= 7.0) base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) diff --git a/lib/kamal/version.rb b/lib/kamal/version.rb index 4c771efd..11afd2fa 100644 --- a/lib/kamal/version.rb +++ b/lib/kamal/version.rb @@ -1,3 +1,3 @@ module Kamal - VERSION = "2.0.0.alpha" + VERSION = "2.0.0.beta1" end From 1d7c9fec1d660f0e35a64b1b3b5345ccca961d27 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 19 Sep 2024 12:25:57 +0100 Subject: [PATCH 54/71] Fix /home/kamal-proxy/.config/kamal-proxy ownership 1. Update to kamal-proxy 0.4.0 which creates and chowns /home/kamal-proxy/.config/kamal-proxy to kamal-proxy 2. Use a docker volume rather than mapping in a directory, so docker keeps it owned by the correct user --- lib/kamal/cli/proxy.rb | 11 ----------- lib/kamal/commands/proxy.rb | 7 +------ lib/kamal/configuration.rb | 12 +----------- test/cli/proxy_test.rb | 19 ++++++------------- test/commands/proxy_test.rb | 8 ++++---- test/integration/integration_test.rb | 4 ---- test/integration/main_test.rb | 1 - test/integration/proxy_test.rb | 1 - 8 files changed, 12 insertions(+), 51 deletions(-) diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index f48faf0a..d006c8c6 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -167,7 +167,6 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base stop remove_container remove_image - remove_host_directory end end end @@ -192,16 +191,6 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base 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, raise_on_non_zero_exit: false - end - end - end - private def removal_allowed?(force) on(KAMAL.proxy_hosts) do |host| diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb index aa347e2a..df264a6b 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -8,8 +8,7 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base "--detach", "--restart", "unless-stopped", *config.proxy_publish_args, - "--volume", "/var/run/docker.sock:/var/run/docker.sock", - *config.proxy_config_volume.docker_args, + "--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", *config.logging_args, config.proxy_image end @@ -57,10 +56,6 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base 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/configuration.rb b/lib/kamal/configuration.rb index 92d6b9d8..2848a365 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -14,7 +14,7 @@ class Kamal::Configuration include Validation - PROXY_MINIMUM_VERSION = "v0.3.0" + PROXY_MINIMUM_VERSION = "v0.4.0" PROXY_HTTP_PORT = 80 PROXY_HTTPS_PORT = 443 @@ -216,10 +216,6 @@ class Kamal::Configuration File.join apps_directory, [ service, destination ].compact.join("-") end - def proxy_directory - File.join run_directory, "proxy" - end - def env_directory File.join app_directory, "env" end @@ -262,12 +258,6 @@ class Kamal::Configuration "kamal-proxy" end - def proxy_config_volume - Kamal::Configuration::Volume.new \ - host_path: File.join(proxy_directory, "config"), - container_path: "/home/kamal-proxy/.config/kamal-proxy" - end - def to_h { diff --git a/test/cli/proxy_test.rb b/test/cli/proxy_test.rb index dfc3aff9..72a0aa13 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 $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}", output + assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}", output end end @@ -18,7 +18,7 @@ class CliProxyTest < CliTestCase 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.config.proxy_image}", output + assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}", output end end @@ -36,7 +36,7 @@ class CliProxyTest < CliTestCase 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.config.proxy_image}", 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 kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}", output end ensure Thread.report_on_exception = false @@ -57,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:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_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 kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image} on 1.1.1.1", output assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\" on 1.1.1.1", output assert_match "docker container stop kamal-proxy on 1.1.1.2", output assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.2", output assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.2", output - assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --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.config.proxy_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 kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image} on 1.1.1.2", output assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\" on 1.1.1.2", output end end @@ -136,7 +136,6 @@ class CliProxyTest < CliTestCase 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 @@ -176,12 +175,6 @@ 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 - test "upgrade" do Object.any_instance.stubs(:sleep) @@ -205,7 +198,7 @@ class CliProxyTest < CliTestCase assert_match "/usr/bin/env mkdir -p .kamal", output assert_match "docker network create kamal", output assert_match "docker login -u [REDACTED] -p [REDACTED]", 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\" basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", 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 kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", output assert_match "/usr/bin/env mkdir -p .kamal", output assert_match %r{docker rename app-web-latest app-web-latest_replaced_.*}, output assert_match "/usr/bin/env mkdir -p .kamal/apps/app/env/roles", output diff --git a/test/commands/proxy_test.rb b/test/commands/proxy_test.rb index f0be198a..4a4e029e 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:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}", + "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", 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:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}", + "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", 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:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}", + "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", 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:/home/kamal-proxy/.config/kamal-proxy --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" #{KAMAL.config.proxy_image}", + "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", new_command.run.join(" ") end diff --git a/test/integration/integration_test.rb b/test/integration/integration_test.rb index 5c675da6..c7938689 100644 --- a/test/integration/integration_test.rb +++ b/test/integration/integration_test.rb @@ -153,10 +153,6 @@ class IntegrationTest < ActiveSupport::TestCase 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 diff --git a/test/integration/main_test.rb b/test/integration/main_test.rb index a015c1ce..2385799e 100644 --- a/test/integration/main_test.rb +++ b/test/integration/main_test.rb @@ -98,7 +98,6 @@ 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 0eb65aec..1a40d079 100644 --- a/test/integration/proxy_test.rb +++ b/test/integration/proxy_test.rb @@ -48,7 +48,6 @@ class ProxyTest < IntegrationTest kamal :proxy, :remove assert_proxy_not_running - assert_proxy_directory_removed end private From 834b343deda648fcf59f242c3ee4c3069c75c843 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 19 Sep 2024 14:29:33 +0100 Subject: [PATCH 55/71] Run app exec in the kamal network All other containers run in the kamal network, so let's add app exec-ed containers as well. --- lib/kamal/commands/app/execution.rb | 1 + test/cli/app_test.rb | 6 +++--- test/commands/app_test.rb | 14 +++++++------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/kamal/commands/app/execution.rb b/lib/kamal/commands/app/execution.rb index 215821dc..4434c26a 100644 --- a/lib/kamal/commands/app/execution.rb +++ b/lib/kamal/commands/app/execution.rb @@ -11,6 +11,7 @@ module Kamal::Commands::App::Execution docker :run, ("-it" if interactive), "--rm", + "--network", "kamal", *role&.env_args(host), *argumentize("--env", env), *config.volume_args, diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index 3cb61bf0..97e1c461 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -263,13 +263,13 @@ class CliAppTest < CliTestCase test "exec" do run_command("exec", "ruby -v").tap do |output| - assert_match "docker run --rm --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v", output + assert_match "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env 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/apps/app/env/roles/web.env dhh/app:latest ruby -v", output + assert_match "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v", output end end @@ -282,7 +282,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/apps/app/env/roles/web.env dhh/app:latest ruby -v'") + .with("ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env 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 67810a6e..57f8c2fe 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -196,13 +196,13 @@ class CommandsAppTest < ActiveSupport::TestCase test "execute in new container" do assert_equal \ - "docker run --rm --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails db:setup", + "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env 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/apps/app/env/roles/web.env --env foo=\"bar\" dhh/app:999 bin/rails db:setup", + "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --env foo=\"bar\" dhh/app:999 bin/rails db:setup", new_command.execute_in_new_container("bin/rails", "db:setup", env: { "foo" => "bar" }).join(" ") end @@ -211,14 +211,14 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } } assert_equal \ - "docker run --rm --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails db:setup", + "docker run --rm --network kamal --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env 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/apps/app/env/roles/web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails db:setup", + "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails db:setup", new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ") end @@ -235,7 +235,7 @@ class CommandsAppTest < ActiveSupport::TestCase end test "execute in new container over ssh" do - assert_match %r{docker run -it --rm --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails c}, + assert_match %r{docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails c}, new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {}) end @@ -243,13 +243,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/apps/app/env/roles/web.env dhh/app:999 bin/rails c'", + assert_equal "ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --network kamal --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env 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/apps/app/env/roles/web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c}, + assert_match %r{docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c}, new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {}) end From 2c829a482408cff4b55f1bc5731def98a0e3f31b Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 19 Sep 2024 14:58:13 +0100 Subject: [PATCH 56/71] Avoid setting env via SSHKit SSHKit puts the env in the command, so leaks them in process listings. --- lib/kamal/cli/base.rb | 15 ++++++++++++-- lib/kamal/cli/build.rb | 38 ++++++++++++++++++----------------- lib/kamal/cli/main.rb | 6 +++--- lib/kamal/commands/hook.rb | 11 ++++++---- test/cli/build_test.rb | 4 ++-- test/cli/cli_test_case.rb | 22 +------------------- test/commands/hook_test.rb | 41 ++++++++++++++++---------------------- 7 files changed, 63 insertions(+), 74 deletions(-) diff --git a/lib/kamal/cli/base.rb b/lib/kamal/cli/base.rb index f594c54e..4aebfd90 100644 --- a/lib/kamal/cli/base.rb +++ b/lib/kamal/cli/base.rb @@ -135,8 +135,10 @@ module Kamal::Cli details = { hosts: KAMAL.hosts.join(","), command: command, subcommand: subcommand } say "Running the #{hook} hook...", :magenta - run_locally do - execute *KAMAL.hook.run(hook, **details, **extra_details) + with_env KAMAL.hook.env(**details, **extra_details) do + run_locally do + execute *KAMAL.hook.run(hook) + end rescue SSHKit::Command::Failed => e raise HookError.new("Hook `#{hook}` failed:\n#{e.message}") end @@ -183,5 +185,14 @@ module Kamal::Cli execute(*KAMAL.server.ensure_run_directory) end end + + def with_env(env) + current_env = ENV.to_h.dup + ENV.update(env) + yield + ensure + ENV.clear + ENV.update(current_env) + end end end diff --git a/lib/kamal/cli/build.rb b/lib/kamal/cli/build.rb index 0347d18c..53ecb0bb 100644 --- a/lib/kamal/cli/build.rb +++ b/lib/kamal/cli/build.rb @@ -30,28 +30,30 @@ class Kamal::Cli::Build < Kamal::Cli::Base say "Building with uncommitted changes:\n #{uncommitted_changes}", :yellow end - run_locally do - begin - execute *KAMAL.builder.inspect_builder - rescue SSHKit::Command::Failed => e - if e.message =~ /(context not found|no builder|no compatible builder|does not exist)/ - warn "Missing compatible builder, so creating a new one first" - begin - cli.remove - rescue SSHKit::Command::Failed - raise unless e.message =~ /(context not found|no builder|does not exist)/ + with_env(KAMAL.config.builder.secrets) do + run_locally do + begin + execute *KAMAL.builder.inspect_builder + rescue SSHKit::Command::Failed => e + if e.message =~ /(context not found|no builder|no compatible builder|does not exist)/ + warn "Missing compatible builder, so creating a new one first" + begin + cli.remove + rescue SSHKit::Command::Failed + raise unless e.message =~ /(context not found|no builder|does not exist)/ + end + cli.create + else + raise end - cli.create - else - raise end - end - # Get the command here to ensure the Dir.chdir doesn't interfere with it - push = KAMAL.builder.push + # Get the command here to ensure the Dir.chdir doesn't interfere with it + push = KAMAL.builder.push - KAMAL.with_verbosity(:debug) do - Dir.chdir(KAMAL.config.builder.build_directory) { execute *push, env: KAMAL.config.builder.secrets } + KAMAL.with_verbosity(:debug) do + Dir.chdir(KAMAL.config.builder.build_directory) { execute *push } + end end end end diff --git a/lib/kamal/cli/main.rb b/lib/kamal/cli/main.rb index a47d8592..69735f2a 100644 --- a/lib/kamal/cli/main.rb +++ b/lib/kamal/cli/main.rb @@ -48,7 +48,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base end end - run_hook "post-deploy", secrets: true, runtime: runtime.round + run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s end desc "redeploy", "Deploy app to servers without bootstrapping servers, starting kamal-proxy, pruning, and registry login" @@ -75,7 +75,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base end end - run_hook "post-deploy", secrets: true, runtime: runtime.round + run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s end desc "rollback [VERSION]", "Rollback app to VERSION" @@ -99,7 +99,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base end end - run_hook "post-deploy", secrets: true, runtime: runtime.round if rolled_back + run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s if rolled_back end desc "details", "Show details about all containers" diff --git a/lib/kamal/commands/hook.rb b/lib/kamal/commands/hook.rb index eb710d5e..cb622d3b 100644 --- a/lib/kamal/commands/hook.rb +++ b/lib/kamal/commands/hook.rb @@ -1,9 +1,12 @@ class Kamal::Commands::Hook < Kamal::Commands::Base - def run(hook, secrets: false, **details) - env = tags(**details).env - env.merge!(config.secrets.to_h) if secrets + def run(hook) + [ hook_file(hook) ] + end - [ hook_file(hook), env: env ] + def env(secrets: false, **details) + tags(**details).env.tap do |env| + env.merge!(config.secrets.to_h) if secrets + end end def hook_exists?(hook) diff --git a/test/cli/build_test.rb b/test/cli/build_test.rb index 2d5d1051..4259fa5b 100644 --- a/test/cli/build_test.rb +++ b/test/cli/build_test.rb @@ -49,7 +49,7 @@ class CliBuildTest < CliTestCase SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :submodule, :update, "--init") SSHKit::Backend::Abstract.any_instance.expects(:execute) - .with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".", env: {}) + .with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".") SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:git, "-C", anything, :"rev-parse", :HEAD) @@ -140,7 +140,7 @@ class CliBuildTest < CliTestCase .returns("") SSHKit::Backend::Abstract.any_instance.expects(:execute) - .with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".", env: {}) + .with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".") run_command("push").tap do |output| assert_match /WARN Missing compatible builder, so creating a new one first/, output diff --git a/test/cli/cli_test_case.rb b/test/cli/cli_test_case.rb index 5a2bb76f..27bf7b69 100644 --- a/test/cli/cli_test_case.rb +++ b/test/cli/cli_test_case.rb @@ -41,27 +41,7 @@ class CliTestCase < ActiveSupport::TestCase end def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: false, secrets: false) - whoami = `whoami`.chomp - performer = Kamal::Git.email.presence || whoami - service = service_version.split("@").first - - assert_match "Running the #{hook} hook...\n", output - - expected = %r{Running\s/usr/bin/env\s\.kamal/hooks/#{hook}\sas\s#{whoami}@localhost\n\s - DEBUG\s\[[0-9a-f]*\]\sCommand:\s\(\sexport\s - KAMAL_RECORDED_AT=\"\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ\"\s - KAMAL_PERFORMER=\"#{performer}\"\s - KAMAL_VERSION=\"#{version}\"\s - KAMAL_SERVICE_VERSION=\"#{service_version}\"\s - KAMAL_SERVICE=\"#{service}\"\s - KAMAL_HOSTS=\"#{hosts}\"\s - KAMAL_COMMAND=\"#{command}\"\s - #{"KAMAL_SUBCOMMAND=\\\"#{subcommand}\\\"\\s" if subcommand} - #{"KAMAL_RUNTIME=\\\"\\d+\\\"\\s" if runtime} - #{"DB_PASSWORD=\"secret\"\\s" if secrets} - ;\s/usr/bin/env\s\.kamal/hooks/#{hook} }x - - assert_match expected, output + assert_match %r{usr/bin/env\s\.kamal/hooks/#{hook}}, output end def with_argv(*argv) diff --git a/test/commands/hook_test.rb b/test/commands/hook_test.rb index dc2afc5a..fe1cdc3f 100644 --- a/test/commands/hook_test.rb +++ b/test/commands/hook_test.rb @@ -16,41 +16,34 @@ class CommandsHookTest < ActiveSupport::TestCase end test "run" do - assert_equal [ - ".kamal/hooks/foo", - { env: { - "KAMAL_RECORDED_AT" => @recorded_at, - "KAMAL_PERFORMER" => @performer, - "KAMAL_VERSION" => "123", - "KAMAL_SERVICE_VERSION" => "app@123", - "KAMAL_SERVICE" => "app" } } - ], new_command.run("foo") + assert_equal [ ".kamal/hooks/foo" ], new_command.run("foo") + end + + test "env" do + assert_equal ({ + "KAMAL_RECORDED_AT" => @recorded_at, + "KAMAL_PERFORMER" => @performer, + "KAMAL_VERSION" => "123", + "KAMAL_SERVICE_VERSION" => "app@123", + "KAMAL_SERVICE" => "app" + }), new_command.env end test "run with custom hooks_path" do - assert_equal [ - "custom/hooks/path/foo", - { env: { - "KAMAL_RECORDED_AT" => @recorded_at, - "KAMAL_PERFORMER" => @performer, - "KAMAL_VERSION" => "123", - "KAMAL_SERVICE_VERSION" => "app@123", - "KAMAL_SERVICE" => "app" } } - ], new_command(hooks_path: "custom/hooks/path").run("foo") + assert_equal [ "custom/hooks/path/foo" ], new_command(hooks_path: "custom/hooks/path").run("foo") end - test "hook with secrets" do + test "env with secrets" do with_test_secrets("secrets" => "DB_PASSWORD=secret") do - assert_equal [ - ".kamal/hooks/foo", - { env: { + assert_equal ( + { "KAMAL_RECORDED_AT" => @recorded_at, "KAMAL_PERFORMER" => @performer, "KAMAL_VERSION" => "123", "KAMAL_SERVICE_VERSION" => "app@123", "KAMAL_SERVICE" => "app", - "DB_PASSWORD" => "secret" } } - ], new_command(env: { "secret" => [ "DB_PASSWORD" ] }).run("foo", secrets: true) + "DB_PASSWORD" => "secret" } + ), new_command.env(secrets: true) end end From 6e65968bdc8f8aa4c467d3f78252bc51e0e405af Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 19 Sep 2024 16:25:09 +0100 Subject: [PATCH 57/71] Integration test two apps Use localhost for app_with_roles and 127.0.0.1 for app. Confirm we can deploy both and the respond to requests. Ensure the proxy is removed once both have been removed. --- test/integration/broken_deploy_test.rb | 6 ++-- .../docker/deployer/app/config/deploy.yml | 3 +- .../deployer/app_with_roles/config/deploy.yml | 5 +-- .../docker/load_balancer/default.conf | 2 ++ test/integration/integration_test.rb | 27 +++++++++++--- test/integration/main_test.rb | 35 +++++++++++++++---- test/integration/proxy_test.rb | 13 ------- 7 files changed, 62 insertions(+), 29 deletions(-) diff --git a/test/integration/broken_deploy_test.rb b/test/integration/broken_deploy_test.rb index 964f1d0b..5d69416a 100644 --- a/test/integration/broken_deploy_test.rb +++ b/test/integration/broken_deploy_test.rb @@ -9,7 +9,7 @@ class BrokenDeployTest < IntegrationTest kamal :deploy assert_app_is_up version: first_version - assert_container_running host: :vm3, name: "app-workers-#{first_version}" + assert_container_running host: :vm3, name: "app_with_roles-workers-#{first_version}" second_version = break_app @@ -17,8 +17,8 @@ class BrokenDeployTest < IntegrationTest assert_failed_deploy output assert_app_is_up version: first_version - assert_container_running host: :vm3, name: "app-workers-#{first_version}" - assert_container_not_running host: :vm3, name: "app-workers-#{second_version}" + assert_container_running host: :vm3, name: "app_with_roles-workers-#{first_version}" + assert_container_not_running host: :vm3, name: "app_with_roles-workers-#{second_version}" end private diff --git a/test/integration/docker/deployer/app/config/deploy.yml b/test/integration/docker/deployer/app/config/deploy.yml index 13408ab7..242d893a 100644 --- a/test/integration/docker/deployer/app/config/deploy.yml +++ b/test/integration/docker/deployer/app/config/deploy.yml @@ -23,7 +23,8 @@ asset_path: /usr/share/nginx/html/versions deploy_timeout: 2 drain_timeout: 2 readiness_delay: 0 - +proxy: + host: 127.0.0.1 registry: server: registry:4443 username: root 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 3539185e..b31fb4fd 100644 --- a/test/integration/docker/deployer/app_with_roles/config/deploy.yml +++ b/test/integration/docker/deployer/app_with_roles/config/deploy.yml @@ -1,5 +1,5 @@ -service: app -image: app +service: app_with_roles +image: app_with_roles servers: web: hosts: @@ -14,6 +14,7 @@ drain_timeout: 2 readiness_delay: 0 proxy: + host: localhost healthcheck: interval: 1 timeout: 1 diff --git a/test/integration/docker/load_balancer/default.conf b/test/integration/docker/load_balancer/default.conf index 82d49973..79bbb89f 100644 --- a/test/integration/docker/load_balancer/default.conf +++ b/test/integration/docker/load_balancer/default.conf @@ -8,6 +8,8 @@ server { location / { proxy_pass http://loadbalancer; + proxy_set_header Host $host; + proxy_connect_timeout 10; proxy_send_timeout 10; proxy_read_timeout 10; diff --git a/test/integration/integration_test.rb b/test/integration/integration_test.rb index c7938689..ad99f3e2 100644 --- a/test/integration/integration_test.rb +++ b/test/integration/integration_test.rb @@ -50,8 +50,8 @@ class IntegrationTest < ActiveSupport::TestCase assert_equal "502", response.code end - def assert_app_is_up(version: nil) - response = app_response + def assert_app_is_up(version: nil, app: @app) + response = app_response(app: app) debug_response_code(response, "200") assert_equal "200", response.code assert_app_version(version, response) if version @@ -69,8 +69,8 @@ class IntegrationTest < ActiveSupport::TestCase assert_equal up_times, up_count end - def app_response - Net::HTTP.get_response(URI.parse("http://localhost:12345/version")) + def app_response(app: @app) + Net::HTTP.get_response(URI.parse("http://#{app_host(app)}:12345/version")) end def update_app_rev @@ -156,4 +156,23 @@ class IntegrationTest < ActiveSupport::TestCase def assert_directory_removed(directory) assert docker_compose("exec vm1 ls #{directory} | wc -l", capture: true).strip == "0" end + + def assert_proxy_running + assert_container_running(host: "vm1", name: "kamal-proxy") + end + + def assert_proxy_not_running + assert_container_not_running(host: "vm1", name: "kamal-proxy") + end + + def app_host(app = @app) + case app + when "app" + "127.0.0.1" + when "app_with_roles" + "localhost" + else + raise "Unknown app: #{app}" + end + end end diff --git a/test/integration/main_test.rb b/test/integration/main_test.rb index 2385799e..e3aa1ef3 100644 --- a/test/integration/main_test.rb +++ b/test/integration/main_test.rb @@ -46,13 +46,13 @@ class MainTest < IntegrationTest assert_app_is_up version: version assert_hooks_ran "pre-connect", "pre-build", "pre-deploy", "post-deploy" - assert_container_running host: :vm3, name: "app-workers-#{version}" + assert_container_running host: :vm3, name: "app_with_roles-workers-#{version}" second_version = update_app_rev kamal :redeploy assert_app_is_up version: second_version - assert_container_running host: :vm3, name: "app-workers-#{second_version}" + assert_container_running host: :vm3, name: "app_with_roles-workers-#{second_version}" end test "config" do @@ -100,6 +100,29 @@ class MainTest < IntegrationTest assert_app_directory_removed end + test "two apps" do + @app = "app" + kamal :deploy + app1_version = latest_app_version + + @app = "app_with_roles" + kamal :deploy + app2_version = latest_app_version + + assert_app_is_up version: app1_version, app: "app" + assert_app_is_up version: app2_version, app: "app_with_roles" + + @app = "app" + kamal :remove, "-y" + assert_app_directory_removed + assert_proxy_running + + @app = "app_with_roles" + kamal :remove, "-y" + assert_app_directory_removed + assert_proxy_not_running + end + private def assert_envs(version:) assert_env :CLEAR_TOKEN, "4321", version: version, vm: :vm1 @@ -115,21 +138,21 @@ class MainTest < IntegrationTest end def assert_env(key, value, vm:, version:) - assert_equal "#{key}=#{value}", docker_compose("exec #{vm} docker exec app-web-#{version} env | grep #{key}", capture: true) + assert_equal "#{key}=#{value}", docker_compose("exec #{vm} docker exec #{@app}-web-#{version} env | grep #{key}", capture: true) end def assert_no_env(key, vm:, version:) assert_raises(RuntimeError, /exit 1/) do - docker_compose("exec #{vm} docker exec app-web-#{version} env | grep #{key}", capture: true) + docker_compose("exec #{vm} docker exec #{@app}-web-#{version} env | grep #{key}", capture: true) end end def assert_accumulated_assets(*versions) versions.each do |version| - assert_equal "200", Net::HTTP.get_response(URI.parse("http://localhost:12345/versions/#{version}")).code + assert_equal "200", Net::HTTP.get_response(URI.parse("http://#{app_host}:12345/versions/#{version}")).code end - assert_equal "200", Net::HTTP.get_response(URI.parse("http://localhost:12345/versions/.hidden")).code + assert_equal "200", Net::HTTP.get_response(URI.parse("http://#{app_host}:12345/versions/.hidden")).code end def vm1_image_ids diff --git a/test/integration/proxy_test.rb b/test/integration/proxy_test.rb index 1a40d079..d861a887 100644 --- a/test/integration/proxy_test.rb +++ b/test/integration/proxy_test.rb @@ -49,17 +49,4 @@ class ProxyTest < IntegrationTest kamal :proxy, :remove assert_proxy_not_running end - - private - def assert_proxy_running - assert_match /basecamp\/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION} \"kamal-proxy run\"/, proxy_details - end - - def assert_proxy_not_running - assert_no_match /basecamp\/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION} \"kamal-proxy run\"/, proxy_details - end - - def proxy_details - kamal :proxy, :details, capture: true - end end From a4e5dbe5d4fcb5881017be381c9d5736e8dbeb49 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 19 Sep 2024 11:37:22 -0700 Subject: [PATCH 58/71] Bump version for 2.0.0.beta2 --- Gemfile.lock | 2 +- lib/kamal/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1d6096e0..959a5418 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - kamal (2.0.0.beta1) + kamal (2.0.0.beta2) activesupport (>= 7.0) base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) diff --git a/lib/kamal/version.rb b/lib/kamal/version.rb index 11afd2fa..b4968859 100644 --- a/lib/kamal/version.rb +++ b/lib/kamal/version.rb @@ -1,3 +1,3 @@ module Kamal - VERSION = "2.0.0.beta1" + VERSION = "2.0.0.beta2" end From a6a48c456c02f9e986efd244536e4514457436b0 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Fri, 20 Sep 2024 09:26:06 +0100 Subject: [PATCH 59/71] Response timeout should be a number Kamal will append the `s` for the duration when talking to kamal-proxy so no need to have it in the config. --- lib/kamal/configuration/docs/proxy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/kamal/configuration/docs/proxy.yml b/lib/kamal/configuration/docs/proxy.yml index 332a2c28..1fdc39dd 100644 --- a/lib/kamal/configuration/docs/proxy.yml +++ b/lib/kamal/configuration/docs/proxy.yml @@ -47,7 +47,7 @@ proxy: # Response timeout # # How long to wait for requests to complete before timing out, defaults to 30 seconds - response_timeout: 10s + response_timeout: 10 # Healthcheck # From 6df169a4fb26a02e274c54be28f0558b81942e22 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Fri, 20 Sep 2024 15:27:10 +0100 Subject: [PATCH 60/71] Doc updates --- lib/kamal/configuration/docs/builder.yml | 6 +----- .../configuration/docs/configuration.yml | 8 ++++++-- lib/kamal/configuration/docs/env.yml | 19 ++++++++++++++++--- lib/kamal/configuration/docs/proxy.yml | 2 +- lib/kamal/configuration/docs/role.yml | 8 ++++++-- 5 files changed, 30 insertions(+), 13 deletions(-) diff --git a/lib/kamal/configuration/docs/builder.yml b/lib/kamal/configuration/docs/builder.yml index 1c41a386..cdde194f 100644 --- a/lib/kamal/configuration/docs/builder.yml +++ b/lib/kamal/configuration/docs/builder.yml @@ -2,10 +2,6 @@ # # The builder configuration controls how the application is built with `docker build` # -# If no configuration is specified, Kamal will: -# 1. Create a buildx context called `kamal-local-docker-container`, using the docker-container driver -# 2. Use `docker build` to build a multiarch image for linux/amd64,linux/arm64 with that context -# # See https://kamal-deploy.org/docs/configuration/builder-examples/ for more information # Builder options @@ -78,7 +74,7 @@ builder: # Build secrets # - # Values are read from the .kamal/secrets. + # Values are read from .kamal/secrets. # secrets: - SECRET1 diff --git a/lib/kamal/configuration/docs/configuration.yml b/lib/kamal/configuration/docs/configuration.yml index 597a8bea..20530a3d 100644 --- a/lib/kamal/configuration/docs/configuration.yml +++ b/lib/kamal/configuration/docs/configuration.yml @@ -36,6 +36,8 @@ image: my-image labels: my-label: my-value +# Volumes +# # Additional volumes to mount into the container volumes: - /path/on/host:/path/in/container:ro @@ -58,7 +60,7 @@ servers: env: ... -# Asset Bridging +# Asset Path # # Used for asset bridging across deployments, default to `nil` # @@ -74,6 +76,8 @@ env: # To configure this, set the path to the assets: asset_path: /path/to/assets +# Hooks path +# # Path to hooks, defaults to `.kamal/hooks` # See https://kamal-deploy.org/docs/hooks for more information hooks_path: /user_home/kamal/hooks @@ -83,7 +87,7 @@ hooks_path: /user_home/kamal/hooks # Whether deployments require a destination to be specified, defaults to `false` require_destination: true -# The primary role +# Primary role # # This defaults to `web`, but if you have no web role, you can change this primary_role: workers diff --git a/lib/kamal/configuration/docs/env.yml b/lib/kamal/configuration/docs/env.yml index 513799d4..4c38442c 100644 --- a/lib/kamal/configuration/docs/env.yml +++ b/lib/kamal/configuration/docs/env.yml @@ -12,11 +12,16 @@ env: DATABASE_HOST: mysql-db1 DATABASE_PORT: 3306 -# Using .kamal/secrets file to load required environment variables +# Secrets # -# Kamal uses dotenv to automatically load environment variables set in the .kamal/secrets file. +# Kamal uses dotenv to automatically load environment variables set in the `.kamal/secrets` file. # -# This file can be used to set variables like KAMAL_REGISTRY_PASSWORD or database passwords. +# If you are using destinations, secrets will instead be read from `.kamal/secrets-` if +# it exists. +# +# Common secrets across all destinations can be set in `.kamal/secrets-common`. +# +# This file can be used to set variables like `KAMAL_REGISTRY_PASSWORD` or database passwords. # You can use variable or command substitution in the secrets file. # # ``` @@ -24,6 +29,14 @@ env: # RAILS_MASTER_KEY=$(cat config/master.key) # ``` # +# You can also use [secret helpers](../commands/secrets) for some common password managers. +# ``` +# SECRETS=$(kamal secrets fetch ...) +# +# REGISTRY_PASSWORD=$(kamal secrets extract REGISTRY_PASSWORD $SECRETS) +# DB_PASSWORD=$(kamal secrets extract DB_PASSWORD $SECRETS) +# ``` +# # If you store secrets directly in .kamal/secrets, ensure that it is not checked into version control. # # To pass the secrets you should list them under the `secret` key. When you do this the diff --git a/lib/kamal/configuration/docs/proxy.yml b/lib/kamal/configuration/docs/proxy.yml index 1fdc39dd..04eea4fc 100644 --- a/lib/kamal/configuration/docs/proxy.yml +++ b/lib/kamal/configuration/docs/proxy.yml @@ -91,7 +91,7 @@ proxy: # Forward headers # - # Whether to forward the X-Forwarded-For and X-Forwarded-Proto headers (defaults to false) + # Whether to forward the X-Forwarded-For and X-Forwarded-Proto headers. # # If you are behind a trusted proxy, you can set this to true to forward the headers. # diff --git a/lib/kamal/configuration/docs/role.yml b/lib/kamal/configuration/docs/role.yml index fbc8a813..dc27c69e 100644 --- a/lib/kamal/configuration/docs/role.yml +++ b/lib/kamal/configuration/docs/role.yml @@ -26,8 +26,12 @@ servers: # # When there are other options to set, the list of hosts goes under the `hosts` key # - # By default only the primary role uses a proxy, but you can set `proxy` to change - # it. + # By default only the primary role uses a proxy. + # + # For other roles, you can set it to `proxy: true` enable it and inherit the root proxy + # configuration or provide a map of options to override the root configuration. + # + # For the primary role, you can set `proxy: false` to disable the proxy. # # You can also set a custom cmd to run in the container, and overwrite other settings # from the root configuration. From 384b36d158550cc0754f7f11478036b232786f2b Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 20 Sep 2024 07:42:31 -0700 Subject: [PATCH 61/71] Add option to skip timestamps on logging output So it is easier to follow live when you are doing debugging, especially early days app setup when you are the only user. --- lib/kamal/cli/accessory.rb | 8 +++++--- lib/kamal/cli/app.rb | 8 +++++--- lib/kamal/cli/proxy.rb | 8 +++++--- lib/kamal/commands/accessory.rb | 8 ++++---- lib/kamal/commands/app/logging.rb | 4 ++-- lib/kamal/commands/proxy.rb | 10 ++++++---- test/commands/accessory_test.rb | 8 ++++++++ test/commands/app_test.rb | 4 ++++ test/commands/proxy_test.rb | 6 ++++++ 9 files changed, 45 insertions(+), 19 deletions(-) diff --git a/lib/kamal/cli/accessory.rb b/lib/kamal/cli/accessory.rb index dd56231e..45efbe6e 100644 --- a/lib/kamal/cli/accessory.rb +++ b/lib/kamal/cli/accessory.rb @@ -147,23 +147,25 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base 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)" + option :skip_timestamps, aliases: "-T", desc: "Skip appending timestamps to logging output" def logs(name) with_accessory(name) do |accessory, hosts| grep = options[:grep] grep_options = options[:grep_options] + timestamps = !options[:skip_timestamps] if options[:follow] run_locally do info "Following logs on #{hosts}..." - info accessory.follow_logs(grep: grep, grep_options: grep_options) - exec accessory.follow_logs(grep: grep, grep_options: grep_options) + info accessory.follow_logs(timestamps: timestamps, grep: grep, grep_options: grep_options) + exec accessory.follow_logs(timestamps: timestamps, 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(hosts) do - puts capture_with_info(*accessory.logs(since: since, lines: lines, grep: grep, grep_options: grep_options)) + puts capture_with_info(*accessory.logs(timestamps: timestamps, since: since, lines: lines, grep: grep, grep_options: grep_options)) end end end diff --git a/lib/kamal/cli/app.rb b/lib/kamal/cli/app.rb index d70d066b..2382951d 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -188,12 +188,14 @@ class Kamal::Cli::App < Kamal::Cli::Base 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 log on primary server (or specific host set by --hosts)" + option :skip_timestamps, aliases: "-T", desc: "Skip appending timestamps to logging output" def logs # FIXME: Catch when app containers aren't running grep = options[:grep] grep_options = options[:grep_options] since = options[:since] + timestamps = !options[:skip_timestamps] if options[:follow] lines = options[:lines].presence || ((since || grep) ? nil : 10) # Default to 10 lines if since or grep isn't set @@ -205,8 +207,8 @@ class Kamal::Cli::App < Kamal::Cli::Base role = KAMAL.roles_on(KAMAL.primary_host).first app = KAMAL.app(role: role, host: host) - info app.follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep, grep_options: grep_options) - exec app.follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep, grep_options: grep_options) + info app.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options) + exec app.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options) end else lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set @@ -216,7 +218,7 @@ class Kamal::Cli::App < Kamal::Cli::Base roles.each do |role| begin - puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).logs(since: since, lines: lines, grep: grep, grep_options: grep_options)) + puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).logs(timestamps: timestamps, since: since, lines: lines, grep: grep, grep_options: grep_options)) rescue SSHKit::Command::Failed puts_by_host host, "Nothing found" end diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index d006c8c6..0fc595b0 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -140,21 +140,23 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base 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)" + option :skip_timestamps, aliases: "-T", desc: "Skip appending timestamps to logging output" def logs grep = options[:grep] + timestamps = !options[:skip_timestamps] if options[:follow] run_locally do info "Following logs on #{KAMAL.primary_host}..." - info KAMAL.proxy.follow_logs(host: KAMAL.primary_host, grep: grep) - exec KAMAL.proxy.follow_logs(host: KAMAL.primary_host, grep: grep) + info KAMAL.proxy.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, grep: grep) + exec KAMAL.proxy.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, 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.proxy_hosts) do |host| - puts_by_host host, capture(*KAMAL.proxy.logs(since: since, lines: lines, grep: grep)), type: "Proxy" + puts_by_host host, capture(*KAMAL.proxy.logs(timestamps: timestamps, since: since, lines: lines, grep: grep)), type: "Proxy" end end end diff --git a/lib/kamal/commands/accessory.rb b/lib/kamal/commands/accessory.rb index 787f7d43..0c1b9009 100644 --- a/lib/kamal/commands/accessory.rb +++ b/lib/kamal/commands/accessory.rb @@ -39,16 +39,16 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base end - def logs(since: nil, lines: nil, grep: nil, grep_options: nil) + def logs(timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil) pipe \ - docker(:logs, service_name, (" --since #{since}" if since), (" --tail #{lines}" if lines), "--timestamps", "2>&1"), + docker(:logs, service_name, (" --since #{since}" if since), (" --tail #{lines}" if lines), ("--timestamps" if timestamps), "2>&1"), ("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep) end - def follow_logs(grep: nil, grep_options: nil) + def follow_logs(timestamps: true, grep: nil, grep_options: nil) run_over_ssh \ pipe \ - docker(:logs, service_name, "--timestamps", "--tail", "10", "--follow", "2>&1"), + docker(:logs, service_name, ("--timestamps" if timestamps), "--tail", "10", "--follow", "2>&1"), (%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep) end diff --git a/lib/kamal/commands/app/logging.rb b/lib/kamal/commands/app/logging.rb index be8a4bad..31b1981e 100644 --- a/lib/kamal/commands/app/logging.rb +++ b/lib/kamal/commands/app/logging.rb @@ -6,11 +6,11 @@ module Kamal::Commands::App::Logging ("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep) end - def follow_logs(host:, lines: nil, grep: nil, grep_options: nil) + def follow_logs(host:, timestamps: true, lines: nil, grep: nil, grep_options: nil) run_over_ssh \ pipe( current_running_container_id, - "xargs docker logs --timestamps#{" --tail #{lines}" if lines} --follow 2>&1", + "xargs docker logs #{"--timestamps" if timestamps}#{" --tail #{lines}" if lines} --follow 2>&1", (%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep) ), host: host diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb index df264a6b..90d03141 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -35,15 +35,17 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base [ :cut, "-d:", "-f2" ] end - def logs(since: nil, lines: nil, grep: nil, grep_options: nil) + def logs(timestamps: true, 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"), + docker(:logs, container_name, (" --since #{since}" if since), (" --tail #{lines}" if lines), ("--timestamps" if timestamps), "2>&1"), ("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep) end - def follow_logs(host:, grep: nil, grep_options: nil) + def follow_logs(host:, timestamps: true, grep: nil, grep_options: nil) run_over_ssh pipe( - docker(:logs, container_name, "--timestamps", "--tail", "10", "--follow", "2>&1"), + docker(:logs, container_name), + ("--timestamps" if timestamps), + "--tail", "10", "--follow", "2>&1"), (%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep) ).join(" "), host: host end diff --git a/test/commands/accessory_test.rb b/test/commands/accessory_test.rb index 19fe745d..f3d71ffd 100644 --- a/test/commands/accessory_test.rb +++ b/test/commands/accessory_test.rb @@ -130,12 +130,20 @@ class CommandsAccessoryTest < ActiveSupport::TestCase assert_equal \ "docker logs app-mysql --since 5m --tail 100 --timestamps 2>&1 | grep 'thing' -C 2", new_command(:mysql).logs(since: "5m", lines: 100, grep: "thing", grep_options: "-C 2").join(" ") + + assert_equal \ + "docker logs app-mysql --since 5m --tail 100 2>&1 | grep 'thing' -C 2", + new_command(:mysql).logs(timestamps: false, since: "5m", lines: 100, grep: "thing", grep_options: "-C 2").join(" ") end test "follow logs" do assert_equal \ "ssh -t root@1.1.1.5 -p 22 'docker logs app-mysql --timestamps --tail 10 --follow 2>&1'", new_command(:mysql).follow_logs + + assert_equal \ + "ssh -t root@1.1.1.5 -p 22 'docker logs app-mysql --tail 10 --follow 2>&1'", + new_command(:mysql).follow_logs(timestamps: false) end test "remove container" do diff --git a/test/commands/app_test.rb b/test/commands/app_test.rb index 57f8c2fe..23dd6c86 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -191,6 +191,10 @@ class CommandsAppTest < ActiveSupport::TestCase assert_equal \ "ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --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 --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --tail 123 --follow 2>&1 | grep \"Completed\"'", new_command.follow_logs(host: "app-1", lines: 123, grep: "Completed") + + assert_equal \ + "ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --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 --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --tail 123 --follow 2>&1 | grep \"Completed\"'", + new_command.follow_logs(host: "app-1", timestamps: false, lines: 123, grep: "Completed") end diff --git a/test/commands/proxy_test.rb b/test/commands/proxy_test.rb index 4a4e029e..4f6275a2 100644 --- a/test/commands/proxy_test.rb +++ b/test/commands/proxy_test.rb @@ -77,6 +77,12 @@ class CommandsProxyTest < ActiveSupport::TestCase new_command.logs(lines: 10).join(" ") end + test "proxy logs without timestamps" do + assert_equal \ + "docker logs kamal-proxy 2>&1", + new_command.logs(timestamps: false).join(" ") + end + test "proxy logs with grep hello!" do assert_equal \ "docker logs kamal-proxy --timestamps 2>&1 | grep 'hello!'", From afa6898a82b374dfd1e9b07edea6ca9d0b4b5863 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 20 Sep 2024 07:58:38 -0700 Subject: [PATCH 62/71] Fix pipe --- lib/kamal/commands/proxy.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb index 90d03141..9b145d2d 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -43,9 +43,7 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base def follow_logs(host:, timestamps: true, grep: nil, grep_options: nil) run_over_ssh pipe( - docker(:logs, container_name), - ("--timestamps" if timestamps), - "--tail", "10", "--follow", "2>&1"), + docker(:logs, container_name, ("--timestamps" if timestamps), "--tail", "10", "--follow", "2>&1"), (%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep) ).join(" "), host: host end From 6ab5fc9459bd42af4bd4e0d3d09b58fc856cc0b3 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 20 Sep 2024 08:04:28 -0700 Subject: [PATCH 63/71] Allow timestamps on/off for app logging too --- lib/kamal/commands/app/logging.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/kamal/commands/app/logging.rb b/lib/kamal/commands/app/logging.rb index 31b1981e..ad66f370 100644 --- a/lib/kamal/commands/app/logging.rb +++ b/lib/kamal/commands/app/logging.rb @@ -1,8 +1,8 @@ module Kamal::Commands::App::Logging - def logs(version: nil, since: nil, lines: nil, grep: nil, grep_options: nil) + def logs(version: nil, timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil) pipe \ version ? container_id_for_version(version) : current_running_container_id, - "xargs docker logs#{" --since #{since}" if since}#{" --tail #{lines}" if lines} 2>&1", + "xargs docker logs#{" --timestamps" if timestamps}#{" --since #{since}" if since}#{" --tail #{lines}" if lines} 2>&1", ("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep) end @@ -10,7 +10,7 @@ module Kamal::Commands::App::Logging run_over_ssh \ pipe( current_running_container_id, - "xargs docker logs #{"--timestamps" if timestamps}#{" --tail #{lines}" if lines} --follow 2>&1", + "xargs docker logs#{" --timestamps" if timestamps}#{" --tail #{lines}" if lines} --follow 2>&1", (%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep) ), host: host From 8693e968c1d5565746dc8a2fc70ecdd5a59e82ec Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 20 Sep 2024 08:17:19 -0700 Subject: [PATCH 64/71] Timestamps now default on for app logs too --- test/commands/app_test.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/commands/app_test.rb b/test/commands/app_test.rb index 23dd6c86..dc28ba57 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -129,25 +129,25 @@ class CommandsAppTest < ActiveSupport::TestCase test "logs" do assert_equal \ - "sh -c 'docker ps --latest --quiet --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 --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs 2>&1", + "sh -c 'docker ps --latest --quiet --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 --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1", new_command.logs.join(" ") end test "logs with since" do assert_equal \ - "sh -c 'docker ps --latest --quiet --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 --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --since 5m 2>&1", + "sh -c 'docker ps --latest --quiet --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 --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1", new_command.logs(since: "5m").join(" ") end test "logs with lines" do assert_equal \ - "sh -c 'docker ps --latest --quiet --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 --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --tail 100 2>&1", + "sh -c 'docker ps --latest --quiet --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 --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --tail 100 2>&1", new_command.logs(lines: "100").join(" ") end test "logs with since and lines" do assert_equal \ - "sh -c 'docker ps --latest --quiet --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 --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --since 5m --tail 100 2>&1", + "sh -c 'docker ps --latest --quiet --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 --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m --tail 100 2>&1", new_command.logs(since: "5m", lines: "100").join(" ") end From d98500982d98d80a122b42c223e4dc1052068ee7 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 20 Sep 2024 08:19:38 -0700 Subject: [PATCH 65/71] Update tests --- test/cli/app_test.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index 97e1c461..7c208da2 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -315,11 +315,11 @@ class CliAppTest < CliTestCase SSHKit::Backend::Abstract.any_instance.stubs(:exec) .with("ssh -t root@1.1.1.1 'sh -c 'docker ps --latest --quiet --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 --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1| xargs docker logs --timestamps --tail 10 2>&1'") - assert_match "sh -c 'docker ps --latest --quiet --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 --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --tail 100 2>&1", run_command("logs") + assert_match "sh -c 'docker ps --latest --quiet --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 --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --tail 100 2>&1", run_command("logs") - assert_match "sh -c 'docker ps --latest --quiet --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 --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs 2>&1 | grep 'hey'", run_command("logs", "--grep", "hey") + assert_match "sh -c 'docker ps --latest --quiet --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 --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'hey'", run_command("logs", "--grep", "hey") - assert_match "sh -c 'docker ps --latest --quiet --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 --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs 2>&1 | grep 'hey' -C 2", run_command("logs", "--grep", "hey", "--grep-options", "-C 2") + assert_match "sh -c 'docker ps --latest --quiet --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 --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'hey' -C 2", run_command("logs", "--grep", "hey", "--grep-options", "-C 2") end test "logs with follow" do From 487f6f5f5358c79544e5abde0303ce39b14f2fa9 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 20 Sep 2024 08:31:56 -0700 Subject: [PATCH 66/71] Fix excess spacing --- lib/kamal/commands/proxy.rb | 2 +- test/commands/proxy_test.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb index 9b145d2d..5ac1c94e 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -37,7 +37,7 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base def logs(timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil) pipe \ - docker(:logs, container_name, (" --since #{since}" if since), (" --tail #{lines}" if lines), ("--timestamps" if timestamps), "2>&1"), + docker(:logs, container_name, ("--since #{since}" if since), ("--tail #{lines}" if lines), ("--timestamps" if timestamps), "2>&1"), ("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep) end diff --git a/test/commands/proxy_test.rb b/test/commands/proxy_test.rb index 4f6275a2..e8a3f252 100644 --- a/test/commands/proxy_test.rb +++ b/test/commands/proxy_test.rb @@ -67,19 +67,19 @@ class CommandsProxyTest < ActiveSupport::TestCase test "proxy logs since 2h" do assert_equal \ - "docker logs kamal-proxy --since 2h --timestamps 2>&1", + "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", + "docker logs kamal-proxy --tail 10 --timestamps 2>&1", new_command.logs(lines: 10).join(" ") end test "proxy logs without timestamps" do assert_equal \ - "docker logs kamal-proxy 2>&1", + "docker logs kamal-proxy 2>&1", new_command.logs(timestamps: false).join(" ") end From eabd57350cf3f465396f7837cbbaf8a1da9cb6a5 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 20 Sep 2024 08:33:14 -0700 Subject: [PATCH 67/71] Fix tests --- test/commands/app_test.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/commands/app_test.rb b/test/commands/app_test.rb index dc28ba57..6704adb6 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -153,25 +153,25 @@ class CommandsAppTest < ActiveSupport::TestCase test "logs with grep" do assert_equal \ - "sh -c 'docker ps --latest --quiet --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 --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs 2>&1 | grep 'my-id'", + "sh -c 'docker ps --latest --quiet --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 --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'my-id'", new_command.logs(grep: "my-id").join(" ") end test "logs with grep and grep options" do assert_equal \ - "sh -c 'docker ps --latest --quiet --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 --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs 2>&1 | grep 'my-id' -C 2", + "sh -c 'docker ps --latest --quiet --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 --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'my-id' -C 2", new_command.logs(grep: "my-id", grep_options: "-C 2").join(" ") end test "logs with since, grep and grep options" do assert_equal \ - "sh -c 'docker ps --latest --quiet --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 --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --since 5m 2>&1 | grep 'my-id' -C 2", + "sh -c 'docker ps --latest --quiet --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 --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1 | grep 'my-id' -C 2", new_command.logs(since: "5m", grep: "my-id", grep_options: "-C 2").join(" ") end test "logs with since and grep" do assert_equal \ - "sh -c 'docker ps --latest --quiet --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 --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --since 5m 2>&1 | grep 'my-id'", + "sh -c 'docker ps --latest --quiet --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 --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1 | grep 'my-id'", new_command.logs(since: "5m", grep: "my-id").join(" ") end From 04d21f45bbe1f0a791ef33c90323f7f7d61a260a Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 20 Sep 2024 08:45:40 -0700 Subject: [PATCH 68/71] Fix test --- test/cli/proxy_test.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/cli/proxy_test.rb b/test/cli/proxy_test.rb index 72a0aa13..c9987a51 100644 --- a/test/cli/proxy_test.rb +++ b/test/cli/proxy_test.rb @@ -111,11 +111,11 @@ class CliProxyTest < CliTestCase test "logs" do SSHKit::Backend::Abstract.any_instance.stubs(:capture) - .with(:docker, :logs, "kamal-proxy", " --tail 100", "--timestamps", "2>&1") + .with(:docker, :logs, "kamal-proxy", "--tail 100", "--timestamps", "2>&1") .returns("Log entry") SSHKit::Backend::Abstract.any_instance.stubs(:capture) - .with(:docker, :logs, "proxy", " --tail 100", "--timestamps", "2>&1") + .with(:docker, :logs, "proxy", "--tail 100", "--timestamps", "2>&1") .returns("Log entry") run_command("logs").tap do |output| From b89ec2bf638976ce3dc942cf1a6609106b91d243 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 20 Sep 2024 11:08:45 -0700 Subject: [PATCH 69/71] Bump version for 2.0.0.rc1 --- Gemfile.lock | 2 +- lib/kamal/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 959a5418..bb89ab64 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - kamal (2.0.0.beta2) + kamal (2.0.0.rc1) activesupport (>= 7.0) base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) diff --git a/lib/kamal/version.rb b/lib/kamal/version.rb index b4968859..6b75ba0f 100644 --- a/lib/kamal/version.rb +++ b/lib/kamal/version.rb @@ -1,3 +1,3 @@ module Kamal - VERSION = "2.0.0.beta2" + VERSION = "2.0.0.rc1" end From 567309596a6f972cb2003f784d8e26d137b5d986 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 20 Sep 2024 12:50:46 -0700 Subject: [PATCH 70/71] Make the skip of timestamps a boolean --- lib/kamal/cli/accessory.rb | 2 +- lib/kamal/cli/app.rb | 2 +- lib/kamal/cli/proxy.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/kamal/cli/accessory.rb b/lib/kamal/cli/accessory.rb index 45efbe6e..14114a25 100644 --- a/lib/kamal/cli/accessory.rb +++ b/lib/kamal/cli/accessory.rb @@ -147,7 +147,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base 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)" - option :skip_timestamps, aliases: "-T", desc: "Skip appending timestamps to logging output" + option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output" def logs(name) with_accessory(name) do |accessory, hosts| grep = options[:grep] diff --git a/lib/kamal/cli/app.rb b/lib/kamal/cli/app.rb index 2382951d..7c77a7cf 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -188,7 +188,7 @@ class Kamal::Cli::App < Kamal::Cli::Base 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 log on primary server (or specific host set by --hosts)" - option :skip_timestamps, aliases: "-T", desc: "Skip appending timestamps to logging output" + option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output" def logs # FIXME: Catch when app containers aren't running diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index 0fc595b0..66912e07 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -140,7 +140,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base 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)" - option :skip_timestamps, aliases: "-T", desc: "Skip appending timestamps to logging output" + option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output" def logs grep = options[:grep] timestamps = !options[:skip_timestamps] From e71bfcbaddecbd108099f3aab76a06d2c7eecf9d Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 20 Sep 2024 15:41:26 -0700 Subject: [PATCH 71/71] Bump version for 2.0.0.rc2 --- Gemfile.lock | 2 +- lib/kamal/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index bb89ab64..ed0d8a8b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - kamal (2.0.0.rc1) + kamal (2.0.0.rc2) activesupport (>= 7.0) base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) diff --git a/lib/kamal/version.rb b/lib/kamal/version.rb index 6b75ba0f..46616deb 100644 --- a/lib/kamal/version.rb +++ b/lib/kamal/version.rb @@ -1,3 +1,3 @@ module Kamal - VERSION = "2.0.0.rc1" + VERSION = "2.0.0.rc2" end