Compare commits

..

1 Commits

Author SHA1 Message Date
Donal McBreen
00e3015142 Concurrent local and remote docker login
Login to the local and remote docker registries concurrently to shave
a couple of seconds off a deployment.
2024-01-11 10:02:04 +00:00
54 changed files with 145 additions and 437 deletions

View File

@@ -1,7 +1,7 @@
PATH
remote: .
specs:
kamal (1.4.0)
kamal (1.3.1)
activesupport (>= 7.0)
base64 (~> 0.2)
bcrypt_pbkdf (~> 1.0)

View File

@@ -5,11 +5,11 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
if name == "all"
KAMAL.accessory_names.each { |accessory_name| boot(accessory_name) }
else
with_accessory(name) do |accessory, hosts|
with_accessory(name) do |accessory|
directories(name)
upload(name)
on(hosts) do
on(accessory.hosts) do
execute *KAMAL.registry.login if login
execute *KAMAL.auditor.record("Booted #{name} accessory"), verbosity: :debug
execute *accessory.run
@@ -22,8 +22,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
desc "upload [NAME]", "Upload accessory files to host", hide: true
def upload(name)
mutating do
with_accessory(name) do |accessory, hosts|
on(hosts) do
with_accessory(name) do |accessory|
on(accessory.hosts) do
accessory.files.each do |(local, remote)|
accessory.ensure_local_file_present(local)
@@ -39,8 +39,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
desc "directories [NAME]", "Create accessory directories on host", hide: true
def directories(name)
mutating do
with_accessory(name) do |accessory, hosts|
on(hosts) do
with_accessory(name) do |accessory|
on(accessory.hosts) do
accessory.directories.keys.each do |host_path|
execute *accessory.make_directory(host_path)
end
@@ -55,8 +55,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
if name == "all"
KAMAL.accessory_names.each { |accessory_name| reboot(accessory_name) }
else
with_accessory(name) do |accessory, hosts|
on(hosts) do
with_accessory(name) do |accessory|
on(accessory.hosts) do
execute *KAMAL.registry.login
end
@@ -71,8 +71,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
desc "start [NAME]", "Start existing accessory container on host"
def start(name)
mutating do
with_accessory(name) do |accessory, hosts|
on(hosts) do
with_accessory(name) do |accessory|
on(accessory.hosts) do
execute *KAMAL.auditor.record("Started #{name} accessory"), verbosity: :debug
execute *accessory.start
end
@@ -83,8 +83,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
desc "stop [NAME]", "Stop existing accessory container on host"
def stop(name)
mutating do
with_accessory(name) do |accessory, hosts|
on(hosts) do
with_accessory(name) do |accessory|
on(accessory.hosts) do
execute *KAMAL.auditor.record("Stopped #{name} accessory"), verbosity: :debug
execute *accessory.stop, raise_on_non_zero_exit: false
end
@@ -107,8 +107,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
if name == "all"
KAMAL.accessory_names.each { |accessory_name| details(accessory_name) }
else
with_accessory(name) do |accessory, hosts|
on(hosts) { puts capture_with_info(*accessory.info) }
with_accessory(name) do |accessory|
on(accessory.hosts) { puts capture_with_info(*accessory.info) }
end
end
end
@@ -117,7 +117,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
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"
def exec(name, cmd)
with_accessory(name) do |accessory, hosts|
with_accessory(name) do |accessory|
case
when options[:interactive] && options[:reuse]
say "Launching interactive command with via SSH from existing container...", :magenta
@@ -129,14 +129,14 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
when options[:reuse]
say "Launching command from existing container...", :magenta
on(hosts) do
on(accessory.hosts) do
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
capture_with_info(*accessory.execute_in_existing_container(cmd))
end
else
say "Launching command from new container...", :magenta
on(hosts) do
on(accessory.hosts) do
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
capture_with_info(*accessory.execute_in_new_container(cmd))
end
@@ -150,12 +150,12 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
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(name)
with_accessory(name) do |accessory, hosts|
with_accessory(name) do |accessory|
grep = options[:grep]
if options[:follow]
run_locally do
info "Following logs on #{hosts}..."
info "Following logs on #{accessory.hosts}..."
info accessory.follow_logs(grep: grep)
exec accessory.follow_logs(grep: grep)
end
@@ -163,7 +163,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
since = options[:since]
lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
on(hosts) do
on(accessory.hosts) do
puts capture_with_info(*accessory.logs(since: since, lines: lines, grep: grep))
end
end
@@ -192,8 +192,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
desc "remove_container [NAME]", "Remove accessory container from host", hide: true
def remove_container(name)
mutating do
with_accessory(name) do |accessory, hosts|
on(hosts) do
with_accessory(name) do |accessory|
on(accessory.hosts) do
execute *KAMAL.auditor.record("Remove #{name} accessory container"), verbosity: :debug
execute *accessory.remove_container
end
@@ -204,8 +204,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
desc "remove_image [NAME]", "Remove accessory image from host", hide: true
def remove_image(name)
mutating do
with_accessory(name) do |accessory, hosts|
on(hosts) do
with_accessory(name) do |accessory|
on(accessory.hosts) do
execute *KAMAL.auditor.record("Removed #{name} accessory image"), verbosity: :debug
execute *accessory.remove_image
end
@@ -216,8 +216,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host", hide: true
def remove_service_directory(name)
mutating do
with_accessory(name) do |accessory, hosts|
on(hosts) do
with_accessory(name) do |accessory|
on(accessory.hosts) do
execute *accessory.remove_service_directory
end
end
@@ -227,7 +227,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
private
def with_accessory(name)
if accessory = KAMAL.accessory(name)
yield accessory, accessory_hosts(accessory)
yield accessory
else
error_on_missing_accessory(name)
end
@@ -240,12 +240,4 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
"No accessory by the name of '#{name}'" +
(options ? " (options: #{options.to_sentence})" : "")
end
def accessory_hosts(accessory)
if KAMAL.specific_hosts&.any?
KAMAL.specific_hosts & accessory.hosts
else
accessory.hosts
end
end
end

View File

@@ -13,8 +13,9 @@ class Kamal::Cli::App < Kamal::Cli::Base
KAMAL.roles_on(host).each do |role|
app = KAMAL.app(role: role)
role_config = KAMAL.config.role(role)
if role.assets?
if role_config.assets?
execute *app.extract_assets
old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
execute *app.sync_asset_volumes(old_version: old_version)
@@ -26,6 +27,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
KAMAL.roles_on(host).each do |role|
app = KAMAL.app(role: role)
auditor = KAMAL.auditor(role: role)
role_config = KAMAL.config.role(role)
if capture_with_info(*app.container_id_for_version(version), raise_on_non_zero_exit: false).present?
tmp_version = "#{version}_replaced_#{SecureRandom.hex(8)}"
@@ -36,7 +38,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
execute *app.tie_cord(role.cord_host_file) if role.uses_cord?
execute *app.tie_cord(role_config.cord_host_file) if role_config.uses_cord?
execute *auditor.record("Booted app version #{version}"), verbosity: :debug
@@ -45,7 +47,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
if old_version.present?
if role.uses_cord?
if role_config.uses_cord?
cord = capture_with_info(*app.cord(version: old_version), raise_on_non_zero_exit: false).strip
if cord.present?
execute *app.cut_cord(cord)
@@ -55,7 +57,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
execute *app.stop(version: old_version), raise_on_non_zero_exit: false
execute *app.clean_up_assets if role.assets?
execute *app.clean_up_assets if role_config.assets?
end
end
end
@@ -200,20 +202,19 @@ class Kamal::Cli::App < Kamal::Cli::Base
# FIXME: Catch when app containers aren't running
grep = options[:grep]
since = options[:since]
if options[:follow]
lines = options[:lines].presence || ((since || grep) ? nil : 10) # Default to 10 lines if since or grep isn't set
if options[:follow]
run_locally do
info "Following logs on #{KAMAL.primary_host}..."
KAMAL.specific_roles ||= ["web"]
role = KAMAL.roles_on(KAMAL.primary_host).first
info KAMAL.app(role: role).follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep)
exec KAMAL.app(role: role).follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep)
info KAMAL.app(role: role).follow_logs(host: KAMAL.primary_host, grep: grep)
exec KAMAL.app(role: role).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.hosts) do |host|

View File

@@ -123,9 +123,8 @@ module Kamal::Cli
yield
rescue SSHKit::Runner::ExecuteError => e
if e.message =~ /cannot create directory/
say "Deploy lock already in place!", :red
on(KAMAL.primary_host) { puts capture_with_debug(*KAMAL.lock.status) }
raise LockError, "Deploy lock found. Run 'kamal lock help' for more information"
raise LockError, "Deploy lock found"
else
raise e
end

View File

@@ -8,8 +8,9 @@ class Kamal::Cli::Env < Kamal::Cli::Base
execute *KAMAL.auditor.record("Pushed env files"), verbosity: :debug
KAMAL.roles_on(host).each do |role|
role_config = KAMAL.config.role(role)
execute *KAMAL.app(role: role).make_env_directory
upload! StringIO.new(role.env_file), role.host_env_file_path, mode: 400
upload! StringIO.new(role_config.env_file), role_config.host_env_file_path, mode: 400
end
end
@@ -35,6 +36,7 @@ class Kamal::Cli::Env < Kamal::Cli::Base
execute *KAMAL.auditor.record("Deleted env files"), verbosity: :debug
KAMAL.roles_on(host).each do |role|
role_config = KAMAL.config.role(role)
execute *KAMAL.app(role: role).remove_env_file
end
end

View File

@@ -1,18 +1,15 @@
class Kamal::Cli::Main < Kamal::Cli::Base
desc "setup", "Setup all accessories, push the env, and deploy app to servers"
option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
def setup
print_runtime do
mutating do
invoke_options = deploy_options
say "Ensure Docker is installed...", :magenta
invoke "kamal:cli:server:bootstrap", [], invoke_options
invoke "kamal:cli:server:bootstrap"
say "Push env files...", :magenta
invoke "kamal:cli:env:push", [], invoke_options
invoke "kamal:cli:env:push"
invoke "kamal:cli:accessory:boot", [ "all" ], invoke_options
invoke "kamal:cli:accessory:boot", [ "all" ]
deploy
end
end

View File

@@ -18,16 +18,12 @@ class Kamal::Cli::Prune < Kamal::Cli::Base
end
end
desc "containers", "Prune all stopped containers, except the last n (default 5)"
option :retain, type: :numeric, default: nil, desc: "Number of containers to retain"
desc "containers", "Prune all stopped containers, except the last 5"
def containers
retain = options.fetch(:retain, KAMAL.config.retain_containers)
raise "retain must be at least 1" if retain < 1
mutating do
on(KAMAL.hosts) do
execute *KAMAL.auditor.record("Pruned containers"), verbosity: :debug
execute *KAMAL.prune.app_containers(retain: retain)
execute *KAMAL.prune.app_containers
execute *KAMAL.prune.healthcheck_containers
end
end

View File

@@ -1,8 +1,7 @@
class Kamal::Cli::Registry < Kamal::Cli::Base
desc "login", "Log in to registry locally and remotely"
def login
run_locally { execute *KAMAL.registry.login }
on(KAMAL.hosts) { execute *KAMAL.registry.login }
on([ :local ] + KAMAL.hosts) { execute *KAMAL.registry.login }
# FIXME: This rescue needed?
rescue ArgumentError => e
puts e.message

View File

@@ -17,9 +17,7 @@ class Kamal::Cli::Server < Kamal::Cli::Base
end
if missing.any?
raise "Docker is not installed on #{missing.join(", ")} and can't be automatically installed without having root access and either `wget` or `curl`. Install Docker manually: https://docs.docker.com/engine/install/"
end
run_hook "docker-setup"
raise "Docker is not installed on #{missing.join(", ")} and can't be automatically installed without having root access and the `curl` command available. Install Docker manually: https://docs.docker.com/engine/install/"
end
end
end

View File

@@ -77,10 +77,6 @@ registry:
# Bridge fingerprinted assets, like JS and CSS, between versions to avoid
# hitting 404 on in-flight requests. Combines all files from new and old
# version inside the asset_path.
#
# If your app is using the Sprockets gem, ensure it sets `config.assets.manifest`.
# See https://github.com/basecamp/kamal/issues/626 for details
#
# asset_path: /rails/public/assets
# Configure rolling deploys by setting a wait time between batches of restarts.

View File

@@ -1,7 +0,0 @@
#!/usr/bin/env ruby
# A sample docker-setup hook
#
# Sets up a Docker network which can then be used by the applications containers
ssh user@example.com docker network create kamal

View File

@@ -20,7 +20,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
on(hosts) do
execute *KAMAL.auditor.record("Rebooted traefik"), verbosity: :debug
execute *KAMAL.registry.login
execute *KAMAL.traefik.stop, raise_on_non_zero_exit: false
execute *KAMAL.traefik.stop
execute *KAMAL.traefik.remove_container
execute *KAMAL.traefik.run
end
@@ -44,7 +44,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, raise_on_non_zero_exit: false
execute *KAMAL.traefik.stop
end
end
end

View File

@@ -53,7 +53,7 @@ class Kamal::Commander
def primary_host
# Given a list of specific roles, make an effort to match up with the primary_role
specific_hosts&.first || specific_roles&.detect { |role| role == config.primary_role }&.primary_host || specific_roles&.first&.primary_host || config.primary_host
specific_hosts&.first || specific_roles&.detect { |role| role.name == config.primary_role }&.primary_host || specific_roles&.first&.primary_host || config.primary_host
end
def primary_role
@@ -73,7 +73,7 @@ class Kamal::Commander
end
def roles_on(host)
roles.select { |role| role.hosts.include?(host.to_s) }
roles.select { |role| role.hosts.include?(host.to_s) }.map(&:name)
end
def traefik_hosts

View File

@@ -3,11 +3,12 @@ class Kamal::Commands::App < Kamal::Commands::Base
ACTIVE_DOCKER_STATUSES = [ :running, :restarting ]
attr_reader :role, :role
attr_reader :role, :role_config
def initialize(config, role: nil)
super(config)
@role = role
@role_config = config.role(self.role)
end
def run(hostname: nil)
@@ -18,15 +19,15 @@ class Kamal::Commands::App < Kamal::Commands::Base
*(["--hostname", hostname] if hostname),
"-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
"-e", "KAMAL_VERSION=\"#{config.version}\"",
*role.env_args,
*role.health_check_args,
*role.logging_args,
*role_config.env_args,
*role_config.health_check_args,
*config.logging_args,
*config.volume_args,
*role.asset_volume_args,
*role.label_args,
*role.option_args,
*role_config.asset_volume_args,
*role_config.label_args,
*role_config.option_args,
config.absolute_image,
role.cmd
role_config.cmd
end
def start
@@ -63,22 +64,22 @@ 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##{role.container_prefix}-}; done) # Extract SHA from "service-role-dest-SHA"
%(while read line; do echo ${line##{role_config.container_prefix}-}; done) # Extract SHA from "service-role-dest-SHA"
end
def make_env_directory
make_directory role.host_env_directory
make_directory role_config.host_env_directory
end
def remove_env_file
[ :rm, "-f", role.host_env_file_path ]
[ :rm, "-f", role_config.host_env_file_path ]
end
private
def container_name(version = nil)
[ role.container_prefix, version || config.version ].compact.join("-")
[ role_config.container_prefix, version || config.version ].compact.join("-")
end
def filter_args(statuses: nil)

View File

@@ -1,20 +1,20 @@
module Kamal::Commands::App::Assets
def extract_assets
asset_container = "#{role.container_prefix}-assets"
asset_container = "#{role_config.container_prefix}-assets"
combine \
make_directory(role.asset_extracted_path),
make_directory(role_config.asset_extracted_path),
[*docker(:stop, "-t 1", asset_container, "2> /dev/null"), "|| true"],
docker(:run, "--name", asset_container, "--detach", "--rm", config.latest_image, "sleep 1000000"),
docker(:cp, "-L", "#{asset_container}:#{role.asset_path}/.", role.asset_extracted_path),
docker(:cp, "-L", "#{asset_container}:#{role_config.asset_path}/.", role_config.asset_extracted_path),
docker(:stop, "-t 1", asset_container),
by: "&&"
end
def sync_asset_volumes(old_version: nil)
new_extracted_path, new_volume_path = role.asset_extracted_path(config.version), role.asset_volume.host_path
new_extracted_path, new_volume_path = role_config.asset_extracted_path(config.version), role_config.asset_volume.host_path
if old_version.present?
old_extracted_path, old_volume_path = role.asset_extracted_path(old_version), role.asset_volume(old_version).host_path
old_extracted_path, old_volume_path = role_config.asset_extracted_path(old_version), role_config.asset_volume(old_version).host_path
end
commands = [make_directory(new_volume_path), copy_contents(new_extracted_path, new_volume_path)]
@@ -29,8 +29,8 @@ module Kamal::Commands::App::Assets
def clean_up_assets
chain \
find_and_remove_older_siblings(role.asset_extracted_path),
find_and_remove_older_siblings(role.asset_volume_path)
find_and_remove_older_siblings(role_config.asset_extracted_path),
find_and_remove_older_siblings(role_config.asset_volume_path)
end
private
@@ -39,7 +39,7 @@ module Kamal::Commands::App::Assets
:find,
Pathname.new(path).dirname.to_s,
"-maxdepth 1",
"-name", "'#{role.container_prefix}-*'",
"-name", "'#{role_config.container_prefix}-*'",
"!", "-name", Pathname.new(path).basename.to_s,
"-exec rm -rf \"{}\" +"
]

View File

@@ -2,7 +2,7 @@ module Kamal::Commands::App::Cord
def cord(version:)
pipe \
docker(:inspect, "-f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}'", container_name(version)),
[:awk, "'$2 == \"#{role.cord_volume.container_path}\" {print $1}'"]
[:awk, "'$2 == \"#{role_config.cord_volume.container_path}\" {print $1}'"]
end
def tie_cord(cord)

View File

@@ -10,9 +10,9 @@ module Kamal::Commands::App::Execution
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

View File

@@ -6,11 +6,11 @@ module Kamal::Commands::App::Logging
("grep '#{grep}'" if grep)
end
def follow_logs(host:, lines: nil, grep: nil)
def follow_logs(host:, grep: nil)
run_over_ssh \
pipe(
current_running_container_id,
"xargs docker logs --timestamps#{" --tail #{lines}" if lines} --follow 2>&1",
"xargs docker logs --timestamps --tail 10 --follow 2>&1",
(%(grep "#{grep}") if grep)
),
host: host

View File

@@ -62,18 +62,10 @@ module Kamal::Commands
combine *commands, by: ">"
end
def any(*commands)
combine *commands, by: "||"
end
def xargs(command)
[ :xargs, command ].flatten
end
def shell(command)
[ :sh, "-c", "'#{command.flatten.join(" ").gsub("'", "'\\''")}'" ]
end
def docker(*args)
args.compact.unshift :docker
end

View File

@@ -3,7 +3,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
class BuilderError < StandardError; end
delegate :argumentize, to: Kamal::Utils
delegate :args, :secrets, :dockerfile, :local_arch, :local_host, :remote_arch, :remote_host, :cache_from, :cache_to, :ssh, to: :builder_config
delegate :args, :secrets, :dockerfile, :local_arch, :local_host, :remote_arch, :remote_host, :cache_from, :cache_to, to: :builder_config
def clean
docker :image, :rm, "--force", config.absolute_image
@@ -14,7 +14,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
end
def build_options
[ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_ssh ]
[ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile ]
end
def build_context
@@ -24,10 +24,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
def validate_image
pipe \
docker(:inspect, "-f", "'{{ .Config.Labels.service }}'", config.absolute_image),
any(
[:grep, "-x", config.service],
"(echo \"Image #{config.absolute_image} is missing the 'service' label\" && exit 1)"
)
[:grep, "-x", config.service, "||", "(echo \"Image #{config.absolute_image} is missing the `service` label\" && exit 1)"]
end
@@ -63,10 +60,6 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
end
end
def build_ssh
argumentize "--ssh", ssh if ssh.present?
end
def builder_config
config.builder
end

View File

@@ -1,7 +1,7 @@
class Kamal::Commands::Docker < Kamal::Commands::Base
# Install Docker using the https://github.com/docker/docker-install convenience script.
def install
pipe get_docker, :sh
pipe [ :curl, "-fsSL", "https://get.docker.com" ], :sh
end
# Checks the Docker client version. Fails if Docker is not installed.
@@ -18,13 +18,4 @@ class Kamal::Commands::Docker < Kamal::Commands::Base
def superuser?
[ '[ "${EUID:-$(id -u)}" -eq 0 ] || command -v sudo >/dev/null || command -v su >/dev/null' ]
end
private
def get_docker
shell \
any \
[ :curl, "-fsSL", "https://get.docker.com" ],
[ :wget, "-O -", "https://get.docker.com" ],
[ :echo, "\"exit 1\"" ]
end
end

View File

@@ -13,10 +13,10 @@ class Kamal::Commands::Prune < Kamal::Commands::Base
"while read image tag; do docker rmi $tag; done"
end
def app_containers(retain:)
def app_containers(keep_last: 5)
pipe \
docker(:ps, "-q", "-a", *service_filter, *stopped_containers_filters),
"tail -n +#{retain + 1}",
"tail -n +#{keep_last + 1}",
"while read container_id; do docker rm $container_id; done"
end

View File

@@ -2,10 +2,7 @@ class Kamal::Commands::Registry < Kamal::Commands::Base
delegate :registry, to: :config
def login
docker :login,
registry["server"],
"-u", sensitive(Kamal::Utils.escape_shell_value(lookup("username"))),
"-p", sensitive(Kamal::Utils.escape_shell_value(lookup("password")))
docker :login, registry["server"], "-u", sensitive(lookup("username")), "-p", sensitive(lookup("password"))
end
def logout

View File

@@ -1,7 +1,7 @@
class Kamal::Commands::Traefik < Kamal::Commands::Base
delegate :argumentize, :optionize, to: Kamal::Utils
DEFAULT_IMAGE = "traefik:v2.10"
DEFAULT_IMAGE = "traefik:v2.9"
CONTAINER_PORT = 80
DEFAULT_ARGS = {
'log.level' => 'DEBUG'
@@ -39,7 +39,7 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base
end
def start_or_run
any start, run
combine start, run, by: "||"
end
def info

View File

@@ -6,7 +6,7 @@ require "erb"
require "net/ssh/proxy/jump"
class Kamal::Configuration
delegate :service, :image, :servers, :env, :labels, :registry, :stop_wait_time, :hooks_path, :logging, to: :raw_config, allow_nil: true
delegate :service, :image, :servers, :env, :labels, :registry, :stop_wait_time, :hooks_path, to: :raw_config, allow_nil: true
delegate :argumentize, :optionize, to: Kamal::Utils
attr_reader :destination, :raw_config
@@ -92,19 +92,7 @@ class Kamal::Configuration
end
def primary_host
primary_role&.primary_host
end
def primary_role_name
raw_config.primary_role || "web"
end
def primary_role
role(primary_role_name)
end
def allow_empty_roles?
raw_config.allow_empty_roles
role(primary_role)&.primary_host
end
def traefik_roles
@@ -139,10 +127,6 @@ class Kamal::Configuration
raw_config.require_destination
end
def retain_containers
raw_config.retain_containers || 5
end
def volume_args
if raw_config.volumes.present?
@@ -153,9 +137,9 @@ class Kamal::Configuration
end
def logging_args
if logging.present?
optionize({ "log-driver" => logging["driver"] }.compact) +
argumentize("--log-opt", logging["options"])
if raw_config.logging.present?
optionize({ "log-driver" => raw_config.logging["driver"] }.compact) +
argumentize("--log-opt", raw_config.logging["options"])
else
argumentize("--log-opt", { "max-size" => "10m" })
end
@@ -224,9 +208,17 @@ class Kamal::Configuration
raw_config.asset_path
end
def primary_role
raw_config.primary_role || "web"
end
def allow_empty_roles?
raw_config.allow_empty_roles
end
def valid?
ensure_destination_if_required && ensure_required_keys_present && ensure_valid_kamal_version && ensure_retain_containers_valid && ensure_valid_service_name
ensure_destination_if_required && ensure_required_keys_present && ensure_valid_kamal_version
end
def to_h
@@ -272,12 +264,12 @@ class Kamal::Configuration
raise ArgumentError, "You must specify a password for the registry in config/deploy.yml (or set the ENV variable if that's used)"
end
unless role_names.include?(primary_role_name)
raise ArgumentError, "The primary_role #{primary_role_name} isn't defined"
unless role_names.include?(primary_role)
raise ArgumentError, "The primary_role #{primary_role} isn't defined"
end
if primary_role.hosts.empty?
raise ArgumentError, "No servers specified for the #{primary_role.name} primary_role"
if role(primary_role).hosts.empty?
raise ArgumentError, "No servers specified for the #{primary_role} primary_role"
end
unless allow_empty_roles?
@@ -291,12 +283,6 @@ class Kamal::Configuration
true
end
def ensure_valid_service_name
raise ArgumentError, "Service name can only include alphanumeric characters, hyphens, and underscores" unless raw_config[:service] =~ /^[a-z0-9_-]+$/
true
end
def ensure_valid_kamal_version
if minimum_version && Gem::Version.new(minimum_version) > Gem::Version.new(Kamal::VERSION)
raise ArgumentError, "Current version is #{Kamal::VERSION}, minimum required is #{minimum_version}"
@@ -305,12 +291,6 @@ class Kamal::Configuration
true
end
def ensure_retain_containers_valid
raise ArgumentError, "Must retain at least 1 container" if retain_containers < 1
true
end
def role_names
raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort

View File

@@ -8,7 +8,7 @@ class Kamal::Configuration::Accessory
end
def service_name
specifics["service"] || "#{config.service}-#{name}"
"#{config.service}-#{name}"
end
def image

View File

@@ -8,7 +8,7 @@ class Kamal::Configuration::Boot
limit = @options["limit"]
if limit.to_s.end_with?("%")
[@host_count * limit.to_i / 100, 1].max
@host_count * limit.to_i / 100
else
limit
end

View File

@@ -81,10 +81,6 @@ class Kamal::Configuration::Builder
end
end
def ssh
@options["ssh"]
end
private
def valid?
if @options["cache"] && @options["cache"]["type"]

View File

@@ -3,7 +3,6 @@ class Kamal::Configuration::Role
delegate :argumentize, :optionize, to: Kamal::Utils
attr_accessor :name
alias to_s name
def initialize(name, config:)
@name, @config = name.inquiry, config
@@ -37,18 +36,6 @@ class Kamal::Configuration::Role
argumentize "--label", labels
end
def logging_args
args = config.logging || {}
args.deep_merge!(specializations["logging"]) if specializations["logging"].present?
if args.any?
optionize({ "log-driver" => args["driver"] }.compact) +
argumentize("--log-opt", args["options"])
else
config.logging_args
end
end
def env
if config.env && config.env["secret"]
@@ -114,7 +101,7 @@ class Kamal::Configuration::Role
end
def primary?
self == @config.primary_role
@config.primary_role == name
end

View File

@@ -1,6 +1,5 @@
require "sshkit"
require "sshkit/dsl"
require "net/scp"
require "active_support/core_ext/hash/deep_merge"
require "json"

View File

@@ -1,3 +1,3 @@
module Kamal
VERSION = "1.4.0"
VERSION = "1.3.1"
end

View File

@@ -148,30 +148,6 @@ class CliAccessoryTest < CliTestCase
assert_match "rm -rf app-mysql", run_command("remove_service_directory", "mysql")
end
test "hosts param respected" do
Kamal::Cli::Accessory.any_instance.expects(:directories).with("redis")
Kamal::Cli::Accessory.any_instance.expects(:upload).with("redis")
run_command("boot", "redis", "--hosts", "1.1.1.1").tap do |output|
assert_match /docker login.*on 1.1.1.1/, output
refute_match /docker login.*on 1.1.1.2/, output
assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output
refute_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", output
end
end
test "hosts param intersected with configuration" do
Kamal::Cli::Accessory.any_instance.expects(:directories).with("redis")
Kamal::Cli::Accessory.any_instance.expects(:upload).with("redis")
run_command("boot", "redis", "--hosts", "1.1.1.1,1.1.1.3").tap do |output|
assert_match /docker login.*on 1.1.1.1/, output
refute_match /docker login.*on 1.1.1.3/, output
assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output
refute_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.3", output
end
end
private
def run_command(*command)
stdouted { Kamal::Cli::Accessory.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) }

View File

@@ -57,7 +57,7 @@ class CliBuildTest < CliTestCase
run_command("pull").tap do |output|
assert_match /docker image rm --force dhh\/app:999/, output
assert_match /docker pull dhh\/app:999/, output
assert_match "docker inspect -f '{{ .Config.Labels.service }}' dhh/app:999 | grep -x app || (echo \"Image dhh/app:999 is missing the 'service' label\" && exit 1)", output
assert_match "docker inspect -f '{{ .Config.Labels.service }}' dhh/app:999 | grep -x app || (echo \"Image dhh/app:999 is missing the `service` label\" && exit 1)", output
end
end

View File

@@ -2,47 +2,12 @@ require_relative "cli_test_case"
class CliMainTest < CliTestCase
test "setup" do
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:server:bootstrap", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:env:push", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:boot", [ "all" ], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:server:bootstrap")
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:env:push")
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:boot", [ "all" ])
Kamal::Cli::Main.any_instance.expects(:deploy)
run_command("setup").tap do |output|
assert_match /Ensure Docker is installed.../, output
assert_match /Push env files.../, output
end
end
test "setup with skip_push" do
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:server:bootstrap", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:env:push", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:boot", [ "all" ], invoke_options)
# deploy
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:pull", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:boot", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:healthcheck:perform", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true))
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options)
run_command("setup", "--skip_push").tap do |output|
assert_match /Ensure Docker is installed.../, output
assert_match /Push env files.../, output
# deploy
assert_match /Acquiring the deploy lock/, output
assert_match /Log into image registry/, output
assert_match /Pull app image/, output
assert_match /Ensure Traefik is running/, output
assert_match /Ensure app can pass healthcheck/, output
assert_match /Detect stale containers/, output
assert_match /Prune old containers and images/, output
assert_match /Releasing the deploy lock/, output
end
run_command("setup")
end
test "deploy" do

View File

@@ -20,15 +20,6 @@ class CliPruneTest < CliTestCase
assert_match /docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +6 | while read container_id; do docker rm $container_id; done on 1.1.1.\d/, output
assert_match /docker container prune --force --filter label=service=healthcheck-app on 1.1.1.\d/, output
end
run_command("containers", "--retain", "10").tap do |output|
assert_match /docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +11 | while read container_id; do docker rm $container_id; done on 1.1.1.\d/, output
assert_match /docker container prune --force --filter label=service=healthcheck-app on 1.1.1.\d/, output
end
assert_raises(RuntimeError, "retain must be at least 1") do
run_command("containers", "--retain", "0")
end
end
private

View File

@@ -21,15 +21,12 @@ class CliServerTest < CliTestCase
test "bootstrap install as root user" do
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(false).at_least_once
SSHKit::Backend::Abstract.any_instance.expects(:execute).with('[ "${EUID:-$(id -u)}" -eq 0 ] || command -v sudo >/dev/null || command -v su >/dev/null', raise_on_non_zero_exit: false).returns(true).at_least_once
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:sh, "-c", "'curl -fsSL https://get.docker.com || wget -O - https://get.docker.com || echo \"exit 1\"'", "|", :sh).at_least_once
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:curl, "-fsSL", "https://get.docker.com", "|", :sh).at_least_once
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:mkdir, "-p", ".kamal").returns("").at_least_once
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(".kamal/hooks/docker-setup", anything).at_least_once
run_command("bootstrap").tap do |output|
("1.1.1.1".."1.1.1.4").map do |host|
assert_match "Missing Docker on #{host}. Installing…", output
assert_match "Running the docker-setup hook", output
end
end
end

View File

@@ -83,14 +83,14 @@ class CommanderTest < ActiveSupport::TestCase
end
test "primary_role" do
assert_equal "web", @kamal.primary_role.name
assert_equal "web", @kamal.primary_role
@kamal.specific_roles = "workers"
assert_equal "workers", @kamal.primary_role.name
assert_equal "workers", @kamal.primary_role
end
test "roles_on" do
assert_equal [ "web" ], @kamal.roles_on("1.1.1.1").map(&:name)
assert_equal [ "workers" ], @kamal.roles_on("1.1.1.3").map(&:name)
assert_equal [ "web" ], @kamal.roles_on("1.1.1.1")
assert_equal [ "workers" ], @kamal.roles_on("1.1.1.3")
end
test "default group strategy" do
@@ -109,18 +109,12 @@ class CommanderTest < ActiveSupport::TestCase
assert_equal({ in: :groups, limit: 1, wait: 2 }, @kamal.boot_strategy)
end
test "percentage-based group strategy limit is at least 1" do
configure_with(:deploy_with_low_percentage_boot_strategy)
assert_equal({ in: :groups, limit: 1, wait: 2 }, @kamal.boot_strategy)
end
test "try to match the primary role from a list of specific roles" do
configure_with(:deploy_primary_web_role_override)
@kamal.specific_roles = [ "web_*" ]
assert_equal [ "web_chicago", "web_tokyo" ], @kamal.roles.map(&:name)
assert_equal "web_tokyo", @kamal.primary_role.name
assert_equal "web_tokyo", @kamal.primary_role
assert_equal "1.1.1.3", @kamal.primary_host
end

View File

@@ -34,7 +34,6 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
]
},
"busybox" => {
"service" => "custom-busybox",
"image" => "busybox:latest",
"host" => "1.1.1.7"
}
@@ -58,7 +57,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
new_command(:redis).run.join(" ")
assert_equal \
"docker run --name custom-busybox --detach --restart unless-stopped --log-opt max-size=\"10m\" --env-file .kamal/env/accessories/custom-busybox.env --label service=\"custom-busybox\" busybox:latest",
"docker run --name app-busybox --detach --restart unless-stopped --log-opt max-size=\"10m\" --env-file .kamal/env/accessories/app-busybox.env --label service=\"app-busybox\" busybox:latest",
new_command(:busybox).run.join(" ")
end
@@ -66,7 +65,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
assert_equal \
"docker run --name custom-busybox --detach --restart unless-stopped --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --env-file .kamal/env/accessories/custom-busybox.env --label service=\"custom-busybox\" busybox:latest",
"docker run --name app-busybox --detach --restart unless-stopped --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --env-file .kamal/env/accessories/app-busybox.env --label service=\"app-busybox\" busybox:latest",
new_command(:busybox).run.join(" ")
end

View File

@@ -71,15 +71,6 @@ class CommandsAppTest < ActiveSupport::TestCase
new_command.run.join(" ")
end
test "run with role logging config" do
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "10m", "max-file" => "3" } }
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "logging" => { "driver" => "local", "options" => { "max-size" => "100m" } } } }
assert_equal \
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/up || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
new_command.run.join(" ")
end
test "start" do
assert_equal \
"docker start app-web-999",
@@ -154,20 +145,12 @@ class CommandsAppTest < ActiveSupport::TestCase
test "follow logs" do
assert_match \
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --timestamps --follow 2>&1",
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --timestamps --tail 10 --follow 2>&1",
new_command.follow_logs(host: "app-1")
assert_match \
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --timestamps --follow 2>&1 | grep \"Completed\"",
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --timestamps --tail 10 --follow 2>&1 | grep \"Completed\"",
new_command.follow_logs(host: "app-1", grep: "Completed")
assert_match \
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --timestamps --tail 123 --follow 2>&1",
new_command.follow_logs(host: "app-1", lines: 123)
assert_match \
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --timestamps --tail 123 --follow 2>&1 | grep \"Completed\"",
new_command.follow_logs(host: "app-1", lines: 123, grep: "Completed")
end
@@ -400,7 +383,6 @@ class CommandsAppTest < ActiveSupport::TestCase
private
def new_command(role: "web", **additional_config)
config = Kamal::Configuration.new(@config.merge(additional_config), destination: @destination, version: "999")
Kamal::Commands::App.new(config, role: config.role(role))
Kamal::Commands::App.new(Kamal::Configuration.new(@config.merge(additional_config), destination: @destination, version: "999"), role: role)
end
end

View File

@@ -111,16 +111,8 @@ class CommandsBuilderTest < ActiveSupport::TestCase
builder.push.join(" ")
end
test "build with ssh agent socket" do
builder = new_builder_command(builder: { "ssh" => 'default=$SSH_AUTH_SOCK' })
assert_equal \
"-t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile --ssh default=$SSH_AUTH_SOCK",
builder.target.build_options.join(" ")
end
test "validate image" do
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(" ")
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
private

View File

@@ -9,7 +9,7 @@ class CommandsDockerTest < ActiveSupport::TestCase
end
test "install" do
assert_equal "sh -c 'curl -fsSL https://get.docker.com || wget -O - https://get.docker.com || echo \"exit 1\"' | sh", @docker.install.join(" ")
assert_equal "curl -fsSL https://get.docker.com | sh", @docker.install.join(" ")
end
test "installed?" do

View File

@@ -23,11 +23,7 @@ class CommandsPruneTest < ActiveSupport::TestCase
test "app containers" do
assert_equal \
"docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +6 | while read container_id; do docker rm $container_id; done",
new_command.app_containers(retain: 5).join(" ")
assert_equal \
"docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +4 | while read container_id; do docker rm $container_id; done",
new_command.app_containers(retain: 3).join(" ")
new_command.app_containers.join(" ")
end
test "healthcheck containers" do

View File

@@ -15,7 +15,7 @@ class CommandsRegistryTest < ActiveSupport::TestCase
test "registry login" do
assert_equal \
"docker login hub.docker.com -u \"dhh\" -p \"secret\"",
"docker login hub.docker.com -u dhh -p secret",
@registry.login.join(" ")
end
@@ -24,18 +24,7 @@ class CommandsRegistryTest < ActiveSupport::TestCase
@config[:registry]["password"] = [ "KAMAL_REGISTRY_PASSWORD" ]
assert_equal \
"docker login hub.docker.com -u \"dhh\" -p \"more-secret\"",
@registry.login.join(" ")
ensure
ENV.delete("KAMAL_REGISTRY_PASSWORD")
end
test "registry login escape password" do
ENV["KAMAL_REGISTRY_PASSWORD"] = "more-secret'\""
@config[:registry]["password"] = [ "KAMAL_REGISTRY_PASSWORD" ]
assert_equal \
"docker login hub.docker.com -u \"dhh\" -p \"more-secret'\\\"\"",
"docker login hub.docker.com -u dhh -p more-secret",
@registry.login.join(" ")
ensure
ENV.delete("KAMAL_REGISTRY_PASSWORD")
@@ -46,7 +35,7 @@ class CommandsRegistryTest < ActiveSupport::TestCase
@config[:registry]["username"] = [ "KAMAL_REGISTRY_USERNAME" ]
assert_equal \
"docker login hub.docker.com -u \"also-secret\" -p \"secret\"",
"docker login hub.docker.com -u also-secret -p secret",
@registry.login.join(" ")
ensure
ENV.delete("KAMAL_REGISTRY_USERNAME")

View File

@@ -49,7 +49,6 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
}
},
"monitoring" => {
"service" => "custom-monitoring",
"image" => "monitoring:latest",
"roles" => [ "web" ],
"port" => "4321:4321",
@@ -73,7 +72,6 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
test "service name" do
assert_equal "app-mysql", @config.accessory(:mysql).service_name
assert_equal "app-redis", @config.accessory(:redis).service_name
assert_equal "custom-monitoring", @config.accessory(:monitoring).service_name
end
test "port" do

View File

@@ -148,14 +148,4 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
assert_equal "..", @config_with_builder_option.builder.context
end
test "ssh" do
assert_nil @config.builder.ssh
end
test "setting ssh params" do
@deploy_with_builder_option[:builder] = { "ssh" => 'default=$SSH_AUTH_SOCK' }
assert_equal 'default=$SSH_AUTH_SOCK', @config_with_builder_option.builder.ssh
end
end

View File

@@ -42,16 +42,6 @@ class ConfigurationTest < ActiveSupport::TestCase
end
end
test "service name valid" do
assert Kamal::Configuration.new(@deploy.tap { _1[:service] = "hey-app1_primary" }).valid?
end
test "service name invalid" do
assert_raise(ArgumentError) do
Kamal::Configuration.new @deploy.tap { _1[:service] = "app.com" }
end
end
test "roles" do
assert_equal %w[ web ], @config.roles.collect(&:name)
assert_equal %w[ web workers ], @config_with_roles.roles.collect(&:name)
@@ -300,14 +290,14 @@ class ConfigurationTest < ActiveSupport::TestCase
end
test "primary role" do
assert_equal "web", @config.primary_role.name
assert_equal "web", @config.primary_role
config = Kamal::Configuration.new(@deploy_with_roles.deep_merge({
servers: { "alternate_web" => { "hosts" => [ "1.1.1.4", "1.1.1.5" ] } },
primary_role: "alternate_web" } ))
assert_equal "alternate_web", config.primary_role.name
assert_equal "alternate_web", config.primary_role
assert_equal "1.1.1.4", config.primary_host
assert config.role(:alternate_web).primary?
assert config.role(:alternate_web).running_traefik?
@@ -319,12 +309,4 @@ class ConfigurationTest < ActiveSupport::TestCase
end
assert_match /bar isn't defined/, error.message
end
test "retain_containers" do
assert_equal 5, @config.retain_containers
config = Kamal::Configuration.new(@deploy_with_roles.merge(retain_containers: 2))
assert_equal 2, config.retain_containers
assert_raises(ArgumentError) { Kamal::Configuration.new(@deploy_with_roles.merge(retain_containers: 0)) }
end
end

View File

@@ -1,17 +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
boot:
limit: 1%
wait: 2

View File

@@ -13,5 +13,5 @@ registry:
password: pw
boot:
limit: 1%
limit: 25%
wait: 2

View File

@@ -1,3 +0,0 @@
#!/bin/sh
echo "Docker set up!"
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/docker-setup

View File

@@ -24,10 +24,9 @@ traefik:
args:
accesslog: true
accesslog.format: json
image: registry:4443/traefik:v2.10
image: registry:4443/traefik:v2.9
accessories:
busybox:
service: custom-busybox
image: registry:4443/busybox:1.36.0
cmd: sh -c 'echo "Starting busybox..."; trap exit term; while true; do sleep 1; done'
roles:

View File

@@ -19,7 +19,7 @@ push_image_to_registry_4443() {
install_kamal
push_image_to_registry_4443 nginx 1-alpine-slim
push_image_to_registry_4443 traefik v2.10
push_image_to_registry_4443 traefik v2.9
push_image_to_registry_4443 busybox 1.36.0
# .ssh is on a shared volume that persists between runs. Clean it up as the

View File

@@ -10,7 +10,7 @@ class LockTest < IntegrationTest
assert_match /Locked by: Deployer at .*\nVersion: #{latest_app_version}\nMessage: Integration Tests/m, status
error = kamal :deploy, capture: true, raise_on_error: false
assert_match /Deploy lock found. Run 'kamal lock help' for more information/m, error
assert_match /Deploy lock found/m, error
kamal :lock, :release

View File

@@ -32,7 +32,7 @@ class MainTest < IntegrationTest
assert_match /Traefik Host: vm2/, details
assert_match /App Host: vm1/, details
assert_match /App Host: vm2/, details
assert_match /traefik:v2.10/, details
assert_match /traefik:v2.9/, details
assert_match /registry:4443\/app:#{first_version}/, details
audit = kamal :audit, capture: true
@@ -60,19 +60,6 @@ class MainTest < IntegrationTest
assert_equal({ "path" => "/up", "port" => 3000, "max_attempts" => 7, "exposed_port" => 3999, "cord"=>"/tmp/kamal-cord", "log_lines" => 50, "cmd"=>"wget -qO- http://localhost > /dev/null || exit 1" }, config[:healthcheck])
end
test "setup and remove" do
# Check remove completes when nothing has been setup yet
kamal :remove, "-y"
assert_no_images_or_containers
kamal :envify
kamal :setup
assert_images_and_containers
kamal :remove, "-y"
assert_no_images_or_containers
end
private
def assert_local_env_file(contents)
assert_equal contents, deployer_exec("cat .env", capture: true)
@@ -97,22 +84,4 @@ class MainTest < IntegrationTest
assert_equal "200", Net::HTTP.get_response(URI.parse("http://localhost:12345/versions/.hidden")).code
end
def vm1_image_ids
docker_compose("exec vm1 docker image ls -q", capture: true).strip.split("\n")
end
def vm1_container_ids
docker_compose("exec vm1 docker ps -a -q", capture: true).strip.split("\n")
end
def assert_no_images_or_containers
assert vm1_image_ids.empty?
assert vm1_container_ids.empty?
end
def assert_images_and_containers
assert vm1_image_ids.any?
assert vm1_container_ids.any?
end
end

View File

@@ -52,11 +52,11 @@ class TraefikTest < IntegrationTest
private
def assert_traefik_running
assert_match /traefik:v2.10 "\/entrypoint.sh/, traefik_details
assert_match /traefik:v2.9 "\/entrypoint.sh/, traefik_details
end
def assert_traefik_not_running
refute_match /traefik:v2.10 "\/entrypoint.sh/, traefik_details
refute_match /traefik:v2.9 "\/entrypoint.sh/, traefik_details
end
def traefik_details