Compare commits
61 Commits
concurrent
...
v1.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c985fa33d1 | ||
|
|
e8b9f8907f | ||
|
|
4966d52919 | ||
|
|
52bb40add0 | ||
|
|
73a9276cdd | ||
|
|
8c0784ed4a | ||
|
|
089a2d3bba | ||
|
|
bd76d23916 | ||
|
|
fa37fcd10c | ||
|
|
f5dc0858b0 | ||
|
|
9dddb140b1 | ||
|
|
26b1d57c90 | ||
|
|
b94199415f | ||
|
|
f69c45b7ea | ||
|
|
32a2ae5b2c | ||
|
|
37544a6383 | ||
|
|
a1bc6d61af | ||
|
|
5c32be10f1 | ||
|
|
dc5af03593 | ||
|
|
1abd029ea0 | ||
|
|
c4d0d3e5eb | ||
|
|
46e7cf8e78 | ||
|
|
c7cfc074b6 | ||
|
|
c10f43e365 | ||
|
|
8e2184d65e | ||
|
|
2be397b679 | ||
|
|
cc8c508556 | ||
|
|
3b16e047c5 | ||
|
|
6563393d9a | ||
|
|
91f350fcce | ||
|
|
e4e9664049 | ||
|
|
1acef5221f | ||
|
|
788a57e85e | ||
|
|
f9a934a01f | ||
|
|
f286fdc374 | ||
|
|
828cca322b | ||
|
|
cb030e8751 | ||
|
|
6892abb4be | ||
|
|
bcfd0ca88a | ||
|
|
2e8071a5b3 | ||
|
|
200e2686fd | ||
|
|
db94789dc1 | ||
|
|
2bffc3bc74 | ||
|
|
064ace0598 | ||
|
|
a02af74dda | ||
|
|
5ef384d666 | ||
|
|
b94dfe193b | ||
|
|
bc6c027315 | ||
|
|
1c2a45817a | ||
|
|
b411356409 | ||
|
|
77e72e34ce | ||
|
|
ad04bb7556 | ||
|
|
1ec69d3764 | ||
|
|
2d1a0dc9ba | ||
|
|
c984db152f | ||
|
|
0d709a3fdb | ||
|
|
414d29ae4e | ||
|
|
f8d8319c2f | ||
|
|
f6a9d54902 | ||
|
|
b2fd5744fb | ||
|
|
cbd99306eb |
@@ -1,7 +1,7 @@
|
||||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
kamal (1.3.1)
|
||||
kamal (1.4.0)
|
||||
activesupport (>= 7.0)
|
||||
base64 (~> 0.2)
|
||||
bcrypt_pbkdf (~> 1.0)
|
||||
|
||||
@@ -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|
|
||||
with_accessory(name) do |accessory, hosts|
|
||||
directories(name)
|
||||
upload(name)
|
||||
|
||||
on(accessory.hosts) do
|
||||
on(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|
|
||||
on(accessory.hosts) do
|
||||
with_accessory(name) do |accessory, hosts|
|
||||
on(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|
|
||||
on(accessory.hosts) do
|
||||
with_accessory(name) do |accessory, hosts|
|
||||
on(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|
|
||||
on(accessory.hosts) do
|
||||
with_accessory(name) do |accessory, hosts|
|
||||
on(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|
|
||||
on(accessory.hosts) do
|
||||
with_accessory(name) do |accessory, hosts|
|
||||
on(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|
|
||||
on(accessory.hosts) do
|
||||
with_accessory(name) do |accessory, hosts|
|
||||
on(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|
|
||||
on(accessory.hosts) { puts capture_with_info(*accessory.info) }
|
||||
with_accessory(name) do |accessory, hosts|
|
||||
on(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|
|
||||
with_accessory(name) do |accessory, hosts|
|
||||
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(accessory.hosts) do
|
||||
on(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(accessory.hosts) do
|
||||
on(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|
|
||||
with_accessory(name) do |accessory, hosts|
|
||||
grep = options[:grep]
|
||||
|
||||
if options[:follow]
|
||||
run_locally do
|
||||
info "Following logs on #{accessory.hosts}..."
|
||||
info "Following logs on #{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(accessory.hosts) do
|
||||
on(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|
|
||||
on(accessory.hosts) do
|
||||
with_accessory(name) do |accessory, hosts|
|
||||
on(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|
|
||||
on(accessory.hosts) do
|
||||
with_accessory(name) do |accessory, hosts|
|
||||
on(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|
|
||||
on(accessory.hosts) do
|
||||
with_accessory(name) do |accessory, hosts|
|
||||
on(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
|
||||
yield accessory, accessory_hosts(accessory)
|
||||
else
|
||||
error_on_missing_accessory(name)
|
||||
end
|
||||
@@ -240,4 +240,12 @@ 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
|
||||
|
||||
@@ -13,9 +13,8 @@ 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_config.assets?
|
||||
if role.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)
|
||||
@@ -27,7 +26,6 @@ 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)}"
|
||||
@@ -38,7 +36,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_config.cord_host_file) if role_config.uses_cord?
|
||||
execute *app.tie_cord(role.cord_host_file) if role.uses_cord?
|
||||
|
||||
execute *auditor.record("Booted app version #{version}"), verbosity: :debug
|
||||
|
||||
@@ -47,7 +45,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_config.uses_cord?
|
||||
if role.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)
|
||||
@@ -57,7 +55,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_config.assets?
|
||||
execute *app.clean_up_assets if role.assets?
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -202,19 +200,20 @@ 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
|
||||
|
||||
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, grep: grep)
|
||||
exec KAMAL.app(role: role).follow_logs(host: KAMAL.primary_host, grep: grep)
|
||||
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)
|
||||
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|
|
||||
|
||||
@@ -123,8 +123,9 @@ 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"
|
||||
raise LockError, "Deploy lock found. Run 'kamal lock help' for more information"
|
||||
else
|
||||
raise e
|
||||
end
|
||||
|
||||
@@ -8,9 +8,8 @@ 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_config.env_file), role_config.host_env_file_path, mode: 400
|
||||
upload! StringIO.new(role.env_file), role.host_env_file_path, mode: 400
|
||||
end
|
||||
end
|
||||
|
||||
@@ -36,7 +35,6 @@ 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
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
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 "kamal:cli:server:bootstrap", [], invoke_options
|
||||
|
||||
say "Push env files...", :magenta
|
||||
invoke "kamal:cli:env:push"
|
||||
invoke "kamal:cli:env:push", [], invoke_options
|
||||
|
||||
invoke "kamal:cli:accessory:boot", [ "all" ]
|
||||
invoke "kamal:cli:accessory:boot", [ "all" ], invoke_options
|
||||
deploy
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,12 +18,16 @@ class Kamal::Cli::Prune < Kamal::Cli::Base
|
||||
end
|
||||
end
|
||||
|
||||
desc "containers", "Prune all stopped containers, except the last 5"
|
||||
desc "containers", "Prune all stopped containers, except the last n (default 5)"
|
||||
option :retain, type: :numeric, default: nil, desc: "Number of containers to retain"
|
||||
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
|
||||
execute *KAMAL.prune.app_containers(retain: retain)
|
||||
execute *KAMAL.prune.healthcheck_containers
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
class Kamal::Cli::Registry < Kamal::Cli::Base
|
||||
desc "login", "Log in to registry locally and remotely"
|
||||
def login
|
||||
on([ :local ] + KAMAL.hosts) { execute *KAMAL.registry.login }
|
||||
run_locally { execute *KAMAL.registry.login }
|
||||
on(KAMAL.hosts) { execute *KAMAL.registry.login }
|
||||
# FIXME: This rescue needed?
|
||||
rescue ArgumentError => e
|
||||
puts e.message
|
||||
|
||||
@@ -17,7 +17,9 @@ 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 the `curl` command available. Install Docker manually: https://docs.docker.com/engine/install/"
|
||||
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"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -77,6 +77,10 @@ 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.
|
||||
|
||||
7
lib/kamal/cli/templates/sample_hooks/docker-setup.sample
Normal file
7
lib/kamal/cli/templates/sample_hooks/docker-setup.sample
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
# A sample docker-setup hook
|
||||
#
|
||||
# Sets up a Docker network which can then be used by the application’s containers
|
||||
|
||||
ssh user@example.com docker network create kamal
|
||||
@@ -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
|
||||
execute *KAMAL.traefik.stop, raise_on_non_zero_exit: false
|
||||
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
|
||||
execute *KAMAL.traefik.stop, raise_on_non_zero_exit: false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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.name == config.primary_role }&.primary_host || specific_roles&.first&.primary_host || config.primary_host
|
||||
specific_hosts&.first || specific_roles&.detect { |role| role == 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) }.map(&:name)
|
||||
roles.select { |role| role.hosts.include?(host.to_s) }
|
||||
end
|
||||
|
||||
def traefik_hosts
|
||||
|
||||
@@ -3,12 +3,11 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
||||
|
||||
ACTIVE_DOCKER_STATUSES = [ :running, :restarting ]
|
||||
|
||||
attr_reader :role, :role_config
|
||||
attr_reader :role, :role
|
||||
|
||||
def initialize(config, role: nil)
|
||||
super(config)
|
||||
@role = role
|
||||
@role_config = config.role(self.role)
|
||||
end
|
||||
|
||||
def run(hostname: nil)
|
||||
@@ -19,15 +18,15 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
||||
*(["--hostname", hostname] if hostname),
|
||||
"-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
|
||||
"-e", "KAMAL_VERSION=\"#{config.version}\"",
|
||||
*role_config.env_args,
|
||||
*role_config.health_check_args,
|
||||
*config.logging_args,
|
||||
*role.env_args,
|
||||
*role.health_check_args,
|
||||
*role.logging_args,
|
||||
*config.volume_args,
|
||||
*role_config.asset_volume_args,
|
||||
*role_config.label_args,
|
||||
*role_config.option_args,
|
||||
*role.asset_volume_args,
|
||||
*role.label_args,
|
||||
*role.option_args,
|
||||
config.absolute_image,
|
||||
role_config.cmd
|
||||
role.cmd
|
||||
end
|
||||
|
||||
def start
|
||||
@@ -64,22 +63,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_config.container_prefix}-}; done) # Extract SHA from "service-role-dest-SHA"
|
||||
%(while read line; do echo ${line##{role.container_prefix}-}; done) # Extract SHA from "service-role-dest-SHA"
|
||||
end
|
||||
|
||||
|
||||
def make_env_directory
|
||||
make_directory role_config.host_env_directory
|
||||
make_directory role.host_env_directory
|
||||
end
|
||||
|
||||
def remove_env_file
|
||||
[ :rm, "-f", role_config.host_env_file_path ]
|
||||
[ :rm, "-f", role.host_env_file_path ]
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
def container_name(version = nil)
|
||||
[ role_config.container_prefix, version || config.version ].compact.join("-")
|
||||
[ role.container_prefix, version || config.version ].compact.join("-")
|
||||
end
|
||||
|
||||
def filter_args(statuses: nil)
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
module Kamal::Commands::App::Assets
|
||||
def extract_assets
|
||||
asset_container = "#{role_config.container_prefix}-assets"
|
||||
asset_container = "#{role.container_prefix}-assets"
|
||||
|
||||
combine \
|
||||
make_directory(role_config.asset_extracted_path),
|
||||
make_directory(role.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_config.asset_path}/.", role_config.asset_extracted_path),
|
||||
docker(:cp, "-L", "#{asset_container}:#{role.asset_path}/.", role.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_config.asset_extracted_path(config.version), role_config.asset_volume.host_path
|
||||
new_extracted_path, new_volume_path = role.asset_extracted_path(config.version), role.asset_volume.host_path
|
||||
if old_version.present?
|
||||
old_extracted_path, old_volume_path = role_config.asset_extracted_path(old_version), role_config.asset_volume(old_version).host_path
|
||||
old_extracted_path, old_volume_path = role.asset_extracted_path(old_version), role.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_config.asset_extracted_path),
|
||||
find_and_remove_older_siblings(role_config.asset_volume_path)
|
||||
find_and_remove_older_siblings(role.asset_extracted_path),
|
||||
find_and_remove_older_siblings(role.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_config.container_prefix}-*'",
|
||||
"-name", "'#{role.container_prefix}-*'",
|
||||
"!", "-name", Pathname.new(path).basename.to_s,
|
||||
"-exec rm -rf \"{}\" +"
|
||||
]
|
||||
|
||||
@@ -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_config.cord_volume.container_path}\" {print $1}'"]
|
||||
[:awk, "'$2 == \"#{role.cord_volume.container_path}\" {print $1}'"]
|
||||
end
|
||||
|
||||
def tie_cord(cord)
|
||||
@@ -12,8 +12,8 @@ module Kamal::Commands::App::Cord
|
||||
def cut_cord(cord)
|
||||
remove_directory(cord)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
private
|
||||
def create_empty_file(file)
|
||||
chain \
|
||||
make_directory_for(file),
|
||||
|
||||
@@ -10,9 +10,9 @@ module Kamal::Commands::App::Execution
|
||||
docker :run,
|
||||
("-it" if interactive),
|
||||
"--rm",
|
||||
*role_config&.env_args,
|
||||
*role&.env_args,
|
||||
*config.volume_args,
|
||||
*role_config&.option_args,
|
||||
*role&.option_args,
|
||||
config.absolute_image,
|
||||
*command
|
||||
end
|
||||
|
||||
@@ -6,11 +6,11 @@ module Kamal::Commands::App::Logging
|
||||
("grep '#{grep}'" if grep)
|
||||
end
|
||||
|
||||
def follow_logs(host:, grep: nil)
|
||||
def follow_logs(host:, lines: nil, grep: nil)
|
||||
run_over_ssh \
|
||||
pipe(
|
||||
current_running_container_id,
|
||||
"xargs docker logs --timestamps --tail 10 --follow 2>&1",
|
||||
"xargs docker logs --timestamps#{" --tail #{lines}" if lines} --follow 2>&1",
|
||||
(%(grep "#{grep}") if grep)
|
||||
),
|
||||
host: host
|
||||
|
||||
@@ -62,10 +62,18 @@ 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
|
||||
|
||||
@@ -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, to: :builder_config
|
||||
delegate :args, :secrets, :dockerfile, :local_arch, :local_host, :remote_arch, :remote_host, :cache_from, :cache_to, :ssh, to: :builder_config
|
||||
|
||||
def clean
|
||||
docker :image, :rm, "--force", config.absolute_image
|
||||
@@ -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_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_ssh ]
|
||||
end
|
||||
|
||||
def build_context
|
||||
@@ -24,7 +24,10 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
||||
def validate_image
|
||||
pipe \
|
||||
docker(:inspect, "-f", "'{{ .Config.Labels.service }}'", config.absolute_image),
|
||||
[:grep, "-x", config.service, "||", "(echo \"Image #{config.absolute_image} is missing the `service` label\" && exit 1)"]
|
||||
any(
|
||||
[:grep, "-x", config.service],
|
||||
"(echo \"Image #{config.absolute_image} is missing the 'service' label\" && exit 1)"
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -60,6 +63,10 @@ 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
|
||||
|
||||
@@ -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 [ :curl, "-fsSL", "https://get.docker.com" ], :sh
|
||||
pipe get_docker, :sh
|
||||
end
|
||||
|
||||
# Checks the Docker client version. Fails if Docker is not installed.
|
||||
@@ -18,4 +18,13 @@ 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
|
||||
|
||||
@@ -13,10 +13,10 @@ class Kamal::Commands::Prune < Kamal::Commands::Base
|
||||
"while read image tag; do docker rmi $tag; done"
|
||||
end
|
||||
|
||||
def app_containers(keep_last: 5)
|
||||
def app_containers(retain:)
|
||||
pipe \
|
||||
docker(:ps, "-q", "-a", *service_filter, *stopped_containers_filters),
|
||||
"tail -n +#{keep_last + 1}",
|
||||
"tail -n +#{retain + 1}",
|
||||
"while read container_id; do docker rm $container_id; done"
|
||||
end
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@ class Kamal::Commands::Registry < Kamal::Commands::Base
|
||||
delegate :registry, to: :config
|
||||
|
||||
def login
|
||||
docker :login, registry["server"], "-u", sensitive(lookup("username")), "-p", sensitive(lookup("password"))
|
||||
docker :login,
|
||||
registry["server"],
|
||||
"-u", sensitive(Kamal::Utils.escape_shell_value(lookup("username"))),
|
||||
"-p", sensitive(Kamal::Utils.escape_shell_value(lookup("password")))
|
||||
end
|
||||
|
||||
def logout
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class Kamal::Commands::Traefik < Kamal::Commands::Base
|
||||
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||
|
||||
DEFAULT_IMAGE = "traefik:v2.9"
|
||||
DEFAULT_IMAGE = "traefik:v2.10"
|
||||
CONTAINER_PORT = 80
|
||||
DEFAULT_ARGS = {
|
||||
'log.level' => 'DEBUG'
|
||||
@@ -39,7 +39,7 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base
|
||||
end
|
||||
|
||||
def start_or_run
|
||||
combine start, run, by: "||"
|
||||
any start, run
|
||||
end
|
||||
|
||||
def info
|
||||
|
||||
@@ -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, to: :raw_config, allow_nil: true
|
||||
delegate :service, :image, :servers, :env, :labels, :registry, :stop_wait_time, :hooks_path, :logging, to: :raw_config, allow_nil: true
|
||||
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||
|
||||
attr_reader :destination, :raw_config
|
||||
@@ -92,7 +92,19 @@ class Kamal::Configuration
|
||||
end
|
||||
|
||||
def primary_host
|
||||
role(primary_role)&.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
|
||||
end
|
||||
|
||||
def traefik_roles
|
||||
@@ -127,6 +139,10 @@ 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?
|
||||
@@ -137,9 +153,9 @@ class Kamal::Configuration
|
||||
end
|
||||
|
||||
def logging_args
|
||||
if raw_config.logging.present?
|
||||
optionize({ "log-driver" => raw_config.logging["driver"] }.compact) +
|
||||
argumentize("--log-opt", raw_config.logging["options"])
|
||||
if logging.present?
|
||||
optionize({ "log-driver" => logging["driver"] }.compact) +
|
||||
argumentize("--log-opt", logging["options"])
|
||||
else
|
||||
argumentize("--log-opt", { "max-size" => "10m" })
|
||||
end
|
||||
@@ -208,17 +224,9 @@ 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_destination_if_required && ensure_required_keys_present && ensure_valid_kamal_version && ensure_retain_containers_valid && ensure_valid_service_name
|
||||
end
|
||||
|
||||
def to_h
|
||||
@@ -264,12 +272,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)
|
||||
raise ArgumentError, "The primary_role #{primary_role} isn't defined"
|
||||
unless role_names.include?(primary_role_name)
|
||||
raise ArgumentError, "The primary_role #{primary_role_name} isn't defined"
|
||||
end
|
||||
|
||||
if role(primary_role).hosts.empty?
|
||||
raise ArgumentError, "No servers specified for the #{primary_role} primary_role"
|
||||
if primary_role.hosts.empty?
|
||||
raise ArgumentError, "No servers specified for the #{primary_role.name} primary_role"
|
||||
end
|
||||
|
||||
unless allow_empty_roles?
|
||||
@@ -283,6 +291,12 @@ 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}"
|
||||
@@ -291,6 +305,12 @@ 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
|
||||
|
||||
@@ -8,7 +8,7 @@ class Kamal::Configuration::Accessory
|
||||
end
|
||||
|
||||
def service_name
|
||||
"#{config.service}-#{name}"
|
||||
specifics["service"] || "#{config.service}-#{name}"
|
||||
end
|
||||
|
||||
def image
|
||||
|
||||
@@ -8,7 +8,7 @@ class Kamal::Configuration::Boot
|
||||
limit = @options["limit"]
|
||||
|
||||
if limit.to_s.end_with?("%")
|
||||
@host_count * limit.to_i / 100
|
||||
[@host_count * limit.to_i / 100, 1].max
|
||||
else
|
||||
limit
|
||||
end
|
||||
|
||||
@@ -81,6 +81,10 @@ class Kamal::Configuration::Builder
|
||||
end
|
||||
end
|
||||
|
||||
def ssh
|
||||
@options["ssh"]
|
||||
end
|
||||
|
||||
private
|
||||
def valid?
|
||||
if @options["cache"] && @options["cache"]["type"]
|
||||
|
||||
@@ -3,9 +3,10 @@ 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
|
||||
@name, @config = name.inquiry, config
|
||||
end
|
||||
|
||||
def primary_host
|
||||
@@ -36,6 +37,18 @@ 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"]
|
||||
@@ -101,7 +114,7 @@ class Kamal::Configuration::Role
|
||||
end
|
||||
|
||||
def primary?
|
||||
@config.primary_role == name
|
||||
self == @config.primary_role
|
||||
end
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
require "sshkit"
|
||||
require "sshkit/dsl"
|
||||
require "net/scp"
|
||||
require "active_support/core_ext/hash/deep_merge"
|
||||
require "json"
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
module Kamal
|
||||
VERSION = "1.3.1"
|
||||
VERSION = "1.4.0"
|
||||
end
|
||||
|
||||
@@ -148,6 +148,30 @@ 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"]) }
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -2,12 +2,47 @@ require_relative "cli_test_case"
|
||||
|
||||
class CliMainTest < CliTestCase
|
||||
test "setup" do
|
||||
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" ])
|
||||
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(:deploy)
|
||||
|
||||
run_command("setup")
|
||||
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
|
||||
end
|
||||
|
||||
test "deploy" do
|
||||
|
||||
@@ -20,6 +20,15 @@ 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
|
||||
|
||||
@@ -21,12 +21,15 @@ 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(:curl, "-fsSL", "https://get.docker.com", "|", :sh).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(: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
|
||||
|
||||
@@ -83,14 +83,14 @@ class CommanderTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
test "primary_role" do
|
||||
assert_equal "web", @kamal.primary_role
|
||||
assert_equal "web", @kamal.primary_role.name
|
||||
@kamal.specific_roles = "workers"
|
||||
assert_equal "workers", @kamal.primary_role
|
||||
assert_equal "workers", @kamal.primary_role.name
|
||||
end
|
||||
|
||||
test "roles_on" do
|
||||
assert_equal [ "web" ], @kamal.roles_on("1.1.1.1")
|
||||
assert_equal [ "workers" ], @kamal.roles_on("1.1.1.3")
|
||||
assert_equal [ "web" ], @kamal.roles_on("1.1.1.1").map(&:name)
|
||||
assert_equal [ "workers" ], @kamal.roles_on("1.1.1.3").map(&:name)
|
||||
end
|
||||
|
||||
test "default group strategy" do
|
||||
@@ -109,12 +109,18 @@ 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
|
||||
assert_equal "web_tokyo", @kamal.primary_role.name
|
||||
assert_equal "1.1.1.3", @kamal.primary_host
|
||||
end
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
||||
]
|
||||
},
|
||||
"busybox" => {
|
||||
"service" => "custom-busybox",
|
||||
"image" => "busybox:latest",
|
||||
"host" => "1.1.1.7"
|
||||
}
|
||||
@@ -57,7 +58,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
||||
new_command(:redis).run.join(" ")
|
||||
|
||||
assert_equal \
|
||||
"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",
|
||||
"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",
|
||||
new_command(:busybox).run.join(" ")
|
||||
end
|
||||
|
||||
@@ -65,7 +66,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
||||
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
|
||||
|
||||
assert_equal \
|
||||
"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",
|
||||
"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",
|
||||
new_command(:busybox).run.join(" ")
|
||||
end
|
||||
|
||||
|
||||
@@ -71,6 +71,15 @@ 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",
|
||||
@@ -145,12 +154,20 @@ 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 --tail 10 --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 --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 --tail 10 --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 --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
|
||||
|
||||
|
||||
@@ -383,6 +400,7 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
|
||||
private
|
||||
def new_command(role: "web", **additional_config)
|
||||
Kamal::Commands::App.new(Kamal::Configuration.new(@config.merge(additional_config), destination: @destination, version: "999"), role: role)
|
||||
config = Kamal::Configuration.new(@config.merge(additional_config), destination: @destination, version: "999")
|
||||
Kamal::Commands::App.new(config, role: config.role(role))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -111,8 +111,16 @@ 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
|
||||
|
||||
@@ -9,7 +9,7 @@ class CommandsDockerTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
test "install" do
|
||||
assert_equal "curl -fsSL https://get.docker.com | sh", @docker.install.join(" ")
|
||||
assert_equal "sh -c 'curl -fsSL https://get.docker.com || wget -O - https://get.docker.com || echo \"exit 1\"' | sh", @docker.install.join(" ")
|
||||
end
|
||||
|
||||
test "installed?" do
|
||||
|
||||
@@ -23,7 +23,11 @@ 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.join(" ")
|
||||
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(" ")
|
||||
end
|
||||
|
||||
test "healthcheck containers" do
|
||||
|
||||
@@ -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,7 +24,18 @@ class CommandsRegistryTest < ActiveSupport::TestCase
|
||||
@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")
|
||||
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'\\\"\"",
|
||||
@registry.login.join(" ")
|
||||
ensure
|
||||
ENV.delete("KAMAL_REGISTRY_PASSWORD")
|
||||
@@ -35,7 +46,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")
|
||||
|
||||
@@ -49,6 +49,7 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
|
||||
}
|
||||
},
|
||||
"monitoring" => {
|
||||
"service" => "custom-monitoring",
|
||||
"image" => "monitoring:latest",
|
||||
"roles" => [ "web" ],
|
||||
"port" => "4321:4321",
|
||||
@@ -72,6 +73,7 @@ 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
|
||||
|
||||
@@ -148,4 +148,14 @@ 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
|
||||
|
||||
@@ -42,6 +42,16 @@ 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)
|
||||
@@ -290,16 +300,16 @@ class ConfigurationTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
test "primary role" do
|
||||
assert_equal "web", @config.primary_role
|
||||
assert_equal "web", @config.primary_role.name
|
||||
|
||||
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
|
||||
assert_equal "alternate_web", config.primary_role.name
|
||||
assert_equal "1.1.1.4", config.primary_host
|
||||
assert config.role(:alternate_web).primary?
|
||||
assert config.role(:alternate_web).primary?
|
||||
assert config.role(:alternate_web).running_traefik?
|
||||
end
|
||||
|
||||
@@ -309,4 +319,12 @@ 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
|
||||
|
||||
17
test/fixtures/deploy_with_low_percentage_boot_strategy.yml
vendored
Normal file
17
test/fixtures/deploy_with_low_percentage_boot_strategy.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
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
|
||||
@@ -13,5 +13,5 @@ registry:
|
||||
password: pw
|
||||
|
||||
boot:
|
||||
limit: 25%
|
||||
limit: 1%
|
||||
wait: 2
|
||||
|
||||
3
test/integration/docker/deployer/app/.kamal/hooks/docker-setup
Executable file
3
test/integration/docker/deployer/app/.kamal/hooks/docker-setup
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
echo "Docker set up!"
|
||||
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/docker-setup
|
||||
@@ -24,9 +24,10 @@ traefik:
|
||||
args:
|
||||
accesslog: true
|
||||
accesslog.format: json
|
||||
image: registry:4443/traefik:v2.9
|
||||
image: registry:4443/traefik:v2.10
|
||||
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:
|
||||
|
||||
@@ -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.9
|
||||
push_image_to_registry_4443 traefik v2.10
|
||||
push_image_to_registry_4443 busybox 1.36.0
|
||||
|
||||
# .ssh is on a shared volume that persists between runs. Clean it up as the
|
||||
|
||||
@@ -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/m, error
|
||||
assert_match /Deploy lock found. Run 'kamal lock help' for more information/m, error
|
||||
|
||||
kamal :lock, :release
|
||||
|
||||
|
||||
@@ -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.9/, details
|
||||
assert_match /traefik:v2.10/, details
|
||||
assert_match /registry:4443\/app:#{first_version}/, details
|
||||
|
||||
audit = kamal :audit, capture: true
|
||||
@@ -60,6 +60,19 @@ 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)
|
||||
@@ -84,4 +97,22 @@ 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
|
||||
|
||||
@@ -52,11 +52,11 @@ class TraefikTest < IntegrationTest
|
||||
|
||||
private
|
||||
def assert_traefik_running
|
||||
assert_match /traefik:v2.9 "\/entrypoint.sh/, traefik_details
|
||||
assert_match /traefik:v2.10 "\/entrypoint.sh/, traefik_details
|
||||
end
|
||||
|
||||
def assert_traefik_not_running
|
||||
refute_match /traefik:v2.9 "\/entrypoint.sh/, traefik_details
|
||||
refute_match /traefik:v2.10 "\/entrypoint.sh/, traefik_details
|
||||
end
|
||||
|
||||
def traefik_details
|
||||
|
||||
Reference in New Issue
Block a user