diff --git a/README.md b/README.md index 9b95066f9..924e1e278 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Kamal: Deploy web apps anywhere -From bare metal to cloud VMs, deploy web apps anywhere with zero downtime. Kamal has the dynamic reverse-proxy Traefik hold requests while a new app container is started and the old one is stopped. Works seamlessly across multiple hosts, using SSHKit to execute commands. Originally built for Rails apps, Kamal will work with any type of web app that can be containerized with Docker. +From bare metal to cloud VMs, deploy web apps anywhere with zero downtime. Kamal uses [kamal-proxy](https://github.com/basecamp/kamal-proxy) to seamlessly switch requests between containers. Works seamlessly across multiple servers, using SSHKit to execute commands. Originally built for Rails apps, Kamal will work with any type of web app that can be containerized with Docker. ➡️ See [kamal-deploy.org](https://kamal-deploy.org) for documentation on [installation](https://kamal-deploy.org/docs/installation), [configuration](https://kamal-deploy.org/docs/configuration), and [commands](https://kamal-deploy.org/docs/commands). diff --git a/bin/docs b/bin/docs index a8731ce27..b76872d3b 100755 --- a/bin/docs +++ b/bin/docs @@ -22,14 +22,13 @@ DOCS = { "builder" => "Builders", "configuration" => "Configuration overview", "env" => "Environment variables", - "healthcheck" => "Healthchecks", "logging" => "Logging", + "proxy" => "Proxy", "registry" => "Docker Registry", "role" => "Roles", "servers" => "Servers", "ssh" => "SSH", - "sshkit" => "SSHKit", - "traefik" => "Traefik" + "sshkit" => "SSHKit" } class DocWriter diff --git a/lib/kamal/cli.rb b/lib/kamal/cli.rb index 6772556e3..dc35c4031 100644 --- a/lib/kamal/cli.rb +++ b/lib/kamal/cli.rb @@ -1,4 +1,5 @@ module Kamal::Cli + class BootError < StandardError; end class HookError < StandardError; end class LockError < StandardError; end end diff --git a/lib/kamal/cli/accessory.rb b/lib/kamal/cli/accessory.rb index 2bf9a7860..bc84fe522 100644 --- a/lib/kamal/cli/accessory.rb +++ b/lib/kamal/cli/accessory.rb @@ -1,16 +1,17 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)" - def boot(name, login: true) + def boot(name, prepare: true) with_lock do if name == "all" KAMAL.accessory_names.each { |accessory_name| boot(accessory_name) } else + prepare(name) if prepare + with_accessory(name) do |accessory, hosts| directories(name) upload(name) on(hosts) do - execute *KAMAL.registry.login if login execute *KAMAL.auditor.record("Booted #{name} accessory"), verbosity: :debug execute *accessory.ensure_env_directory upload! accessory.secrets_io, accessory.secrets_path, mode: "0600" @@ -57,15 +58,10 @@ def reboot(name) if name == "all" KAMAL.accessory_names.each { |accessory_name| reboot(accessory_name) } else - with_accessory(name) do |accessory, hosts| - on(hosts) do - execute *KAMAL.registry.login - end - - stop(name) - remove_container(name) - boot(name, login: false) - end + prepare(name) + stop(name) + remove_container(name) + boot(name, prepare: false) end end end @@ -97,10 +93,8 @@ def stop(name) desc "restart [NAME]", "Restart existing accessory container on host" def restart(name) with_lock do - with_accessory(name) do - stop(name) - start(name) - end + stop(name) + start(name) end end @@ -251,11 +245,20 @@ def accessory_hosts(accessory) end def remove_accessory(name) - with_accessory(name) do - stop(name) - remove_container(name) - remove_image(name) - remove_service_directory(name) + stop(name) + remove_container(name) + remove_image(name) + remove_service_directory(name) + end + + def prepare(name) + with_accessory(name) do |accessory, hosts| + on(hosts) do + execute *KAMAL.registry.login + execute *KAMAL.docker.create_network + rescue SSHKit::Command::Failed => e + raise unless e.message.include?("already exists") + end end end end diff --git a/lib/kamal/cli/app.rb b/lib/kamal/cli/app.rb index ce21b2cf2..2a02ed652 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -4,7 +4,7 @@ def boot with_lock do say "Get most recent version available as an image...", :magenta unless options[:version] using_version(version_or_latest) do |version| - say "Start container with version #{version} using a #{KAMAL.config.readiness_delay}s readiness delay (or reboot if already running)...", :magenta + say "Start container with version #{version} (or reboot if already running)...", :magenta # Assets are prepared in a separate step to ensure they are on all hosts before booting on(KAMAL.hosts) do @@ -38,8 +38,17 @@ def start roles = KAMAL.roles_on(host) roles.each do |role| + app = KAMAL.app(role: role, host: host) execute *KAMAL.auditor.record("Started app version #{KAMAL.config.version}"), verbosity: :debug - execute *KAMAL.app(role: role, host: host).start, raise_on_non_zero_exit: false + execute *app.start, raise_on_non_zero_exit: false + + if role.running_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? + + execute *KAMAL.proxy.deploy(role.container_prefix, target: endpoint) + end end end end @@ -52,8 +61,18 @@ def stop roles = KAMAL.roles_on(host) roles.each do |role| + app = KAMAL.app(role: role, host: host) execute *KAMAL.auditor.record("Stopped app", role: role), verbosity: :debug - execute *KAMAL.app(role: role, host: host).stop, raise_on_non_zero_exit: false + + if role.running_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? + 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 @@ -212,6 +231,7 @@ def remove stop remove_containers remove_images + remove_app_directory end end @@ -253,6 +273,20 @@ def remove_images end end + 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.app_directory} on all servers", role: role), verbosity: :debug + execute *KAMAL.server.remove_app_directory, raise_on_non_zero_exit: false + 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/app/boot.rb b/lib/kamal/cli/app/boot.rb index df3e69254..6939825a2 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 @@ -45,15 +45,22 @@ def old_version_renamed_if_clashing def start_new_version audit "Booted app version #{version}" - - execute *app.tie_cord(role.cord_host_file) if uses_cord? hostname = "#{host.to_s[0...51].gsub(/\.+$/, '')}-#{SecureRandom.hex(6)}" execute *app.ensure_env_directory upload! role.secrets_io(host), role.secrets_path, mode: "0600" - execute *app.run(hostname: hostname) - Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) } + 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 + 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 @@ -61,16 +68,7 @@ def stop_new_version end def stop_old_version(version) - if uses_cord? - 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 diff --git a/lib/kamal/cli/base.rb b/lib/kamal/cli/base.rb index 85815506b..b3af23cce 100644 --- a/lib/kamal/cli/base.rb +++ b/lib/kamal/cli/base.rb @@ -101,7 +101,7 @@ def confirming(question) end def acquire_lock - ensure_run_and_locks_directory + ensure_run_directory raise_if_locked do say "Acquiring the deploy lock...", :magenta @@ -174,14 +174,10 @@ def first_invocation instance_variable_get("@_invocations").first end - def ensure_run_and_locks_directory + def ensure_run_directory on(KAMAL.hosts) do execute(*KAMAL.server.ensure_run_directory) end - - on(KAMAL.primary_host) do - execute(*KAMAL.lock.ensure_locks_directory) - end end end end diff --git a/lib/kamal/cli/healthcheck/poller.rb b/lib/kamal/cli/healthcheck/poller.rb index 249a1f6b5..fad518454 100644 --- a/lib/kamal/cli/healthcheck/poller.rb +++ b/lib/kamal/cli/healthcheck/poller.rb @@ -1,51 +1,30 @@ module Kamal::Cli::Healthcheck::Poller extend self - TRAEFIK_UPDATE_DELAY = 5 - - - def wait_for_healthy(pause_after_ready: false, &block) + def wait_for_healthy(role, &block) attempt = 1 - max_attempts = KAMAL.config.healthcheck.max_attempts + timeout_at = Time.now + KAMAL.config.readiness_timeout + readiness_delay = KAMAL.config.readiness_delay 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 - raise Kamal::Cli::Healthcheck::Error, "container not ready (#{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 + 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 - end - info "Container is healthy!" - end - - def wait_for_unhealthy(pause_after_ready: false, &block) - attempt = 1 - max_attempts = KAMAL.config.healthcheck.max_attempts - - 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})" + 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 @@ -53,7 +32,7 @@ def wait_for_unhealthy(pause_after_ready: false, &block) end end - info "Container is unhealthy!" + info "Container is healthy!" end private diff --git a/lib/kamal/cli/lock.rb b/lib/kamal/cli/lock.rb index 306c8a078..ceab1f27b 100644 --- a/lib/kamal/cli/lock.rb +++ b/lib/kamal/cli/lock.rb @@ -12,7 +12,7 @@ def status option :message, aliases: "-m", type: :string, desc: "A lock message", required: true def acquire message = options[:message] - ensure_run_and_locks_directory + ensure_run_directory raise_if_locked do on(KAMAL.primary_host) do diff --git a/lib/kamal/cli/main.rb b/lib/kamal/cli/main.rb index 3bd6dc24f..46ce1583c 100644 --- a/lib/kamal/cli/main.rb +++ b/lib/kamal/cli/main.rb @@ -35,8 +35,8 @@ def deploy with_lock do run_hook "pre-deploy", secrets: true - say "Ensure Traefik is running...", :magenta - invoke "kamal:cli:traefik:boot", [], invoke_options + 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) @@ -51,7 +51,7 @@ def deploy run_hook "post-deploy", secrets: true, runtime: runtime.round end - desc "redeploy", "Deploy app to servers without bootstrapping servers, starting Traefik, pruning, and registry login" + desc "redeploy", "Deploy app to servers without bootstrapping servers, starting kamal-proxy, pruning, and registry login" option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push" def redeploy runtime = print_runtime do @@ -104,7 +104,7 @@ def rollback(version) desc "details", "Show details about all containers" def details - invoke "kamal:cli:traefik:details" + invoke "kamal:cli:proxy:details" invoke "kamal:cli:app:details" invoke "kamal:cli:accessory:details", [ "all" ] end @@ -176,13 +176,13 @@ def init end end - desc "remove", "Remove Traefik, app, accessories, and registry session from servers" + desc "remove", "Remove kamal-proxy, app, accessories, and registry session from servers" option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question" def remove confirming "This will remove all containers and images. Are you sure?" do with_lock do - invoke "kamal:cli:traefik: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 @@ -206,6 +206,9 @@ def version desc "lock", "Manage the deploy lock" subcommand "lock", Kamal::Cli::Lock + desc "proxy", "Manage kamal-proxy" + subcommand "proxy", Kamal::Cli::Proxy + desc "prune", "Prune old application images and containers" subcommand "prune", Kamal::Cli::Prune @@ -218,9 +221,6 @@ def version desc "server", "Bootstrap servers with curl and Docker" subcommand "server", Kamal::Cli::Server - desc "traefik", "Manage Traefik load balancer" - subcommand "traefik", Kamal::Cli::Traefik - private def container_available?(version) begin diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb new file mode 100644 index 000000000..108d78552 --- /dev/null +++ b/lib/kamal/cli/proxy.rb @@ -0,0 +1,229 @@ +class Kamal::Cli::Proxy < Kamal::Cli::Base + desc "boot", "Boot proxy on servers" + def boot + 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.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 + 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 + 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 |host| + execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug + execute *KAMAL.registry.login + + "Stopping and removing Traefik on #{host}, if running..." + 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.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_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 + end + end + run_hook "post-proxy-reboot", hosts: host_list + end + end + 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.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 ] + host_groups.each do |hosts| + host_list = Array(hosts).join(",") + 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.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.proxy.remove_image + end + + 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 + end + end + + desc "start", "Start existing proxy container on servers" + def start + with_lock do + on(KAMAL.proxy_hosts) do |host| + execute *KAMAL.auditor.record("Started proxy"), verbosity: :debug + execute *KAMAL.proxy.start + end + end + end + + desc "stop", "Stop existing proxy container on servers" + def stop + with_lock do + on(KAMAL.proxy_hosts) do |host| + execute *KAMAL.auditor.record("Stopped proxy"), verbosity: :debug + execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false + end + end + end + + desc "restart", "Restart existing proxy container on servers" + def restart + with_lock do + stop + start + 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" } + 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 + grep = options[:grep] + + 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) + 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" + end + end + 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 + if removal_allowed?(options[:force]) + stop + remove_container + remove_image + remove_host_directory + end + end + end + + desc "remove_container", "Remove proxy container from servers", hide: true + def remove_container + with_lock do + on(KAMAL.proxy_hosts) do + execute *KAMAL.auditor.record("Removed proxy container"), verbosity: :debug + execute *KAMAL.proxy.remove_container + end + end + end + + desc "remove_image", "Remove proxy image from servers", hide: true + def remove_image + with_lock do + on(KAMAL.proxy_hosts) do + execute *KAMAL.auditor.record("Removed proxy image"), verbosity: :debug + execute *KAMAL.proxy.remove_image + end + 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 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/cli/prune.rb b/lib/kamal/cli/prune.rb index 7635e97da..aaf799803 100644 --- a/lib/kamal/cli/prune.rb +++ b/lib/kamal/cli/prune.rb @@ -28,7 +28,6 @@ def containers on(KAMAL.hosts) do execute *KAMAL.auditor.record("Pruned containers"), verbosity: :debug execute *KAMAL.prune.app_containers(retain: retain) - execute *KAMAL.prune.healthcheck_containers end end end diff --git a/lib/kamal/cli/server.rb b/lib/kamal/cli/server.rb index 5b1b0cc7e..452ab089e 100644 --- a/lib/kamal/cli/server.rb +++ b/lib/kamal/cli/server.rb @@ -36,8 +36,6 @@ def bootstrap missing << host end end - - execute(*KAMAL.server.ensure_run_directory) end if missing.any? diff --git a/lib/kamal/cli/templates/deploy.yml b/lib/kamal/cli/templates/deploy.yml index 119961b9d..8ecf5d044 100644 --- a/lib/kamal/cli/templates/deploy.yml +++ b/lib/kamal/cli/templates/deploy.yml @@ -57,17 +57,6 @@ builder: # directories: # - data:/data -# Configure custom arguments for Traefik. Be sure to reboot traefik when you modify it. -# traefik: -# args: -# accesslog: true -# accesslog.format: json - -# Configure a custom healthcheck (default is /up on port 3000) -# healthcheck: -# path: /healthz -# port: 4000 - # Bridge fingerprinted assets, like JS and CSS, between versions to avoid # hitting 404 on in-flight requests. Combines all files from new and old # version inside the asset_path. diff --git a/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample b/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample new file mode 100755 index 000000000..1435a677f --- /dev/null +++ b/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooted kamal-proxy on $KAMAL_HOSTS" diff --git a/lib/kamal/cli/templates/sample_hooks/post-traefik-reboot.sample b/lib/kamal/cli/templates/sample_hooks/post-traefik-reboot.sample deleted file mode 100755 index e3d9e3ccb..000000000 --- a/lib/kamal/cli/templates/sample_hooks/post-traefik-reboot.sample +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -echo "Rebooted Traefik on $KAMAL_HOSTS" diff --git a/lib/kamal/cli/templates/sample_hooks/pre-traefik-reboot.sample b/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample similarity index 100% rename from lib/kamal/cli/templates/sample_hooks/pre-traefik-reboot.sample rename to lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample diff --git a/lib/kamal/cli/traefik.rb b/lib/kamal/cli/traefik.rb deleted file mode 100644 index 41ffbc045..000000000 --- a/lib/kamal/cli/traefik.rb +++ /dev/null @@ -1,124 +0,0 @@ -class Kamal::Cli::Traefik < Kamal::Cli::Base - desc "boot", "Boot Traefik on servers" - def boot - 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 - 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 - 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 - 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 - with_lock do - stop - start - end - end - - desc "details", "Show details about Traefik container from servers" - def details - 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 - 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 - with_lock do - stop - remove_container - remove_image - end - end - - desc "remove_container", "Remove Traefik container from servers", hide: true - def remove_container - 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 - 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 -end diff --git a/lib/kamal/commander.rb b/lib/kamal/commander.rb index 11914a676..c07e89331 100644 --- a/lib/kamal/commander.rb +++ b/lib/kamal/commander.rb @@ -4,7 +4,7 @@ class Kamal::Commander attr_accessor :verbosity, :holding_lock, :connected - delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :traefik_hosts, :accessory_hosts, to: :specifics + delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :proxy_hosts, :accessory_hosts, to: :specifics def initialize self.verbosity = :info @@ -94,10 +94,6 @@ def docker @docker ||= Kamal::Commands::Docker.new(config) end - def healthcheck - @healthcheck ||= Kamal::Commands::Healthcheck.new(config) - end - def hook @hook ||= Kamal::Commands::Hook.new(config) end @@ -106,6 +102,10 @@ def lock @lock ||= Kamal::Commands::Lock.new(config) end + def proxy + @proxy ||= Kamal::Commands::Proxy.new(config) + end + def prune @prune ||= Kamal::Commands::Prune.new(config) end @@ -118,10 +118,6 @@ def server @server ||= Kamal::Commands::Server.new(config) end - def traefik - @traefik ||= Kamal::Commands::Traefik.new(config) - end - def alias(name) config.aliases[name] end diff --git a/lib/kamal/commander/specifics.rb b/lib/kamal/commander/specifics.rb index 127bd40e5..288fd9b55 100644 --- a/lib/kamal/commander/specifics.rb +++ b/lib/kamal/commander/specifics.rb @@ -18,8 +18,8 @@ def roles_on(host) roles.select { |role| role.hosts.include?(host.to_s) } end - def traefik_hosts - config.traefik_hosts & specified_hosts + def proxy_hosts + config.proxy_hosts & specified_hosts end def accessory_hosts diff --git a/lib/kamal/commands/accessory.rb b/lib/kamal/commands/accessory.rb index f3b676d14..787f7d43a 100644 --- a/lib/kamal/commands/accessory.rb +++ b/lib/kamal/commands/accessory.rb @@ -15,6 +15,7 @@ def run "--name", service_name, "--detach", "--restart", "unless-stopped", + "--network", "kamal", *config.logging_args, *publish_args, *env_args, @@ -63,6 +64,7 @@ def execute_in_new_container(*command, interactive: false) docker :run, ("-it" if interactive), "--rm", + "--network", "kamal", *env_args, *volume_args, image, diff --git a/lib/kamal/commands/app.rb b/lib/kamal/commands/app.rb index f1991e481..417ad4637 100644 --- a/lib/kamal/commands/app.rb +++ b/lib/kamal/commands/app.rb @@ -1,10 +1,12 @@ 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 ] attr_reader :role, :host + delegate :container_name, to: :role + def initialize(config, role: nil, host: nil) super(config) @role = role @@ -16,11 +18,11 @@ def run(hostname: nil) "--detach", "--restart unless-stopped", "--name", container_name, + "--network", "kamal", *([ "--hostname", hostname ] if hostname), "-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"", "-e", "KAMAL_VERSION=\"#{config.version}\"", *role.env_args(host), - *role.health_check_args, *role.logging_args, *config.volume_args, *role.asset_volume_args, @@ -74,10 +76,6 @@ def ensure_env_directory end private - def container_name(version = nil) - [ role.container_prefix, version || config.version ].compact.join("-") - end - def latest_image_id docker :image, :ls, *argumentize("--filter", "reference=#{config.latest_image}"), "--format", "'{{.ID}}'" end diff --git a/lib/kamal/commands/app/assets.rb b/lib/kamal/commands/app/assets.rb index 9841f4fbb..c1e65d188 100644 --- a/lib/kamal/commands/app/assets.rb +++ b/lib/kamal/commands/app/assets.rb @@ -3,18 +3,18 @@ def extract_assets asset_container = "#{role.container_prefix}-assets" combine \ - make_directory(role.asset_extracted_path), + make_directory(role.asset_extracted_directory), [ *docker(:stop, "-t 1", asset_container, "2> /dev/null"), "|| true" ], docker(:run, "--name", asset_container, "--detach", "--rm", "--entrypoint", "sleep", config.absolute_image, "1000000"), - docker(:cp, "-L", "#{asset_container}:#{role.asset_path}/.", role.asset_extracted_path), + docker(:cp, "-L", "#{asset_container}:#{role.asset_path}/.", role.asset_extracted_directory), docker(:stop, "-t 1", asset_container), by: "&&" end def sync_asset_volumes(old_version: nil) - new_extracted_path, new_volume_path = role.asset_extracted_path(config.version), role.asset_volume.host_path + new_extracted_path, new_volume_path = role.asset_extracted_directory(config.version), role.asset_volume.host_path if old_version.present? - old_extracted_path, old_volume_path = role.asset_extracted_path(old_version), role.asset_volume(old_version).host_path + old_extracted_path, old_volume_path = role.asset_extracted_directory(old_version), role.asset_volume(old_version).host_path end commands = [ make_directory(new_volume_path), copy_contents(new_extracted_path, new_volume_path) ] @@ -29,8 +29,8 @@ def sync_asset_volumes(old_version: nil) def clean_up_assets chain \ - find_and_remove_older_siblings(role.asset_extracted_path), - find_and_remove_older_siblings(role.asset_volume_path) + find_and_remove_older_siblings(role.asset_extracted_directory), + find_and_remove_older_siblings(role.asset_volume_directory) end private @@ -39,7 +39,7 @@ def find_and_remove_older_siblings(path) :find, Pathname.new(path).dirname.to_s, "-maxdepth 1", - "-name", "'#{role.container_prefix}-*'", + "-name", "'#{role.name}-*'", "!", "-name", Pathname.new(path).basename.to_s, "-exec rm -rf \"{}\" +" ] diff --git a/lib/kamal/commands/app/cord.rb b/lib/kamal/commands/app/cord.rb deleted file mode 100644 index 4912992e1..000000000 --- a/lib/kamal/commands/app/cord.rb +++ /dev/null @@ -1,22 +0,0 @@ -module Kamal::Commands::App::Cord - def cord(version:) - pipe \ - docker(:inspect, "-f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}'", container_name(version)), - [ :awk, "'$2 == \"#{role.cord_volume.container_path}\" {print $1}'" ] - end - - def tie_cord(cord) - create_empty_file(cord) - end - - def cut_cord(cord) - remove_directory(cord) - end - - private - def create_empty_file(file) - chain \ - make_directory_for(file), - [ :touch, file ] - end -end diff --git a/lib/kamal/commands/docker.rb b/lib/kamal/commands/docker.rb index 2966e095d..bc9345dfc 100644 --- a/lib/kamal/commands/docker.rb +++ b/lib/kamal/commands/docker.rb @@ -19,6 +19,10 @@ def superuser? [ '[ "${EUID:-$(id -u)}" -eq 0 ] || command -v sudo >/dev/null || command -v su >/dev/null' ] end + def create_network + docker :network, :create, :kamal + end + private def get_docker shell \ diff --git a/lib/kamal/commands/lock.rb b/lib/kamal/commands/lock.rb index 395b6f3a4..aafaec708 100644 --- a/lib/kamal/commands/lock.rb +++ b/lib/kamal/commands/lock.rb @@ -44,14 +44,10 @@ def stat_lock_dir "/dev/null" end - def locks_dir - File.join(config.run_directory, "locks") - end - def lock_dir - dir_name = [ config.service, config.destination ].compact.join("-") + dir_name = [ "lock", config.service, config.destination ].compact.join("-") - File.join(locks_dir, dir_name) + File.join(config.run_directory, dir_name) end def lock_details_file diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb new file mode 100644 index 000000000..354493db2 --- /dev/null +++ b/lib/kamal/commands/proxy.rb @@ -0,0 +1,88 @@ +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, + "--name", container_name, + "--network", "kamal", + "--detach", + "--restart", "unless-stopped", + *proxy_config.publish_args, + "--volume", "/var/run/docker.sock:/var/run/docker.sock", + *proxy_config.config_volume.docker_args, + *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:) + 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 + + 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"), + ("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 + + def remove_host_directory + remove_directory config.proxy_directory + end + + def cleanup_traefik + chain \ + docker(:container, :stop, "traefik"), + combine( + docker(:container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=Traefik"), + docker(:image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=Traefik") + ) + end +end diff --git a/lib/kamal/commands/prune.rb b/lib/kamal/commands/prune.rb index b820b5af7..30b6eafd3 100644 --- a/lib/kamal/commands/prune.rb +++ b/lib/kamal/commands/prune.rb @@ -20,10 +20,6 @@ def app_containers(retain:) "while read container_id; do docker rm $container_id; done" end - def healthcheck_containers - docker :container, :prune, "--force", *healthcheck_service_filter - end - private def stopped_containers_filters [ "created", "exited", "dead" ].flat_map { |status| [ "--filter", "status=#{status}" ] } @@ -39,8 +35,4 @@ def active_image_list def service_filter [ "--filter", "label=service=#{config.service}" ] end - - def healthcheck_service_filter - [ "--filter", "label=service=#{config.healthcheck_service}" ] - end end diff --git a/lib/kamal/commands/server.rb b/lib/kamal/commands/server.rb index fb781fe8e..305903f6f 100644 --- a/lib/kamal/commands/server.rb +++ b/lib/kamal/commands/server.rb @@ -1,5 +1,15 @@ class Kamal::Commands::Server < Kamal::Commands::Base def ensure_run_directory - [ :mkdir, "-p", config.run_directory ] + make_directory config.run_directory + end + + 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/commands/traefik.rb b/lib/kamal/commands/traefik.rb deleted file mode 100644 index 964ef3eb9..000000000 --- a/lib/kamal/commands/traefik.rb +++ /dev/null @@ -1,77 +0,0 @@ -class Kamal::Commands::Traefik < Kamal::Commands::Base - delegate :argumentize, :optionize, to: Kamal::Utils - delegate :port, :publish?, :labels, :env, :image, :options, :args, :env_args, :secrets_io, :env_directory, :secrets_path, to: :"config.traefik" - - def run - docker :run, "--name traefik", - "--detach", - "--restart", "unless-stopped", - *publish_args, - "--volume", "/var/run/docker.sock:/var/run/docker.sock", - *env_args, - *config.logging_args, - *label_args, - *docker_options_args, - image, - "--providers.docker", - *cmd_option_args - end - - def start - docker :container, :start, "traefik" - end - - def stop - docker :container, :stop, "traefik" - end - - def start_or_run - any start, run - end - - def info - docker :ps, "--filter", "name=^traefik$" - end - - def logs(since: nil, lines: nil, grep: nil, grep_options: nil) - pipe \ - docker(:logs, "traefik", (" --since #{since}" if since), (" --tail #{lines}" if lines), "--timestamps", "2>&1"), - ("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep) - end - - def follow_logs(host:, grep: nil, grep_options: nil) - run_over_ssh pipe( - docker(:logs, "traefik", "--timestamps", "--tail", "10", "--follow", "2>&1"), - (%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep) - ).join(" "), host: host - end - - def remove_container - docker :container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=Traefik" - end - - def remove_image - docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=Traefik" - end - - def ensure_env_directory - make_directory env_directory - end - - private - def publish_args - argumentize "--publish", port if publish? - end - - def label_args - argumentize "--label", labels - end - - def docker_options_args - optionize(options) - end - - def cmd_option_args - optionize args, with: "=" - end -end diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index 2758d15ab..bd33b08c7 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, :logging, :proxy, :servers, :ssh, :sshkit, :registry include Validation @@ -58,9 +58,8 @@ def initialize(raw_config, destination: nil, version: nil, validate: true) @builder = Builder.new(config: self) @env = Env.new(config: @raw_config.env || {}, secrets: secrets) - @healthcheck = Healthcheck.new(healthcheck_config: @raw_config.healthcheck) @logging = Logging.new(logging_config: @raw_config.logging) - @traefik = Traefik.new(config: self) + @proxy = Proxy.new(config: self) @ssh = Ssh.new(config: self) @sshkit = Sshkit.new(config: self) @@ -71,6 +70,7 @@ def initialize(raw_config, destination: nil, version: nil, validate: true) ensure_valid_kamal_version ensure_retain_containers_valid ensure_valid_service_name + ensure_no_traefik_reboot_hooks end @@ -131,16 +131,16 @@ def allow_empty_roles? raw_config.allow_empty_roles end - def traefik_roles - roles.select(&:running_traefik?) + def proxy_roles + roles.select(&:running_proxy?) end - def traefik_role_names - traefik_roles.flat_map(&:name) + def proxy_role_names + proxy_roles.flat_map(&:name) end - def traefik_hosts - traefik_roles.flat_map(&:hosts).uniq + def proxy_hosts + proxy_roles.flat_map(&:hosts).uniq end def repository @@ -185,31 +185,40 @@ def logging_args end - def healthcheck_service - [ "healthcheck", service, destination ].compact.join("-") - end - def readiness_delay raw_config.readiness_delay || 7 end - def run_id - @run_id ||= SecureRandom.hex(16) + def readiness_timeout + raw_config.readiness_timeout || 30 end 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 + 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 app_directory, "env" + end + + def assets_directory + File.join app_directory, "assets" + end + + def hooks_path raw_config.hooks_path || ".kamal/hooks" end @@ -219,10 +228,6 @@ def asset_path end - def env_directory - File.join(run_directory, "env") - end - def env_tags @env_tags ||= if (tags = raw_config.env["tags"]) tags.collect { |name, config| Env::Tag.new(name, config: config, secrets: secrets) } @@ -250,8 +255,7 @@ def to_h sshkit: sshkit.to_h, builder: builder.to_h, accessories: raw_config.accessories, - logging: logging_args, - healthcheck: healthcheck.to_h + logging: logging_args }.compact end @@ -313,6 +317,16 @@ def ensure_retain_containers_valid true end + def ensure_no_traefik_reboot_hooks + hooks = %w[ pre-traefik-reboot post-traefik-reboot ].select { |hook_file| File.exist?(File.join(hooks_path, hook_file)) } + + if hooks.any? + raise Kamal::ConfigurationError, "Found #{hooks.join(", ")}, these should be renamed to (pre|post)-proxy-reboot" + end + + true + end + def role_names raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort diff --git a/lib/kamal/configuration/accessory.rb b/lib/kamal/configuration/accessory.rb index 57489f17e..804a15029 100644 --- a/lib/kamal/configuration/accessory.rb +++ b/lib/kamal/configuration/accessory.rb @@ -63,7 +63,7 @@ def secrets_io end def secrets_path - File.join(config.env_directory, "accessories", "#{service_name}.env") + File.join(config.env_directory, "accessories", "#{name}.env") end def files diff --git a/lib/kamal/configuration/docs/configuration.yml b/lib/kamal/configuration/docs/configuration.yml index f1045dd69..2e7b618d1 100644 --- a/lib/kamal/configuration/docs/configuration.yml +++ b/lib/kamal/configuration/docs/configuration.yml @@ -111,9 +111,15 @@ minimum_version: 1.3.0 # Readiness delay # # Seconds to wait for a container to boot after is running, default 7 -# This only applies to containers that do not specify a healthcheck +# This only applies to containers that do not run a proxy or specify a healthcheck readiness_delay: 4 +# Readiness timeout +# +# How long to wait for a container to become ready, default 30 +# This only applies to containers that do not run a proxy +readiness_timeout: 4 + # Run directory # # Directory to store kamal runtime files in on the host, default `.kamal` @@ -137,10 +143,10 @@ builder: accessories: ... -# Traefik +# Proxy # -# The Traefik proxy is used for zero-downtime deployments, see kamal docs traefik -traefik: +# Configuration for kamal-proxy, see kamal docs proxy +proxy: ... # SSHKit @@ -155,12 +161,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/env.yml b/lib/kamal/configuration/docs/env.yml index 0ca2bfa9e..c2cf0ed0c 100644 --- a/lib/kamal/configuration/docs/env.yml +++ b/lib/kamal/configuration/docs/env.yml @@ -24,14 +24,12 @@ env: # KAMAL_REGISTRY_PASSWORD=pw # DB_PASSWORD=secret123 # ``` -# See https://kamal-deploy.org/docs/commands/envify/ for how to use generated .env files. # # To pass the secrets you should list them under the `secret` key. When you do this the # other variables need to be moved under the `clear` key. # # Unlike clear values, secrets are not passed directly to the container, # but are stored in an env file on the host -# The file is not updated when deploying, only when running `kamal envify` or `kamal env push`. env: clear: DB_USER: app diff --git a/lib/kamal/configuration/docs/healthcheck.yml b/lib/kamal/configuration/docs/healthcheck.yml deleted file mode 100644 index 3c55d5ddf..000000000 --- a/lib/kamal/configuration/docs/healthcheck.yml +++ /dev/null @@ -1,59 +0,0 @@ -# Healthcheck configuration -# -# On roles that are running Traefik, Kamal will supply a default healthcheck to `docker run`. -# For other roles, by default no healthcheck is supplied. -# -# If no healthcheck is supplied and the image does not define one, then we wait for the container -# to reach a running state and then pause for the readiness delay. -# -# The default healthcheck is `curl -f http://localhost:/`, so it assumes that `curl` -# is available within the container. - -# Healthcheck options -# -# These go under the `healthcheck` key in the root or role configuration. -healthcheck: - - # Command - # - # The command to run, defaults to `curl -f http://localhost:/` on roles running Traefik - cmd: "curl -f http://localhost" - - # Interval - # - # The Docker healthcheck interval, defaults to `1s` - interval: 10s - - # Max attempts - # - # The maximum number of times we poll the container to see if it is healthy, defaults to `7` - # Each check is separated by an increasing interval starting with 1 second. - max_attempts: 3 - - # Port - # - # The port to use in the healthcheck, defaults to `3000` - port: "80" - - # Path - # - # The path to use in the healthcheck, defaults to `/up` - path: /health - - # Cords for zero-downtime deployments - # - # The cord file is used for zero-downtime deployments. The healthcheck is augmented with a check - # for the existance of the file. This allows us to delete the file and force the container to - # become unhealthy, causing Traefik to stop routing traffic to it. - # - # Kamal mounts a volume at this location and creates the file before starting the container. - # You can set the value to `false` to disable the cord file, but this loses the zero-downtime - # guarantee. - # - # The default value is `/tmp/kamal-cord` - cord: /cord - - # Log lines - # - # Number of lines to log from the container when the healthcheck fails, defaults to `50` - log_lines: 100 diff --git a/lib/kamal/configuration/docs/proxy.yml b/lib/kamal/configuration/docs/proxy.yml new file mode 100644 index 000000000..e754eb36d --- /dev/null +++ b/lib/kamal/configuration/docs/proxy.yml @@ -0,0 +1,87 @@ +# Proxy +# +# 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 +# run on the same proxy. +proxy: + + # Host + # + # 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. + 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. + 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 + 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 + + # Buffering + # + # 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: + requests: true + responses: true + max_request_body: 40_000_000 + 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) + # + # 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/docs/role.yml b/lib/kamal/configuration/docs/role.yml index 9f6962a58..8ed6e46c7 100644 --- a/lib/kamal/configuration/docs/role.yml +++ b/lib/kamal/configuration/docs/role.yml @@ -26,7 +26,7 @@ servers: # # When there are other options to set, the list of hosts goes under the `hosts` key # - # By default only the primary role uses Traefik, but you can set `traefik` to change + # By default only the primary role uses a proxy, but you can set `proxy` to change # it. # # You can also set a custom cmd to run in the container, and overwrite other settings @@ -35,13 +35,11 @@ servers: hosts: - 172.1.0.3 - 172.1.0.4: experiment1 - traefik: true + proxy: true cmd: "bin/jobs" options: memory: 2g cpus: 4 - healthcheck: - ... logging: ... labels: diff --git a/lib/kamal/configuration/docs/traefik.yml b/lib/kamal/configuration/docs/traefik.yml deleted file mode 100644 index 756afa9e2..000000000 --- a/lib/kamal/configuration/docs/traefik.yml +++ /dev/null @@ -1,62 +0,0 @@ -# Traefik -# -# Traefik is a reverse proxy, used by Kamal for zero-downtime deployments. -# -# We start an instance on the hosts in it's own container. -# -# During a deployment: -# 1. We start a new container which Traefik automatically detects due to the labels we have applied -# 2. Traefik starts routing traffic to the new container -# 3. We force the old container to fail it's healthcheck, causing Traefik to stop routing traffic to it -# 4. We stop the old container - -# Traefik settings -# -# Traekik is configured in the root configuration under `traefik`. -traefik: - - # Image - # - # The Traefik image to use, defaults to `traefik:v2.10` - image: traefik:v2.9 - - # Host port - # - # The host port to publish the Traefik container on, defaults to `80` - host_port: "8080" - - # Disabling publishing - # - # To avoid publishing the Traefik container, set this to `false` - publish: false - - # Labels - # - # Additional labels to apply to the Traefik container - labels: - traefik.http.routers.catchall.entryPoints: http - traefik.http.routers.catchall.rule: PathPrefix(`/`) - traefik.http.routers.catchall.service: unavailable - traefik.http.routers.catchall.priority: "1" - traefik.http.services.unavailable.loadbalancer.server.port: "0" - - # Arguments - # - # Additional arguments to pass to the Traefik container - args: - entryPoints.http.address: ":80" - entryPoints.http.forwardedHeaders.insecure: true - accesslog: true - accesslog.format: json - - # Options - # - # Additional options to pass to `docker run` - options: - cpus: 2 - - # Environment variables - # - # See kamal docs env - env: - ... diff --git a/lib/kamal/configuration/healthcheck.rb b/lib/kamal/configuration/healthcheck.rb deleted file mode 100644 index 888068a44..000000000 --- a/lib/kamal/configuration/healthcheck.rb +++ /dev/null @@ -1,63 +0,0 @@ -class Kamal::Configuration::Healthcheck - include Kamal::Configuration::Validation - - attr_reader :healthcheck_config - - def initialize(healthcheck_config:, context: "healthcheck") - @healthcheck_config = healthcheck_config || {} - validate! @healthcheck_config, context: context - end - - def merge(other) - self.class.new healthcheck_config: healthcheck_config.deep_merge(other.healthcheck_config) - end - - def cmd - healthcheck_config.fetch("cmd", http_health_check) - end - - def port - healthcheck_config.fetch("port", 3000) - end - - def path - healthcheck_config.fetch("path", "/up") - end - - def max_attempts - healthcheck_config.fetch("max_attempts", 7) - end - - def interval - healthcheck_config.fetch("interval", "1s") - end - - def cord - healthcheck_config.fetch("cord", "/tmp/kamal-cord") - end - - def log_lines - healthcheck_config.fetch("log_lines", 50) - end - - def set_port_or_path? - healthcheck_config["port"].present? || healthcheck_config["path"].present? - end - - def to_h - { - "cmd" => cmd, - "interval" => interval, - "max_attempts" => max_attempts, - "port" => port, - "path" => path, - "cord" => cord, - "log_lines" => log_lines - } - end - - private - def http_health_check - "curl -f #{URI.join("http://localhost:#{port}", path)} || exit 1" if path.present? || port.present? - end -end diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb new file mode 100644 index 000000000..2671b4c84 --- /dev/null +++ b/lib/kamal/configuration/proxy.rb @@ -0,0 +1,71 @@ +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:#{MINIMUM_VERSION}" + DEFAULT_LOG_REQUEST_HEADERS = [ "Cache-Control", "Last-Modified", "User-Agent" ] + + 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 + + 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 + + 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("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), + "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") + }.compact + end + + def deploy_command_args + optionize 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" + end + + private + attr_reader :config, :proxy_config +end diff --git a/lib/kamal/configuration/role.rb b/lib/kamal/configuration/role.rb index ef651898f..3f1e6d74b 100644 --- a/lib/kamal/configuration/role.rb +++ b/lib/kamal/configuration/role.rb @@ -1,10 +1,9 @@ class Kamal::Configuration::Role include Kamal::Configuration::Validation - CORD_FILE = "cord" delegate :argumentize, :optionize, to: Kamal::Utils - attr_reader :name, :config, :specialized_env, :specialized_logging, :specialized_healthcheck + attr_reader :name, :config, :specialized_env, :specialized_logging alias to_s name @@ -24,10 +23,6 @@ def initialize(name, config:) @specialized_logging = Kamal::Configuration::Logging.new \ logging_config: specializations.fetch("logging", {}), context: "servers/#{name}/logging" - - @specialized_healthcheck = Kamal::Configuration::Healthcheck.new \ - healthcheck_config: specializations.fetch("healthcheck", {}), - context: "servers/#{name}/healthcheck" end def primary_host @@ -55,7 +50,7 @@ def option_args end def labels - default_labels.merge(traefik_labels).merge(custom_labels) + default_labels.merge(custom_labels) end def label_args @@ -89,7 +84,7 @@ def secrets_io(host) end def secrets_path - File.join(config.env_directory, "roles", "#{container_prefix}.env") + File.join(config.env_directory, "roles", "#{name}.env") end def asset_volume_args @@ -97,38 +92,11 @@ def asset_volume_args end - def health_check_args(cord: true) - if running_traefik? || healthcheck.set_port_or_path? - if cord && uses_cord? - optionize({ "health-cmd" => health_check_cmd_with_cord, "health-interval" => healthcheck.interval }) - .concat(cord_volume.docker_args) - else - optionize({ "health-cmd" => healthcheck.cmd, "health-interval" => healthcheck.interval }) - end - else - [] - end - end - - def healthcheck - @healthcheck ||= - if running_traefik? - config.healthcheck.merge(specialized_healthcheck) - else - specialized_healthcheck - end - end - - def health_check_cmd_with_cord - "(#{healthcheck.cmd}) && (stat #{cord_container_file} > /dev/null || exit 1)" - end - - - def running_traefik? - if specializations["traefik"].nil? + def running_proxy? + if specializations["proxy"].nil? primary? else - specializations["traefik"] + specializations["proxy"] end end @@ -137,35 +105,6 @@ def primary? end - def uses_cord? - running_traefik? && cord_volume && healthcheck.cmd.present? - end - - def cord_host_directory - File.join config.run_directory_as_docker_volume, "cords", [ container_prefix, config.run_id ].join("-") - end - - def cord_volume - if (cord = healthcheck.cord) - @cord_volume ||= Kamal::Configuration::Volume.new \ - host_path: File.join(config.run_directory, "cords", [ container_prefix, config.run_id ].join("-")), - container_path: cord - end - end - - def cord_host_file - File.join cord_volume.host_path, CORD_FILE - end - - def cord_container_directory - health_check_options.fetch("cord", nil) - end - - def cord_container_file - File.join cord_volume.container_path, CORD_FILE - end - - def container_name(version = nil) [ container_prefix, version || config.version ].compact.join("-") end @@ -180,22 +119,22 @@ def asset_path end def assets? - asset_path.present? && running_traefik? + 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 @@ -233,27 +172,6 @@ def specializations end end - def traefik_labels - if running_traefik? - { - # Setting a service property ensures that the generated service name will be consistent between versions - "traefik.http.services.#{traefik_service}.loadbalancer.server.scheme" => "http", - - "traefik.http.routers.#{traefik_service}.rule" => "PathPrefix(`/`)", - "traefik.http.routers.#{traefik_service}.priority" => "2", - "traefik.http.middlewares.#{traefik_service}-retry.retry.attempts" => "5", - "traefik.http.middlewares.#{traefik_service}-retry.retry.initialinterval" => "500ms", - "traefik.http.routers.#{traefik_service}.middlewares" => "#{traefik_service}-retry@docker" - } - else - {} - end - end - - def traefik_service - container_prefix - end - def custom_labels Hash.new.tap do |labels| labels.merge!(config.labels) if config.labels.present? diff --git a/lib/kamal/configuration/traefik.rb b/lib/kamal/configuration/traefik.rb deleted file mode 100644 index 45d8bac54..000000000 --- a/lib/kamal/configuration/traefik.rb +++ /dev/null @@ -1,78 +0,0 @@ -class Kamal::Configuration::Traefik - delegate :argumentize, to: Kamal::Utils - - DEFAULT_IMAGE = "traefik:v2.10" - CONTAINER_PORT = 80 - DEFAULT_ARGS = { - "log.level" => "DEBUG" - } - DEFAULT_LABELS = { - # These ensure we serve a 502 rather than a 404 if no containers are available - "traefik.http.routers.catchall.entryPoints" => "http", - "traefik.http.routers.catchall.rule" => "PathPrefix(`/`)", - "traefik.http.routers.catchall.service" => "unavailable", - "traefik.http.routers.catchall.priority" => 1, - "traefik.http.services.unavailable.loadbalancer.server.port" => "0" - } - - include Kamal::Configuration::Validation - - attr_reader :config, :traefik_config - - def initialize(config:) - @config = config - @traefik_config = config.raw_config.traefik || {} - validate! traefik_config - end - - def publish? - traefik_config["publish"] != false - end - - def labels - DEFAULT_LABELS.merge(traefik_config["labels"] || {}) - end - - def env - Kamal::Configuration::Env.new \ - config: traefik_config.fetch("env", {}), - secrets: config.secrets, - context: "traefik/env" - end - - def host_port - traefik_config.fetch("host_port", CONTAINER_PORT) - end - - def options - traefik_config.fetch("options", {}) - end - - def port - "#{host_port}:#{CONTAINER_PORT}" - end - - def args - DEFAULT_ARGS.merge(traefik_config.fetch("args", {})) - end - - def image - traefik_config.fetch("image", DEFAULT_IMAGE) - end - - def env_args - [ *env.clear_args, *argumentize("--env-file", secrets_path) ] - end - - def env_directory - File.join(config.env_directory, "traefik") - end - - def secrets_io - env.secrets_io - end - - def secrets_path - File.join(config.env_directory, "traefik", "traefik.env") - end -end diff --git a/lib/kamal/configuration/validator/proxy.rb b/lib/kamal/configuration/validator/proxy.rb new file mode 100644 index 000000000..a4ee19bf5 --- /dev/null +++ b/lib/kamal/configuration/validator/proxy.rb @@ -0,0 +1,9 @@ +class Kamal::Configuration::Validator::Proxy < Kamal::Configuration::Validator + def validate! + super + + if config["host"].blank? && config["ssl"] + error "Must set a host to enable automatic SSL" + end + end +end diff --git a/lib/kamal/utils.rb b/lib/kamal/utils.rb index 266d6a96b..ab8dd50e7 100644 --- a/lib/kamal/utils.rb +++ b/lib/kamal/utils.rb @@ -101,4 +101,8 @@ def docker_arch arch end end + + def older_version?(version, other_version) + Gem::Version.new(version.delete_prefix("v")) < Gem::Version.new(other_version.delete_prefix("v")) + end end diff --git a/test/cli/accessory_test.rb b/test/cli/accessory_test.rb index 0e3abc467..5bb8762a8 100644 --- a/test/cli/accessory_test.rb +++ b/test/cli/accessory_test.rb @@ -15,7 +15,7 @@ class CliAccessoryTest < CliTestCase run_command("boot", "mysql").tap do |output| assert_match /docker login.*on 1.1.1.3/, output - assert_match "docker run --name app-mysql --detach --restart unless-stopped --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 @@ -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/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 @@ -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/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 @@ -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/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 85a966fd6..f3bd9fe5f 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 @@ -45,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 @@ -70,25 +62,21 @@ 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 "/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 @@ -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 @@ -140,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 @@ -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\"", "--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| @@ -160,17 +138,59 @@ 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 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 + 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 @@ -243,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/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 @@ -262,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/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 @@ -342,7 +362,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 +372,19 @@ 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 + + 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 --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 end @@ -369,13 +401,5 @@ def stub_running Object.any_instance.stubs(:sleep) SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version - - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'") - .returns("running") # health check - - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'") - .returns("unhealthy") # health check end end diff --git a/test/cli/cli_test_case.rb b/test/cli/cli_test_case.rb index 3f3e9294e..5a2bb76f7 100644 --- a/test/cli/cli_test_case.rb +++ b/test/cli/cli_test_case.rb @@ -29,13 +29,13 @@ def fail_hook(hook) def stub_setup SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |*args| args == [ :mkdir, "-p", ".kamal" ] } + .with { |*args| args == [ :mkdir, "-p", ".kamal/apps/app" ] } SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |arg1, arg2, arg3| arg1 == :mkdir && arg2 == "-p" && arg3 == ".kamal/locks" } + .with { |arg1, arg2, arg3| arg1 == :mkdir && arg2 == "-p" && arg3 == ".kamal/lock-app" } SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |arg1, arg2| arg1 == :mkdir && arg2 == ".kamal/locks/app" } + .with { |arg1, arg2| arg1 == :mkdir && arg2 == ".kamal/lock-app" } SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |arg1, arg2| arg1 == :rm && arg2 == ".kamal/locks/app/details" } + .with { |arg1, arg2| arg1 == :rm && arg2 == ".kamal/lock-app/details" } SSHKit::Backend::Abstract.any_instance.stubs(:execute) .with(:docker, :buildx, :inspect, "kamal-local-docker-container") end diff --git a/test/cli/lock_test.rb b/test/cli/lock_test.rb index f6874d49e..66482153e 100644 --- a/test/cli/lock_test.rb +++ b/test/cli/lock_test.rb @@ -3,7 +3,7 @@ class CliLockTest < CliTestCase test "status" do run_command("status").tap do |output| - assert_match "Running /usr/bin/env stat .kamal/locks/app > /dev/null && cat .kamal/locks/app/details | base64 -d on 1.1.1.1", output + assert_match "Running /usr/bin/env stat .kamal/lock-app > /dev/null && cat .kamal/lock-app/details | base64 -d on 1.1.1.1", output end end diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index 2115f4180..607fee21b 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 @@ -97,17 +97,14 @@ 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" ] } - - 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) @@ -134,13 +131,10 @@ class CliMainTest < CliTestCase Dir.stubs(:chdir) SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |*args| args == [ :mkdir, "-p", ".kamal" ] } - - SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |*args| args == [ :mkdir, "-p", ".kamal/locks" ] } + .with { |*args| args == [ :mkdir, "-p", ".kamal/apps/app" ] } 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) @@ -180,7 +174,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 +184,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 +252,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 +273,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 +290,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,13 +404,14 @@ 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 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 @@ -480,7 +451,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 new file mode 100644 index 000000000..2a0834fc9 --- /dev/null +++ b/test/cli/proxy_test.rb @@ -0,0 +1,189 @@ +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 --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") + .returns("abcdefabcdef") + .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 "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 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 + 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") + .returns("abcdefabcdef") + .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, "proxy", " --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 + run_command("remove").tap do |output| + assert_match "/usr/bin/env ls .kamal/apps | wc -l", output + assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy", output + assert_match "docker image prune --all --force --filter label=org.opencontainers.image.title=kamal-proxy", output + assert_match "/usr/bin/env rm -r .kamal/proxy", output + end + end + + test "remove with other apps" do + Thread.report_on_exception = false + + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info).with(:ls, ".kamal/apps", "|", :wc, "-l").returns("1\n").twice + + run_command("remove").tap do |output| + assert_match "Not removing the proxy, as other apps are installed, ignore this check with kamal proxy remove --force", output + end + ensure + Thread.report_on_exception = true + end + + test "force remove with other apps" do + Thread.report_on_exception = false + + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info).with(:ls, ".kamal/apps", "|", :wc, "-l").returns("1\n").twice + + run_command("remove").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 + 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 "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" ]) } + end +end diff --git a/test/cli/prune_test.rb b/test/cli/prune_test.rb index 33723f13d..bb4ced51a 100644 --- a/test/cli/prune_test.rb +++ b/test/cli/prune_test.rb @@ -18,12 +18,10 @@ class CliPruneTest < CliTestCase test "containers" do run_command("containers").tap do |output| assert_match /docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +6 | while read container_id; do docker rm $container_id; done on 1.1.1.\d/, output - assert_match /docker container prune --force --filter label=service=healthcheck-app on 1.1.1.\d/, output end run_command("containers", "--retain", "10").tap do |output| assert_match /docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +11 | while read container_id; do docker rm $container_id; done on 1.1.1.\d/, output - assert_match /docker container prune --force --filter label=service=healthcheck-app on 1.1.1.\d/, output end assert_raises(RuntimeError, "retain must be at least 1") do diff --git a/test/cli/traefik_test.rb b/test/cli/traefik_test.rb deleted file mode 100644 index 291711509..000000000 --- a/test/cli/traefik_test.rb +++ /dev/null @@ -1,110 +0,0 @@ -require_relative "cli_test_case" - -class CliTraefikTest < CliTestCase - test "boot" do - run_command("boot").tap do |output| - assert_match "docker login", output - assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Configuration::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output - end - end - - test "reboot" do - Kamal::Commands::Registry.any_instance.expects(:login).twice - - run_command("reboot", "-y").tap do |output| - assert_match "docker container stop traefik", output - assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik", output - assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Configuration::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output - end - end - - test "reboot --rolling" do - Object.any_instance.stubs(:sleep) - - run_command("reboot", "--rolling", "-y").tap do |output| - assert_match "Running docker container prune --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output - end - end - - test "start" do - run_command("start").tap do |output| - assert_match "docker container start traefik", output - end - end - - test "stop" do - run_command("stop").tap do |output| - assert_match "docker container stop traefik", output - end - end - - test "restart" do - Kamal::Cli::Traefik.any_instance.expects(:stop) - Kamal::Cli::Traefik.any_instance.expects(:start) - - run_command("restart") - end - - test "details" do - run_command("details").tap do |output| - assert_match "docker ps --filter name=^traefik$", output - end - end - - test "logs" do - SSHKit::Backend::Abstract.any_instance.stubs(:capture) - .with(:docker, :logs, "traefik", " --tail 100", "--timestamps", "2>&1") - .returns("Log entry") - - run_command("logs").tap do |output| - assert_match "Traefik Host: 1.1.1.1", output - assert_match "Log entry", output - end - end - - test "logs with follow" do - SSHKit::Backend::Abstract.any_instance.stubs(:exec) - .with("ssh -t root@1.1.1.1 -p 22 'docker logs traefik --timestamps --tail 10 --follow 2>&1'") - - assert_match "docker logs traefik --timestamps --tail 10 --follow", run_command("logs", "--follow") - end - - test "logs with follow and grep" do - SSHKit::Backend::Abstract.any_instance.stubs(:exec) - .with("ssh -t root@1.1.1.1 -p 22 'docker logs traefik --timestamps --tail 10 --follow 2>&1 | grep \"hey\"'") - - assert_match "docker logs traefik --timestamps --tail 10 --follow 2>&1 | grep \"hey\"", run_command("logs", "--follow", "--grep", "hey") - end - - test "logs with follow, grep, and grep options" do - SSHKit::Backend::Abstract.any_instance.stubs(:exec) - .with("ssh -t root@1.1.1.1 -p 22 'docker logs traefik --timestamps --tail 10 --follow 2>&1 | grep \"hey\" -C 2'") - - assert_match "docker logs traefik --timestamps --tail 10 --follow 2>&1 | grep \"hey\" -C 2", run_command("logs", "--follow", "--grep", "hey", "--grep-options", "-C 2") - end - - test "remove" do - Kamal::Cli::Traefik.any_instance.expects(:stop) - Kamal::Cli::Traefik.any_instance.expects(:remove_container) - Kamal::Cli::Traefik.any_instance.expects(:remove_image) - - run_command("remove") - end - - test "remove_container" do - run_command("remove_container").tap do |output| - assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik", output - end - end - - test "remove_image" do - run_command("remove_image").tap do |output| - assert_match "docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik", output - end - end - - private - def run_command(*command) - stdouted { Kamal::Cli::Traefik.start([ *command, "-c", "test/fixtures/deploy_with_accessories.yml" ]) } - end -end diff --git a/test/commander_test.rb b/test/commander_test.rb index c2b78b213..54031e803 100644 --- a/test/commander_test.rb +++ b/test/commander_test.rb @@ -136,18 +136,18 @@ class CommanderTest < ActiveSupport::TestCase assert_equal [ "1.1.1.3", "1.1.1.4", "1.1.1.1", "1.1.1.2" ], @kamal.hosts end - test "traefik hosts should observe filtered roles" do - configure_with(:deploy_with_multiple_traefik_roles) + test "proxy hosts should observe filtered roles" do + configure_with(:deploy_with_multiple_proxy_roles) @kamal.specific_roles = [ "web_tokyo" ] - assert_equal [ "1.1.1.3", "1.1.1.4" ], @kamal.traefik_hosts + assert_equal [ "1.1.1.3", "1.1.1.4" ], @kamal.proxy_hosts end - test "traefik hosts should observe filtered hosts" do - configure_with(:deploy_with_multiple_traefik_roles) + test "proxy hosts should observe filtered hosts" do + configure_with(:deploy_with_multiple_proxy_roles) @kamal.specific_hosts = [ "1.1.1.2" ] - assert_equal [ "1.1.1.2" ], @kamal.traefik_hosts + assert_equal [ "1.1.1.2" ], @kamal.proxy_hosts end private diff --git a/test/commands/accessory_test.rb b/test/commands/accessory_test.rb index 23d304dac..19fe745d8 100644 --- a/test/commands/accessory_test.rb +++ b/test/commands/accessory_test.rb @@ -51,15 +51,15 @@ class CommandsAccessoryTest < ActiveSupport::TestCase test "run" do assert_equal \ - "docker run --name app-mysql --detach --restart unless-stopped --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 --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 --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 --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 --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 --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 e385764e0..440626243 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -3,7 +3,6 @@ class CommandsAppTest < ActiveSupport::TestCase setup do setup_test_secrets("secrets" => "RAILS_MASTER_KEY=456") - Kamal::Configuration.any_instance.stubs(:run_id).returns("12345678901234567890123456789012") @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], env: { "secret" => [ "RAILS_MASTER_KEY" ] }, builder: { "arch" => "amd64" } } end @@ -14,13 +13,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/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 --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/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 @@ -28,38 +27,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/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 -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 @@ -67,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 -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/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 @@ -76,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 -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/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 @@ -85,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 -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/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 @@ -204,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 @@ -219,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 @@ -243,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 @@ -251,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 @@ -412,48 +387,34 @@ 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", "&&", + :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/hook_test.rb b/test/commands/hook_test.rb index f6234d6a1..dc2afc5ac 100644 --- a/test/commands/hook_test.rb +++ b/test/commands/hook_test.rb @@ -8,7 +8,7 @@ class CommandsHookTest < ActiveSupport::TestCase @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], - builder: { "arch" => "amd64" }, traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } + builder: { "arch" => "amd64" } } @performer = Kamal::Git.email.presence || `whoami`.chomp diff --git a/test/commands/lock_test.rb b/test/commands/lock_test.rb index 028719220..0ad0ba46f 100644 --- a/test/commands/lock_test.rb +++ b/test/commands/lock_test.rb @@ -4,25 +4,25 @@ 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 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/proxy_test.rb b/test/commands/proxy_test.rb new file mode 100644 index 000000000..349e1a1be --- /dev/null +++ b/test/commands/proxy_test.rb @@ -0,0 +1,132 @@ +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" ], builder: { "arch" => "amd64" } + } + + ENV["EXAMPLE_API_KEY"] = "456" + end + + teardown do + ENV.delete("EXAMPLE_API_KEY") + end + + 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}", + 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}", + new_command.run.join(" ") + end + + test "run without configuration" do + @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}", + 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 --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 + + 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\" --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", + new_command.version.join(" ") + end + + private + def new_command + Kamal::Commands::Proxy.new(Kamal::Configuration.new(@config, version: "123")) + end +end diff --git a/test/commands/prune_test.rb b/test/commands/prune_test.rb index 430a13db6..50e852bcb 100644 --- a/test/commands/prune_test.rb +++ b/test/commands/prune_test.rb @@ -4,7 +4,7 @@ class CommandsPruneTest < ActiveSupport::TestCase setup do @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], - builder: { "arch" => "amd64" }, traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } + builder: { "arch" => "amd64" } } end @@ -30,12 +30,6 @@ class CommandsPruneTest < ActiveSupport::TestCase new_command.app_containers(retain: 3).join(" ") end - test "healthcheck containers" do - assert_equal \ - "docker container prune --force --filter label=service=healthcheck-app", - new_command.healthcheck_containers.join(" ") - end - private def new_command Kamal::Commands::Prune.new(Kamal::Configuration.new(@config, version: "123")) diff --git a/test/commands/server_test.rb b/test/commands/server_test.rb index 8c465fd9d..648821b4a 100644 --- a/test/commands/server_test.rb +++ b/test/commands/server_test.rb @@ -4,7 +4,7 @@ class CommandsServerTest < ActiveSupport::TestCase setup do @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], - builder: { "arch" => "amd64" }, traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } + builder: { "arch" => "amd64" } } end @@ -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/commands/traefik_test.rb b/test/commands/traefik_test.rb deleted file mode 100644 index b13e37005..000000000 --- a/test/commands/traefik_test.rb +++ /dev/null @@ -1,195 +0,0 @@ -require "test_helper" - -class CommandsTraefikTest < ActiveSupport::TestCase - setup do - @image = "traefik:test" - - @config = { - service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], builder: { "arch" => "amd64" }, - traefik: { "image" => @image, "args" => { "accesslog.format" => "json", "api.insecure" => true, "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } - } - - setup_test_secrets("secrets" => "EXAMPLE_API_KEY=456") - end - - teardown do - teardown_test_secrets - end - - test "run" do - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - - @config[:traefik]["host_port"] = "8080" - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 8080:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - - @config[:traefik]["publish"] = false - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - end - - test "run with ports configured" do - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - - @config[:traefik]["options"] = { "publish" => %w[9000:9000 9001:9001] } - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --publish \"9000:9000\" --publish \"9001:9001\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - end - - test "run with volumes configured" do - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - - @config[:traefik]["options"] = { "volume" => %w[./letsencrypt/acme.json:/letsencrypt/acme.json] } - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - end - - test "run with several options configured" do - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - - @config[:traefik]["options"] = { "volume" => %w[./letsencrypt/acme.json:/letsencrypt/acme.json], "publish" => %w[8080:8080], "memory" => "512m" } - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" --publish \"8080:8080\" --memory \"512m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - end - - test "run with labels configured" do - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - - @config[:traefik]["labels"] = { "traefik.http.routers.dashboard.service" => "api@internal", "traefik.http.routers.dashboard.middlewares" => "auth" } - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --label traefik.http.routers.dashboard.service=\"api@internal\" --label traefik.http.routers.dashboard.middlewares=\"auth\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - end - - test "run with env configured" do - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - - @config[:traefik]["env"] = { "EXAMPLE_API_KEY" => "456" } - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env EXAMPLE_API_KEY=\"456\" --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - end - - test "run without configuration" do - @config.delete(:traefik) - - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Configuration::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", - new_command.run.join(" ") - end - - test "run with logging config" do - @config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } } - - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - end - - test "run with default args overriden" do - @config[:traefik]["args"]["log.level"] = "ERROR" - - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"ERROR\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - end - - test "run with args array" do - @config[:traefik]["args"] = { "entrypoints.web.forwardedheaders.trustedips" => %w[ 127.0.0.1 127.0.0.2 ] } - assert_equal "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" traefik:test --providers.docker --log.level=\"DEBUG\" --entrypoints.web.forwardedheaders.trustedips=\"127.0.0.1\" --entrypoints.web.forwardedheaders.trustedips=\"127.0.0.2\"", new_command.run.join(" ") - end - - test "traefik start" do - assert_equal \ - "docker container start traefik", - new_command.start.join(" ") - end - - test "traefik stop" do - assert_equal \ - "docker container stop traefik", - new_command.stop.join(" ") - end - - test "traefik info" do - assert_equal \ - "docker ps --filter name=^traefik$", - new_command.info.join(" ") - end - - test "traefik logs" do - assert_equal \ - "docker logs traefik --timestamps 2>&1", - new_command.logs.join(" ") - end - - test "traefik logs since 2h" do - assert_equal \ - "docker logs traefik --since 2h --timestamps 2>&1", - new_command.logs(since: "2h").join(" ") - end - - test "traefik logs last 10 lines" do - assert_equal \ - "docker logs traefik --tail 10 --timestamps 2>&1", - new_command.logs(lines: 10).join(" ") - end - - test "traefik logs with grep hello!" do - assert_equal \ - "docker logs traefik --timestamps 2>&1 | grep 'hello!'", - new_command.logs(grep: "hello!").join(" ") - end - - test "traefik logs with grep hello! and grep options" do - assert_equal \ - "docker logs traefik --timestamps 2>&1 | grep 'hello!' -C 2", - new_command.logs(grep: "hello!", grep_options: "-C 2").join(" ") - end - - test "traefik remove container" do - assert_equal \ - "docker container prune --force --filter label=org.opencontainers.image.title=Traefik", - new_command.remove_container.join(" ") - end - - test "traefik remove image" do - assert_equal \ - "docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik", - new_command.remove_image.join(" ") - end - - test "traefik follow logs" do - assert_equal \ - "ssh -t root@1.1.1.1 -p 22 'docker logs traefik --timestamps --tail 10 --follow 2>&1'", - new_command.follow_logs(host: @config[:servers].first) - end - - test "traefik follow logs with grep hello!" do - assert_equal \ - "ssh -t root@1.1.1.1 -p 22 'docker logs traefik --timestamps --tail 10 --follow 2>&1 | grep \"hello!\"'", - new_command.follow_logs(host: @config[:servers].first, grep: "hello!") - end - - private - def new_command - Kamal::Commands::Traefik.new(Kamal::Configuration.new(@config, version: "123")) - end -end diff --git a/test/configuration/accessory_test.rb b/test/configuration/accessory_test.rb index acfe991fc..2615dab60 100644 --- a/test/configuration/accessory_test.rb +++ b/test/configuration/accessory_test.rb @@ -119,9 +119,9 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase with_test_secrets("secrets" => "MYSQL_ROOT_PASSWORD=secret123") do config = Kamal::Configuration.new(@deploy) - assert_equal [ "--env", "MYSQL_ROOT_HOST=\"%\"", "--env-file", ".kamal/env/accessories/app-mysql.env" ], config.accessory(:mysql).env_args.map(&:to_s) + assert_equal [ "--env", "MYSQL_ROOT_HOST=\"%\"", "--env-file", ".kamal/apps/app/env/accessories/mysql.env" ], config.accessory(:mysql).env_args.map(&:to_s) assert_equal "MYSQL_ROOT_PASSWORD=secret123\n", config.accessory(:mysql).secrets_io.string - assert_equal [ "--env", "SOMETHING=\"else\"", "--env-file", ".kamal/env/accessories/app-redis.env" ], @config.accessory(:redis).env_args + assert_equal [ "--env", "SOMETHING=\"else\"", "--env-file", ".kamal/apps/app/env/accessories/redis.env" ], @config.accessory(:redis).env_args assert_equal "\n", config.accessory(:redis).secrets_io.string end end diff --git a/test/configuration/proxy_test.rb b/test/configuration/proxy_test.rb new file mode 100644 index 000000000..3aa3f85e3 --- /dev/null +++ b/test/configuration/proxy_test.rb @@ -0,0 +1,25 @@ +require "test_helper" + +class ConfigurationEnvTest < ActiveSupport::TestCase + setup do + @deploy = { + service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, + builder: { "arch" => "amd64" }, servers: [ "1.1.1.1" ] + } + end + + test "ssl with host" do + @deploy[:proxy] = { "ssl" => true, "host" => "example.com" } + assert_equal true, config.proxy.ssl? + end + + test "ssl with no host" do + @deploy[:proxy] = { "ssl" => true } + assert_raises(Kamal::ConfigurationError) { config.proxy.ssl? } + end + + private + def config + Kamal::Configuration.new(@deploy) + end +end diff --git a/test/configuration/role_test.rb b/test/configuration/role_test.rb index c0b643bfa..577190775 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,24 +53,19 @@ 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 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 \ @@ -89,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 \ @@ -119,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 \ @@ -141,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 \ @@ -162,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 \ @@ -189,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 \ @@ -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 @@ -232,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? @@ -242,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? @@ -253,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 diff --git a/test/configuration/validation_test.rb b/test/configuration/validation_test.rb index ac409e845..d8ac3e3bf 100644 --- a/test/configuration/validation_test.rb +++ b/test/configuration/validation_test.rb @@ -22,7 +22,7 @@ class ConfigurationValidationTest < ActiveSupport::TestCase assert_error "servers: should be an array or a hash", servers: "foo" - [ :labels, :registry, :accessories, :env, :ssh, :sshkit, :builder, :traefik, :boot, :healthcheck, :logging ].each do |key| + [ :labels, :registry, :accessories, :env, :ssh, :sshkit, :builder, :proxy, :boot, :logging ].each do |key| assert_error "#{key}: should be a hash", **{ key =>[] } end end diff --git a/test/configuration_test.rb b/test/configuration_test.rb index aa78dda98..81dbb5acb 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 @@ -294,22 +289,6 @@ 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 - SecureRandom.expects(:hex).with(16).returns("09876543211234567890098765432112") - assert_equal "09876543211234567890098765432112", @config.run_id end test "asset path" do @@ -328,7 +307,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 @@ -350,7 +329,21 @@ 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 + + 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 diff --git a/test/fixtures/deploy_primary_web_role_override.yml b/test/fixtures/deploy_primary_web_role_override.yml index aa52f5edd..6bc2f837d 100644 --- a/test/fixtures/deploy_primary_web_role_override.yml +++ b/test/fixtures/deploy_primary_web_role_override.yml @@ -2,12 +2,12 @@ service: app image: dhh/app servers: web_chicago: - traefik: true + proxy: true hosts: - 1.1.1.1 - 1.1.1.2 web_tokyo: - traefik: true + proxy: true hosts: - 1.1.1.3 - 1.1.1.4 diff --git a/test/fixtures/deploy_with_extensions.yml b/test/fixtures/deploy_with_extensions.yml index 4a5f934a5..c45104de3 100644 --- a/test/fixtures/deploy_with_extensions.yml +++ b/test/fixtures/deploy_with_extensions.yml @@ -1,6 +1,6 @@ x-web: &web - traefik: true + proxy: true service: app image: dhh/app diff --git a/test/fixtures/deploy_with_multiple_traefik_roles.yml b/test/fixtures/deploy_with_multiple_proxy_roles.yml similarity index 93% rename from test/fixtures/deploy_with_multiple_traefik_roles.yml rename to test/fixtures/deploy_with_multiple_proxy_roles.yml index a12705831..6b785a6a4 100644 --- a/test/fixtures/deploy_with_multiple_traefik_roles.yml +++ b/test/fixtures/deploy_with_multiple_proxy_roles.yml @@ -8,14 +8,14 @@ servers: - 1.1.1.2 env: ROLE: "web" - traefik: true + proxy: true web_tokyo: hosts: - 1.1.1.3 - 1.1.1.4 env: ROLE: "web" - traefik: true + proxy: true workers: cmd: bin/jobs hosts: diff --git a/test/fixtures/deploy_with_proxy.yml b/test/fixtures/deploy_with_proxy.yml new file mode 100644 index 000000000..e91e9657a --- /dev/null +++ b/test/fixtures/deploy_with_proxy.yml @@ -0,0 +1,41 @@ +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 +builder: + arch: amd64 + +proxy: + 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/fixtures/deploy_with_roles.yml b/test/fixtures/deploy_with_roles.yml index 1eb8cc5c0..d831a5b47 100644 --- a/test/fixtures/deploy_with_roles.yml +++ b/test/fixtures/deploy_with_roles.yml @@ -16,3 +16,4 @@ registry: password: pw builder: arch: amd64 +readiness_timeout: 1 diff --git a/test/fixtures/deploy_workers_only.yml b/test/fixtures/deploy_workers_only.yml index d83adeeb7..6f6568c3a 100644 --- a/test/fixtures/deploy_workers_only.yml +++ b/test/fixtures/deploy_workers_only.yml @@ -2,7 +2,7 @@ service: app image: dhh/app servers: workers: - traefik: false + proxy: false hosts: - 1.1.1.1 - 1.1.1.2 diff --git a/test/integration/app_test.rb b/test/integration/app_test.rb index b7dcdc343..40a896b1e 100644 --- a/test/integration/app_test.rb +++ b/test/integration/app_test.rb @@ -20,34 +20,35 @@ 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 assert_app_is_down + assert_app_directory_removed end end diff --git a/test/integration/broken_deploy_test.rb b/test/integration/broken_deploy_test.rb index 77f0ff96a..964f1d0bb 100644 --- a/test/integration/broken_deploy_test.rb +++ b/test/integration/broken_deploy_test.rb @@ -27,6 +27,5 @@ def assert_failed_deploy(output) assert_match /First web container is unhealthy on vm[12], not booting any other roles/, output assert_match "First web container is unhealthy, not booting workers on vm3", output assert_match "nginx: [emerg] unexpected end of file, expecting \";\" or \"}\" in /etc/nginx/conf.d/default.conf:2", output - assert_match 'ERROR {"Status":"unhealthy","FailingStreak":0,"Log":[]}', output end end diff --git a/test/integration/docker/deployer/app/.kamal/hooks/post-proxy-reboot b/test/integration/docker/deployer/app/.kamal/hooks/post-proxy-reboot new file mode 100755 index 000000000..fb47d3cc6 --- /dev/null +++ b/test/integration/docker/deployer/app/.kamal/hooks/post-proxy-reboot @@ -0,0 +1,3 @@ +#!/bin/sh +echo "Rebooted kamal-proxy on ${KAMAL_HOSTS}" +mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/post-proxy-reboot diff --git a/test/integration/docker/deployer/app/.kamal/hooks/post-traefik-reboot b/test/integration/docker/deployer/app/.kamal/hooks/post-traefik-reboot deleted file mode 100755 index 598ddb633..000000000 --- a/test/integration/docker/deployer/app/.kamal/hooks/post-traefik-reboot +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -echo "Rebooted Traefik on ${KAMAL_HOSTS}" -mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/post-traefik-reboot diff --git a/test/integration/docker/deployer/app/.kamal/hooks/pre-proxy-reboot b/test/integration/docker/deployer/app/.kamal/hooks/pre-proxy-reboot new file mode 100755 index 000000000..65a70a001 --- /dev/null +++ b/test/integration/docker/deployer/app/.kamal/hooks/pre-proxy-reboot @@ -0,0 +1,3 @@ +#!/bin/sh +echo "Rebooting Traefik on ${KAMAL_HOSTS}..." +mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-proxy-reboot diff --git a/test/integration/docker/deployer/app/.kamal/hooks/pre-traefik-reboot b/test/integration/docker/deployer/app/.kamal/hooks/pre-traefik-reboot deleted file mode 100755 index 81269d246..000000000 --- a/test/integration/docker/deployer/app/.kamal/hooks/pre-traefik-reboot +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -echo "Rebooting Traefik on ${KAMAL_HOSTS}..." -mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-traefik-reboot diff --git a/test/integration/docker/deployer/app/Dockerfile b/test/integration/docker/deployer/app/Dockerfile index dc270aa9a..0e6237df4 100644 --- a/test/integration/docker/deployer/app/Dockerfile +++ b/test/integration/docker/deployer/app/Dockerfile @@ -6,4 +6,4 @@ ARG COMMIT_SHA RUN echo $COMMIT_SHA > /usr/share/nginx/html/version RUN mkdir -p /usr/share/nginx/html/versions && echo "version" > /usr/share/nginx/html/versions/$COMMIT_SHA RUN mkdir -p /usr/share/nginx/html/versions && echo "hidden" > /usr/share/nginx/html/versions/.hidden - +RUN echo "Up!" > /usr/share/nginx/html/up diff --git a/test/integration/docker/deployer/app/config/deploy.yml b/test/integration/docker/deployer/app/config/deploy.yml index a85412ad2..5e9994026 100644 --- a/test/integration/docker/deployer/app/config/deploy.yml +++ b/test/integration/docker/deployer/app/config/deploy.yml @@ -20,6 +20,8 @@ env: secret: - SECRET_TAG asset_path: /usr/share/nginx/html/versions +proxy: + deploy_timeout: 2s registry: server: registry:4443 @@ -30,14 +32,6 @@ builder: arch: <%= Kamal::Utils.docker_arch %> args: COMMIT_SHA: <%= `git rev-parse HEAD` %> -healthcheck: - cmd: wget -qO- http://localhost > /dev/null || exit 1 - max_attempts: 3 -traefik: - args: - accesslog: true - accesslog.format: json - image: registry:4443/traefik:v2.10 accessories: busybox: service: custom-busybox diff --git a/test/integration/docker/deployer/app_with_roles/.kamal/hooks/post-proxy-reboot b/test/integration/docker/deployer/app_with_roles/.kamal/hooks/post-proxy-reboot new file mode 100755 index 000000000..fb47d3cc6 --- /dev/null +++ b/test/integration/docker/deployer/app_with_roles/.kamal/hooks/post-proxy-reboot @@ -0,0 +1,3 @@ +#!/bin/sh +echo "Rebooted kamal-proxy on ${KAMAL_HOSTS}" +mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/post-proxy-reboot diff --git a/test/integration/docker/deployer/app_with_roles/.kamal/hooks/post-traefik-reboot b/test/integration/docker/deployer/app_with_roles/.kamal/hooks/post-traefik-reboot deleted file mode 100755 index 598ddb633..000000000 --- a/test/integration/docker/deployer/app_with_roles/.kamal/hooks/post-traefik-reboot +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -echo "Rebooted Traefik on ${KAMAL_HOSTS}" -mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/post-traefik-reboot diff --git a/test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-proxy-reboot b/test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-proxy-reboot new file mode 100755 index 000000000..6413b6ef2 --- /dev/null +++ b/test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-proxy-reboot @@ -0,0 +1,3 @@ +#!/bin/sh +echo "Rebooting kamal-proxy on ${KAMAL_HOSTS}..." +mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-proxy-reboot diff --git a/test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-traefik-reboot b/test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-traefik-reboot deleted file mode 100755 index 81269d246..000000000 --- a/test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-traefik-reboot +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -echo "Rebooting Traefik on ${KAMAL_HOSTS}..." -mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-traefik-reboot diff --git a/test/integration/docker/deployer/app_with_roles/Dockerfile b/test/integration/docker/deployer/app_with_roles/Dockerfile index dc270aa9a..0e6237df4 100644 --- a/test/integration/docker/deployer/app_with_roles/Dockerfile +++ b/test/integration/docker/deployer/app_with_roles/Dockerfile @@ -6,4 +6,4 @@ ARG COMMIT_SHA RUN echo $COMMIT_SHA > /usr/share/nginx/html/version RUN mkdir -p /usr/share/nginx/html/versions && echo "version" > /usr/share/nginx/html/versions/$COMMIT_SHA RUN mkdir -p /usr/share/nginx/html/versions && echo "hidden" > /usr/share/nginx/html/versions/.hidden - +RUN echo "Up!" > /usr/share/nginx/html/up diff --git a/test/integration/docker/deployer/app_with_roles/config/deploy.yml b/test/integration/docker/deployer/app_with_roles/config/deploy.yml index 3b9426654..835b4ffda 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,8 @@ servers: hosts: - vm3 cmd: sleep infinity +proxy: + deploy_timeout: 2s asset_path: /usr/share/nginx/html/versions @@ -21,14 +23,6 @@ builder: arch: <%= Kamal::Utils.docker_arch %> args: COMMIT_SHA: <%= `git rev-parse HEAD` %> -healthcheck: - cmd: wget -qO- http://localhost > /dev/null || exit 1 - max_attempts: 3 -traefik: - args: - accesslog: true - accesslog.format: json - image: registry:4443/traefik:v2.10 accessories: busybox: service: custom-busybox diff --git a/test/integration/docker/deployer/setup.sh b/test/integration/docker/deployer/setup.sh index 0cd511d92..4867519ee 100755 --- a/test/integration/docker/deployer/setup.sh +++ b/test/integration/docker/deployer/setup.sh @@ -19,7 +19,6 @@ push_image_to_registry_4443() { install_kamal push_image_to_registry_4443 nginx 1-alpine-slim -push_image_to_registry_4443 traefik v2.10 push_image_to_registry_4443 busybox 1.36.0 # .ssh is on a shared volume that persists between runs. Clean it up as the diff --git a/test/integration/integration_test.rb b/test/integration/integration_test.rb index ea445d9e7..5c675da69 100644 --- a/test/integration/integration_test.rb +++ b/test/integration/integration_test.rb @@ -31,7 +31,7 @@ def docker_compose(*commands, capture: false, raise_on_error: true) succeeded = system("cd test/integration && #{command}") end - raise "Command `#{command}` failed with error code `#{$?}`" if !succeeded && raise_on_error + raise "Command `#{command}` failed with error code `#{$?}`, and output:\n#{result}" if !succeeded && raise_on_error result end @@ -101,8 +101,8 @@ def assert_hooks_ran(*hooks) def assert_200(response) code = response.code if code != "200" - puts "Got response code #{code}, here are the traefik logs:" - kamal :traefik, :logs + puts "Got response code #{code}, here are the proxy logs:" + kamal :proxy, :logs puts "And here are the load balancer logs" docker_compose :logs, :load_balancer puts "Tried to get the response code again and got #{app_response.code}" @@ -129,8 +129,8 @@ def setup_deployer def debug_response_code(app_response, expected_code) code = app_response.code if code != expected_code - puts "Got response code #{code}, here are the traefik logs:" - kamal :traefik, :logs + puts "Got response code #{code}, here are the proxy logs:" + kamal :proxy, :logs puts "And here are the load balancer logs" docker_compose :logs, :load_balancer puts "Tried to get the response code again and got #{app_response.code}" @@ -148,4 +148,16 @@ def assert_container_not_running(host:, name:) def container_running?(host:, name:) docker_compose("exec #{host} docker ps --filter=name=#{name} | tail -n+2", capture: true).strip.present? end + + def assert_app_directory_removed + assert_directory_removed("./kamal/apps/#{@app}") + end + + def assert_proxy_directory_removed + assert_directory_removed("./kamal/proxy") + end + + def assert_directory_removed(directory) + assert docker_compose("exec vm1 ls #{directory} | wc -l", capture: true).strip == "0" + end end diff --git a/test/integration/main_test.rb b/test/integration/main_test.rb index b58aeeb9b..5a4eb91c8 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:v0.1.0/, details assert_match /registry:4443\/app:#{first_version}/, details audit = kamal :audit, capture: true @@ -70,13 +70,11 @@ 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 @app = "app_with_roles" - kamal :envify kamal :deploy output = kamal :whome, capture: true @@ -99,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 new file mode 100644 index 000000000..f9c7133b3 --- /dev/null +++ b/test/integration/proxy_test.rb @@ -0,0 +1,66 @@ +require_relative "integration_test" + +class ProxyTest < IntegrationTest + setup do + @app = "app_with_roles" + end + + test "boot, reboot, stop, start, restart, logs, remove" do + kamal :proxy, :boot + assert_proxy_running + + output = kamal :proxy, :reboot, "-y", "--verbose", capture: true + assert_proxy_running + 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-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 + + # Check booting when booted doesn't raise an error + kamal :proxy, :stop + assert_proxy_not_running + + # Check booting when stopped works + kamal :proxy, :boot + assert_proxy_running + + kamal :proxy, :stop + assert_proxy_not_running + + kamal :proxy, :start + assert_proxy_running + + kamal :proxy, :restart + assert_proxy_running + + logs = kamal :proxy, :logs, capture: true + assert_match /No previous state to restore/, logs + + kamal :proxy, :remove + assert_proxy_not_running + assert_proxy_directory_removed + end + + private + def assert_proxy_running + 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:v0.1.0 \"kamal-proxy run\"/, proxy_details + end + + def proxy_details + kamal :proxy, :details, capture: true + end +end diff --git a/test/integration/traefik_test.rb b/test/integration/traefik_test.rb deleted file mode 100644 index 48f9ea024..000000000 --- a/test/integration/traefik_test.rb +++ /dev/null @@ -1,61 +0,0 @@ -require_relative "integration_test" - -class TraefikTest < IntegrationTest - test "boot, reboot, stop, start, restart, logs, remove" do - kamal :traefik, :boot - assert_traefik_running - - output = kamal :traefik, :reboot, "-y", "--verbose", capture: true - assert_traefik_running - assert_hooks_ran "pre-traefik-reboot", "post-traefik-reboot" - assert_match /Rebooting Traefik on vm1,vm2.../, output - assert_match /Rebooted Traefik on vm1,vm2/, output - - output = kamal :traefik, :reboot, "--rolling", "-y", "--verbose", capture: true - assert_traefik_running - assert_hooks_ran "pre-traefik-reboot", "post-traefik-reboot" - assert_match /Rebooting Traefik on vm1.../, output - assert_match /Rebooted Traefik on vm1/, output - assert_match /Rebooting Traefik on vm2.../, output - assert_match /Rebooted Traefik on vm2/, output - - kamal :traefik, :boot - assert_traefik_running - - # Check booting when booted doesn't raise an error - kamal :traefik, :stop - assert_traefik_not_running - - # Check booting when stopped works - kamal :traefik, :boot - assert_traefik_running - - kamal :traefik, :stop - assert_traefik_not_running - - kamal :traefik, :start - assert_traefik_running - - kamal :traefik, :restart - assert_traefik_running - - logs = kamal :traefik, :logs, capture: true - assert_match /Traefik version [\d.]+ built on/, logs - - kamal :traefik, :remove - assert_traefik_not_running - end - - private - def assert_traefik_running - assert_match /traefik:v2.10 "\/entrypoint.sh/, traefik_details - end - - def assert_traefik_not_running - assert_no_match /traefik:v2.10 "\/entrypoint.sh/, traefik_details - end - - def traefik_details - kamal :traefik, :details, capture: true - end -end