Compare commits
40 Commits
command-li
...
proxy-with
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46e3085052 | ||
|
|
81f3508507 | ||
|
|
9a16873f21 | ||
|
|
e5ca53db6e | ||
|
|
82a436fa02 | ||
|
|
7be2e7e0ba | ||
|
|
4f7ebd73a3 | ||
|
|
279bda2770 | ||
|
|
aa15fa532a | ||
|
|
276b469c2b | ||
|
|
c10b3fb07a | ||
|
|
f47fd13e5b | ||
|
|
1d8c40f5d2 | ||
|
|
73c78079bc | ||
|
|
cd12f95a97 | ||
|
|
641e9056b3 | ||
|
|
b4bcf35f78 | ||
|
|
7f6095c9eb | ||
|
|
ef1271df47 | ||
|
|
df1232d90f | ||
|
|
e75365c8c6 | ||
|
|
e441399255 | ||
|
|
af992ce755 | ||
|
|
32caf4b148 | ||
|
|
28a02262df | ||
|
|
b11fb93a6c | ||
|
|
67ad7662ab | ||
|
|
c63ec39f07 | ||
|
|
8df7d7d92d | ||
|
|
1d48a0fb0a | ||
|
|
0f815e17e4 | ||
|
|
a310aa8fef | ||
|
|
29b02f5c30 | ||
|
|
6d63c4e9c6 | ||
|
|
472d163cc7 | ||
|
|
dadac999d7 | ||
|
|
2b0810d063 | ||
|
|
098f1855e2 | ||
|
|
88351312bf | ||
|
|
190f4fba28 |
@@ -33,7 +33,7 @@ WORKDIR /workdir
|
||||
|
||||
# Tell git it's safe to access /workdir/.git even if
|
||||
# the directory is owned by a different user
|
||||
RUN git config --global --add safe.directory /workdir
|
||||
RUN git config --global --add safe.directory '*'
|
||||
|
||||
# Set the entrypoint to run the installed binary in /workdir
|
||||
# Example: docker run -it -v "$PWD:/workdir" kamal init
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
kamal (2.0.0)
|
||||
kamal (2.1.1)
|
||||
activesupport (>= 7.0)
|
||||
base64 (~> 0.2)
|
||||
bcrypt_pbkdf (~> 1.0)
|
||||
|
||||
@@ -68,7 +68,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
||||
version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
|
||||
endpoint = capture_with_info(*app.container_id_for_version(version)).strip
|
||||
if endpoint.present?
|
||||
execute *app.remove(target: endpoint), raise_on_non_zero_exit: false
|
||||
execute *app.remove, raise_on_non_zero_exit: false
|
||||
end
|
||||
end
|
||||
|
||||
@@ -203,7 +203,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
||||
run_locally do
|
||||
info "Following logs on #{KAMAL.primary_host}..."
|
||||
|
||||
KAMAL.specific_roles ||= [ "web" ]
|
||||
KAMAL.specific_roles ||= [ KAMAL.primary_role.name ]
|
||||
role = KAMAL.roles_on(KAMAL.primary_host).first
|
||||
|
||||
app = KAMAL.app(role: role, host: host)
|
||||
|
||||
@@ -7,7 +7,7 @@ class Kamal::Cli::Secrets < Kamal::Cli::Base
|
||||
def fetch(*secrets)
|
||||
results = adapter(options[:adapter]).fetch(secrets, **options.slice(:account, :from).symbolize_keys)
|
||||
|
||||
return_or_puts JSON.dump(results), inline: options[:inline]
|
||||
return_or_puts JSON.dump(results).shellescape, inline: options[:inline]
|
||||
end
|
||||
|
||||
desc "extract", "Extract a single secret from the results of a fetch call"
|
||||
@@ -21,6 +21,13 @@ class Kamal::Cli::Secrets < Kamal::Cli::Base
|
||||
return_or_puts value, inline: options[:inline]
|
||||
end
|
||||
|
||||
desc "print", "Print the secrets (for debugging)"
|
||||
def print
|
||||
KAMAL.config.secrets.to_h.each do |key, value|
|
||||
puts "#{key}=#{value}"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def adapter(adapter)
|
||||
Kamal::Secrets::Adapters.lookup(adapter)
|
||||
|
||||
@@ -18,6 +18,8 @@ servers:
|
||||
proxy:
|
||||
ssl: true
|
||||
host: app.example.com
|
||||
# kamal-proxy connects to your container over port 80, use `app_port` to specify a different port.
|
||||
# app_port: 3000
|
||||
|
||||
# Credentials for your image host.
|
||||
registry:
|
||||
|
||||
@@ -43,7 +43,12 @@ class Kamal::Commander::Specifics
|
||||
end
|
||||
|
||||
def specified_hosts
|
||||
(specific_hosts || config.all_hosts) \
|
||||
.select { |host| (specific_roles || config.roles).flat_map(&:hosts).include?(host) }
|
||||
specified_hosts = specific_hosts || config.all_hosts
|
||||
|
||||
if (specific_role_hosts = specific_roles&.flat_map(&:hosts)).present?
|
||||
specified_hosts.select { |host| specific_role_hosts.include?(host) }
|
||||
else
|
||||
specified_hosts
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,8 +5,8 @@ module Kamal::Commands::App::Proxy
|
||||
proxy_exec :deploy, role.container_prefix, *role.proxy.deploy_command_args(target: target)
|
||||
end
|
||||
|
||||
def remove(target:)
|
||||
proxy_exec :remove, role.container_prefix, *role.proxy.remove_command_args(target: target)
|
||||
def remove
|
||||
proxy_exec :remove, role.container_prefix
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -14,12 +14,14 @@ class Kamal::Configuration
|
||||
|
||||
include Validation
|
||||
|
||||
PROXY_MINIMUM_VERSION = "v0.6.0"
|
||||
PROXY_MINIMUM_VERSION = "v0.7.0"
|
||||
PROXY_HTTP_PORT = 80
|
||||
PROXY_HTTPS_PORT = 443
|
||||
|
||||
class << self
|
||||
def create_from(config_file:, destination: nil, version: nil)
|
||||
ENV["KAMAL_DESTINATION"] = destination
|
||||
|
||||
raw_config = load_config_files(config_file, *destination_config_file(config_file, destination))
|
||||
|
||||
new raw_config, destination: destination, version: version
|
||||
@@ -360,7 +362,7 @@ class Kamal::Configuration
|
||||
end
|
||||
|
||||
def ensure_unique_hosts_for_ssl_roles
|
||||
hosts = roles.select(&:ssl?).map { |role| role.proxy.host }
|
||||
hosts = roles.select(&:ssl?).flat_map { |role| role.proxy.hosts }
|
||||
duplicates = hosts.tally.filter_map { |host, count| host if count > 1 }
|
||||
|
||||
raise Kamal::ConfigurationError, "Different roles can't share the same host for SSL: #{duplicates.join(", ")}" if duplicates.any?
|
||||
|
||||
@@ -17,16 +17,19 @@
|
||||
# `proxy: true` or providing a proxy configuration.
|
||||
proxy:
|
||||
|
||||
# Host
|
||||
# Hosts
|
||||
#
|
||||
# The hosts that will be used to serve the app. The proxy will only route requests
|
||||
# to this host to your app.
|
||||
#
|
||||
# If no hosts are set, then all requests will be forwarded, except for matching
|
||||
# requests for other apps deployed on that server that do have a host set.
|
||||
#
|
||||
# Specify one of `host` or `hosts`.
|
||||
host: foo.example.com
|
||||
# If multiple hosts are needed, these can be specified by comma-separating the hosts.
|
||||
host: foo.example.com,bar.example.com
|
||||
hosts:
|
||||
- foo.example.com
|
||||
- bar.example.com
|
||||
|
||||
# App port
|
||||
#
|
||||
|
||||
@@ -29,8 +29,8 @@ ssh:
|
||||
|
||||
# Proxy host
|
||||
#
|
||||
# Specified in the form <host> or <user>@<host>:
|
||||
proxy: root@proxy-host
|
||||
# Specified in the form <host> or <user>@<host>
|
||||
proxy: proxy-host
|
||||
|
||||
# Proxy command
|
||||
#
|
||||
@@ -61,3 +61,10 @@ ssh:
|
||||
# An array of strings, with each element of the array being
|
||||
# a raw private key in PEM format.
|
||||
key_data: [ "-----BEGIN OPENSSH PRIVATE KEY-----" ]
|
||||
|
||||
# Config
|
||||
#
|
||||
# Set to true to load the default OpenSSH config files (~/.ssh/config,
|
||||
# /etc/ssh_config), to false ignore config files, or to a file path
|
||||
# (or array of paths) to load specific configuration. Defaults to true.
|
||||
config: true
|
||||
|
||||
@@ -22,14 +22,14 @@ class Kamal::Configuration::Proxy
|
||||
proxy_config.fetch("ssl", false)
|
||||
end
|
||||
|
||||
def host
|
||||
proxy_config["host"]
|
||||
def hosts
|
||||
proxy_config["hosts"] || proxy_config["host"]&.split(",") || []
|
||||
end
|
||||
|
||||
def deploy_options
|
||||
{
|
||||
host: proxy_config["host"],
|
||||
tls: proxy_config["ssl"] ? true : nil,
|
||||
host: hosts,
|
||||
tls: proxy_config["ssl"],
|
||||
"deploy-timeout": seconds_duration(config.deploy_timeout),
|
||||
"drain-timeout": seconds_duration(config.drain_timeout),
|
||||
"health-check-interval": seconds_duration(proxy_config.dig("healthcheck", "interval")),
|
||||
@@ -48,11 +48,7 @@ class Kamal::Configuration::Proxy
|
||||
end
|
||||
|
||||
def deploy_command_args(target:)
|
||||
optionize ({ target: "#{target}:#{app_port}" }).merge(deploy_options)
|
||||
end
|
||||
|
||||
def remove_command_args(target:)
|
||||
optionize({ target: "#{target}:#{app_port}" })
|
||||
optionize ({ target: "#{target}:#{app_port}" }).merge(deploy_options), with: "="
|
||||
end
|
||||
|
||||
def merge(other)
|
||||
|
||||
@@ -19,9 +19,9 @@ class Kamal::Configuration::Ssh
|
||||
end
|
||||
|
||||
def proxy
|
||||
if (proxy = ssh_config["proxy"])
|
||||
Net::SSH::Proxy::Jump.new(proxy.include?("@") ? proxy : "root@#{proxy}")
|
||||
elsif (proxy_command = ssh_config["proxy_command"])
|
||||
if proxy = ssh_config["proxy"]
|
||||
Net::SSH::Proxy::Jump.new(proxy)
|
||||
elsif proxy_command = ssh_config["proxy_command"]
|
||||
Net::SSH::Proxy::Command.new(proxy_command)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,9 +3,13 @@ class Kamal::Configuration::Validator::Proxy < Kamal::Configuration::Validator
|
||||
unless config.nil?
|
||||
super
|
||||
|
||||
if config["host"].blank? && config["ssl"]
|
||||
if config["host"].blank? && config["hosts"].blank? && config["ssl"]
|
||||
error "Must set a host to enable automatic SSL"
|
||||
end
|
||||
|
||||
if (config.keys & [ "host", "hosts" ]).size > 1
|
||||
error "Specify one of 'host' or 'hosts', not both"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -35,8 +35,10 @@ class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base
|
||||
value = item_field["value"]
|
||||
results["#{item}/#{field}"] = value
|
||||
end
|
||||
elsif item_json.dig("login", "password")
|
||||
results[item] = item_json.dig("login", "password")
|
||||
else
|
||||
results[item] = item_json["login"]["password"]
|
||||
raise RuntimeError, "Item #{item} is not a login type item and no fields were specified"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -16,7 +16,7 @@ class Kamal::Secrets::Dotenv::InlineCommandSubstitution
|
||||
else
|
||||
if command =~ /\A\s*kamal\s*secrets\s+/
|
||||
# Inline the command
|
||||
inline_secrets_command(command).shellescape
|
||||
inline_secrets_command(command)
|
||||
else
|
||||
# Execute the command and return the value
|
||||
`#{command}`.chomp
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
module Kamal
|
||||
VERSION = "2.0.0"
|
||||
VERSION = "2.1.1"
|
||||
end
|
||||
|
||||
@@ -130,7 +130,7 @@ class CliAppTest < CliTestCase
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :stop, raise_on_non_zero_exit: false)
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
||||
.with(:docker, :exec, "kamal-proxy", "kamal-proxy", :deploy, "app-web", "--target", "\"123:80\"", "--deploy-timeout", "\"1s\"", "--drain-timeout", "\"30s\"", "--buffer-requests", "--buffer-responses", "--log-request-header", "\"Cache-Control\"", "--log-request-header", "\"Last-Modified\"", "--log-request-header", "\"User-Agent\"").raises(SSHKit::Command::Failed.new("Failed to deploy"))
|
||||
.with(:docker, :exec, "kamal-proxy", "kamal-proxy", :deploy, "app-web", "--target=\"123:80\"", "--deploy-timeout=\"1s\"", "--drain-timeout=\"30s\"", "--buffer-requests", "--buffer-responses", "--log-request-header=\"Cache-Control\"", "--log-request-header=\"Last-Modified\"", "--log-request-header=\"User-Agent\"").raises(SSHKit::Command::Failed.new("Failed to deploy"))
|
||||
|
||||
stderred do
|
||||
run_command("boot", config: :with_roles, host: nil, allow_execute_error: true).tap do |output|
|
||||
@@ -190,7 +190,7 @@ class CliAppTest < CliTestCase
|
||||
|
||||
run_command("start").tap do |output|
|
||||
assert_match "docker start app-web-999", output
|
||||
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"999:80\" --deploy-timeout \"30s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\"", output
|
||||
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target=\"999:80\" --deploy-timeout=\"30s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\"", output
|
||||
end
|
||||
end
|
||||
|
||||
@@ -383,7 +383,7 @@ class CliAppTest < CliTestCase
|
||||
assert_match /Renaming container .* to .* as already deployed on 1.1.1.1/, output # Rename
|
||||
assert_match /docker rename app-web-latest app-web-latest_replaced_[0-9a-f]{16}/, output
|
||||
assert_match /docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-[0-9a-f]{12} -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 "123:80"/, output
|
||||
assert_match /docker exec kamal-proxy kamal-proxy deploy app-web --target="123:80"/, output
|
||||
assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output
|
||||
end
|
||||
end
|
||||
@@ -392,8 +392,8 @@ class CliAppTest < CliTestCase
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version
|
||||
|
||||
run_command("boot", config: :with_proxy_roles, host: nil).tap do |output|
|
||||
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"123:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --target-timeout \"10s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\"", output
|
||||
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web2 --target \"123:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --target-timeout \"15s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\"", output
|
||||
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target=\"123:80\" --deploy-timeout=\"6s\" --drain-timeout=\"30s\" --target-timeout=\"10s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"", output
|
||||
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web2 --target=\"123:80\" --deploy-timeout=\"6s\" --drain-timeout=\"30s\" --target-timeout=\"15s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"", output
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -58,13 +58,13 @@ class CliProxyTest < CliTestCase
|
||||
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 --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 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 --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
|
||||
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
|
||||
|
||||
@@ -204,7 +204,7 @@ class CliProxyTest < CliTestCase
|
||||
assert_match "/usr/bin/env mkdir -p .kamal/apps/app/env/roles", 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 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
|
||||
assert_match "docker tag dhh/app:latest dhh/app:latest", output
|
||||
assert_match "/usr/bin/env mkdir -p .kamal", output
|
||||
|
||||
@@ -3,7 +3,7 @@ require_relative "cli_test_case"
|
||||
class CliSecretsTest < CliTestCase
|
||||
test "fetch" do
|
||||
assert_equal \
|
||||
"{\"foo\":\"oof\",\"bar\":\"rab\",\"baz\":\"zab\"}",
|
||||
"\\{\\\"foo\\\":\\\"oof\\\",\\\"bar\\\":\\\"rab\\\",\\\"baz\\\":\\\"zab\\\"\\}",
|
||||
run_command("fetch", "foo", "bar", "baz", "--account", "myaccount", "--adapter", "test")
|
||||
end
|
||||
|
||||
@@ -15,6 +15,12 @@ class CliSecretsTest < CliTestCase
|
||||
assert_equal "oof", run_command("extract", "foo", "{\"abc/foo\":\"oof\", \"bar\":\"rab\", \"baz\":\"zab\"}")
|
||||
end
|
||||
|
||||
test "print" do
|
||||
with_test_secrets("secrets" => "SECRET1=ABC\nSECRET2=${SECRET1}DEF\n") do
|
||||
assert_equal "SECRET1=ABC\nSECRET2=ABCDEF", run_command("print")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def run_command(*command)
|
||||
stdouted { Kamal::Cli::Secrets.start([ *command, "-c", "test/fixtures/deploy_with_accessories.yml" ]) }
|
||||
|
||||
@@ -150,6 +150,27 @@ class CommanderTest < ActiveSupport::TestCase
|
||||
assert_equal [ "1.1.1.2" ], @kamal.proxy_hosts
|
||||
end
|
||||
|
||||
test "accessory hosts without filtering" do
|
||||
configure_with(:deploy_with_single_accessory)
|
||||
assert_equal [ "1.1.1.5" ], @kamal.accessory_hosts
|
||||
|
||||
configure_with(:deploy_with_accessories_on_independent_server)
|
||||
assert_equal [ "1.1.1.5", "1.1.1.1", "1.1.1.2" ], @kamal.accessory_hosts
|
||||
end
|
||||
|
||||
test "accessory hosts with role filtering" do
|
||||
configure_with(:deploy_with_single_accessory)
|
||||
@kamal.specific_roles = [ "web" ]
|
||||
assert_equal [], @kamal.accessory_hosts
|
||||
|
||||
configure_with(:deploy_with_accessories_on_independent_server)
|
||||
@kamal.specific_roles = [ "web" ]
|
||||
assert_equal [ "1.1.1.1", "1.1.1.2" ], @kamal.accessory_hosts
|
||||
|
||||
@kamal.specific_roles = [ "workers" ]
|
||||
assert_equal [], @kamal.accessory_hosts
|
||||
end
|
||||
|
||||
private
|
||||
def configure_with(variant)
|
||||
@kamal = Kamal::Commander.new.tap do |kamal|
|
||||
|
||||
@@ -115,14 +115,30 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
|
||||
test "deploy" do
|
||||
assert_equal \
|
||||
"docker exec kamal-proxy kamal-proxy deploy app-web --target \"172.1.0.2:80\" --deploy-timeout \"30s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\"",
|
||||
"docker exec kamal-proxy kamal-proxy deploy app-web --target=\"172.1.0.2:80\" --deploy-timeout=\"30s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"",
|
||||
new_command.deploy(target: "172.1.0.2").join(" ")
|
||||
end
|
||||
|
||||
test "deploy with SSL" do
|
||||
@config[:proxy] = { "ssl" => true, "host" => "example.com" }
|
||||
|
||||
assert_equal \
|
||||
"docker exec kamal-proxy kamal-proxy deploy app-web --target=\"172.1.0.2:80\" --host=\"example.com\" --tls --deploy-timeout=\"30s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"",
|
||||
new_command.deploy(target: "172.1.0.2").join(" ")
|
||||
end
|
||||
|
||||
test "deploy with SSL targeting multiple hosts" do
|
||||
@config[:proxy] = { "ssl" => true, "hosts" => [ "example.com", "anotherexample.com" ] }
|
||||
|
||||
assert_equal \
|
||||
"docker exec kamal-proxy kamal-proxy deploy app-web --target=\"172.1.0.2:80\" --host=\"example.com\" --host=\"anotherexample.com\" --tls --deploy-timeout=\"30s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"",
|
||||
new_command.deploy(target: "172.1.0.2").join(" ")
|
||||
end
|
||||
|
||||
test "remove" do
|
||||
assert_equal \
|
||||
"docker exec kamal-proxy kamal-proxy remove app-web --target \"172.1.0.2:80\"",
|
||||
new_command.remove(target: "172.1.0.2").join(" ")
|
||||
"docker exec kamal-proxy kamal-proxy remove app-web",
|
||||
new_command.remove.join(" ")
|
||||
end
|
||||
|
||||
|
||||
@@ -278,7 +294,7 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
|
||||
test "run over ssh with proxy" do
|
||||
@config[:ssh] = { "proxy" => "2.2.2.2" }
|
||||
assert_equal "ssh -J root@2.2.2.2 -t root@1.1.1.1 -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
|
||||
assert_equal "ssh -J 2.2.2.2 -t root@1.1.1.1 -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
|
||||
end
|
||||
|
||||
test "run over ssh with proxy user" do
|
||||
@@ -288,7 +304,7 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
|
||||
test "run over ssh with custom user with proxy" do
|
||||
@config[:ssh] = { "user" => "app", "proxy" => "2.2.2.2" }
|
||||
assert_equal "ssh -J root@2.2.2.2 -t app@1.1.1.1 -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
|
||||
assert_equal "ssh -J 2.2.2.2 -t app@1.1.1.1 -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
|
||||
end
|
||||
|
||||
test "run over ssh with proxy_command" do
|
||||
|
||||
@@ -13,15 +13,29 @@ class ConfigurationProxyTest < ActiveSupport::TestCase
|
||||
assert_equal true, config.proxy.ssl?
|
||||
end
|
||||
|
||||
test "ssl with multiple hosts passed via host" do
|
||||
@deploy[:proxy] = { "ssl" => true, "host" => "example.com,anotherexample.com" }
|
||||
assert_equal true, config.proxy.ssl?
|
||||
end
|
||||
|
||||
test "ssl with multiple hosts passed via hosts" do
|
||||
@deploy[:proxy] = { "ssl" => true, "hosts" => [ "example.com", "anotherexample.com" ] }
|
||||
assert_equal true, config.proxy.ssl?
|
||||
end
|
||||
|
||||
test "ssl with no host" do
|
||||
@deploy[:proxy] = { "ssl" => true }
|
||||
assert_raises(Kamal::ConfigurationError) { config.proxy.ssl? }
|
||||
end
|
||||
|
||||
test "ssl with both host and hosts" do
|
||||
@deploy[:proxy] = { "ssl" => true, host: "example.com", hosts: [ "anotherexample.com" ] }
|
||||
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
|
||||
|
||||
@@ -30,7 +30,7 @@ class ConfigurationSshTest < ActiveSupport::TestCase
|
||||
|
||||
test "ssh options with proxy host" do
|
||||
config = Kamal::Configuration.new(@deploy.tap { |c| c.merge!(ssh: { "proxy" => "1.2.3.4" }) })
|
||||
assert_equal "root@1.2.3.4", config.ssh.options[:proxy].jump_proxies
|
||||
assert_equal "1.2.3.4", config.ssh.options[:proxy].jump_proxies
|
||||
end
|
||||
|
||||
test "ssh options with proxy host and user" do
|
||||
|
||||
@@ -222,6 +222,13 @@ class ConfigurationTest < ActiveSupport::TestCase
|
||||
assert_equal "my-user", config.registry.username
|
||||
end
|
||||
|
||||
test "destination is loaded into env" do
|
||||
dest_config_file = Pathname.new(File.expand_path("fixtures/deploy_for_dest.yml", __dir__))
|
||||
|
||||
config = Kamal::Configuration.create_from config_file: dest_config_file, destination: "world"
|
||||
assert_equal ENV["KAMAL_DESTINATION"], "world"
|
||||
end
|
||||
|
||||
test "destination yml config merge" do
|
||||
dest_config_file = Pathname.new(File.expand_path("fixtures/deploy_for_dest.yml", __dir__))
|
||||
|
||||
@@ -377,4 +384,15 @@ class ConfigurationTest < ActiveSupport::TestCase
|
||||
|
||||
assert_equal "Different roles can't share the same host for SSL: foo.example.com", exception.message
|
||||
end
|
||||
|
||||
test "two proxy ssl roles with same host in a hosts array" do
|
||||
@deploy_with_roles[:servers]["web"] = { "hosts" => [ "1.1.1.1" ], "proxy" => { "ssl" => true, "hosts" => [ "foo.example.com", "bar.example.com" ] } }
|
||||
@deploy_with_roles[:servers]["workers"] = { "hosts" => [ "1.1.1.1" ], "proxy" => { "ssl" => true, "hosts" => [ "www.example.com", "foo.example.com" ] } }
|
||||
|
||||
exception = assert_raises(Kamal::ConfigurationError) do
|
||||
Kamal::Configuration.new(@deploy_with_roles)
|
||||
end
|
||||
|
||||
assert_equal "Different roles can't share the same host for SSL: foo.example.com", exception.message
|
||||
end
|
||||
end
|
||||
|
||||
38
test/fixtures/deploy_with_accessories_on_independent_server.yml
vendored
Normal file
38
test/fixtures/deploy_with_accessories_on_independent_server.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
service: app
|
||||
image: dhh/app
|
||||
servers:
|
||||
web:
|
||||
- "1.1.1.1"
|
||||
- "1.1.1.2"
|
||||
workers:
|
||||
- "1.1.1.3"
|
||||
- "1.1.1.4"
|
||||
registry:
|
||||
username: user
|
||||
password: pw
|
||||
builder:
|
||||
arch: amd64
|
||||
|
||||
accessories:
|
||||
mysql:
|
||||
image: mysql:5.7
|
||||
host: 1.1.1.5
|
||||
port: 3306
|
||||
env:
|
||||
clear:
|
||||
MYSQL_ROOT_HOST: '%'
|
||||
secret:
|
||||
- MYSQL_ROOT_PASSWORD
|
||||
files:
|
||||
- test/fixtures/files/my.cnf:/etc/mysql/my.cnf
|
||||
directories:
|
||||
- data:/var/lib/mysql
|
||||
redis:
|
||||
image: redis:latest
|
||||
roles:
|
||||
- web
|
||||
port: 6379
|
||||
directories:
|
||||
- data:/data
|
||||
|
||||
readiness_delay: 0
|
||||
29
test/fixtures/deploy_with_single_accessory.yml
vendored
Normal file
29
test/fixtures/deploy_with_single_accessory.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
service: app
|
||||
image: dhh/app
|
||||
servers:
|
||||
web:
|
||||
- "1.1.1.1"
|
||||
- "1.1.1.2"
|
||||
workers:
|
||||
- "1.1.1.3"
|
||||
- "1.1.1.4"
|
||||
registry:
|
||||
username: user
|
||||
password: pw
|
||||
builder:
|
||||
arch: amd64
|
||||
|
||||
accessories:
|
||||
mysql:
|
||||
image: mysql:5.7
|
||||
host: 1.1.1.5
|
||||
port: 3306
|
||||
env:
|
||||
clear:
|
||||
MYSQL_ROOT_HOST: '%'
|
||||
secret:
|
||||
- MYSQL_ROOT_PASSWORD
|
||||
files:
|
||||
- test/fixtures/files/my.cnf:/etc/mysql/my.cnf
|
||||
directories:
|
||||
- data:/var/lib/mysql
|
||||
@@ -8,7 +8,7 @@ class AppTest < IntegrationTest
|
||||
|
||||
kamal :app, :stop
|
||||
|
||||
assert_app_is_down
|
||||
assert_app_not_found
|
||||
|
||||
kamal :app, :start
|
||||
|
||||
@@ -48,7 +48,7 @@ class AppTest < IntegrationTest
|
||||
|
||||
kamal :app, :remove
|
||||
|
||||
assert_app_is_down
|
||||
assert_app_not_found
|
||||
assert_app_directory_removed
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,14 +15,15 @@ readiness_delay: 0
|
||||
|
||||
proxy:
|
||||
host: localhost
|
||||
ssl: false
|
||||
healthcheck:
|
||||
interval: 1
|
||||
timeout: 1
|
||||
path: "/up"
|
||||
response_timeout: 2
|
||||
buffering:
|
||||
requests: true
|
||||
responses: true
|
||||
requests: false
|
||||
responses: false
|
||||
memory: 400_000
|
||||
max_request_body: 40_000_000
|
||||
max_response_body: 40_000_000
|
||||
|
||||
@@ -50,6 +50,12 @@ class IntegrationTest < ActiveSupport::TestCase
|
||||
assert_equal "502", response.code
|
||||
end
|
||||
|
||||
def assert_app_not_found
|
||||
response = app_response
|
||||
debug_response_code(response, "404")
|
||||
assert_equal "404", response.code
|
||||
end
|
||||
|
||||
def assert_app_is_up(version: nil, app: @app)
|
||||
response = app_response(app: app)
|
||||
debug_response_code(response, "200")
|
||||
|
||||
@@ -6,19 +6,30 @@ class BitwardenAdapterTest < SecretAdapterTestCase
|
||||
stub_ticks.with("bw sync").returns("")
|
||||
stub_mypassword
|
||||
|
||||
json = JSON.parse(run_command("fetch", "mypassword"))
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "mypassword")))
|
||||
|
||||
expected_json = { "mypassword"=>"secret123" }
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch with no login" do
|
||||
stub_unlocked
|
||||
stub_ticks.with("bw sync").returns("")
|
||||
stub_noteitem
|
||||
|
||||
error = assert_raises RuntimeError do
|
||||
JSON.parse(shellunescape(run_command("fetch", "mynote")))
|
||||
end
|
||||
assert_match(/not a login type item/, error.message)
|
||||
end
|
||||
|
||||
test "fetch with from" do
|
||||
stub_unlocked
|
||||
stub_ticks.with("bw sync").returns("")
|
||||
stub_myitem
|
||||
|
||||
json = JSON.parse(run_command("fetch", "--from", "myitem", "field1", "field2", "field3"))
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "--from", "myitem", "field1", "field2", "field3")))
|
||||
|
||||
expected_json = {
|
||||
"myitem/field1"=>"secret1", "myitem/field2"=>"blam", "myitem/field3"=>"fewgrwjgk"
|
||||
@@ -59,7 +70,7 @@ class BitwardenAdapterTest < SecretAdapterTestCase
|
||||
JSON
|
||||
|
||||
|
||||
json = JSON.parse(run_command("fetch", "mypassword", "myitem/field1", "myitem/field2", "myitem2/field3"))
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "mypassword", "myitem/field1", "myitem/field2", "myitem2/field3")))
|
||||
|
||||
expected_json = {
|
||||
"mypassword"=>"secret123", "myitem/field1"=>"secret1", "myitem/field2"=>"blam", "myitem2/field3"=>"fewgrwjgk"
|
||||
@@ -82,7 +93,7 @@ class BitwardenAdapterTest < SecretAdapterTestCase
|
||||
stub_ticks.with("bw sync").returns("")
|
||||
stub_mypassword
|
||||
|
||||
json = JSON.parse(run_command("fetch", "mypassword"))
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "mypassword")))
|
||||
|
||||
expected_json = { "mypassword"=>"secret123" }
|
||||
|
||||
@@ -107,7 +118,7 @@ class BitwardenAdapterTest < SecretAdapterTestCase
|
||||
stub_ticks.with("bw sync").returns("")
|
||||
stub_mypassword
|
||||
|
||||
json = JSON.parse(run_command("fetch", "mypassword"))
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "mypassword")))
|
||||
|
||||
expected_json = { "mypassword"=>"secret123" }
|
||||
|
||||
@@ -132,7 +143,7 @@ class BitwardenAdapterTest < SecretAdapterTestCase
|
||||
stub_ticks.with("BW_SESSION=0987654321 bw sync").returns("")
|
||||
stub_mypassword(session: "0987654321")
|
||||
|
||||
json = JSON.parse(run_command("fetch", "mypassword"))
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "mypassword")))
|
||||
|
||||
expected_json = { "mypassword"=>"secret123" }
|
||||
|
||||
@@ -181,6 +192,30 @@ class BitwardenAdapterTest < SecretAdapterTestCase
|
||||
JSON
|
||||
end
|
||||
|
||||
def stub_noteitem(session: nil)
|
||||
stub_ticks
|
||||
.with("#{"BW_SESSION=#{session} " if session}bw get item mynote")
|
||||
.returns(<<~JSON)
|
||||
{
|
||||
"passwordHistory":null,
|
||||
"revisionDate":"2024-09-28T09:07:27.461Z",
|
||||
"creationDate":"2024-09-28T09:07:00.740Z",
|
||||
"deletedDate":null,
|
||||
"object":"item",
|
||||
"id":"aaaaaaaa-cccc-eeee-0000-222222222222",
|
||||
"organizationId":null,
|
||||
"folderId":null,
|
||||
"type":2,
|
||||
"reprompt":0,
|
||||
"name":"noteitem",
|
||||
"notes":"NOTES",
|
||||
"favorite":false,
|
||||
"secureNote":{"type":0},
|
||||
"collectionIds":[]
|
||||
}
|
||||
JSON
|
||||
end
|
||||
|
||||
def stub_myitem
|
||||
stub_ticks
|
||||
.with("bw get item myitem")
|
||||
|
||||
@@ -12,10 +12,4 @@ class SecretsInlineCommandSubstitution < SecretAdapterTestCase
|
||||
substituted = Kamal::Secrets::Dotenv::InlineCommandSubstitution.call("FOO=$(blah)", nil, overwrite: false)
|
||||
assert_equal "FOO=results", substituted
|
||||
end
|
||||
|
||||
test "escapes correctly" do
|
||||
Kamal::Cli::Main.expects(:start).with { |command| command == [ "secrets", "fetch", "...", "--inline" ] }.returns("{ \"foo\" : \"bar\" }")
|
||||
substituted = Kamal::Secrets::Dotenv::InlineCommandSubstitution.call("SECRETS=$(kamal secrets fetch ...)", nil, overwrite: false)
|
||||
assert_equal "SECRETS=\\{\\ \\\"foo\\\"\\ :\\ \\\"bar\\\"\\ \\}", substituted
|
||||
end
|
||||
end
|
||||
|
||||
@@ -51,7 +51,7 @@ class LastPassAdapterTest < SecretAdapterTestCase
|
||||
]
|
||||
JSON
|
||||
|
||||
json = JSON.parse(run_command("fetch", "SECRET1", "FOLDER1/FSECRET1", "FOLDER1/FSECRET2"))
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "SECRET1", "FOLDER1/FSECRET1", "FOLDER1/FSECRET2")))
|
||||
|
||||
expected_json = {
|
||||
"SECRET1"=>"secret1",
|
||||
@@ -96,7 +96,7 @@ class LastPassAdapterTest < SecretAdapterTestCase
|
||||
]
|
||||
JSON
|
||||
|
||||
json = JSON.parse(run_command("fetch", "--from", "FOLDER1", "FSECRET1", "FSECRET2"))
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "--from", "FOLDER1", "FSECRET1", "FSECRET2")))
|
||||
|
||||
expected_json = {
|
||||
"FOLDER1/FSECRET1"=>"fsecret1",
|
||||
@@ -111,7 +111,7 @@ class LastPassAdapterTest < SecretAdapterTestCase
|
||||
stub_ticks_with("lpass login email@example.com", succeed: true).returns("")
|
||||
stub_ticks.with("lpass show SECRET1 --json").returns(single_item_json)
|
||||
|
||||
json = JSON.parse(run_command("fetch", "SECRET1"))
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "SECRET1")))
|
||||
|
||||
expected_json = {
|
||||
"SECRET1"=>"secret1"
|
||||
|
||||
@@ -44,7 +44,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
|
||||
]
|
||||
JSON
|
||||
|
||||
json = JSON.parse(run_command("fetch", "--from", "op://myvault/myitem", "section/SECRET1", "section/SECRET2", "section2/SECRET3"))
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "--from", "op://myvault/myitem", "section/SECRET1", "section/SECRET2", "section2/SECRET3")))
|
||||
|
||||
expected_json = {
|
||||
"myvault/myitem/section/SECRET1"=>"VALUE1",
|
||||
@@ -103,7 +103,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
|
||||
}
|
||||
JSON
|
||||
|
||||
json = JSON.parse(run_command("fetch", "--from", "op://myvault", "myitem/section/SECRET1", "myitem/section/SECRET2", "myitem2/section2/SECRET3"))
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "--from", "op://myvault", "myitem/section/SECRET1", "myitem/section/SECRET2", "myitem2/section2/SECRET3")))
|
||||
|
||||
expected_json = {
|
||||
"myvault/myitem/section/SECRET1"=>"VALUE1",
|
||||
@@ -122,7 +122,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
|
||||
.with("op item get myitem --vault \"myvault\" --fields \"label=section.SECRET1\" --format \"json\" --account \"myaccount\"")
|
||||
.returns(single_item_json)
|
||||
|
||||
json = JSON.parse(run_command("fetch", "--from", "op://myvault/myitem", "section/SECRET1"))
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "--from", "op://myvault/myitem", "section/SECRET1")))
|
||||
|
||||
expected_json = {
|
||||
"myvault/myitem/section/SECRET1"=>"VALUE1"
|
||||
@@ -139,7 +139,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
|
||||
.with("op item get myitem --vault \"myvault\" --fields \"label=section.SECRET1\" --format \"json\" --account \"myaccount\" --session \"1234567890\"")
|
||||
.returns(single_item_json)
|
||||
|
||||
json = JSON.parse(run_command("fetch", "--from", "op://myvault/myitem", "section/SECRET1"))
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "--from", "op://myvault/myitem", "section/SECRET1")))
|
||||
|
||||
expected_json = {
|
||||
"myvault/myitem/section/SECRET1"=>"VALUE1"
|
||||
|
||||
@@ -86,4 +86,8 @@ class SecretAdapterTestCase < ActiveSupport::TestCase
|
||||
stub_ticks.with { |c| c == command && (succeed ? `true` : `false`) }
|
||||
Kamal::Secrets::Adapters::Base.any_instance.stubs(:`)
|
||||
end
|
||||
|
||||
def shellunescape(string)
|
||||
"\"#{string}\"".undump.gsub(/\\([{}])/, "\\1")
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user