This commit is contained in:
Donal McBreen
2023-08-28 09:39:14 +01:00
parent 989d09e027
commit 782b1979ab
29 changed files with 558 additions and 262 deletions

View File

@@ -1,6 +1,7 @@
module Kamal::Cli
class LockError < StandardError; end
class HookError < StandardError; end
class TraefikError < StandardError; end
end
# SSHKit uses instance eval, so we need a global const for ergonomics

View File

@@ -2,6 +2,8 @@ class Kamal::Cli::App < Kamal::Cli::Base
desc "boot", "Boot app on servers (or reboot app if already running)"
def boot
mutating do
ensure_traefik_file_provider_enabled
hold_lock_on_error do
say "Get most recent version available as an image...", :magenta unless options[:version]
using_version(version_or_latest) do |version|
@@ -18,6 +20,8 @@ class Kamal::Cli::App < Kamal::Cli::Base
roles.each do |role|
app = KAMAL.app(role: role)
auditor = KAMAL.auditor(role: role)
traefik_dynamic = KAMAL.traefik_dynamic(role: role)
role_config = KAMAL.config.role(role)
if capture_with_info(*app.container_id_for_version(version, only_running: true), raise_on_non_zero_exit: false).present?
tmp_version = "#{version}_replaced_#{SecureRandom.hex(8)}"
@@ -33,6 +37,12 @@ class Kamal::Cli::App < Kamal::Cli::Base
Kamal::Utils::HealthcheckPoller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
if role_config.running_traefik?
ip_address = capture_with_info(*app.ip_address(version: version)).strip
execute *traefik_dynamic.write_config(ip_address: ip_address)
Kamal::Utils::SwitchPoller.wait_for_switch(traefik_dynamic) { capture_with_info(*traefik_dynamic.run_id)&.strip }
end
execute *app.stop(version: old_version), raise_on_non_zero_exit: false if old_version.present?
end
end
@@ -44,12 +54,23 @@ class Kamal::Cli::App < Kamal::Cli::Base
desc "start", "Start existing app container on servers"
def start
mutating do
ensure_traefik_file_provider_enabled
on(KAMAL.hosts) do |host|
roles = KAMAL.roles_on(host)
roles.each do |role|
app = KAMAL.app(role: role)
role_config = KAMAL.config.role(role)
execute *KAMAL.auditor.record("Started app version #{KAMAL.config.version}"), verbosity: :debug
execute *KAMAL.app(role: role).start, raise_on_non_zero_exit: false
execute *app.start, raise_on_non_zero_exit: false
version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
if role_config.running_traefik?
ip_address = capture_with_info(*app.ip_address(version: version)).strip
execute *KAMAL.traefik_dynamic(role: role).write_config(ip_address: ip_address)
end
end
end
end
@@ -62,8 +83,10 @@ class Kamal::Cli::App < Kamal::Cli::Base
roles = KAMAL.roles_on(host)
roles.each do |role|
app = KAMAL.app(role: role)
execute *KAMAL.auditor.record("Stopped app", role: role), verbosity: :debug
execute *KAMAL.app(role: role).stop, raise_on_non_zero_exit: false
execute *KAMAL.traefik_dynamic(role: role).remove_config if KAMAL.config.role(role).running_traefik?
execute *app.stop, raise_on_non_zero_exit: false
end
end
end
@@ -293,4 +316,13 @@ class Kamal::Cli::App < Kamal::Cli::Base
def version_or_latest
options[:version] || "latest"
end
def ensure_traefik_file_provider_enabled
# Ensure traefik has been rebooted to switch to the file provider
on(KAMAL.traefik_hosts) do
unless capture_with_info(*KAMAL.traefik_static.docker_entrypoint_args).include?("--providers.file.directory=")
raise Kamal::Cli::TraefikError, "File provider not enabled, you'll need to run `kamal traefik reboot` to deploy"
end
end
end
end

View File

@@ -13,8 +13,9 @@ class Kamal::Cli::Env < Kamal::Cli::Base
end
on(KAMAL.traefik_hosts) do
execute *KAMAL.traefik.make_env_directory
upload! StringIO.new(KAMAL.traefik.env_file), KAMAL.traefik.host_env_file_path, mode: 400
traefik_static_config = KAMAL.traefik_static.static_config
execute *KAMAL.traefik_static.make_env_directory
upload! StringIO.new(traefik_static_config.env_file), traefik_static_config.host_env_file_path, mode: 400
end
on(KAMAL.accessory_hosts) do
@@ -38,7 +39,7 @@ class Kamal::Cli::Env < Kamal::Cli::Base
end
on(KAMAL.traefik_hosts) do
execute *KAMAL.traefik.remove_env_file
execute *KAMAL.traefik_static.remove_env_file
end
on(KAMAL.accessory_hosts) do

View File

@@ -4,7 +4,8 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
mutating do
on(KAMAL.traefik_hosts) do
execute *KAMAL.registry.login
execute *KAMAL.traefik.start_or_run
execute *KAMAL.traefik_static.ensure_config_directory
execute *KAMAL.traefik_static.start_or_run
end
end
end
@@ -16,9 +17,10 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
on(KAMAL.traefik_hosts, in: options[:rolling] ? :sequence : :parallel) do
execute *KAMAL.auditor.record("Rebooted traefik"), verbosity: :debug
execute *KAMAL.registry.login
execute *KAMAL.traefik.stop
execute *KAMAL.traefik.remove_container
execute *KAMAL.traefik.run
execute *KAMAL.traefik_static.stop
execute *KAMAL.traefik_static.remove_container
execute *KAMAL.traefik_static.ensure_config_directory
execute *KAMAL.traefik_static.run
end
end
end
@@ -28,7 +30,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
mutating do
on(KAMAL.traefik_hosts) do
execute *KAMAL.auditor.record("Started traefik"), verbosity: :debug
execute *KAMAL.traefik.start
execute *KAMAL.traefik_static.start
end
end
end
@@ -38,7 +40,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
mutating do
on(KAMAL.traefik_hosts) do
execute *KAMAL.auditor.record("Stopped traefik"), verbosity: :debug
execute *KAMAL.traefik.stop
execute *KAMAL.traefik_static.stop
end
end
end
@@ -53,7 +55,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
desc "details", "Show details about Traefik container from servers"
def details
on(KAMAL.traefik_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.traefik.info), type: "Traefik" }
on(KAMAL.traefik_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.traefik_static.info), type: "Traefik" }
end
desc "logs", "Show log lines from Traefik on servers"
@@ -67,15 +69,15 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
if options[:follow]
run_locally do
info "Following logs on #{KAMAL.primary_host}..."
info KAMAL.traefik.follow_logs(host: KAMAL.primary_host, grep: grep)
exec KAMAL.traefik.follow_logs(host: KAMAL.primary_host, grep: grep)
info KAMAL.traefik_static.follow_logs(host: KAMAL.primary_host, grep: grep)
exec KAMAL.traefik_static.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.logs(since: since, lines: lines, grep: grep)), type: "Traefik"
puts_by_host host, capture(*KAMAL.traefik_static.logs(since: since, lines: lines, grep: grep)), type: "Traefik"
end
end
end
@@ -94,7 +96,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
mutating do
on(KAMAL.traefik_hosts) do
execute *KAMAL.auditor.record("Removed traefik container"), verbosity: :debug
execute *KAMAL.traefik.remove_container
execute *KAMAL.traefik_static.remove_container
end
end
end
@@ -104,7 +106,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
mutating do
on(KAMAL.traefik_hosts) do
execute *KAMAL.auditor.record("Removed traefik image"), verbosity: :debug
execute *KAMAL.traefik.remove_image
execute *KAMAL.traefik_static.remove_image
end
end
end

View File

@@ -81,7 +81,7 @@ class Kamal::Commander
def app(role: nil)
Kamal::Commands::App.new(config, role: role)
Kamal::Commands::App.new(config, role: role || config.roles.first.name)
end
def accessory(name)
@@ -124,8 +124,12 @@ class Kamal::Commander
@server ||= Kamal::Commands::Server.new(config)
end
def traefik
@traefik ||= Kamal::Commands::Traefik.new(config)
def traefik_static
@traefik_static ||= Kamal::Commands::Traefik::Static.new(config)
end
def traefik_dynamic(role: nil)
Kamal::Commands::Traefik::Dynamic.new(config, role: role || config.roles.first.name)
end
def with_verbosity(level)

View File

@@ -86,6 +86,10 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
end
end
def make_directory_for(remote_file)
make_directory Pathname.new(remote_file).dirname.to_s
end
def remove_service_directory
[ :rm, "-rf", service_name ]
end

View File

@@ -13,22 +13,20 @@ class Kamal::Commands::App < Kamal::Commands::Base
end
def run(hostname: nil)
role = config.role(self.role)
docker :run,
"--detach",
"--restart unless-stopped",
"--name", container_name,
*(["--hostname", hostname] if hostname),
"-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
*role.env_args,
*role.health_check_args,
*role_config.env_args,
*role_config.health_check_args,
*config.logging_args,
*config.volume_args,
*role.label_args,
*role.option_args,
*role_config.label_args,
*role_config.option_args,
config.absolute_image,
role.cmd
role_config.cmd
end
def start
@@ -49,6 +47,10 @@ class Kamal::Commands::App < Kamal::Commands::Base
docker :ps, *filter_args
end
def ip_address(version:)
docker :inspect, "-f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}'", container_name(version)
end
def logs(since: nil, lines: nil, grep: nil)
pipe \
@@ -76,14 +78,12 @@ class Kamal::Commands::App < Kamal::Commands::Base
end
def execute_in_new_container(*command, interactive: false)
role = config.role(self.role)
docker :run,
("-it" if interactive),
"--rm",
*role&.env_args,
*role_config&.env_args,
*config.volume_args,
*role&.option_args,
*role_config&.option_args,
config.absolute_image,
*command
end
@@ -112,7 +112,7 @@ class Kamal::Commands::App < Kamal::Commands::Base
def list_versions(*docker_args, statuses: nil)
pipe \
docker(:ps, *filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'),
%(while read line; do echo ${line##{service_role_dest}-}; done) # Extract SHA from "service-role-dest-SHA"
%(while read line; do echo ${line##{role_config.full_name}-}; done) # Extract SHA from "service-role-dest-SHA"
end
def list_containers
@@ -157,19 +157,19 @@ class Kamal::Commands::App < Kamal::Commands::Base
[:rm, "-f", config.role(role).host_env_file_path]
end
def service_role_dest
[config.service, role, config.destination].compact.join("-")
end
private
def container_name(version = nil)
[ config.service, role, config.destination, version || config.version ].compact.join("-")
[ role_config.full_name, version || config.version ].compact.join("-")
end
def filter_args(statuses: nil)
argumentize "--filter", filters(statuses: statuses)
end
def service_role_dest
[config.service, role, config.destination].compact.join("-")
end
def filters(statuses: nil)
[ "label=service=#{config.service}" ].tap do |filters|
filters << "label=destination=#{config.destination}" if config.destination
@@ -179,4 +179,8 @@ class Kamal::Commands::App < Kamal::Commands::Base
end
end
end
def role_config
@role_config ||= config.role(self.role)
end
end

View File

@@ -1,5 +1,5 @@
class Kamal::Commands::Server < Kamal::Commands::Base
def ensure_run_directory
[:mkdir, "-p", config.run_directory]
make_directory config.run_directory
end
end

View File

@@ -1,122 +0,0 @@
class Kamal::Commands::Traefik < Kamal::Commands::Base
delegate :argumentize, :env_file_with_secrets, :optionize, to: Kamal::Utils
DEFAULT_IMAGE = "traefik:v2.9"
CONTAINER_PORT = 80
DEFAULT_ARGS = {
'log.level' => 'DEBUG'
}
def run
docker :run, "--name traefik",
"--detach",
"--restart", "unless-stopped",
*publish_args,
"--volume", "/var/run/docker.sock:/var/run/docker.sock",
*env_args,
*config.logging_args,
*label_args,
*docker_options_args,
image,
"--providers.docker",
*cmd_option_args
end
def start
docker :container, :start, "traefik"
end
def stop
docker :container, :stop, "traefik"
end
def start_or_run
combine start, run, by: "||"
end
def info
docker :ps, "--filter", "name=^traefik$"
end
def logs(since: nil, lines: nil, grep: nil)
pipe \
docker(:logs, "traefik", (" --since #{since}" if since), (" --tail #{lines}" if lines), "--timestamps", "2>&1"),
("grep '#{grep}'" if grep)
end
def follow_logs(host:, grep: nil)
run_over_ssh pipe(
docker(:logs, "traefik", "--timestamps", "--tail", "10", "--follow", "2>&1"),
(%(grep "#{grep}") if grep)
).join(" "), host: host
end
def remove_container
docker :container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=Traefik"
end
def remove_image
docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=Traefik"
end
def port
"#{host_port}:#{CONTAINER_PORT}"
end
def env_file
env_file_with_secrets config.traefik.fetch("env", {})
end
def host_env_file_path
File.join host_env_directory, "traefik.env"
end
def make_env_directory
make_directory(host_env_directory)
end
def remove_env_file
[:rm, "-f", host_env_file_path]
end
private
def publish_args
argumentize "--publish", port unless config.traefik["publish"] == false
end
def label_args
argumentize "--label", labels
end
def env_args
argumentize "--env-file", host_env_file_path
end
def host_env_directory
File.join config.host_env_directory, "traefik"
end
def labels
config.traefik["labels"] || []
end
def image
config.traefik.fetch("image") { DEFAULT_IMAGE }
end
def docker_options_args
optionize(config.traefik["options"] || {})
end
def cmd_option_args
if args = config.traefik["args"]
optionize DEFAULT_ARGS.merge(args), with: "="
else
optionize DEFAULT_ARGS, with: "="
end
end
def host_port
config.traefik["host_port"] || CONTAINER_PORT
end
end

View File

@@ -0,0 +1,43 @@
class Kamal::Commands::Traefik::Dynamic < Kamal::Commands::Base
attr_reader :static_config, :dynamic_config
def initialize(config, role: nil)
super(config)
@static_config = Kamal::Configuration::Traefik::Static.new(config: config)
@dynamic_config = Kamal::Configuration::Traefik::Dynamic.new(config: config, role: role)
end
def run_id
pipe \
[:docker, :exec, :traefik, :wget, "-qSO", "/dev/null", "http://localhost:#{Kamal::Configuration::Traefik::Static::CONTAINER_PORT}#{config.healthcheck["path"]}", "2>&1"],
[:grep, "-i", Kamal::Configuration::Traefik::Dynamic::RUN_ID_HEADER],
[:cut, "-d ' ' -f 4"]
end
def write_config(ip_address:)
# Write to tmp then mv for an atomic copy. If you write directly traefik sees an empty file
# and removes the service before picking up the new config.
temp_config_file = "/tmp/kamal-traefik-config-#{rand(10000000)}"
chain \
write([:echo, dynamic_config.config(ip_address: ip_address).to_yaml.shellescape], temp_config_file),
[:mv, temp_config_file, host_file]
end
def remove_config
[:rm, host_file]
end
def boot_check?
dynamic_config.boot_check?
end
def config_run_id
dynamic_config.run_id
end
private
def host_file
"#{static_config.host_directory}/#{dynamic_config.host_file}"
end
end

View File

@@ -0,0 +1,75 @@
class Kamal::Commands::Traefik::Static < Kamal::Commands::Base
attr_reader :static_config, :dynamic_config
def initialize(config, role: nil)
super(config)
@static_config = Kamal::Configuration::Traefik::Static.new(config: config)
@dynamic_config = Kamal::Configuration::Traefik::Dynamic.new(config: config, role: role)
end
def run
docker :run, static_config.docker_args, static_config.image, static_config.traefik_args
end
def start
docker :container, :start, "traefik"
end
def stop
docker :container, :stop, "traefik"
end
def start_or_run
combine start, run, by: "||"
end
def info
docker :ps, "--filter", "name=^traefik$"
end
def logs(since: nil, lines: nil, grep: nil)
pipe \
docker(:logs, "traefik", (" --since #{since}" if since), (" --tail #{lines}" if lines), "--timestamps", "2>&1"),
("grep '#{grep}'" if grep)
end
def follow_logs(host:, grep: nil)
run_over_ssh pipe(
docker(:logs, "traefik", "--timestamps", "--tail", "10", "--follow", "2>&1"),
(%(grep "#{grep}") if grep)
).join(" "), host: host
end
def remove_container
docker :container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=Traefik"
end
def remove_image
docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=Traefik"
end
def port
"#{host_port}:#{CONTAINER_PORT}"
end
def make_env_directory
make_directory(static_config.host_env_directory)
end
def remove_env_file
[:rm, "-f", static_config.host_env_file_path]
end
def ensure_config_directory
make_directory(static_config.host_directory)
end
def docker_entrypoint_args
docker :inspect, "-f '{{index .Args 1 }}'", :traefik
end
def boot_check?
dynamic_config.boot_check?
end
end

View File

@@ -7,7 +7,7 @@ require "net/ssh/proxy/jump"
class Kamal::Configuration
delegate :service, :image, :servers, :env, :labels, :registry, :stop_wait_time, :hooks_path, to: :raw_config, allow_nil: true
delegate :argumentize, :optionize, to: Kamal::Utils
delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils
attr_accessor :destination
attr_accessor :raw_config
@@ -153,7 +153,7 @@ class Kamal::Configuration
end
def valid?
ensure_required_keys_present && ensure_valid_kamal_version
ensure_required_keys_present && ensure_valid_kamal_version && ensure_no_traefik_labels
end
@@ -231,6 +231,17 @@ class Kamal::Configuration
true
end
def ensure_no_traefik_labels
# The switch to a traefik file provider means that traefik labels on app containers are ignored
# We'll raise an error and suggest moving them
if roles.any? { |role| role.labels.keys.any? { |label| label.start_with?("traefik.") } }
raise ArgumentError, "Traefik is not configured to read labels, move traefik config to dynamic:"
end
true
end
def role_names
raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort

View File

@@ -4,7 +4,7 @@ class Kamal::Configuration::Role
attr_accessor :name
def initialize(name, config:)
@name, @config = name.inquiry, config
@name, @config = name.inquiry, config
end
def primary_host
@@ -16,7 +16,7 @@ class Kamal::Configuration::Role
end
def labels
default_labels.merge(traefik_labels).merge(custom_labels)
default_labels.merge(custom_labels)
end
def label_args
@@ -82,9 +82,23 @@ class Kamal::Configuration::Role
end
def running_traefik?
name.web? || specializations["traefik"]
name.web? || (specializations["traefik"] != nil && specializations["traefik"] != false)
end
def traefik
case specializations["traefik"]
when NilClass, TrueClass, FalseClass
{}
else
specializations["traefik"]
end
end
def full_name
[ config.service, name, config.destination ].compact.join("-")
end
private
attr_accessor :config
@@ -105,26 +119,6 @@ class Kamal::Configuration::Role
end
end
def traefik_labels
if running_traefik?
{
# Setting a service property ensures that the generated service name will be consistent between versions
"traefik.http.services.#{traefik_service}.loadbalancer.server.scheme" => "http",
"traefik.http.routers.#{traefik_service}.rule" => "PathPrefix(`/`)",
"traefik.http.middlewares.#{traefik_service}-retry.retry.attempts" => "5",
"traefik.http.middlewares.#{traefik_service}-retry.retry.initialinterval" => "500ms",
"traefik.http.routers.#{traefik_service}.middlewares" => "#{traefik_service}-retry@docker"
}
else
{}
end
end
def traefik_service
[ config.service, name, config.destination ].compact.join("-")
end
def custom_labels
Hash.new.tap do |labels|
labels.merge!(config.labels) if config.labels.present?

View File

@@ -0,0 +1,66 @@
class Kamal::Configuration::Traefik::Dynamic
RUN_ID_HEADER = "X-Kamal-Run-ID"
delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils
attr_reader :traefik_config, :role_config, :role_traefik_config
def initialize(config:, role:)
@traefik_config = config.traefik || {}
@role_config = config.role(role)
@role_traefik_config = role_config&.traefik || {}
end
def host_file
"#{role_config.full_name}.yml"
end
def config(ip_address:)
default_config(ip_address:).deep_merge!(custom_config)
end
def boot_check?
role_traefik_config.fetch("boot_check") { traefik_config.fetch("boot_check", true) }
end
def run_id
@run_id ||= SecureRandom.hex(16)
end
private
def default_config(ip_address:)
run_id_header_middleware = "#{role_config.full_name}-id-header"
{
"http" => {
"routers" => {
role_config.full_name => {
"rule" => "PathPrefix(`/`)",
"middlewares" => [ run_id_header_middleware ],
"service" => role_config.full_name
}
},
"services" => {
role_config.full_name => {
"loadbalancer" => {
"servers" => [ { "url" => "http://#{ip_address}:80" } ]
}
}
},
"middlewares" => {
run_id_header_middleware => {
"headers" => {
"customresponseheaders" => {
RUN_ID_HEADER => run_id
}
}
}
}
}
}
end
def custom_config
traefik_config.fetch("dynamic", {}).deep_merge(role_traefik_config.fetch("dynamic", {}))
end
end

View File

@@ -0,0 +1,84 @@
class Kamal::Configuration::Traefik::Static
CONTAINER_PORT = 80
DEFAULT_IMAGE = "traefik:v2.9"
CONFIG_DIRECTORY = "/var/run/traefik-config"
DEFAULT_ARGS = {
"providers.docker": true, # Obsolete now but required for zero-downtime upgrade from previous versions
"providers.file.directory" => "/var/run/traefik-config",
"providers.file.watch": true,
"log.level" => "DEBUG",
}
delegate :argumentize, :env_file_with_secrets, :optionize, to: Kamal::Utils
attr_reader :config, :traefik_config
def initialize(config:)
@config = config
@traefik_config = config.traefik || {}
end
def docker_args
[
"--name traefik",
"--detach",
"--restart", "unless-stopped",
*publish_args,
"--volume", "/var/run/docker.sock:/var/run/docker.sock",
"--volume", "#{host_directory}:#{CONFIG_DIRECTORY}",
*env_args,
*config.logging_args,
*label_args,
*docker_options_args
]
end
def image
traefik_config.fetch("image") { DEFAULT_IMAGE }
end
def traefik_args
optionize DEFAULT_ARGS.merge(traefik_config.fetch("args", {})), with: "="
end
def host_directory
if Pathname.new(config.run_directory).absolute?
"#{config.run_directory}/traefik-config"
else
"$(pwd)/#{config.run_directory}/traefik-config"
end
end
def host_env_file_path
File.join host_env_directory, "traefik.env"
end
def host_env_directory
File.join config.host_env_directory, "traefik"
end
def env_file
env_file_with_secrets config.traefik.fetch("env", {})
end
private
def host_port
traefik_config.fetch("host_port", CONTAINER_PORT)
end
def publish_args
argumentize "--publish", "#{host_port}:#{CONTAINER_PORT}" unless traefik_config["publish"] == false
end
def env_args
argumentize "--env-file", host_env_file_path
end
def label_args
argumentize "--label", traefik_config.fetch("labels", [])
end
def docker_options_args
optionize(traefik_config["options"] || {})
end
end

View File

@@ -115,4 +115,25 @@ module Kamal::Utils
"#{key.to_s}=#{value.to_s}\n"
end
def poll(max_attempts:, exception:, &block)
attempt = 1
begin
block.call
rescue exception => e
if attempt <= max_attempts
info "#{e.message}, retrying in #{attempt}s (attempt #{attempt}/#{max_attempts})..."
sleep attempt
attempt += 1
retry
else
raise
end
end
end
def info(message)
SSHKit.config.output.info(message)
end
end

View File

@@ -5,10 +5,7 @@ class Kamal::Utils::HealthcheckPoller
class << self
def wait_for_healthy(pause_after_ready: false, &block)
attempt = 1
max_attempts = KAMAL.config.healthcheck["max_attempts"]
begin
Kamal::Utils.poll(max_attempts: KAMAL.config.healthcheck["max_attempts"], exception: HealthcheckError) do
case status = block.call
when "healthy"
sleep TRAEFIK_HEALTHY_DELAY if pause_after_ready
@@ -17,23 +14,9 @@ class Kamal::Utils::HealthcheckPoller
else
raise HealthcheckError, "container not ready (#{status})"
end
rescue HealthcheckError => e
if attempt <= max_attempts
info "#{e.message}, retrying in #{attempt}s (attempt #{attempt}/#{max_attempts})..."
sleep attempt
attempt += 1
retry
else
raise
end
end
info "Container is healthy!"
SSHKit.config.output.info "Container is healthy!"
end
end
private
def info(message)
SSHKit.config.output.info(message)
end
end
end

View File

@@ -0,0 +1,17 @@
class Kamal::Utils::SwitchPoller
class SwitchError < StandardError; end
class << self
TRAEFIK_SWITCH_DELAY = 2
def wait_for_switch(traefik_dynamic, &block)
if traefik_dynamic.boot_check?
Kamal::Utils.poll(max_attempts: 5, exception: SwitchError) do
polled_run_id = block.call
raise SwitchError, "Waiting for #{traefik_dynamic.config_run_id}, currently #{polled_run_id}" unless polled_run_id == traefik_dynamic.config_run_id
end
else
sleep TRAEFIK_SWITCH_DELAY
end
end
end
end