diff --git a/Gemfile.lock b/Gemfile.lock index ed0d8a8b..d98fcb4f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - kamal (2.0.0.rc2) + kamal (2.0.0) activesupport (>= 7.0) base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) 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/cli/templates/deploy.yml b/lib/kamal/cli/templates/deploy.yml index 8ecf5d04..7eec6aa2 100644 --- a/lib/kamal/cli/templates/deploy.yml +++ b/lib/kamal/cli/templates/deploy.yml @@ -2,11 +2,22 @@ service: my-app # Name of the container image. -image: user/my-app +image: my-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: @@ -14,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 @@ -22,19 +33,44 @@ registry: 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 different ssh user than root +# # ssh: # user: app -# Use accessory services (secrets come from .env). +# 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: # db: # image: mysql:8.0 @@ -56,29 +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. -# -# 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 - -# 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/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/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 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..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 @@ -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/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/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) diff --git a/lib/kamal/version.rb b/lib/kamal/version.rb index 46616deb..c43aab05 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" end 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/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) 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/.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 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