Compare commits
9 Commits
v2.7.0
...
proxy-expe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b21af0f5b3 | ||
|
|
459ba95bbf | ||
|
|
01d08738ff | ||
|
|
19cf94457f | ||
|
|
269a5ff3e6 | ||
|
|
c8adda1550 | ||
|
|
d891eb91e4 | ||
|
|
268ec1c6e0 | ||
|
|
42fdbd98cb |
1
bin/docs
1
bin/docs
@@ -24,6 +24,7 @@ DOCS = {
|
||||
"env" => "Environment variables",
|
||||
"healthcheck" => "Healthchecks",
|
||||
"logging" => "Logging",
|
||||
"proxy" => "Proxy (Experimental)",
|
||||
"registry" => "Docker Registry",
|
||||
"role" => "Roles",
|
||||
"servers" => "Servers",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
module Kamal::Cli
|
||||
class BootError < StandardError; end
|
||||
class HookError < StandardError; end
|
||||
class LockError < StandardError; end
|
||||
end
|
||||
|
||||
@@ -38,8 +38,17 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
||||
roles = KAMAL.roles_on(host)
|
||||
|
||||
roles.each do |role|
|
||||
app = KAMAL.app(role: role, host: host)
|
||||
execute *KAMAL.auditor.record("Started app version #{KAMAL.config.version}"), verbosity: :debug
|
||||
execute *KAMAL.app(role: role, host: host).start, raise_on_non_zero_exit: false
|
||||
execute *app.start, raise_on_non_zero_exit: false
|
||||
|
||||
if role.running_traefik? && KAMAL.proxy_host?(host)
|
||||
version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
|
||||
endpoint = capture_with_info(*app.container_endpoint(version: version)).strip
|
||||
raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, did the container boot?" if endpoint.empty?
|
||||
|
||||
execute *KAMAL.proxy.deploy(role.container_prefix, target: endpoint)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -52,8 +61,18 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
||||
roles = KAMAL.roles_on(host)
|
||||
|
||||
roles.each do |role|
|
||||
app = KAMAL.app(role: role, host: host)
|
||||
execute *KAMAL.auditor.record("Stopped app", role: role), verbosity: :debug
|
||||
execute *KAMAL.app(role: role, host: host).stop, raise_on_non_zero_exit: false
|
||||
|
||||
if role.running_traefik? && KAMAL.proxy_host?(host)
|
||||
version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
|
||||
endpoint = capture_with_info(*app.container_endpoint(version: version)).strip
|
||||
if endpoint.present?
|
||||
execute *KAMAL.proxy.remove(role.container_prefix, target: endpoint), raise_on_non_zero_exit: false
|
||||
end
|
||||
end
|
||||
|
||||
execute *app.stop, raise_on_non_zero_exit: false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -45,11 +45,22 @@ class Kamal::Cli::App::Boot
|
||||
|
||||
def start_new_version
|
||||
audit "Booted app version #{version}"
|
||||
|
||||
execute *app.tie_cord(role.cord_host_file) if uses_cord?
|
||||
hostname = "#{host.to_s[0...51].gsub(/\.+$/, '')}-#{SecureRandom.hex(6)}"
|
||||
execute *app.run(hostname: hostname)
|
||||
Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
|
||||
|
||||
if proxy_host?
|
||||
execute *app.run_for_proxy(hostname: hostname)
|
||||
if running_traefik?
|
||||
endpoint = capture_with_info(*app.container_endpoint(version: version)).strip
|
||||
raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, did the container boot?" if endpoint.empty?
|
||||
execute *KAMAL.proxy.deploy(role.container_prefix, target: endpoint)
|
||||
else
|
||||
Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
|
||||
end
|
||||
else
|
||||
execute *app.tie_cord(role.cord_host_file) if uses_cord?
|
||||
execute *app.run(hostname: hostname)
|
||||
Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
|
||||
end
|
||||
end
|
||||
|
||||
def stop_new_version
|
||||
@@ -57,7 +68,7 @@ class Kamal::Cli::App::Boot
|
||||
end
|
||||
|
||||
def stop_old_version(version)
|
||||
if uses_cord?
|
||||
if uses_cord? && !proxy_host?
|
||||
cord = capture_with_info(*app.cord(version: version), raise_on_non_zero_exit: false).strip
|
||||
if cord.present?
|
||||
execute *app.cut_cord(cord)
|
||||
@@ -116,4 +127,8 @@ class Kamal::Cli::App::Boot
|
||||
def queuer?
|
||||
barrier && !barrier_role?
|
||||
end
|
||||
|
||||
def proxy_host?
|
||||
KAMAL.proxy_host?(host)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,9 +30,18 @@ class Kamal::Cli::Build < Kamal::Cli::Base
|
||||
say "Building with uncommitted changes:\n #{uncommitted_changes}", :yellow
|
||||
end
|
||||
|
||||
# Get the command here to ensure the Dir.chdir doesn't interfere with it
|
||||
push = KAMAL.builder.push
|
||||
|
||||
run_locally do
|
||||
begin
|
||||
execute *KAMAL.builder.buildx_inspect
|
||||
context_hosts = capture_with_info(*KAMAL.builder.context_hosts).split("\n")
|
||||
|
||||
if context_hosts != KAMAL.builder.config_context_hosts
|
||||
warn "Context hosts have changed, so re-creating builder, was: #{context_hosts.join(", ")}], now: #{KAMAL.builder.config_context_hosts.join(", ")}"
|
||||
cli.remove
|
||||
cli.create
|
||||
end
|
||||
rescue SSHKit::Command::Failed => e
|
||||
if e.message =~ /(context not found|no builder|does not exist)/
|
||||
warn "Missing compatible builder, so creating a new one first"
|
||||
@@ -42,9 +51,6 @@ class Kamal::Cli::Build < Kamal::Cli::Base
|
||||
end
|
||||
end
|
||||
|
||||
# Get the command here to ensure the Dir.chdir doesn't interfere with it
|
||||
push = KAMAL.builder.push
|
||||
|
||||
KAMAL.with_verbosity(:debug) do
|
||||
Dir.chdir(KAMAL.config.builder.build_directory) { execute *push }
|
||||
end
|
||||
@@ -66,7 +72,7 @@ class Kamal::Cli::Build < Kamal::Cli::Base
|
||||
|
||||
desc "create", "Create a build setup"
|
||||
def create
|
||||
if (remote_host = KAMAL.config.builder.remote)
|
||||
if (remote_host = KAMAL.config.builder.remote_host)
|
||||
connect_to_remote_host(remote_host)
|
||||
end
|
||||
|
||||
|
||||
@@ -39,8 +39,13 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
||||
with_lock do
|
||||
run_hook "pre-deploy"
|
||||
|
||||
say "Ensure Traefik is running...", :magenta
|
||||
invoke "kamal:cli:traefik:boot", [], invoke_options
|
||||
if KAMAL.config.proxy.enabled?
|
||||
say "Ensure Traefik/kamal-proxy is running...", :magenta
|
||||
invoke "kamal:cli:proxy:boot", [], invoke_options
|
||||
else
|
||||
say "Ensure Traefik is running...", :magenta
|
||||
invoke "kamal:cli:traefik:boot", [], invoke_options
|
||||
end
|
||||
|
||||
say "Detect stale containers...", :magenta
|
||||
invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
|
||||
@@ -108,7 +113,11 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
||||
|
||||
desc "details", "Show details about all containers"
|
||||
def details
|
||||
invoke "kamal:cli:traefik:details"
|
||||
if KAMAL.config.proxy.enabled?
|
||||
invoke "kamal:cli:proxy:details"
|
||||
else
|
||||
invoke "kamal:cli:traefik:details"
|
||||
end
|
||||
invoke "kamal:cli:app:details"
|
||||
invoke "kamal:cli:accessory:details", [ "all" ]
|
||||
end
|
||||
@@ -209,7 +218,11 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
||||
def remove
|
||||
confirming "This will remove all containers and images. Are you sure?" do
|
||||
with_lock do
|
||||
invoke "kamal:cli:traefik:remove", [], options.without(:confirmed)
|
||||
if KAMAL.config.proxy.enabled?
|
||||
invoke "kamal:cli:proxy:remove", [], options.without(:confirmed)
|
||||
else
|
||||
invoke "kamal:cli:traefik:remove", [], options.without(:confirmed)
|
||||
end
|
||||
invoke "kamal:cli:app:remove", [], options.without(:confirmed)
|
||||
invoke "kamal:cli:accessory:remove", [ "all" ], options
|
||||
invoke "kamal:cli:registry:logout", [], options.without(:confirmed).merge(skip_local: true)
|
||||
@@ -237,6 +250,9 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
||||
desc "lock", "Manage the deploy lock"
|
||||
subcommand "lock", Kamal::Cli::Lock
|
||||
|
||||
desc "proxy", "Prune old application images and containers"
|
||||
subcommand "proxy", Kamal::Cli::Proxy
|
||||
|
||||
desc "prune", "Prune old application images and containers"
|
||||
subcommand "prune", Kamal::Cli::Prune
|
||||
|
||||
|
||||
160
lib/kamal/cli/proxy.rb
Normal file
160
lib/kamal/cli/proxy.rb
Normal file
@@ -0,0 +1,160 @@
|
||||
class Kamal::Cli::Proxy < Kamal::Cli::Base
|
||||
desc "boot", "Boot proxy on servers"
|
||||
def boot
|
||||
raise_unless_kamal_proxy_enabled!
|
||||
with_lock do
|
||||
on(KAMAL.traefik_hosts) do |host|
|
||||
execute *KAMAL.registry.login
|
||||
execute *KAMAL.traefik_or_proxy(host).start_or_run
|
||||
end
|
||||
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"
|
||||
def reboot
|
||||
raise_unless_kamal_proxy_enabled!
|
||||
confirming "This will cause a brief outage on each host. Are you sure?" do
|
||||
with_lock do
|
||||
host_groups = options[:rolling] ? KAMAL.traefik_hosts : [ KAMAL.traefik_hosts ]
|
||||
host_groups.each do |hosts|
|
||||
host_list = Array(hosts).join(",")
|
||||
run_hook "pre-traefik-reboot", hosts: host_list
|
||||
on(hosts) do |host|
|
||||
execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug
|
||||
execute *KAMAL.registry.login
|
||||
|
||||
"Stopping and removing Traefik on #{host}, if running..."
|
||||
execute *KAMAL.traefik.stop, raise_on_non_zero_exit: false
|
||||
execute *KAMAL.traefik.remove_container
|
||||
|
||||
"Stopping and removing kamal-proxy on #{host}, if running..."
|
||||
execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false
|
||||
execute *KAMAL.proxy.remove_container
|
||||
|
||||
execute *KAMAL.traefik_or_proxy(host).run
|
||||
|
||||
if KAMAL.proxy_host?(host)
|
||||
KAMAL.roles_on(host).select(&:running_traefik?).each do |role|
|
||||
app = KAMAL.app(role: role, host: host)
|
||||
|
||||
version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
|
||||
endpoint = capture_with_info(*app.container_endpoint(version: version)).strip
|
||||
|
||||
if endpoint.present?
|
||||
info "Deploying #{endpoint} for role `#{role}` on #{host}..."
|
||||
execute *KAMAL.proxy.deploy(role.container_prefix, target: endpoint)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
run_hook "post-traefik-reboot", hosts: host_list
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc "start", "Start existing proxy container on servers"
|
||||
def start
|
||||
raise_unless_kamal_proxy_enabled!
|
||||
with_lock do
|
||||
on(KAMAL.traefik_hosts) do |host|
|
||||
execute *KAMAL.auditor.record("Started proxy"), verbosity: :debug
|
||||
execute *KAMAL.traefik_or_proxy(host).start
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc "stop", "Stop existing proxy container on servers"
|
||||
def stop
|
||||
raise_unless_kamal_proxy_enabled!
|
||||
with_lock do
|
||||
on(KAMAL.traefik_hosts) do |host|
|
||||
execute *KAMAL.auditor.record("Stopped proxy"), verbosity: :debug
|
||||
execute *KAMAL.traefik_or_proxy(host).stop, raise_on_non_zero_exit: false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc "restart", "Restart existing proxy container on servers"
|
||||
def restart
|
||||
raise_unless_kamal_proxy_enabled!
|
||||
with_lock do
|
||||
stop
|
||||
start
|
||||
end
|
||||
end
|
||||
|
||||
desc "details", "Show details about proxy container from servers"
|
||||
def details
|
||||
raise_unless_kamal_proxy_enabled!
|
||||
on(KAMAL.traefik_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.traefik_or_proxy(host).info), type: "Proxy" }
|
||||
end
|
||||
|
||||
desc "logs", "Show log lines from proxy on servers"
|
||||
option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
|
||||
option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
|
||||
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
|
||||
option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
|
||||
def logs
|
||||
raise_unless_kamal_proxy_enabled!
|
||||
grep = options[:grep]
|
||||
|
||||
if options[:follow]
|
||||
run_locally do
|
||||
info "Following logs on #{KAMAL.primary_host}..."
|
||||
info KAMAL.traefik_or_proxy(KAMAL.primary_host).follow_logs(host: KAMAL.primary_host, grep: grep)
|
||||
exec KAMAL.traefik_or_proxy(KAMAL.primary_host).follow_logs(host: KAMAL.primary_host, grep: grep)
|
||||
end
|
||||
else
|
||||
since = options[:since]
|
||||
lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
|
||||
|
||||
on(KAMAL.traefik_hosts) do |host|
|
||||
puts_by_host host, capture(*KAMAL.traefik_or_proxy(host).logs(since: since, lines: lines, grep: grep)), type: "Proxy"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc "remove", "Remove proxy container and image from servers"
|
||||
def remove
|
||||
raise_unless_kamal_proxy_enabled!
|
||||
with_lock do
|
||||
stop
|
||||
remove_container
|
||||
remove_image
|
||||
end
|
||||
end
|
||||
|
||||
desc "remove_container", "Remove proxy container from servers", hide: true
|
||||
def remove_container
|
||||
raise_unless_kamal_proxy_enabled!
|
||||
with_lock do
|
||||
on(KAMAL.traefik_hosts) do
|
||||
execute *KAMAL.auditor.record("Removed proxy container"), verbosity: :debug
|
||||
execute *KAMAL.proxy.remove_container
|
||||
execute *KAMAL.traefik.remove_container
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc "remove_image", "Remove proxy image from servers", hide: true
|
||||
def remove_image
|
||||
raise_unless_kamal_proxy_enabled!
|
||||
with_lock do
|
||||
on(KAMAL.traefik_hosts) do
|
||||
execute *KAMAL.auditor.record("Removed proxy image"), verbosity: :debug
|
||||
execute *KAMAL.proxy.remove_image
|
||||
execute *KAMAL.traefik.remove_image
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def raise_unless_kamal_proxy_enabled!
|
||||
unless KAMAL.config.proxy.enabled?
|
||||
raise "kamal proxy commands are disabled unless experimental proxy support is enabled. Use `kamal traefik` commands instead."
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -18,10 +18,6 @@ registry:
|
||||
password:
|
||||
- KAMAL_REGISTRY_PASSWORD
|
||||
|
||||
# Configure builder setup.
|
||||
builder:
|
||||
arch: amd64
|
||||
|
||||
# Inject ENV variables into containers (secrets come from .env).
|
||||
# Remember to run `kamal env push` after making changes!
|
||||
# env:
|
||||
@@ -34,6 +30,16 @@ builder:
|
||||
# ssh:
|
||||
# user: app
|
||||
|
||||
# Configure builder setup.
|
||||
# builder:
|
||||
# args:
|
||||
# RUBY_VERSION: 3.2.0
|
||||
# secrets:
|
||||
# - GITHUB_TOKEN
|
||||
# remote:
|
||||
# arch: amd64
|
||||
# host: ssh://app@192.168.0.1
|
||||
|
||||
# Use accessory services (secrets come from .env).
|
||||
# accessories:
|
||||
# db:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
class Kamal::Cli::Traefik < Kamal::Cli::Base
|
||||
desc "boot", "Boot Traefik on servers"
|
||||
def boot
|
||||
raise_if_kamal_proxy_enabled!
|
||||
with_lock do
|
||||
on(KAMAL.traefik_hosts) do
|
||||
execute *KAMAL.registry.login
|
||||
@@ -13,6 +14,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
||||
option :rolling, type: :boolean, default: false, desc: "Reboot traefik on hosts in sequence, rather than in parallel"
|
||||
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
|
||||
def reboot
|
||||
raise_if_kamal_proxy_enabled!
|
||||
confirming "This will cause a brief outage on each host. Are you sure?" do
|
||||
with_lock do
|
||||
host_groups = options[:rolling] ? KAMAL.traefik_hosts : [ KAMAL.traefik_hosts ]
|
||||
@@ -34,6 +36,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
||||
|
||||
desc "start", "Start existing Traefik container on servers"
|
||||
def start
|
||||
raise_if_kamal_proxy_enabled!
|
||||
with_lock do
|
||||
on(KAMAL.traefik_hosts) do
|
||||
execute *KAMAL.auditor.record("Started traefik"), verbosity: :debug
|
||||
@@ -44,6 +47,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
||||
|
||||
desc "stop", "Stop existing Traefik container on servers"
|
||||
def stop
|
||||
raise_if_kamal_proxy_enabled!
|
||||
with_lock do
|
||||
on(KAMAL.traefik_hosts) do
|
||||
execute *KAMAL.auditor.record("Stopped traefik"), verbosity: :debug
|
||||
@@ -54,6 +58,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
||||
|
||||
desc "restart", "Restart existing Traefik container on servers"
|
||||
def restart
|
||||
raise_if_kamal_proxy_enabled!
|
||||
with_lock do
|
||||
stop
|
||||
start
|
||||
@@ -62,6 +67,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
||||
|
||||
desc "details", "Show details about Traefik container from servers"
|
||||
def details
|
||||
raise_if_kamal_proxy_enabled!
|
||||
on(KAMAL.traefik_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.traefik.info), type: "Traefik" }
|
||||
end
|
||||
|
||||
@@ -72,6 +78,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
||||
option :grep_options, aliases: "-o", desc: "Additional options supplied to grep"
|
||||
option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
|
||||
def logs
|
||||
raise_if_kamal_proxy_enabled!
|
||||
grep = options[:grep]
|
||||
grep_options = options[:grep_options]
|
||||
|
||||
@@ -93,6 +100,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
||||
|
||||
desc "remove", "Remove Traefik container and image from servers"
|
||||
def remove
|
||||
raise_if_kamal_proxy_enabled!
|
||||
with_lock do
|
||||
stop
|
||||
remove_container
|
||||
@@ -102,6 +110,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
||||
|
||||
desc "remove_container", "Remove Traefik container from servers", hide: true
|
||||
def remove_container
|
||||
raise_if_kamal_proxy_enabled!
|
||||
with_lock do
|
||||
on(KAMAL.traefik_hosts) do
|
||||
execute *KAMAL.auditor.record("Removed traefik container"), verbosity: :debug
|
||||
@@ -112,6 +121,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
||||
|
||||
desc "remove_image", "Remove Traefik image from servers", hide: true
|
||||
def remove_image
|
||||
raise_if_kamal_proxy_enabled!
|
||||
with_lock do
|
||||
on(KAMAL.traefik_hosts) do
|
||||
execute *KAMAL.auditor.record("Removed traefik image"), verbosity: :debug
|
||||
@@ -119,4 +129,11 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def raise_if_kamal_proxy_enabled!
|
||||
if KAMAL.config.proxy.enabled?
|
||||
raise "kamal traefik commands are disabled when experimental proxy support is enabled. Use `kamal proxy` commands instead."
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,7 +3,7 @@ require "active_support/core_ext/module/delegation"
|
||||
|
||||
class Kamal::Commander
|
||||
attr_accessor :verbosity, :holding_lock, :connected
|
||||
delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :traefik_hosts, :accessory_hosts, to: :specifics
|
||||
delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :traefik_hosts, :proxy_hosts, :proxy_host?, :accessory_hosts, to: :specifics
|
||||
|
||||
def initialize
|
||||
self.verbosity = :info
|
||||
@@ -101,6 +101,10 @@ class Kamal::Commander
|
||||
@lock ||= Kamal::Commands::Lock.new(config)
|
||||
end
|
||||
|
||||
def proxy
|
||||
@proxy ||= Kamal::Commands::Proxy.new(config)
|
||||
end
|
||||
|
||||
def prune
|
||||
@prune ||= Kamal::Commands::Prune.new(config)
|
||||
end
|
||||
@@ -122,6 +126,10 @@ class Kamal::Commander
|
||||
end
|
||||
|
||||
|
||||
def traefik_or_proxy(host)
|
||||
proxy_host?(host) ? proxy : traefik
|
||||
end
|
||||
|
||||
def with_verbosity(level)
|
||||
old_level = self.verbosity
|
||||
|
||||
|
||||
@@ -22,6 +22,15 @@ class Kamal::Commander::Specifics
|
||||
config.traefik_hosts & specified_hosts
|
||||
end
|
||||
|
||||
def proxy_hosts
|
||||
traefik_hosts & config.proxy_hosts
|
||||
end
|
||||
|
||||
def proxy_host?(host)
|
||||
host = host.hostname if host.is_a?(SSHKit::Host)
|
||||
proxy_hosts.include?(host)
|
||||
end
|
||||
|
||||
def accessory_hosts
|
||||
specific_hosts || config.accessories.flat_map(&:hosts)
|
||||
end
|
||||
|
||||
@@ -30,6 +30,24 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
||||
role.cmd
|
||||
end
|
||||
|
||||
def run_for_proxy(hostname: nil)
|
||||
docker :run,
|
||||
"--detach",
|
||||
"--restart unless-stopped",
|
||||
"--name", container_name,
|
||||
*([ "--hostname", hostname ] if hostname),
|
||||
"-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
|
||||
"-e", "KAMAL_VERSION=\"#{config.version}\"",
|
||||
*role.env_args(host),
|
||||
*role.logging_args,
|
||||
*config.volume_args,
|
||||
*role.asset_volume_args,
|
||||
*role.label_args_for_proxy,
|
||||
*role.option_args,
|
||||
config.absolute_image,
|
||||
role.cmd
|
||||
end
|
||||
|
||||
def start
|
||||
docker :start, container_name
|
||||
end
|
||||
|
||||
@@ -28,4 +28,11 @@ module Kamal::Commands::App::Containers
|
||||
container_id_for(container_name: container_name(version)),
|
||||
xargs(docker(:inspect, "--format", DOCKER_HEALTH_LOG_FORMAT))
|
||||
end
|
||||
|
||||
def container_endpoint(version:)
|
||||
pipe \
|
||||
container_id_for(container_name: container_name(version)),
|
||||
xargs(docker(:inspect, "--format", "'{{.NetworkSettings.IPAddress}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'")),
|
||||
[ :sed, "-e", "'s/\\/tcp$//'" ]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
require "active_support/core_ext/string/filters"
|
||||
|
||||
class Kamal::Commands::Builder < Kamal::Commands::Base
|
||||
delegate :create, :remove, :push, :clean, :pull, :info, :buildx_inspect, :validate_image, :first_mirror, to: :target
|
||||
delegate :local?, :remote?, to: "config.builder"
|
||||
delegate :create, :remove, :push, :clean, :pull, :info, :context_hosts, :config_context_hosts, :validate_image,
|
||||
:first_mirror, to: :target
|
||||
|
||||
include Clone
|
||||
|
||||
@@ -11,27 +11,43 @@ class Kamal::Commands::Builder < Kamal::Commands::Base
|
||||
end
|
||||
|
||||
def target
|
||||
if remote?
|
||||
if local?
|
||||
hybrid
|
||||
if config.builder.multiarch?
|
||||
if config.builder.remote?
|
||||
if config.builder.local?
|
||||
multiarch_remote
|
||||
else
|
||||
native_remote
|
||||
end
|
||||
else
|
||||
remote
|
||||
multiarch
|
||||
end
|
||||
else
|
||||
local
|
||||
if config.builder.cached?
|
||||
native_cached
|
||||
else
|
||||
native
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def remote
|
||||
@remote ||= Kamal::Commands::Builder::Remote.new(config)
|
||||
def native
|
||||
@native ||= Kamal::Commands::Builder::Native.new(config)
|
||||
end
|
||||
|
||||
def local
|
||||
@local ||= Kamal::Commands::Builder::Local.new(config)
|
||||
def native_cached
|
||||
@native ||= Kamal::Commands::Builder::Native::Cached.new(config)
|
||||
end
|
||||
|
||||
def hybrid
|
||||
@hybrid ||= Kamal::Commands::Builder::Hybrid.new(config)
|
||||
def native_remote
|
||||
@native ||= Kamal::Commands::Builder::Native::Remote.new(config)
|
||||
end
|
||||
|
||||
def multiarch
|
||||
@multiarch ||= Kamal::Commands::Builder::Multiarch.new(config)
|
||||
end
|
||||
|
||||
def multiarch_remote
|
||||
@multiarch_remote ||= Kamal::Commands::Builder::Multiarch::Remote.new(config)
|
||||
end
|
||||
|
||||
|
||||
|
||||
@@ -1,41 +1,20 @@
|
||||
|
||||
class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
||||
class BuilderError < StandardError; end
|
||||
|
||||
ENDPOINT_DOCKER_HOST_INSPECT = "'{{.Endpoints.docker.Host}}'"
|
||||
|
||||
delegate :argumentize, to: Kamal::Utils
|
||||
delegate \
|
||||
:args, :secrets, :dockerfile, :target, :arches, :local_arches, :remote_arches, :remote,
|
||||
:cache_from, :cache_to, :ssh, :driver, :docker_driver?,
|
||||
to: :builder_config
|
||||
delegate :args, :secrets, :dockerfile, :target, :local_arch, :local_host, :remote_arch, :remote_host, :cache_from, :cache_to, :ssh, to: :builder_config
|
||||
|
||||
def clean
|
||||
docker :image, :rm, "--force", config.absolute_image
|
||||
end
|
||||
|
||||
def push
|
||||
docker :buildx, :build,
|
||||
"--push",
|
||||
*platform_options(arches),
|
||||
*([ "--builder", builder_name ] unless docker_driver?),
|
||||
*build_options,
|
||||
build_context
|
||||
end
|
||||
|
||||
def pull
|
||||
docker :pull, config.absolute_image
|
||||
end
|
||||
|
||||
def info
|
||||
combine \
|
||||
docker(:context, :ls),
|
||||
docker(:buildx, :ls)
|
||||
end
|
||||
|
||||
def buildx_inspect
|
||||
docker :buildx, :inspect, builder_name unless docker_driver?
|
||||
end
|
||||
|
||||
def build_options
|
||||
[ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh ]
|
||||
end
|
||||
@@ -53,6 +32,14 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
||||
)
|
||||
end
|
||||
|
||||
def context_hosts
|
||||
:true
|
||||
end
|
||||
|
||||
def config_context_hosts
|
||||
[]
|
||||
end
|
||||
|
||||
def first_mirror
|
||||
docker(:info, "--format '{{index .RegistryConfig.Mirrors 0}}'")
|
||||
end
|
||||
@@ -104,8 +91,4 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
||||
def context_host(builder_name)
|
||||
docker :context, :inspect, builder_name, "--format", ENDPOINT_DOCKER_HOST_INSPECT
|
||||
end
|
||||
|
||||
def platform_options(arches)
|
||||
argumentize "--platform", arches.map { |arch| "linux/#{arch}" }.join(",") if arches.any?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
class Kamal::Commands::Builder::Hybrid < Kamal::Commands::Builder::Remote
|
||||
def create
|
||||
combine \
|
||||
create_local_buildx,
|
||||
create_remote_context,
|
||||
append_remote_buildx
|
||||
end
|
||||
|
||||
private
|
||||
def builder_name
|
||||
"kamal-hybrid-#{driver}-#{remote.gsub(/[^a-z0-9_-]/, "-")}"
|
||||
end
|
||||
|
||||
def create_local_buildx
|
||||
docker :buildx, :create, *platform_options(local_arches), "--name", builder_name, "--driver=#{driver}"
|
||||
end
|
||||
|
||||
def append_remote_buildx
|
||||
docker :buildx, :create, *platform_options(remote_arches), "--append", "--name", builder_name, builder_name
|
||||
end
|
||||
end
|
||||
@@ -1,14 +0,0 @@
|
||||
class Kamal::Commands::Builder::Local < Kamal::Commands::Builder::Base
|
||||
def create
|
||||
docker :buildx, :create, "--name", builder_name, "--driver=#{driver}" unless docker_driver?
|
||||
end
|
||||
|
||||
def remove
|
||||
docker :buildx, :rm, builder_name unless docker_driver?
|
||||
end
|
||||
|
||||
private
|
||||
def builder_name
|
||||
"kamal-local-#{driver}"
|
||||
end
|
||||
end
|
||||
41
lib/kamal/commands/builder/multiarch.rb
Normal file
41
lib/kamal/commands/builder/multiarch.rb
Normal file
@@ -0,0 +1,41 @@
|
||||
class Kamal::Commands::Builder::Multiarch < Kamal::Commands::Builder::Base
|
||||
def create
|
||||
docker :buildx, :create, "--use", "--name", builder_name
|
||||
end
|
||||
|
||||
def remove
|
||||
docker :buildx, :rm, builder_name
|
||||
end
|
||||
|
||||
def info
|
||||
combine \
|
||||
docker(:context, :ls),
|
||||
docker(:buildx, :ls)
|
||||
end
|
||||
|
||||
def push
|
||||
docker :buildx, :build,
|
||||
"--push",
|
||||
"--platform", platform_names,
|
||||
"--builder", builder_name,
|
||||
*build_options,
|
||||
build_context
|
||||
end
|
||||
|
||||
def context_hosts
|
||||
docker :buildx, :inspect, builder_name, "> /dev/null"
|
||||
end
|
||||
|
||||
private
|
||||
def builder_name
|
||||
"kamal-#{config.service}-multiarch"
|
||||
end
|
||||
|
||||
def platform_names
|
||||
if local_arch
|
||||
"linux/#{local_arch}"
|
||||
else
|
||||
"linux/amd64,linux/arm64"
|
||||
end
|
||||
end
|
||||
end
|
||||
61
lib/kamal/commands/builder/multiarch/remote.rb
Normal file
61
lib/kamal/commands/builder/multiarch/remote.rb
Normal file
@@ -0,0 +1,61 @@
|
||||
class Kamal::Commands::Builder::Multiarch::Remote < Kamal::Commands::Builder::Multiarch
|
||||
def create
|
||||
combine \
|
||||
create_contexts,
|
||||
create_local_buildx,
|
||||
append_remote_buildx
|
||||
end
|
||||
|
||||
def remove
|
||||
combine \
|
||||
remove_contexts,
|
||||
super
|
||||
end
|
||||
|
||||
def context_hosts
|
||||
chain \
|
||||
context_host(builder_name_with_arch(local_arch)),
|
||||
context_host(builder_name_with_arch(remote_arch))
|
||||
end
|
||||
|
||||
def config_context_hosts
|
||||
[ local_host, remote_host ].compact
|
||||
end
|
||||
|
||||
private
|
||||
def builder_name
|
||||
super + "-remote"
|
||||
end
|
||||
|
||||
def builder_name_with_arch(arch)
|
||||
"#{builder_name}-#{arch}"
|
||||
end
|
||||
|
||||
def create_local_buildx
|
||||
docker :buildx, :create, "--name", builder_name, builder_name_with_arch(local_arch), "--platform", "linux/#{local_arch}"
|
||||
end
|
||||
|
||||
def append_remote_buildx
|
||||
docker :buildx, :create, "--append", "--name", builder_name, builder_name_with_arch(remote_arch), "--platform", "linux/#{remote_arch}"
|
||||
end
|
||||
|
||||
def create_contexts
|
||||
combine \
|
||||
create_context(local_arch, local_host),
|
||||
create_context(remote_arch, remote_host)
|
||||
end
|
||||
|
||||
def create_context(arch, host)
|
||||
docker :context, :create, builder_name_with_arch(arch), "--description", "'#{builder_name} #{arch} native host'", "--docker", "'host=#{host}'"
|
||||
end
|
||||
|
||||
def remove_contexts
|
||||
combine \
|
||||
remove_context(local_arch),
|
||||
remove_context(remote_arch)
|
||||
end
|
||||
|
||||
def remove_context(arch)
|
||||
docker :context, :rm, builder_name_with_arch(arch)
|
||||
end
|
||||
end
|
||||
20
lib/kamal/commands/builder/native.rb
Normal file
20
lib/kamal/commands/builder/native.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
class Kamal::Commands::Builder::Native < Kamal::Commands::Builder::Base
|
||||
def create
|
||||
# No-op on native without cache
|
||||
end
|
||||
|
||||
def remove
|
||||
# No-op on native without cache
|
||||
end
|
||||
|
||||
def info
|
||||
# No-op on native
|
||||
end
|
||||
|
||||
def push
|
||||
combine \
|
||||
docker(:build, *build_options, build_context),
|
||||
docker(:push, config.absolute_image),
|
||||
docker(:push, config.latest_image)
|
||||
end
|
||||
end
|
||||
25
lib/kamal/commands/builder/native/cached.rb
Normal file
25
lib/kamal/commands/builder/native/cached.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
class Kamal::Commands::Builder::Native::Cached < Kamal::Commands::Builder::Native
|
||||
def create
|
||||
docker :buildx, :create, "--name", builder_name, "--use", "--driver=docker-container"
|
||||
end
|
||||
|
||||
def remove
|
||||
docker :buildx, :rm, builder_name
|
||||
end
|
||||
|
||||
def push
|
||||
docker :buildx, :build,
|
||||
"--push",
|
||||
*build_options,
|
||||
build_context
|
||||
end
|
||||
|
||||
def context_hosts
|
||||
docker :buildx, :inspect, builder_name, "> /dev/null"
|
||||
end
|
||||
|
||||
private
|
||||
def builder_name
|
||||
"kamal-#{config.service}-native-cached"
|
||||
end
|
||||
end
|
||||
67
lib/kamal/commands/builder/native/remote.rb
Normal file
67
lib/kamal/commands/builder/native/remote.rb
Normal file
@@ -0,0 +1,67 @@
|
||||
class Kamal::Commands::Builder::Native::Remote < Kamal::Commands::Builder::Native
|
||||
def create
|
||||
chain \
|
||||
create_context,
|
||||
create_buildx
|
||||
end
|
||||
|
||||
def remove
|
||||
chain \
|
||||
remove_context,
|
||||
remove_buildx
|
||||
end
|
||||
|
||||
def info
|
||||
chain \
|
||||
docker(:context, :ls),
|
||||
docker(:buildx, :ls)
|
||||
end
|
||||
|
||||
def push
|
||||
docker :buildx, :build,
|
||||
"--push",
|
||||
"--platform", platform,
|
||||
"--builder", builder_name,
|
||||
*build_options,
|
||||
build_context
|
||||
end
|
||||
|
||||
def context_hosts
|
||||
context_host(builder_name_with_arch)
|
||||
end
|
||||
|
||||
def config_context_hosts
|
||||
[ remote_host ]
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
def builder_name
|
||||
"kamal-#{config.service}-native-remote"
|
||||
end
|
||||
|
||||
def builder_name_with_arch
|
||||
"#{builder_name}-#{remote_arch}"
|
||||
end
|
||||
|
||||
def platform
|
||||
"linux/#{remote_arch}"
|
||||
end
|
||||
|
||||
def create_context
|
||||
docker :context, :create,
|
||||
builder_name_with_arch, "--description", "'#{builder_name} #{remote_arch} native host'", "--docker", "'host=#{remote_host}'"
|
||||
end
|
||||
|
||||
def remove_context
|
||||
docker :context, :rm, builder_name_with_arch
|
||||
end
|
||||
|
||||
def create_buildx
|
||||
docker :buildx, :create, "--name", builder_name, builder_name_with_arch, "--platform", platform
|
||||
end
|
||||
|
||||
def remove_buildx
|
||||
docker :buildx, :rm, builder_name
|
||||
end
|
||||
end
|
||||
@@ -1,40 +0,0 @@
|
||||
class Kamal::Commands::Builder::Remote < Kamal::Commands::Builder::Base
|
||||
def create
|
||||
chain \
|
||||
create_remote_context,
|
||||
create_buildx
|
||||
end
|
||||
|
||||
def remove
|
||||
chain \
|
||||
remove_remote_context,
|
||||
remove_buildx
|
||||
end
|
||||
|
||||
def info
|
||||
chain \
|
||||
docker(:context, :ls),
|
||||
docker(:buildx, :ls)
|
||||
end
|
||||
|
||||
private
|
||||
def builder_name
|
||||
"kamal-remote-#{driver}-#{remote.gsub(/[^a-z0-9_-]/, "-")}"
|
||||
end
|
||||
|
||||
def create_remote_context
|
||||
docker :context, :create, builder_name, "--description", "'#{builder_name} host'", "--docker", "'host=#{remote}'"
|
||||
end
|
||||
|
||||
def remove_remote_context
|
||||
docker :context, :rm, builder_name
|
||||
end
|
||||
|
||||
def create_buildx
|
||||
docker :buildx, :create, "--name", builder_name, builder_name
|
||||
end
|
||||
|
||||
def remove_buildx
|
||||
docker :buildx, :rm, builder_name
|
||||
end
|
||||
end
|
||||
68
lib/kamal/commands/proxy.rb
Normal file
68
lib/kamal/commands/proxy.rb
Normal file
@@ -0,0 +1,68 @@
|
||||
class Kamal::Commands::Proxy < Kamal::Commands::Base
|
||||
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||
delegate :container_name, to: :proxy_config
|
||||
|
||||
attr_reader :proxy_config
|
||||
|
||||
def initialize(config)
|
||||
super
|
||||
@proxy_config = config.proxy
|
||||
end
|
||||
|
||||
def run
|
||||
docker :run,
|
||||
"--name", container_name,
|
||||
"--detach",
|
||||
"--restart", "unless-stopped",
|
||||
*proxy_config.publish_args,
|
||||
"--volume", "/var/run/docker.sock:/var/run/docker.sock",
|
||||
"--volume", "#{container_name}:/root/.config/kamal-proxy",
|
||||
*config.logging_args,
|
||||
proxy_config.image
|
||||
end
|
||||
|
||||
def start
|
||||
docker :container, :start, container_name
|
||||
end
|
||||
|
||||
def stop(name: container_name)
|
||||
docker :container, :stop, name
|
||||
end
|
||||
|
||||
def start_or_run
|
||||
combine start, run, by: "||"
|
||||
end
|
||||
|
||||
def deploy(service, target:)
|
||||
docker :exec, container_name, "kamal-proxy", :deploy, service, *optionize({ target: target }), *proxy_config.deploy_command_args
|
||||
end
|
||||
|
||||
def remove(service, target:)
|
||||
docker :exec, container_name, "kamal-proxy", :remove, service, *optionize({ target: target })
|
||||
end
|
||||
|
||||
def info
|
||||
docker :ps, "--filter", "name=^#{container_name}$"
|
||||
end
|
||||
|
||||
def logs(since: nil, lines: nil, grep: nil, grep_options: nil)
|
||||
pipe \
|
||||
docker(:logs, container_name, (" --since #{since}" if since), (" --tail #{lines}" if lines), "--timestamps", "2>&1"),
|
||||
("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep)
|
||||
end
|
||||
|
||||
def follow_logs(host:, grep: nil, grep_options: nil)
|
||||
run_over_ssh pipe(
|
||||
docker(:logs, container_name, "--timestamps", "--tail", "10", "--follow", "2>&1"),
|
||||
(%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep)
|
||||
).join(" "), host: host
|
||||
end
|
||||
|
||||
def remove_container
|
||||
docker :container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=kamal-proxy"
|
||||
end
|
||||
|
||||
def remove_image
|
||||
docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=kamal-proxy"
|
||||
end
|
||||
end
|
||||
@@ -11,7 +11,7 @@ class Kamal::Configuration
|
||||
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||
|
||||
attr_reader :destination, :raw_config
|
||||
attr_reader :accessories, :aliases, :boot, :builder, :env, :healthcheck, :logging, :traefik, :servers, :ssh, :sshkit, :registry
|
||||
attr_reader :accessories, :aliases, :boot, :builder, :env, :healthcheck, :logging, :proxy, :traefik, :servers, :ssh, :sshkit, :registry
|
||||
|
||||
include Validation
|
||||
|
||||
@@ -61,6 +61,7 @@ class Kamal::Configuration
|
||||
|
||||
@healthcheck = Healthcheck.new(healthcheck_config: @raw_config.healthcheck)
|
||||
@logging = Logging.new(logging_config: @raw_config.logging)
|
||||
@proxy = Proxy.new(config: self)
|
||||
@traefik = Traefik.new(config: self)
|
||||
@ssh = Ssh.new(config: self)
|
||||
@sshkit = Sshkit.new(config: self)
|
||||
@@ -142,6 +143,10 @@ class Kamal::Configuration
|
||||
traefik_roles.flat_map(&:hosts).uniq
|
||||
end
|
||||
|
||||
def proxy_hosts
|
||||
proxy.hosts
|
||||
end
|
||||
|
||||
def repository
|
||||
[ registry.server, image ].compact.join("/")
|
||||
end
|
||||
|
||||
@@ -19,36 +19,16 @@ class Kamal::Configuration::Builder
|
||||
builder_config
|
||||
end
|
||||
|
||||
def remote
|
||||
builder_config["remote"]
|
||||
end
|
||||
|
||||
def arches
|
||||
Array(builder_config.fetch("arch", default_arch))
|
||||
end
|
||||
|
||||
def local_arches
|
||||
@local_arches ||= if remote
|
||||
arches & [ Kamal::Utils.docker_arch ]
|
||||
else
|
||||
arches
|
||||
end
|
||||
end
|
||||
|
||||
def remote_arches
|
||||
@remote_arches ||= if remote
|
||||
arches - local_arches
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def remote?
|
||||
remote_arches.any?
|
||||
def multiarch?
|
||||
builder_config["multiarch"] != false
|
||||
end
|
||||
|
||||
def local?
|
||||
arches.empty? || local_arches.any?
|
||||
!!builder_config["local"]
|
||||
end
|
||||
|
||||
def remote?
|
||||
!!builder_config["remote"]
|
||||
end
|
||||
|
||||
def cached?
|
||||
@@ -75,8 +55,20 @@ class Kamal::Configuration::Builder
|
||||
builder_config["context"] || "."
|
||||
end
|
||||
|
||||
def driver
|
||||
builder_config.fetch("driver", "docker-container")
|
||||
def local_arch
|
||||
builder_config["local"]["arch"] if local?
|
||||
end
|
||||
|
||||
def local_host
|
||||
builder_config["local"]["host"] if local?
|
||||
end
|
||||
|
||||
def remote_arch
|
||||
builder_config["remote"]["arch"] if remote?
|
||||
end
|
||||
|
||||
def remote_host
|
||||
builder_config["remote"]["host"] if remote?
|
||||
end
|
||||
|
||||
def cache_from
|
||||
@@ -122,23 +114,7 @@ class Kamal::Configuration::Builder
|
||||
end
|
||||
end
|
||||
|
||||
def docker_driver?
|
||||
driver == "docker"
|
||||
end
|
||||
|
||||
private
|
||||
def valid?
|
||||
if docker_driver?
|
||||
raise ArgumentError, "Invalid builder configuration: the `docker` driver does not not support remote builders" if remote
|
||||
raise ArgumentError, "Invalid builder configuration: the `docker` driver does not not support caching" if cached?
|
||||
raise ArgumentError, "Invalid builder configuration: the `docker` driver does not not support multiple arches" if arches.many?
|
||||
end
|
||||
|
||||
if @options["cache"] && @options["cache"]["type"]
|
||||
raise ArgumentError, "Invalid cache type: #{@options["cache"]["type"]}" unless [ "gha", "registry" ].include?(@options["cache"]["type"])
|
||||
end
|
||||
end
|
||||
|
||||
def cache_image
|
||||
builder_config["cache"]&.fetch("image", nil) || "#{image}-build-cache"
|
||||
end
|
||||
@@ -174,8 +150,4 @@ class Kamal::Configuration::Builder
|
||||
def pwd_sha
|
||||
Digest::SHA256.hexdigest(Dir.pwd)[0..12]
|
||||
end
|
||||
|
||||
def default_arch
|
||||
docker_driver? ? [] : [ "amd64", "arm64" ]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Builder
|
||||
#
|
||||
# The builder configuration controls how the application is built with `docker build`
|
||||
# The builder configuration controls how the application is built with `docker build` or `docker buildx build`
|
||||
#
|
||||
# If no configuration is specified, Kamal will:
|
||||
# 1. Create a buildx context called `kamal-local-docker-container`, using the docker-container driver
|
||||
# 2. Use `docker build` to build a multiarch image for linux/amd64,linux/arm64 with that context
|
||||
# 1. Create a buildx context called `kamal-<service>-multiarch`
|
||||
# 2. Use `docker buildx build` to build a multiarch image for linux/amd64,linux/arm64 with that context
|
||||
#
|
||||
# See https://kamal-deploy.org/docs/configuration/builder-examples/ for more information
|
||||
|
||||
@@ -12,29 +12,36 @@
|
||||
#
|
||||
# Options go under the builder key in the root configuration.
|
||||
builder:
|
||||
# Driver
|
||||
#
|
||||
# The build driver to use, defaults to `docker-container`
|
||||
driver: docker
|
||||
|
||||
# Arch
|
||||
# Multiarch
|
||||
#
|
||||
# The architectures to build for, defaults to `[ amd64, arm64 ]`
|
||||
# Unless you are using the docker driver, when it defaults to the local architecture
|
||||
# You can set an array or just a single value
|
||||
arch:
|
||||
- amd64
|
||||
# Enables multiarch builds, defaults to `true`
|
||||
multiarch: false
|
||||
|
||||
# Local configuration
|
||||
#
|
||||
# The build configuration for local builds, only used if multiarch is enabled (the default)
|
||||
#
|
||||
# If there is no remote configuration, by default we build for amd64 and arm64.
|
||||
# If you only want to build for one architecture, you can specify it here.
|
||||
# The docker socket is optional and uses the default docker host socket when not specified
|
||||
local:
|
||||
arch: amd64
|
||||
host: /var/run/docker.sock
|
||||
|
||||
# Remote configuration
|
||||
#
|
||||
# If you have a remote builder, you can configure it here
|
||||
remote: ssh://docker@docker-builder
|
||||
# The build configuration for remote builds, also only used if multiarch is enabled.
|
||||
# The arch is required and can be either amd64 or arm64.
|
||||
remote:
|
||||
arch: arm64
|
||||
host: ssh://docker@docker-builder
|
||||
|
||||
# Builder cache
|
||||
#
|
||||
# The type must be either 'gha' or 'registry'
|
||||
#
|
||||
# The image is only used for registry cache. Not compatible with the docker driver
|
||||
# The image is only used for registry cache
|
||||
cache:
|
||||
type: registry
|
||||
options: mode=max
|
||||
|
||||
@@ -143,6 +143,12 @@ accessories:
|
||||
traefik:
|
||||
...
|
||||
|
||||
# Proxy
|
||||
#
|
||||
# **Experimental** Configuration for kamal-proxy the replacement for Traefik, see kamal docs proxy
|
||||
proxy:
|
||||
...
|
||||
|
||||
# SSHKit
|
||||
#
|
||||
# See kamal docs sshkit
|
||||
|
||||
114
lib/kamal/configuration/docs/proxy.yml
Normal file
114
lib/kamal/configuration/docs/proxy.yml
Normal file
@@ -0,0 +1,114 @@
|
||||
# Proxy
|
||||
#
|
||||
# **Experimental** [kamal-proxy](http://github.com/basecamp/kamal-proxy) is a
|
||||
# custom built specifically for Kamal. It will replace Traefik in Kamal v2.0,
|
||||
# but currently is available as an experimental feature.
|
||||
#
|
||||
# When this is enabled, the proxy will be started on the hosts listed under the hosts key.
|
||||
# In addition, the kamal traefik command will be disabled and replaced by kamal proxy.
|
||||
#
|
||||
# The kamal proxy command works identically to kamal traefik on hosts that have not
|
||||
# been included. It will also handle switching between Traefik and kamal-proxy when you
|
||||
# run kamal proxy reboot.
|
||||
|
||||
# Limitations
|
||||
#
|
||||
# Currently the proxy will run on ports 80 and 443 and will bind to those
|
||||
# ports on the host.
|
||||
#
|
||||
# There is no way to set custom options for `docker run` when booting the proxy.
|
||||
#
|
||||
# If you have custom Traefik configuration via labels or boot arguments they may
|
||||
# not have an equivalent in kamal-proxy.
|
||||
|
||||
# Proxy settings
|
||||
#
|
||||
# The proxy is configured in the root configuration under `traefik`. These are
|
||||
# options that are set when deploying the application, not when booting the proxy
|
||||
#
|
||||
# They are application specific, so are not shared when multiple applications
|
||||
# with the same proxy.
|
||||
proxy:
|
||||
|
||||
# Enabled
|
||||
#
|
||||
# Whether to enable experimental proxy support. Defaults to false
|
||||
enabled: true
|
||||
|
||||
# Hosts
|
||||
#
|
||||
# The hosts to run the proxy on, instead of Traefik
|
||||
# This is a temporary setting and will be removed when we full switch to kamal-proxy
|
||||
#
|
||||
# If you run `kamal traefik reboot`, then the proxy will be started on these hosts
|
||||
# in place of traefik.
|
||||
hosts:
|
||||
- 10.0.0.1
|
||||
- 10.0.0.2
|
||||
|
||||
# Host
|
||||
#
|
||||
# This is the host that will be used to serve the app. By setting this you can run
|
||||
# multiple apps on the same server sharing the same instance of the proxy.
|
||||
#
|
||||
# If this is set only requests that match this host will be forwarded by the proxy.
|
||||
# if this is not set, then all requests will be forwarded, except for matching
|
||||
# requests for other apps that do have a host set.
|
||||
host: foo.example.com
|
||||
|
||||
# Deploy timeout
|
||||
#
|
||||
# How long to wait for the app to boot when deploying, defaults to 30 seconds
|
||||
deploy_timeout: 10s
|
||||
|
||||
# Response timeout
|
||||
#
|
||||
# How long to wait for requests to complete before timing out, defaults to 30 seconds
|
||||
response_timeout: 10
|
||||
|
||||
# Healthcheck
|
||||
#
|
||||
# When deploying, the proxy will by default hit /up once every second until we hit
|
||||
# the deploy timeout, with a 5 second timeout for each request.
|
||||
#
|
||||
# Once the app is up, the proxy will stop hitting the healthcheck endpoint.
|
||||
healthcheck:
|
||||
interval: 3
|
||||
path: /health
|
||||
timeout: 3
|
||||
|
||||
# Buffering
|
||||
#
|
||||
# Whether to buffer request and response bodies in the proxy
|
||||
#
|
||||
# By default buffering is enabled with a max request body size of 1GB and no limit
|
||||
# for response size.
|
||||
#
|
||||
# You can also set the memory limit for buffering, which defaults to 1MB, anything
|
||||
# larger than that is written to disk.
|
||||
buffering:
|
||||
requests: true
|
||||
responses: true
|
||||
max_request_body: 40_000_000
|
||||
max_response_body: 0
|
||||
memory: 2_000_000
|
||||
|
||||
# Logging
|
||||
#
|
||||
# Configure request logging for the proxy
|
||||
# You can specify request and response headers to log.
|
||||
# By default, Cache-Control and Last-Modified request headers are logged
|
||||
logging:
|
||||
request_headers:
|
||||
- Cache-Control
|
||||
- X-Forwarded-Proto
|
||||
response_headers:
|
||||
- X-Request-ID
|
||||
- X-Request-Start
|
||||
|
||||
# Forward headers
|
||||
#
|
||||
# Whether to forward the X-Forwarded-For and X-Forwarded-Proto headers (defaults to false)
|
||||
#
|
||||
# If you are behind a trusted proxy, you can set this to true to forward the headers.
|
||||
forward_headers: true
|
||||
66
lib/kamal/configuration/proxy.rb
Normal file
66
lib/kamal/configuration/proxy.rb
Normal file
@@ -0,0 +1,66 @@
|
||||
class Kamal::Configuration::Proxy
|
||||
include Kamal::Configuration::Validation
|
||||
|
||||
DEFAULT_HTTP_PORT = 80
|
||||
DEFAULT_HTTPS_PORT = 443
|
||||
DEFAULT_IMAGE = "basecamp/kamal-proxy:latest"
|
||||
DEFAULT_LOG_REQUEST_HEADERS = [ "Cache-Control", "Last-Modified" ]
|
||||
|
||||
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||
|
||||
def initialize(config:)
|
||||
@proxy_config = config.raw_config.proxy || {}
|
||||
validate! proxy_config
|
||||
end
|
||||
|
||||
def enabled?
|
||||
!!proxy_config.fetch("enabled", false)
|
||||
end
|
||||
|
||||
def hosts
|
||||
if enabled?
|
||||
proxy_config.fetch("hosts", [])
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def image
|
||||
proxy_config.fetch("image", DEFAULT_IMAGE)
|
||||
end
|
||||
|
||||
def container_name
|
||||
"kamal-proxy"
|
||||
end
|
||||
|
||||
def publish_args
|
||||
argumentize "--publish", [ "#{DEFAULT_HTTP_PORT}:#{DEFAULT_HTTP_PORT}", "#{DEFAULT_HTTPS_PORT}:#{DEFAULT_HTTPS_PORT}" ]
|
||||
end
|
||||
|
||||
def deploy_options
|
||||
{
|
||||
host: proxy_config["host"],
|
||||
"deploy-timeout": proxy_config["deploy_timeout"],
|
||||
"drain-timeout": proxy_config["drain_timeout"],
|
||||
"health-check-interval": proxy_config.dig("health_check", "interval"),
|
||||
"health-check-timeout": proxy_config.dig("health_check", "timeout"),
|
||||
"health-check-path": proxy_config.dig("health_check", "path"),
|
||||
"target-timeout": proxy_config["response_timeout"],
|
||||
"buffer-requests": proxy_config.fetch("buffering", { "requests": true }).fetch("requests", true),
|
||||
"buffer-responses": proxy_config.fetch("buffering", { "responses": true }).fetch("responses", true),
|
||||
"buffer-memory": proxy_config.dig("buffering", "memory"),
|
||||
"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"),
|
||||
"log-request-header": proxy_config.dig("logging", "request_headers") || DEFAULT_LOG_REQUEST_HEADERS,
|
||||
"log-response-header": proxy_config.dig("logging", "response_headers")
|
||||
}.compact
|
||||
end
|
||||
|
||||
def deploy_command_args
|
||||
optionize deploy_options
|
||||
end
|
||||
|
||||
private
|
||||
attr_accessor :proxy_config
|
||||
end
|
||||
@@ -58,10 +58,18 @@ class Kamal::Configuration::Role
|
||||
default_labels.merge(traefik_labels).merge(custom_labels)
|
||||
end
|
||||
|
||||
def labels_for_proxy
|
||||
default_labels.merge(custom_labels)
|
||||
end
|
||||
|
||||
def label_args
|
||||
argumentize "--label", labels
|
||||
end
|
||||
|
||||
def label_args_for_proxy
|
||||
argumentize "--label", labels_for_proxy
|
||||
end
|
||||
|
||||
def logging_args
|
||||
logging.args
|
||||
end
|
||||
|
||||
@@ -28,11 +28,7 @@ class Kamal::Configuration::Validator
|
||||
elsif key == "hosts"
|
||||
validate_servers! value
|
||||
elsif example_value.is_a?(Array)
|
||||
if key == "arch"
|
||||
validate_array_of_or_type! value, example_value.first.class
|
||||
else
|
||||
validate_array_of! value, example_value.first.class
|
||||
end
|
||||
validate_array_of! value, example_value.first.class
|
||||
elsif example_value.is_a?(Hash)
|
||||
case key.to_s
|
||||
when "options", "args"
|
||||
@@ -75,16 +71,6 @@ class Kamal::Configuration::Validator
|
||||
value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Numeric) || value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
||||
end
|
||||
|
||||
def validate_array_of_or_type!(value, type)
|
||||
if value.is_a?(Array)
|
||||
validate_array_of! value, type
|
||||
else
|
||||
validate_type! value, type
|
||||
end
|
||||
rescue Kamal::ConfigurationError
|
||||
type_error(Array, type)
|
||||
end
|
||||
|
||||
def validate_array_of!(array, type)
|
||||
validate_type! array, Array
|
||||
|
||||
|
||||
@@ -5,7 +5,5 @@ class Kamal::Configuration::Validator::Builder < Kamal::Configuration::Validator
|
||||
if config["cache"] && config["cache"]["type"]
|
||||
error "Invalid cache type: #{config["cache"]["type"]}" unless [ "gha", "registry" ].include?(config["cache"]["type"])
|
||||
end
|
||||
|
||||
error "Builder arch not set" unless config["arch"].present?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -81,16 +81,4 @@ module Kamal::Utils
|
||||
def join_commands(commands)
|
||||
commands.map(&:strip).join(" ")
|
||||
end
|
||||
|
||||
def docker_arch
|
||||
arch = `docker info --format '{{.Architecture}}'`.strip
|
||||
case arch
|
||||
when /aarch64/
|
||||
"arm64"
|
||||
when /x86_64/
|
||||
"amd64"
|
||||
else
|
||||
arch
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -356,6 +356,18 @@ class CliAppTest < CliTestCase
|
||||
end
|
||||
end
|
||||
|
||||
test "boot proxy" do
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version
|
||||
|
||||
run_command("boot", config: :with_proxy).tap do |output|
|
||||
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 --hostname 1.1.1.1-[0-9a-f]{12} -e KAMAL_CONTAINER_NAME="app-web-latest" -e KAMAL_VERSION="latest" --env-file .kamal\/env\/roles\/app-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"/, output
|
||||
assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def run_command(*command, config: :with_accessories, host: "1.1.1.1", allow_execute_error: false)
|
||||
stdouted do
|
||||
|
||||
@@ -21,12 +21,16 @@ class CliBuildTest < CliTestCase
|
||||
.with(:git, "-C", anything, :status, "--porcelain")
|
||||
.returns("")
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :buildx, :inspect, "kamal-app-multiarch", "> /dev/null")
|
||||
.returns("")
|
||||
|
||||
run_command("push", "--verbose").tap do |output|
|
||||
assert_hook_ran "pre-build", output, **hook_variables
|
||||
assert_match /Cloning repo into build directory/, output
|
||||
assert_match /git -C #{Dir.tmpdir}\/kamal-clones\/app-#{pwd_sha} clone #{Dir.pwd}/, output
|
||||
assert_match /docker --version && docker buildx version/, output
|
||||
assert_match /docker buildx build --push --platform linux\/amd64 --builder kamal-local-docker-container -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile \. as .*@localhost/, output
|
||||
assert_match /docker buildx build --push --platform linux\/amd64,linux\/arm64 --builder kamal-app-multiarch -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile \. as .*@localhost/, output
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -49,7 +53,7 @@ class CliBuildTest < CliTestCase
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :submodule, :update, "--init")
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
||||
.with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".")
|
||||
.with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "kamal-app-multiarch", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".")
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:git, "-C", anything, :"rev-parse", :HEAD)
|
||||
@@ -74,7 +78,7 @@ class CliBuildTest < CliTestCase
|
||||
assert_no_match /Cloning repo into build directory/, output
|
||||
assert_hook_ran "pre-build", output, **hook_variables
|
||||
assert_match /docker --version && docker buildx version/, output
|
||||
assert_match /docker buildx build --push --platform linux\/amd64 --builder kamal-local-docker-container -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile . as .*@localhost/, output
|
||||
assert_match /docker buildx build --push --platform linux\/amd64,linux\/arm64 --builder kamal-app-multiarch -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile . as .*@localhost/, output
|
||||
end
|
||||
end
|
||||
|
||||
@@ -120,10 +124,10 @@ class CliBuildTest < CliTestCase
|
||||
.with(:docker, "--version", "&&", :docker, :buildx, "version")
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
||||
.with(:docker, :buildx, :create, "--name", "kamal-local-docker-container", "--driver=docker-container")
|
||||
.with(:docker, :buildx, :create, "--use", "--name", "kamal-app-multiarch")
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
||||
.with(:docker, :buildx, :inspect, "kamal-local-docker-container")
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :buildx, :inspect, "kamal-app-multiarch", "> /dev/null")
|
||||
.raises(SSHKit::Command::Failed.new("no builder"))
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with { |*args| args.first.start_with?("git") }
|
||||
@@ -137,7 +141,7 @@ class CliBuildTest < CliTestCase
|
||||
.returns("")
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
||||
.with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".")
|
||||
.with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "kamal-app-multiarch", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".")
|
||||
|
||||
run_command("push").tap do |output|
|
||||
assert_match /WARN Missing compatible builder, so creating a new one first/, output
|
||||
@@ -161,7 +165,7 @@ class CliBuildTest < CliTestCase
|
||||
error = assert_raises(Kamal::Cli::HookError) { run_command("push") }
|
||||
assert_equal "Hook `pre-build` failed:\nfailed", error.message
|
||||
|
||||
assert @executions.none? { |args| args[0..2] == [ :docker, :build ] }
|
||||
assert @executions.none? { |args| args[0..2] == [ :docker, :buildx, :build ] }
|
||||
end
|
||||
|
||||
test "pull" do
|
||||
@@ -203,23 +207,23 @@ class CliBuildTest < CliTestCase
|
||||
|
||||
test "create" do
|
||||
run_command("create").tap do |output|
|
||||
assert_match /docker buildx create --name kamal-local-docker-container --driver=docker-container/, output
|
||||
assert_match /docker buildx create --use --name kamal-app-multiarch/, output
|
||||
end
|
||||
end
|
||||
|
||||
test "create remote" do
|
||||
run_command("create", fixture: :with_remote_builder).tap do |output|
|
||||
assert_match "Running /usr/bin/env true on 1.1.1.5", output
|
||||
assert_match "docker context create kamal-remote-docker-container-ssh---app-1-1-1-5 --description 'kamal-remote-docker-container-ssh---app-1-1-1-5 host' --docker 'host=ssh://app@1.1.1.5'", output
|
||||
assert_match "docker buildx create --name kamal-remote-docker-container-ssh---app-1-1-1-5 kamal-remote-docker-container-ssh---app-1-1-1-5", output
|
||||
assert_match "docker context create kamal-app-native-remote-amd64 --description 'kamal-app-native-remote amd64 native host' --docker 'host=ssh://app@1.1.1.5'", output
|
||||
assert_match "docker buildx create --name kamal-app-native-remote kamal-app-native-remote-amd64 --platform linux/amd64", output
|
||||
end
|
||||
end
|
||||
|
||||
test "create remote with custom ports" do
|
||||
run_command("create", fixture: :with_remote_builder_and_custom_ports).tap do |output|
|
||||
assert_match "Running /usr/bin/env true on 1.1.1.5", output
|
||||
assert_match "docker context create kamal-remote-docker-container-ssh---app-1-1-1-5-2122 --description 'kamal-remote-docker-container-ssh---app-1-1-1-5-2122 host' --docker 'host=ssh://app@1.1.1.5:2122'", output
|
||||
assert_match "docker buildx create --name kamal-remote-docker-container-ssh---app-1-1-1-5-2122 kamal-remote-docker-container-ssh---app-1-1-1-5-2122", output
|
||||
assert_match "docker context create kamal-app-native-remote-amd64 --description 'kamal-app-native-remote amd64 native host' --docker 'host=ssh://app@1.1.1.5:2122'", output
|
||||
assert_match "docker buildx create --name kamal-app-native-remote kamal-app-native-remote-amd64 --platform linux/amd64", output
|
||||
end
|
||||
end
|
||||
|
||||
@@ -236,7 +240,7 @@ class CliBuildTest < CliTestCase
|
||||
|
||||
test "remove" do
|
||||
run_command("remove").tap do |output|
|
||||
assert_match /docker buildx rm kamal-local/, output
|
||||
assert_match /docker buildx rm kamal-app-multiarch/, output
|
||||
end
|
||||
end
|
||||
|
||||
@@ -246,7 +250,7 @@ class CliBuildTest < CliTestCase
|
||||
.returns("docker builder info")
|
||||
|
||||
run_command("details").tap do |output|
|
||||
assert_match /Builder: local/, output
|
||||
assert_match /Builder: multiarch/, output
|
||||
assert_match /docker builder info/, output
|
||||
end
|
||||
end
|
||||
|
||||
@@ -36,8 +36,9 @@ class CliTestCase < ActiveSupport::TestCase
|
||||
.with { |arg1, arg2| arg1 == :mkdir && arg2 == ".kamal/locks/app" }
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.with { |arg1, arg2| arg1 == :rm && arg2 == ".kamal/locks/app/details" }
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.with(:docker, :buildx, :inspect, "kamal-local-docker-container")
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info)
|
||||
.with { |*args| args[0..2] == [ :docker, :buildx, :inspect ] }
|
||||
.returns("")
|
||||
end
|
||||
|
||||
def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: false)
|
||||
|
||||
@@ -121,6 +121,10 @@ class CliMainTest < CliTestCase
|
||||
.with(:git, "-C", anything, :status, "--porcelain")
|
||||
.returns("")
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :buildx, :inspect, "kamal-app-multiarch", "> /dev/null")
|
||||
.returns("")
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :info, "--format '{{index .RegistryConfig.Mirrors 0}}'")
|
||||
.returns("")
|
||||
@@ -155,6 +159,10 @@ class CliMainTest < CliTestCase
|
||||
.with(:git, "-C", anything, :status, "--porcelain")
|
||||
.returns("")
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :buildx, :inspect, "kamal-app-multiarch", "> /dev/null")
|
||||
.returns("")
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :info, "--format '{{index .RegistryConfig.Mirrors 0}}'")
|
||||
.returns("")
|
||||
|
||||
141
test/cli/proxy_test.rb
Normal file
141
test/cli/proxy_test.rb
Normal file
@@ -0,0 +1,141 @@
|
||||
require_relative "cli_test_case"
|
||||
|
||||
class CliProxyTest < CliTestCase
|
||||
test "boot" do
|
||||
run_command("boot").tap do |output|
|
||||
assert_match "docker login", output
|
||||
assert_match "docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", output
|
||||
end
|
||||
end
|
||||
|
||||
test "reboot" do
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{.NetworkSettings.IPAddress}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'", "|", :sed, "-e", "'s/\\/tcp$//'")
|
||||
.returns("172.1.0.2:80")
|
||||
.at_least_once
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with { |*args| args[0..1] == [ :sh, "-c" ] }
|
||||
.returns("123")
|
||||
.at_least_once
|
||||
|
||||
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 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 container prune --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output
|
||||
assert_match "docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE} on 1.1.1.1", output
|
||||
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"172.1.0.2:80\" --deploy-timeout \"6s\" on 1.1.1.1", output
|
||||
|
||||
assert_match "docker container stop kamal-proxy on 1.1.1.2", output
|
||||
assert_match "docker container stop 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 container prune --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.2", output
|
||||
assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" traefik:v2.10 --providers.docker --log.level=\"DEBUG\" on 1.1.1.2", output
|
||||
end
|
||||
end
|
||||
|
||||
test "reboot --rolling" do
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{.NetworkSettings.IPAddress}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'", "|", :sed, "-e", "'s/\\/tcp$//'")
|
||||
.returns("172.1.0.2:80")
|
||||
.at_least_once
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with { |*args| args[0..1] == [ :sh, "-c" ] }
|
||||
.returns("123")
|
||||
.at_least_once
|
||||
|
||||
run_command("reboot", "--rolling", "-y").tap do |output|
|
||||
assert_match "Running docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output
|
||||
end
|
||||
end
|
||||
|
||||
test "start" do
|
||||
run_command("start").tap do |output|
|
||||
assert_match "docker container start kamal-proxy", output
|
||||
end
|
||||
end
|
||||
|
||||
test "stop" do
|
||||
run_command("stop").tap do |output|
|
||||
assert_match "docker container stop kamal-proxy", output
|
||||
end
|
||||
end
|
||||
|
||||
test "restart" do
|
||||
Kamal::Cli::Proxy.any_instance.expects(:stop)
|
||||
Kamal::Cli::Proxy.any_instance.expects(:start)
|
||||
|
||||
run_command("restart")
|
||||
end
|
||||
|
||||
test "details" do
|
||||
run_command("details").tap do |output|
|
||||
assert_match "docker ps --filter name=^kamal-proxy$", output
|
||||
end
|
||||
end
|
||||
|
||||
test "logs" do
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:capture)
|
||||
.with(:docker, :logs, "kamal-proxy", " --tail 100", "--timestamps", "2>&1")
|
||||
.returns("Log entry")
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:capture)
|
||||
.with(:docker, :logs, "traefik", " --tail 100", "--timestamps", "2>&1")
|
||||
.returns("Log entry")
|
||||
|
||||
run_command("logs").tap do |output|
|
||||
assert_match "Proxy Host: 1.1.1.1", output
|
||||
assert_match "Log entry", output
|
||||
end
|
||||
end
|
||||
|
||||
test "logs with follow" do
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
|
||||
.with("ssh -t root@1.1.1.1 -p 22 'docker logs kamal-proxy --timestamps --tail 10 --follow 2>&1'")
|
||||
|
||||
assert_match "docker logs kamal-proxy --timestamps --tail 10 --follow", run_command("logs", "--follow")
|
||||
end
|
||||
|
||||
test "remove" do
|
||||
Kamal::Cli::Proxy.any_instance.expects(:stop)
|
||||
Kamal::Cli::Proxy.any_instance.expects(:remove_container)
|
||||
Kamal::Cli::Proxy.any_instance.expects(:remove_image)
|
||||
|
||||
run_command("remove")
|
||||
end
|
||||
|
||||
test "remove_container" do
|
||||
run_command("remove_container").tap do |output|
|
||||
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy", output
|
||||
end
|
||||
end
|
||||
|
||||
test "remove_image" do
|
||||
run_command("remove_image").tap do |output|
|
||||
assert_match "docker image prune --all --force --filter label=org.opencontainers.image.title=kamal-proxy", output
|
||||
end
|
||||
end
|
||||
|
||||
test "commands disallowed when proxy is disabled" do
|
||||
assert_raises_when_disabled "boot"
|
||||
assert_raises_when_disabled "reboot"
|
||||
assert_raises_when_disabled "start"
|
||||
assert_raises_when_disabled "stop"
|
||||
assert_raises_when_disabled "details"
|
||||
assert_raises_when_disabled "logs"
|
||||
assert_raises_when_disabled "remove"
|
||||
end
|
||||
|
||||
private
|
||||
def run_command(*command, fixture: :with_proxy)
|
||||
stdouted { Kamal::Cli::Proxy.start([ *command, "-c", "test/fixtures/deploy_#{fixture}.yml" ]) }
|
||||
end
|
||||
|
||||
def assert_raises_when_disabled(command)
|
||||
assert_raises "kamal proxy commands are disabled unless experimental proxy support is enabled. Use `kamal traefik` commands instead." do
|
||||
run_command(command, fixture: :with_accessories)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -5,7 +5,6 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
||||
@config = {
|
||||
service: "app", image: "dhh/app", registry: { "server" => "private.registry", "username" => "dhh", "password" => "secret" },
|
||||
servers: [ "1.1.1.1" ],
|
||||
builder: { "arch" => "amd64" },
|
||||
accessories: {
|
||||
"mysql" => {
|
||||
"image" => "private.registry/mysql:8.0",
|
||||
|
||||
@@ -5,7 +5,7 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
ENV["RAILS_MASTER_KEY"] = "456"
|
||||
Kamal::Configuration.any_instance.stubs(:run_id).returns("12345678901234567890123456789012")
|
||||
|
||||
@config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], env: { "secret" => [ "RAILS_MASTER_KEY" ] }, builder: { "arch" => "amd64" } }
|
||||
@config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], env: { "secret" => [ "RAILS_MASTER_KEY" ] } }
|
||||
end
|
||||
|
||||
teardown do
|
||||
|
||||
@@ -8,7 +8,7 @@ class CommandsAuditorTest < ActiveSupport::TestCase
|
||||
freeze_time
|
||||
|
||||
@config = {
|
||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, builder: { "arch" => "amd64" }, servers: [ "1.1.1.1" ]
|
||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ]
|
||||
}
|
||||
|
||||
@auditor = new_command
|
||||
|
||||
@@ -2,54 +2,54 @@ require "test_helper"
|
||||
|
||||
class CommandsBuilderTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], builder: { "arch" => "amd64" } }
|
||||
@config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ] }
|
||||
end
|
||||
|
||||
test "target linux/amd64 locally by default" do
|
||||
test "target multiarch by default" do
|
||||
builder = new_builder_command(builder: { "cache" => { "type" => "gha" } })
|
||||
assert_equal "local", builder.name
|
||||
assert_equal "multiarch", builder.name
|
||||
assert_equal \
|
||||
"docker buildx build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
|
||||
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
|
||||
builder.push.join(" ")
|
||||
end
|
||||
|
||||
test "target specified arch locally by default" do
|
||||
builder = new_builder_command(builder: { "arch" => [ "amd64" ] })
|
||||
assert_equal "local", builder.name
|
||||
test "target native when multiarch is off" do
|
||||
builder = new_builder_command(builder: { "multiarch" => false })
|
||||
assert_equal "native", builder.name
|
||||
assert_equal \
|
||||
"docker buildx build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile .",
|
||||
"docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile . && docker push dhh/app:123 && docker push dhh/app:latest",
|
||||
builder.push.join(" ")
|
||||
end
|
||||
|
||||
test "build with caching" do
|
||||
builder = new_builder_command(builder: { "cache" => { "type" => "gha" } })
|
||||
assert_equal "local", builder.name
|
||||
test "target native cached when multiarch is off and cache is set" do
|
||||
builder = new_builder_command(builder: { "multiarch" => false, "cache" => { "type" => "gha" } })
|
||||
assert_equal "native/cached", builder.name
|
||||
assert_equal \
|
||||
"docker buildx build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
|
||||
"docker buildx build --push -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
|
||||
builder.push.join(" ")
|
||||
end
|
||||
|
||||
test "hybrid build if remote is set and building multiarch" do
|
||||
builder = new_builder_command(builder: { "arch" => [ "amd64", "arm64" ], "remote" => "ssh://app@127.0.0.1", "cache" => { "type" => "gha" } })
|
||||
assert_equal "hybrid", builder.name
|
||||
test "target multiarch remote when local and remote is set" do
|
||||
builder = new_builder_command(builder: { "local" => {}, "remote" => {}, "cache" => { "type" => "gha" } })
|
||||
assert_equal "multiarch/remote", builder.name
|
||||
assert_equal \
|
||||
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-hybrid-docker-container-ssh---app-127-0-0-1 -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
|
||||
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch-remote -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
|
||||
builder.push.join(" ")
|
||||
end
|
||||
|
||||
test "target remote when remote set and arch is non local" do
|
||||
builder = new_builder_command(builder: { "arch" => [ "#{remote_arch}" ], "remote" => "ssh://app@host", "cache" => { "type" => "gha" } })
|
||||
assert_equal "remote", builder.name
|
||||
test "target multiarch local when arch is set" do
|
||||
builder = new_builder_command(builder: { "local" => { "arch" => "amd64" } })
|
||||
assert_equal "multiarch", builder.name
|
||||
assert_equal \
|
||||
"docker buildx build --push --platform linux/#{remote_arch} --builder kamal-remote-docker-container-ssh---app-host -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
|
||||
"docker buildx build --push --platform linux/amd64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile .",
|
||||
builder.push.join(" ")
|
||||
end
|
||||
|
||||
test "target local when remote set and arch is local" do
|
||||
builder = new_builder_command(builder: { "arch" => [ "#{local_arch}" ], "remote" => "ssh://app@host", "cache" => { "type" => "gha" } })
|
||||
assert_equal "local", builder.name
|
||||
test "target native remote when only remote is set" do
|
||||
builder = new_builder_command(builder: { "remote" => { "arch" => "amd64" }, "cache" => { "type" => "gha" } })
|
||||
assert_equal "native/remote", builder.name
|
||||
assert_equal \
|
||||
"docker buildx build --push --platform linux/#{local_arch} --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
|
||||
"docker buildx build --push --platform linux/amd64 --builder kamal-app-native-remote -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
|
||||
builder.push.join(" ")
|
||||
end
|
||||
|
||||
@@ -93,21 +93,28 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
||||
test "build context" do
|
||||
builder = new_builder_command(builder: { "context" => ".." })
|
||||
assert_equal \
|
||||
"docker buildx build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ..",
|
||||
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ..",
|
||||
builder.push.join(" ")
|
||||
end
|
||||
|
||||
test "push with build args" do
|
||||
test "native push with build args" do
|
||||
builder = new_builder_command(builder: { "multiarch" => false, "args" => { "a" => 1, "b" => 2 } })
|
||||
assert_equal \
|
||||
"docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile . && docker push dhh/app:123 && docker push dhh/app:latest",
|
||||
builder.push.join(" ")
|
||||
end
|
||||
|
||||
test "multiarch push with build args" do
|
||||
builder = new_builder_command(builder: { "args" => { "a" => 1, "b" => 2 } })
|
||||
assert_equal \
|
||||
"docker buildx build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile .",
|
||||
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile .",
|
||||
builder.push.join(" ")
|
||||
end
|
||||
|
||||
test "push with build secrets" do
|
||||
builder = new_builder_command(builder: { "secrets" => [ "a", "b" ] })
|
||||
test "native push with build secrets" do
|
||||
builder = new_builder_command(builder: { "multiarch" => false, "secrets" => [ "a", "b" ] })
|
||||
assert_equal \
|
||||
"docker buildx build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"a\" --secret id=\"b\" --file Dockerfile .",
|
||||
"docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"a\" --secret id=\"b\" --file Dockerfile . && docker push dhh/app:123 && docker push dhh/app:latest",
|
||||
builder.push.join(" ")
|
||||
end
|
||||
|
||||
@@ -123,13 +130,76 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
||||
assert_equal "docker inspect -f '{{ .Config.Labels.service }}' dhh/app:123 | grep -x app || (echo \"Image dhh/app:123 is missing the 'service' label\" && exit 1)", new_builder_command.validate_image.join(" ")
|
||||
end
|
||||
|
||||
test "context build" do
|
||||
test "multiarch context build" do
|
||||
builder = new_builder_command(builder: { "context" => "./foo" })
|
||||
assert_equal \
|
||||
"docker buildx build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo",
|
||||
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo",
|
||||
builder.push.join(" ")
|
||||
end
|
||||
|
||||
test "native context build" do
|
||||
builder = new_builder_command(builder: { "multiarch" => false, "context" => "./foo" })
|
||||
assert_equal \
|
||||
"docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo && docker push dhh/app:123 && docker push dhh/app:latest",
|
||||
builder.push.join(" ")
|
||||
end
|
||||
|
||||
test "cached context build" do
|
||||
builder = new_builder_command(builder: { "multiarch" => false, "context" => "./foo", "cache" => { "type" => "gha" } })
|
||||
assert_equal \
|
||||
"docker buildx build --push -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile ./foo",
|
||||
builder.push.join(" ")
|
||||
end
|
||||
|
||||
test "remote context build" do
|
||||
builder = new_builder_command(builder: { "remote" => { "arch" => "amd64" }, "context" => "./foo" })
|
||||
assert_equal \
|
||||
"docker buildx build --push --platform linux/amd64 --builder kamal-app-native-remote -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo",
|
||||
builder.push.join(" ")
|
||||
end
|
||||
|
||||
test "multiarch context hosts" do
|
||||
command = new_builder_command
|
||||
assert_equal "docker buildx inspect kamal-app-multiarch > /dev/null", command.context_hosts.join(" ")
|
||||
assert_equal "", command.config_context_hosts.join(" ")
|
||||
end
|
||||
|
||||
test "native context hosts" do
|
||||
command = new_builder_command(builder: { "multiarch" => false })
|
||||
assert_equal :true, command.context_hosts
|
||||
assert_equal "", command.config_context_hosts.join(" ")
|
||||
end
|
||||
|
||||
test "native cached context hosts" do
|
||||
command = new_builder_command(builder: { "multiarch" => false, "cache" => { "type" => "registry" } })
|
||||
assert_equal "docker buildx inspect kamal-app-native-cached > /dev/null", command.context_hosts.join(" ")
|
||||
assert_equal "", command.config_context_hosts.join(" ")
|
||||
end
|
||||
|
||||
test "native remote context hosts" do
|
||||
command = new_builder_command(builder: { "remote" => { "arch" => "amd64", "host" => "ssh://host" } })
|
||||
assert_equal "docker context inspect kamal-app-native-remote-amd64 --format '{{.Endpoints.docker.Host}}'", command.context_hosts.join(" ")
|
||||
assert_equal [ "ssh://host" ], command.config_context_hosts
|
||||
end
|
||||
|
||||
test "multiarch remote context hosts" do
|
||||
command = new_builder_command(builder: {
|
||||
"remote" => { "arch" => "amd64", "host" => "ssh://host" },
|
||||
"local" => { "arch" => "arm64" }
|
||||
})
|
||||
assert_equal "docker context inspect kamal-app-multiarch-remote-arm64 --format '{{.Endpoints.docker.Host}}' ; docker context inspect kamal-app-multiarch-remote-amd64 --format '{{.Endpoints.docker.Host}}'", command.context_hosts.join(" ")
|
||||
assert_equal [ "ssh://host" ], command.config_context_hosts
|
||||
end
|
||||
|
||||
test "multiarch remote context hosts with local host" do
|
||||
command = new_builder_command(builder: {
|
||||
"remote" => { "arch" => "amd64", "host" => "ssh://host" },
|
||||
"local" => { "arch" => "arm64", "host" => "unix:///var/run/docker.sock" }
|
||||
})
|
||||
assert_equal "docker context inspect kamal-app-multiarch-remote-arm64 --format '{{.Endpoints.docker.Host}}' ; docker context inspect kamal-app-multiarch-remote-amd64 --format '{{.Endpoints.docker.Host}}'", command.context_hosts.join(" ")
|
||||
assert_equal [ "unix:///var/run/docker.sock", "ssh://host" ], command.config_context_hosts
|
||||
end
|
||||
|
||||
test "mirror count" do
|
||||
command = new_builder_command
|
||||
assert_equal "docker info --format '{{index .RegistryConfig.Mirrors 0}}'", command.first_mirror.join(" ")
|
||||
@@ -137,18 +207,10 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
||||
|
||||
private
|
||||
def new_builder_command(additional_config = {})
|
||||
Kamal::Commands::Builder.new(Kamal::Configuration.new(@config.deep_merge(additional_config), version: "123"))
|
||||
Kamal::Commands::Builder.new(Kamal::Configuration.new(@config.merge(additional_config), version: "123"))
|
||||
end
|
||||
|
||||
def build_directory
|
||||
"#{Dir.tmpdir}/kamal-clones/app/kamal/"
|
||||
end
|
||||
|
||||
def local_arch
|
||||
Kamal::Utils.docker_arch
|
||||
end
|
||||
|
||||
def remote_arch
|
||||
Kamal::Utils.docker_arch == "arm64" ? "amd64" : "arm64"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,7 +3,7 @@ require "test_helper"
|
||||
class CommandsDockerTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@config = {
|
||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], builder: { "arch" => "amd64" }
|
||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ]
|
||||
}
|
||||
@docker = Kamal::Commands::Docker.new(Kamal::Configuration.new(@config))
|
||||
end
|
||||
|
||||
@@ -8,7 +8,7 @@ class CommandsHookTest < ActiveSupport::TestCase
|
||||
|
||||
@config = {
|
||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ],
|
||||
builder: { "arch" => "amd64" }, traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
|
||||
traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
|
||||
}
|
||||
|
||||
@performer = Kamal::Git.email.presence || `whoami`.chomp
|
||||
|
||||
@@ -4,7 +4,7 @@ class CommandsLockTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@config = {
|
||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ],
|
||||
builder: { "arch" => "amd64" }, traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
|
||||
traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
126
test/commands/proxy_test.rb
Normal file
126
test/commands/proxy_test.rb
Normal file
@@ -0,0 +1,126 @@
|
||||
require "test_helper"
|
||||
|
||||
class CommandsProxyTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@config = {
|
||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ]
|
||||
}
|
||||
|
||||
ENV["EXAMPLE_API_KEY"] = "456"
|
||||
end
|
||||
|
||||
teardown do
|
||||
ENV.delete("EXAMPLE_API_KEY")
|
||||
end
|
||||
|
||||
test "run" do
|
||||
assert_equal \
|
||||
"docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}",
|
||||
new_command.run.join(" ")
|
||||
end
|
||||
|
||||
test "run with ports configured" do
|
||||
assert_equal \
|
||||
"docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}",
|
||||
new_command.run.join(" ")
|
||||
end
|
||||
|
||||
test "run without configuration" do
|
||||
@config.delete(:proxy)
|
||||
|
||||
assert_equal \
|
||||
"docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}",
|
||||
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 --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}",
|
||||
new_command.run.join(" ")
|
||||
end
|
||||
|
||||
test "proxy start" do
|
||||
assert_equal \
|
||||
"docker container start kamal-proxy",
|
||||
new_command.start.join(" ")
|
||||
end
|
||||
|
||||
test "proxy stop" do
|
||||
assert_equal \
|
||||
"docker container stop kamal-proxy",
|
||||
new_command.stop.join(" ")
|
||||
end
|
||||
|
||||
test "proxy info" do
|
||||
assert_equal \
|
||||
"docker ps --filter name=^kamal-proxy$",
|
||||
new_command.info.join(" ")
|
||||
end
|
||||
|
||||
test "proxy logs" do
|
||||
assert_equal \
|
||||
"docker logs kamal-proxy --timestamps 2>&1",
|
||||
new_command.logs.join(" ")
|
||||
end
|
||||
|
||||
test "proxy logs since 2h" do
|
||||
assert_equal \
|
||||
"docker logs kamal-proxy --since 2h --timestamps 2>&1",
|
||||
new_command.logs(since: "2h").join(" ")
|
||||
end
|
||||
|
||||
test "proxy logs last 10 lines" do
|
||||
assert_equal \
|
||||
"docker logs kamal-proxy --tail 10 --timestamps 2>&1",
|
||||
new_command.logs(lines: 10).join(" ")
|
||||
end
|
||||
|
||||
test "proxy logs with grep hello!" do
|
||||
assert_equal \
|
||||
"docker logs kamal-proxy --timestamps 2>&1 | grep 'hello!'",
|
||||
new_command.logs(grep: "hello!").join(" ")
|
||||
end
|
||||
|
||||
test "proxy remove container" do
|
||||
assert_equal \
|
||||
"docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy",
|
||||
new_command.remove_container.join(" ")
|
||||
end
|
||||
|
||||
test "proxy remove image" do
|
||||
assert_equal \
|
||||
"docker image prune --all --force --filter label=org.opencontainers.image.title=kamal-proxy",
|
||||
new_command.remove_image.join(" ")
|
||||
end
|
||||
|
||||
test "proxy follow logs" do
|
||||
assert_equal \
|
||||
"ssh -t root@1.1.1.1 -p 22 'docker logs kamal-proxy --timestamps --tail 10 --follow 2>&1'",
|
||||
new_command.follow_logs(host: @config[:servers].first)
|
||||
end
|
||||
|
||||
test "proxy follow logs with grep hello!" do
|
||||
assert_equal \
|
||||
"ssh -t root@1.1.1.1 -p 22 'docker logs kamal-proxy --timestamps --tail 10 --follow 2>&1 | grep \"hello!\"'",
|
||||
new_command.follow_logs(host: @config[:servers].first, grep: "hello!")
|
||||
end
|
||||
|
||||
test "deploy" do
|
||||
assert_equal \
|
||||
"docker exec kamal-proxy kamal-proxy deploy service --target \"172.1.0.2:80\"",
|
||||
new_command.deploy("service", target: "172.1.0.2:80").join(" ")
|
||||
end
|
||||
|
||||
test "remove" do
|
||||
assert_equal \
|
||||
"docker exec kamal-proxy kamal-proxy remove service --target \"172.1.0.2:80\"",
|
||||
new_command.remove("service", target: "172.1.0.2:80").join(" ")
|
||||
end
|
||||
|
||||
private
|
||||
def new_command
|
||||
Kamal::Commands::Proxy.new(Kamal::Configuration.new(@config, version: "123"))
|
||||
end
|
||||
end
|
||||
@@ -4,7 +4,7 @@ class CommandsPruneTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@config = {
|
||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ],
|
||||
builder: { "arch" => "amd64" }, traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
|
||||
traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ class CommandsRegistryTest < ActiveSupport::TestCase
|
||||
"password" => "secret",
|
||||
"server" => "hub.docker.com"
|
||||
},
|
||||
builder: { "arch" => "amd64" },
|
||||
servers: [ "1.1.1.1" ]
|
||||
}
|
||||
@registry = Kamal::Commands::Registry.new Kamal::Configuration.new(@config)
|
||||
|
||||
@@ -4,7 +4,7 @@ class CommandsServerTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@config = {
|
||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ],
|
||||
builder: { "arch" => "amd64" }, traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
|
||||
traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ class CommandsTraefikTest < ActiveSupport::TestCase
|
||||
@image = "traefik:test"
|
||||
|
||||
@config = {
|
||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], builder: { "arch" => "amd64" },
|
||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ],
|
||||
traefik: { "image" => @image, "args" => { "accesslog.format" => "json", "api.insecure" => true, "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
|
||||
"web" => [ "1.1.1.1", "1.1.1.2" ],
|
||||
"workers" => [ "1.1.1.3", "1.1.1.4" ]
|
||||
},
|
||||
builder: { "arch" => "amd64" },
|
||||
env: { "REDIS_URL" => "redis://x/y" },
|
||||
accessories: {
|
||||
"mysql" => {
|
||||
|
||||
@@ -4,33 +4,56 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@deploy = {
|
||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" },
|
||||
builder: { "arch" => "amd64" }, servers: [ "1.1.1.1" ]
|
||||
servers: [ "1.1.1.1" ]
|
||||
}
|
||||
|
||||
@deploy_with_builder_option = {
|
||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" },
|
||||
servers: [ "1.1.1.1" ],
|
||||
builder: {}
|
||||
}
|
||||
end
|
||||
|
||||
test "multiarch?" do
|
||||
assert_equal true, config.builder.multiarch?
|
||||
end
|
||||
|
||||
test "setting multiarch to false" do
|
||||
@deploy_with_builder_option[:builder] = { "multiarch" => false }
|
||||
|
||||
assert_equal false, config_with_builder_option.builder.multiarch?
|
||||
end
|
||||
|
||||
test "local?" do
|
||||
assert_equal true, config.builder.local?
|
||||
assert_equal false, config.builder.local?
|
||||
end
|
||||
|
||||
test "remote?" do
|
||||
assert_equal false, config.builder.remote?
|
||||
end
|
||||
|
||||
test "remote" do
|
||||
assert_nil config.builder.remote
|
||||
test "remote_arch" do
|
||||
assert_nil config.builder.remote_arch
|
||||
end
|
||||
|
||||
test "remote_host" do
|
||||
assert_nil config.builder.remote_host
|
||||
end
|
||||
|
||||
test "setting both local and remote configs" do
|
||||
@deploy[:builder] = {
|
||||
"arch" => [ "amd64", "arm64" ],
|
||||
"remote" => "ssh://root@192.168.0.1"
|
||||
@deploy_with_builder_option[:builder] = {
|
||||
"local" => { "arch" => "arm64", "host" => "unix:///Users/<%= `whoami`.strip %>/.docker/run/docker.sock" },
|
||||
"remote" => { "arch" => "amd64", "host" => "ssh://root@192.168.0.1" }
|
||||
}
|
||||
|
||||
assert_equal true, config.builder.local?
|
||||
assert_equal true, config.builder.remote?
|
||||
assert_equal true, config_with_builder_option.builder.local?
|
||||
assert_equal true, config_with_builder_option.builder.remote?
|
||||
|
||||
assert_equal [ "amd64", "arm64" ], config.builder.arches
|
||||
assert_equal "ssh://root@192.168.0.1", config.builder.remote
|
||||
assert_equal "amd64", config_with_builder_option.builder.remote_arch
|
||||
assert_equal "ssh://root@192.168.0.1", config_with_builder_option.builder.remote_host
|
||||
|
||||
assert_equal "arm64", config_with_builder_option.builder.local_arch
|
||||
assert_equal "unix:///Users/<%= `whoami`.strip %>/.docker/run/docker.sock", config_with_builder_option.builder.local_host
|
||||
end
|
||||
|
||||
test "cached?" do
|
||||
@@ -38,10 +61,10 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
test "invalid cache type specified" do
|
||||
@deploy[:builder]["cache"] = { "type" => "invalid" }
|
||||
@deploy_with_builder_option[:builder] = { "cache" => { "type" => "invalid" } }
|
||||
|
||||
assert_raises(Kamal::ConfigurationError) do
|
||||
config.builder
|
||||
config_with_builder_option.builder
|
||||
end
|
||||
end
|
||||
|
||||
@@ -54,32 +77,32 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
test "setting gha cache" do
|
||||
@deploy[:builder] = { "arch" => "amd64", "cache" => { "type" => "gha", "options" => "mode=max" } }
|
||||
@deploy_with_builder_option[:builder] = { "cache" => { "type" => "gha", "options" => "mode=max" } }
|
||||
|
||||
assert_equal "type=gha", config.builder.cache_from
|
||||
assert_equal "type=gha,mode=max", config.builder.cache_to
|
||||
assert_equal "type=gha", config_with_builder_option.builder.cache_from
|
||||
assert_equal "type=gha,mode=max", config_with_builder_option.builder.cache_to
|
||||
end
|
||||
|
||||
test "setting registry cache" do
|
||||
@deploy[:builder] = { "arch" => "amd64", "cache" => { "type" => "registry", "options" => "mode=max,image-manifest=true,oci-mediatypes=true" } }
|
||||
@deploy_with_builder_option[:builder] = { "cache" => { "type" => "registry", "options" => "mode=max,image-manifest=true,oci-mediatypes=true" } }
|
||||
|
||||
assert_equal "type=registry,ref=dhh/app-build-cache", config.builder.cache_from
|
||||
assert_equal "type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=dhh/app-build-cache", config.builder.cache_to
|
||||
assert_equal "type=registry,ref=dhh/app-build-cache", config_with_builder_option.builder.cache_from
|
||||
assert_equal "type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=dhh/app-build-cache", config_with_builder_option.builder.cache_to
|
||||
end
|
||||
|
||||
test "setting registry cache when using a custom registry" do
|
||||
@deploy[:registry]["server"] = "registry.example.com"
|
||||
@deploy[:builder] = { "arch" => "amd64", "cache" => { "type" => "registry", "options" => "mode=max,image-manifest=true,oci-mediatypes=true" } }
|
||||
@deploy_with_builder_option[:registry]["server"] = "registry.example.com"
|
||||
@deploy_with_builder_option[:builder] = { "cache" => { "type" => "registry", "options" => "mode=max,image-manifest=true,oci-mediatypes=true" } }
|
||||
|
||||
assert_equal "type=registry,ref=registry.example.com/dhh/app-build-cache", config.builder.cache_from
|
||||
assert_equal "type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=registry.example.com/dhh/app-build-cache", config.builder.cache_to
|
||||
assert_equal "type=registry,ref=registry.example.com/dhh/app-build-cache", config_with_builder_option.builder.cache_from
|
||||
assert_equal "type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=registry.example.com/dhh/app-build-cache", config_with_builder_option.builder.cache_to
|
||||
end
|
||||
|
||||
test "setting registry cache with image" do
|
||||
@deploy[:builder] = { "arch" => "amd64", "cache" => { "type" => "registry", "image" => "kamal", "options" => "mode=max" } }
|
||||
@deploy_with_builder_option[:builder] = { "cache" => { "type" => "registry", "image" => "kamal", "options" => "mode=max" } }
|
||||
|
||||
assert_equal "type=registry,ref=kamal", config.builder.cache_from
|
||||
assert_equal "type=registry,mode=max,ref=kamal", config.builder.cache_to
|
||||
assert_equal "type=registry,ref=kamal", config_with_builder_option.builder.cache_from
|
||||
assert_equal "type=registry,mode=max,ref=kamal", config_with_builder_option.builder.cache_to
|
||||
end
|
||||
|
||||
test "args" do
|
||||
@@ -87,9 +110,9 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
test "setting args" do
|
||||
@deploy[:builder]["args"] = { "key" => "value" }
|
||||
@deploy_with_builder_option[:builder] = { "args" => { "key" => "value" } }
|
||||
|
||||
assert_equal({ "key" => "value" }, config.builder.args)
|
||||
assert_equal({ "key" => "value" }, config_with_builder_option.builder.args)
|
||||
end
|
||||
|
||||
test "secrets" do
|
||||
@@ -97,9 +120,9 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
test "setting secrets" do
|
||||
@deploy[:builder]["secrets"] = [ "GITHUB_TOKEN" ]
|
||||
@deploy_with_builder_option[:builder] = { "secrets" => [ "GITHUB_TOKEN" ] }
|
||||
|
||||
assert_equal [ "GITHUB_TOKEN" ], config.builder.secrets
|
||||
assert_equal [ "GITHUB_TOKEN" ], config_with_builder_option.builder.secrets
|
||||
end
|
||||
|
||||
test "dockerfile" do
|
||||
@@ -107,9 +130,9 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
test "setting dockerfile" do
|
||||
@deploy[:builder]["dockerfile"] = "Dockerfile.dev"
|
||||
@deploy_with_builder_option[:builder] = { "dockerfile" => "Dockerfile.dev" }
|
||||
|
||||
assert_equal "Dockerfile.dev", config.builder.dockerfile
|
||||
assert_equal "Dockerfile.dev", config_with_builder_option.builder.dockerfile
|
||||
end
|
||||
|
||||
test "context" do
|
||||
@@ -117,9 +140,9 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
test "setting context" do
|
||||
@deploy[:builder]["context"] = ".."
|
||||
@deploy_with_builder_option[:builder] = { "context" => ".." }
|
||||
|
||||
assert_equal "..", config.builder.context
|
||||
assert_equal "..", config_with_builder_option.builder.context
|
||||
end
|
||||
|
||||
test "ssh" do
|
||||
@@ -127,13 +150,17 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
test "setting ssh params" do
|
||||
@deploy[:builder]["ssh"] = "default=$SSH_AUTH_SOCK"
|
||||
@deploy_with_builder_option[:builder] = { "ssh" => "default=$SSH_AUTH_SOCK" }
|
||||
|
||||
assert_equal "default=$SSH_AUTH_SOCK", config.builder.ssh
|
||||
assert_equal "default=$SSH_AUTH_SOCK", config_with_builder_option.builder.ssh
|
||||
end
|
||||
|
||||
private
|
||||
def config
|
||||
Kamal::Configuration.new(@deploy)
|
||||
end
|
||||
|
||||
def config_with_builder_option
|
||||
Kamal::Configuration.new(@deploy_with_builder_option)
|
||||
end
|
||||
end
|
||||
|
||||
4
test/configuration/env/tags_test.rb
vendored
4
test/configuration/env/tags_test.rb
vendored
@@ -5,7 +5,6 @@ class ConfigurationEnvTagsTest < ActiveSupport::TestCase
|
||||
@deploy = {
|
||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" },
|
||||
servers: [ { "1.1.1.1" => "odd" }, { "1.1.1.2" => "even" }, { "1.1.1.3" => [ "odd", "three" ] } ],
|
||||
builder: { "arch" => "amd64" },
|
||||
env: {
|
||||
"clear" => { "REDIS_URL" => "redis://x/y", "THREE" => "false" },
|
||||
"tags" => {
|
||||
@@ -65,7 +64,6 @@ class ConfigurationEnvTagsTest < ActiveSupport::TestCase
|
||||
deploy = {
|
||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" },
|
||||
servers: [ { "1.1.1.1" => [ "first", "second" ] } ],
|
||||
builder: { "arch" => "amd64" },
|
||||
env: {
|
||||
"tags" => {
|
||||
"first" => { "TYPE" => "first" },
|
||||
@@ -84,7 +82,6 @@ class ConfigurationEnvTagsTest < ActiveSupport::TestCase
|
||||
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" ] }
|
||||
@@ -102,7 +99,6 @@ class ConfigurationEnvTagsTest < ActiveSupport::TestCase
|
||||
deploy = {
|
||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" },
|
||||
servers: [ { "1.1.1.1" => "clearly" } ],
|
||||
builder: { "arch" => "amd64" },
|
||||
env: {
|
||||
"tags" => {
|
||||
"clearly" => { "clear" => { "FOO" => "bar" } }
|
||||
|
||||
@@ -5,7 +5,6 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
|
||||
@deploy = {
|
||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" },
|
||||
servers: [ "1.1.1.1", "1.1.1.2" ],
|
||||
builder: { "arch" => "amd64" },
|
||||
env: { "REDIS_URL" => "redis://x/y" }
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ class ConfigurationSshTest < ActiveSupport::TestCase
|
||||
@deploy = {
|
||||
service: "app", image: "dhh/app",
|
||||
registry: { "username" => "dhh", "password" => "secret" },
|
||||
builder: { "arch" => "amd64" },
|
||||
env: { "REDIS_URL" => "redis://x/y" },
|
||||
servers: [ "1.1.1.1", "1.1.1.2" ],
|
||||
volumes: [ "/local/path:/container/path" ]
|
||||
|
||||
@@ -6,7 +6,6 @@ class ConfigurationSshkitTest < ActiveSupport::TestCase
|
||||
service: "app", image: "dhh/app",
|
||||
registry: { "username" => "dhh", "password" => "secret" },
|
||||
env: { "REDIS_URL" => "redis://x/y" },
|
||||
builder: { "arch" => "amd64" },
|
||||
servers: [ "1.1.1.1", "1.1.1.2" ],
|
||||
volumes: [ "/local/path:/container/path" ]
|
||||
}
|
||||
|
||||
@@ -90,8 +90,10 @@ class ConfigurationValidationTest < ActiveSupport::TestCase
|
||||
|
||||
test "builder" do
|
||||
assert_error "builder: unknown key: foo", builder: { "foo" => "bar" }
|
||||
assert_error "builder/remote: should be a string", builder: { "remote" => { "foo" => "bar" } }
|
||||
assert_error "builder/arch: should be an array or a string", builder: { "arch" => {} }
|
||||
assert_error "builder/remote: should be a hash", builder: { "remote" => true }
|
||||
assert_error "builder/remote: unknown key: foo", builder: { "remote" => { "foo" => "bar" } }
|
||||
assert_error "builder/local: unknown key: foo", builder: { "local" => { "foo" => "bar" } }
|
||||
assert_error "builder/remote/arch: should be a string", builder: { "remote" => { "arch" => [] } }
|
||||
assert_error "builder/args: should be a hash", builder: { "args" => [ "foo" ] }
|
||||
assert_error "builder/cache/options: should be a string", builder: { "cache" => { "options" => [] } }
|
||||
end
|
||||
@@ -101,7 +103,6 @@ class ConfigurationValidationTest < ActiveSupport::TestCase
|
||||
valid_config = {
|
||||
service: "app",
|
||||
image: "app",
|
||||
builder: { "arch" => "amd64" },
|
||||
registry: { "username" => "user", "password" => "secret" },
|
||||
servers: [ "1.1.1.1" ]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ class ConfigurationTest < ActiveSupport::TestCase
|
||||
@deploy = {
|
||||
service: "app", image: "dhh/app",
|
||||
registry: { "username" => "dhh", "password" => "secret" },
|
||||
builder: { "arch" => "amd64" },
|
||||
env: { "REDIS_URL" => "redis://x/y" },
|
||||
servers: [ "1.1.1.1", "1.1.1.2" ],
|
||||
volumes: [ "/local/path:/container/path" ]
|
||||
@@ -122,7 +121,7 @@ class ConfigurationTest < ActiveSupport::TestCase
|
||||
test "version from uncommitted context" do
|
||||
ENV.delete("VERSION")
|
||||
|
||||
config = Kamal::Configuration.new(@deploy.tap { |c| c[:builder]["context"] = "." })
|
||||
config = Kamal::Configuration.new(@deploy.tap { |c| c[:builder] = { "context" => "." } })
|
||||
|
||||
Kamal::Git.expects(:revision).returns("git-version")
|
||||
Kamal::Git.expects(:uncommitted_changes).returns("M file\n")
|
||||
@@ -268,7 +267,7 @@ class ConfigurationTest < ActiveSupport::TestCase
|
||||
ssh_options: { user: "root", port: 22, log_level: :fatal, keepalive: true, keepalive_interval: 30 },
|
||||
sshkit: {},
|
||||
volume_args: [ "--volume", "/local/path:/container/path" ],
|
||||
builder: { "arch" => "amd64" },
|
||||
builder: {},
|
||||
logging: [ "--log-opt", "max-size=\"10m\"" ],
|
||||
healthcheck: { "cmd"=>"curl -f http://localhost:3000/up || exit 1", "interval" => "1s", "path"=>"/up", "port"=>3000, "max_attempts" => 7, "cord" => "/tmp/kamal-cord", "log_lines" => 50 } }
|
||||
|
||||
|
||||
2
test/fixtures/deploy.erb.yml
vendored
2
test/fixtures/deploy.erb.yml
vendored
@@ -9,5 +9,3 @@ registry:
|
||||
server: registry.digitalocean.com
|
||||
username: <%= "my-user" %>
|
||||
password: <%= "my-password" %>
|
||||
builder:
|
||||
arch: amd64
|
||||
|
||||
2
test/fixtures/deploy_for_dest.yml
vendored
2
test/fixtures/deploy_for_dest.yml
vendored
@@ -4,5 +4,3 @@ registry:
|
||||
server: registry.digitalocean.com
|
||||
username: <%= "my-user" %>
|
||||
password: <%= "my-password" %>
|
||||
builder:
|
||||
arch: amd64
|
||||
|
||||
2
test/fixtures/deploy_for_required_dest.yml
vendored
2
test/fixtures/deploy_for_required_dest.yml
vendored
@@ -4,6 +4,4 @@ registry:
|
||||
server: registry.digitalocean.com
|
||||
username: <%= "my-user" %>
|
||||
password: <%= "my-password" %>
|
||||
builder:
|
||||
arch: amd64
|
||||
require_destination: true
|
||||
|
||||
@@ -17,6 +17,4 @@ registry:
|
||||
server: registry.digitalocean.com
|
||||
username: user
|
||||
password: pw
|
||||
builder:
|
||||
arch: amd64
|
||||
primary_role: web_tokyo
|
||||
|
||||
2
test/fixtures/deploy_simple.yml
vendored
2
test/fixtures/deploy_simple.yml
vendored
@@ -6,5 +6,3 @@ servers:
|
||||
registry:
|
||||
username: user
|
||||
password: pw
|
||||
builder:
|
||||
arch: amd64
|
||||
|
||||
2
test/fixtures/deploy_with_accessories.yml
vendored
2
test/fixtures/deploy_with_accessories.yml
vendored
@@ -10,8 +10,6 @@ servers:
|
||||
registry:
|
||||
username: user
|
||||
password: pw
|
||||
builder:
|
||||
arch: amd64
|
||||
|
||||
accessories:
|
||||
mysql:
|
||||
|
||||
2
test/fixtures/deploy_with_aliases.yml
vendored
2
test/fixtures/deploy_with_aliases.yml
vendored
@@ -11,8 +11,6 @@ servers:
|
||||
console:
|
||||
hosts:
|
||||
- 1.1.1.5
|
||||
builder:
|
||||
arch: amd64
|
||||
registry:
|
||||
username: user
|
||||
password: pw
|
||||
|
||||
2
test/fixtures/deploy_with_assets.yml
vendored
2
test/fixtures/deploy_with_assets.yml
vendored
@@ -6,6 +6,4 @@ servers:
|
||||
registry:
|
||||
username: user
|
||||
password: pw
|
||||
builder:
|
||||
arch: amd64
|
||||
asset_path: /public/assets
|
||||
|
||||
2
test/fixtures/deploy_with_boot_strategy.yml
vendored
2
test/fixtures/deploy_with_boot_strategy.yml
vendored
@@ -7,8 +7,6 @@ servers:
|
||||
workers:
|
||||
- "1.1.1.3"
|
||||
- "1.1.1.4"
|
||||
builder:
|
||||
arch: amd64
|
||||
|
||||
registry:
|
||||
username: user
|
||||
|
||||
2
test/fixtures/deploy_with_env_tags.yml
vendored
2
test/fixtures/deploy_with_env_tags.yml
vendored
@@ -11,8 +11,6 @@ servers:
|
||||
- 1.1.1.4: site1
|
||||
- 1.2.1.3: site2
|
||||
- 1.2.1.4: [ site2 experimental ]
|
||||
builder:
|
||||
arch: amd64
|
||||
env:
|
||||
clear:
|
||||
TEST: "root"
|
||||
|
||||
2
test/fixtures/deploy_with_extensions.yml
vendored
2
test/fixtures/deploy_with_extensions.yml
vendored
@@ -21,6 +21,4 @@ registry:
|
||||
server: registry.digitalocean.com
|
||||
username: user
|
||||
password: pw
|
||||
builder:
|
||||
arch: amd64
|
||||
primary_role: web_tokyo
|
||||
|
||||
@@ -7,8 +7,6 @@ servers:
|
||||
workers:
|
||||
- "1.1.1.3"
|
||||
- "1.1.1.4"
|
||||
builder:
|
||||
arch: amd64
|
||||
|
||||
registry:
|
||||
username: user
|
||||
|
||||
@@ -26,8 +26,6 @@ servers:
|
||||
hosts:
|
||||
- 1.1.1.3
|
||||
- 1.1.1.4
|
||||
builder:
|
||||
arch: amd64
|
||||
env:
|
||||
REDIS_URL: redis://x/y
|
||||
registry:
|
||||
|
||||
@@ -7,8 +7,6 @@ servers:
|
||||
workers:
|
||||
- "1.1.1.3"
|
||||
- "1.1.1.4"
|
||||
builder:
|
||||
arch: amd64
|
||||
|
||||
registry:
|
||||
username: user
|
||||
|
||||
42
test/fixtures/deploy_with_proxy.yml
vendored
Normal file
42
test/fixtures/deploy_with_proxy.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
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
|
||||
|
||||
proxy:
|
||||
enabled: true
|
||||
hosts:
|
||||
- "1.1.1.1"
|
||||
deploy_timeout: 6s
|
||||
|
||||
accessories:
|
||||
mysql:
|
||||
image: mysql:5.7
|
||||
host: 1.1.1.3
|
||||
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
|
||||
7
test/fixtures/deploy_with_remote_builder.yml
vendored
7
test/fixtures/deploy_with_remote_builder.yml
vendored
@@ -32,11 +32,10 @@ accessories:
|
||||
port: 6379
|
||||
directories:
|
||||
- data:/data
|
||||
builder:
|
||||
arch: amd64
|
||||
|
||||
readiness_delay: 0
|
||||
|
||||
builder:
|
||||
arch: <%= Kamal::Utils.docker_arch == "arm64" ? "amd64" : "arm64" %>
|
||||
remote: ssh://app@1.1.1.5
|
||||
remote:
|
||||
arch: amd64
|
||||
host: ssh://app@1.1.1.5
|
||||
|
||||
@@ -32,8 +32,6 @@ accessories:
|
||||
port: 6379
|
||||
directories:
|
||||
- data:/data
|
||||
builder:
|
||||
arch: amd64
|
||||
|
||||
readiness_delay: 0
|
||||
|
||||
@@ -42,5 +40,6 @@ ssh:
|
||||
port: 22
|
||||
|
||||
builder:
|
||||
arch: <%= Kamal::Utils.docker_arch == "arm64" ? "amd64" : "arm64" %>
|
||||
remote: ssh://app@1.1.1.5:2122
|
||||
remote:
|
||||
arch: amd64
|
||||
host: ssh://app@1.1.1.5:2122
|
||||
|
||||
2
test/fixtures/deploy_with_roles.yml
vendored
2
test/fixtures/deploy_with_roles.yml
vendored
@@ -14,5 +14,3 @@ registry:
|
||||
server: registry.digitalocean.com
|
||||
username: user
|
||||
password: pw
|
||||
builder:
|
||||
arch: amd64
|
||||
|
||||
2
test/fixtures/deploy_with_secrets.yml
vendored
2
test/fixtures/deploy_with_secrets.yml
vendored
@@ -9,5 +9,3 @@ registry:
|
||||
env:
|
||||
secret:
|
||||
- PASSWORD
|
||||
builder:
|
||||
arch: amd64
|
||||
|
||||
@@ -13,5 +13,3 @@ registry:
|
||||
server: registry.digitalocean.com
|
||||
username: user
|
||||
password: pw
|
||||
builder:
|
||||
arch: amd64
|
||||
|
||||
@@ -6,5 +6,3 @@ servers:
|
||||
registry:
|
||||
username: user
|
||||
password: pw
|
||||
builder:
|
||||
arch: amd64
|
||||
|
||||
1
test/fixtures/deploy_without_clone.yml
vendored
1
test/fixtures/deploy_without_clone.yml
vendored
@@ -36,5 +36,4 @@ accessories:
|
||||
readiness_delay: 0
|
||||
|
||||
builder:
|
||||
arch: amd64
|
||||
context: "."
|
||||
|
||||
2
test/fixtures/deploy_workers_only.yml
vendored
2
test/fixtures/deploy_workers_only.yml
vendored
@@ -10,5 +10,3 @@ primary_role: workers
|
||||
registry:
|
||||
username: user
|
||||
password: pw
|
||||
builder:
|
||||
arch: amd64
|
||||
|
||||
@@ -29,6 +29,5 @@ class BrokenDeployTest < IntegrationTest
|
||||
assert_match /First web container is unhealthy on vm[12], not booting any other roles/, output
|
||||
assert_match "First web container is unhealthy, not booting workers on vm3", output
|
||||
assert_match "nginx: [emerg] unexpected end of file, expecting \";\" or \"}\" in /etc/nginx/conf.d/default.conf:2", output
|
||||
assert_match 'ERROR {"Status":"unhealthy","FailingStreak":0,"Log":[]}', output
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,4 +6,4 @@ 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
|
||||
|
||||
@@ -23,8 +23,7 @@ registry:
|
||||
username: root
|
||||
password: root
|
||||
builder:
|
||||
driver: docker
|
||||
arch: <%= Kamal::Utils.docker_arch %>
|
||||
multiarch: false
|
||||
args:
|
||||
COMMIT_SHA: <%= `git rev-parse HEAD` %>
|
||||
healthcheck:
|
||||
|
||||
@@ -6,4 +6,4 @@ 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
|
||||
|
||||
@@ -9,6 +9,11 @@ servers:
|
||||
hosts:
|
||||
- vm3
|
||||
cmd: sleep infinity
|
||||
proxy:
|
||||
enabled: true
|
||||
hosts:
|
||||
- vm2
|
||||
deploy_timeout: 2s
|
||||
|
||||
asset_path: /usr/share/nginx/html/versions
|
||||
|
||||
@@ -17,8 +22,7 @@ registry:
|
||||
username: root
|
||||
password: root
|
||||
builder:
|
||||
driver: docker
|
||||
arch: <%= Kamal::Utils.docker_arch %>
|
||||
multiarch: false
|
||||
args:
|
||||
COMMIT_SHA: <%= `git rev-parse HEAD` %>
|
||||
healthcheck:
|
||||
|
||||
@@ -31,7 +31,7 @@ class IntegrationTest < ActiveSupport::TestCase
|
||||
succeeded = system("cd test/integration && #{command}")
|
||||
end
|
||||
|
||||
raise "Command `#{command}` failed with error code `#{$?}`" if !succeeded && raise_on_error
|
||||
raise "Command `#{command}` failed with error code `#{$?}`, and output:\n#{result}" if !succeeded && raise_on_error
|
||||
result
|
||||
end
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ class MainTest < IntegrationTest
|
||||
assert_equal "app-#{version}", config[:service_with_version]
|
||||
assert_equal [], config[:volume_args]
|
||||
assert_equal({ user: "root", port: 22, keepalive: true, keepalive_interval: 30, log_level: :fatal }, config[:ssh_options])
|
||||
assert_equal({ "driver" => "docker", "arch" => "amd64", "args" => { "COMMIT_SHA" => version } }, config[:builder])
|
||||
assert_equal({ "multiarch" => false, "args" => { "COMMIT_SHA" => version } }, config[:builder])
|
||||
assert_equal [ "--log-opt", "max-size=\"10m\"" ], config[:logging]
|
||||
assert_equal({ "cmd"=>"wget -qO- http://localhost > /dev/null || exit 1", "interval"=>"1s", "max_attempts"=>3, "port"=>3000, "path"=>"/up", "cord"=>"/tmp/kamal-cord", "log_lines"=>50 }, config[:healthcheck])
|
||||
end
|
||||
|
||||
84
test/integration/proxy_test.rb
Normal file
84
test/integration/proxy_test.rb
Normal file
@@ -0,0 +1,84 @@
|
||||
require_relative "integration_test"
|
||||
|
||||
class ProxyTest < IntegrationTest
|
||||
setup do
|
||||
@app = "app_with_roles"
|
||||
end
|
||||
|
||||
test "boot, reboot, stop, start, restart, logs, remove" do
|
||||
kamal :envify
|
||||
|
||||
kamal :proxy, :boot
|
||||
assert_proxy_running
|
||||
|
||||
output = kamal :proxy, :reboot, "-y", "--verbose", capture: true
|
||||
assert_proxy_running
|
||||
assert_hooks_ran "pre-traefik-reboot", "post-traefik-reboot"
|
||||
assert_match /Rebooting Traefik on vm1,vm2.../, output
|
||||
assert_match /Rebooted Traefik on vm1,vm2/, output
|
||||
|
||||
output = kamal :proxy, :reboot, "--rolling", "-y", "--verbose", capture: true
|
||||
assert_proxy_running
|
||||
assert_hooks_ran "pre-traefik-reboot", "post-traefik-reboot"
|
||||
assert_match /Rebooting Traefik on vm1.../, output
|
||||
assert_match /Rebooted Traefik on vm1/, output
|
||||
assert_match /Rebooting Traefik on vm2.../, output
|
||||
assert_match /Rebooted Traefik on vm2/, output
|
||||
|
||||
kamal :proxy, :boot
|
||||
assert_proxy_running
|
||||
assert_traefik_running
|
||||
|
||||
# Check booting when booted doesn't raise an error
|
||||
kamal :proxy, :stop
|
||||
assert_proxy_not_running
|
||||
assert_traefik_not_running
|
||||
|
||||
# Check booting when stopped works
|
||||
kamal :proxy, :boot
|
||||
assert_proxy_running
|
||||
assert_traefik_running
|
||||
|
||||
kamal :proxy, :stop
|
||||
assert_proxy_not_running
|
||||
assert_traefik_not_running
|
||||
|
||||
kamal :proxy, :start
|
||||
assert_proxy_running
|
||||
assert_traefik_running
|
||||
|
||||
kamal :proxy, :restart
|
||||
assert_proxy_running
|
||||
assert_traefik_running
|
||||
|
||||
logs = kamal :proxy, :logs, capture: true
|
||||
assert_match /Traefik version [\d.]+ built on/, logs
|
||||
|
||||
kamal :proxy, :remove
|
||||
assert_proxy_not_running
|
||||
assert_traefik_not_running
|
||||
|
||||
kamal :env, :delete
|
||||
end
|
||||
|
||||
private
|
||||
def assert_proxy_running
|
||||
assert_match /basecamp\/kamal-proxy:latest \"kamal-proxy run\"/, proxy_details
|
||||
end
|
||||
|
||||
def assert_proxy_not_running
|
||||
assert_no_match /basecamp\/kamal-proxy:latest \"kamal-proxy run\"/, proxy_details
|
||||
end
|
||||
|
||||
def assert_traefik_running
|
||||
assert_match /traefik:v2.10 "\/entrypoint.sh/, proxy_details
|
||||
end
|
||||
|
||||
def assert_traefik_not_running
|
||||
assert_no_match /traefik:v2.10 "\/entrypoint.sh/, proxy_details
|
||||
end
|
||||
|
||||
def proxy_details
|
||||
kamal :proxy, :details, capture: true
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user