diff --git a/Dockerfile b/Dockerfile index d8346804..e8b4e4ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:3.3-alpine +FROM ruby:3.4-alpine # Install docker/buildx-bin COPY --from=docker/buildx-bin /buildx /usr/libexec/docker/cli-plugins/docker-buildx @@ -13,9 +13,9 @@ COPY Gemfile Gemfile.lock kamal.gemspec ./ COPY lib/kamal/version.rb /kamal/lib/kamal/version.rb # Install system dependencies -RUN apk add --no-cache build-base git docker openrc openssh-client-default \ +RUN apk add --no-cache build-base git docker openrc openssh-client-default yaml-dev \ && rc-update add docker boot \ - && gem install bundler --version=2.4.3 \ + && gem install bundler --version=2.6.5 \ && bundle install # Copy the rest of our application code into the container. diff --git a/Gemfile.lock b/Gemfile.lock index 56ba684b..5875f349 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - kamal (2.5.1) + kamal (2.5.3) activesupport (>= 7.0) base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) @@ -48,8 +48,6 @@ GEM ast (2.4.2) base64 (0.2.0) bcrypt_pbkdf (1.1.1) - bcrypt_pbkdf (1.1.1-arm64-darwin) - bcrypt_pbkdf (1.1.1-x86_64-darwin) benchmark (0.4.0) bigdecimal (3.1.8) builder (3.3.0) @@ -84,11 +82,15 @@ GEM net-sftp (4.0.0) net-ssh (>= 5.0.0, < 8.0.0) net-ssh (7.3.0) - nokogiri (1.17.2-arm64-darwin) + nokogiri (1.18.3-aarch64-linux-musl) racc (~> 1.4) - nokogiri (1.17.2-x86_64-darwin) + nokogiri (1.18.3-arm64-darwin) racc (~> 1.4) - nokogiri (1.17.2-x86_64-linux) + nokogiri (1.18.3-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.18.3-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.3-x86_64-linux-musl) racc (~> 1.4) ostruct (0.6.1) parallel (1.26.3) @@ -99,7 +101,7 @@ GEM date stringio racc (1.8.1) - rack (3.1.8) + rack (3.1.12) rack-session (2.0.0) rack (>= 3.0.0) rack-test (2.1.0) @@ -172,14 +174,16 @@ GEM unicode-display_width (3.1.2) unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) - uri (1.0.2) + uri (1.0.3) useragent (0.16.11) zeitwerk (2.7.1) PLATFORMS + aarch64-linux-musl arm64-darwin x86_64-darwin x86_64-linux + x86_64-linux-musl DEPENDENCIES debug @@ -189,4 +193,4 @@ DEPENDENCIES rubocop-rails-omakase BUNDLED WITH - 2.4.3 + 2.6.5 diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb index acff3dbd..6ca87e02 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -2,14 +2,14 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base delegate :argumentize, :optionize, to: Kamal::Utils def run - docker :run, - "--name", container_name, - "--network", "kamal", - "--detach", - "--restart", "unless-stopped", - "--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", - "\$\(#{get_boot_options.join(" ")}\)", - config.proxy_image + pipe \ + [ :echo, "\$\(#{get_boot_options.join(" ")}\) #{config.proxy_image}" ], + xargs(docker(:run, + "--name", container_name, + "--network", "kamal", + "--detach", + "--restart", "unless-stopped", + "--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy")) end def start @@ -73,7 +73,7 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base end def get_boot_options - combine [ :cat, config.proxy_options_file ], [ :echo, "\"#{config.proxy_options_default.join(" ")}\"" ], by: "||" + combine [ :cat, config.proxy_options_file, "2>", "/dev/null" ], [ :echo, "\"#{config.proxy_options_default.join(" ")}\"" ], by: "||" end def reset_boot_options diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index 023a2d7c..1c0b79cf 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -14,7 +14,7 @@ class Kamal::Configuration include Validation - PROXY_MINIMUM_VERSION = "v0.8.4" + PROXY_MINIMUM_VERSION = "v0.8.7" PROXY_HTTP_PORT = 80 PROXY_HTTPS_PORT = 443 PROXY_LOG_MAX_SIZE = "10m" diff --git a/lib/kamal/configuration/docs/env.yml b/lib/kamal/configuration/docs/env.yml index 98521ae4..b73e6309 100644 --- a/lib/kamal/configuration/docs/env.yml +++ b/lib/kamal/configuration/docs/env.yml @@ -51,6 +51,37 @@ env: secret: - DB_PASSWORD +# Aliased secrets +# +# You can also alias secrets to other secrets using a `:` separator. +# +# This is useful when the ENV name is different from the secret name. For example, if you have two +# places where you need to define the ENV variable `DB_PASSWORD`, but the value is different depending +# on the context. +# +# ```shell +# SECRETS=$(kamal secrets fetch ...) +# +# MAIN_DB_PASSWORD=$(kamal secrets extract MAIN_DB_PASSWORD $SECRETS) +# SECONDARY_DB_PASSWORD=$(kamal secrets extract SECONDARY_DB_PASSWORD $SECRETS) +# ``` +env: + secret: + - DB_PASSWORD:MAIN_DB_PASSWORD + tags: + secondary_db: + secret: + - DB_PASSWORD:SECONDARY_DB_PASSWORD +accessories: + main_db_accessory: + env: + secret: + - DB_PASSWORD:MAIN_DB_PASSWORD + secondary_db_accessory: + env: + secret: + - DB_PASSWORD:SECONDARY_DB_PASSWORD + # Tags # # Tags are used to add extra env variables to specific hosts. diff --git a/lib/kamal/configuration/docs/proxy.yml b/lib/kamal/configuration/docs/proxy.yml index 9ed6a97f..49c11ac8 100644 --- a/lib/kamal/configuration/docs/proxy.yml +++ b/lib/kamal/configuration/docs/proxy.yml @@ -52,6 +52,13 @@ proxy: # Defaults to `false`: ssl: true + # SSL redirect + # + # By default, kamal-proxy will redirect all HTTP requests to HTTPS when SSL is enabled. + # If you prefer that HTTP traffic is passed through to your application (along with + # HTTPS traffic), you can disable this redirect by setting `ssl_redirect: false`: + ssl_redirect: false + # Forward headers # # Whether to forward the `X-Forwarded-For` and `X-Forwarded-Proto` headers. diff --git a/lib/kamal/configuration/env.rb b/lib/kamal/configuration/env.rb index 8e52d9e4..bebcf1db 100644 --- a/lib/kamal/configuration/env.rb +++ b/lib/kamal/configuration/env.rb @@ -1,8 +1,7 @@ class Kamal::Configuration::Env include Kamal::Configuration::Validation - attr_reader :context, :secrets - attr_reader :clear, :secret_keys + attr_reader :context, :clear, :secret_keys delegate :argumentize, to: Kamal::Utils def initialize(config:, secrets:, context: "env") @@ -18,12 +17,22 @@ class Kamal::Configuration::Env end def secrets_io - Kamal::EnvFile.new(secret_keys.to_h { |key| [ key, secrets[key] ] }).to_io + Kamal::EnvFile.new(aliased_secrets).to_io end def merge(other) self.class.new \ config: { "clear" => clear.merge(other.clear), "secret" => secret_keys | other.secret_keys }, - secrets: secrets + secrets: @secrets end + + private + def aliased_secrets + secret_keys.to_h { |key| extract_alias(key) }.transform_values { |secret_key| @secrets[secret_key] } + end + + def extract_alias(key) + key_name, key_aliased_to = key.split(":", 2) + [ key_name, key_aliased_to || key_name ] + end end diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb index 6232c3e0..a8272e88 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -42,6 +42,7 @@ class Kamal::Configuration::Proxy "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"), + "tls-redirect": proxy_config.dig("ssl_redirect"), "log-request-header": proxy_config.dig("logging", "request_headers") || DEFAULT_LOG_REQUEST_HEADERS, "log-response-header": proxy_config.dig("logging", "response_headers") }.compact diff --git a/lib/kamal/version.rb b/lib/kamal/version.rb index eefac514..3099a7cc 100644 --- a/lib/kamal/version.rb +++ b/lib/kamal/version.rb @@ -1,3 +1,3 @@ module Kamal - VERSION = "2.5.1" + VERSION = "2.5.3" end diff --git a/test/cli/proxy_test.rb b/test/cli/proxy_test.rb index b9de3e15..4880a839 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 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy_image}", output + assert_match "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION} | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", 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 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy_image}", output + assert_match "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION} | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", 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 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy_image}", output + assert_match "docker container start kamal-proxy || echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION} | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", output end ensure Thread.report_on_exception = false @@ -56,12 +56,12 @@ class CliProxyTest < CliTestCase run_command("reboot", "-y").tap do |output| assert_match "docker container stop kamal-proxy 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 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy_image} on 1.1.1.1", output + assert_match "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION} | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy 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 "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 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy_image} on 1.1.1.2", output + assert_match "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION} | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy 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 @@ -196,7 +196,7 @@ 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 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", output + assert_match "docker container start kamal-proxy || echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION} | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", 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 @@ -316,7 +316,7 @@ class CliProxyTest < CliTestCase 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 --log-opt max-size=10m\"") + .with(:cat, ".kamal/proxy/options", "2>", "/dev/null", "||", :echo, "\"--publish 80:80 --publish 443:443 --log-opt max-size=10m\"") .returns("--publish 80:80 --publish 8443:443 --label=foo=bar") .twice diff --git a/test/commands/proxy_test.rb b/test/commands/proxy_test.rb index b7cc9f3d..e42415b1 100644 --- a/test/commands/proxy_test.rb +++ b/test/commands/proxy_test.rb @@ -15,7 +15,7 @@ class CommandsProxyTest < ActiveSupport::TestCase test "run" do assert_equal \ - "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 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", + "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION} | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", new_command.run.join(" ") end @@ -23,7 +23,7 @@ class CommandsProxyTest < ActiveSupport::TestCase @config.delete(:proxy) assert_equal \ - "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 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", + "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION} | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", new_command.run.join(" ") end @@ -113,7 +113,7 @@ class CommandsProxyTest < ActiveSupport::TestCase test "get_boot_options" do assert_equal \ - "cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\"", + "cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\"", new_command.get_boot_options.join(" ") end diff --git a/test/configuration/env/tags_test.rb b/test/configuration/env/tags_test.rb index 7db61777..e857faaa 100644 --- a/test/configuration/env/tags_test.rb +++ b/test/configuration/env/tags_test.rb @@ -92,7 +92,25 @@ class ConfigurationEnvTagsTest < ActiveSupport::TestCase } config = Kamal::Configuration.new(deploy) - assert_equal "hello", config.role("web").env("1.1.1.1").secrets["PASSWORD"] + assert_equal "PASSWORD=hello\n", config.role("web").env("1.1.1.1").secrets_io.string + end + end + + test "aliased tag secret env" do + with_test_secrets("secrets" => "PASSWORD=hello\nALIASED_PASSWORD=aliased_hello") do + deploy = { + service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, + servers: [ { "1.1.1.1" => "secrets" } ], + builder: { "arch" => "amd64" }, + env: { + "tags" => { + "secrets" => { "secret" => [ "PASSWORD:ALIASED_PASSWORD" ] } + } + } + } + + config = Kamal::Configuration.new(deploy) + assert_equal "PASSWORD=aliased_hello\n", config.role("web").env("1.1.1.1").secrets_io.string end end diff --git a/test/configuration/env_test.rb b/test/configuration/env_test.rb index 627d3a6c..29cf0d85 100644 --- a/test/configuration/env_test.rb +++ b/test/configuration/env_test.rb @@ -48,6 +48,20 @@ class ConfigurationEnvTest < ActiveSupport::TestCase end end + test "aliased secrets" do + with_test_secrets("secrets" => "ALIASED_PASSWORD=hello") do + config = { + "secret" => [ "PASSWORD:ALIASED_PASSWORD" ], + "clear" => {} + } + + assert_config \ + config: config, + clear: {}, + secrets: { "PASSWORD" => "hello" } + end + end + private def assert_config(config:, clear: {}, secrets: {}) env = Kamal::Configuration::Env.new config: config, secrets: Kamal::Secrets.new 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 index d0483a43..b2d8b112 100755 --- a/test/integration/docker/deployer/app_with_traefik/.kamal/hooks/pre-deploy +++ b/test/integration/docker/deployer/app_with_traefik/.kamal/hooks/pre-deploy @@ -1,3 +1,4 @@ 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\(\`/\`\) + label=traefik.http.routers.kamal_proxy.rule=PathPrefix\(\`/\`\) \ + sysctl=net.ipv4.ip_local_port_range=\"10000\ 60999\" diff --git a/test/integration/proxy_test.rb b/test/integration/proxy_test.rb index d861a887..39cacb94 100644 --- a/test/integration/proxy_test.rb +++ b/test/integration/proxy_test.rb @@ -46,7 +46,27 @@ class ProxyTest < IntegrationTest logs = kamal :proxy, :logs, capture: true assert_match /No previous state to restore/, logs + kamal :proxy, :boot_config, :set, "--docker-options='sysctl net.ipv4.ip_local_port_range=\"10000 60999\"'" + assert_docker_options_in_file + + kamal :proxy, :reboot, "-y" + assert_docker_options_in_container + + kamal :proxy, :boot_config, :reset + kamal :proxy, :remove assert_proxy_not_running end + + private + def assert_docker_options_in_file + boot_config = kamal :proxy, :boot_config, :get, capture: true + assert_match "Host vm1: --publish 80:80 --publish 443:443 --log-opt max-size=10m --sysctl net.ipv4.ip_local_port_range=\"10000 60999\"", boot_config + end + + def assert_docker_options_in_container + assert_equal \ + "{\"net.ipv4.ip_local_port_range\":\"10000 60999\"}", + docker_compose("exec vm1 docker inspect --format '{{ json .HostConfig.Sysctls }}' kamal-proxy", capture: true).strip + end end