From b6cd4f8070dfc50ec8e252c4bb9225cf838a09fe Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 23 Sep 2024 14:41:31 -0700 Subject: [PATCH 01/11] Bring default templates up to par with what Rails generates --- lib/kamal/cli/templates/deploy.yml | 50 +++++++++++++++++++----------- lib/kamal/cli/templates/secrets | 5 +-- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/lib/kamal/cli/templates/deploy.yml b/lib/kamal/cli/templates/deploy.yml index 8ecf5d04..2a2a9ffd 100644 --- a/lib/kamal/cli/templates/deploy.yml +++ b/lib/kamal/cli/templates/deploy.yml @@ -6,7 +6,18 @@ image: user/my-app # Deploy to these servers. servers: - - 192.168.0.1 + web: + - 192.168.0.1 + # job: + # hosts: + # - 192.168.0.1 + # cmd: bin/jobs + +# Enable SSL auto certification via Let's Encrypt (and allow for multiple apps on one server). +# Set ssl: false if using something like Cloudflare to terminate SSL (but keep host!). +proxy: + ssl: true + host: app.example.com # Credentials for your image host. registry: @@ -18,23 +29,35 @@ registry: password: - KAMAL_REGISTRY_PASSWORD -# Configure builder setup. -builder: - arch: amd64 - -# Inject ENV variables into containers (secrets come from .env). -# Remember to run `kamal env push` after making changes! +# Inject ENV variables into containers (secrets come from .kamal/secrets). +# # env: # clear: # DB_HOST: 192.168.0.2 # secret: # - RAILS_MASTER_KEY +# Aliases are triggered with "bin/kamal ". You can overwrite arguments on invocation: +# "bin/kamal logs -r job" will tail logs from the first server in the job section. +aliases: + shell: app exec --interactive --reuse "bash" + +# Use a persistent storage volume. +# +# volumes: +# - "app_storage:/rails/storage" + +# Configure builder setup. +builder: + arch: amd64 + # Use a different ssh user than root +# # ssh: # user: app -# Use accessory services (secrets come from .env). +# Use accessory services (secrets come from .kamal/secrets). +# # accessories: # db: # image: mysql:8.0 @@ -61,12 +84,10 @@ builder: # hitting 404 on in-flight requests. Combines all files from new and old # version inside the asset_path. # -# If your app is using the Sprockets gem, ensure it sets `config.assets.manifest`. -# See https://github.com/basecamp/kamal/issues/626 for details -# # asset_path: /rails/public/assets # Configure rolling deploys by setting a wait time between batches of restarts. +# # boot: # limit: 10 # Can also specify as a percentage of total hosts, such as "25%" # wait: 2 @@ -74,11 +95,4 @@ builder: # Configure the role used to determine the primary_host. This host takes # deploy locks, runs health checks during the deploy, and follow logs, etc. # -# Caution: there's no support for role renaming yet, so be careful to cleanup -# the previous role on the deployed hosts. # primary_role: web - -# Controls if we abort when see a role with no hosts. Disabling this may be -# useful for more complex deploy configurations. -# -# allow_empty_roles: false diff --git a/lib/kamal/cli/templates/secrets b/lib/kamal/cli/templates/secrets index 91f4f239..b1366604 100644 --- a/lib/kamal/cli/templates/secrets +++ b/lib/kamal/cli/templates/secrets @@ -1,5 +1,6 @@ -# WARNING: Avoid adding secrets directly to this file -# If you must, then add `.kamal/secrets*` to your .gitignore file +# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets, +# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either +# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git. # Option 1: Read secrets from the environment KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD From 9dde2044804a3f1e13fcdf08234b085b3e66d678 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 23 Sep 2024 16:30:16 -0700 Subject: [PATCH 02/11] Rearange --- lib/kamal/cli/templates/deploy.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/kamal/cli/templates/deploy.yml b/lib/kamal/cli/templates/deploy.yml index 2a2a9ffd..cf8d752b 100644 --- a/lib/kamal/cli/templates/deploy.yml +++ b/lib/kamal/cli/templates/deploy.yml @@ -29,6 +29,10 @@ registry: password: - KAMAL_REGISTRY_PASSWORD +# Configure builder setup. +builder: + arch: amd64 + # Inject ENV variables into containers (secrets come from .kamal/secrets). # # env: @@ -39,18 +43,15 @@ registry: # Aliases are triggered with "bin/kamal ". You can overwrite arguments on invocation: # "bin/kamal logs -r job" will tail logs from the first server in the job section. -aliases: - shell: app exec --interactive --reuse "bash" +# +# aliases: +# shell: app exec --interactive --reuse "bash" # Use a persistent storage volume. # # volumes: # - "app_storage:/rails/storage" -# Configure builder setup. -builder: - arch: amd64 - # Use a different ssh user than root # # ssh: From 0f5dfa204f0f8e96023075f9ae19b82f66fb7609 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 23 Sep 2024 16:44:54 -0700 Subject: [PATCH 03/11] Rearrange one last time --- lib/kamal/cli/templates/deploy.yml | 43 +++++++++++++----------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/lib/kamal/cli/templates/deploy.yml b/lib/kamal/cli/templates/deploy.yml index cf8d752b..7eec6aa2 100644 --- a/lib/kamal/cli/templates/deploy.yml +++ b/lib/kamal/cli/templates/deploy.yml @@ -2,7 +2,7 @@ service: my-app # Name of the container image. -image: user/my-app +image: my-user/my-app # Deploy to these servers. servers: @@ -25,7 +25,7 @@ registry: # server: registry.digitalocean.com / ghcr.io / ... username: my-user - # Always use an access token rather than real password when possible. + # Always use an access token rather than real password (pulled from .kamal/secrets). password: - KAMAL_REGISTRY_PASSWORD @@ -47,16 +47,28 @@ builder: # aliases: # shell: app exec --interactive --reuse "bash" -# Use a persistent storage volume. -# -# volumes: -# - "app_storage:/rails/storage" - # Use a different ssh user than root # # ssh: # user: app +# Use a persistent storage volume. +# +# volumes: +# - "app_storage:/app/storage" + +# 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. +# +# asset_path: /app/public/assets + +# Configure rolling deploys by setting a wait time between batches of restarts. +# +# boot: +# limit: 10 # Can also specify as a percentage of total hosts, such as "25%" +# wait: 2 + # Use accessory services (secrets come from .kamal/secrets). # # accessories: @@ -80,20 +92,3 @@ builder: # port: 6379 # directories: # - data:/data - -# 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. -# -# asset_path: /rails/public/assets - -# Configure rolling deploys by setting a wait time between batches of restarts. -# -# boot: -# limit: 10 # Can also specify as a percentage of total hosts, such as "25%" -# wait: 2 - -# Configure the role used to determine the primary_host. This host takes -# deploy locks, runs health checks during the deploy, and follow logs, etc. -# -# primary_role: web From 4a138031195bebfcf12562692ef4540446042618 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 23 Sep 2024 16:48:07 -0700 Subject: [PATCH 04/11] Bump version for 2.0.0.rc3 --- Gemfile.lock | 2 +- lib/kamal/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index ed0d8a8b..31bddde3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - kamal (2.0.0.rc2) + kamal (2.0.0.rc3) activesupport (>= 7.0) base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) diff --git a/lib/kamal/version.rb b/lib/kamal/version.rb index 46616deb..5b327dfa 100644 --- a/lib/kamal/version.rb +++ b/lib/kamal/version.rb @@ -1,3 +1,3 @@ module Kamal - VERSION = "2.0.0.rc2" + VERSION = "2.0.0.rc3" end From 0258ac4297e70a963ba9c93a2d2ef34dfd175f11 Mon Sep 17 00:00:00 2001 From: eroluysal Date: Wed, 25 Sep 2024 21:22:59 +0300 Subject: [PATCH 05/11] Fix adapter names. --- lib/kamal/secrets/adapters/last_pass.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/kamal/secrets/adapters/last_pass.rb b/lib/kamal/secrets/adapters/last_pass.rb index 16dad150..390e84ed 100644 --- a/lib/kamal/secrets/adapters/last_pass.rb +++ b/lib/kamal/secrets/adapters/last_pass.rb @@ -3,7 +3,7 @@ class Kamal::Secrets::Adapters::LastPass < Kamal::Secrets::Adapters::Base def login(account) unless loggedin?(account) `lpass login #{account.shellescape}` - raise RuntimeError, "Failed to login to 1Password" unless $?.success? + raise RuntimeError, "Failed to login to LastPass" unless $?.success? end end @@ -13,7 +13,7 @@ class Kamal::Secrets::Adapters::LastPass < Kamal::Secrets::Adapters::Base def fetch_secrets(secrets, account:, session:) items = `lpass show #{secrets.map(&:shellescape).join(" ")} --json` - raise RuntimeError, "Could not read #{secrets} from 1Password" unless $?.success? + raise RuntimeError, "Could not read #{secrets} from LastPass" unless $?.success? items = JSON.parse(items) From f0d7f786fa9a462c474d3ff42a726e61e0d87dfa Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 25 Sep 2024 14:51:22 -0400 Subject: [PATCH 06/11] Traefik should be kamal-proxy in reboot hooks --- lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample | 2 +- .../docker/deployer/app/.kamal/hooks/pre-proxy-reboot | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample b/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample index 8cfda6d9..061f8059 100755 --- a/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample +++ b/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample @@ -1,3 +1,3 @@ #!/bin/sh -echo "Rebooting Traefik on $KAMAL_HOSTS..." +echo "Rebooting kamal-proxy on $KAMAL_HOSTS..." diff --git a/test/integration/docker/deployer/app/.kamal/hooks/pre-proxy-reboot b/test/integration/docker/deployer/app/.kamal/hooks/pre-proxy-reboot index 65a70a00..6413b6ef 100755 --- a/test/integration/docker/deployer/app/.kamal/hooks/pre-proxy-reboot +++ b/test/integration/docker/deployer/app/.kamal/hooks/pre-proxy-reboot @@ -1,3 +1,3 @@ #!/bin/sh -echo "Rebooting Traefik on ${KAMAL_HOSTS}..." +echo "Rebooting kamal-proxy on ${KAMAL_HOSTS}..." mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-proxy-reboot From f6851048a6721ad7f0bee3cc8074bb316edc52fc Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 25 Sep 2024 14:31:10 -0400 Subject: [PATCH 07/11] Proxy boot config Add commands for managing proxy boot config. Since the proxy can be shared by multiple applications, the configuration doesn't belong in `config/deploy.yml`. Instead you can set the config with: ``` Usage: kamal proxy boot_config Options: [--publish], [--no-publish], [--skip-publish] # Publish the proxy ports on the host # Default: true [--http-port=N] # HTTP port to publish on the host # Default: 80 [--https-port=N] # HTTPS port to publish on the host # Default: 443 [--docker-options=option=value option2=value2] # Docker options to pass to the proxy container ``` By default we boot the proxy with `--publish 80:80 --publish 443:443`. You can stop it from publishing ports, specify different ports and pass other docker options. The config is stored in `.kamal/proxy/options` as arguments to be passed verbatim to docker run. Where someone wants to set the options in their application they can do that by calling `kamal proxy boot_config set` in a pre-deploy hook. There's an example in the integration tests showing how to use this to front kamal-proxy with Traefik, using an accessory. --- lib/kamal/cli/proxy.rb | 40 +++++++++++ lib/kamal/commands/proxy.rb | 19 ++++- lib/kamal/configuration.rb | 16 ++++- test/cli/accessory_test.rb | 2 +- test/cli/proxy_test.rb | 70 +++++++++++++++++-- test/commands/proxy_test.rb | 36 +++++----- test/integration/docker/deployer/Dockerfile | 2 + .../app_with_traefik/.kamal/hooks/pre-deploy | 3 + .../deployer/app_with_traefik/.kamal/secrets | 1 + .../deployer/app_with_traefik/Dockerfile | 9 +++ .../app_with_traefik/config/deploy.yml | 29 ++++++++ .../deployer/app_with_traefik/default.conf | 17 +++++ test/integration/integration_test.rb | 4 +- test/integration/main_test.rb | 17 +++++ test/test_helper.rb | 7 ++ 15 files changed, 241 insertions(+), 31 deletions(-) create mode 100755 test/integration/docker/deployer/app_with_traefik/.kamal/hooks/pre-deploy create mode 100644 test/integration/docker/deployer/app_with_traefik/.kamal/secrets create mode 100644 test/integration/docker/deployer/app_with_traefik/Dockerfile create mode 100644 test/integration/docker/deployer/app_with_traefik/config/deploy.yml create mode 100644 test/integration/docker/deployer/app_with_traefik/default.conf diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index 66912e07..89fb2fd7 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -21,6 +21,36 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base end end + desc "boot_config ", "Mange kamal-proxy boot configuration" + option :publish, type: :boolean, default: true, desc: "Publish the proxy ports on the host" + option :http_port, type: :numeric, default: Kamal::Configuration::PROXY_HTTP_PORT, desc: "HTTP port to publish on the host" + option :https_port, type: :numeric, default: Kamal::Configuration::PROXY_HTTPS_PORT, desc: "HTTPS port to publish on the host" + option :docker_options, type: :array, default: [], desc: "Docker options to pass to the proxy container", banner: "option=value option2=value2" + def boot_config(subcommand) + case subcommand + when "set" + boot_options = [ + *(KAMAL.config.proxy_publish_args(options[:http_port], options[:https_port]) if options[:publish]), + *options[:docker_options].map { |option| "--#{option}" } + ] + + on(KAMAL.proxy_hosts) do |host| + execute(*KAMAL.proxy.ensure_proxy_directory) + upload! StringIO.new(boot_options.join(" ")), KAMAL.config.proxy_options_file + end + when "get" + on(KAMAL.proxy_hosts) do |host| + puts "Host #{host}: #{capture_with_info(*KAMAL.proxy.get_boot_options)}" + end + when "reset" + on(KAMAL.proxy_hosts) do |host| + execute *KAMAL.proxy.reset_boot_options + end + else + raise ArgumentError, "Unknown boot_config subcommand #{subcommand}" + 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" @@ -169,6 +199,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base stop remove_container remove_image + remove_proxy_directory end end end @@ -193,6 +224,15 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base end end + desc "remove_proxy_directory", "Remove the proxy directory from servers", hide: true + def remove_proxy_directory + with_lock do + on(KAMAL.proxy_hosts) do + execute *KAMAL.proxy.remove_proxy_directory, raise_on_non_zero_exit: false + end + end + end + private def removal_allowed?(force) on(KAMAL.proxy_hosts) do |host| diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb index 5ac1c94e..acff3dbd 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -7,9 +7,8 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base "--network", "kamal", "--detach", "--restart", "unless-stopped", - *config.proxy_publish_args, "--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", - *config.logging_args, + "\$\(#{get_boot_options.join(" ")}\)", config.proxy_image end @@ -65,6 +64,22 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base ) end + def ensure_proxy_directory + make_directory config.proxy_directory + end + + def remove_proxy_directory + remove_directory config.proxy_directory + end + + def get_boot_options + combine [ :cat, config.proxy_options_file ], [ :echo, "\"#{config.proxy_options_default.join(" ")}\"" ], by: "||" + end + + def reset_boot_options + remove_file config.proxy_options_file + end + private def container_name config.proxy_container_name diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index 2848a365..c5db6edf 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -246,8 +246,12 @@ class Kamal::Configuration env_tags.detect { |t| t.name == name.to_s } end - def proxy_publish_args - argumentize "--publish", [ "#{PROXY_HTTP_PORT}:#{PROXY_HTTP_PORT}", "#{PROXY_HTTPS_PORT}:#{PROXY_HTTPS_PORT}" ] + def proxy_publish_args(http_port, https_port) + argumentize "--publish", [ "#{http_port}:#{PROXY_HTTP_PORT}", "#{https_port}:#{PROXY_HTTPS_PORT}" ] + end + + def proxy_options_default + proxy_publish_args PROXY_HTTP_PORT, PROXY_HTTPS_PORT end def proxy_image @@ -258,6 +262,14 @@ class Kamal::Configuration "kamal-proxy" end + def proxy_directory + File.join run_directory, "proxy" + end + + def proxy_options_file + File.join proxy_directory, "options" + end + def to_h { diff --git a/test/cli/accessory_test.rb b/test/cli/accessory_test.rb index b8da2a7a..05431fcb 100644 --- a/test/cli/accessory_test.rb +++ b/test/cli/accessory_test.rb @@ -41,7 +41,7 @@ class CliAccessoryTest < CliTestCase test "upload" do run_command("upload", "mysql").tap do |output| assert_match "mkdir -p app-mysql/etc/mysql", output - assert_match "test/fixtures/files/my.cnf app-mysql/etc/mysql/my.cnf", output + assert_match "test/fixtures/files/my.cnf to app-mysql/etc/mysql/my.cnf", output assert_match "chmod 755 app-mysql/etc/mysql/my.cnf", output end end diff --git a/test/cli/proxy_test.rb b/test/cli/proxy_test.rb index c9987a51..2cfdf0f3 100644 --- a/test/cli/proxy_test.rb +++ b/test/cli/proxy_test.rb @@ -4,7 +4,7 @@ class CliProxyTest < CliTestCase test "boot" do run_command("boot").tap do |output| assert_match "docker login", output - assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}", output + assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") #{KAMAL.config.proxy_image}", output end end @@ -18,7 +18,7 @@ class CliProxyTest < CliTestCase exception = assert_raises do run_command("boot").tap do |output| assert_match "docker login", output - assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}", output + assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") #{KAMAL.config.proxy_image}", output end end @@ -36,7 +36,7 @@ class CliProxyTest < CliTestCase run_command("boot").tap do |output| assert_match "docker login", output - assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}", output + assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") #{KAMAL.config.proxy_image}", output end ensure Thread.report_on_exception = false @@ -57,13 +57,13 @@ class CliProxyTest < CliTestCase assert_match "docker container stop kamal-proxy on 1.1.1.1", output assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output - assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image} on 1.1.1.1", output + assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") #{KAMAL.config.proxy_image} on 1.1.1.1", output assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\" on 1.1.1.1", output assert_match "docker container stop kamal-proxy on 1.1.1.2", output assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.2", output assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.2", output - assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image} on 1.1.1.2", output + assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") #{KAMAL.config.proxy_image} on 1.1.1.2", output assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\" on 1.1.1.2", output end end @@ -198,11 +198,11 @@ class CliProxyTest < CliTestCase assert_match "/usr/bin/env mkdir -p .kamal", output assert_match "docker network create kamal", output assert_match "docker login -u [REDACTED] -p [REDACTED]", output - assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", output + assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", output assert_match "/usr/bin/env mkdir -p .kamal", output assert_match %r{docker rename app-web-latest app-web-latest_replaced_.*}, output assert_match "/usr/bin/env mkdir -p .kamal/apps/app/env/roles", output - assert_match %r{/usr/bin/env .* .kamal/apps/app/env/roles/web.env}, output + assert_match "Uploading \"\\n\" to .kamal/apps/app/env/roles/web.env", output assert_match %r{docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-.* -e KAMAL_CONTAINER_NAME="app-web-latest" -e KAMAL_VERSION="latest" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size="10m" --label service="app" --label role="web" --label destination dhh/app:latest}, output assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"12345678:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\"", output assert_match "docker container ls --all --filter name=^app-web-12345678$ --quiet | xargs docker stop", output @@ -236,6 +236,62 @@ class CliProxyTest < CliTestCase end end + test "boot_config set" do + run_command("boot_config", "set").tap do |output| + %w[ 1.1.1.1 1.1.1.2 ].each do |host| + assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output + assert_match "Uploading \"--publish 80:80 --publish 443:443\" to .kamal/proxy/options on #{host}", output + end + end + end + + test "boot_config set no publish" do + run_command("boot_config", "set", "--publish", "false").tap do |output| + %w[ 1.1.1.1 1.1.1.2 ].each do |host| + assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output + assert_match "Uploading \"\" to .kamal/proxy/options on #{host}", output + end + end + end + + test "boot_config set custom ports" do + run_command("boot_config", "set", "--http-port", "8080", "--https-port", "8443").tap do |output| + %w[ 1.1.1.1 1.1.1.2 ].each do |host| + assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output + assert_match "Uploading \"--publish 8080:80 --publish 8443:443\" to .kamal/proxy/options on #{host}", output + end + end + end + + test "boot_config set docker options" do + run_command("boot_config", "set", "--docker_options", "label=foo=bar", "add_host=thishost:thathost").tap do |output| + %w[ 1.1.1.1 1.1.1.2 ].each do |host| + assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output + assert_match "Uploading \"--publish 80:80 --publish 443:443 --label=foo=bar --add_host=thishost:thathost\" to .kamal/proxy/options on #{host}", output + end + end + end + + test "boot_config get" do + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:cat, ".kamal/proxy/options", "||", :echo, "\"--publish 80:80 --publish 443:443\"") + .returns("--publish 80:80 --publish 8443:443 --label=foo=bar") + .twice + + run_command("boot_config", "get").tap do |output| + assert_match "Host 1.1.1.1: --publish 80:80 --publish 8443:443 --label=foo=bar", output + assert_match "Host 1.1.1.2: --publish 80:80 --publish 8443:443 --label=foo=bar", output + end + end + + test "boot_config reset" do + run_command("boot_config", "reset").tap do |output| + %w[ 1.1.1.1 1.1.1.2 ].each do |host| + assert_match "rm .kamal/proxy/options on #{host}", output + end + end + end + private def run_command(*command, fixture: :with_proxy) stdouted { Kamal::Cli::Proxy.start([ *command, "-c", "test/fixtures/deploy_#{fixture}.yml" ]) } diff --git a/test/commands/proxy_test.rb b/test/commands/proxy_test.rb index e8a3f252..4af78533 100644 --- a/test/commands/proxy_test.rb +++ b/test/commands/proxy_test.rb @@ -15,13 +15,7 @@ class CommandsProxyTest < ActiveSupport::TestCase test "run" do assert_equal \ - "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", - new_command.run.join(" ") - end - - test "run with ports configured" do - assert_equal \ - "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", + "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", new_command.run.join(" ") end @@ -29,15 +23,7 @@ class CommandsProxyTest < ActiveSupport::TestCase @config.delete(:proxy) assert_equal \ - "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", - new_command.run.join(" ") - end - - test "run with 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 kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", + "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", new_command.run.join(" ") end @@ -119,6 +105,24 @@ class CommandsProxyTest < ActiveSupport::TestCase new_command.version.join(" ") end + test "ensure_proxy_directory" do + assert_equal \ + "mkdir -p .kamal/proxy", + new_command.ensure_proxy_directory.join(" ") + end + + test "get_boot_options" do + assert_equal \ + "cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\"", + new_command.get_boot_options.join(" ") + end + + test "reset_boot_options" do + assert_equal \ + "rm .kamal/proxy/options", + new_command.reset_boot_options.join(" ") + end + private def new_command Kamal::Commands::Proxy.new(Kamal::Configuration.new(@config, version: "123")) diff --git a/test/integration/docker/deployer/Dockerfile b/test/integration/docker/deployer/Dockerfile index 269f78b0..c7132861 100644 --- a/test/integration/docker/deployer/Dockerfile +++ b/test/integration/docker/deployer/Dockerfile @@ -19,6 +19,7 @@ RUN apt-get update --fix-missing && apt-get install -y docker-ce docker-ce-cli c COPY *.sh . COPY app/ app/ COPY app_with_roles/ app_with_roles/ +COPY app_with_traefik/ app_with_traefik/ RUN rm -rf /root/.ssh RUN ln -s /shared/ssh /root/.ssh @@ -28,6 +29,7 @@ RUN git config --global user.email "deployer@example.com" RUN git config --global user.name "Deployer" RUN cd app && git init && git add . && git commit -am "Initial version" RUN cd app_with_roles && git init && git add . && git commit -am "Initial version" +RUN cd app_with_traefik && git init && git add . && git commit -am "Initial version" HEALTHCHECK --interval=1s CMD pgrep sleep diff --git a/test/integration/docker/deployer/app_with_traefik/.kamal/hooks/pre-deploy b/test/integration/docker/deployer/app_with_traefik/.kamal/hooks/pre-deploy new file mode 100755 index 00000000..d0483a43 --- /dev/null +++ b/test/integration/docker/deployer/app_with_traefik/.kamal/hooks/pre-deploy @@ -0,0 +1,3 @@ +kamal proxy boot_config set --publish false \ + --docker_options label=traefik.http.services.kamal_proxy.loadbalancer.server.scheme=http \ + label=traefik.http.routers.kamal_proxy.rule=PathPrefix\(\`/\`\) diff --git a/test/integration/docker/deployer/app_with_traefik/.kamal/secrets b/test/integration/docker/deployer/app_with_traefik/.kamal/secrets new file mode 100644 index 00000000..cb2988d6 --- /dev/null +++ b/test/integration/docker/deployer/app_with_traefik/.kamal/secrets @@ -0,0 +1 @@ +SECRET_TOKEN='1234 with "中文"' diff --git a/test/integration/docker/deployer/app_with_traefik/Dockerfile b/test/integration/docker/deployer/app_with_traefik/Dockerfile new file mode 100644 index 00000000..0e6237df --- /dev/null +++ b/test/integration/docker/deployer/app_with_traefik/Dockerfile @@ -0,0 +1,9 @@ +FROM registry:4443/nginx:1-alpine-slim + +COPY default.conf /etc/nginx/conf.d/default.conf + +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_traefik/config/deploy.yml b/test/integration/docker/deployer/app_with_traefik/config/deploy.yml new file mode 100644 index 00000000..e48ebc1f --- /dev/null +++ b/test/integration/docker/deployer/app_with_traefik/config/deploy.yml @@ -0,0 +1,29 @@ +service: app_with_traefik +image: app_with_traefik +servers: + - vm1 + - vm2 +deploy_timeout: 2 +drain_timeout: 2 +readiness_delay: 0 + +registry: + server: registry:4443 + username: root + password: root +builder: + driver: docker + arch: <%= Kamal::Utils.docker_arch %> + args: + COMMIT_SHA: <%= `git rev-parse HEAD` %> +accessories: + traefik: + service: traefik + image: traefik:v2.10 + port: 80 + cmd: "--providers.docker" + options: + volume: + - "/var/run/docker.sock:/var/run/docker.sock" + roles: + - web diff --git a/test/integration/docker/deployer/app_with_traefik/default.conf b/test/integration/docker/deployer/app_with_traefik/default.conf new file mode 100644 index 00000000..e37a9bc1 --- /dev/null +++ b/test/integration/docker/deployer/app_with_traefik/default.conf @@ -0,0 +1,17 @@ +server { + listen 80; + listen [::]:80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + } + + # redirect server error pages to the static page /50x.html + # + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} diff --git a/test/integration/integration_test.rb b/test/integration/integration_test.rb index ad99f3e2..b6943ced 100644 --- a/test/integration/integration_test.rb +++ b/test/integration/integration_test.rb @@ -169,10 +169,8 @@ class IntegrationTest < ActiveSupport::TestCase case app when "app" "127.0.0.1" - when "app_with_roles" - "localhost" else - raise "Unknown app: #{app}" + "localhost" end end end diff --git a/test/integration/main_test.rb b/test/integration/main_test.rb index e3aa1ef3..ce32e640 100644 --- a/test/integration/main_test.rb +++ b/test/integration/main_test.rb @@ -88,6 +88,14 @@ class MainTest < IntegrationTest end test "setup and remove" do + @app = "app_with_roles" + + kamal :proxy, :set_config, + "--publish=false", + "--options=label=traefik.http.services.kamal_proxy.loadbalancer.server.scheme=http", + "label=traefik.http.routers.kamal_proxy.rule=PathPrefix\\\(\\\`/\\\`\\\)", + "label=traefik.http.routers.kamal_proxy.priority=2" + # Check remove completes when nothing has been setup yet kamal :remove, "-y" assert_no_images_or_containers @@ -123,6 +131,15 @@ class MainTest < IntegrationTest assert_proxy_not_running end + test "deploy with traefik" do + @app = "app_with_traefik" + + first_version = latest_app_version + + kamal :setup + assert_app_is_up version: first_version + end + private def assert_envs(version:) assert_env :CLEAR_TOKEN, "4321", version: version, vm: :vm1 diff --git a/test/test_helper.rb b/test/test_helper.rb index f8f4f4e4..1749ee32 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -13,6 +13,13 @@ ActiveSupport::LogSubscriber.logger = ActiveSupport::Logger.new(STDOUT) if ENV[" # Applies to remote commands only. SSHKit.config.backend = SSHKit::Backend::Printer +class SSHKit::Backend::Printer + def upload!(local, location, **kwargs) + local = local.string.inspect if local.respond_to?(:string) + puts "Uploading #{local} to #{location} on #{host}" + end +end + # Ensure local commands use the printer backend too. # See https://github.com/capistrano/sshkit/blob/master/lib/sshkit/dsl.rb#L9 module SSHKit From 7bdf6cd2e8dc3cde195dab32c38d1046858b1f2e Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 25 Sep 2024 17:54:38 -0400 Subject: [PATCH 08/11] Bump to latest version of kamal-proxy --- lib/kamal/configuration.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index c5db6edf..32f11850 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -14,7 +14,7 @@ class Kamal::Configuration include Validation - PROXY_MINIMUM_VERSION = "v0.4.0" + PROXY_MINIMUM_VERSION = "v0.6.0" PROXY_HTTP_PORT = 80 PROXY_HTTPS_PORT = 443 From b3a692111801ea87add3919396dbae9328632d34 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 26 Sep 2024 06:17:40 -0400 Subject: [PATCH 09/11] Handle ssl: false in proxy config Fixes: https://github.com/basecamp/kamal/issues/956 --- lib/kamal/configuration/proxy.rb | 2 +- test/configuration/proxy_test.rb | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb index af09e2c7..c8fbbb6a 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -29,7 +29,7 @@ class Kamal::Configuration::Proxy def deploy_options { host: proxy_config["host"], - tls: proxy_config["ssl"], + tls: proxy_config["ssl"] ? true : nil, "deploy-timeout": seconds_duration(config.deploy_timeout), "drain-timeout": seconds_duration(config.drain_timeout), "health-check-interval": seconds_duration(proxy_config.dig("healthcheck", "interval")), diff --git a/test/configuration/proxy_test.rb b/test/configuration/proxy_test.rb index 3aa3f85e..891bf6b8 100644 --- a/test/configuration/proxy_test.rb +++ b/test/configuration/proxy_test.rb @@ -1,6 +1,6 @@ require "test_helper" -class ConfigurationEnvTest < ActiveSupport::TestCase +class ConfigurationProxyTest < ActiveSupport::TestCase setup do @deploy = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, @@ -18,6 +18,12 @@ class ConfigurationEnvTest < ActiveSupport::TestCase assert_raises(Kamal::ConfigurationError) { config.proxy.ssl? } end + test "ssl false" do + @deploy[:proxy] = { "ssl" => false } + assert_not config.proxy.ssl? + assert_not config.proxy.deploy_options.has_key?(:tls) + end + private def config Kamal::Configuration.new(@deploy) From a765c501a39375355b7c9cac32d78786f6295847 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 26 Sep 2024 07:06:51 -0400 Subject: [PATCH 10/11] Bump version for 2.0.0.rc4 --- Gemfile.lock | 2 +- lib/kamal/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 31bddde3..52c18946 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - kamal (2.0.0.rc3) + kamal (2.0.0.rc4) activesupport (>= 7.0) base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) diff --git a/lib/kamal/version.rb b/lib/kamal/version.rb index 5b327dfa..8faaf58d 100644 --- a/lib/kamal/version.rb +++ b/lib/kamal/version.rb @@ -1,3 +1,3 @@ module Kamal - VERSION = "2.0.0.rc3" + VERSION = "2.0.0.rc4" end From 8c32e6af07356031ffbad2e8eb60169904bc5e52 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 26 Sep 2024 15:34:24 -0400 Subject: [PATCH 11/11] Bump version for 2.0.0 --- Gemfile.lock | 2 +- lib/kamal/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 52c18946..d98fcb4f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - kamal (2.0.0.rc4) + kamal (2.0.0) activesupport (>= 7.0) base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) diff --git a/lib/kamal/version.rb b/lib/kamal/version.rb index 8faaf58d..c43aab05 100644 --- a/lib/kamal/version.rb +++ b/lib/kamal/version.rb @@ -1,3 +1,3 @@ module Kamal - VERSION = "2.0.0.rc4" + VERSION = "2.0.0" end