From 015c5a6f901c8d96dc8cce93e57c471b605cef3e Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Tue, 28 May 2024 15:19:27 +0100 Subject: [PATCH] Add kamal proxy update For easy updates from Traefik to kamal-proxy, add `kamal proxy update`. This stops and removes Traefik and kamal-proxy containers (just in case it is run after an update). Then it starts kamal-proxy and calls `kamal-proxy deploy` to route to any app containers that are already running. It can be run with a rolling option and calls the `pre-proxy-reboot` and `post-proxy-reboot` hooks for each host. --- lib/kamal/cli/proxy.rb | 44 +++++++++++++++++++++++++++++++++++++ lib/kamal/commands/proxy.rb | 12 +++++----- test/cli/proxy_test.rb | 22 +++++++++++++++++++ 3 files changed, 72 insertions(+), 6 deletions(-) diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index d0373eaf..ab0c50a7 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -60,6 +60,50 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base end end + desc "update", "Update from Traefik to kamal-proxy, for when moving from Kamal v1 to Kamal v2" + 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 update + confirming "This will cause a brief outage on each host. Are you sure?" do + with_lock do + host_groups = options[:rolling] ? KAMAL.proxy_hosts : [ KAMAL.proxy_hosts ] + host_groups.each do |hosts| + host_list = Array(hosts).join(",") + run_hook "pre-proxy-reboot", hosts: host_list + on(hosts) do + info "Updating proxy from Traefik to kamal-proxy on #{host}..." + execute *KAMAL.auditor.record("Updated proxy from Traefik to kamal-proxy"), verbosity: :debug + execute *KAMAL.registry.login + + info "Stopping and removing Traefik on #{host}..." + execute *KAMAL.proxy.stop(name: "traefik"), raise_on_non_zero_exit: false + execute *KAMAL.proxy.remove_container(filter: "label=org.opencontainers.image.title=traefik") + execute *KAMAL.proxy.remove_image(filter: "label=org.opencontainers.image.title=traefik") + + info "Stopping and removing kamal-proxy on #{host}, if running..." + execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false + execute *KAMAL.proxy.remove_container + + info "Starting kamal-proxy on #{host}..." + execute *KAMAL.proxy.run + + 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_endpoint(version: version)).strip + raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, is the app container running?" if endpoint.empty? + + info "Deploying #{endpoint} for role `#{role}` on #{host}..." + execute *KAMAL.proxy.deploy(role.container_prefix, target: endpoint) + end + end + run_hook "post-proxy-reboot", hosts: host_list + end + end + end + end + desc "details", "Show details about proxy container from servers" def details on(KAMAL.proxy_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.proxy.info), type: "Proxy" } diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb index 8b300014..e591efec 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -26,8 +26,8 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base docker :container, :start, container_name end - def stop - docker :container, :stop, container_name + def stop(name: container_name) + docker :container, :stop, name end def start_or_run @@ -60,12 +60,12 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base ).join(" "), host: host end - def remove_container - docker :container, :prune, "--force", "--filter", container_filter + def remove_container(filter: container_filter) + docker :container, :prune, "--force", "--filter", filter end - def remove_image - docker :image, :prune, "--all", "--force", "--filter", image_filter + def remove_image(filter: image_filter) + docker :image, :prune, "--all", "--force", "--filter", filter end private diff --git a/test/cli/proxy_test.rb b/test/cli/proxy_test.rb index a120560f..b27dfec5 100644 --- a/test/cli/proxy_test.rb +++ b/test/cli/proxy_test.rb @@ -87,6 +87,28 @@ class CliProxyTest < CliTestCase end end + test "update" 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("update", "-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 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 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 exec kamal-proxy kamal-proxy deploy app-web --target \"172.1.0.2:80\"", output + end + end + private def run_command(*command) stdouted { Kamal::Cli::Proxy.start([ *command, "-c", "test/fixtures/deploy_with_accessories.yml" ]) }