Compare commits
5 Commits
more-robus
...
eval-proxy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6ebe3492f | ||
|
|
0d034ec5dc | ||
|
|
598bd65b78 | ||
|
|
36f4e90a76 | ||
|
|
973fa1a7ff |
@@ -76,6 +76,10 @@ module Kamal::Commands
|
||||
[ :sh, "-c", "'#{command.flatten.join(" ").gsub("'", "'\\\\''")}'" ]
|
||||
end
|
||||
|
||||
def eval(*args)
|
||||
[ :eval, *args ]
|
||||
end
|
||||
|
||||
def docker(*args)
|
||||
args.compact.unshift :docker
|
||||
end
|
||||
|
||||
@@ -2,14 +2,21 @@ 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
|
||||
shell \
|
||||
chain \
|
||||
boot_options,
|
||||
eval(
|
||||
docker(
|
||||
:run,
|
||||
"--name", container_name,
|
||||
"--network", "kamal",
|
||||
"--detach",
|
||||
"--restart", "unless-stopped",
|
||||
"--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy",
|
||||
"\$OPTIONS",
|
||||
config.proxy_image
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
def start
|
||||
@@ -72,6 +79,10 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
|
||||
remove_directory config.proxy_directory
|
||||
end
|
||||
|
||||
def boot_options
|
||||
"OPTIONS=$(cat #{config.proxy_options_file} 2> /dev/null || echo \"#{config.proxy_options_default.join(" ")}\")"
|
||||
end
|
||||
|
||||
def get_boot_options
|
||||
combine [ :cat, config.proxy_options_file ], [ :echo, "\"#{config.proxy_options_default.join(" ")}\"" ], by: "||"
|
||||
end
|
||||
|
||||
@@ -14,7 +14,7 @@ class Kamal::Configuration
|
||||
|
||||
include Validation
|
||||
|
||||
PROXY_MINIMUM_VERSION = "v0.8.4"
|
||||
PROXY_MINIMUM_VERSION = "v0.8.6"
|
||||
PROXY_HTTP_PORT = 80
|
||||
PROXY_HTTPS_PORT = 443
|
||||
PROXY_LOG_MAX_SIZE = "10m"
|
||||
|
||||
@@ -51,6 +51,30 @@ 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)
|
||||
# ```
|
||||
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.
|
||||
|
||||
@@ -18,7 +18,7 @@ class Kamal::Configuration::Env
|
||||
end
|
||||
|
||||
def secrets_io
|
||||
Kamal::EnvFile.new(secret_keys.to_h { |key| [ key, secrets[key] ] }).to_io
|
||||
Kamal::EnvFile.new(secrets_hash).to_io
|
||||
end
|
||||
|
||||
def merge(other)
|
||||
@@ -26,4 +26,12 @@ class Kamal::Configuration::Env
|
||||
config: { "clear" => clear.merge(other.clear), "secret" => secret_keys | other.secret_keys },
|
||||
secrets: secrets
|
||||
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
|
||||
|
||||
@@ -4,36 +4,9 @@ class Kamal::Secrets::Dotenv::InlineCommandSubstitution
|
||||
::Dotenv::Parser.substitutions.map! { |sub| sub == ::Dotenv::Substitutions::Command ? self : sub }
|
||||
end
|
||||
|
||||
# Improved version of Dotenv::Substitutions::Command's INTERPOLATED_SHELL_COMMAND
|
||||
# Handles:
|
||||
# $(echo 'foo)')
|
||||
# $(echo "foo)")
|
||||
# $(echo foo\))
|
||||
# $(echo "foo\")")
|
||||
# $(echo foo\\)
|
||||
# $(echo 'foo'"'"')')
|
||||
INTERPOLATED_SHELL_COMMAND = /
|
||||
(?<backslash>\\)? # (1) Optional backslash (escaped '$')
|
||||
\$ # (2) Match a literal '$' (start of command)
|
||||
(?<cmd> # (3) Capture the command within '$()' as 'cmd'
|
||||
\( # (4) Require an opening parenthesis '('
|
||||
(?: # (5) Match either:
|
||||
[^()\\'"]+ # - Any non-parens, non-escape, non-quotes (normal chars)
|
||||
| \\ (?!\)) . # - Escaped character (e.g., `\(`, `\'`, `\"`), but **not** `\)` alone
|
||||
| \\\\ \) # - Special case: Match `\\)` as a literal `\)`
|
||||
| '(?:[^'\\]* (?:\\.[^'\\]*)*)' # - Single-quoted strings with escaped quotes (`\'`)
|
||||
| "(?:[^"\\]* (?:\\.[^"\\]*)*)" # - Double-quoted strings with escaped quotes (`\"`)
|
||||
| '(?:[^']*)' (?:"[^"]*")* # - Single-quoted, followed by optional mixed double-quoted parts
|
||||
| "(?:[^"]*)" (?:'[^']*')* # - Double-quoted, followed by optional mixed single-quoted parts
|
||||
| \g<cmd> # - Nested `$()` expressions (recursive call)
|
||||
)* # (6) Repeat to allow full parsing
|
||||
\) # (7) Require a closing parenthesis ')'
|
||||
)
|
||||
/x
|
||||
|
||||
def call(value, _env, overwrite: false)
|
||||
# Process interpolated shell commands
|
||||
value.gsub(INTERPOLATED_SHELL_COMMAND) do |*|
|
||||
value.gsub(Dotenv::Substitutions::Command.singleton_class::INTERPOLATED_SHELL_COMMAND) do |*|
|
||||
# Eliminate opening and closing parentheses
|
||||
command = $LAST_MATCH_INFO[:cmd][1..-2]
|
||||
|
||||
|
||||
@@ -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 "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
|
||||
|
||||
@@ -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 "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
|
||||
|
||||
@@ -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 || 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
|
||||
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 "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 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 "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
|
||||
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 || 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 %r{docker rename app-web-latest app-web-latest_replaced_.*}, output
|
||||
assert_match "/usr/bin/env mkdir -p .kamal/apps/app/env/roles", output
|
||||
|
||||
@@ -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}",
|
||||
"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(" ")
|
||||
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}",
|
||||
"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(" ")
|
||||
end
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -34,30 +34,6 @@ class SecretsTest < ActiveSupport::TestCase
|
||||
end
|
||||
end
|
||||
|
||||
test "secret with open bracket" do
|
||||
with_test_secrets("secrets" => "SECRET1=$(echo 'foo)')") do
|
||||
assert_equal "foo)", Kamal::Secrets.new["SECRET1"]
|
||||
end
|
||||
end
|
||||
|
||||
test "secret with close bracket" do
|
||||
with_test_secrets("secrets" => "SECRET1=$(echo 'foo(')") do
|
||||
assert_equal "foo(", Kamal::Secrets.new["SECRET1"]
|
||||
end
|
||||
end
|
||||
|
||||
test "secret with escaped quote" do
|
||||
with_test_secrets("secrets" => "SECRET1=$(echo \"foo\\\")") do
|
||||
assert_equal "foo", Kamal::Secrets.new["SECRET1"]
|
||||
end
|
||||
end
|
||||
|
||||
test "secret with escaped single quote" do
|
||||
with_test_secrets("secrets" => "SECRET1= $(echo 'foo'\"'\"'bar')") do
|
||||
assert_equal "foo'bar", Kamal::Secrets.new["SECRET1"]
|
||||
end
|
||||
end
|
||||
|
||||
test "destinations" do
|
||||
with_test_secrets("secrets.dest" => "SECRET=DEF", "secrets" => "SECRET=ABC", "secrets-common" => "SECRET=GHI\nSECRET2=JKL") do
|
||||
assert_equal "ABC", Kamal::Secrets.new["SECRET"]
|
||||
|
||||
Reference in New Issue
Block a user