diff --git a/README.md b/README.md index 9b95066f..924e1e27 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Kamal: Deploy web apps anywhere -From bare metal to cloud VMs, deploy web apps anywhere with zero downtime. Kamal has the dynamic reverse-proxy Traefik hold requests while a new app container is started and the old one is stopped. Works seamlessly across multiple hosts, using SSHKit to execute commands. Originally built for Rails apps, Kamal will work with any type of web app that can be containerized with Docker. +From bare metal to cloud VMs, deploy web apps anywhere with zero downtime. Kamal uses [kamal-proxy](https://github.com/basecamp/kamal-proxy) to seamlessly switch requests between containers. Works seamlessly across multiple servers, using SSHKit to execute commands. Originally built for Rails apps, Kamal will work with any type of web app that can be containerized with Docker. ➡️ See [kamal-deploy.org](https://kamal-deploy.org) for documentation on [installation](https://kamal-deploy.org/docs/installation), [configuration](https://kamal-deploy.org/docs/configuration), and [commands](https://kamal-deploy.org/docs/commands). diff --git a/bin/docs b/bin/docs index a8731ce2..b76872d3 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 6772556e..dc35c403 100644 --- a/lib/kamal/cli.rb +++ b/lib/kamal/cli.rb @@ -1,4 +1,5 @@ module Kamal::Cli + class BootError < StandardError; end class HookError < StandardError; end class LockError < StandardError; end end diff --git a/lib/kamal/cli/accessory.rb b/lib/kamal/cli/accessory.rb index 2bf9a786..bc84fe52 100644 --- a/lib/kamal/cli/accessory.rb +++ b/lib/kamal/cli/accessory.rb @@ -1,16 +1,17 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)" - def boot(name, login: true) + def boot(name, prepare: true) with_lock do if name == "all" KAMAL.accessory_names.each { |accessory_name| boot(accessory_name) } else + prepare(name) if prepare + with_accessory(name) do |accessory, hosts| directories(name) upload(name) on(hosts) do - execute *KAMAL.registry.login if login execute *KAMAL.auditor.record("Booted #{name} accessory"), verbosity: :debug execute *accessory.ensure_env_directory upload! accessory.secrets_io, accessory.secrets_path, mode: "0600" @@ -57,15 +58,10 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base if name == "all" KAMAL.accessory_names.each { |accessory_name| reboot(accessory_name) } else - with_accessory(name) do |accessory, hosts| - on(hosts) do - execute *KAMAL.registry.login - end - - stop(name) - remove_container(name) - boot(name, login: false) - end + prepare(name) + stop(name) + remove_container(name) + boot(name, prepare: false) end end end @@ -97,10 +93,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base desc "restart [NAME]", "Restart existing accessory container on host" def restart(name) with_lock do - with_accessory(name) do - stop(name) - start(name) - end + stop(name) + start(name) end end @@ -251,11 +245,20 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base end def remove_accessory(name) - with_accessory(name) do - stop(name) - remove_container(name) - remove_image(name) - remove_service_directory(name) + stop(name) + remove_container(name) + remove_image(name) + remove_service_directory(name) + end + + def prepare(name) + with_accessory(name) do |accessory, hosts| + on(hosts) do + execute *KAMAL.registry.login + execute *KAMAL.docker.create_network + rescue SSHKit::Command::Failed => e + raise unless e.message.include?("already exists") + end end end end diff --git a/lib/kamal/cli/app.rb b/lib/kamal/cli/app.rb index ce21b2cf..2a02ed65 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -4,7 +4,7 @@ class Kamal::Cli::App < Kamal::Cli::Base with_lock do say "Get most recent version available as an image...", :magenta unless options[:version] using_version(version_or_latest) do |version| - say "Start container with version #{version} using a #{KAMAL.config.readiness_delay}s readiness delay (or reboot if already running)...", :magenta + say "Start container with version #{version} (or reboot if already running)...", :magenta # Assets are prepared in a separate step to ensure they are on all hosts before booting on(KAMAL.hosts) do @@ -38,8 +38,17 @@ class Kamal::Cli::App < Kamal::Cli::Base roles = KAMAL.roles_on(host) roles.each do |role| + app = KAMAL.app(role: role, host: host) execute *KAMAL.auditor.record("Started app version #{KAMAL.config.version}"), verbosity: :debug - execute *KAMAL.app(role: role, host: host).start, raise_on_non_zero_exit: false + execute *app.start, raise_on_non_zero_exit: false + + if role.running_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 @@ class Kamal::Cli::App < Kamal::Cli::Base roles = KAMAL.roles_on(host) roles.each do |role| + app = KAMAL.app(role: role, host: host) execute *KAMAL.auditor.record("Stopped app", role: role), verbosity: :debug - execute *KAMAL.app(role: role, host: host).stop, raise_on_non_zero_exit: false + + if role.running_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 @@ class Kamal::Cli::App < Kamal::Cli::Base stop remove_containers remove_images + remove_app_directory end end @@ -253,6 +273,20 @@ class Kamal::Cli::App < Kamal::Cli::Base 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 df3e6925..6939825a 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 @@ class Kamal::Cli::App::Boot def start_new_version audit "Booted app version #{version}" - - execute *app.tie_cord(role.cord_host_file) if uses_cord? hostname = "#{host.to_s[0...51].gsub(/\.+$/, '')}-#{SecureRandom.hex(6)}" execute *app.ensure_env_directory upload! role.secrets_io(host), role.secrets_path, mode: "0600" - execute *app.run(hostname: hostname) - Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) } + 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 @@ class Kamal::Cli::App::Boot 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 85815506..b3af23cc 100644 --- a/lib/kamal/cli/base.rb +++ b/lib/kamal/cli/base.rb @@ -101,7 +101,7 @@ module Kamal::Cli end def acquire_lock - ensure_run_and_locks_directory + ensure_run_directory raise_if_locked do say "Acquiring the deploy lock...", :magenta @@ -174,14 +174,10 @@ module Kamal::Cli 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 249a1f6b..fad51845 100644 --- a/lib/kamal/cli/healthcheck/poller.rb +++ b/lib/kamal/cli/healthcheck/poller.rb @@ -1,26 +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})" + status = block.call + + if status == "running" + # Wait for the readiness delay and confirm it is still running + if readiness_delay > 0 + info "Container is running, waiting for readiness delay of #{readiness_delay} seconds" + sleep readiness_delay + status = block.call + end + end + + unless %w[ running healthy ].include?(status) + raise Kamal::Cli::Healthcheck::Error, "container not ready after #{KAMAL.config.readiness_timeout} seconds (#{status})" end rescue Kamal::Cli::Healthcheck::Error => e - if attempt <= max_attempts - info "#{e.message}, retrying in #{attempt}s (attempt #{attempt}/#{max_attempts})..." - sleep attempt + time_left = timeout_at - Time.now + if time_left > 0 + sleep [ attempt, time_left ].min attempt += 1 retry else @@ -31,31 +35,6 @@ module Kamal::Cli::Healthcheck::Poller 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})" - end - rescue Kamal::Cli::Healthcheck::Error => e - if attempt <= max_attempts - info "#{e.message}, retrying in #{attempt}s (attempt #{attempt}/#{max_attempts})..." - sleep attempt - attempt += 1 - retry - else - raise - end - end - - info "Container is unhealthy!" - end - private def info(message) SSHKit.config.output.info(message) diff --git a/lib/kamal/cli/lock.rb b/lib/kamal/cli/lock.rb index 306c8a07..ceab1f27 100644 --- a/lib/kamal/cli/lock.rb +++ b/lib/kamal/cli/lock.rb @@ -12,7 +12,7 @@ class Kamal::Cli::Lock < Kamal::Cli::Base option :message, aliases: "-m", type: :string, desc: "A lock message", required: true def acquire message = options[:message] - ensure_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 3bd6dc24..46ce1583 100644 --- a/lib/kamal/cli/main.rb +++ b/lib/kamal/cli/main.rb @@ -35,8 +35,8 @@ class Kamal::Cli::Main < Kamal::Cli::Base with_lock do run_hook "pre-deploy", secrets: true - say "Ensure Traefik is running...", :magenta - invoke "kamal:cli:traefik:boot", [], invoke_options + 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 @@ class Kamal::Cli::Main < Kamal::Cli::Base run_hook "post-deploy", secrets: true, runtime: runtime.round end - desc "redeploy", "Deploy app to servers without bootstrapping servers, starting Traefik, pruning, and registry login" + desc "redeploy", "Deploy app to servers without bootstrapping servers, starting kamal-proxy, pruning, and registry login" option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push" def redeploy runtime = print_runtime do @@ -104,7 +104,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base 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 @@ class Kamal::Cli::Main < Kamal::Cli::Base end end - desc "remove", "Remove Traefik, app, accessories, and registry session from servers" + desc "remove", "Remove kamal-proxy, app, accessories, and registry session from servers" option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question" def remove confirming "This will remove all containers and images. Are you sure?" do with_lock do - 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 @@ class Kamal::Cli::Main < Kamal::Cli::Base 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 @@ class Kamal::Cli::Main < Kamal::Cli::Base desc "server", "Bootstrap servers with curl and Docker" subcommand "server", Kamal::Cli::Server - desc "traefik", "Manage Traefik load balancer" - subcommand "traefik", Kamal::Cli::Traefik - private def container_available?(version) begin diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb new file mode 100644 index 00000000..108d7855 --- /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 7635e97d..aaf79980 100644 --- a/lib/kamal/cli/prune.rb +++ b/lib/kamal/cli/prune.rb @@ -28,7 +28,6 @@ class Kamal::Cli::Prune < Kamal::Cli::Base on(KAMAL.hosts) do execute *KAMAL.auditor.record("Pruned containers"), verbosity: :debug execute *KAMAL.prune.app_containers(retain: retain) - execute *KAMAL.prune.healthcheck_containers end end end diff --git a/lib/kamal/cli/server.rb b/lib/kamal/cli/server.rb index 5b1b0cc7..452ab089 100644 --- a/lib/kamal/cli/server.rb +++ b/lib/kamal/cli/server.rb @@ -36,8 +36,6 @@ class Kamal::Cli::Server < Kamal::Cli::Base missing << host end end - - execute(*KAMAL.server.ensure_run_directory) end if missing.any? diff --git a/lib/kamal/cli/templates/deploy.yml b/lib/kamal/cli/templates/deploy.yml index 119961b9..8ecf5d04 100644 --- a/lib/kamal/cli/templates/deploy.yml +++ b/lib/kamal/cli/templates/deploy.yml @@ -57,17 +57,6 @@ builder: # directories: # - data:/data -# Configure custom arguments for Traefik. Be sure to reboot traefik when you modify it. -# traefik: -# args: -# accesslog: true -# accesslog.format: json - -# Configure a custom healthcheck (default is /up on port 3000) -# healthcheck: -# path: /healthz -# port: 4000 - # Bridge fingerprinted assets, like JS and CSS, between versions to avoid # hitting 404 on in-flight requests. Combines all files from new and old # version inside the asset_path. diff --git a/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample b/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample new file mode 100755 index 00000000..1435a677 --- /dev/null +++ b/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooted kamal-proxy on $KAMAL_HOSTS" diff --git a/lib/kamal/cli/templates/sample_hooks/post-traefik-reboot.sample b/lib/kamal/cli/templates/sample_hooks/post-traefik-reboot.sample deleted file mode 100755 index e3d9e3cc..00000000 --- a/lib/kamal/cli/templates/sample_hooks/post-traefik-reboot.sample +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -echo "Rebooted Traefik on $KAMAL_HOSTS" diff --git a/lib/kamal/cli/templates/sample_hooks/pre-traefik-reboot.sample b/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample similarity index 100% rename from lib/kamal/cli/templates/sample_hooks/pre-traefik-reboot.sample rename to lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample diff --git a/lib/kamal/cli/traefik.rb b/lib/kamal/cli/traefik.rb deleted file mode 100644 index 41ffbc04..00000000 --- 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 11914a67..c07e8933 100644 --- a/lib/kamal/commander.rb +++ b/lib/kamal/commander.rb @@ -4,7 +4,7 @@ require "active_support/core_ext/object/blank" class Kamal::Commander attr_accessor :verbosity, :holding_lock, :connected - delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :traefik_hosts, :accessory_hosts, to: :specifics + delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :proxy_hosts, :accessory_hosts, to: :specifics def initialize self.verbosity = :info @@ -94,10 +94,6 @@ class Kamal::Commander @docker ||= Kamal::Commands::Docker.new(config) end - def healthcheck - @healthcheck ||= Kamal::Commands::Healthcheck.new(config) - end - def hook @hook ||= Kamal::Commands::Hook.new(config) end @@ -106,6 +102,10 @@ class Kamal::Commander @lock ||= Kamal::Commands::Lock.new(config) end + def proxy + @proxy ||= Kamal::Commands::Proxy.new(config) + end + def prune @prune ||= Kamal::Commands::Prune.new(config) end @@ -118,10 +118,6 @@ class Kamal::Commander @server ||= Kamal::Commands::Server.new(config) end - def traefik - @traefik ||= Kamal::Commands::Traefik.new(config) - end - def alias(name) config.aliases[name] end diff --git a/lib/kamal/commander/specifics.rb b/lib/kamal/commander/specifics.rb index 127bd40e..288fd9b5 100644 --- a/lib/kamal/commander/specifics.rb +++ b/lib/kamal/commander/specifics.rb @@ -18,8 +18,8 @@ class Kamal::Commander::Specifics 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 f3b676d1..787f7d43 100644 --- a/lib/kamal/commands/accessory.rb +++ b/lib/kamal/commands/accessory.rb @@ -15,6 +15,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base "--name", service_name, "--detach", "--restart", "unless-stopped", + "--network", "kamal", *config.logging_args, *publish_args, *env_args, @@ -63,6 +64,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base docker :run, ("-it" if interactive), "--rm", + "--network", "kamal", *env_args, *volume_args, image, diff --git a/lib/kamal/commands/app.rb b/lib/kamal/commands/app.rb index f1991e48..417ad463 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 @@ class Kamal::Commands::App < Kamal::Commands::Base "--detach", "--restart unless-stopped", "--name", container_name, + "--network", "kamal", *([ "--hostname", hostname ] if hostname), "-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"", "-e", "KAMAL_VERSION=\"#{config.version}\"", *role.env_args(host), - *role.health_check_args, *role.logging_args, *config.volume_args, *role.asset_volume_args, @@ -74,10 +76,6 @@ class Kamal::Commands::App < Kamal::Commands::Base end private - def container_name(version = nil) - [ role.container_prefix, version || config.version ].compact.join("-") - end - def latest_image_id docker :image, :ls, *argumentize("--filter", "reference=#{config.latest_image}"), "--format", "'{{.ID}}'" end diff --git a/lib/kamal/commands/app/assets.rb b/lib/kamal/commands/app/assets.rb index 9841f4fb..c1e65d18 100644 --- a/lib/kamal/commands/app/assets.rb +++ b/lib/kamal/commands/app/assets.rb @@ -3,18 +3,18 @@ module Kamal::Commands::App::Assets asset_container = "#{role.container_prefix}-assets" combine \ - make_directory(role.asset_extracted_path), + make_directory(role.asset_extracted_directory), [ *docker(:stop, "-t 1", asset_container, "2> /dev/null"), "|| true" ], docker(:run, "--name", asset_container, "--detach", "--rm", "--entrypoint", "sleep", config.absolute_image, "1000000"), - docker(:cp, "-L", "#{asset_container}:#{role.asset_path}/.", role.asset_extracted_path), + docker(:cp, "-L", "#{asset_container}:#{role.asset_path}/.", role.asset_extracted_directory), docker(:stop, "-t 1", asset_container), by: "&&" end def sync_asset_volumes(old_version: nil) - new_extracted_path, new_volume_path = role.asset_extracted_path(config.version), role.asset_volume.host_path + new_extracted_path, new_volume_path = role.asset_extracted_directory(config.version), role.asset_volume.host_path if old_version.present? - old_extracted_path, old_volume_path = role.asset_extracted_path(old_version), role.asset_volume(old_version).host_path + old_extracted_path, old_volume_path = role.asset_extracted_directory(old_version), role.asset_volume(old_version).host_path end commands = [ make_directory(new_volume_path), copy_contents(new_extracted_path, new_volume_path) ] @@ -29,8 +29,8 @@ module Kamal::Commands::App::Assets def clean_up_assets chain \ - find_and_remove_older_siblings(role.asset_extracted_path), - find_and_remove_older_siblings(role.asset_volume_path) + find_and_remove_older_siblings(role.asset_extracted_directory), + find_and_remove_older_siblings(role.asset_volume_directory) end private @@ -39,7 +39,7 @@ module Kamal::Commands::App::Assets :find, Pathname.new(path).dirname.to_s, "-maxdepth 1", - "-name", "'#{role.container_prefix}-*'", + "-name", "'#{role.name}-*'", "!", "-name", Pathname.new(path).basename.to_s, "-exec rm -rf \"{}\" +" ] diff --git a/lib/kamal/commands/app/cord.rb b/lib/kamal/commands/app/cord.rb deleted file mode 100644 index 4912992e..00000000 --- a/lib/kamal/commands/app/cord.rb +++ /dev/null @@ -1,22 +0,0 @@ -module Kamal::Commands::App::Cord - def cord(version:) - pipe \ - docker(:inspect, "-f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}'", container_name(version)), - [ :awk, "'$2 == \"#{role.cord_volume.container_path}\" {print $1}'" ] - end - - def tie_cord(cord) - create_empty_file(cord) - end - - def cut_cord(cord) - remove_directory(cord) - end - - private - def create_empty_file(file) - chain \ - make_directory_for(file), - [ :touch, file ] - end -end diff --git a/lib/kamal/commands/docker.rb b/lib/kamal/commands/docker.rb index 2966e095..bc9345df 100644 --- a/lib/kamal/commands/docker.rb +++ b/lib/kamal/commands/docker.rb @@ -19,6 +19,10 @@ class Kamal::Commands::Docker < Kamal::Commands::Base [ '[ "${EUID:-$(id -u)}" -eq 0 ] || command -v sudo >/dev/null || command -v su >/dev/null' ] end + def create_network + docker :network, :create, :kamal + end + private def get_docker shell \ diff --git a/lib/kamal/commands/lock.rb b/lib/kamal/commands/lock.rb index 395b6f3a..aafaec70 100644 --- a/lib/kamal/commands/lock.rb +++ b/lib/kamal/commands/lock.rb @@ -44,14 +44,10 @@ class Kamal::Commands::Lock < Kamal::Commands::Base "/dev/null" end - def locks_dir - File.join(config.run_directory, "locks") - end - def lock_dir - dir_name = [ config.service, config.destination ].compact.join("-") + dir_name = [ "lock", config.service, config.destination ].compact.join("-") - File.join(locks_dir, dir_name) + File.join(config.run_directory, dir_name) end def lock_details_file diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb new file mode 100644 index 00000000..354493db --- /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 b820b5af..30b6eafd 100644 --- a/lib/kamal/commands/prune.rb +++ b/lib/kamal/commands/prune.rb @@ -20,10 +20,6 @@ class Kamal::Commands::Prune < Kamal::Commands::Base "while read container_id; do docker rm $container_id; done" end - def healthcheck_containers - docker :container, :prune, "--force", *healthcheck_service_filter - end - private def stopped_containers_filters [ "created", "exited", "dead" ].flat_map { |status| [ "--filter", "status=#{status}" ] } @@ -39,8 +35,4 @@ class Kamal::Commands::Prune < Kamal::Commands::Base def service_filter [ "--filter", "label=service=#{config.service}" ] end - - def healthcheck_service_filter - [ "--filter", "label=service=#{config.healthcheck_service}" ] - end end diff --git a/lib/kamal/commands/server.rb b/lib/kamal/commands/server.rb index fb781fe8..305903f6 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 964ef3eb..00000000 --- a/lib/kamal/commands/traefik.rb +++ /dev/null @@ -1,77 +0,0 @@ -class Kamal::Commands::Traefik < Kamal::Commands::Base - delegate :argumentize, :optionize, to: Kamal::Utils - delegate :port, :publish?, :labels, :env, :image, :options, :args, :env_args, :secrets_io, :env_directory, :secrets_path, to: :"config.traefik" - - def run - docker :run, "--name traefik", - "--detach", - "--restart", "unless-stopped", - *publish_args, - "--volume", "/var/run/docker.sock:/var/run/docker.sock", - *env_args, - *config.logging_args, - *label_args, - *docker_options_args, - image, - "--providers.docker", - *cmd_option_args - end - - def start - docker :container, :start, "traefik" - end - - def stop - docker :container, :stop, "traefik" - end - - def start_or_run - any start, run - end - - def info - docker :ps, "--filter", "name=^traefik$" - end - - def logs(since: nil, lines: nil, grep: nil, grep_options: nil) - pipe \ - docker(:logs, "traefik", (" --since #{since}" if since), (" --tail #{lines}" if lines), "--timestamps", "2>&1"), - ("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep) - end - - def follow_logs(host:, grep: nil, grep_options: nil) - run_over_ssh pipe( - docker(:logs, "traefik", "--timestamps", "--tail", "10", "--follow", "2>&1"), - (%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep) - ).join(" "), host: host - end - - def remove_container - docker :container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=Traefik" - end - - def remove_image - docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=Traefik" - end - - def ensure_env_directory - make_directory env_directory - end - - private - def publish_args - argumentize "--publish", port if publish? - end - - def label_args - argumentize "--label", labels - end - - def docker_options_args - optionize(options) - end - - def cmd_option_args - optionize args, with: "=" - end -end diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index 2758d15a..bd33b08c 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 @@ class Kamal::Configuration @builder = Builder.new(config: self) @env = Env.new(config: @raw_config.env || {}, secrets: secrets) - @healthcheck = Healthcheck.new(healthcheck_config: @raw_config.healthcheck) @logging = Logging.new(logging_config: @raw_config.logging) - @traefik = Traefik.new(config: self) + @proxy = Proxy.new(config: self) @ssh = Ssh.new(config: self) @sshkit = Sshkit.new(config: self) @@ -71,6 +70,7 @@ class Kamal::Configuration ensure_valid_kamal_version ensure_retain_containers_valid ensure_valid_service_name + ensure_no_traefik_reboot_hooks end @@ -131,16 +131,16 @@ class Kamal::Configuration raw_config.allow_empty_roles end - def traefik_roles - roles.select(&:running_traefik?) + def proxy_roles + roles.select(&:running_proxy?) end - def traefik_role_names - traefik_roles.flat_map(&:name) + 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 @@ class Kamal::Configuration 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 @@ class Kamal::Configuration end - def env_directory - File.join(run_directory, "env") - end - def env_tags @env_tags ||= if (tags = raw_config.env["tags"]) tags.collect { |name, config| Env::Tag.new(name, config: config, secrets: secrets) } @@ -250,8 +255,7 @@ class Kamal::Configuration sshkit: sshkit.to_h, builder: builder.to_h, accessories: raw_config.accessories, - logging: logging_args, - healthcheck: healthcheck.to_h + logging: logging_args }.compact end @@ -313,6 +317,16 @@ class Kamal::Configuration true end + def ensure_no_traefik_reboot_hooks + hooks = %w[ pre-traefik-reboot post-traefik-reboot ].select { |hook_file| File.exist?(File.join(hooks_path, hook_file)) } + + if hooks.any? + raise Kamal::ConfigurationError, "Found #{hooks.join(", ")}, these should be renamed to (pre|post)-proxy-reboot" + end + + true + end + def role_names raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort diff --git a/lib/kamal/configuration/accessory.rb b/lib/kamal/configuration/accessory.rb index 57489f17..804a1502 100644 --- a/lib/kamal/configuration/accessory.rb +++ b/lib/kamal/configuration/accessory.rb @@ -63,7 +63,7 @@ class Kamal::Configuration::Accessory end def secrets_path - File.join(config.env_directory, "accessories", "#{service_name}.env") + File.join(config.env_directory, "accessories", "#{name}.env") end def files diff --git a/lib/kamal/configuration/docs/configuration.yml b/lib/kamal/configuration/docs/configuration.yml index f1045dd6..2e7b618d 100644 --- a/lib/kamal/configuration/docs/configuration.yml +++ b/lib/kamal/configuration/docs/configuration.yml @@ -111,9 +111,15 @@ minimum_version: 1.3.0 # Readiness delay # # Seconds to wait for a container to boot after is running, default 7 -# This only applies to containers that do not specify a healthcheck +# This only applies to containers that do not run a proxy or specify a healthcheck readiness_delay: 4 +# Readiness timeout +# +# How long to wait for a container to become ready, default 30 +# This only applies to containers that do not run a proxy +readiness_timeout: 4 + # Run directory # # Directory to store kamal runtime files in on the host, default `.kamal` @@ -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 0ca2bfa9..c2cf0ed0 100644 --- a/lib/kamal/configuration/docs/env.yml +++ b/lib/kamal/configuration/docs/env.yml @@ -24,14 +24,12 @@ env: # KAMAL_REGISTRY_PASSWORD=pw # DB_PASSWORD=secret123 # ``` -# See https://kamal-deploy.org/docs/commands/envify/ for how to use generated .env files. # # To pass the secrets you should list them under the `secret` key. When you do this the # other variables need to be moved under the `clear` key. # # Unlike clear values, secrets are not passed directly to the container, # but are stored in an env file on the host -# The file is not updated when deploying, only when running `kamal envify` or `kamal env push`. env: clear: DB_USER: app diff --git a/lib/kamal/configuration/docs/healthcheck.yml b/lib/kamal/configuration/docs/healthcheck.yml deleted file mode 100644 index 3c55d5dd..00000000 --- a/lib/kamal/configuration/docs/healthcheck.yml +++ /dev/null @@ -1,59 +0,0 @@ -# Healthcheck configuration -# -# On roles that are running Traefik, Kamal will supply a default healthcheck to `docker run`. -# For other roles, by default no healthcheck is supplied. -# -# If no healthcheck is supplied and the image does not define one, then we wait for the container -# to reach a running state and then pause for the readiness delay. -# -# The default healthcheck is `curl -f http://localhost:/`, so it assumes that `curl` -# is available within the container. - -# Healthcheck options -# -# These go under the `healthcheck` key in the root or role configuration. -healthcheck: - - # Command - # - # The command to run, defaults to `curl -f http://localhost:/` on roles running Traefik - cmd: "curl -f http://localhost" - - # Interval - # - # The Docker healthcheck interval, defaults to `1s` - interval: 10s - - # Max attempts - # - # The maximum number of times we poll the container to see if it is healthy, defaults to `7` - # Each check is separated by an increasing interval starting with 1 second. - max_attempts: 3 - - # Port - # - # The port to use in the healthcheck, defaults to `3000` - port: "80" - - # Path - # - # The path to use in the healthcheck, defaults to `/up` - path: /health - - # Cords for zero-downtime deployments - # - # The cord file is used for zero-downtime deployments. The healthcheck is augmented with a check - # for the existance of the file. This allows us to delete the file and force the container to - # become unhealthy, causing Traefik to stop routing traffic to it. - # - # Kamal mounts a volume at this location and creates the file before starting the container. - # You can set the value to `false` to disable the cord file, but this loses the zero-downtime - # guarantee. - # - # The default value is `/tmp/kamal-cord` - cord: /cord - - # Log lines - # - # Number of lines to log from the container when the healthcheck fails, defaults to `50` - log_lines: 100 diff --git a/lib/kamal/configuration/docs/proxy.yml b/lib/kamal/configuration/docs/proxy.yml new file mode 100644 index 00000000..e754eb36 --- /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 9f6962a5..8ed6e46c 100644 --- a/lib/kamal/configuration/docs/role.yml +++ b/lib/kamal/configuration/docs/role.yml @@ -26,7 +26,7 @@ servers: # # When there are other options to set, the list of hosts goes under the `hosts` key # - # By default only the primary role uses Traefik, but you can set `traefik` to change + # By default only the primary role uses a proxy, but you can set `proxy` to change # it. # # You can also set a custom cmd to run in the container, and overwrite other settings @@ -35,13 +35,11 @@ servers: hosts: - 172.1.0.3 - 172.1.0.4: experiment1 - traefik: true + proxy: true cmd: "bin/jobs" options: memory: 2g cpus: 4 - healthcheck: - ... logging: ... labels: diff --git a/lib/kamal/configuration/docs/traefik.yml b/lib/kamal/configuration/docs/traefik.yml deleted file mode 100644 index 756afa9e..00000000 --- a/lib/kamal/configuration/docs/traefik.yml +++ /dev/null @@ -1,62 +0,0 @@ -# Traefik -# -# Traefik is a reverse proxy, used by Kamal for zero-downtime deployments. -# -# We start an instance on the hosts in it's own container. -# -# During a deployment: -# 1. We start a new container which Traefik automatically detects due to the labels we have applied -# 2. Traefik starts routing traffic to the new container -# 3. We force the old container to fail it's healthcheck, causing Traefik to stop routing traffic to it -# 4. We stop the old container - -# Traefik settings -# -# Traekik is configured in the root configuration under `traefik`. -traefik: - - # Image - # - # The Traefik image to use, defaults to `traefik:v2.10` - image: traefik:v2.9 - - # Host port - # - # The host port to publish the Traefik container on, defaults to `80` - host_port: "8080" - - # Disabling publishing - # - # To avoid publishing the Traefik container, set this to `false` - publish: false - - # Labels - # - # Additional labels to apply to the Traefik container - labels: - traefik.http.routers.catchall.entryPoints: http - traefik.http.routers.catchall.rule: PathPrefix(`/`) - traefik.http.routers.catchall.service: unavailable - traefik.http.routers.catchall.priority: "1" - traefik.http.services.unavailable.loadbalancer.server.port: "0" - - # Arguments - # - # Additional arguments to pass to the Traefik container - args: - entryPoints.http.address: ":80" - entryPoints.http.forwardedHeaders.insecure: true - accesslog: true - accesslog.format: json - - # Options - # - # Additional options to pass to `docker run` - options: - cpus: 2 - - # Environment variables - # - # See kamal docs env - env: - ... diff --git a/lib/kamal/configuration/healthcheck.rb b/lib/kamal/configuration/healthcheck.rb deleted file mode 100644 index 888068a4..00000000 --- a/lib/kamal/configuration/healthcheck.rb +++ /dev/null @@ -1,63 +0,0 @@ -class Kamal::Configuration::Healthcheck - include Kamal::Configuration::Validation - - attr_reader :healthcheck_config - - def initialize(healthcheck_config:, context: "healthcheck") - @healthcheck_config = healthcheck_config || {} - validate! @healthcheck_config, context: context - end - - def merge(other) - self.class.new healthcheck_config: healthcheck_config.deep_merge(other.healthcheck_config) - end - - def cmd - healthcheck_config.fetch("cmd", http_health_check) - end - - def port - healthcheck_config.fetch("port", 3000) - end - - def path - healthcheck_config.fetch("path", "/up") - end - - def max_attempts - healthcheck_config.fetch("max_attempts", 7) - end - - def interval - healthcheck_config.fetch("interval", "1s") - end - - def cord - healthcheck_config.fetch("cord", "/tmp/kamal-cord") - end - - def log_lines - healthcheck_config.fetch("log_lines", 50) - end - - def set_port_or_path? - healthcheck_config["port"].present? || healthcheck_config["path"].present? - end - - def to_h - { - "cmd" => cmd, - "interval" => interval, - "max_attempts" => max_attempts, - "port" => port, - "path" => path, - "cord" => cord, - "log_lines" => log_lines - } - end - - private - def http_health_check - "curl -f #{URI.join("http://localhost:#{port}", path)} || exit 1" if path.present? || port.present? - end -end diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb new file mode 100644 index 00000000..2671b4c8 --- /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 ef651898..3f1e6d74 100644 --- a/lib/kamal/configuration/role.rb +++ b/lib/kamal/configuration/role.rb @@ -1,10 +1,9 @@ class Kamal::Configuration::Role include Kamal::Configuration::Validation - CORD_FILE = "cord" delegate :argumentize, :optionize, to: Kamal::Utils - attr_reader :name, :config, :specialized_env, :specialized_logging, :specialized_healthcheck + attr_reader :name, :config, :specialized_env, :specialized_logging alias to_s name @@ -24,10 +23,6 @@ class Kamal::Configuration::Role @specialized_logging = Kamal::Configuration::Logging.new \ logging_config: specializations.fetch("logging", {}), context: "servers/#{name}/logging" - - @specialized_healthcheck = Kamal::Configuration::Healthcheck.new \ - healthcheck_config: specializations.fetch("healthcheck", {}), - context: "servers/#{name}/healthcheck" end def primary_host @@ -55,7 +50,7 @@ class Kamal::Configuration::Role end def labels - default_labels.merge(traefik_labels).merge(custom_labels) + default_labels.merge(custom_labels) end def label_args @@ -89,7 +84,7 @@ class Kamal::Configuration::Role end def secrets_path - File.join(config.env_directory, "roles", "#{container_prefix}.env") + File.join(config.env_directory, "roles", "#{name}.env") end def asset_volume_args @@ -97,38 +92,11 @@ class Kamal::Configuration::Role end - def health_check_args(cord: true) - if running_traefik? || healthcheck.set_port_or_path? - if cord && uses_cord? - optionize({ "health-cmd" => health_check_cmd_with_cord, "health-interval" => healthcheck.interval }) - .concat(cord_volume.docker_args) - else - optionize({ "health-cmd" => healthcheck.cmd, "health-interval" => healthcheck.interval }) - end - else - [] - end - end - - def healthcheck - @healthcheck ||= - if running_traefik? - config.healthcheck.merge(specialized_healthcheck) - else - specialized_healthcheck - end - end - - def health_check_cmd_with_cord - "(#{healthcheck.cmd}) && (stat #{cord_container_file} > /dev/null || exit 1)" - end - - - def running_traefik? - if specializations["traefik"].nil? + def running_proxy? + if specializations["proxy"].nil? primary? else - specializations["traefik"] + specializations["proxy"] end end @@ -137,35 +105,6 @@ class Kamal::Configuration::Role end - def uses_cord? - running_traefik? && cord_volume && healthcheck.cmd.present? - end - - def cord_host_directory - File.join config.run_directory_as_docker_volume, "cords", [ container_prefix, config.run_id ].join("-") - end - - def cord_volume - if (cord = healthcheck.cord) - @cord_volume ||= Kamal::Configuration::Volume.new \ - host_path: File.join(config.run_directory, "cords", [ container_prefix, config.run_id ].join("-")), - container_path: cord - end - end - - def cord_host_file - File.join cord_volume.host_path, CORD_FILE - end - - def cord_container_directory - health_check_options.fetch("cord", nil) - end - - def cord_container_file - File.join cord_volume.container_path, CORD_FILE - end - - def container_name(version = nil) [ container_prefix, version || config.version ].compact.join("-") end @@ -180,22 +119,22 @@ class Kamal::Configuration::Role 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 @@ class Kamal::Configuration::Role end end - def traefik_labels - if running_traefik? - { - # Setting a service property ensures that the generated service name will be consistent between versions - "traefik.http.services.#{traefik_service}.loadbalancer.server.scheme" => "http", - - "traefik.http.routers.#{traefik_service}.rule" => "PathPrefix(`/`)", - "traefik.http.routers.#{traefik_service}.priority" => "2", - "traefik.http.middlewares.#{traefik_service}-retry.retry.attempts" => "5", - "traefik.http.middlewares.#{traefik_service}-retry.retry.initialinterval" => "500ms", - "traefik.http.routers.#{traefik_service}.middlewares" => "#{traefik_service}-retry@docker" - } - else - {} - end - end - - def traefik_service - container_prefix - end - def custom_labels Hash.new.tap do |labels| labels.merge!(config.labels) if config.labels.present? diff --git a/lib/kamal/configuration/traefik.rb b/lib/kamal/configuration/traefik.rb deleted file mode 100644 index 45d8bac5..00000000 --- a/lib/kamal/configuration/traefik.rb +++ /dev/null @@ -1,78 +0,0 @@ -class Kamal::Configuration::Traefik - delegate :argumentize, to: Kamal::Utils - - DEFAULT_IMAGE = "traefik:v2.10" - CONTAINER_PORT = 80 - DEFAULT_ARGS = { - "log.level" => "DEBUG" - } - DEFAULT_LABELS = { - # These ensure we serve a 502 rather than a 404 if no containers are available - "traefik.http.routers.catchall.entryPoints" => "http", - "traefik.http.routers.catchall.rule" => "PathPrefix(`/`)", - "traefik.http.routers.catchall.service" => "unavailable", - "traefik.http.routers.catchall.priority" => 1, - "traefik.http.services.unavailable.loadbalancer.server.port" => "0" - } - - include Kamal::Configuration::Validation - - attr_reader :config, :traefik_config - - def initialize(config:) - @config = config - @traefik_config = config.raw_config.traefik || {} - validate! traefik_config - end - - def publish? - traefik_config["publish"] != false - end - - def labels - DEFAULT_LABELS.merge(traefik_config["labels"] || {}) - end - - def env - Kamal::Configuration::Env.new \ - config: traefik_config.fetch("env", {}), - secrets: config.secrets, - context: "traefik/env" - end - - def host_port - traefik_config.fetch("host_port", CONTAINER_PORT) - end - - def options - traefik_config.fetch("options", {}) - end - - def port - "#{host_port}:#{CONTAINER_PORT}" - end - - def args - DEFAULT_ARGS.merge(traefik_config.fetch("args", {})) - end - - def image - traefik_config.fetch("image", DEFAULT_IMAGE) - end - - def env_args - [ *env.clear_args, *argumentize("--env-file", secrets_path) ] - end - - def env_directory - File.join(config.env_directory, "traefik") - end - - def secrets_io - env.secrets_io - end - - def secrets_path - File.join(config.env_directory, "traefik", "traefik.env") - end -end diff --git a/lib/kamal/configuration/validator/proxy.rb b/lib/kamal/configuration/validator/proxy.rb new file mode 100644 index 00000000..a4ee19bf --- /dev/null +++ b/lib/kamal/configuration/validator/proxy.rb @@ -0,0 +1,9 @@ +class Kamal::Configuration::Validator::Proxy < Kamal::Configuration::Validator + def validate! + super + + if config["host"].blank? && config["ssl"] + error "Must set a host to enable automatic SSL" + end + end +end diff --git a/lib/kamal/utils.rb b/lib/kamal/utils.rb index 266d6a96..ab8dd50e 100644 --- a/lib/kamal/utils.rb +++ b/lib/kamal/utils.rb @@ -101,4 +101,8 @@ module Kamal::Utils arch end end + + def older_version?(version, other_version) + Gem::Version.new(version.delete_prefix("v")) < Gem::Version.new(other_version.delete_prefix("v")) + end end diff --git a/test/cli/accessory_test.rb b/test/cli/accessory_test.rb index 0e3abc46..5bb8762a 100644 --- a/test/cli/accessory_test.rb +++ b/test/cli/accessory_test.rb @@ -15,7 +15,7 @@ class CliAccessoryTest < CliTestCase run_command("boot", "mysql").tap do |output| assert_match /docker login.*on 1.1.1.3/, output - assert_match "docker run --name app-mysql --detach --restart unless-stopped --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 85a966fd..f3bd9fe5 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 @@ class CliAppTest < CliTestCase Object.any_instance.stubs(:sleep) SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version - - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'") - .returns("running") # health check - - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'") - .returns("unhealthy") # health check end end diff --git a/test/cli/cli_test_case.rb b/test/cli/cli_test_case.rb index 3f3e9294..5a2bb76f 100644 --- a/test/cli/cli_test_case.rb +++ b/test/cli/cli_test_case.rb @@ -29,13 +29,13 @@ class CliTestCase < ActiveSupport::TestCase def stub_setup SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |*args| args == [ :mkdir, "-p", ".kamal" ] } + .with { |*args| args == [ :mkdir, "-p", ".kamal/apps/app" ] } SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |arg1, arg2, arg3| arg1 == :mkdir && arg2 == "-p" && arg3 == ".kamal/locks" } + .with { |arg1, arg2, arg3| arg1 == :mkdir && arg2 == "-p" && arg3 == ".kamal/lock-app" } SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |arg1, arg2| arg1 == :mkdir && arg2 == ".kamal/locks/app" } + .with { |arg1, arg2| arg1 == :mkdir && arg2 == ".kamal/lock-app" } SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |arg1, arg2| arg1 == :rm && arg2 == ".kamal/locks/app/details" } + .with { |arg1, arg2| arg1 == :rm && arg2 == ".kamal/lock-app/details" } SSHKit::Backend::Abstract.any_instance.stubs(:execute) .with(:docker, :buildx, :inspect, "kamal-local-docker-container") end diff --git a/test/cli/lock_test.rb b/test/cli/lock_test.rb index f6874d49..66482153 100644 --- a/test/cli/lock_test.rb +++ b/test/cli/lock_test.rb @@ -3,7 +3,7 @@ require_relative "cli_test_case" class CliLockTest < CliTestCase test "status" do run_command("status").tap do |output| - assert_match "Running /usr/bin/env stat .kamal/locks/app > /dev/null && cat .kamal/locks/app/details | base64 -d on 1.1.1.1", output + assert_match "Running /usr/bin/env stat .kamal/lock-app > /dev/null && cat .kamal/lock-app/details | base64 -d on 1.1.1.1", output end end diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index 2115f418..607fee21 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" ] } + .with { |*args| args == [ :mkdir, "-p", ".kamal/apps/app" ] } SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |*args| args == [ :mkdir, "-p", ".kamal/locks" ] } - - SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |*arg| arg[0..1] == [ :mkdir, ".kamal/locks/app" ] } + .with { |*arg| arg[0..1] == [ :mkdir, ".kamal/lock-app" ] } .raises(SocketError, "getaddrinfo: nodename nor servname provided, or not known") SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) @@ -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 00000000..2a0834fc --- /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 33723f13..bb4ced51 100644 --- a/test/cli/prune_test.rb +++ b/test/cli/prune_test.rb @@ -18,12 +18,10 @@ class CliPruneTest < CliTestCase test "containers" do run_command("containers").tap do |output| assert_match /docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +6 | while read container_id; do docker rm $container_id; done on 1.1.1.\d/, output - assert_match /docker container prune --force --filter label=service=healthcheck-app on 1.1.1.\d/, output end run_command("containers", "--retain", "10").tap do |output| assert_match /docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +11 | while read container_id; do docker rm $container_id; done on 1.1.1.\d/, output - assert_match /docker container prune --force --filter label=service=healthcheck-app on 1.1.1.\d/, output end assert_raises(RuntimeError, "retain must be at least 1") do diff --git a/test/cli/traefik_test.rb b/test/cli/traefik_test.rb deleted file mode 100644 index 29171150..00000000 --- a/test/cli/traefik_test.rb +++ /dev/null @@ -1,110 +0,0 @@ -require_relative "cli_test_case" - -class CliTraefikTest < CliTestCase - test "boot" do - run_command("boot").tap do |output| - assert_match "docker login", output - assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Configuration::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output - end - end - - test "reboot" do - Kamal::Commands::Registry.any_instance.expects(:login).twice - - run_command("reboot", "-y").tap do |output| - assert_match "docker container stop traefik", output - assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik", output - assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Configuration::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output - end - end - - test "reboot --rolling" do - Object.any_instance.stubs(:sleep) - - run_command("reboot", "--rolling", "-y").tap do |output| - assert_match "Running docker container prune --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output - end - end - - test "start" do - run_command("start").tap do |output| - assert_match "docker container start traefik", output - end - end - - test "stop" do - run_command("stop").tap do |output| - assert_match "docker container stop traefik", output - end - end - - test "restart" do - Kamal::Cli::Traefik.any_instance.expects(:stop) - Kamal::Cli::Traefik.any_instance.expects(:start) - - run_command("restart") - end - - test "details" do - run_command("details").tap do |output| - assert_match "docker ps --filter name=^traefik$", output - end - end - - test "logs" do - SSHKit::Backend::Abstract.any_instance.stubs(:capture) - .with(:docker, :logs, "traefik", " --tail 100", "--timestamps", "2>&1") - .returns("Log entry") - - run_command("logs").tap do |output| - assert_match "Traefik Host: 1.1.1.1", output - assert_match "Log entry", output - end - end - - test "logs with follow" do - SSHKit::Backend::Abstract.any_instance.stubs(:exec) - .with("ssh -t root@1.1.1.1 -p 22 'docker logs traefik --timestamps --tail 10 --follow 2>&1'") - - assert_match "docker logs traefik --timestamps --tail 10 --follow", run_command("logs", "--follow") - end - - test "logs with follow and grep" do - SSHKit::Backend::Abstract.any_instance.stubs(:exec) - .with("ssh -t root@1.1.1.1 -p 22 'docker logs traefik --timestamps --tail 10 --follow 2>&1 | grep \"hey\"'") - - assert_match "docker logs traefik --timestamps --tail 10 --follow 2>&1 | grep \"hey\"", run_command("logs", "--follow", "--grep", "hey") - end - - test "logs with follow, grep, and grep options" do - SSHKit::Backend::Abstract.any_instance.stubs(:exec) - .with("ssh -t root@1.1.1.1 -p 22 'docker logs traefik --timestamps --tail 10 --follow 2>&1 | grep \"hey\" -C 2'") - - assert_match "docker logs traefik --timestamps --tail 10 --follow 2>&1 | grep \"hey\" -C 2", run_command("logs", "--follow", "--grep", "hey", "--grep-options", "-C 2") - end - - test "remove" do - Kamal::Cli::Traefik.any_instance.expects(:stop) - Kamal::Cli::Traefik.any_instance.expects(:remove_container) - Kamal::Cli::Traefik.any_instance.expects(:remove_image) - - run_command("remove") - end - - test "remove_container" do - run_command("remove_container").tap do |output| - assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik", output - end - end - - test "remove_image" do - run_command("remove_image").tap do |output| - assert_match "docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik", output - end - end - - private - def run_command(*command) - stdouted { Kamal::Cli::Traefik.start([ *command, "-c", "test/fixtures/deploy_with_accessories.yml" ]) } - end -end diff --git a/test/commander_test.rb b/test/commander_test.rb index c2b78b21..54031e80 100644 --- a/test/commander_test.rb +++ b/test/commander_test.rb @@ -136,18 +136,18 @@ class CommanderTest < ActiveSupport::TestCase assert_equal [ "1.1.1.3", "1.1.1.4", "1.1.1.1", "1.1.1.2" ], @kamal.hosts end - test "traefik hosts should observe filtered roles" do - configure_with(:deploy_with_multiple_traefik_roles) + test "proxy hosts should observe filtered roles" do + configure_with(:deploy_with_multiple_proxy_roles) @kamal.specific_roles = [ "web_tokyo" ] - assert_equal [ "1.1.1.3", "1.1.1.4" ], @kamal.traefik_hosts + assert_equal [ "1.1.1.3", "1.1.1.4" ], @kamal.proxy_hosts end - test "traefik hosts should observe filtered hosts" do - configure_with(:deploy_with_multiple_traefik_roles) + test "proxy hosts should observe filtered hosts" do + configure_with(:deploy_with_multiple_proxy_roles) @kamal.specific_hosts = [ "1.1.1.2" ] - assert_equal [ "1.1.1.2" ], @kamal.traefik_hosts + assert_equal [ "1.1.1.2" ], @kamal.proxy_hosts end private diff --git a/test/commands/accessory_test.rb b/test/commands/accessory_test.rb index 23d304da..19fe745d 100644 --- a/test/commands/accessory_test.rb +++ b/test/commands/accessory_test.rb @@ -51,15 +51,15 @@ class CommandsAccessoryTest < ActiveSupport::TestCase test "run" do assert_equal \ - "docker run --name app-mysql --detach --restart unless-stopped --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 e385764e..44062624 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -3,7 +3,6 @@ require "test_helper" class CommandsAppTest < ActiveSupport::TestCase setup do setup_test_secrets("secrets" => "RAILS_MASTER_KEY=456") - Kamal::Configuration.any_instance.stubs(:run_id).returns("12345678901234567890123456789012") @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], env: { "secret" => [ "RAILS_MASTER_KEY" ] }, builder: { "arch" => "amd64" } } end @@ -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 f6234d6a..dc2afc5a 100644 --- a/test/commands/hook_test.rb +++ b/test/commands/hook_test.rb @@ -8,7 +8,7 @@ class CommandsHookTest < ActiveSupport::TestCase @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], - builder: { "arch" => "amd64" }, traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } + builder: { "arch" => "amd64" } } @performer = Kamal::Git.email.presence || `whoami`.chomp diff --git a/test/commands/lock_test.rb b/test/commands/lock_test.rb index 02871922..0ad0ba46 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 00000000..349e1a1b --- /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 430a13db..50e852bc 100644 --- a/test/commands/prune_test.rb +++ b/test/commands/prune_test.rb @@ -4,7 +4,7 @@ class CommandsPruneTest < ActiveSupport::TestCase setup do @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], - builder: { "arch" => "amd64" }, traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } + builder: { "arch" => "amd64" } } end @@ -30,12 +30,6 @@ class CommandsPruneTest < ActiveSupport::TestCase new_command.app_containers(retain: 3).join(" ") end - test "healthcheck containers" do - assert_equal \ - "docker container prune --force --filter label=service=healthcheck-app", - new_command.healthcheck_containers.join(" ") - end - private def new_command Kamal::Commands::Prune.new(Kamal::Configuration.new(@config, version: "123")) diff --git a/test/commands/server_test.rb b/test/commands/server_test.rb index 8c465fd9..648821b4 100644 --- a/test/commands/server_test.rb +++ b/test/commands/server_test.rb @@ -4,7 +4,7 @@ class CommandsServerTest < ActiveSupport::TestCase setup do @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], - builder: { "arch" => "amd64" }, traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } + builder: { "arch" => "amd64" } } end @@ -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 b13e3700..00000000 --- a/test/commands/traefik_test.rb +++ /dev/null @@ -1,195 +0,0 @@ -require "test_helper" - -class CommandsTraefikTest < ActiveSupport::TestCase - setup do - @image = "traefik:test" - - @config = { - service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], builder: { "arch" => "amd64" }, - traefik: { "image" => @image, "args" => { "accesslog.format" => "json", "api.insecure" => true, "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } - } - - setup_test_secrets("secrets" => "EXAMPLE_API_KEY=456") - end - - teardown do - teardown_test_secrets - end - - test "run" do - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - - @config[:traefik]["host_port"] = "8080" - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 8080:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - - @config[:traefik]["publish"] = false - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - end - - test "run with ports configured" do - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - - @config[:traefik]["options"] = { "publish" => %w[9000:9000 9001:9001] } - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --publish \"9000:9000\" --publish \"9001:9001\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - end - - test "run with volumes configured" do - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - - @config[:traefik]["options"] = { "volume" => %w[./letsencrypt/acme.json:/letsencrypt/acme.json] } - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - end - - test "run with several options configured" do - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - - @config[:traefik]["options"] = { "volume" => %w[./letsencrypt/acme.json:/letsencrypt/acme.json], "publish" => %w[8080:8080], "memory" => "512m" } - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" --publish \"8080:8080\" --memory \"512m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - end - - test "run with labels configured" do - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - - @config[:traefik]["labels"] = { "traefik.http.routers.dashboard.service" => "api@internal", "traefik.http.routers.dashboard.middlewares" => "auth" } - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --label traefik.http.routers.dashboard.service=\"api@internal\" --label traefik.http.routers.dashboard.middlewares=\"auth\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - end - - test "run with env configured" do - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - - @config[:traefik]["env"] = { "EXAMPLE_API_KEY" => "456" } - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env EXAMPLE_API_KEY=\"456\" --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - end - - test "run without configuration" do - @config.delete(:traefik) - - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Configuration::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", - new_command.run.join(" ") - end - - test "run with logging config" do - @config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } } - - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - end - - test "run with default args overriden" do - @config[:traefik]["args"]["log.level"] = "ERROR" - - assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"ERROR\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", - new_command.run.join(" ") - end - - test "run with args array" do - @config[:traefik]["args"] = { "entrypoints.web.forwardedheaders.trustedips" => %w[ 127.0.0.1 127.0.0.2 ] } - assert_equal "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" traefik:test --providers.docker --log.level=\"DEBUG\" --entrypoints.web.forwardedheaders.trustedips=\"127.0.0.1\" --entrypoints.web.forwardedheaders.trustedips=\"127.0.0.2\"", new_command.run.join(" ") - end - - test "traefik start" do - assert_equal \ - "docker container start traefik", - new_command.start.join(" ") - end - - test "traefik stop" do - assert_equal \ - "docker container stop traefik", - new_command.stop.join(" ") - end - - test "traefik info" do - assert_equal \ - "docker ps --filter name=^traefik$", - new_command.info.join(" ") - end - - test "traefik logs" do - assert_equal \ - "docker logs traefik --timestamps 2>&1", - new_command.logs.join(" ") - end - - test "traefik logs since 2h" do - assert_equal \ - "docker logs traefik --since 2h --timestamps 2>&1", - new_command.logs(since: "2h").join(" ") - end - - test "traefik logs last 10 lines" do - assert_equal \ - "docker logs traefik --tail 10 --timestamps 2>&1", - new_command.logs(lines: 10).join(" ") - end - - test "traefik logs with grep hello!" do - assert_equal \ - "docker logs traefik --timestamps 2>&1 | grep 'hello!'", - new_command.logs(grep: "hello!").join(" ") - end - - test "traefik logs with grep hello! and grep options" do - assert_equal \ - "docker logs traefik --timestamps 2>&1 | grep 'hello!' -C 2", - new_command.logs(grep: "hello!", grep_options: "-C 2").join(" ") - end - - test "traefik remove container" do - assert_equal \ - "docker container prune --force --filter label=org.opencontainers.image.title=Traefik", - new_command.remove_container.join(" ") - end - - test "traefik remove image" do - assert_equal \ - "docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik", - new_command.remove_image.join(" ") - end - - test "traefik follow logs" do - assert_equal \ - "ssh -t root@1.1.1.1 -p 22 'docker logs traefik --timestamps --tail 10 --follow 2>&1'", - new_command.follow_logs(host: @config[:servers].first) - end - - test "traefik follow logs with grep hello!" do - assert_equal \ - "ssh -t root@1.1.1.1 -p 22 'docker logs traefik --timestamps --tail 10 --follow 2>&1 | grep \"hello!\"'", - new_command.follow_logs(host: @config[:servers].first, grep: "hello!") - end - - private - def new_command - Kamal::Commands::Traefik.new(Kamal::Configuration.new(@config, version: "123")) - end -end diff --git a/test/configuration/accessory_test.rb b/test/configuration/accessory_test.rb index acfe991f..2615dab6 100644 --- a/test/configuration/accessory_test.rb +++ b/test/configuration/accessory_test.rb @@ -119,9 +119,9 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase with_test_secrets("secrets" => "MYSQL_ROOT_PASSWORD=secret123") do config = Kamal::Configuration.new(@deploy) - assert_equal [ "--env", "MYSQL_ROOT_HOST=\"%\"", "--env-file", ".kamal/env/accessories/app-mysql.env" ], config.accessory(:mysql).env_args.map(&:to_s) + assert_equal [ "--env", "MYSQL_ROOT_HOST=\"%\"", "--env-file", ".kamal/apps/app/env/accessories/mysql.env" ], config.accessory(:mysql).env_args.map(&:to_s) assert_equal "MYSQL_ROOT_PASSWORD=secret123\n", config.accessory(:mysql).secrets_io.string - assert_equal [ "--env", "SOMETHING=\"else\"", "--env-file", ".kamal/env/accessories/app-redis.env" ], @config.accessory(:redis).env_args + assert_equal [ "--env", "SOMETHING=\"else\"", "--env-file", ".kamal/apps/app/env/accessories/redis.env" ], @config.accessory(:redis).env_args assert_equal "\n", config.accessory(:redis).secrets_io.string end end diff --git a/test/configuration/proxy_test.rb b/test/configuration/proxy_test.rb new file mode 100644 index 00000000..3aa3f85e --- /dev/null +++ b/test/configuration/proxy_test.rb @@ -0,0 +1,25 @@ +require "test_helper" + +class ConfigurationEnvTest < ActiveSupport::TestCase + setup do + @deploy = { + service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, + builder: { "arch" => "amd64" }, servers: [ "1.1.1.1" ] + } + end + + test "ssl with host" do + @deploy[:proxy] = { "ssl" => true, "host" => "example.com" } + assert_equal true, config.proxy.ssl? + end + + test "ssl with no host" do + @deploy[:proxy] = { "ssl" => true } + assert_raises(Kamal::ConfigurationError) { config.proxy.ssl? } + end + + private + def config + Kamal::Configuration.new(@deploy) + end +end diff --git a/test/configuration/role_test.rb b/test/configuration/role_test.rb index c0b643bf..57719077 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 ac409e84..d8ac3e3b 100644 --- a/test/configuration/validation_test.rb +++ b/test/configuration/validation_test.rb @@ -22,7 +22,7 @@ class ConfigurationValidationTest < ActiveSupport::TestCase assert_error "servers: should be an array or a hash", servers: "foo" - [ :labels, :registry, :accessories, :env, :ssh, :sshkit, :builder, :traefik, :boot, :healthcheck, :logging ].each do |key| + [ :labels, :registry, :accessories, :env, :ssh, :sshkit, :builder, :proxy, :boot, :logging ].each do |key| assert_error "#{key}: should be a hash", **{ key =>[] } end end diff --git a/test/configuration_test.rb b/test/configuration_test.rb index aa78dda9..81dbb5ac 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 aa52f5ed..6bc2f837 100644 --- a/test/fixtures/deploy_primary_web_role_override.yml +++ b/test/fixtures/deploy_primary_web_role_override.yml @@ -2,12 +2,12 @@ service: app image: dhh/app servers: web_chicago: - traefik: true + proxy: true hosts: - 1.1.1.1 - 1.1.1.2 web_tokyo: - traefik: true + proxy: true hosts: - 1.1.1.3 - 1.1.1.4 diff --git a/test/fixtures/deploy_with_extensions.yml b/test/fixtures/deploy_with_extensions.yml index 4a5f934a..c45104de 100644 --- a/test/fixtures/deploy_with_extensions.yml +++ b/test/fixtures/deploy_with_extensions.yml @@ -1,6 +1,6 @@ x-web: &web - traefik: true + proxy: true service: app image: dhh/app diff --git a/test/fixtures/deploy_with_multiple_traefik_roles.yml b/test/fixtures/deploy_with_multiple_proxy_roles.yml similarity index 93% rename from test/fixtures/deploy_with_multiple_traefik_roles.yml rename to test/fixtures/deploy_with_multiple_proxy_roles.yml index a1270583..6b785a6a 100644 --- a/test/fixtures/deploy_with_multiple_traefik_roles.yml +++ b/test/fixtures/deploy_with_multiple_proxy_roles.yml @@ -8,14 +8,14 @@ servers: - 1.1.1.2 env: ROLE: "web" - traefik: true + proxy: true web_tokyo: hosts: - 1.1.1.3 - 1.1.1.4 env: ROLE: "web" - traefik: true + proxy: true workers: cmd: bin/jobs hosts: diff --git a/test/fixtures/deploy_with_proxy.yml b/test/fixtures/deploy_with_proxy.yml new file mode 100644 index 00000000..e91e9657 --- /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 1eb8cc5c..d831a5b4 100644 --- a/test/fixtures/deploy_with_roles.yml +++ b/test/fixtures/deploy_with_roles.yml @@ -16,3 +16,4 @@ registry: password: pw builder: arch: amd64 +readiness_timeout: 1 diff --git a/test/fixtures/deploy_workers_only.yml b/test/fixtures/deploy_workers_only.yml index d83adeeb..6f6568c3 100644 --- a/test/fixtures/deploy_workers_only.yml +++ b/test/fixtures/deploy_workers_only.yml @@ -2,7 +2,7 @@ service: app image: dhh/app servers: workers: - traefik: false + proxy: false hosts: - 1.1.1.1 - 1.1.1.2 diff --git a/test/integration/app_test.rb b/test/integration/app_test.rb index b7dcdc34..40a896b1 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 77f0ff96..964f1d0b 100644 --- a/test/integration/broken_deploy_test.rb +++ b/test/integration/broken_deploy_test.rb @@ -27,6 +27,5 @@ class BrokenDeployTest < IntegrationTest assert_match /First web container is unhealthy on vm[12], not booting any other roles/, output assert_match "First web container is unhealthy, not booting workers on vm3", output assert_match "nginx: [emerg] unexpected end of file, expecting \";\" or \"}\" in /etc/nginx/conf.d/default.conf:2", output - assert_match 'ERROR {"Status":"unhealthy","FailingStreak":0,"Log":[]}', output end end diff --git a/test/integration/docker/deployer/app/.kamal/hooks/post-proxy-reboot b/test/integration/docker/deployer/app/.kamal/hooks/post-proxy-reboot new file mode 100755 index 00000000..fb47d3cc --- /dev/null +++ b/test/integration/docker/deployer/app/.kamal/hooks/post-proxy-reboot @@ -0,0 +1,3 @@ +#!/bin/sh +echo "Rebooted kamal-proxy on ${KAMAL_HOSTS}" +mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/post-proxy-reboot diff --git a/test/integration/docker/deployer/app/.kamal/hooks/post-traefik-reboot b/test/integration/docker/deployer/app/.kamal/hooks/post-traefik-reboot deleted file mode 100755 index 598ddb63..00000000 --- a/test/integration/docker/deployer/app/.kamal/hooks/post-traefik-reboot +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -echo "Rebooted Traefik on ${KAMAL_HOSTS}" -mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/post-traefik-reboot diff --git a/test/integration/docker/deployer/app/.kamal/hooks/pre-proxy-reboot b/test/integration/docker/deployer/app/.kamal/hooks/pre-proxy-reboot new file mode 100755 index 00000000..65a70a00 --- /dev/null +++ b/test/integration/docker/deployer/app/.kamal/hooks/pre-proxy-reboot @@ -0,0 +1,3 @@ +#!/bin/sh +echo "Rebooting Traefik on ${KAMAL_HOSTS}..." +mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-proxy-reboot diff --git a/test/integration/docker/deployer/app/.kamal/hooks/pre-traefik-reboot b/test/integration/docker/deployer/app/.kamal/hooks/pre-traefik-reboot deleted file mode 100755 index 81269d24..00000000 --- a/test/integration/docker/deployer/app/.kamal/hooks/pre-traefik-reboot +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -echo "Rebooting Traefik on ${KAMAL_HOSTS}..." -mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-traefik-reboot diff --git a/test/integration/docker/deployer/app/Dockerfile b/test/integration/docker/deployer/app/Dockerfile index dc270aa9..0e6237df 100644 --- a/test/integration/docker/deployer/app/Dockerfile +++ b/test/integration/docker/deployer/app/Dockerfile @@ -6,4 +6,4 @@ ARG COMMIT_SHA RUN echo $COMMIT_SHA > /usr/share/nginx/html/version RUN mkdir -p /usr/share/nginx/html/versions && echo "version" > /usr/share/nginx/html/versions/$COMMIT_SHA RUN mkdir -p /usr/share/nginx/html/versions && echo "hidden" > /usr/share/nginx/html/versions/.hidden - +RUN echo "Up!" > /usr/share/nginx/html/up diff --git a/test/integration/docker/deployer/app/config/deploy.yml b/test/integration/docker/deployer/app/config/deploy.yml index a85412ad..5e999402 100644 --- a/test/integration/docker/deployer/app/config/deploy.yml +++ b/test/integration/docker/deployer/app/config/deploy.yml @@ -20,6 +20,8 @@ env: secret: - SECRET_TAG asset_path: /usr/share/nginx/html/versions +proxy: + deploy_timeout: 2s registry: server: registry:4443 @@ -30,14 +32,6 @@ builder: arch: <%= Kamal::Utils.docker_arch %> args: COMMIT_SHA: <%= `git rev-parse HEAD` %> -healthcheck: - cmd: wget -qO- http://localhost > /dev/null || exit 1 - max_attempts: 3 -traefik: - args: - accesslog: true - accesslog.format: json - image: registry:4443/traefik:v2.10 accessories: busybox: service: custom-busybox diff --git a/test/integration/docker/deployer/app_with_roles/.kamal/hooks/post-proxy-reboot b/test/integration/docker/deployer/app_with_roles/.kamal/hooks/post-proxy-reboot new file mode 100755 index 00000000..fb47d3cc --- /dev/null +++ b/test/integration/docker/deployer/app_with_roles/.kamal/hooks/post-proxy-reboot @@ -0,0 +1,3 @@ +#!/bin/sh +echo "Rebooted kamal-proxy on ${KAMAL_HOSTS}" +mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/post-proxy-reboot diff --git a/test/integration/docker/deployer/app_with_roles/.kamal/hooks/post-traefik-reboot b/test/integration/docker/deployer/app_with_roles/.kamal/hooks/post-traefik-reboot deleted file mode 100755 index 598ddb63..00000000 --- a/test/integration/docker/deployer/app_with_roles/.kamal/hooks/post-traefik-reboot +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -echo "Rebooted Traefik on ${KAMAL_HOSTS}" -mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/post-traefik-reboot diff --git a/test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-proxy-reboot b/test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-proxy-reboot new file mode 100755 index 00000000..6413b6ef --- /dev/null +++ b/test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-proxy-reboot @@ -0,0 +1,3 @@ +#!/bin/sh +echo "Rebooting kamal-proxy on ${KAMAL_HOSTS}..." +mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-proxy-reboot diff --git a/test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-traefik-reboot b/test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-traefik-reboot deleted file mode 100755 index 81269d24..00000000 --- a/test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-traefik-reboot +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -echo "Rebooting Traefik on ${KAMAL_HOSTS}..." -mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-traefik-reboot diff --git a/test/integration/docker/deployer/app_with_roles/Dockerfile b/test/integration/docker/deployer/app_with_roles/Dockerfile index dc270aa9..0e6237df 100644 --- a/test/integration/docker/deployer/app_with_roles/Dockerfile +++ b/test/integration/docker/deployer/app_with_roles/Dockerfile @@ -6,4 +6,4 @@ ARG COMMIT_SHA RUN echo $COMMIT_SHA > /usr/share/nginx/html/version RUN mkdir -p /usr/share/nginx/html/versions && echo "version" > /usr/share/nginx/html/versions/$COMMIT_SHA RUN mkdir -p /usr/share/nginx/html/versions && echo "hidden" > /usr/share/nginx/html/versions/.hidden - +RUN echo "Up!" > /usr/share/nginx/html/up diff --git a/test/integration/docker/deployer/app_with_roles/config/deploy.yml b/test/integration/docker/deployer/app_with_roles/config/deploy.yml index 3b942665..835b4ffd 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 0cd511d9..4867519e 100755 --- a/test/integration/docker/deployer/setup.sh +++ b/test/integration/docker/deployer/setup.sh @@ -19,7 +19,6 @@ push_image_to_registry_4443() { install_kamal push_image_to_registry_4443 nginx 1-alpine-slim -push_image_to_registry_4443 traefik v2.10 push_image_to_registry_4443 busybox 1.36.0 # .ssh is on a shared volume that persists between runs. Clean it up as the diff --git a/test/integration/integration_test.rb b/test/integration/integration_test.rb index ea445d9e..5c675da6 100644 --- a/test/integration/integration_test.rb +++ b/test/integration/integration_test.rb @@ -31,7 +31,7 @@ class IntegrationTest < ActiveSupport::TestCase succeeded = system("cd test/integration && #{command}") end - raise "Command `#{command}` failed with error code `#{$?}`" if !succeeded && raise_on_error + raise "Command `#{command}` failed with error code `#{$?}`, and output:\n#{result}" if !succeeded && raise_on_error result end @@ -101,8 +101,8 @@ class IntegrationTest < ActiveSupport::TestCase def assert_200(response) code = response.code if code != "200" - puts "Got response code #{code}, here are the traefik logs:" - kamal :traefik, :logs + puts "Got response code #{code}, here are the proxy logs:" + kamal :proxy, :logs puts "And here are the load balancer logs" docker_compose :logs, :load_balancer puts "Tried to get the response code again and got #{app_response.code}" @@ -129,8 +129,8 @@ class IntegrationTest < ActiveSupport::TestCase def debug_response_code(app_response, expected_code) code = app_response.code if code != expected_code - puts "Got response code #{code}, here are the traefik logs:" - kamal :traefik, :logs + puts "Got response code #{code}, here are the proxy logs:" + kamal :proxy, :logs puts "And here are the load balancer logs" docker_compose :logs, :load_balancer puts "Tried to get the response code again and got #{app_response.code}" @@ -148,4 +148,16 @@ class IntegrationTest < ActiveSupport::TestCase def container_running?(host:, name:) docker_compose("exec #{host} docker ps --filter=name=#{name} | tail -n+2", capture: true).strip.present? end + + def assert_app_directory_removed + assert_directory_removed("./kamal/apps/#{@app}") + end + + def assert_proxy_directory_removed + assert_directory_removed("./kamal/proxy") + end + + def assert_directory_removed(directory) + assert docker_compose("exec vm1 ls #{directory} | wc -l", capture: true).strip == "0" + end end diff --git a/test/integration/main_test.rb b/test/integration/main_test.rb index b58aeeb9..5a4eb91c 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 00000000..f9c7133b --- /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 48f9ea02..00000000 --- a/test/integration/traefik_test.rb +++ /dev/null @@ -1,61 +0,0 @@ -require_relative "integration_test" - -class TraefikTest < IntegrationTest - test "boot, reboot, stop, start, restart, logs, remove" do - kamal :traefik, :boot - assert_traefik_running - - output = kamal :traefik, :reboot, "-y", "--verbose", capture: true - assert_traefik_running - assert_hooks_ran "pre-traefik-reboot", "post-traefik-reboot" - assert_match /Rebooting Traefik on vm1,vm2.../, output - assert_match /Rebooted Traefik on vm1,vm2/, output - - output = kamal :traefik, :reboot, "--rolling", "-y", "--verbose", capture: true - assert_traefik_running - assert_hooks_ran "pre-traefik-reboot", "post-traefik-reboot" - assert_match /Rebooting Traefik on vm1.../, output - assert_match /Rebooted Traefik on vm1/, output - assert_match /Rebooting Traefik on vm2.../, output - assert_match /Rebooted Traefik on vm2/, output - - kamal :traefik, :boot - assert_traefik_running - - # Check booting when booted doesn't raise an error - kamal :traefik, :stop - assert_traefik_not_running - - # Check booting when stopped works - kamal :traefik, :boot - assert_traefik_running - - kamal :traefik, :stop - assert_traefik_not_running - - kamal :traefik, :start - assert_traefik_running - - kamal :traefik, :restart - assert_traefik_running - - logs = kamal :traefik, :logs, capture: true - assert_match /Traefik version [\d.]+ built on/, logs - - kamal :traefik, :remove - assert_traefik_not_running - end - - private - def assert_traefik_running - assert_match /traefik:v2.10 "\/entrypoint.sh/, traefik_details - end - - def assert_traefik_not_running - assert_no_match /traefik:v2.10 "\/entrypoint.sh/, traefik_details - end - - def traefik_details - kamal :traefik, :details, capture: true - end -end