Compare commits

..

1 Commits

Author SHA1 Message Date
Donal McBreen
70096160c9 Replace .env* with .kamal/env*
By default look for the env file in .kamal/env to avoid clashes with
other tools using .env.

For now we'll still load .env and issue a deprecation warning, but in
future we'll stop reading those.
2024-07-31 10:18:59 +01:00
57 changed files with 136 additions and 1243 deletions

View File

@@ -1,7 +1,7 @@
PATH
remote: .
specs:
kamal (2.0.0.alpha)
kamal (1.8.1)
activesupport (>= 7.0)
base64 (~> 0.2)
bcrypt_pbkdf (~> 1.0)
@@ -10,7 +10,7 @@ PATH
ed25519 (~> 1.2)
net-ssh (~> 7.0)
sshkit (>= 1.23.0, < 2.0)
thor (~> 1.3)
thor (~> 1.2)
zeitwerk (~> 2.5)
GEM

View File

@@ -17,14 +17,12 @@ end
DOCS = {
"accessory" => "Accessories",
"alias" => "Aliases",
"boot" => "Booting",
"builder" => "Builders",
"configuration" => "Configuration overview",
"env" => "Environment variables",
"healthcheck" => "Healthchecks",
"logging" => "Logging",
"proxy" => "Proxy (Experimental)",
"registry" => "Docker Registry",
"role" => "Roles",
"servers" => "Servers",
@@ -69,27 +67,26 @@ class DocWriter
output.puts
place = :new_section
elsif line =~ /^ *#/
generate_line(line, heading: place == :new_section)
generate_line(line, place: place)
place = :in_section
else
output.puts "```yaml"
output.puts line
output.print line
place = :in_yaml
end
when :in_yaml, :in_empty_line_yaml
when :in_yaml
if line =~ /^ *#/
output.puts "```"
generate_line(line, heading: place == :in_empty_line_yaml)
generate_line(line, place: :new_section)
place = :in_section
elsif line.empty?
place = :in_empty_line_yaml
else
output.puts line
output.puts
output.print line
end
end
end
output.puts "```" if place == :in_yaml
output.puts "\n```" if place == :in_yaml
end
def generate_header
@@ -101,7 +98,7 @@ class DocWriter
output.puts
end
def generate_line(line, heading: false)
def generate_line(line, place: :in_section)
line = line.gsub(/^ *#\s?/, "")
if line =~ /(.*)kamal docs ([a-z]*)(.*)/
@@ -112,7 +109,7 @@ class DocWriter
line = "#{$1}[#{titlify($2.split("/").last)}](#{$2})#{$3}"
end
if heading
if place == :new_section
output.puts "## [#{line}](##{linkify(line)})"
else
output.puts line

View File

@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
spec.add_dependency "activesupport", ">= 7.0"
spec.add_dependency "sshkit", ">= 1.23.0", "< 2.0"
spec.add_dependency "net-ssh", "~> 7.0"
spec.add_dependency "thor", "~> 1.3"
spec.add_dependency "thor", "~> 1.2"
spec.add_dependency "dotenv", "~> 2.8"
spec.add_dependency "zeitwerk", "~> 2.5"
spec.add_dependency "ed25519", "~> 1.2"

View File

@@ -1,5 +1,4 @@
module Kamal::Cli
class BootError < StandardError; end
class HookError < StandardError; end
class LockError < StandardError; end
end

View File

@@ -1,9 +0,0 @@
class Kamal::Cli::Alias::Command < Thor::DynamicCommand
def run(instance, args = [])
if (_alias = KAMAL.config.aliases[name])
Kamal::Cli::Main.start(Shellwords.split(_alias.command) + ARGV[1..-1])
else
super
end
end
end

View File

@@ -38,17 +38,8 @@ 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 *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
execute *KAMAL.app(role: role, host: host).start, raise_on_non_zero_exit: false
end
end
end
@@ -61,18 +52,8 @@ 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
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
execute *KAMAL.app(role: role, host: host).stop, raise_on_non_zero_exit: false
end
end
end
@@ -90,12 +71,11 @@ class Kamal::Cli::App < Kamal::Cli::Base
end
end
desc "exec [CMD...]", "Execute a custom command on servers within the app container (use --help to show options)"
desc "exec [CMD]", "Execute a custom command on servers within the app container (use --help to show options)"
option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
option :env, aliases: "-e", type: :hash, desc: "Set environment variables for the command"
def exec(*cmd)
cmd = Kamal::Utils.join_commands(cmd)
def exec(cmd)
env = options[:env]
case
when options[:interactive] && options[:reuse]

View File

@@ -45,22 +45,11 @@ class Kamal::Cli::App::Boot
def start_new_version
audit "Booted app version #{version}"
hostname = "#{host.to_s[0...51].gsub(/\.+$/, '')}-#{SecureRandom.hex(6)}"
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
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)) }
end
def stop_new_version
@@ -68,7 +57,7 @@ class Kamal::Cli::App::Boot
end
def stop_old_version(version)
if uses_cord? && !proxy_host?
if uses_cord?
cord = capture_with_info(*app.cord(version: version), raise_on_non_zero_exit: false).strip
if cord.present?
execute *app.cut_cord(cord)
@@ -127,8 +116,4 @@ class Kamal::Cli::App::Boot
def queuer?
barrier && !barrier_role?
end
def proxy_host?
KAMAL.proxy_host?(host)
end
end

View File

@@ -6,8 +6,7 @@ module Kamal::Cli
class Base < Thor
include SSHKit::DSL
def self.exit_on_failure?() false end
def self.dynamic_command_class() Kamal::Cli::Alias::Command end
def self.exit_on_failure?() true end
class_option :verbose, type: :boolean, aliases: "-v", desc: "Detailed logging"
class_option :quiet, type: :boolean, aliases: "-q", desc: "Minimal logging"
@@ -23,14 +22,8 @@ module Kamal::Cli
class_option :skip_hooks, aliases: "-H", type: :boolean, default: false, desc: "Don't run hooks"
def initialize(args = [], local_options = {}, config = {})
if config[:current_command].is_a?(Kamal::Cli::Alias::Command)
# When Thor generates a dynamic command, it doesn't attempt to parse the arguments.
# For our purposes, it means the arguments are passed in args rather than local_options.
super([], args, config)
else
super
end
def initialize(*)
super
@original_env = ENV.to_h.dup
load_env
initialize_commander(options_with_subcommand_class_options)
@@ -44,9 +37,22 @@ module Kamal::Cli
def load_env
if destination = options[:destination]
Dotenv.load(".env.#{destination}", ".env")
if File.exist?(".kamal/env.#{destination}") || File.exist?(".kamal/env")
Dotenv.load(".kamal/env.#{destination}", ".kamal/env")
else
loading_files = [ (".env" if File.exist?(".env")), (".env.#{destination}" if File.exist?(".env.#{destination}")) ].compact
if loading_files.any?
warn "Loading #{loading_files.join(" and ")} from the project root, use .kamal/env* instead"
Dotenv.load(".env.#{destination}", ".env")
end
end
else
Dotenv.load(".env")
if File.exist?(".kamal/env")
Dotenv.load(".kamal/env")
elsif File.exist?(".env")
warn "Loading .env from the project root is deprecated, use .kamal/env instead"
Dotenv.load(".env")
end
end
end

View File

@@ -39,13 +39,8 @@ class Kamal::Cli::Main < Kamal::Cli::Base
with_lock do
run_hook "pre-deploy"
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 "Ensure Traefik is running...", :magenta
invoke "kamal:cli:traefik:boot", [], invoke_options
say "Detect stale containers...", :magenta
invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
@@ -113,11 +108,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
desc "details", "Show details about all containers"
def details
if KAMAL.config.proxy.enabled?
invoke "kamal:cli:proxy:details"
else
invoke "kamal:cli:traefik:details"
end
invoke "kamal:cli:traefik:details"
invoke "kamal:cli:app:details"
invoke "kamal:cli:accessory:details", [ "all" ]
end
@@ -192,11 +183,25 @@ class Kamal::Cli::Main < Kamal::Cli::Base
option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip .env file push"
def envify
if destination = options[:destination]
env_template_path = ".env.#{destination}.erb"
env_path = ".env.#{destination}"
env_template_path = ".kamal/env.#{destination}.erb"
env_path = ".kamal/env.#{destination}"
else
env_template_path = ".env.erb"
env_path = ".env"
env_template_path = ".kamal/env.erb"
env_path = ".kamal/env"
end
unless Pathname.new(File.expand_path(env_template_path)).exist?
if destination = options[:destination]
env_template_path = ".env.#{destination}.erb"
env_path = ".env.#{destination}"
else
env_template_path = ".env.erb"
env_path = ".env"
end
if Pathname.new(File.expand_path(env_template_path)).exist?
warn "Loading #{env_template_path} from the project root is deprecated, use .kamal/env[.<DESTINATION>].erb instead"
end
end
if Pathname.new(File.expand_path(env_template_path)).exist?
@@ -218,11 +223,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
def remove
confirming "This will remove all containers and images. Are you sure?" do
with_lock do
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:traefik:remove", [], options.without(:confirmed)
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)
@@ -250,9 +251,6 @@ 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

View File

@@ -1,160 +0,0 @@
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

View File

@@ -1,8 +1,7 @@
class Kamal::Cli::Server < Kamal::Cli::Base
desc "exec", "Run a custom command on the server (use --help to show options)"
option :interactive, type: :boolean, aliases: "-i", default: false, desc: "Run the command interactively (use for console/bash)"
def exec(*cmd)
cmd = Kamal::Utils.join_commands(cmd)
def exec(cmd)
hosts = KAMAL.hosts | KAMAL.accessory_hosts
case

View File

@@ -1,7 +1,6 @@
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
@@ -14,7 +13,6 @@ 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 ]
@@ -36,7 +34,6 @@ 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
@@ -47,7 +44,6 @@ 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
@@ -58,7 +54,6 @@ 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
@@ -67,7 +62,6 @@ 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
@@ -78,7 +72,6 @@ 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]
@@ -100,7 +93,6 @@ 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
@@ -110,7 +102,6 @@ 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
@@ -121,7 +112,6 @@ 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
@@ -129,11 +119,4 @@ 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

View File

@@ -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, :proxy_hosts, :proxy_host?, :accessory_hosts, to: :specifics
delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :traefik_hosts, :accessory_hosts, to: :specifics
def initialize
self.verbosity = :info
@@ -27,11 +27,7 @@ class Kamal::Commander
def specific_primary!
@specifics = nil
if specific_roles.present?
self.specific_hosts = [ specific_roles.first.primary_host ]
else
self.specific_hosts = [ config.primary_host ]
end
self.specific_hosts = [ config.primary_host ]
end
def specific_roles=(role_names)
@@ -101,10 +97,6 @@ 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
@@ -121,14 +113,6 @@ class Kamal::Commander
@traefik ||= Kamal::Commands::Traefik.new(config)
end
def alias(name)
config.aliases[name]
end
def traefik_or_proxy(host)
proxy_host?(host) ? proxy : traefik
end
def with_verbosity(level)
old_level = self.verbosity

View File

@@ -22,15 +22,6 @@ 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

View File

@@ -30,24 +30,6 @@ 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

View File

@@ -28,11 +28,4 @@ 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

View File

@@ -6,7 +6,7 @@ module Kamal::Commands::Builder::Clone
end
def clone
git :clone, Kamal::Git.root, "--recurse-submodules", path: clone_directory
git :clone, Kamal::Git.root, path: clone_directory
end
def clone_reset_steps
@@ -14,8 +14,7 @@ module Kamal::Commands::Builder::Clone
git(:remote, "set-url", :origin, Kamal::Git.root, path: build_directory),
git(:fetch, :origin, path: build_directory),
git(:reset, "--hard", Kamal::Git.revision, path: build_directory),
git(:clean, "-fdx", path: build_directory),
git(:submodule, :update, "--init", path: build_directory)
git(:clean, "-fdx", path: build_directory)
]
end

View File

@@ -1,68 +0,0 @@
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

View File

@@ -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, :proxy, :traefik, :servers, :ssh, :sshkit, :registry
attr_reader :accessories, :boot, :builder, :env, :healthcheck, :logging, :traefik, :servers, :ssh, :sshkit, :registry
include Validation
@@ -54,14 +54,12 @@ class Kamal::Configuration
@registry = Registry.new(config: self)
@accessories = @raw_config.accessories&.keys&.collect { |name| Accessory.new(name, config: self) } || []
@aliases = @raw_config.aliases&.keys&.to_h { |name| [ name, Alias.new(name, config: self) ] } || {}
@boot = Boot.new(config: self)
@builder = Builder.new(config: self)
@env = Env.new(config: @raw_config.env || {})
@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)
@@ -143,10 +141,6 @@ class Kamal::Configuration
traefik_roles.flat_map(&:hosts).uniq
end
def proxy_hosts
proxy.hosts
end
def repository
[ registry.server, image ].compact.join("/")
end

View File

@@ -1,15 +0,0 @@
class Kamal::Configuration::Alias
include Kamal::Configuration::Validation
attr_reader :name, :command
def initialize(name, config:)
@name, @command = name.inquiry, config.raw_config["aliases"][name]
validate! \
command,
example: validation_yml["aliases"]["uname"],
context: "aliases/#{name}",
with: Kamal::Configuration::Validator::Alias
end
end

View File

@@ -1,26 +0,0 @@
# Aliases
#
# Aliases are shortcuts for Kamal commands.
#
# For example, for a Rails app, you might open a console with:
#
# ```shell
# kamal app exec -i -r console "rails console"
# ```
#
# By defining an alias, like this:
aliases:
console: app exec -r console -i "rails console"
# You can now open the console with:
# ```shell
# kamal console
# ```
# Configuring aliases
#
# Aliases are defined in the root config under the alias key
#
# Each alias is named and can only contain lowercase letters, numbers, dashes and underscores.
aliases:
uname: app exec -p -q -r web "uname -a"

View File

@@ -143,12 +143,6 @@ accessories:
traefik:
...
# Proxy
#
# **Experimental** Configuration for kamal-proxy the replacement for Traefik, see kamal docs proxy
proxy:
...
# SSHKit
#
# See kamal docs sshkit
@@ -172,9 +166,3 @@ healthcheck:
# Docker logging configuration, see kamal docs logging
logging:
...
# Aliases
#
# Alias configuration, see kamal docs alias
aliases:
...

View File

@@ -29,7 +29,7 @@ env:
# To pass the secrets you should list them under the `secret` key. When you do this the
# other variables need to be moved under the `clear` key.
#
# Unlike clear values, secrets are not passed directly to the container,
# Unlike clear valies, secrets are not passed directly to the container,
# but are stored in an env file on the host
# The file is not updated when deploying, only when running `kamal envify` or `kamal env push`.
env:

View File

@@ -1,114 +0,0 @@
# 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

View File

@@ -1,66 +0,0 @@
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

View File

@@ -58,18 +58,10 @@ 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

View File

@@ -13,34 +13,32 @@ class Kamal::Configuration::Validator
private
def validate_against_example!(validation_config, example)
validate_type! validation_config, example.class
validate_type! validation_config, Hash
if example.class == Hash
check_unknown_keys! validation_config, example
check_unknown_keys! validation_config, example
validation_config.each do |key, value|
next if extension?(key)
with_context(key) do
example_value = example[key]
validation_config.each do |key, value|
next if extension?(key)
with_context(key) do
example_value = example[key]
if example_value == "..."
validate_type! value, *(Array if key == :servers), Hash
elsif key == "hosts"
validate_servers! value
elsif example_value.is_a?(Array)
validate_array_of! value, example_value.first.class
elsif example_value.is_a?(Hash)
case key.to_s
when "options", "args"
validate_type! value, Hash
when "labels"
validate_hash_of! value, example_value.first[1].class
else
validate_against_example! value, example_value
end
if example_value == "..."
validate_type! value, *(Array if key == :servers), Hash
elsif key == "hosts"
validate_servers! value
elsif example_value.is_a?(Array)
validate_array_of! value, example_value.first.class
elsif example_value.is_a?(Hash)
case key.to_s
when "options", "args"
validate_type! value, Hash
when "labels"
validate_hash_of! value, example_value.first[1].class
else
validate_type! value, example_value.class
validate_against_example! value, example_value
end
else
validate_type! value, example_value.class
end
end
end

View File

@@ -1,15 +0,0 @@
class Kamal::Configuration::Validator::Alias < Kamal::Configuration::Validator
def validate!
super
name = context.delete_prefix("aliases/")
if name !~ /\A[a-z0-9_-]+\z/
error "Invalid alias name: '#{name}'. Must only contain lowercase letters, alphanumeric, hyphens and underscores."
end
if Kamal::Cli::Main.commands.include?(name)
error "Alias '#{name}' conflicts with a built-in command."
end
end
end

View File

@@ -77,8 +77,4 @@ module Kamal::Utils
def stable_sort!(elements, &block)
elements.sort_by!.with_index { |element, index| [ block.call(element), index ] }
end
def join_commands(commands)
commands.map(&:strip).join(" ")
end
end

View File

@@ -1,3 +1,3 @@
module Kamal
VERSION = "2.0.0.alpha"
VERSION = "1.8.1"
end

View File

@@ -247,12 +247,6 @@ class CliAppTest < CliTestCase
end
end
test "exec separate arguments" do
run_command("exec", "ruby", " -v").tap do |output|
assert_match "docker run --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v", output
end
end
test "exec with reuse" do
run_command("exec", "--reuse", "ruby -v").tap do |output|
assert_match "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", output # Get current version
@@ -356,18 +350,6 @@ 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

View File

@@ -42,7 +42,7 @@ class CliBuildTest < CliTestCase
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "--version", "&&", :docker, :buildx, "version")
SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:git, "-C", "#{Dir.tmpdir}/kamal-clones/app-#{pwd_sha}", :clone, Dir.pwd, "--recurse-submodules")
.with(:git, "-C", "#{Dir.tmpdir}/kamal-clones/app-#{pwd_sha}", :clone, Dir.pwd)
.raises(SSHKit::Command::Failed.new("fatal: destination path 'kamal' already exists and is not an empty directory"))
.then
.returns(true)
@@ -50,7 +50,6 @@ class CliBuildTest < CliTestCase
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :fetch, :origin)
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :reset, "--hard", Kamal::Git.revision)
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :clean, "-fdx")
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,linux/arm64", "--builder", "kamal-app-multiarch", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".")
@@ -89,7 +88,7 @@ class CliBuildTest < CliTestCase
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "--version", "&&", :docker, :buildx, "version")
SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:git, "-C", "#{Dir.tmpdir}/kamal-clones/app-#{pwd_sha}", :clone, Dir.pwd, "--recurse-submodules")
.with(:git, "-C", "#{Dir.tmpdir}/kamal-clones/app-#{pwd_sha}", :clone, Dir.pwd)
.raises(SSHKit::Command::Failed.new("fatal: destination path 'kamal' already exists and is not an empty directory"))
.then
.returns(true)

View File

@@ -63,12 +63,4 @@ class CliTestCase < ActiveSupport::TestCase
assert_match expected, output
end
def with_argv(*argv)
old_argv = ARGV
ARGV.replace(*argv)
yield
ensure
ARGV.replace(old_argv)
end
end

View File

@@ -447,9 +447,9 @@ class CliMainTest < CliTestCase
end
test "envify" do
with_test_dotenv(".env.erb": "HELLO=<%= 'world' %>") do
with_test_env_files("env.erb": "HELLO=<%= 'world' %>") do
run_command("envify")
assert_equal("HELLO=world", File.read(".env"))
assert_equal("HELLO=world", File.read(".kamal/env"))
end
end
@@ -461,32 +461,32 @@ class CliMainTest < CliTestCase
<% end -%>
EOF
with_test_dotenv(".env.erb": file) do
with_test_env_files("env.erb": file) do
run_command("envify")
assert_equal("HELLO=world\nKEY=value\n", File.read(".env"))
assert_equal("HELLO=world\nKEY=value\n", File.read(".kamal/env"))
end
end
test "envify with destination" do
with_test_dotenv(".env.world.erb": "HELLO=<%= 'world' %>") do
with_test_env_files("env.world.erb": "HELLO=<%= 'world' %>") do
run_command("envify", "-d", "world", config_file: "deploy_for_dest")
assert_equal "HELLO=world", File.read(".env.world")
assert_equal "HELLO=world", File.read(".kamal/env.world")
end
end
test "envify with skip_push" do
Pathname.any_instance.expects(:exist?).returns(true).times(1)
File.expects(:read).with(".env.erb").returns("HELLO=<%= 'world' %>")
File.expects(:write).with(".env", "HELLO=world", perm: 0600)
Pathname.any_instance.expects(:exist?).returns(true).times(2)
File.expects(:read).with(".kamal/env.erb").returns("HELLO=<%= 'world' %>")
File.expects(:write).with(".kamal/env", "HELLO=world", perm: 0600)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:env:push").never
run_command("envify", "--skip-push")
end
test "envify with clean env" do
with_test_dotenv(".env": "HELLO=already", ".env.erb": "HELLO=<%= ENV.fetch 'HELLO', 'never' %>") do
with_test_env_files("env": "HELLO=already", "env.erb": "HELLO=<%= ENV.fetch 'HELLO', 'never' %>") do
run_command("envify", "--skip-push")
assert_equal "HELLO=never", File.read(".env")
assert_equal "HELLO=never", File.read(".kamal/env")
end
end
@@ -537,58 +537,23 @@ class CliMainTest < CliTestCase
assert_equal Kamal::VERSION, version
end
test "run an alias for details" do
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:details")
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:details")
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:details", [ "all" ])
run_command("info", config_file: "deploy_with_aliases")
end
test "run an alias for a console" do
run_command("console", config_file: "deploy_with_aliases").tap do |output|
assert_match "docker exec app-console-999 bin/console on 1.1.1.5", output
assert_match "App Host: 1.1.1.5", output
end
end
test "run an alias for a console overriding role" do
run_command("console", "-r", "workers", config_file: "deploy_with_aliases").tap do |output|
assert_match "docker exec app-workers-999 bin/console on 1.1.1.3", output
assert_match "App Host: 1.1.1.3", output
end
end
test "run an alias for a console passing command" do
run_command("exec", "bin/job", config_file: "deploy_with_aliases").tap do |output|
assert_match "docker exec app-console-999 bin/job on 1.1.1.5", output
assert_match "App Host: 1.1.1.5", output
end
end
test "append to command with an alias" do
run_command("rails", "db:migrate:status", config_file: "deploy_with_aliases").tap do |output|
assert_match "docker exec app-console-999 rails db:migrate:status on 1.1.1.5", output
assert_match "App Host: 1.1.1.5", output
end
end
private
def run_command(*command, config_file: "deploy_simple")
with_argv([ *command, "-c", "test/fixtures/#{config_file}.yml" ]) do
stdouted { Kamal::Cli::Main.start }
end
stdouted { Kamal::Cli::Main.start([ *command, "-c", "test/fixtures/#{config_file}.yml" ]) }
end
def with_test_dotenv(**files)
def with_test_env_files(**files)
Dir.mktmpdir do |dir|
fixtures_dup = File.join(dir, "test")
FileUtils.mkdir_p(fixtures_dup)
FileUtils.cp_r("test/fixtures/", fixtures_dup)
Dir.chdir(dir) do
files.each do |filename, contents|
File.binwrite(filename.to_s, contents)
FileUtils.mkdir_p(".kamal")
Dir.chdir(".kamal") do
files.each do |filename, contents|
File.binwrite(filename.to_s, contents)
end
end
yield
end

View File

@@ -1,141 +0,0 @@
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

View File

@@ -3,8 +3,8 @@ require_relative "cli_test_case"
class CliServerTest < CliTestCase
test "running a command with exec" do
SSHKit::Backend::Abstract.any_instance.stubs(:capture)
.with("date", verbosity: 1)
.returns("Today")
.with("date", verbosity: 1)
.returns("Today")
hosts = "1.1.1.1".."1.1.1.4"
run_command("exec", "date").tap do |output|
@@ -15,20 +15,6 @@ class CliServerTest < CliTestCase
end
end
test "running a command with exec multiple arguments" do
SSHKit::Backend::Abstract.any_instance.stubs(:capture)
.with("date -j", verbosity: 1)
.returns("Today")
hosts = "1.1.1.1".."1.1.1.4"
run_command("exec", "date", "-j").tap do |output|
hosts.map do |host|
assert_match "Running 'date -j' on #{hosts.to_a.join(', ')}...", output
assert_match "App Host: #{host}\nToday", output
end
end
end
test "bootstrap already installed" do
stub_setup
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(true).at_least_once

View File

@@ -1,126 +0,0 @@
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

View File

@@ -1,21 +0,0 @@
service: app
image: dhh/app
servers:
web:
- 1.1.1.1
- 1.1.1.2
workers:
hosts:
- 1.1.1.3
- 1.1.1.4
console:
hosts:
- 1.1.1.5
registry:
username: user
password: pw
aliases:
info: details
console: app exec --reuse -p -r console "bin/console"
exec: app exec --reuse -p -r console
rails: app exec --reuse -p -r console rails

View File

@@ -1,42 +0,0 @@
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

View File

@@ -29,5 +29,6 @@ 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

View File

@@ -29,6 +29,8 @@ services:
context: docker/registry
environment:
- REGISTRY_HTTP_ADDR=0.0.0.0:4443
- REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt
- REGISTRY_HTTP_TLS_KEY=/certs/domain.key
volumes:
- shared:/shared
- registry:/var/lib/registry/

View File

@@ -22,6 +22,7 @@ COPY app_with_roles/ app_with_roles/
RUN rm -rf /root/.ssh
RUN ln -s /shared/ssh /root/.ssh
RUN mkdir -p /etc/docker/certs.d/registry:4443 && ln -s /shared/certs/domain.crt /etc/docker/certs.d/registry:4443/ca.crt
RUN git config --global user.email "deployer@example.com"
RUN git config --global user.name "Deployer"

View File

@@ -1,4 +1,8 @@
#!/bin/sh
echo "About to lock..."
if [ "$KAMAL_HOSTS" != "vm1,vm2" ]; then
echo "Expected hosts to be 'vm1,vm2', got $KAMAL_HOSTS"
exit 1
fi
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-connect

View File

@@ -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

View File

@@ -1,4 +1,8 @@
#!/bin/sh
echo "About to lock..."
if [ "$KAMAL_HOSTS" != "vm1,vm2,vm3" ]; then
echo "Expected hosts to be 'vm1,vm2,vm3', got $KAMAL_HOSTS"
exit 1
fi
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-connect

View File

@@ -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

View File

@@ -9,11 +9,6 @@ servers:
hosts:
- vm3
cmd: sleep infinity
proxy:
enabled: true
hosts:
- vm2
deploy_timeout: 2s
asset_path: /usr/share/nginx/html/versions
@@ -42,7 +37,3 @@ accessories:
- web
stop_wait_time: 1
readiness_delay: 0
aliases:
whome: version
worker_hostname: app exec -r workers -q --reuse hostname
uname: server exec -q -p uname

View File

@@ -1,5 +1,5 @@
#!/bin/bash
dockerd --max-concurrent-downloads 1 --insecure-registry registry:4443 &
dockerd --max-concurrent-downloads 1 &
exec sleep infinity

View File

@@ -1,3 +1,5 @@
#!/bin/sh
while [ ! -f /certs/domain.crt ]; do sleep 1; done
exec /entrypoint.sh /etc/docker/registry/config.yml

View File

@@ -10,6 +10,8 @@ RUN mkdir ssh && \
COPY registry-dns.conf .
COPY boot.sh .
RUN mkdir certs && openssl req -newkey rsa:4096 -nodes -sha256 -keyout certs/domain.key -x509 -days 365 -out certs/domain.crt -subj '/CN=registry' -extensions EXT -config registry-dns.conf
HEALTHCHECK --interval=1s CMD pgrep sleep
CMD ["./boot.sh"]

View File

@@ -5,6 +5,7 @@ WORKDIR /work
RUN apt-get update --fix-missing && apt-get -y install openssh-client openssh-server docker.io
RUN mkdir /root/.ssh && ln -s /shared/ssh/id_rsa.pub /root/.ssh/authorized_keys
RUN mkdir -p /etc/docker/certs.d/registry:4443 && ln -s /shared/certs/domain.crt /etc/docker/certs.d/registry:4443/ca.crt
RUN echo "HOST_TOKEN=abcd" >> /etc/environment

View File

@@ -4,6 +4,6 @@ while [ ! -f /root/.ssh/authorized_keys ]; do echo "Waiting for ssh keys"; sleep
service ssh restart
dockerd --max-concurrent-downloads 1 --insecure-registry registry:4443 &
dockerd --max-concurrent-downloads 1 &
exec sleep infinity

View File

@@ -31,7 +31,7 @@ class IntegrationTest < ActiveSupport::TestCase
succeeded = system("cd test/integration && #{command}")
end
raise "Command `#{command}` failed with error code `#{$?}`, and output:\n#{result}" if !succeeded && raise_on_error
raise "Command `#{command}` failed with error code `#{$?}`" if !succeeded && raise_on_error
result
end

View File

@@ -82,22 +82,6 @@ class MainTest < IntegrationTest
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
test "aliases" do
@app = "app_with_roles"
kamal :envify
kamal :deploy
output = kamal :whome, capture: true
assert_equal Kamal::VERSION, output
output = kamal :worker_hostname, capture: true
assert_match /App Host: vm3\nvm3-[0-9a-f]{12}$/, output
output = kamal :uname, "-o", capture: true
assert_match "App Host: vm1\nGNU/Linux", output
end
test "setup and remove" do
# Check remove completes when nothing has been setup yet
kamal :remove, "-y"
@@ -113,7 +97,7 @@ class MainTest < IntegrationTest
private
def assert_local_env_file(contents)
assert_equal contents, deployer_exec("cat .env", capture: true)
assert_equal contents, deployer_exec("cat .kamal/env", capture: true)
end
def assert_envs(version:)
@@ -143,7 +127,7 @@ class MainTest < IntegrationTest
end
def remove_local_env_file
deployer_exec("rm .env")
deployer_exec("rm .kamal/env")
end
def assert_remote_env_file(contents, vm:)

View File

@@ -1,84 +0,0 @@
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