Compare commits

...

5 Commits

Author SHA1 Message Date
Donal McBreen
a6ebe3492f Eval proxy options
To ensure that the proxy options are evaluated as if they were
added verbatim to the command, we'll eval the docker run command.

Fixes: https://github.com/basecamp/kamal/issues/1448
2025-03-10 10:07:04 +00:00
Donal McBreen
0d034ec5dc Merge pull request #1439 from matthewbjones/feature/aliased-secrets
Adds the ability to alias/map secrets
2025-03-10 10:06:45 +00:00
Donal McBreen
598bd65b78 Merge pull request #1449 from basecamp/kamal-proxy-drain-timeout-fix
Update to kamal-proxy 0.8.6
2025-03-10 10:06:14 +00:00
Donal McBreen
36f4e90a76 Update to kamal-proxy 0.8.6
Includes a fix for locking the proxy while draining
2025-03-07 11:33:17 +00:00
Matthew Jones
973fa1a7ff Adds the ability to alias/map secrets 2025-03-04 07:23:26 -07:00
9 changed files with 99 additions and 18 deletions

View File

@@ -76,6 +76,10 @@ module Kamal::Commands
[ :sh, "-c", "'#{command.flatten.join(" ").gsub("'", "'\\\\''")}'" ] [ :sh, "-c", "'#{command.flatten.join(" ").gsub("'", "'\\\\''")}'" ]
end end
def eval(*args)
[ :eval, *args ]
end
def docker(*args) def docker(*args)
args.compact.unshift :docker args.compact.unshift :docker
end end

View File

@@ -2,14 +2,21 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
delegate :argumentize, :optionize, to: Kamal::Utils delegate :argumentize, :optionize, to: Kamal::Utils
def run def run
docker :run, shell \
"--name", container_name, chain \
"--network", "kamal", boot_options,
"--detach", eval(
"--restart", "unless-stopped", docker(
"--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", :run,
"\$\(#{get_boot_options.join(" ")}\)", "--name", container_name,
config.proxy_image "--network", "kamal",
"--detach",
"--restart", "unless-stopped",
"--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy",
"\$OPTIONS",
config.proxy_image
)
)
end end
def start def start
@@ -72,6 +79,10 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
remove_directory config.proxy_directory remove_directory config.proxy_directory
end end
def boot_options
"OPTIONS=$(cat #{config.proxy_options_file} 2> /dev/null || echo \"#{config.proxy_options_default.join(" ")}\")"
end
def get_boot_options def get_boot_options
combine [ :cat, config.proxy_options_file ], [ :echo, "\"#{config.proxy_options_default.join(" ")}\"" ], by: "||" combine [ :cat, config.proxy_options_file ], [ :echo, "\"#{config.proxy_options_default.join(" ")}\"" ], by: "||"
end end

View File

@@ -14,7 +14,7 @@ class Kamal::Configuration
include Validation include Validation
PROXY_MINIMUM_VERSION = "v0.8.4" PROXY_MINIMUM_VERSION = "v0.8.6"
PROXY_HTTP_PORT = 80 PROXY_HTTP_PORT = 80
PROXY_HTTPS_PORT = 443 PROXY_HTTPS_PORT = 443
PROXY_LOG_MAX_SIZE = "10m" PROXY_LOG_MAX_SIZE = "10m"

View File

@@ -51,6 +51,30 @@ env:
secret: secret:
- DB_PASSWORD - 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)
# ```
accessories:
main_db_accessory:
env:
secret:
- DB_PASSWORD:MAIN_DB_PASSWORD
secondary_db_accessory:
env:
secret:
- DB_PASSWORD:SECONDARY_DB_PASSWORD
# Tags # Tags
# #
# Tags are used to add extra env variables to specific hosts. # Tags are used to add extra env variables to specific hosts.

View File

@@ -18,7 +18,7 @@ class Kamal::Configuration::Env
end end
def secrets_io def secrets_io
Kamal::EnvFile.new(secret_keys.to_h { |key| [ key, secrets[key] ] }).to_io Kamal::EnvFile.new(secrets_hash).to_io
end end
def merge(other) def merge(other)
@@ -26,4 +26,12 @@ class Kamal::Configuration::Env
config: { "clear" => clear.merge(other.clear), "secret" => secret_keys | other.secret_keys }, config: { "clear" => clear.merge(other.clear), "secret" => secret_keys | other.secret_keys },
secrets: secrets secrets: secrets
end end
private
def secrets_hash
secret_keys.to_h do |key|
key_name, key_aliased_to = key.split(":")
[ key_name, secrets[key_aliased_to || key_name] ]
end
end
end end

View File

@@ -4,7 +4,7 @@ class CliProxyTest < CliTestCase
test "boot" do test "boot" do
run_command("boot").tap do |output| run_command("boot").tap do |output|
assert_match "docker login", 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 "sh -c 'OPTIONS=$(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") ; eval docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $OPTIONS #{KAMAL.config.proxy_image}", output
end end
end end
@@ -18,7 +18,7 @@ class CliProxyTest < CliTestCase
exception = assert_raises do exception = assert_raises do
run_command("boot").tap do |output| run_command("boot").tap do |output|
assert_match "docker login", 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 "sh -c 'OPTIONS=$(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") ; eval docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $OPTIONS #{KAMAL.config.proxy_image}", output
end end
end end
@@ -36,7 +36,7 @@ class CliProxyTest < CliTestCase
run_command("boot").tap do |output| run_command("boot").tap do |output|
assert_match "docker login", 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 || sh -c 'OPTIONS=$(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") ; eval docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $OPTIONS #{KAMAL.config.proxy_image}", output
end end
ensure ensure
Thread.report_on_exception = false Thread.report_on_exception = false
@@ -56,12 +56,12 @@ class CliProxyTest < CliTestCase
run_command("reboot", "-y").tap do |output| run_command("reboot", "-y").tap do |output|
assert_match "docker container stop kamal-proxy on 1.1.1.1", 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 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 "sh -c 'OPTIONS=$(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") ; eval docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $OPTIONS #{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 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 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 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 "sh -c 'OPTIONS=$(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") ; eval docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $OPTIONS #{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 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
end end
@@ -196,7 +196,7 @@ class CliProxyTest < CliTestCase
assert_match "/usr/bin/env mkdir -p .kamal", output assert_match "/usr/bin/env mkdir -p .kamal", output
assert_match "docker network create kamal", output assert_match "docker network create kamal", output
assert_match "docker login -u [REDACTED] -p [REDACTED]", 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 || sh -c 'OPTIONS=$(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") ; eval docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $OPTIONS basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", output
assert_match "/usr/bin/env mkdir -p .kamal", 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 %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 "/usr/bin/env mkdir -p .kamal/apps/app/env/roles", output

View File

@@ -15,7 +15,7 @@ class CommandsProxyTest < ActiveSupport::TestCase
test "run" do test "run" do
assert_equal \ 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}", "sh -c 'OPTIONS=$(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") ; eval docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $OPTIONS basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}'",
new_command.run.join(" ") new_command.run.join(" ")
end end
@@ -23,7 +23,7 @@ class CommandsProxyTest < ActiveSupport::TestCase
@config.delete(:proxy) @config.delete(:proxy)
assert_equal \ 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}", "sh -c 'OPTIONS=$(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") ; eval docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $OPTIONS basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}'",
new_command.run.join(" ") new_command.run.join(" ")
end end

View File

@@ -48,6 +48,20 @@ class ConfigurationEnvTest < ActiveSupport::TestCase
end end
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 private
def assert_config(config:, clear: {}, secrets: {}) def assert_config(config:, clear: {}, secrets: {})
env = Kamal::Configuration::Env.new config: config, secrets: Kamal::Secrets.new env = Kamal::Configuration::Env.new config: config, secrets: Kamal::Secrets.new

View File

@@ -46,7 +46,27 @@ class ProxyTest < IntegrationTest
logs = kamal :proxy, :logs, capture: true logs = kamal :proxy, :logs, capture: true
assert_match /No previous state to restore/, logs 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 kamal :proxy, :remove
assert_proxy_not_running assert_proxy_not_running
end 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 end