Compare commits

..

40 Commits

Author SHA1 Message Date
Donal McBreen
c985fa33d1 Bump version for 1.4.0 2024-03-20 09:27:23 +00:00
Donal McBreen
e8b9f8907f Merge pull request #715 from basecamp/use-role-not-string-in-config
Pass around Roles instead of Strings
2024-03-08 08:55:53 +00:00
Donal McBreen
4966d52919 Pass around Roles instead of Strings
Avoid looking up roles by names everywhere. This avoids the awkward
role/role_config naming as well.
2024-03-08 08:44:35 +00:00
Donal McBreen
52bb40add0 Merge pull request #656 from DanielJackson-Oslo/informative-error-message-on-lock
Informative message on lock error
2024-03-07 11:16:18 +00:00
Donal McBreen
73a9276cdd Fix up app command tests 2024-03-07 11:11:20 +00:00
Donal McBreen
8c0784ed4a Merge pull request #634 from alhafoudh/main
Allow lines option to be configured when following app logs
2024-03-07 11:11:08 +00:00
Donal McBreen
089a2d3bba Merge pull request #710 from basecamp/install-wget-or-curl
Install docker with curl or wget
2024-03-07 11:01:30 +00:00
Donal McBreen
bd76d23916 Merge pull request #593 from CleverFew/role_logging_config
Role specific logging configuration
2024-03-07 10:53:34 +00:00
Donal McBreen
fa37fcd10c Merge pull request #585 from tsvallender/docker-network
Add docker-setup hook
2024-03-07 10:51:08 +00:00
Donal McBreen
f5dc0858b0 Update error message to include wget 2024-03-07 10:49:32 +00:00
Donal McBreen
9dddb140b1 Merge pull request #558 from GeNiuS69/add-skip_push-to-setup
Add --skip_push option to setup
2024-03-07 10:26:41 +00:00
Donal McBreen
26b1d57c90 Install docker with curl or wget
If curl is not available to download the docker install script, try
with wget instead.

If neither is available or both fail, return a simple failing script
so that we don't carry on regardless.

Fixes: https://github.com/basecamp/kamal/issues/395
2024-03-07 10:16:03 +00:00
Donal McBreen
b94199415f Convert combine by: '||' to any 2024-03-07 09:10:49 +00:00
Trevor Vallender
f69c45b7ea Add docker-setup hook
This allows the user to make any necessary configuration changes to
Docker before setting up any containers, allowing those configuration
changes to take effect from the outset.
2024-03-06 19:01:48 +00:00
Donal McBreen
32a2ae5b2c Merge pull request #708 from nickhammond/valid_service_name
Remove warning for valid service name
2024-03-06 16:22:04 +00:00
Nick Hammond
37544a6383 Merge branch 'basecamp:main' into valid_service_name 2024-03-06 09:09:13 -07:00
Nick Hammond
a1bc6d61af Switch the regex ordering for hyphen and underscore for service name to remove warning 2024-03-06 09:08:17 -07:00
Donal McBreen
5c32be10f1 Merge pull request #707 from basecamp/boot-strategy-min-limit-1
Ensure a minimum limit of 1 for % boot strategy
2024-03-06 16:06:35 +00:00
Donal McBreen
dc5af03593 Update tests to match single quotes 2024-03-06 16:04:31 +00:00
Donal McBreen
1abd029ea0 Merge pull request #696 from dorianmariecom/patch-1
Replace \`service\` by 'service' so it doesn't get executed by bash
2024-03-06 16:04:11 +00:00
Donal McBreen
c4d0d3e5eb Merge pull request #704 from basecamp/escape-registry-username-password
Escape the docker registry username and password
2024-03-06 15:58:46 +00:00
Donal McBreen
46e7cf8e78 Merge pull request #706 from basecamp/kamal-remove-noop
Ensure `kamal remove` completes without setup
2024-03-06 15:58:34 +00:00
Donal McBreen
c7cfc074b6 Ensure a minimum limit of 1 for % boot strategy
Fixes: https://github.com/basecamp/kamal/issues/681
2024-03-06 15:51:35 +00:00
Donal McBreen
8e2184d65e Ensure kamal remove completes without setup
If `kamal setup` has not run or errored out part way through,
`kamal remove` should still complete.

Fixes: https://github.com/basecamp/kamal/issues/629
2024-03-06 14:59:26 +00:00
Donal McBreen
2be397b679 Escape the docker registry username and password
Fixes: https://github.com/basecamp/kamal/issues/278
2024-03-06 11:04:55 +00:00
Ahmed Al Hafoudh
91f350fcce Merge branch 'basecamp:main' into main 2024-03-04 16:22:28 +01:00
Nick Lozon
e4e9664049 use double quotes 2024-03-04 10:10:51 -05:00
Nick Lozon
1acef5221f test deep_merge 2024-03-04 10:06:30 -05:00
Nick Lozon
788a57e85e role logging_args method, use in app 2024-03-04 10:06:30 -05:00
Nick Lozon
f9a934a01f configuration logging accessor 2024-03-04 10:06:30 -05:00
Dorian Marié
2bffc3bc74 Replace \service\ by 'service' so it doesn't get executed by bash
Fixes #694
2024-03-01 09:54:06 +01:00
Aleksandr Nigomatulin
064ace0598 Rollback passing invoke_options 2024-02-24 21:36:20 +06:00
Aleksandr Nigomatulin
5ef384d666 Add test 2024-02-17 00:11:03 +06:00
Aleksandr Nigomatulin
b94dfe193b Remove unnecessary code 2024-02-16 12:52:07 +06:00
Aleksandr Nigomatulin
bc6c027315 Upds according remarks 2024-02-16 11:56:58 +06:00
Daniel Jackson
ad04bb7556 Show context for lock status message on raise_if_locked 2024-01-23 09:17:15 +01:00
Daniel Jackson
1ec69d3764 Tell user about 'kamal lock help' when deploy fails due to a lock 2024-01-23 09:16:09 +01:00
Daniel Jackson
2d1a0dc9ba Informative message on lock error 2024-01-22 09:11:17 +01:00
Ahmed Al Hafoudh
0d709a3fdb Allow lines option to be configured when following app logs 2024-01-08 09:34:38 +01:00
Aleksandr Nigomatulin
cbd99306eb Add skip_push option to setup 2023-10-30 23:27:58 +06:00
53 changed files with 461 additions and 499 deletions

View File

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

View File

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

View File

@@ -123,8 +123,9 @@ module Kamal::Cli
yield yield
rescue SSHKit::Runner::ExecuteError => e rescue SSHKit::Runner::ExecuteError => e
if e.message =~ /cannot create directory/ if e.message =~ /cannot create directory/
say "Deploy lock already in place!", :red
on(KAMAL.primary_host) { puts capture_with_debug(*KAMAL.lock.status) } 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 else
raise e raise e
end end

View File

@@ -1,61 +1,52 @@
require "tempfile" require "tempfile"
class Kamal::Cli::Env < Kamal::Cli::Base class Kamal::Cli::Env < Kamal::Cli::Base
desc "push", "Push the env files to the remote hosts" desc "push", "Push the env file to the remote hosts"
option :env_type, type: :string, desc: "Type of env files", enum: %w[secret clear all], default: "all"
def push def push
secret = %w[secret all].include?(options[:env_type])
clear = %w[clear all].include?(options[:env_type])
mutating do mutating do
on(KAMAL.hosts) do on(KAMAL.hosts) do
execute *KAMAL.auditor.record("Pushed env files"), verbosity: :debug execute *KAMAL.auditor.record("Pushed env files"), verbosity: :debug
KAMAL.roles_on(host).each do |role| KAMAL.roles_on(host).each do |role|
role_config = KAMAL.config.role(role)
execute *KAMAL.app(role: role).make_env_directory execute *KAMAL.app(role: role).make_env_directory
upload! StringIO.new(role_config.env_file.secret), role_config.host_secret_env_file_path, mode: 400 if secret upload! StringIO.new(role.env_file), role.host_env_file_path, mode: 400
upload! StringIO.new(role_config.env_file.clear), role_config.host_clear_env_file_path, mode: 400 if clear
end end
end end
on(KAMAL.traefik_hosts) do on(KAMAL.traefik_hosts) do
execute *KAMAL.traefik.make_env_directory execute *KAMAL.traefik.make_env_directory
upload! StringIO.new(KAMAL.traefik.env_file.secret), KAMAL.traefik.host_secret_env_file_path, mode: 400 if secret upload! StringIO.new(KAMAL.traefik.env_file), KAMAL.traefik.host_env_file_path, mode: 400
upload! StringIO.new(KAMAL.traefik.env_file.clear), KAMAL.traefik.host_clear_env_file_path, mode: 400 if clear
end end
on(KAMAL.accessory_hosts) do on(KAMAL.accessory_hosts) do
KAMAL.accessories_on(host).each do |accessory| KAMAL.accessories_on(host).each do |accessory|
accessory_config = KAMAL.config.accessory(accessory) accessory_config = KAMAL.config.accessory(accessory)
execute *KAMAL.accessory(accessory).make_env_directory execute *KAMAL.accessory(accessory).make_env_directory
upload! StringIO.new(accessory_config.env_file.secret), accessory_config.host_secret_env_file_path, mode: 400 if secret upload! StringIO.new(accessory_config.env_file), accessory_config.host_env_file_path, mode: 400
upload! StringIO.new(accessory_config.env_file.clear), accessory_config.host_clear_env_file_path, mode: 400 if clear
end end
end end
end end
end end
desc "delete", "Delete the env files from the remote hosts" desc "delete", "Delete the env file from the remote hosts"
def delete def delete
mutating do mutating do
on(KAMAL.hosts) do on(KAMAL.hosts) do
execute *KAMAL.auditor.record("Deleted env files"), verbosity: :debug execute *KAMAL.auditor.record("Deleted env files"), verbosity: :debug
KAMAL.roles_on(host).each do |role| KAMAL.roles_on(host).each do |role|
role_config = KAMAL.config.role(role) execute *KAMAL.app(role: role).remove_env_file
execute *KAMAL.app(role: role).remove_env_files
end end
end end
on(KAMAL.traefik_hosts) do on(KAMAL.traefik_hosts) do
execute *KAMAL.traefik.remove_env_files execute *KAMAL.traefik.remove_env_file
end end
on(KAMAL.accessory_hosts) do on(KAMAL.accessory_hosts) do
KAMAL.accessories_on(host).each do |accessory| KAMAL.accessories_on(host).each do |accessory|
accessory_config = KAMAL.config.accessory(accessory) accessory_config = KAMAL.config.accessory(accessory)
execute *KAMAL.accessory(accessory).remove_env_files execute *KAMAL.accessory(accessory).remove_env_file
end end
end end
end end

View File

@@ -1,15 +1,18 @@
class Kamal::Cli::Main < Kamal::Cli::Base class Kamal::Cli::Main < Kamal::Cli::Base
desc "setup", "Setup all accessories, push the env, and deploy app to servers" 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 def setup
print_runtime do print_runtime do
mutating do mutating do
invoke_options = deploy_options
say "Ensure Docker is installed...", :magenta say "Ensure Docker is installed...", :magenta
invoke "kamal:cli:server:bootstrap" invoke "kamal:cli:server:bootstrap", [], invoke_options
say "Push env files...", :magenta 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 deploy
end end
end end
@@ -35,8 +38,6 @@ class Kamal::Cli::Main < Kamal::Cli::Base
run_hook "pre-deploy" run_hook "pre-deploy"
push_env(invoke_options)
say "Ensure Traefik is running...", :magenta say "Ensure Traefik is running...", :magenta
invoke "kamal:cli:traefik:boot", [], invoke_options invoke "kamal:cli:traefik:boot", [], invoke_options
@@ -75,8 +76,6 @@ class Kamal::Cli::Main < Kamal::Cli::Base
run_hook "pre-deploy" run_hook "pre-deploy"
push_env(invoke_options)
say "Ensure app can pass healthcheck...", :magenta say "Ensure app can pass healthcheck...", :magenta
invoke "kamal:cli:healthcheck:perform", [], invoke_options invoke "kamal:cli:healthcheck:perform", [], invoke_options
@@ -103,8 +102,6 @@ class Kamal::Cli::Main < Kamal::Cli::Base
if container_available?(version) if container_available?(version)
run_hook "pre-deploy" run_hook "pre-deploy"
push_env(invoke_options)
invoke "kamal:cli:app:boot", [], invoke_options.merge(version: version) invoke "kamal:cli:app:boot", [], invoke_options.merge(version: version)
rolled_back = true rolled_back = true
else else
@@ -268,11 +265,4 @@ class Kamal::Cli::Main < Kamal::Cli::Base
def deploy_options def deploy_options
{ "version" => KAMAL.config.version }.merge(options.without("skip_push")) { "version" => KAMAL.config.version }.merge(options.without("skip_push"))
end end
def push_env(invoke_options)
if KAMAL.config.push_env
say "Pushing #{KAMAL.config.push_env} env files..."
invoke "kamal:cli:env:push", [], invoke_options.merge(env_type: KAMAL.config.push_env)
end
end
end end

View File

@@ -17,7 +17,9 @@ class Kamal::Cli::Server < Kamal::Cli::Base
end end
if missing.any? 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 end
run_hook "docker-setup"
end end
end end

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

View File

@@ -53,7 +53,7 @@ class Kamal::Commander
def primary_host def primary_host
# Given a list of specific roles, make an effort to match up with the primary_role # 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 end
def primary_role def primary_role
@@ -73,7 +73,7 @@ class Kamal::Commander
end end
def roles_on(host) 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 end
def traefik_hosts def traefik_hosts

View File

@@ -102,8 +102,8 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
make_directory accessory_config.host_env_directory make_directory accessory_config.host_env_directory
end end
def remove_env_files def remove_env_file
[:rm, "-f", File.join(accessory_config.host_env_directory, "#{accessory_config.service_name}*.env")] [:rm, "-f", accessory_config.host_env_file_path]
end end
private private

View File

@@ -3,12 +3,11 @@ class Kamal::Commands::App < Kamal::Commands::Base
ACTIVE_DOCKER_STATUSES = [ :running, :restarting ] ACTIVE_DOCKER_STATUSES = [ :running, :restarting ]
attr_reader :role, :role_config attr_reader :role, :role
def initialize(config, role: nil) def initialize(config, role: nil)
super(config) super(config)
@role = role @role = role
@role_config = config.role(self.role)
end end
def run(hostname: nil) def run(hostname: nil)
@@ -19,15 +18,15 @@ class Kamal::Commands::App < Kamal::Commands::Base
*(["--hostname", hostname] if hostname), *(["--hostname", hostname] if hostname),
"-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"", "-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
"-e", "KAMAL_VERSION=\"#{config.version}\"", "-e", "KAMAL_VERSION=\"#{config.version}\"",
*role_config.env_args, *role.env_args,
*role_config.health_check_args, *role.health_check_args,
*config.logging_args, *role.logging_args,
*config.volume_args, *config.volume_args,
*role_config.asset_volume_args, *role.asset_volume_args,
*role_config.label_args, *role.label_args,
*role_config.option_args, *role.option_args,
config.absolute_image, config.absolute_image,
role_config.cmd role.cmd
end end
def start def start
@@ -64,22 +63,22 @@ class Kamal::Commands::App < Kamal::Commands::Base
def list_versions(*docker_args, statuses: nil) def list_versions(*docker_args, statuses: nil)
pipe \ pipe \
docker(:ps, *filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'), 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 end
def make_env_directory def make_env_directory
make_directory role_config.host_env_directory make_directory role.host_env_directory
end end
def remove_env_files def remove_env_file
[ :rm, "-f", File.join(role_config.host_env_directory, "#{role_config.container_prefix}*.env") ] [ :rm, "-f", role.host_env_file_path ]
end end
private private
def container_name(version = nil) def container_name(version = nil)
[ role_config.container_prefix, version || config.version ].compact.join("-") [ role.container_prefix, version || config.version ].compact.join("-")
end end
def filter_args(statuses: nil) def filter_args(statuses: nil)

View File

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

View File

@@ -2,7 +2,7 @@ module Kamal::Commands::App::Cord
def cord(version:) def cord(version:)
pipe \ pipe \
docker(:inspect, "-f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}'", container_name(version)), 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 end
def tie_cord(cord) def tie_cord(cord)
@@ -12,8 +12,8 @@ module Kamal::Commands::App::Cord
def cut_cord(cord) def cut_cord(cord)
remove_directory(cord) remove_directory(cord)
end end
private private
def create_empty_file(file) def create_empty_file(file)
chain \ chain \
make_directory_for(file), make_directory_for(file),

View File

@@ -10,9 +10,9 @@ module Kamal::Commands::App::Execution
docker :run, docker :run,
("-it" if interactive), ("-it" if interactive),
"--rm", "--rm",
*role_config&.env_args, *role&.env_args,
*config.volume_args, *config.volume_args,
*role_config&.option_args, *role&.option_args,
config.absolute_image, config.absolute_image,
*command *command
end end

View File

@@ -6,11 +6,11 @@ module Kamal::Commands::App::Logging
("grep '#{grep}'" if grep) ("grep '#{grep}'" if grep)
end end
def follow_logs(host:, grep: nil) def follow_logs(host:, lines: nil, grep: nil)
run_over_ssh \ run_over_ssh \
pipe( pipe(
current_running_container_id, 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) (%(grep "#{grep}") if grep)
), ),
host: host host: host

View File

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

View File

@@ -24,7 +24,10 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
def validate_image def validate_image
pipe \ pipe \
docker(:inspect, "-f", "'{{ .Config.Labels.service }}'", config.absolute_image), 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 end

View File

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

View File

@@ -2,7 +2,10 @@ class Kamal::Commands::Registry < Kamal::Commands::Base
delegate :registry, to: :config delegate :registry, to: :config
def login 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 end
def logout def logout

View File

@@ -39,7 +39,7 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base
end end
def start_or_run def start_or_run
combine start, run, by: "||" any start, run
end end
def info def info
@@ -72,23 +72,19 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base
end end
def env_file def env_file
Kamal::EnvFiles.new(config.traefik.fetch("env", {})) Kamal::EnvFile.new(config.traefik.fetch("env", {}))
end end
def host_clear_env_file_path def host_env_file_path
File.join host_env_directory, "traefik-clear.env" File.join host_env_directory, "traefik.env"
end
def host_secret_env_file_path
File.join host_env_directory, "traefik-secret.env"
end end
def make_env_directory def make_env_directory
make_directory(host_env_directory) make_directory(host_env_directory)
end end
def remove_env_files def remove_env_file
[:rm, "-f", File.join(host_env_directory, "traefik*.env")] [:rm, "-f", host_env_file_path]
end end
private private
@@ -101,10 +97,7 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base
end end
def env_args def env_args
[ argumentize "--env-file", host_env_file_path
*argumentize("--env-file", host_secret_env_file_path),
*argumentize("--env-file", host_clear_env_file_path)
]
end end
def host_env_directory def host_env_directory

View File

@@ -6,7 +6,7 @@ require "erb"
require "net/ssh/proxy/jump" require "net/ssh/proxy/jump"
class Kamal::Configuration class Kamal::Configuration
delegate :service, :image, :servers, :env, :labels, :registry, :stop_wait_time, :hooks_path, :push_env, 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 delegate :argumentize, :optionize, to: Kamal::Utils
attr_reader :destination, :raw_config attr_reader :destination, :raw_config
@@ -92,7 +92,19 @@ class Kamal::Configuration
end end
def primary_host 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 end
def traefik_roles def traefik_roles
@@ -141,9 +153,9 @@ class Kamal::Configuration
end end
def logging_args def logging_args
if raw_config.logging.present? if logging.present?
optionize({ "log-driver" => raw_config.logging["driver"] }.compact) + optionize({ "log-driver" => logging["driver"] }.compact) +
argumentize("--log-opt", raw_config.logging["options"]) argumentize("--log-opt", logging["options"])
else else
argumentize("--log-opt", { "max-size" => "10m" }) argumentize("--log-opt", { "max-size" => "10m" })
end end
@@ -212,22 +224,9 @@ class Kamal::Configuration
raw_config.asset_path raw_config.asset_path
end end
def primary_role
raw_config.primary_role || "web"
end
def allow_empty_roles?
raw_config.allow_empty_roles
end
def valid? def valid?
ensure_destination_if_required \ ensure_destination_if_required && ensure_required_keys_present && ensure_valid_kamal_version && ensure_retain_containers_valid && ensure_valid_service_name
&& ensure_required_keys_present \
&& ensure_valid_kamal_version \
&& ensure_retain_containers_valid \
&& ensure_valid_service_name \
&& ensure_push_env_valid
end end
def to_h def to_h
@@ -273,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)" raise ArgumentError, "You must specify a password for the registry in config/deploy.yml (or set the ENV variable if that's used)"
end end
unless role_names.include?(primary_role) unless role_names.include?(primary_role_name)
raise ArgumentError, "The primary_role #{primary_role} isn't defined" raise ArgumentError, "The primary_role #{primary_role_name} isn't defined"
end end
if role(primary_role).hosts.empty? if primary_role.hosts.empty?
raise ArgumentError, "No servers specified for the #{primary_role} primary_role" raise ArgumentError, "No servers specified for the #{primary_role.name} primary_role"
end end
unless allow_empty_roles? unless allow_empty_roles?
@@ -293,7 +292,7 @@ class Kamal::Configuration
end end
def ensure_valid_service_name def ensure_valid_service_name
raise ArgumentError, "Service name can only include alphanumeric characters, hyphens, and underscores" unless raw_config[:service] =~ /^[a-z0-9-_]+$/ raise ArgumentError, "Service name can only include alphanumeric characters, hyphens, and underscores" unless raw_config[:service] =~ /^[a-z0-9_-]+$/
true true
end end
@@ -312,14 +311,6 @@ class Kamal::Configuration
true true
end end
def ensure_push_env_valid
if raw_config.push_env && !%w[ all clear secret ].include?(raw_config.push_env)
raise ArgumentError, "push_env must be one of `all`, `clear` `secret`"
end
true
end
def role_names def role_names
raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort

View File

@@ -46,26 +46,19 @@ class Kamal::Configuration::Accessory
end end
def env_file def env_file
Kamal::EnvFiles.new(env) Kamal::EnvFile.new(env)
end end
def host_env_directory def host_env_directory
File.join config.host_env_directory, "accessories" File.join config.host_env_directory, "accessories"
end end
def host_clear_env_file_path def host_env_file_path
File.join host_env_directory, "#{service_name}-clear.env" File.join host_env_directory, "#{service_name}.env"
end
def host_secret_env_file_path
File.join host_env_directory, "#{service_name}-secret.env"
end end
def env_args def env_args
[ argumentize "--env-file", host_env_file_path
*argumentize("--env-file", host_secret_env_file_path),
*argumentize("--env-file", host_clear_env_file_path)
]
end end
def files def files

View File

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

View File

@@ -3,9 +3,10 @@ class Kamal::Configuration::Role
delegate :argumentize, :optionize, to: Kamal::Utils delegate :argumentize, :optionize, to: Kamal::Utils
attr_accessor :name attr_accessor :name
alias to_s name
def initialize(name, config:) def initialize(name, config:)
@name, @config = name.inquiry, config @name, @config = name.inquiry, config
end end
def primary_host def primary_host
@@ -36,6 +37,18 @@ class Kamal::Configuration::Role
argumentize "--label", labels argumentize "--label", labels
end 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 def env
if config.env && config.env["secret"] if config.env && config.env["secret"]
@@ -46,26 +59,19 @@ class Kamal::Configuration::Role
end end
def env_file def env_file
Kamal::EnvFiles.new(env) Kamal::EnvFile.new(env)
end end
def host_env_directory def host_env_directory
File.join config.host_env_directory, "roles" File.join config.host_env_directory, "roles"
end end
def host_clear_env_file_path def host_env_file_path
host_env_file_path(:clear) File.join host_env_directory, "#{[config.service, name, config.destination].compact.join("-")}.env"
end
def host_secret_env_file_path
host_env_file_path(:secret)
end end
def env_args def env_args
[ argumentize "--env-file", host_env_file_path
*argumentize("--env-file", host_secret_env_file_path),
*argumentize("--env-file", host_clear_env_file_path)
]
end end
def asset_volume_args def asset_volume_args
@@ -108,7 +114,7 @@ class Kamal::Configuration::Role
end end
def primary? def primary?
@config.primary_role == name self == @config.primary_role
end end
@@ -250,10 +256,6 @@ class Kamal::Configuration::Role
end end
end end
def host_env_file_path(env_type)
File.join host_env_directory, "#{[container_prefix, env_type].compact.join("-")}.env"
end
def http_health_check(port:, path:) def http_health_check(port:, path:)
"curl -f #{URI.join("http://localhost:#{port}", path)} || exit 1" if path.present? || port.present? "curl -f #{URI.join("http://localhost:#{port}", path)} || exit 1" if path.present? || port.present?
end end

View File

@@ -1,25 +1,32 @@
# Encode an env hash as a string where secret values have been looked up and all values escaped for Docker. # Encode an env hash as a string where secret values have been looked up and all values escaped for Docker.
class Kamal::EnvFiles class Kamal::EnvFile
def initialize(env) def initialize(env)
@env = env @env = env
end end
def secret def to_s
env_file do env_file = StringIO.new.tap do |contents|
@env["secret"]&.to_h { |key| [ key, ENV.fetch(key) ] }
end
end
def clear
env_file do
if (secrets = @env["secret"]).present? if (secrets = @env["secret"]).present?
@env["clear"] @env.fetch("secret", @env)&.each do |key|
contents << docker_env_file_line(key, ENV.fetch(key))
end
@env["clear"]&.each do |key, value|
contents << docker_env_file_line(key, value)
end
else else
@env.fetch("clear", @env) @env.fetch("clear", @env)&.each do |key, value|
contents << docker_env_file_line(key, value)
end
end end
end end.string
# Ensure the file has some contents to avoid the SSHKIT empty file warning
env_file.presence || "\n"
end end
alias to_str to_s
private private
def docker_env_file_line(key, value) def docker_env_file_line(key, value)
"#{key.to_s}=#{escape_docker_env_file_value(value)}\n" "#{key.to_s}=#{escape_docker_env_file_value(value)}\n"
@@ -31,14 +38,4 @@ class Kamal::EnvFiles
# so remove leading and trailing ones and unescape any others # so remove leading and trailing ones and unescape any others
value.to_s.dump[1..-2].gsub(/\\"/, "\"") value.to_s.dump[1..-2].gsub(/\\"/, "\"")
end end
def env_file(&block)
StringIO.new.tap do |contents|
block.call&.each do |key, value|
contents << docker_env_file_line(key, value)
end
# Ensure the file has some contents to avoid the SSHKit empty file warning
contents << "\n" if contents.length == 0
end.string
end
end end

View File

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

View File

@@ -7,7 +7,7 @@ class CliAccessoryTest < CliTestCase
run_command("boot", "mysql").tap do |output| run_command("boot", "mysql").tap do |output|
assert_match /docker login.*on 1.1.1.3/, output assert_match /docker login.*on 1.1.1.3/, output
assert_match "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 --env-file .kamal/env/accessories/app-mysql-secret.env --env-file .kamal/env/accessories/app-mysql-clear.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output assert_match "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 --env-file .kamal/env/accessories/app-mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output
end end
end end
@@ -21,9 +21,9 @@ class CliAccessoryTest < CliTestCase
assert_match /docker login.*on 1.1.1.3/, output assert_match /docker login.*on 1.1.1.3/, output
assert_match /docker login.*on 1.1.1.1/, output assert_match /docker login.*on 1.1.1.1/, output
assert_match /docker login.*on 1.1.1.2/, output assert_match /docker login.*on 1.1.1.2/, output
assert_match "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 --env-file .kamal/env/accessories/app-mysql-secret.env --env-file .kamal/env/accessories/app-mysql-clear.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output assert_match "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 --env-file .kamal/env/accessories/app-mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 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-secret.env --env-file .kamal/env/accessories/app-redis-clear.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", 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
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-secret.env --env-file .kamal/env/accessories/app-redis-clear.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest 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.2", output
end end
end end
@@ -155,8 +155,8 @@ class CliAccessoryTest < CliTestCase
run_command("boot", "redis", "--hosts", "1.1.1.1").tap do |output| run_command("boot", "redis", "--hosts", "1.1.1.1").tap do |output|
assert_match /docker login.*on 1.1.1.1/, output assert_match /docker login.*on 1.1.1.1/, output
refute_match /docker login.*on 1.1.1.2/, 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-secret.env --env-file .kamal/env/accessories/app-redis-clear.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", 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-secret.env --env-file .kamal/env/accessories/app-redis-clear.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", 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
end end
@@ -167,8 +167,8 @@ class CliAccessoryTest < CliTestCase
run_command("boot", "redis", "--hosts", "1.1.1.1,1.1.1.3").tap do |output| 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 assert_match /docker login.*on 1.1.1.1/, output
refute_match /docker login.*on 1.1.1.3/, 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-secret.env --env-file .kamal/env/accessories/app-redis-clear.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", 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-secret.env --env-file .kamal/env/accessories/app-redis-clear.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.3", 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
end end

View File

@@ -159,7 +159,7 @@ class CliAppTest < CliTestCase
test "exec" do test "exec" do
run_command("exec", "ruby -v").tap do |output| run_command("exec", "ruby -v").tap do |output|
assert_match "docker run --rm --env-file .kamal/env/roles/app-web-secret.env --env-file .kamal/env/roles/app-web-clear.env dhh/app:latest ruby -v", output assert_match "docker run --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v", output
end end
end end
@@ -172,7 +172,7 @@ class CliAppTest < CliTestCase
test "exec interactive" do test "exec interactive" do
SSHKit::Backend::Abstract.any_instance.expects(:exec) SSHKit::Backend::Abstract.any_instance.expects(:exec)
.with("ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --env-file .kamal/env/roles/app-web-secret.env --env-file .kamal/env/roles/app-web-clear.env dhh/app:latest ruby -v'") .with("ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v'")
run_command("exec", "-i", "ruby -v").tap do |output| run_command("exec", "-i", "ruby -v").tap do |output|
assert_match "Get most recent version available as an image...", output assert_match "Get most recent version available as an image...", output
assert_match "Launching interactive command with version latest via SSH from new container on 1.1.1.1...", output assert_match "Launching interactive command with version latest via SSH from new container on 1.1.1.1...", output

View File

@@ -57,7 +57,7 @@ class CliBuildTest < CliTestCase
run_command("pull").tap do |output| run_command("pull").tap do |output|
assert_match /docker image rm --force dhh\/app:999/, output assert_match /docker image rm --force dhh\/app:999/, output
assert_match /docker pull 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
end end

View File

@@ -9,29 +9,25 @@ class CliEnvTest < CliTestCase
assert_match "Running /usr/bin/env mkdir -p .kamal/env/roles on 1.1.1.1", output assert_match "Running /usr/bin/env mkdir -p .kamal/env/roles on 1.1.1.1", output
assert_match "Running /usr/bin/env mkdir -p .kamal/env/traefik on 1.1.1.2", output assert_match "Running /usr/bin/env mkdir -p .kamal/env/traefik on 1.1.1.2", output
assert_match "Running /usr/bin/env mkdir -p .kamal/env/accessories on 1.1.1.1", output assert_match "Running /usr/bin/env mkdir -p .kamal/env/accessories on 1.1.1.1", output
assert_match ".kamal/env/roles/app-web-secret.env", output assert_match ".kamal/env/roles/app-web.env", output
assert_match ".kamal/env/roles/app-web-clear.env", output assert_match ".kamal/env/roles/app-workers.env", output
assert_match ".kamal/env/roles/app-workers-secret.env", output assert_match ".kamal/env/traefik/traefik.env", output
assert_match ".kamal/env/roles/app-workers-clear.env", output assert_match ".kamal/env/accessories/app-redis.env", output
assert_match ".kamal/env/traefik/traefik-secret.env", output
assert_match ".kamal/env/traefik/traefik-clear.env", output
assert_match ".kamal/env/accessories/app-redis-secret.env", output
assert_match ".kamal/env/accessories/app-redis-clear.env", output
end end
end end
test "delete" do test "delete" do
run_command("delete").tap do |output| run_command("delete").tap do |output|
assert_match "Running /usr/bin/env rm -f .kamal/env/roles/app-web*.env on 1.1.1.1", output assert_match "Running /usr/bin/env rm -f .kamal/env/roles/app-web.env on 1.1.1.1", output
assert_match "Running /usr/bin/env rm -f .kamal/env/roles/app-web*.env on 1.1.1.2", output assert_match "Running /usr/bin/env rm -f .kamal/env/roles/app-web.env on 1.1.1.2", output
assert_match "Running /usr/bin/env rm -f .kamal/env/roles/app-workers*.env on 1.1.1.3", output assert_match "Running /usr/bin/env rm -f .kamal/env/roles/app-workers.env on 1.1.1.3", output
assert_match "Running /usr/bin/env rm -f .kamal/env/roles/app-workers*.env on 1.1.1.4", output assert_match "Running /usr/bin/env rm -f .kamal/env/roles/app-workers.env on 1.1.1.4", output
assert_match "Running /usr/bin/env rm -f .kamal/env/traefik/traefik*.env on 1.1.1.1", output assert_match "Running /usr/bin/env rm -f .kamal/env/traefik/traefik.env on 1.1.1.1", output
assert_match "Running /usr/bin/env rm -f .kamal/env/traefik/traefik*.env on 1.1.1.2", output assert_match "Running /usr/bin/env rm -f .kamal/env/traefik/traefik.env on 1.1.1.2", output
assert_match "Running /usr/bin/env rm -f .kamal/env/accessories/app-redis*.env on 1.1.1.1", output assert_match "Running /usr/bin/env rm -f .kamal/env/accessories/app-redis.env on 1.1.1.1", output
assert_match "Running /usr/bin/env rm -f .kamal/env/accessories/app-redis*.env on 1.1.1.2", output assert_match "Running /usr/bin/env rm -f .kamal/env/accessories/app-redis.env on 1.1.1.2", output
assert_match "Running /usr/bin/env rm -f .kamal/env/accessories/app-mysql*.env on 1.1.1.3", output assert_match "Running /usr/bin/env rm -f .kamal/env/accessories/app-mysql.env on 1.1.1.3", output
end end
end end

View File

@@ -11,7 +11,7 @@ class CliHealthcheckTest < CliTestCase
SSHKit::Backend::Abstract.any_instance.stubs(:execute) SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :stop, raise_on_non_zero_exit: false) .with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :stop, raise_on_non_zero_exit: false)
SSHKit::Backend::Abstract.any_instance.stubs(:execute) SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with(:docker, :run, "--detach", "--name", "healthcheck-app-999", "--publish", "3999:3000", "--label", "service=healthcheck-app", "-e", "KAMAL_CONTAINER_NAME=\"healthcheck-app\"", "--env-file", ".kamal/env/roles/app-web-secret.env", "--env-file", ".kamal/env/roles/app-web-clear.env", "--health-cmd", "\"curl -f http://localhost:3000/up || exit 1\"", "--health-interval", "\"1s\"", "dhh/app:999") .with(:docker, :run, "--detach", "--name", "healthcheck-app-999", "--publish", "3999:3000", "--label", "service=healthcheck-app", "-e", "KAMAL_CONTAINER_NAME=\"healthcheck-app\"", "--env-file", ".kamal/env/roles/app-web.env", "--health-cmd", "\"curl -f http://localhost:3000/up || exit 1\"", "--health-interval", "\"1s\"", "dhh/app:999")
SSHKit::Backend::Abstract.any_instance.stubs(:execute) SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :container, :rm, raise_on_non_zero_exit: false) .with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :container, :rm, raise_on_non_zero_exit: false)
@@ -40,7 +40,7 @@ class CliHealthcheckTest < CliTestCase
SSHKit::Backend::Abstract.any_instance.stubs(:execute) SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :stop, raise_on_non_zero_exit: false) .with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :stop, raise_on_non_zero_exit: false)
SSHKit::Backend::Abstract.any_instance.stubs(:execute) SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with(:docker, :run, "--detach", "--name", "healthcheck-app-999", "--publish", "3999:3000", "--label", "service=healthcheck-app", "-e", "KAMAL_CONTAINER_NAME=\"healthcheck-app\"", "--env-file", ".kamal/env/roles/app-web-secret.env", "--env-file", ".kamal/env/roles/app-web-clear.env", "--health-cmd", "\"curl -f http://localhost:3000/up || exit 1\"", "--health-interval", "\"1s\"", "dhh/app:999") .with(:docker, :run, "--detach", "--name", "healthcheck-app-999", "--publish", "3999:3000", "--label", "service=healthcheck-app", "-e", "KAMAL_CONTAINER_NAME=\"healthcheck-app\"", "--env-file", ".kamal/env/roles/app-web.env", "--health-cmd", "\"curl -f http://localhost:3000/up || exit 1\"", "--health-interval", "\"1s\"", "dhh/app:999")
SSHKit::Backend::Abstract.any_instance.stubs(:execute) SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :container, :rm, raise_on_non_zero_exit: false) .with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :container, :rm, raise_on_non_zero_exit: false)
@@ -64,10 +64,10 @@ class CliHealthcheckTest < CliTestCase
end end
assert_match "container not ready (unhealthy)", exception.message assert_match "container not ready (unhealthy)", exception.message
end end
test "raises an exception if primary does not have traefik" do test "raises an exception if primary does not have traefik" do
SSHKit::Backend::Abstract.any_instance.expects(:execute).never SSHKit::Backend::Abstract.any_instance.expects(:execute).never
exception = assert_raises do exception = assert_raises do
run_command("perform", config_file: "test/fixtures/deploy_workers_only.yml") run_command("perform", config_file: "test/fixtures/deploy_workers_only.yml")
end end
@@ -77,6 +77,6 @@ class CliHealthcheckTest < CliTestCase
private private
def run_command(*command, config_file: "test/fixtures/deploy_with_accessories.yml") def run_command(*command, config_file: "test/fixtures/deploy_with_accessories.yml")
stdouted { Kamal::Cli::Healthcheck.start([*command, "-c", config_file]) } stdouted { Kamal::Cli::Healthcheck.start([*command, "-c", config_file]) }
end end
end end

View File

@@ -2,12 +2,47 @@ require_relative "cli_test_case"
class CliMainTest < CliTestCase class CliMainTest < CliTestCase
test "setup" do test "setup" do
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:server:bootstrap") invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
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(: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) 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 end
test "deploy" do test "deploy" do
@@ -122,12 +157,12 @@ class CliMainTest < CliTestCase
refute_match /Running the post-deploy hook.../, output refute_match /Running the post-deploy hook.../, output
end end
end end
test "deploy without healthcheck if primary host doesn't have traefik" do test "deploy without healthcheck if primary host doesn't have traefik" do
invoke_options = { "config_file" => "test/fixtures/deploy_workers_only.yml", "version" => "999", "skip_hooks" => false } invoke_options = { "config_file" => "test/fixtures/deploy_workers_only.yml", "version" => "999", "skip_hooks" => false }
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:healthcheck:perform", [], invoke_options).never Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:healthcheck:perform", [], invoke_options).never
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options) 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:deliver", [], invoke_options) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:deliver", [], 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:traefik:boot", [], invoke_options)
@@ -152,26 +187,6 @@ class CliMainTest < CliTestCase
run_command("deploy", config_file: "deploy_with_secrets") run_command("deploy", config_file: "deploy_with_secrets")
end end
test "deploy with push_env" do
invoke_options = { "config_file" => "test/fixtures/deploy_push_clear_env.yml", "version" => "999", "skip_hooks" => false }
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:deliver", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:env:push", [], invoke_options.merge(env_type: "clear"))
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)
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
hook_variables = { version: 999, service_version: "app@999", hosts: "1.1.1.1,1.1.1.2", command: "deploy" }
run_command("deploy", config_file: "deploy_push_clear_env").tap do |output|
assert_match /Pushing clear env files.../, output
end
end
test "redeploy" do test "redeploy" do
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false } invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
@@ -208,23 +223,6 @@ class CliMainTest < CliTestCase
end end
end end
test "redeploy with push_env" do
invoke_options = { "config_file" => "test/fixtures/deploy_push_clear_env.yml", "version" => "999", "skip_hooks" => false }
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:deliver", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:env:push", [], invoke_options.merge(env_type: "clear"))
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::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
hook_variables = { version: 999, service_version: "app@999", hosts: "1.1.1.1,1.1.1.2", command: "redeploy" }
run_command("redeploy", config_file: "deploy_push_clear_env").tap do |output|
assert_match /Pushing clear env files.../, output
end
end
test "rollback bad version" do test "rollback bad version" do
Thread.report_on_exception = false Thread.report_on_exception = false
@@ -237,8 +235,31 @@ class CliMainTest < CliTestCase
end end
test "rollback good version" do test "rollback good version" do
stub_good_rollback Object.any_instance.stubs(:sleep)
[ "web", "workers" ].each do |role|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-#{role}-123$", "--quiet", raise_on_non_zero_exit: false)
.returns("").at_least_once
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-#{role}-123$", "--quiet")
.returns("version-to-rollback\n").at_least_once
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=#{role}", "--filter", "status=running", "--filter", "status=restarting", "--latest", "--format", "\"{{.Names}}\"", "|", "while read line; do echo ${line#app-#{role}-}; done", raise_on_non_zero_exit: false)
.returns("version-to-rollback\n").at_least_once
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-#{role}-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
.returns("running").at_least_once # health check
end
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :inspect, "-f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}'", "app-web-version-to-rollback", "|", :awk, "'$2 == \"/tmp/kamal-cord\" {print $1}'", :raise_on_non_zero_exit => false)
.returns("corddirectory").at_least_once # health check
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-version-to-rollback$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
.returns("unhealthy").at_least_once # health check
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
hook_variables = { version: 123, service_version: "app@123", hosts: "1.1.1.1,1.1.1.2,1.1.1.3,1.1.1.4", command: "rollback" } hook_variables = { version: 123, service_version: "app@123", hosts: "1.1.1.1,1.1.1.2,1.1.1.3,1.1.1.4", command: "rollback" }
run_command("rollback", "123", config_file: "deploy_with_accessories").tap do |output| run_command("rollback", "123", config_file: "deploy_with_accessories").tap do |output|
@@ -271,16 +292,6 @@ class CliMainTest < CliTestCase
end end
end end
test "rollback with push_env" do
invoke_options = { "config_file" => "test/fixtures/deploy_push_clear_env.yml", "version" => "999", "skip_hooks" => false }
stub_good_rollback
run_command("rollback", "123", config_file: "deploy_push_clear_env").tap do |output|
assert_match /Pushing clear env files.../, output
end
end
test "details" do test "details" do
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:details") Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:details")
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:details") Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:details")
@@ -478,32 +489,4 @@ class CliMainTest < CliTestCase
def run_command(*command, config_file: "deploy_simple") def run_command(*command, config_file: "deploy_simple")
stdouted { Kamal::Cli::Main.start([*command, "-c", "test/fixtures/#{config_file}.yml"]) } stdouted { Kamal::Cli::Main.start([*command, "-c", "test/fixtures/#{config_file}.yml"]) }
end end
def stub_good_rollback
Object.any_instance.stubs(:sleep)
[ "web", "workers" ].each do |role|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-#{role}-123$", "--quiet", raise_on_non_zero_exit: false)
.returns("").at_least_once
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-#{role}-123$", "--quiet")
.returns("version-to-rollback\n").at_least_once
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=#{role}", "--filter", "status=running", "--filter", "status=restarting", "--latest", "--format", "\"{{.Names}}\"", "|", "while read line; do echo ${line#app-#{role}-}; done", raise_on_non_zero_exit: false)
.returns("version-to-rollback\n").at_least_once
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-#{role}-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
.returns("running").at_least_once # health check
end
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :inspect, "-f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}'", "app-web-version-to-rollback", "|", :awk, "'$2 == \"/tmp/kamal-cord\" {print $1}'", :raise_on_non_zero_exit => false)
.returns("corddirectory").at_least_once # health check
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-version-to-rollback$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
.returns("unhealthy").at_least_once # health check
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
end
end end

View File

@@ -21,12 +21,15 @@ class CliServerTest < CliTestCase
test "bootstrap install as root user" do 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(: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('[ "${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 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| run_command("bootstrap").tap do |output|
("1.1.1.1".."1.1.1.4").map do |host| ("1.1.1.1".."1.1.1.4").map do |host|
assert_match "Missing Docker on #{host}. Installing…", output assert_match "Missing Docker on #{host}. Installing…", output
assert_match "Running the docker-setup hook", output
end end
end end
end end

View File

@@ -4,7 +4,7 @@ class CliTraefikTest < CliTestCase
test "boot" do test "boot" do
run_command("boot").tap do |output| run_command("boot").tap do |output|
assert_match "docker login", output assert_match "docker login", output
assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik-secret.env --env-file .kamal/env/traefik/traefik-clear.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output
end end
end end
@@ -14,7 +14,7 @@ class CliTraefikTest < CliTestCase
run_command("reboot").tap do |output| run_command("reboot").tap do |output|
assert_match "docker container stop traefik", output assert_match "docker container stop traefik", output
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik", output assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik", output
assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik-secret.env --env-file .kamal/env/traefik/traefik-clear.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output
end end
end end

View File

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

View File

@@ -50,15 +50,15 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
test "run" do test "run" do
assert_equal \ assert_equal \
"docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 --env-file .kamal/env/accessories/app-mysql-secret.env --env-file .kamal/env/accessories/app-mysql-clear.env --label service=\"app-mysql\" private.registry/mysql:8.0", "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 --env-file .kamal/env/accessories/app-mysql.env --label service=\"app-mysql\" private.registry/mysql:8.0",
new_command(:mysql).run.join(" ") new_command(:mysql).run.join(" ")
assert_equal \ assert_equal \
"docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis-secret.env --env-file .kamal/env/accessories/app-redis-clear.env --volume /var/lib/redis:/data --label service=\"app-redis\" --label cache=\"true\" redis:latest", "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 /var/lib/redis:/data --label service=\"app-redis\" --label cache=\"true\" redis:latest",
new_command(:redis).run.join(" ") new_command(:redis).run.join(" ")
assert_equal \ assert_equal \
"docker run --name custom-busybox --detach --restart unless-stopped --log-opt max-size=\"10m\" --env-file .kamal/env/accessories/custom-busybox-secret.env --env-file .kamal/env/accessories/custom-busybox-clear.env --label service=\"custom-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(" ") new_command(:busybox).run.join(" ")
end end
@@ -66,7 +66,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } } @config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
assert_equal \ 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-secret.env --env-file .kamal/env/accessories/custom-busybox-clear.env --label service=\"custom-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(" ") new_command(:busybox).run.join(" ")
end end
@@ -91,7 +91,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
test "execute in new container" do test "execute in new container" do
assert_equal \ assert_equal \
"docker run --rm --env-file .kamal/env/accessories/app-mysql-secret.env --env-file .kamal/env/accessories/app-mysql-clear.env private.registry/mysql:8.0 mysql -u root", "docker run --rm --env-file .kamal/env/accessories/app-mysql.env private.registry/mysql:8.0 mysql -u root",
new_command(:mysql).execute_in_new_container("mysql", "-u", "root").join(" ") new_command(:mysql).execute_in_new_container("mysql", "-u", "root").join(" ")
end end
@@ -103,7 +103,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
test "execute in new container over ssh" do test "execute in new container over ssh" do
new_command(:mysql).stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) do new_command(:mysql).stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) do
assert_match %r|docker run -it --rm --env-file .kamal/env/accessories/app-mysql-secret.env --env-file .kamal/env/accessories/app-mysql-clear.env private.registry/mysql:8.0 mysql -u root|, assert_match %r|docker run -it --rm --env-file .kamal/env/accessories/app-mysql.env private.registry/mysql:8.0 mysql -u root|,
new_command(:mysql).execute_in_new_container_over_ssh("mysql", "-u", "root") new_command(:mysql).execute_in_new_container_over_ssh("mysql", "-u", "root")
end end
end end
@@ -149,8 +149,8 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
assert_equal "mkdir -p .kamal/env/accessories", new_command(:mysql).make_env_directory.join(" ") assert_equal "mkdir -p .kamal/env/accessories", new_command(:mysql).make_env_directory.join(" ")
end end
test "remove_env_files" do test "remove_env_file" do
assert_equal "rm -f .kamal/env/accessories/app-mysql*.env", new_command(:mysql).remove_env_files.join(" ") assert_equal "rm -f .kamal/env/accessories/app-mysql.env", new_command(:mysql).remove_env_file.join(" ")
end end
private private

View File

@@ -14,13 +14,13 @@ class CommandsAppTest < ActiveSupport::TestCase
test "run" do test "run" do
assert_equal \ 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-secret.env --env-file .kamal/env/roles/app-web-clear.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-opt max-size=\"10m\" --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", "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-opt max-size=\"10m\" --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(" ") new_command.run.join(" ")
end end
test "run with hostname" do test "run with hostname" do
assert_equal \ assert_equal \
"docker run --detach --restart unless-stopped --name app-web-999 --hostname myhost -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web-secret.env --env-file .kamal/env/roles/app-web-clear.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-opt max-size=\"10m\" --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", "docker run --detach --restart unless-stopped --name app-web-999 --hostname myhost -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-opt max-size=\"10m\" --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(hostname: "myhost").join(" ") new_command.run(hostname: "myhost").join(" ")
end end
@@ -28,7 +28,7 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:volumes] = ["/local/path:/container/path" ] @config[:volumes] = ["/local/path:/container/path" ]
assert_equal \ 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-secret.env --env-file .kamal/env/roles/app-web-clear.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-opt max-size=\"10m\" --volume /local/path:/container/path --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", "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-opt max-size=\"10m\" --volume /local/path:/container/path --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(" ") new_command.run.join(" ")
end end
@@ -36,7 +36,7 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:healthcheck] = { "path" => "/healthz" } @config[:healthcheck] = { "path" => "/healthz" }
assert_equal \ 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-secret.env --env-file .kamal/env/roles/app-web-clear.env --health-cmd \"(curl -f http://localhost:3000/healthz || 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-opt max-size=\"10m\" --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", "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/healthz || 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-opt max-size=\"10m\" --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(" ") new_command.run.join(" ")
end end
@@ -44,7 +44,7 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:healthcheck] = { "cmd" => "/bin/up" } @config[:healthcheck] = { "cmd" => "/bin/up" }
assert_equal \ 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-secret.env --env-file .kamal/env/roles/app-web-clear.env --health-cmd \"(/bin/up) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --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", "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 \"(/bin/up) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --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(" ") new_command.run.join(" ")
end end
@@ -52,14 +52,14 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "healthcheck" => { "cmd" => "/bin/healthy" } } } @config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "healthcheck" => { "cmd" => "/bin/healthy" } } }
assert_equal \ 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-secret.env --env-file .kamal/env/roles/app-web-clear.env --health-cmd \"(/bin/healthy) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --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", "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 \"(/bin/healthy) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --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(" ") new_command.run.join(" ")
end end
test "run with custom options" do test "run with custom options" do
@config[:servers] = { "web" => [ "1.1.1.1" ], "jobs" => { "hosts" => [ "1.1.1.2" ], "cmd" => "bin/jobs", "options" => { "mount" => "somewhere", "cap-add" => true } } } @config[:servers] = { "web" => [ "1.1.1.1" ], "jobs" => { "hosts" => [ "1.1.1.2" ], "cmd" => "bin/jobs", "options" => { "mount" => "somewhere", "cap-add" => true } } }
assert_equal \ assert_equal \
"docker run --detach --restart unless-stopped --name app-jobs-999 -e KAMAL_CONTAINER_NAME=\"app-jobs-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-jobs-secret.env --env-file .kamal/env/roles/app-jobs-clear.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"jobs\" --mount \"somewhere\" --cap-add dhh/app:999 bin/jobs", "docker run --detach --restart unless-stopped --name app-jobs-999 -e KAMAL_CONTAINER_NAME=\"app-jobs-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-jobs.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"jobs\" --mount \"somewhere\" --cap-add dhh/app:999 bin/jobs",
new_command(role: "jobs").run.join(" ") new_command(role: "jobs").run.join(" ")
end end
@@ -67,7 +67,16 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } } @config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
assert_equal \ 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-secret.env --env-file .kamal/env/roles/app-web-clear.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", "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 "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(" ") new_command.run.join(" ")
end end
@@ -145,25 +154,33 @@ class CommandsAppTest < ActiveSupport::TestCase
test "follow logs" do test "follow logs" do
assert_match \ 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") new_command.follow_logs(host: "app-1")
assert_match \ 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") 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 end
test "execute in new container" do test "execute in new container" do
assert_equal \ assert_equal \
"docker run --rm --env-file .kamal/env/roles/app-web-secret.env --env-file .kamal/env/roles/app-web-clear.env dhh/app:999 bin/rails db:setup", "docker run --rm --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails db:setup",
new_command.execute_in_new_container("bin/rails", "db:setup").join(" ") new_command.execute_in_new_container("bin/rails", "db:setup").join(" ")
end end
test "execute in new container with custom options" do test "execute in new container with custom options" do
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } } @config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } }
assert_equal \ assert_equal \
"docker run --rm --env-file .kamal/env/roles/app-web-secret.env --env-file .kamal/env/roles/app-web-clear.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails db:setup", "docker run --rm --env-file .kamal/env/roles/app-web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails db:setup",
new_command.execute_in_new_container("bin/rails", "db:setup").join(" ") new_command.execute_in_new_container("bin/rails", "db:setup").join(" ")
end end
@@ -174,13 +191,13 @@ class CommandsAppTest < ActiveSupport::TestCase
end end
test "execute in new container over ssh" do test "execute in new container over ssh" do
assert_match %r|docker run -it --rm --env-file .kamal/env/roles/app-web-secret.env --env-file .kamal/env/roles/app-web-clear.env dhh/app:999 bin/rails c|, assert_match %r|docker run -it --rm --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails c|,
new_command.execute_in_new_container_over_ssh("bin/rails", "c", host: "app-1") new_command.execute_in_new_container_over_ssh("bin/rails", "c", host: "app-1")
end end
test "execute in new container with custom options over ssh" do test "execute in new container with custom options over ssh" do
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } } @config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } }
assert_match %r|docker run -it --rm --env-file .kamal/env/roles/app-web-secret.env --env-file .kamal/env/roles/app-web-clear.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c|, assert_match %r|docker run -it --rm --env-file .kamal/env/roles/app-web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c|,
new_command.execute_in_new_container_over_ssh("bin/rails", "c", host: "app-1") new_command.execute_in_new_container_over_ssh("bin/rails", "c", host: "app-1")
end end
@@ -333,7 +350,7 @@ class CommandsAppTest < ActiveSupport::TestCase
end end
test "remove_env_file" do test "remove_env_file" do
assert_equal "rm -f .kamal/env/roles/app-web*.env", new_command.remove_env_files.join(" ") assert_equal "rm -f .kamal/env/roles/app-web.env", new_command.remove_env_file.join(" ")
end end
test "cord" do test "cord" do
@@ -383,6 +400,7 @@ class CommandsAppTest < ActiveSupport::TestCase
private private
def new_command(role: "web", **additional_config) 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
end end

View File

@@ -120,7 +120,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase
end end
test "validate image" do 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 end
private private

View File

@@ -9,7 +9,7 @@ class CommandsDockerTest < ActiveSupport::TestCase
end end
test "install" do 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 end
test "installed?" do test "installed?" do

View File

@@ -10,7 +10,7 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase
test "run" do test "run" do
assert_equal \ assert_equal \
"docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app -e KAMAL_CONTAINER_NAME=\"healthcheck-app\" --env-file .kamal/env/roles/app-web-secret.env --env-file .kamal/env/roles/app-web-clear.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" dhh/app:123", "docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app -e KAMAL_CONTAINER_NAME=\"healthcheck-app\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" dhh/app:123",
new_command.run.join(" ") new_command.run.join(" ")
end end
@@ -18,7 +18,7 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase
@config[:healthcheck] = { "port" => 3001 } @config[:healthcheck] = { "port" => 3001 }
assert_equal \ assert_equal \
"docker run --detach --name healthcheck-app-123 --publish 3999:3001 --label service=healthcheck-app -e KAMAL_CONTAINER_NAME=\"healthcheck-app\" --env-file .kamal/env/roles/app-web-secret.env --env-file .kamal/env/roles/app-web-clear.env --health-cmd \"curl -f http://localhost:3001/up || exit 1\" --health-interval \"1s\" dhh/app:123", "docker run --detach --name healthcheck-app-123 --publish 3999:3001 --label service=healthcheck-app -e KAMAL_CONTAINER_NAME=\"healthcheck-app\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3001/up || exit 1\" --health-interval \"1s\" dhh/app:123",
new_command.run.join(" ") new_command.run.join(" ")
end end
@@ -26,7 +26,7 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase
@destination = "staging" @destination = "staging"
assert_equal \ assert_equal \
"docker run --detach --name healthcheck-app-staging-123 --publish 3999:3000 --label service=healthcheck-app-staging -e KAMAL_CONTAINER_NAME=\"healthcheck-app-staging\" --env-file .kamal/env/roles/app-web-staging-secret.env --env-file .kamal/env/roles/app-web-staging-clear.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" dhh/app:123", "docker run --detach --name healthcheck-app-staging-123 --publish 3999:3000 --label service=healthcheck-app-staging -e KAMAL_CONTAINER_NAME=\"healthcheck-app-staging\" --env-file .kamal/env/roles/app-web-staging.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" dhh/app:123",
new_command.run.join(" ") new_command.run.join(" ")
end end
@@ -34,7 +34,7 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase
@config[:healthcheck] = { "cmd" => "/bin/up" } @config[:healthcheck] = { "cmd" => "/bin/up" }
assert_equal \ assert_equal \
"docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app -e KAMAL_CONTAINER_NAME=\"healthcheck-app\" --env-file .kamal/env/roles/app-web-secret.env --env-file .kamal/env/roles/app-web-clear.env --health-cmd \"/bin/up\" --health-interval \"1s\" dhh/app:123", "docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app -e KAMAL_CONTAINER_NAME=\"healthcheck-app\" --env-file .kamal/env/roles/app-web.env --health-cmd \"/bin/up\" --health-interval \"1s\" dhh/app:123",
new_command.run.join(" ") new_command.run.join(" ")
end end
@@ -42,7 +42,7 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere" } } } @config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere" } } }
@config[:healthcheck] = { "exposed_port" => 4999 } @config[:healthcheck] = { "exposed_port" => 4999 }
assert_equal \ assert_equal \
"docker run --detach --name healthcheck-app-123 --publish 4999:3000 --label service=healthcheck-app -e KAMAL_CONTAINER_NAME=\"healthcheck-app\" --env-file .kamal/env/roles/app-web-secret.env --env-file .kamal/env/roles/app-web-clear.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --mount \"somewhere\" dhh/app:123", "docker run --detach --name healthcheck-app-123 --publish 4999:3000 --label service=healthcheck-app -e KAMAL_CONTAINER_NAME=\"healthcheck-app\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --mount \"somewhere\" dhh/app:123",
new_command.run.join(" ") new_command.run.join(" ")
end end

View File

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

View File

@@ -18,72 +18,72 @@ class CommandsTraefikTest < ActiveSupport::TestCase
test "run" do test "run" do
assert_equal \ assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik-secret.env --env-file .kamal/env/traefik/traefik-clear.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ") new_command.run.join(" ")
@config[:traefik]["host_port"] = "8080" @config[:traefik]["host_port"] = "8080"
assert_equal \ assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 8080:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik-secret.env --env-file .kamal/env/traefik/traefik-clear.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", "docker run --name traefik --detach --restart unless-stopped --publish 8080:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ") new_command.run.join(" ")
@config[:traefik]["publish"] = false @config[:traefik]["publish"] = false
assert_equal \ assert_equal \
"docker run --name traefik --detach --restart unless-stopped --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik-secret.env --env-file .kamal/env/traefik/traefik-clear.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", "docker run --name traefik --detach --restart unless-stopped --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ") new_command.run.join(" ")
end end
test "run with ports configured" do test "run with ports configured" do
assert_equal \ assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik-secret.env --env-file .kamal/env/traefik/traefik-clear.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ") new_command.run.join(" ")
@config[:traefik]["options"] = {"publish" => %w[9000:9000 9001:9001]} @config[:traefik]["options"] = {"publish" => %w[9000:9000 9001:9001]}
assert_equal \ assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik-secret.env --env-file .kamal/env/traefik/traefik-clear.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --publish \"9000:9000\" --publish \"9001:9001\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --publish \"9000:9000\" --publish \"9001:9001\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ") new_command.run.join(" ")
end end
test "run with volumes configured" do test "run with volumes configured" do
assert_equal \ assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik-secret.env --env-file .kamal/env/traefik/traefik-clear.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ") new_command.run.join(" ")
@config[:traefik]["options"] = {"volume" => %w[./letsencrypt/acme.json:/letsencrypt/acme.json] } @config[:traefik]["options"] = {"volume" => %w[./letsencrypt/acme.json:/letsencrypt/acme.json] }
assert_equal \ assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik-secret.env --env-file .kamal/env/traefik/traefik-clear.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ") new_command.run.join(" ")
end end
test "run with several options configured" do test "run with several options configured" do
assert_equal \ assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik-secret.env --env-file .kamal/env/traefik/traefik-clear.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ") new_command.run.join(" ")
@config[:traefik]["options"] = {"volume" => %w[./letsencrypt/acme.json:/letsencrypt/acme.json], "publish" => %w[8080:8080], "memory" => "512m"} @config[:traefik]["options"] = {"volume" => %w[./letsencrypt/acme.json:/letsencrypt/acme.json], "publish" => %w[8080:8080], "memory" => "512m"}
assert_equal \ assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik-secret.env --env-file .kamal/env/traefik/traefik-clear.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" --publish \"8080:8080\" --memory \"512m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" --publish \"8080:8080\" --memory \"512m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ") new_command.run.join(" ")
end end
test "run with labels configured" do test "run with labels configured" do
assert_equal \ assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik-secret.env --env-file .kamal/env/traefik/traefik-clear.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ") new_command.run.join(" ")
@config[:traefik]["labels"] = { "traefik.http.routers.dashboard.service" => "api@internal", "traefik.http.routers.dashboard.middlewares" => "auth" } @config[:traefik]["labels"] = { "traefik.http.routers.dashboard.service" => "api@internal", "traefik.http.routers.dashboard.middlewares" => "auth" }
assert_equal \ assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik-secret.env --env-file .kamal/env/traefik/traefik-clear.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --label traefik.http.routers.dashboard.service=\"api@internal\" --label traefik.http.routers.dashboard.middlewares=\"auth\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --label traefik.http.routers.dashboard.service=\"api@internal\" --label traefik.http.routers.dashboard.middlewares=\"auth\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ") new_command.run.join(" ")
end end
test "run with env configured" do test "run with env configured" do
assert_equal \ assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik-secret.env --env-file .kamal/env/traefik/traefik-clear.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ") new_command.run.join(" ")
@config[:traefik]["env"] = { "secret" => %w[EXAMPLE_API_KEY] } @config[:traefik]["env"] = { "secret" => %w[EXAMPLE_API_KEY] }
assert_equal \ assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik-secret.env --env-file .kamal/env/traefik/traefik-clear.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ") new_command.run.join(" ")
end end
@@ -91,7 +91,7 @@ class CommandsTraefikTest < ActiveSupport::TestCase
@config.delete(:traefik) @config.delete(:traefik)
assert_equal \ assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik-secret.env --env-file .kamal/env/traefik/traefik-clear.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"",
new_command.run.join(" ") new_command.run.join(" ")
end end
@@ -99,7 +99,7 @@ class CommandsTraefikTest < ActiveSupport::TestCase
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } } @config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
assert_equal \ assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik-secret.env --env-file .kamal/env/traefik/traefik-clear.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ") new_command.run.join(" ")
end end
@@ -107,7 +107,7 @@ class CommandsTraefikTest < ActiveSupport::TestCase
@config[:traefik]["args"]["log.level"] = "ERROR" @config[:traefik]["args"]["log.level"] = "ERROR"
assert_equal \ assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik-secret.env --env-file .kamal/env/traefik/traefik-clear.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"ERROR\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"ERROR\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
new_command.run.join(" ") new_command.run.join(" ")
end end
@@ -180,23 +180,19 @@ class CommandsTraefikTest < ActiveSupport::TestCase
test "env_file" do test "env_file" do
@config[:traefik]["env"] = { "secret" => %w[EXAMPLE_API_KEY] } @config[:traefik]["env"] = { "secret" => %w[EXAMPLE_API_KEY] }
assert_equal "EXAMPLE_API_KEY=456\n", new_command.env_file.secret assert_equal "EXAMPLE_API_KEY=456\n", new_command.env_file.to_s
end end
test "host_secret_env_file_path" do test "host_env_file_path" do
assert_equal ".kamal/env/traefik/traefik-secret.env", new_command.host_secret_env_file_path assert_equal ".kamal/env/traefik/traefik.env", new_command.host_env_file_path
end
test "host_clear_env_file_path" do
assert_equal ".kamal/env/traefik/traefik-clear.env", new_command.host_clear_env_file_path
end end
test "make_env_directory" do test "make_env_directory" do
assert_equal "mkdir -p .kamal/env/traefik", new_command.make_env_directory.join(" ") assert_equal "mkdir -p .kamal/env/traefik", new_command.make_env_directory.join(" ")
end end
test "remove_env_files" do test "remove_env_file" do
assert_equal "rm -f .kamal/env/traefik/traefik*.env", new_command.remove_env_files.join(" ") assert_equal "rm -f .kamal/env/traefik/traefik.env", new_command.remove_env_file.join(" ")
end end
private private

View File

@@ -113,27 +113,19 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
end end
test "env args" do test "env args" do
assert_equal \ assert_equal ["--env-file", ".kamal/env/accessories/app-mysql.env"], @config.accessory(:mysql).env_args
["--env-file", ".kamal/env/accessories/app-mysql-secret.env", "--env-file", ".kamal/env/accessories/app-mysql-clear.env"], assert_equal ["--env-file", ".kamal/env/accessories/app-redis.env"], @config.accessory(:redis).env_args
@config.accessory(:mysql).env_args
assert_equal \
["--env-file", ".kamal/env/accessories/app-redis-secret.env", "--env-file", ".kamal/env/accessories/app-redis-clear.env"],
@config.accessory(:redis).env_args
end end
test "env file with secret" do test "env file with secret" do
ENV["MYSQL_ROOT_PASSWORD"] = "secret123" ENV["MYSQL_ROOT_PASSWORD"] = "secret123"
expected_secret = <<~ENV expected = <<~ENV
MYSQL_ROOT_PASSWORD=secret123 MYSQL_ROOT_PASSWORD=secret123
ENV
expected_clear = <<~ENV
MYSQL_ROOT_HOST=% MYSQL_ROOT_HOST=%
ENV ENV
assert_equal expected_secret, @config.accessory(:mysql).env_file.secret assert_equal expected, @config.accessory(:mysql).env_file.to_s
assert_equal expected_clear, @config.accessory(:mysql).env_file.clear
ensure ensure
ENV["MYSQL_ROOT_PASSWORD"] = nil ENV["MYSQL_ROOT_PASSWORD"] = nil
end end
@@ -142,12 +134,8 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
assert_equal ".kamal/env/accessories", @config.accessory(:mysql).host_env_directory assert_equal ".kamal/env/accessories", @config.accessory(:mysql).host_env_directory
end end
test "host_secret_env_file_path" do test "host_env_file_path" do
assert_equal ".kamal/env/accessories/app-mysql-secret.env", @config.accessory(:mysql).host_secret_env_file_path assert_equal ".kamal/env/accessories/app-mysql.env", @config.accessory(:mysql).host_env_file_path
end
test "host_clear_env_file_path" do
assert_equal ".kamal/env/accessories/app-mysql-clear.env", @config.accessory(:mysql).host_clear_env_file_path
end end
test "volume args" do test "volume args" do

View File

@@ -77,8 +77,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
WEB_CONCURRENCY=4 WEB_CONCURRENCY=4
ENV ENV
assert_equal "\n", @config_with_roles.role(:workers).env_file.secret assert_equal expected_env, @config_with_roles.role(:workers).env_file.to_s
assert_equal expected_env, @config_with_roles.role(:workers).env_file.clear
end end
test "container name" do test "container name" do
@@ -91,9 +90,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
end end
test "env args" do test "env args" do
assert_equal \ assert_equal ["--env-file", ".kamal/env/roles/app-workers.env"], @config_with_roles.role(:workers).env_args
["--env-file", ".kamal/env/roles/app-workers-secret.env", "--env-file", ".kamal/env/roles/app-workers-clear.env"],
@config_with_roles.role(:workers).env_args
end end
test "env secret overwritten by role" do test "env secret overwritten by role" do
@@ -119,18 +116,14 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
ENV["REDIS_PASSWORD"] = "secret456" ENV["REDIS_PASSWORD"] = "secret456"
ENV["DB_PASSWORD"] = "secret&\"123" ENV["DB_PASSWORD"] = "secret&\"123"
expected_secret = <<~ENV expected = <<~ENV
REDIS_PASSWORD=secret456 REDIS_PASSWORD=secret456
DB_PASSWORD=secret&\"123 DB_PASSWORD=secret&\"123
ENV
expected_clear = <<~ENV
REDIS_URL=redis://a/b REDIS_URL=redis://a/b
WEB_CONCURRENCY=4 WEB_CONCURRENCY=4
ENV ENV
assert_equal expected_secret, @config_with_roles.role(:workers).env_file.secret assert_equal expected, @config_with_roles.role(:workers).env_file.to_s
assert_equal expected_clear, @config_with_roles.role(:workers).env_file.clear
ensure ensure
ENV["REDIS_PASSWORD"] = nil ENV["REDIS_PASSWORD"] = nil
ENV["DB_PASSWORD"] = nil ENV["DB_PASSWORD"] = nil
@@ -149,17 +142,13 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
ENV["DB_PASSWORD"] = "secret123" ENV["DB_PASSWORD"] = "secret123"
expected_secret = <<~ENV expected = <<~ENV
DB_PASSWORD=secret123 DB_PASSWORD=secret123
ENV
expected_clear = <<~ENV
REDIS_URL=redis://a/b REDIS_URL=redis://a/b
WEB_CONCURRENCY=4 WEB_CONCURRENCY=4
ENV ENV
assert_equal expected_secret, @config_with_roles.role(:workers).env_file.secret assert_equal expected, @config_with_roles.role(:workers).env_file.to_s
assert_equal expected_clear, @config_with_roles.role(:workers).env_file.clear
ensure ensure
ENV["DB_PASSWORD"] = nil ENV["DB_PASSWORD"] = nil
end end
@@ -176,17 +165,13 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
ENV["REDIS_PASSWORD"] = "secret456" ENV["REDIS_PASSWORD"] = "secret456"
expected_secret = <<~ENV expected = <<~ENV
REDIS_PASSWORD=secret456 REDIS_PASSWORD=secret456
ENV
expected_clear = <<~ENV
REDIS_URL=redis://a/b REDIS_URL=redis://a/b
WEB_CONCURRENCY=4 WEB_CONCURRENCY=4
ENV ENV
assert_equal expected_secret, @config_with_roles.role(:workers).env_file.secret assert_equal expected, @config_with_roles.role(:workers).env_file.to_s
assert_equal expected_clear, @config_with_roles.role(:workers).env_file.clear
ensure ensure
ENV["REDIS_PASSWORD"] = nil ENV["REDIS_PASSWORD"] = nil
end end
@@ -209,16 +194,12 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
ENV["REDIS_PASSWORD"] = "secret456" ENV["REDIS_PASSWORD"] = "secret456"
expected_secret = <<~ENV expected = <<~ENV
REDIS_PASSWORD=secret456 REDIS_PASSWORD=secret456
ENV
expected_clear = <<~ENV
REDIS_URL=redis://c/d REDIS_URL=redis://c/d
ENV ENV
assert_equal expected_secret, @config_with_roles.role(:workers).env_file.secret assert_equal expected, @config_with_roles.role(:workers).env_file.to_s
assert_equal expected_clear, @config_with_roles.role(:workers).env_file.clear
ensure ensure
ENV["REDIS_PASSWORD"] = nil ENV["REDIS_PASSWORD"] = nil
end end
@@ -227,12 +208,8 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
assert_equal ".kamal/env/roles", @config_with_roles.role(:workers).host_env_directory assert_equal ".kamal/env/roles", @config_with_roles.role(:workers).host_env_directory
end end
test "host_secret_env_file_path" do test "host_env_file_path" do
assert_equal ".kamal/env/roles/app-workers-secret.env", @config_with_roles.role(:workers).host_secret_env_file_path assert_equal ".kamal/env/roles/app-workers.env", @config_with_roles.role(:workers).host_env_file_path
end
test "host_clear_env_file_path" do
assert_equal ".kamal/env/roles/app-workers-clear.env", @config_with_roles.role(:workers).host_clear_env_file_path
end end
test "uses cord" do test "uses cord" do

View File

@@ -300,14 +300,14 @@ class ConfigurationTest < ActiveSupport::TestCase
end end
test "primary role" do 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({ config = Kamal::Configuration.new(@deploy_with_roles.deep_merge({
servers: { "alternate_web" => { "hosts" => [ "1.1.1.4", "1.1.1.5" ] } }, servers: { "alternate_web" => { "hosts" => [ "1.1.1.4", "1.1.1.5" ] } },
primary_role: "alternate_web" } )) 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_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? assert config.role(:alternate_web).running_traefik?
@@ -327,13 +327,4 @@ class ConfigurationTest < ActiveSupport::TestCase
assert_raises(ArgumentError) { Kamal::Configuration.new(@deploy_with_roles.merge(retain_containers: 0)) } assert_raises(ArgumentError) { Kamal::Configuration.new(@deploy_with_roles.merge(retain_containers: 0)) }
end end
test "push_env" do
assert_nil @config.push_env
assert_equal "all", Kamal::Configuration.new(@deploy.merge(push_env: "all")).push_env
assert_equal "clear", Kamal::Configuration.new(@deploy.merge(push_env: "clear")).push_env
assert_equal "secret", Kamal::Configuration.new(@deploy.merge(push_env: "secret")).push_env
assert_raises(ArgumentError) { Kamal::Configuration.new(@deploy_with_roles.merge(push_env: "foo")) }
end
end end

View File

@@ -1,14 +1,14 @@
require "test_helper" require "test_helper"
class EnvFilesTest < ActiveSupport::TestCase class EnvFileTest < ActiveSupport::TestCase
test "env file simple" do test "env file simple" do
env = { env = {
"foo" => "bar", "foo" => "bar",
"baz" => "haz" "baz" => "haz"
} }
assert_equal "foo=bar\nbaz=haz\n", Kamal::EnvFiles.new(env).clear assert_equal "foo=bar\nbaz=haz\n", \
assert_equal "\n", Kamal::EnvFiles.new(env).secret Kamal::EnvFile.new(env).to_s
end end
test "env file clear" do test "env file clear" do
@@ -19,13 +19,12 @@ class EnvFilesTest < ActiveSupport::TestCase
} }
} }
assert_equal "foo=bar\nbaz=haz\n", Kamal::EnvFiles.new(env).clear assert_equal "foo=bar\nbaz=haz\n", \
assert_equal "\n", Kamal::EnvFiles.new(env).secret Kamal::EnvFile.new(env).to_s
end end
test "env file empty" do test "env file empty" do
assert_equal "\n", Kamal::EnvFiles.new({}).secret assert_equal "\n", Kamal::EnvFile.new({}).to_s
assert_equal "\n", Kamal::EnvFiles.new({}).clear
end end
test "env file secret" do test "env file secret" do
@@ -34,8 +33,8 @@ class EnvFilesTest < ActiveSupport::TestCase
"secret" => [ "PASSWORD" ] "secret" => [ "PASSWORD" ]
} }
assert_equal "PASSWORD=hello\n", Kamal::EnvFiles.new(env).secret assert_equal "PASSWORD=hello\n", \
assert_equal "\n", Kamal::EnvFiles.new(env).clear Kamal::EnvFile.new(env).to_s
ensure ensure
ENV.delete "PASSWORD" ENV.delete "PASSWORD"
end end
@@ -46,7 +45,8 @@ class EnvFilesTest < ActiveSupport::TestCase
"secret" => [ "PASSWORD" ] "secret" => [ "PASSWORD" ]
} }
assert_equal "PASSWORD=hello\\\\nthere\n", Kamal::EnvFiles.new(env).secret assert_equal "PASSWORD=hello\\\\nthere\n", \
Kamal::EnvFile.new(env).to_s
ensure ensure
ENV.delete "PASSWORD" ENV.delete "PASSWORD"
end end
@@ -57,7 +57,8 @@ class EnvFilesTest < ActiveSupport::TestCase
"secret" => [ "PASSWORD" ] "secret" => [ "PASSWORD" ]
} }
assert_equal "PASSWORD=hello\\nthere\n", Kamal::EnvFiles.new(env).secret assert_equal "PASSWORD=hello\\nthere\n", \
Kamal::EnvFile.new(env).to_s
ensure ensure
ENV.delete "PASSWORD" ENV.delete "PASSWORD"
end end
@@ -67,7 +68,7 @@ class EnvFilesTest < ActiveSupport::TestCase
"secret" => [ "PASSWORD" ] "secret" => [ "PASSWORD" ]
} }
assert_raises(KeyError) { Kamal::EnvFiles.new(env).secret } assert_raises(KeyError) { Kamal::EnvFile.new(env).to_s }
ensure ensure
ENV.delete "PASSWORD" ENV.delete "PASSWORD"
@@ -83,9 +84,8 @@ class EnvFilesTest < ActiveSupport::TestCase
} }
} }
assert_equal "PASSWORD=hello\n", Kamal::EnvFiles.new(env).secret assert_equal "PASSWORD=hello\nfoo=bar\nbaz=haz\n", \
assert_equal "foo=bar\nbaz=haz\n", Kamal::EnvFiles.new(env).clear Kamal::EnvFile.new(env).to_s
ensure ensure
ENV.delete "PASSWORD" ENV.delete "PASSWORD"
end end
@@ -97,6 +97,6 @@ class EnvFilesTest < ActiveSupport::TestCase
} }
assert_equal "foo=bar\nbaz=haz\n", \ assert_equal "foo=bar\nbaz=haz\n", \
StringIO.new(Kamal::EnvFiles.new(env).clear).read StringIO.new(Kamal::EnvFile.new(env)).read
end end
end end

View File

@@ -1,37 +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
push_env: clear
accessories:
mysql:
image: mysql:5.7
host: 1.1.1.3
port: 3306
env:
clear:
MYSQL_ROOT_HOST: '%'
secret:
- MYSQL_ROOT_PASSWORD
files:
- test/fixtures/files/my.cnf:/etc/mysql/my.cnf
directories:
- data:/var/lib/mysql
redis:
image: redis:latest
roles:
- web
port: 6379
directories:
- data:/data
readiness_delay: 0

View File

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

View File

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

View File

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

View File

@@ -33,4 +33,3 @@ accessories:
roles: roles:
- web - web
stop_wait_time: 1 stop_wait_time: 1
push_env: clear

View File

@@ -10,7 +10,7 @@ class LockTest < IntegrationTest
assert_match /Locked by: Deployer at .*\nVersion: #{latest_app_version}\nMessage: Integration Tests/m, status assert_match /Locked by: Deployer at .*\nVersion: #{latest_app_version}\nMessage: Integration Tests/m, status
error = kamal :deploy, capture: true, raise_on_error: false 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 kamal :lock, :release

View File

@@ -4,10 +4,8 @@ class MainTest < IntegrationTest
test "envify, deploy, redeploy, rollback, details and audit" do test "envify, deploy, redeploy, rollback, details and audit" do
kamal :envify kamal :envify
assert_local_env_file "SECRET_TOKEN=1234" assert_local_env_file "SECRET_TOKEN=1234"
assert_remote_env_file "CLEAR_TOKEN=4321", :clear assert_remote_env_file "SECRET_TOKEN=1234\nCLEAR_TOKEN=4321"
assert_remote_env_file "SECRET_TOKEN=1234", :secret
remove_local_env_file remove_local_env_file
remove_remote_env_file :clear
first_version = latest_app_version first_version = latest_app_version
@@ -16,7 +14,6 @@ class MainTest < IntegrationTest
kamal :deploy kamal :deploy
assert_app_is_up version: first_version assert_app_is_up version: first_version
assert_hooks_ran "pre-connect", "pre-build", "pre-deploy", "post-deploy" assert_hooks_ran "pre-connect", "pre-build", "pre-deploy", "post-deploy"
assert_remote_env_file "CLEAR_TOKEN=4321", :clear
second_version = update_app_rev second_version = update_app_rev
@@ -42,7 +39,7 @@ class MainTest < IntegrationTest
assert_match /Booted app version #{first_version}.*Booted app version #{second_version}.*Booted app version #{first_version}.*/m, audit assert_match /Booted app version #{first_version}.*Booted app version #{second_version}.*Booted app version #{first_version}.*/m, audit
kamal :env, :delete kamal :env, :delete
assert_no_remote_env_files assert_no_remote_env_file
end end
test "config" do test "config" do
@@ -63,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]) 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 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 private
def assert_local_env_file(contents) def assert_local_env_file(contents)
assert_equal contents, deployer_exec("cat .env", capture: true) assert_equal contents, deployer_exec("cat .env", capture: true)
@@ -72,17 +82,12 @@ class MainTest < IntegrationTest
deployer_exec("rm .env") deployer_exec("rm .env")
end end
def remove_remote_env_file(env_type) def assert_remote_env_file(contents)
docker_compose("exec vm1 cat /root/.kamal/env/roles/app-web-#{env_type}.env") assert_equal contents, docker_compose("exec vm1 cat /root/.kamal/env/roles/app-web.env", capture: true)
end end
def assert_remote_env_file(contents, env_type) def assert_no_remote_env_file
assert_equal contents, docker_compose("exec vm1 cat /root/.kamal/env/roles/app-web-#{env_type}.env", capture: true) assert_equal "nofile", docker_compose("exec vm1 stat /root/.kamal/env/roles/app-web.env 2> /dev/null || echo nofile", capture: true)
end
def assert_no_remote_env_files
assert_equal "nofile", docker_compose("exec vm1 stat /root/.kamal/env/roles/app-web-clear.env 2> /dev/null || echo nofile", capture: true)
assert_equal "nofile", docker_compose("exec vm1 stat /root/.kamal/env/roles/app-web-secret.env 2> /dev/null || echo nofile", capture: true)
end end
def assert_accumulated_assets(*versions) def assert_accumulated_assets(*versions)
@@ -92,4 +97,22 @@ class MainTest < IntegrationTest
assert_equal "200", Net::HTTP.get_response(URI.parse("http://localhost:12345/versions/.hidden")).code assert_equal "200", Net::HTTP.get_response(URI.parse("http://localhost:12345/versions/.hidden")).code
end 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 end