Compare commits

..

3 Commits

Author SHA1 Message Date
Donal McBreen
fd6ef21b09 Merge branch 'main' into auto-push-env 2024-03-06 15:33:47 +00:00
Donal McBreen
49ce64de87 Add push_env config
This setting allows you to automatically push env files when deploying.
The default is not to push any files, but you can set it to `all`,
`clear` or `secret` to push the relevant files.

The most useful setting is `clear` which will push the clear env files
every time you deploy.

In addition you can choose the env_type to push when calling
`kamal env push` directly:

```
kamal env push --env-type clear
kamal env push --env-type secret
kamal env push --env-type all # same as kamal env push
```
2024-03-06 15:12:44 +00:00
Donal McBreen
1fa25200cc Split env into separate secrets/clear envs
Split each env file in two on the deploy hosts, one for secrets and
one for clear values. This will allow us to update them independently.
2024-03-05 15:49:55 +00:00
107 changed files with 1068 additions and 1495 deletions

View File

@@ -5,21 +5,6 @@ on:
- main
pull_request:
jobs:
rubocop:
name: RuboCop
runs-on: ubuntu-latest
env:
BUNDLE_ONLY: rubocop
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Ruby and install gems
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3.0
bundler-cache: true
- name: Run Rubocop
run: bundle exec rubocop --parallel
tests:
strategy:
matrix:

View File

@@ -1,2 +0,0 @@
inherit_gem:
rubocop-rails-omakase: rubocop.yml

View File

@@ -1,8 +1,4 @@
source "https://rubygems.org"
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gemspec
group :rubocop do
gem "rubocop-rails-omakase", require: false
end

View File

@@ -1,7 +1,7 @@
PATH
remote: .
specs:
kamal (1.5.0)
kamal (1.3.1)
activesupport (>= 7.0)
base64 (~> 0.2)
bcrypt_pbkdf (~> 1.0)
@@ -42,7 +42,6 @@ GEM
minitest (>= 5.1)
mutex_m
tzinfo (~> 2.0)
ast (2.4.2)
base64 (0.2.0)
bcrypt_pbkdf (1.1.0)
bigdecimal (3.1.5)
@@ -64,8 +63,6 @@ GEM
irb (1.11.0)
rdoc
reline (>= 0.3.8)
json (2.7.1)
language_server-protocol (3.17.0.3)
loofah (2.22.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
@@ -82,10 +79,6 @@ GEM
racc (~> 1.4)
nokogiri (1.16.0-x86_64-linux)
racc (~> 1.4)
parallel (1.24.0)
parser (3.3.0.5)
ast (~> 2.4.1)
racc
psych (5.1.2)
stringio
racc (1.7.3)
@@ -112,44 +105,11 @@ GEM
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.1.0)
rdoc (6.6.2)
psych (>= 4.0.0)
regexp_parser (2.9.0)
reline (0.4.2)
io-console (~> 0.5)
rexml (3.2.6)
rubocop (1.62.1)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.31.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.31.2)
parser (>= 3.3.0.4)
rubocop-minitest (0.35.0)
rubocop (>= 1.61, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-performance (1.20.2)
rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.30.0, < 2.0)
rubocop-rails (2.24.0)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails-omakase (1.0.0)
rubocop
rubocop-minitest
rubocop-performance
rubocop-rails
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
sshkit (1.21.7)
mutex_m
@@ -159,7 +119,6 @@ GEM
thor (1.3.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.5.0)
webrick (1.8.1)
zeitwerk (2.6.12)
@@ -173,7 +132,6 @@ DEPENDENCIES
kamal!
mocha
railties
rubocop-rails-omakase
BUNDLED WITH
2.4.3

View File

@@ -5,6 +5,6 @@ require "active_support"
require "zeitwerk"
loader = Zeitwerk::Loader.for_gem
loader.ignore(File.join(__dir__, "kamal", "sshkit_with_ext.rb"))
loader.ignore("#{__dir__}/kamal/sshkit_with_ext.rb")
loader.setup
loader.eager_load # We need all commands loaded.

View File

@@ -177,7 +177,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
if name == "all"
KAMAL.accessory_names.each { |accessory_name| remove(accessory_name) }
else
confirming "This will remove all containers, images and data directories for #{name}. Are you sure?" do
if options[:confirmed] || ask("This will remove all containers, images and data directories for #{name}. Are you sure?", limited_to: %w( y N ), default: "N") == "y"
with_accessory(name) do
stop(name)
remove_container(name)
@@ -226,8 +226,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
private
def with_accessory(name)
if KAMAL.config.accessory(name)
accessory = KAMAL.accessory(name)
if accessory = KAMAL.accessory(name)
yield accessory, accessory_hosts(accessory)
else
error_on_missing_accessory(name)

View File

@@ -7,22 +7,59 @@ class Kamal::Cli::App < Kamal::Cli::Base
using_version(version_or_latest) do |version|
say "Start container with version #{version} using a #{KAMAL.config.readiness_delay}s readiness delay (or reboot if already running)...", :magenta
# Assets are prepared in a separate step to ensure they are on all hosts before booting
on(KAMAL.hosts) do
execute *KAMAL.auditor.record("Tagging #{KAMAL.config.absolute_image} as the latest image"), verbosity: :debug
execute *KAMAL.app.tag_current_image_as_latest
KAMAL.roles_on(host).each do |role|
Kamal::Cli::App::PrepareAssets.new(host, role, self).run
app = KAMAL.app(role: role)
role_config = KAMAL.config.role(role)
if role_config.assets?
execute *app.extract_assets
old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
execute *app.sync_asset_volumes(old_version: old_version)
end
end
end
on(KAMAL.hosts, **KAMAL.boot_strategy) do |host|
KAMAL.roles_on(host).each do |role|
Kamal::Cli::App::Boot.new(host, role, version, self).run
app = KAMAL.app(role: role)
auditor = KAMAL.auditor(role: role)
role_config = KAMAL.config.role(role)
if capture_with_info(*app.container_id_for_version(version), raise_on_non_zero_exit: false).present?
tmp_version = "#{version}_replaced_#{SecureRandom.hex(8)}"
info "Renaming container #{version} to #{tmp_version} as already deployed on #{host}"
execute *auditor.record("Renaming container #{version} to #{tmp_version}"), verbosity: :debug
execute *app.rename_container(version: version, new_version: tmp_version)
end
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 *auditor.record("Booted app version #{version}"), verbosity: :debug
execute *app.run(hostname: "#{host}-#{SecureRandom.hex(6)}")
Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
if old_version.present?
if role_config.uses_cord?
cord = capture_with_info(*app.cord(version: old_version), raise_on_non_zero_exit: false).strip
if cord.present?
execute *app.cut_cord(cord)
Kamal::Cli::Healthcheck::Poller.wait_for_unhealthy(pause_after_ready: true) { capture_with_info(*app.status(version: old_version)) }
end
end
on(KAMAL.hosts) do |host|
execute *KAMAL.auditor.record("Tagging #{KAMAL.config.absolute_image} as the latest image"), verbosity: :debug
execute *KAMAL.app.tag_latest_image
execute *app.stop(version: old_version), raise_on_non_zero_exit: false
execute *app.clean_up_assets if role_config.assets?
end
end
end
end
end
@@ -72,15 +109,13 @@ class Kamal::Cli::App < Kamal::Cli::Base
desc "exec [CMD]", "Execute a custom command on servers (use --help to show options)"
option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
option :env, aliases: "-e", type: :hash, desc: "Set environment variables for the command"
def exec(cmd)
env = options[:env]
case
when options[:interactive] && options[:reuse]
say "Get current version of running container...", :magenta unless options[:version]
using_version(options[:version] || current_running_version) do |version|
say "Launching interactive command with version #{version} via SSH from existing container on #{KAMAL.primary_host}...", :magenta
run_locally { exec KAMAL.app(role: KAMAL.primary_role).execute_in_existing_container_over_ssh(cmd, host: KAMAL.primary_host, env: env) }
run_locally { exec KAMAL.app(role: KAMAL.primary_role).execute_in_existing_container_over_ssh(cmd, host: KAMAL.primary_host) }
end
when options[:interactive]
@@ -88,7 +123,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
using_version(version_or_latest) do |version|
say "Launching interactive command with version #{version} via SSH from new container on #{KAMAL.primary_host}...", :magenta
run_locally do
exec KAMAL.app(role: KAMAL.primary_role).execute_in_new_container_over_ssh(cmd, host: KAMAL.primary_host, env: env)
exec KAMAL.app(role: KAMAL.primary_role).execute_in_new_container_over_ssh(cmd, host: KAMAL.primary_host)
end
end
@@ -102,7 +137,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
roles.each do |role|
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}", role: role), verbosity: :debug
puts_by_host host, capture_with_info(*KAMAL.app(role: role).execute_in_existing_container(cmd, env: env))
puts_by_host host, capture_with_info(*KAMAL.app(role: role).execute_in_existing_container(cmd))
end
end
end
@@ -116,7 +151,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
roles.each do |role|
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
puts_by_host host, capture_with_info(*KAMAL.app(role: role).execute_in_new_container(cmd, env: env))
puts_by_host host, capture_with_info(*KAMAL.app(role: role).execute_in_new_container(cmd))
end
end
end
@@ -140,10 +175,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
roles = KAMAL.roles_on(host)
roles.each do |role|
versions = capture_with_info(*KAMAL.app(role: role).list_versions, raise_on_non_zero_exit: false).split("\n")
versions -= [ capture_with_info(*KAMAL.app(role: role).current_running_version, raise_on_non_zero_exit: false).strip ]
versions.each do |version|
cli.send(:stale_versions, host: host, role: role).each do |version|
if stop
puts_by_host host, "Stopping stale container for role #{role} with version #{version}"
execute *KAMAL.app(role: role).stop(version: version), raise_on_non_zero_exit: false
@@ -170,20 +202,19 @@ class Kamal::Cli::App < Kamal::Cli::Base
# FIXME: Catch when app containers aren't running
grep = options[:grep]
since = options[:since]
if options[:follow]
lines = options[:lines].presence || ((since || grep) ? nil : 10) # Default to 10 lines if since or grep isn't set
if options[:follow]
run_locally do
info "Following logs on #{KAMAL.primary_host}..."
KAMAL.specific_roles ||= ["web"]
role = KAMAL.roles_on(KAMAL.primary_host).first
info KAMAL.app(role: role).follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep)
exec KAMAL.app(role: role).follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep)
info KAMAL.app(role: role).follow_logs(host: KAMAL.primary_host, grep: grep)
exec KAMAL.app(role: role).follow_logs(host: KAMAL.primary_host, grep: grep)
end
else
since = options[:since]
lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
on(KAMAL.hosts) do |host|
@@ -279,7 +310,18 @@ class Kamal::Cli::App < Kamal::Cli::Base
version.presence
end
def stale_versions(host:, role:)
versions = nil
on(host) do
versions = \
capture_with_info(*KAMAL.app(role: role).list_versions, raise_on_non_zero_exit: false)
.split("\n")
.drop(1)
end
versions
end
def version_or_latest
options[:version] || KAMAL.config.latest_tag
options[:version] || "latest"
end
end

View File

@@ -1,67 +0,0 @@
class Kamal::Cli::App::Boot
attr_reader :host, :role, :version, :sshkit
delegate :execute, :capture_with_info, :info, to: :sshkit
delegate :uses_cord?, :assets?, to: :role
def initialize(host, role, version, sshkit)
@host = host
@role = role
@version = version
@sshkit = sshkit
end
def run
old_version = old_version_renamed_if_clashing
start_new_version
if old_version
stop_old_version(old_version)
end
end
private
def app
@app ||= KAMAL.app(role: role)
end
def auditor
@auditor = KAMAL.auditor(role: role)
end
def audit(message)
execute *auditor.record(message), verbosity: :debug
end
def old_version_renamed_if_clashing
if capture_with_info(*app.container_id_for_version(version), raise_on_non_zero_exit: false).present?
renamed_version = "#{version}_replaced_#{SecureRandom.hex(8)}"
info "Renaming container #{version} to #{renamed_version} as already deployed on #{host}"
audit("Renaming container #{version} to #{renamed_version}")
execute *app.rename_container(version: version, new_version: renamed_version)
end
capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip.presence
end
def start_new_version
audit "Booted app version #{version}"
execute *app.tie_cord(role.cord_host_file) if uses_cord?
execute *app.run(hostname: "#{host}-#{SecureRandom.hex(6)}")
Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
end
def stop_old_version(version)
if uses_cord?
cord = capture_with_info(*app.cord(version: version), raise_on_non_zero_exit: false).strip
if cord.present?
execute *app.cut_cord(cord)
Kamal::Cli::Healthcheck::Poller.wait_for_unhealthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
end
end
execute *app.stop(version: version), raise_on_non_zero_exit: false
execute *app.clean_up_assets if assets?
end
end

View File

@@ -1,24 +0,0 @@
class Kamal::Cli::App::PrepareAssets
attr_reader :host, :role, :sshkit
delegate :execute, :capture_with_info, :info, to: :sshkit
delegate :assets?, to: :role
def initialize(host, role, sshkit)
@host = host
@role = role
@sshkit = sshkit
end
def run
if assets?
execute *app.extract_assets
old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
execute *app.sync_asset_volumes(old_version: old_version)
end
end
private
def app
@app ||= KAMAL.app(role: role)
end
end

View File

@@ -73,7 +73,7 @@ module Kamal::Cli
def print_runtime
started_at = Time.now
yield
Time.now - started_at
return Time.now - started_at
ensure
runtime = Time.now - started_at
puts " Finished all in #{sprintf("%.1f seconds", runtime)}"
@@ -84,7 +84,7 @@ module Kamal::Cli
run_hook "pre-connect"
ensure_run_and_locks_directory
ensure_run_directory
acquire_lock
@@ -103,16 +103,6 @@ module Kamal::Cli
release_lock
end
def confirming(question)
return yield if options[:confirmed]
if ask(question, limited_to: %w[ y N ], default: "N") == "y"
yield
else
say "Aborted", :red
end
end
def acquire_lock
raise_if_locked do
say "Acquiring the deploy lock...", :magenta
@@ -133,9 +123,8 @@ module Kamal::Cli
yield
rescue SSHKit::Runner::ExecuteError => e
if e.message =~ /cannot create directory/
say "Deploy lock already in place!", :red
on(KAMAL.primary_host) { puts capture_with_debug(*KAMAL.lock.status) }
raise LockError, "Deploy lock found. Run 'kamal lock help' for more information"
raise LockError, "Deploy lock found"
else
raise e
end
@@ -157,9 +146,9 @@ module Kamal::Cli
say "Running the #{hook} hook...", :magenta
run_locally do
execute *KAMAL.hook.run(hook, **details, **extra_details)
rescue SSHKit::Command::Failed => e
raise HookError.new("Hook `#{hook}` failed:\n#{e.message}")
KAMAL.with_verbosity(:debug) { execute *KAMAL.hook.run(hook, **details, **extra_details) }
rescue SSHKit::Command::Failed
raise HookError.new("Hook `#{hook}` failed")
end
end
end
@@ -186,14 +175,10 @@ module Kamal::Cli
instance_variable_get("@_invocations").first
end
def ensure_run_and_locks_directory
def ensure_run_directory
on(KAMAL.hosts) do
execute(*KAMAL.server.ensure_run_directory)
end
on(KAMAL.primary_host) do
execute(*KAMAL.lock.ensure_locks_directory)
end
end
end
end

View File

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

View File

@@ -1,18 +1,15 @@
class Kamal::Cli::Main < Kamal::Cli::Base
desc "setup", "Setup all accessories, push the env, and deploy app to servers"
option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
def setup
print_runtime do
mutating do
invoke_options = deploy_options
say "Ensure Docker is installed...", :magenta
invoke "kamal:cli:server:bootstrap", [], invoke_options
invoke "kamal:cli:server:bootstrap"
say "Push env files...", :magenta
invoke "kamal:cli:env:push", [], invoke_options
invoke "kamal:cli:env:push"
invoke "kamal:cli:accessory:boot", [ "all" ], invoke_options
invoke "kamal:cli:accessory:boot", [ "all" ]
deploy
end
end
@@ -38,6 +35,8 @@ class Kamal::Cli::Main < Kamal::Cli::Base
run_hook "pre-deploy"
push_env(invoke_options)
say "Ensure Traefik is running...", :magenta
invoke "kamal:cli:traefik:boot", [], invoke_options
@@ -76,6 +75,8 @@ class Kamal::Cli::Main < Kamal::Cli::Base
run_hook "pre-deploy"
push_env(invoke_options)
say "Ensure app can pass healthcheck...", :magenta
invoke "kamal:cli:healthcheck:perform", [], invoke_options
@@ -102,6 +103,8 @@ class Kamal::Cli::Main < Kamal::Cli::Base
if container_available?(version)
run_hook "pre-deploy"
push_env(invoke_options)
invoke "kamal:cli:app:boot", [], invoke_options.merge(version: version)
rolled_back = true
else
@@ -197,7 +200,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
def remove
mutating do
confirming "This will remove all containers and images. Are you sure?" do
if options[:confirmed] || ask("This will remove all containers and images. Are you sure?", limited_to: %w( y N ), default: "N") == "y"
invoke "kamal:cli:traefik:remove", [], options.without(:confirmed)
invoke "kamal:cli:app:remove", [], options.without(:confirmed)
invoke "kamal:cli:accessory:remove", [ "all" ], options
@@ -265,4 +268,11 @@ class Kamal::Cli::Main < Kamal::Cli::Base
def deploy_options
{ "version" => KAMAL.config.version }.merge(options.without("skip_push"))
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

View File

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

View File

@@ -63,7 +63,7 @@ registry:
# directories:
# - data:/data
# Configure custom arguments for Traefik. Be sure to reboot traefik when you modify it.
# Configure custom arguments for Traefik
# traefik:
# args:
# accesslog: true

View File

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

View File

@@ -11,9 +11,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
desc "reboot", "Reboot Traefik on servers (stop container, remove container, start new container)"
option :rolling, type: :boolean, default: false, desc: "Reboot traefik on hosts in sequence, rather than in parallel"
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
def reboot
confirming "This will cause a brief outage on each host. Are you sure?" do
mutating do
host_groups = options[:rolling] ? KAMAL.traefik_hosts : [KAMAL.traefik_hosts]
host_groups.each do |hosts|
@@ -22,7 +20,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
on(hosts) do
execute *KAMAL.auditor.record("Rebooted traefik"), verbosity: :debug
execute *KAMAL.registry.login
execute *KAMAL.traefik.stop, raise_on_non_zero_exit: false
execute *KAMAL.traefik.stop
execute *KAMAL.traefik.remove_container
execute *KAMAL.traefik.run
end
@@ -30,7 +28,6 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
end
end
end
end
desc "start", "Start existing Traefik container on servers"
def start
@@ -47,7 +44,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
mutating do
on(KAMAL.traefik_hosts) do
execute *KAMAL.auditor.record("Stopped traefik"), verbosity: :debug
execute *KAMAL.traefik.stop, raise_on_non_zero_exit: false
execute *KAMAL.traefik.stop
end
end
end

View File

@@ -3,13 +3,11 @@ require "active_support/core_ext/module/delegation"
class Kamal::Commander
attr_accessor :verbosity, :holding_lock, :hold_lock_on_error
delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :traefik_hosts, :accessory_hosts, to: :specifics
def initialize
self.verbosity = :info
self.holding_lock = false
self.hold_lock_on_error = false
@specifics = nil
end
def config
@@ -26,12 +24,10 @@ class Kamal::Commander
attr_reader :specific_roles, :specific_hosts
def specific_primary!
@specifics = nil
self.specific_hosts = [ config.primary_host ]
end
def specific_roles=(role_names)
@specifics = nil
if role_names.present?
@specific_roles = Kamal::Utils.filter_specific_items(role_names, config.roles)
@@ -44,7 +40,6 @@ class Kamal::Commander
end
def specific_hosts=(hosts)
@specifics = nil
if hosts.present?
@specific_hosts = Kamal::Utils.filter_specific_items(hosts, config.all_hosts)
@@ -56,6 +51,39 @@ class Kamal::Commander
end
end
def primary_host
# Given a list of specific roles, make an effort to match up with the primary_role
specific_hosts&.first || specific_roles&.detect { |role| role.name == config.primary_role }&.primary_host || specific_roles&.first&.primary_host || config.primary_host
end
def primary_role
roles_on(primary_host).first
end
def roles
(specific_roles || config.roles).select do |role|
((specific_hosts || config.all_hosts) & role.hosts).any?
end
end
def hosts
(specific_hosts || config.all_hosts).select do |host|
(specific_roles || config.roles).flat_map(&:hosts).include?(host)
end
end
def roles_on(host)
roles.select { |role| role.hosts.include?(host.to_s) }.map(&:name)
end
def traefik_hosts
specific_hosts || config.traefik_hosts
end
def accessory_hosts
specific_hosts || config.accessories.flat_map(&:hosts)
end
def accessory_names
config.accessories&.collect(&:name) || []
end
@@ -153,8 +181,4 @@ class Kamal::Commander
SSHKit.config.command_map[:docker] = "docker" # No need to use /usr/bin/env, just clogs up the logs
SSHKit.config.output_verbosity = verbosity
end
def specifics
@specifics ||= Kamal::Commander::Specifics.new(config, specific_hosts, specific_roles)
end
end

View File

@@ -1,49 +0,0 @@
class Kamal::Commander::Specifics
attr_reader :primary_host, :primary_role, :hosts, :roles
delegate :stable_sort!, to: Kamal::Utils
def initialize(config, specific_hosts, specific_roles)
@config, @specific_hosts, @specific_roles = config, specific_hosts, specific_roles
@roles, @hosts = specified_roles, specified_hosts
@primary_host = specific_hosts&.first || primary_specific_role&.primary_host || config.primary_host
@primary_role = primary_or_first_role(roles_on(primary_host))
stable_sort!(roles) { |role| role == primary_role ? 0 : 1 }
stable_sort!(hosts) { |host| roles_on(host).any? { |role| role == primary_role } ? 0 : 1 }
end
def roles_on(host)
roles.select { |role| role.hosts.include?(host.to_s) }
end
def traefik_hosts
specific_hosts || config.traefik_hosts
end
def accessory_hosts
specific_hosts || config.accessories.flat_map(&:hosts)
end
private
attr_reader :config, :specific_hosts, :specific_roles
def primary_specific_role
primary_or_first_role(specific_roles) if specific_roles.present?
end
def primary_or_first_role(roles)
roles.detect { |role| role == config.primary_role } || roles.first
end
def specified_roles
(specific_roles || config.roles) \
.select { |role| ((specific_hosts || config.all_hosts) & role.hosts).any? }
end
def specified_hosts
(specific_hosts || config.all_hosts) \
.select { |host| (specific_roles || config.roles).flat_map(&:hosts).include?(host) }
end
end

View File

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

View File

@@ -3,11 +3,12 @@ class Kamal::Commands::App < Kamal::Commands::Base
ACTIVE_DOCKER_STATUSES = [ :running, :restarting ]
attr_reader :role, :role
attr_reader :role, :role_config
def initialize(config, role: nil)
super(config)
@role = role
@role_config = config.role(self.role)
end
def run(hostname: nil)
@@ -18,15 +19,15 @@ class Kamal::Commands::App < Kamal::Commands::Base
*(["--hostname", hostname] if hostname),
"-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
"-e", "KAMAL_VERSION=\"#{config.version}\"",
*role.env_args,
*role.health_check_args,
*role.logging_args,
*role_config.env_args,
*role_config.health_check_args,
*config.logging_args,
*config.volume_args,
*role.asset_volume_args,
*role.label_args,
*role.option_args,
*role_config.asset_volume_args,
*role_config.label_args,
*role_config.option_args,
config.absolute_image,
role.cmd
role_config.cmd
end
def start
@@ -49,7 +50,7 @@ class Kamal::Commands::App < Kamal::Commands::Base
def current_running_container_id
current_running_container(format: "--quiet")
docker :ps, "--quiet", *filter_args(statuses: ACTIVE_DOCKER_STATUSES), "--latest"
end
def container_id_for_version(version, only_running: false)
@@ -57,57 +58,36 @@ class Kamal::Commands::App < Kamal::Commands::Base
end
def current_running_version
pipe \
current_running_container(format: "--format '{{.Names}}'"),
extract_version_from_name
list_versions("--latest", statuses: ACTIVE_DOCKER_STATUSES)
end
def list_versions(*docker_args, statuses: nil)
pipe \
docker(:ps, *filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'),
extract_version_from_name
%(while read line; do echo ${line##{role_config.container_prefix}-}; done) # Extract SHA from "service-role-dest-SHA"
end
def make_env_directory
make_directory role.env.secrets_directory
make_directory role_config.host_env_directory
end
def remove_env_file
[ :rm, "-f", role.env.secrets_file ]
def remove_env_files
[ :rm, "-f", File.join(role_config.host_env_directory, "#{role_config.container_prefix}*.env") ]
end
private
def container_name(version = nil)
[ role.container_prefix, version || config.version ].compact.join("-")
end
def latest_image_id
docker :image, :ls, *argumentize("--filter", "reference=#{config.latest_image}"), "--format", "'{{.ID}}'"
end
def current_running_container(format:)
pipe \
shell(chain(latest_image_container(format: format), latest_container(format: format))),
[ :head, "-1" ]
end
def latest_image_container(format:)
latest_container format: format, filters: [ "ancestor=$(#{latest_image_id.join(" ")})" ]
end
def latest_container(format:, filters: nil)
docker :ps, "--latest", *format, *filter_args(statuses: ACTIVE_DOCKER_STATUSES), argumentize("--filter", filters)
[ role_config.container_prefix, version || config.version ].compact.join("-")
end
def filter_args(statuses: nil)
argumentize "--filter", filters(statuses: statuses)
end
def extract_version_from_name
# Extract SHA from "service-role-dest-SHA"
%(while read line; do echo ${line##{role.container_prefix}-}; done)
def service_role_dest
[ config.service, role, config.destination ].compact.join("-")
end
def filters(statuses: nil)

View File

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

View File

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

View File

@@ -1,29 +1,27 @@
module Kamal::Commands::App::Execution
def execute_in_existing_container(*command, interactive: false, env:)
def execute_in_existing_container(*command, interactive: false)
docker :exec,
("-it" if interactive),
*argumentize("--env", env),
container_name,
*command
end
def execute_in_new_container(*command, interactive: false, env:)
def execute_in_new_container(*command, interactive: false)
docker :run,
("-it" if interactive),
"--rm",
*role&.env_args,
*argumentize("--env", env),
*role_config&.env_args,
*config.volume_args,
*role&.option_args,
*role_config&.option_args,
config.absolute_image,
*command
end
def execute_in_existing_container_over_ssh(*command, host:, env:)
run_over_ssh execute_in_existing_container(*command, interactive: true, env: env), host: host
def execute_in_existing_container_over_ssh(*command, host:)
run_over_ssh execute_in_existing_container(*command, interactive: true), host: host
end
def execute_in_new_container_over_ssh(*command, host:, env:)
run_over_ssh execute_in_new_container(*command, interactive: true, env: env), host: host
def execute_in_new_container_over_ssh(*command, host:)
run_over_ssh execute_in_new_container(*command, interactive: true), host: host
end
end

View File

@@ -7,7 +7,7 @@ module Kamal::Commands::App::Images
docker :image, :prune, "--all", "--force", *filter_args
end
def tag_latest_image
def tag_current_image_as_latest
docker :tag, config.absolute_image, config.latest_image
end
end

View File

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

View File

@@ -21,7 +21,7 @@ class Kamal::Commands::Auditor < Kamal::Commands::Base
def audit_log_file
file = [ config.service, config.destination, "audit.log" ].compact.join("-")
File.join(config.run_directory, file)
"#{config.run_directory}/#{file}"
end
def audit_tags(**details)

View File

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

View File

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

View File

@@ -7,6 +7,15 @@ class Kamal::Commands::Builder::Multiarch < Kamal::Commands::Builder::Base
docker :buildx, :rm, builder_name
end
def push
docker :buildx, :build,
"--push",
"--platform", platform_names,
"--builder", builder_name,
*build_options,
build_context
end
def info
combine \
docker(:context, :ls),
@@ -25,13 +34,4 @@ class Kamal::Commands::Builder::Multiarch < Kamal::Commands::Builder::Base
"linux/amd64,linux/arm64"
end
end
def build_and_push
docker :buildx, :build,
"--push",
"--platform", platform_names,
"--builder", builder_name,
*build_options,
build_context
end
end

View File

@@ -7,15 +7,14 @@ class Kamal::Commands::Builder::Native < Kamal::Commands::Builder::Base
# No-op on native without cache
end
def info
# No-op on native
end
private
def build_and_push
def push
combine \
docker(:build, *build_options, build_context),
docker(:push, config.absolute_image),
docker(:push, config.latest_image)
end
def info
# No-op on native
end
end

View File

@@ -7,8 +7,7 @@ class Kamal::Commands::Builder::Native::Cached < Kamal::Commands::Builder::Nativ
docker :buildx, :rm, builder_name
end
private
def build_and_push
def push
docker :buildx, :build,
"--push",
*build_options,

View File

@@ -11,6 +11,15 @@ class Kamal::Commands::Builder::Native::Remote < Kamal::Commands::Builder::Nativ
remove_buildx
end
def push
docker :buildx, :build,
"--push",
"--platform", platform,
"--builder", builder_name,
*build_options,
build_context
end
def info
chain \
docker(:context, :ls),
@@ -47,13 +56,4 @@ class Kamal::Commands::Builder::Native::Remote < Kamal::Commands::Builder::Nativ
def remove_buildx
docker :buildx, :rm, builder_name
end
def build_and_push
docker :buildx, :build,
"--push",
"--platform", platform,
"--builder", builder_name,
*build_options,
build_context
end
end

View File

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

View File

@@ -1,4 +1,5 @@
class Kamal::Commands::Healthcheck < Kamal::Commands::Base
def run
primary = config.role(config.primary_role)

View File

@@ -9,6 +9,6 @@ class Kamal::Commands::Hook < Kamal::Commands::Base
private
def hook_file(hook)
File.join(config.hooks_path, hook)
"#{config.hooks_path}/#{hook}"
end
end

View File

@@ -21,10 +21,6 @@ class Kamal::Commands::Lock < Kamal::Commands::Base
read_lock_details
end
def ensure_locks_directory
[ :mkdir, "-p", locks_dir ]
end
private
def write_lock_details(message, version)
write \
@@ -44,18 +40,12 @@ class Kamal::Commands::Lock < Kamal::Commands::Base
"/dev/null"
end
def locks_dir
File.join(config.run_directory, "locks")
end
def lock_dir
dir_name = [ config.service, config.destination ].compact.join("-")
File.join(locks_dir, dir_name)
"#{config.run_directory}/lock-#{config.service}"
end
def lock_details_file
File.join(lock_dir, "details")
[lock_dir, :details].join("/")
end
def lock_details(message, version)

View File

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

View File

@@ -4,7 +4,7 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base
DEFAULT_IMAGE = "traefik:v2.10"
CONTAINER_PORT = 80
DEFAULT_ARGS = {
"log.level" => "DEBUG"
'log.level' => 'DEBUG'
}
DEFAULT_LABELS = {
# These ensure we serve a 502 rather than a 404 if no containers are available
@@ -39,7 +39,7 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base
end
def start_or_run
any start, run
combine start, run, by: "||"
end
def info
@@ -71,18 +71,24 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base
"#{host_port}:#{CONTAINER_PORT}"
end
def env
Kamal::Configuration::Env.from_config \
config: config.traefik.fetch("env", {}),
secrets_file: File.join(config.host_env_directory, "traefik", "traefik.env")
def env_file
Kamal::EnvFiles.new(config.traefik.fetch("env", {}))
end
def host_clear_env_file_path
File.join host_env_directory, "traefik-clear.env"
end
def host_secret_env_file_path
File.join host_env_directory, "traefik-secret.env"
end
def make_env_directory
make_directory(env.secrets_directory)
make_directory(host_env_directory)
end
def remove_env_file
[ :rm, "-f", env.secrets_file ]
def remove_env_files
[:rm, "-f", File.join(host_env_directory, "traefik*.env")]
end
private
@@ -95,7 +101,14 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base
end
def env_args
env.args
[
*argumentize("--env-file", host_secret_env_file_path),
*argumentize("--env-file", host_clear_env_file_path)
]
end
def host_env_directory
File.join config.host_env_directory, "traefik"
end
def labels

View File

@@ -6,7 +6,7 @@ require "erb"
require "net/ssh/proxy/jump"
class Kamal::Configuration
delegate :service, :image, :servers, :labels, :registry, :stop_wait_time, :hooks_path, :logging, to: :raw_config, allow_nil: true
delegate :service, :image, :servers, :env, :labels, :registry, :stop_wait_time, :hooks_path, :push_env, to: :raw_config, allow_nil: true
delegate :argumentize, :optionize, to: Kamal::Utils
attr_reader :destination, :raw_config
@@ -88,23 +88,11 @@ class Kamal::Configuration
def all_hosts
(roles + accessories).flat_map(&:hosts).uniq
roles.flat_map(&:hosts).uniq
end
def primary_host
primary_role&.primary_host
end
def primary_role_name
raw_config.primary_role || "web"
end
def primary_role
role(primary_role_name)
end
def allow_empty_roles?
raw_config.allow_empty_roles
role(primary_role)&.primary_host
end
def traefik_roles
@@ -128,11 +116,7 @@ class Kamal::Configuration
end
def latest_image
"#{repository}:#{latest_tag}"
end
def latest_tag
[ "latest", *destination ].join("-")
"#{repository}:latest"
end
def service_with_version
@@ -157,9 +141,9 @@ class Kamal::Configuration
end
def logging_args
if logging.present?
optionize({ "log-driver" => logging["driver"] }.compact) +
argumentize("--log-opt", logging["options"])
if raw_config.logging.present?
optionize({ "log-driver" => raw_config.logging["driver"] }.compact) +
argumentize("--log-opt", raw_config.logging["options"])
else
argumentize("--log-opt", { "max-size" => "10m" })
end
@@ -220,22 +204,30 @@ class Kamal::Configuration
raw_config.hooks_path || ".kamal/hooks"
end
def host_env_directory
"#{run_directory}/env"
end
def asset_path
raw_config.asset_path
end
def host_env_directory
File.join(run_directory, "env")
def primary_role
raw_config.primary_role || "web"
end
def env
raw_config.env || {}
def allow_empty_roles?
raw_config.allow_empty_roles
end
def valid?
ensure_destination_if_required && ensure_required_keys_present && ensure_valid_kamal_version && ensure_retain_containers_valid && ensure_valid_service_name
ensure_destination_if_required \
&& ensure_required_keys_present \
&& ensure_valid_kamal_version \
&& ensure_retain_containers_valid \
&& ensure_valid_service_name \
&& ensure_push_env_valid
end
def to_h
@@ -281,12 +273,12 @@ class Kamal::Configuration
raise ArgumentError, "You must specify a password for the registry in config/deploy.yml (or set the ENV variable if that's used)"
end
unless role_names.include?(primary_role_name)
raise ArgumentError, "The primary_role #{primary_role_name} isn't defined"
unless role_names.include?(primary_role)
raise ArgumentError, "The primary_role #{primary_role} isn't defined"
end
if primary_role.hosts.empty?
raise ArgumentError, "No servers specified for the #{primary_role.name} primary_role"
if role(primary_role).hosts.empty?
raise ArgumentError, "No servers specified for the #{primary_role} primary_role"
end
unless allow_empty_roles?
@@ -301,7 +293,7 @@ class Kamal::Configuration
end
def ensure_valid_service_name
raise ArgumentError, "Service name can only include alphanumeric characters, hyphens, and underscores" unless raw_config[:service] =~ /^[a-z0-9_-]+$/i
raise ArgumentError, "Service name can only include alphanumeric characters, hyphens, and underscores" unless raw_config[:service] =~ /^[a-z0-9-_]+$/
true
end
@@ -320,6 +312,14 @@ class Kamal::Configuration
true
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
raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
@@ -328,10 +328,7 @@ class Kamal::Configuration
def git_version
@git_version ||=
if Kamal::Git.used?
if Kamal::Git.uncommitted_changes.present? && !builder.git_archive?
uncommitted_suffix = "_uncommitted_#{SecureRandom.hex(8)}"
end
[ Kamal::Git.revision, uncommitted_suffix ].compact.join
[ Kamal::Git.revision, Kamal::Git.uncommitted_changes.present? ? "_uncommitted_#{SecureRandom.hex(8)}" : "" ].join
else
raise "Can't use commit hash as version, no git repository found in #{Dir.pwd}"
end

View File

@@ -42,13 +42,30 @@ class Kamal::Configuration::Accessory
end
def env
Kamal::Configuration::Env.from_config \
config: specifics.fetch("env", {}),
secrets_file: File.join(config.host_env_directory, "accessories", "#{service_name}.env")
specifics["env"] || {}
end
def env_file
Kamal::EnvFiles.new(env)
end
def host_env_directory
File.join config.host_env_directory, "accessories"
end
def host_clear_env_file_path
File.join host_env_directory, "#{service_name}-clear.env"
end
def host_secret_env_file_path
File.join host_env_directory, "#{service_name}-secret.env"
end
def env_args
env.args
[
*argumentize("--env-file", host_secret_env_file_path),
*argumentize("--env-file", host_clear_env_file_path)
]
end
def files
@@ -101,10 +118,10 @@ class Kamal::Configuration::Accessory
end
def with_clear_env_loaded
env.clear.each { |k, v| ENV[k] = v }
(env["clear"] || env).each { |k, v| ENV[k] = v }
yield
ensure
env.clear.each { |k, v| ENV.delete(k) }
(env["clear"] || env).each { |k, v| ENV.delete(k) }
end
def read_dynamic_file(local_file)
@@ -134,7 +151,7 @@ class Kamal::Configuration::Accessory
end
def expand_host_path(host_path)
absolute_path?(host_path) ? host_path : File.join(service_data_directory, host_path)
absolute_path?(host_path) ? host_path : "#{service_data_directory}/#{host_path}"
end
def absolute_path?(path)

View File

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

View File

@@ -40,7 +40,7 @@ class Kamal::Configuration::Builder
end
def context
@options["context"] || (git_archive? ? "-" : ".")
@options["context"] || "."
end
def local_arch
@@ -85,10 +85,6 @@ class Kamal::Configuration::Builder
@options["ssh"]
end
def git_archive?
Kamal::Git.used? && @options["context"].nil?
end
private
def valid?
if @options["cache"] && @options["cache"]["type"]

View File

@@ -1,40 +0,0 @@
class Kamal::Configuration::Env
attr_reader :secrets_keys, :clear, :secrets_file
delegate :argumentize, to: Kamal::Utils
def self.from_config(config:, secrets_file: nil)
secrets_keys = config.fetch("secret", [])
clear = config.fetch("clear", config.key?("secret") ? {} : config)
new clear: clear, secrets_keys: secrets_keys, secrets_file: secrets_file
end
def initialize(clear:, secrets_keys:, secrets_file:)
@clear = clear
@secrets_keys = secrets_keys
@secrets_file = secrets_file
end
def args
[ "--env-file", secrets_file, *argumentize("--env", clear) ]
end
def secrets_io
StringIO.new(Kamal::EnvFile.new(secrets).to_s)
end
def secrets
@secrets ||= secrets_keys.to_h { |key| [ key, ENV.fetch(key) ] }
end
def secrets_directory
File.dirname(secrets_file)
end
def merge(other)
self.class.new \
clear: @clear.merge(other.clear),
secrets_keys: @secrets_keys | other.secrets_keys,
secrets_file: secrets_file
end
end

View File

@@ -3,7 +3,6 @@ class Kamal::Configuration::Role
delegate :argumentize, :optionize, to: Kamal::Utils
attr_accessor :name
alias to_s name
def initialize(name, config:)
@name, @config = name.inquiry, config
@@ -37,25 +36,36 @@ class Kamal::Configuration::Role
argumentize "--label", labels
end
def logging_args
args = config.logging || {}
args.deep_merge!(specializations["logging"]) if specializations["logging"].present?
if args.any?
optionize({ "log-driver" => args["driver"] }.compact) +
argumentize("--log-opt", args["options"])
else
config.logging_args
end
end
def env
@env ||= base_env.merge(specialized_env)
if config.env && config.env["secret"]
merged_env_with_secrets
else
merged_env
end
end
def env_file
Kamal::EnvFiles.new(env)
end
def host_env_directory
File.join config.host_env_directory, "roles"
end
def host_clear_env_file_path
host_env_file_path(:clear)
end
def host_secret_env_file_path
host_env_file_path(:secret)
end
def env_args
env.args
[
*argumentize("--env-file", host_secret_env_file_path),
*argumentize("--env-file", host_clear_env_file_path)
]
end
def asset_volume_args
@@ -98,7 +108,7 @@ class Kamal::Configuration::Role
end
def primary?
self == @config.primary_role
@config.primary_role == name
end
@@ -176,7 +186,11 @@ class Kamal::Configuration::Role
end
def default_labels
if config.destination
{ "service" => config.service, "role" => name, "destination" => config.destination }
else
{ "service" => config.service, "role" => name }
end
end
def traefik_labels
@@ -197,7 +211,7 @@ class Kamal::Configuration::Role
end
def traefik_service
container_prefix
[ config.service, name, config.destination ].compact.join("-")
end
def custom_labels
@@ -216,14 +230,28 @@ class Kamal::Configuration::Role
end
def specialized_env
Kamal::Configuration::Env.from_config config: specializations.fetch("env", {})
specializations["env"] || {}
end
def merged_env
config.env&.merge(specialized_env) || {}
end
# Secrets are stored in an array, which won't merge by default, so have to do it by hand.
def base_env
Kamal::Configuration::Env.from_config \
config: config.env,
secrets_file: File.join(config.host_env_directory, "roles", "#{container_prefix}.env")
def merged_env_with_secrets
merged_env.tap do |new_env|
new_env["secret"] = Array(config.env["secret"]) + Array(specialized_env["secret"])
# If there's no secret/clear split, everything is clear
clear_app_env = config.env["secret"] ? Array(config.env["clear"]) : Array(config.env["clear"] || config.env)
clear_role_env = specialized_env["secret"] ? Array(specialized_env["clear"]) : Array(specialized_env["clear"] || specialized_env)
new_env["clear"] = clear_app_env.to_h.merge(clear_role_env.to_h)
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:)

View File

@@ -1,38 +0,0 @@
# Encode an env hash as a string where secret values have been looked up and all values escaped for Docker.
class Kamal::EnvFile
def initialize(env)
@env = env
end
def to_s
env_file = StringIO.new.tap do |contents|
@env.each do |key, value|
contents << docker_env_file_line(key, value)
end
end.string
# Ensure the file has some contents to avoid the SSHKIT empty file warning
env_file.presence || "\n"
end
alias to_str to_s
private
def docker_env_file_line(key, value)
"#{key}=#{escape_docker_env_file_value(value)}\n"
end
# Escape a value to make it safe to dump in a docker file.
def escape_docker_env_file_value(value)
# keep non-ascii(UTF-8) characters as it is
value.to_s.scan(/[\x00-\x7F]+|[^\x00-\x7F]+/).map do |part|
part.ascii_only? ? escape_docker_env_file_ascii_value(part) : part
end.join
end
def escape_docker_env_file_ascii_value(value)
# Doublequotes are treated literally in docker env files
# so remove leading and trailing ones and unescape any others
value.to_s.dump[1..-2].gsub(/\\"/, "\"")
end
end

44
lib/kamal/env_files.rb Normal file
View File

@@ -0,0 +1,44 @@
# Encode an env hash as a string where secret values have been looked up and all values escaped for Docker.
class Kamal::EnvFiles
def initialize(env)
@env = env
end
def secret
env_file do
@env["secret"]&.to_h { |key| [ key, ENV.fetch(key) ] }
end
end
def clear
env_file do
if (secrets = @env["secret"]).present?
@env["clear"]
else
@env.fetch("clear", @env)
end
end
end
private
def docker_env_file_line(key, value)
"#{key.to_s}=#{escape_docker_env_file_value(value)}\n"
end
# Escape a value to make it safe to dump in a docker file.
def escape_docker_env_file_value(value)
# Doublequotes are treated literally in docker env files
# so remove leading and trailing ones and unescape any others
value.to_s.dump[1..-2].gsub(/\\"/, "\"")
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

View File

@@ -66,7 +66,7 @@ module Kamal::Utils
Array(filters).select do |filter|
matches += Array(items).select do |item|
# Only allow * for a wildcard
pattern = Regexp.escape(filter).gsub('\*', ".*")
pattern = Regexp.escape(filter).gsub('\*', '.*')
# items are roles or hosts
(item.respond_to?(:name) ? item.name : item).match(/^#{pattern}$/)
end
@@ -74,8 +74,4 @@ module Kamal::Utils
matches
end
def stable_sort!(elements, &block)
elements.sort_by!.with_index { |element, index| [ block.call(element), index ] }
end
end

View File

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

View File

@@ -7,7 +7,7 @@ class CliAccessoryTest < CliTestCase
run_command("boot", "mysql").tap do |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.env --env MYSQL_ROOT_HOST=\"%\" --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-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
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.1/, 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.env --env MYSQL_ROOT_HOST=\"%\" --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.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.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-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-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
end
end
@@ -79,10 +79,6 @@ class CliAccessoryTest < CliTestCase
assert_match "docker ps --filter label=service=app-mysql", run_command("details", "mysql")
end
test "details with non-existent accessory" do
assert_equal "No accessory by the name of 'hello' (options: mysql and redis)", stderred { run_command("details", "hello") }
end
test "details with all" do
run_command("details", "all").tap do |output|
assert_match "docker ps --filter label=service=app-mysql", output
@@ -158,9 +154,9 @@ class CliAccessoryTest < CliTestCase
run_command("boot", "redis", "--hosts", "1.1.1.1").tap do |output|
assert_match /docker login.*on 1.1.1.1/, output
assert_no_match /docker login.*on 1.1.1.2/, output
assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output
assert_no_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
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
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
end
end
@@ -170,9 +166,9 @@ class CliAccessoryTest < CliTestCase
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_no_match /docker login.*on 1.1.1.3/, output
assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output
assert_no_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
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
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
end
end

View File

@@ -23,11 +23,11 @@ class CliAppTest < CliTestCase
.returns("running") # health check
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
.with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=web", "--filter", "status=running", "--filter", "status=restarting", "--latest", "--format", "\"{{.Names}}\"", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
.returns("123") # old version
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :inspect, "-f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}'", "app-web-123", "|", :awk, "'$2 == \"/tmp/kamal-cord\" {print $1}'", raise_on_non_zero_exit: false)
.with(:docker, :inspect, "-f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}'", "app-web-123", "|", :awk, "'$2 == \"/tmp/kamal-cord\" {print $1}'", :raise_on_non_zero_exit => false)
.returns("cordfile") # old version
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
@@ -45,7 +45,7 @@ class CliAppTest < CliTestCase
end
test "boot uses group strategy when specified" do
Kamal::Cli::App.any_instance.stubs(:on).with("1.1.1.1").times(3) # ensure locks dir, acquire & release lock
Kamal::Cli::App.any_instance.stubs(:on).with("1.1.1.1").twice # acquire & release lock
Kamal::Cli::App.any_instance.stubs(:on).with([ "1.1.1.1" ]) # tag container
# Strategy is used when booting the containers
@@ -57,7 +57,7 @@ class CliAppTest < CliTestCase
test "boot errors leave lock in place" do
Kamal::Cli::App.any_instance.expects(:using_version).raises(RuntimeError)
assert_not KAMAL.holding_lock?
assert !KAMAL.holding_lock?
assert_raises(RuntimeError) do
stderred { run_command("boot") }
end
@@ -75,11 +75,11 @@ class CliAppTest < CliTestCase
.returns("running") # health check
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
.with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=web", "--filter", "status=running", "--filter", "status=restarting", "--latest", "--format", "\"{{.Names}}\"", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
.returns("123").twice # old version
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :inspect, "-f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}'", "app-web-123", "|", :awk, "'$2 == \"/tmp/kamal-cord\" {print $1}'", raise_on_non_zero_exit: false)
.with(:docker, :inspect, "-f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}'", "app-web-123", "|", :awk, "'$2 == \"/tmp/kamal-cord\" {print $1}'", :raise_on_non_zero_exit => false)
.returns("") # old version
run_command("boot", config: :with_assets).tap do |output|
@@ -100,18 +100,14 @@ class CliAppTest < CliTestCase
test "stop" do
run_command("stop").tap do |output|
assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop", output
assert_match "docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker stop", output
end
end
test "stale_containers" do
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=web", "--format", "\"{{.Names}}\"", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
.returns("12345678\n87654321\n")
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
.returns("12345678\n")
.returns("12345678\n87654321")
run_command("stale_containers").tap do |output|
assert_match /Detected stale container for role web with version 87654321/, output
@@ -121,11 +117,7 @@ class CliAppTest < CliTestCase
test "stop stale_containers" do
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=web", "--format", "\"{{.Names}}\"", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
.returns("12345678\n87654321\n")
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
.returns("12345678\n")
.returns("12345678\n87654321")
run_command("stale_containers", "--stop").tap do |output|
assert_match /Stopping stale container for role web with version 87654321/, output
@@ -141,7 +133,7 @@ class CliAppTest < CliTestCase
test "remove" do
run_command("remove").tap do |output|
assert_match /#{Regexp.escape("sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop")}/, output
assert_match /#{Regexp.escape("docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker stop")}/, output
assert_match /#{Regexp.escape("docker container prune --force --filter label=service=app")}/, output
assert_match /#{Regexp.escape("docker image prune --all --force --filter label=service=app")}/, output
end
@@ -167,20 +159,20 @@ class CliAppTest < CliTestCase
test "exec" do
run_command("exec", "ruby -v").tap do |output|
assert_match "docker run --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v", output
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
end
end
test "exec with reuse" do
run_command("exec", "--reuse", "ruby -v").tap do |output|
assert_match "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", output # Get current version
assert_match "docker ps --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest --format \"{{.Names}}\" | while read line; do echo ${line#app-web-}; done", output # Get current version
assert_match "docker exec app-web-999 ruby -v", output
end
end
test "exec interactive" do
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.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-secret.env --env-file .kamal/env/roles/app-web-clear.env dhh/app:latest ruby -v'")
run_command("exec", "-i", "ruby -v").tap do |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
@@ -192,7 +184,7 @@ class CliAppTest < CliTestCase
.with("ssh -t root@1.1.1.1 -p 22 'docker exec -it app-web-999 ruby -v'")
run_command("exec", "-i", "--reuse", "ruby -v").tap do |output|
assert_match "Get current version of running container...", output
assert_match "Running /usr/bin/env sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done on 1.1.1.1", output
assert_match "Running docker ps --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest --format \"{{.Names}}\" | while read line; do echo ${line#app-web-}; done on 1.1.1.1", output
assert_match "Launching interactive command with version 999 via SSH from existing container on 1.1.1.1...", output
end
end
@@ -211,28 +203,28 @@ class CliAppTest < CliTestCase
test "logs" do
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
.with("ssh -t root@1.1.1.1 'sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1| xargs docker logs --timestamps --tail 10 2>&1'")
.with("ssh -t root@1.1.1.1 'docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest| xargs docker logs --timestamps --tail 10 2>&1'")
assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --tail 100 2>&1", run_command("logs")
assert_match "docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --tail 100 2>&1", run_command("logs")
end
test "logs with follow" do
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
.with("ssh -t root@1.1.1.1 -p 22 'sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --tail 10 --follow 2>&1'")
.with("ssh -t root@1.1.1.1 -p 22 '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'")
assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --tail 10 --follow 2>&1", run_command("logs", "--follow")
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", run_command("logs", "--follow")
end
test "version" do
run_command("version").tap do |output|
assert_match "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", output
assert_match "docker ps --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest --format \"{{.Names}}\" | while read line; do echo ${line#app-web-}; done", output
end
end
test "version through main" do
stdouted { Kamal::Cli::Main.start(["app", "version", "-c", "test/fixtures/deploy_with_accessories.yml", "--hosts", "1.1.1.1"]) }.tap do |output|
assert_match "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", output
assert_match "docker ps --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest --format \"{{.Names}}\" | while read line; do echo ${line#app-web-}; done", output
end
end

View File

@@ -12,10 +12,10 @@ class CliBuildTest < CliTestCase
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,1.1.1.3,1.1.1.4", command: "build", subcommand: "push" }
run_command("push", "--verbose").tap do |output|
run_command("push").tap do |output|
assert_hook_ran "pre-build", output, **hook_variables
assert_match /docker --version && docker buildx version/, output
assert_match /git archive -tar HEAD | docker buildx build --push --platform linux\/amd64,linux\/arm64 --builder kamal-app-multiarch -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile - as .*@localhost/, output
assert_match /docker buildx build --push --platform linux\/amd64,linux\/arm64 --builder kamal-app-multiarch -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile \. as .*@localhost/, output
end
end
@@ -25,10 +25,7 @@ class CliBuildTest < CliTestCase
.with(:docker, "--version", "&&", :docker, :buildx, "version")
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with(:docker, :buildx, :create, "--use", "--name", "kamal-app-multiarch")
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |*args| p args[0..6]; args[0..6] == [ :git, :archive, "--format=tar", :HEAD, "|", :docker, :buildx ] }
.with { |*args| args[0..1] == [:docker, :buildx] }
.raises(SSHKit::Command::Failed.new("no builder"))
.then
.returns(true)
@@ -51,8 +48,7 @@ class CliBuildTest < CliTestCase
test "push pre-build hook failure" do
fail_hook("pre-build")
error = assert_raises(Kamal::Cli::HookError) { run_command("push") }
assert_equal "Hook `pre-build` failed:\nfailed", error.message
assert_raises(Kamal::Cli::HookError) { run_command("push") }
assert @executions.none? { |args| args[0..2] == [:docker, :buildx, :build] }
end
@@ -61,7 +57,7 @@ class CliBuildTest < CliTestCase
run_command("pull").tap do |output|
assert_match /docker image rm --force dhh\/app:999/, output
assert_match /docker pull dhh\/app:999/, output
assert_match "docker inspect -f '{{ .Config.Labels.service }}' dhh/app:999 | grep -x app || (echo \"Image dhh/app:999 is missing the 'service' label\" && exit 1)", output
assert_match "docker inspect -f '{{ .Config.Labels.service }}' dhh/app:999 | grep -x app || (echo \"Image dhh/app:999 is missing the `service` label\" && exit 1)", output
end
end

View File

@@ -31,14 +31,12 @@ class CliTestCase < ActiveSupport::TestCase
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |*args| args == [ :mkdir, "-p", ".kamal" ] }
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |arg1, arg2, arg3| arg1 == :mkdir && arg2 == "-p" && arg3 == ".kamal/locks" }
.with { |arg1, arg2| arg1 == :mkdir && arg2 == ".kamal/lock-app" }
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |arg1, arg2| arg1 == :mkdir && arg2 == ".kamal/locks/app" }
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |arg1, arg2| arg1 == :rm && arg2 == ".kamal/locks/app/details" }
.with { |arg1, arg2| arg1 == :rm && arg2 == ".kamal/lock-app/details" }
end
def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: false)
def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: nil)
performer = `whoami`.strip
assert_match "Running the #{hook} hook...\n", output
@@ -52,7 +50,7 @@ class CliTestCase < ActiveSupport::TestCase
KAMAL_HOSTS=\"#{hosts}\"\s
KAMAL_COMMAND=\"#{command}\"\s
#{"KAMAL_SUBCOMMAND=\\\"#{subcommand}\\\"\\s" if subcommand}
#{"KAMAL_RUNTIME=\\\"\\d+\\\"\\s" if runtime}
#{"KAMAL_RUNTIME=\\\"#{runtime}\\\"\\s" if runtime}
;\s/usr/bin/env\s\.kamal/hooks/#{hook} }x
assert_match expected, output

View File

@@ -9,24 +9,29 @@ 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/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 ".kamal/env/roles/app-web.env", output
assert_match ".kamal/env/roles/app-workers.env", output
assert_match ".kamal/env/traefik/traefik.env", output
assert_match ".kamal/env/accessories/app-redis.env", output
assert_match ".kamal/env/roles/app-web-secret.env", output
assert_match ".kamal/env/roles/app-web-clear.env", output
assert_match ".kamal/env/roles/app-workers-secret.env", output
assert_match ".kamal/env/roles/app-workers-clear.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
test "delete" do
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.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.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.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.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/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-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/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/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-mysql*.env on 1.1.1.3", output
end
end

View File

@@ -11,7 +11,7 @@ class CliHealthcheckTest < CliTestCase
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)
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.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-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")
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)
@@ -40,7 +40,7 @@ class CliHealthcheckTest < CliTestCase
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)
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.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-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")
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)

View File

@@ -3,7 +3,7 @@ require_relative "cli_test_case"
class CliLockTest < CliTestCase
test "status" do
run_command("status").tap do |output|
assert_match "Running /usr/bin/env stat .kamal/locks/app > /dev/null && cat .kamal/locks/app/details | base64 -d on 1.1.1.1", output
assert_match "Running /usr/bin/env stat .kamal/lock-app > /dev/null && cat .kamal/lock-app/details | base64 -d on 1.1.1.1", output
end
end

View File

@@ -2,51 +2,16 @@ require_relative "cli_test_case"
class CliMainTest < CliTestCase
test "setup" do
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:server:bootstrap", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:env:push", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:boot", [ "all" ], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:server:bootstrap")
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:env:push")
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:boot", [ "all" ])
Kamal::Cli::Main.any_instance.expects(:deploy)
run_command("setup").tap do |output|
assert_match /Ensure Docker is installed.../, output
assert_match /Push env files.../, output
end
end
test "setup with skip_push" do
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:server:bootstrap", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:env:push", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:boot", [ "all" ], invoke_options)
# deploy
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:pull", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:boot", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:healthcheck:perform", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true))
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options)
run_command("setup", "--skip_push").tap do |output|
assert_match /Ensure Docker is installed.../, output
assert_match /Push env files.../, output
# deploy
assert_match /Acquiring the deploy lock/, output
assert_match /Log into image registry/, output
assert_match /Pull app image/, output
assert_match /Ensure Traefik is running/, output
assert_match /Ensure app can pass healthcheck/, output
assert_match /Detect stale containers/, output
assert_match /Prune old containers and images/, output
assert_match /Releasing the deploy lock/, output
end
run_command("setup")
end
test "deploy" do
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false, "verbose" => true }
invoke_options = { "config_file" => "test/fixtures/deploy_simple.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)
@@ -59,7 +24,7 @@ class CliMainTest < CliTestCase
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", "--verbose").tap do |output|
run_command("deploy").tap do |output|
assert_hook_ran "pre-connect", output, **hook_variables
assert_match /Log into image registry/, output
assert_match /Build and push app image/, output
@@ -68,7 +33,7 @@ class CliMainTest < CliTestCase
assert_match /Ensure app can pass healthcheck/, output
assert_match /Detect stale containers/, output
assert_match /Prune old containers and images/, output
assert_hook_ran "post-deploy", output, **hook_variables, runtime: true
assert_hook_ran "post-deploy", output, **hook_variables, runtime: 0
end
end
@@ -102,14 +67,11 @@ class CliMainTest < CliTestCase
.with { |*args| args == [ :mkdir, "-p", ".kamal" ] }
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |*args| args == [ :mkdir, "-p", ".kamal/locks" ] }
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |*arg| arg[0..1] == [ :mkdir, ".kamal/locks/app" ] }
.raises(RuntimeError, "mkdir: cannot create directory kamal/locks/app: File exists")
.with { |*arg| arg[0..1] == [:mkdir, ".kamal/lock-app"] }
.raises(RuntimeError, "mkdir: cannot create directory kamal_lock-app: File exists")
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_debug)
.with(:stat, ".kamal/locks/app", ">", "/dev/null", "&&", :cat, ".kamal/locks/app/details", "|", :base64, "-d")
.with(:stat, ".kamal/lock-app", ">", "/dev/null", "&&", :cat, ".kamal/lock-app/details", "|", :base64, "-d")
assert_raises(Kamal::Cli::LockError) do
run_command("deploy")
@@ -123,10 +85,7 @@ class CliMainTest < CliTestCase
.with { |*args| args == [ :mkdir, "-p", ".kamal" ] }
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |*args| args == [ :mkdir, "-p", ".kamal/locks" ] }
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |*arg| arg[0..1] == [ :mkdir, ".kamal/locks/app" ] }
.with { |*arg| arg[0..1] == [:mkdir, ".kamal/lock-app"] }
.raises(SocketError, "getaddrinfo: nodename nor servname provided, or not known")
assert_raises(SSHKit::Runner::ExecuteError) do
@@ -141,11 +100,11 @@ class CliMainTest < CliTestCase
.with("kamal:cli:registry:login", [], invoke_options)
.raises(RuntimeError)
assert_not KAMAL.holding_lock?
assert !KAMAL.holding_lock?
assert_raises(RuntimeError) do
stderred { run_command("deploy") }
end
assert_not KAMAL.holding_lock?
assert !KAMAL.holding_lock?
end
test "deploy with skipped hooks" do
@@ -160,7 +119,7 @@ class CliMainTest < CliTestCase
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options)
run_command("deploy", "--skip_hooks") do
assert_no_match /Running the post-deploy hook.../, output
refute_match /Running the post-deploy hook.../, output
end
end
@@ -193,8 +152,28 @@ class CliMainTest < CliTestCase
run_command("deploy", config_file: "deploy_with_secrets")
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
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false, "verbose" => true }
invoke_options = { "config_file" => "test/fixtures/deploy_simple.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:healthcheck:perform", [], invoke_options)
@@ -205,13 +184,13 @@ class CliMainTest < CliTestCase
hook_variables = { version: 999, service_version: "app@999", hosts: "1.1.1.1,1.1.1.2", command: "redeploy" }
run_command("redeploy", "--verbose").tap do |output|
run_command("redeploy").tap do |output|
assert_hook_ran "pre-connect", output, **hook_variables
assert_match /Build and push app image/, output
assert_hook_ran "pre-deploy", output, **hook_variables
assert_match /Running the pre-deploy hook.../, output
assert_match /Ensure app can pass healthcheck/, output
assert_hook_ran "post-deploy", output, **hook_variables, runtime: true
assert_hook_ran "post-deploy", output, **hook_variables, runtime: "0"
end
end
@@ -229,6 +208,23 @@ class CliMainTest < CliTestCase
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
Thread.report_on_exception = false
@@ -241,39 +237,16 @@ class CliMainTest < CliTestCase
end
test "rollback good version" do
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(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=#{role} --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=#{role} --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "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
stub_good_rollback
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" }
run_command("rollback", "--verbose", "123", config_file: "deploy_with_accessories").tap do |output|
run_command("rollback", "123", config_file: "deploy_with_accessories").tap do |output|
assert_hook_ran "pre-deploy", output, **hook_variables
assert_match "docker tag dhh/app:123 dhh/app:latest", output
assert_match "docker run --detach --restart unless-stopped --name app-web-123", output
assert_match "docker container ls --all --filter name=^app-web-version-to-rollback$ --quiet | xargs docker stop", output, "Should stop the container that was previously running"
assert_hook_ran "post-deploy", output, **hook_variables, runtime: true
assert_hook_ran "post-deploy", output, **hook_variables, runtime: "0"
end
end
@@ -286,7 +259,7 @@ class CliMainTest < CliTestCase
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", raise_on_non_zero_exit: false)
.returns("").at_least_once
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
.with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=web", "--filter", "status=running", "--filter", "status=restarting", "--latest", "--format", "\"{{.Names}}\"", "|", "while read line; do echo ${line#app-web-}; done", 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-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
@@ -298,6 +271,16 @@ class CliMainTest < CliTestCase
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
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:details")
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:details")
@@ -495,4 +478,32 @@ class CliMainTest < CliTestCase
def run_command(*command, config_file: "deploy_simple")
stdouted { Kamal::Cli::Main.start([*command, "-c", "test/fixtures/#{config_file}.yml"]) }
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

View File

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

View File

@@ -4,24 +4,24 @@ class CliTraefikTest < CliTestCase
test "boot" do
run_command("boot").tap do |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.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-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
end
end
test "reboot" do
Kamal::Commands::Registry.any_instance.expects(:login).twice
run_command("reboot", "-y").tap do |output|
run_command("reboot").tap do |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 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
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
end
end
test "reboot --rolling" do
Object.any_instance.stubs(:sleep)
run_command("reboot", "--rolling", "-y").tap do |output|
run_command("reboot", "--rolling").tap do |output|
assert_match "Running docker container prune --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output
end
end

View File

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

View File

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

View File

@@ -14,13 +14,13 @@ class CommandsAppTest < ActiveSupport::TestCase
test "run" do
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-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination --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-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",
new_command.run.join(" ")
end
test "run with hostname" do
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.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 destination --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-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",
new_command.run(hostname: "myhost").join(" ")
end
@@ -28,7 +28,7 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:volumes] = ["/local/path:/container/path" ]
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-opt max-size=\"10m\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label destination --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-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",
new_command.run.join(" ")
end
@@ -36,7 +36,7 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:healthcheck] = { "path" => "/healthz" }
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/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 destination --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-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",
new_command.run.join(" ")
end
@@ -44,7 +44,7 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:healthcheck] = { "cmd" => "/bin/up" }
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 \"(/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 destination --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-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",
new_command.run.join(" ")
end
@@ -52,14 +52,14 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "healthcheck" => { "cmd" => "/bin/healthy" } } }
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 \"(/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 destination --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-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",
new_command.run.join(" ")
end
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 } } }
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.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"jobs\" --label destination --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-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",
new_command(role: "jobs").run.join(" ")
end
@@ -67,16 +67,7 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
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 destination --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 destination --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-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",
new_command.run.join(" ")
end
@@ -95,14 +86,14 @@ class CommandsAppTest < ActiveSupport::TestCase
test "stop" do
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop",
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker stop",
new_command.stop.join(" ")
end
test "stop with custom stop wait time" do
@config[:stop_wait_time] = 30
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop -t 30",
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker stop -t 30",
new_command.stop.join(" ")
end
@@ -128,94 +119,74 @@ class CommandsAppTest < ActiveSupport::TestCase
test "logs" do
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs 2>&1",
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs 2>&1",
new_command.logs.join(" ")
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --since 5m 2>&1",
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --since 5m 2>&1",
new_command.logs(since: "5m").join(" ")
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --tail 100 2>&1",
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --tail 100 2>&1",
new_command.logs(lines: "100").join(" ")
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --since 5m --tail 100 2>&1",
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --since 5m --tail 100 2>&1",
new_command.logs(since: "5m", lines: "100").join(" ")
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs 2>&1 | grep 'my-id'",
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs 2>&1 | grep 'my-id'",
new_command.logs(grep: "my-id").join(" ")
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --since 5m 2>&1 | grep 'my-id'",
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --since 5m 2>&1 | grep 'my-id'",
new_command.logs(since: "5m", grep: "my-id").join(" ")
end
test "follow logs" do
assert_equal \
"ssh -t root@app-1 -p 22 'sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --follow 2>&1'",
assert_match \
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --timestamps --tail 10 --follow 2>&1",
new_command.follow_logs(host: "app-1")
assert_equal \
"ssh -t root@app-1 -p 22 'sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --follow 2>&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 10 --follow 2>&1 | grep \"Completed\"",
new_command.follow_logs(host: "app-1", grep: "Completed")
assert_equal \
"ssh -t root@app-1 -p 22 'sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --tail 123 --follow 2>&1'",
new_command.follow_logs(host: "app-1", lines: 123)
assert_equal \
"ssh -t root@app-1 -p 22 'sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --tail 123 --follow 2>&1 | grep \"Completed\"'",
new_command.follow_logs(host: "app-1", lines: 123, grep: "Completed")
end
test "execute in new container" do
assert_equal \
"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", env: {}).join(" ")
end
test "execute in new container with env" do
assert_equal \
"docker run --rm --env-file .kamal/env/roles/app-web.env --env foo=\"bar\" dhh/app:999 bin/rails db:setup",
new_command.execute_in_new_container("bin/rails", "db:setup", env: { "foo" => "bar" }).join(" ")
"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",
new_command.execute_in_new_container("bin/rails", "db:setup").join(" ")
end
test "execute in new container with custom options" do
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } }
assert_equal \
"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", env: {}).join(" ")
"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",
new_command.execute_in_new_container("bin/rails", "db:setup").join(" ")
end
test "execute in existing container" do
assert_equal \
"docker exec app-web-999 bin/rails db:setup",
new_command.execute_in_existing_container("bin/rails", "db:setup", env: {}).join(" ")
end
test "execute in existing container with env" do
assert_equal \
"docker exec --env foo=\"bar\" app-web-999 bin/rails db:setup",
new_command.execute_in_existing_container("bin/rails", "db:setup", env: { "foo" => "bar" }).join(" ")
new_command.execute_in_existing_container("bin/rails", "db:setup").join(" ")
end
test "execute in new container over ssh" do
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", env: {})
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|,
new_command.execute_in_new_container_over_ssh("bin/rails", "c", host: "app-1")
end
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 } } }
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", env: {})
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|,
new_command.execute_in_new_container_over_ssh("bin/rails", "c", host: "app-1")
end
test "execute in existing container over ssh" do
assert_match %r{docker exec -it app-web-999 bin/rails c},
new_command.execute_in_existing_container_over_ssh("bin/rails", "c", host: "app-1", env: {})
assert_match %r|docker exec -it app-web-999 bin/rails c|,
new_command.execute_in_existing_container_over_ssh("bin/rails", "c", host: "app-1")
end
test "run over ssh" do
@@ -254,14 +225,14 @@ class CommandsAppTest < ActiveSupport::TestCase
test "current_running_container_id" do
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1",
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest",
new_command.current_running_container_id.join(" ")
end
test "current_running_container_id with destination" do
@destination = "staging"
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination=staging --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest-staging --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination=staging --filter label=role=web --filter status=running --filter status=restarting' | head -1",
"docker ps --quiet --filter label=service=app --filter label=destination=staging --filter label=role=web --filter status=running --filter status=restarting --latest",
new_command.current_running_container_id.join(" ")
end
@@ -273,7 +244,7 @@ class CommandsAppTest < ActiveSupport::TestCase
test "current_running_version" do
assert_equal \
"sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done",
"docker ps --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest --format \"{{.Names}}\" | while read line; do echo ${line#app-web-}; done",
new_command.current_running_version.join(" ")
end
@@ -351,17 +322,10 @@ class CommandsAppTest < ActiveSupport::TestCase
new_command.remove_images.join(" ")
end
test "tag_latest_image" do
test "tag_current_image_as_latest" do
assert_equal \
"docker tag dhh/app:999 dhh/app:latest",
new_command.tag_latest_image.join(" ")
end
test "tag_latest_image with destination" do
@destination = "staging"
assert_equal \
"docker tag dhh/app:999 dhh/app:latest-staging",
new_command.tag_latest_image.join(" ")
new_command.tag_current_image_as_latest.join(" ")
end
test "make_env_directory" do
@@ -369,7 +333,7 @@ class CommandsAppTest < ActiveSupport::TestCase
end
test "remove_env_file" do
assert_equal "rm -f .kamal/env/roles/app-web.env", new_command.remove_env_file.join(" ")
assert_equal "rm -f .kamal/env/roles/app-web*.env", new_command.remove_env_files.join(" ")
end
test "cord" do
@@ -390,7 +354,7 @@ class CommandsAppTest < ActiveSupport::TestCase
assert_equal [
:mkdir, "-p", ".kamal/assets/extracted/app-web-999", "&&",
:docker, :stop, "-t 1", "app-web-assets", "2> /dev/null", "|| true", "&&",
:docker, :run, "--name", "app-web-assets", "--detach", "--rm", "dhh/app:999", "sleep 1000000", "&&",
:docker, :run, "--name", "app-web-assets", "--detach", "--rm", "dhh/app:latest", "sleep 1000000", "&&",
:docker, :cp, "-L", "app-web-assets:/public/assets/.", ".kamal/assets/extracted/app-web-999", "&&",
:docker, :stop, "-t 1", "app-web-assets"
], new_command(asset_path: "/public/assets").extract_assets
@@ -406,7 +370,7 @@ class CommandsAppTest < ActiveSupport::TestCase
:mkdir, "-p", ".kamal/assets/volumes/app-web-999", ";",
:cp, "-rnT", ".kamal/assets/extracted/app-web-999", ".kamal/assets/volumes/app-web-999", ";",
:cp, "-rnT", ".kamal/assets/extracted/app-web-999", ".kamal/assets/volumes/app-web-998", "|| true", ";",
:cp, "-rnT", ".kamal/assets/extracted/app-web-998", ".kamal/assets/volumes/app-web-999", "|| true"
:cp, "-rnT", ".kamal/assets/extracted/app-web-998", ".kamal/assets/volumes/app-web-999", "|| true",
], new_command(asset_path: "/public/assets").sync_asset_volumes(old_version: 998)
end
@@ -419,7 +383,6 @@ class CommandsAppTest < ActiveSupport::TestCase
private
def new_command(role: "web", **additional_config)
config = Kamal::Configuration.new(@config.merge(additional_config), destination: @destination, version: "999")
Kamal::Commands::App.new(config, role: config.role(role))
Kamal::Commands::App.new(Kamal::Configuration.new(@config.merge(additional_config), destination: @destination, version: "999"), role: role)
end
end

View File

@@ -9,7 +9,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase
builder = new_builder_command(builder: { "cache" => { "type" => "gha" }})
assert_equal "multiarch", builder.name
assert_equal \
"git archive --format=tar HEAD | docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile -",
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
builder.push.join(" ")
end
@@ -17,7 +17,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase
builder = new_builder_command(builder: { "multiarch" => false })
assert_equal "native", builder.name
assert_equal \
"git archive --format=tar HEAD | docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile - && docker push dhh/app:123 && docker push dhh/app:latest",
"docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile . && docker push dhh/app:123 && docker push dhh/app:latest",
builder.push.join(" ")
end
@@ -25,7 +25,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase
builder = new_builder_command(builder: { "multiarch" => false, "cache" => { "type" => "gha" }})
assert_equal "native/cached", builder.name
assert_equal \
"git archive --format=tar HEAD | docker buildx build --push -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile -",
"docker buildx build --push -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
builder.push.join(" ")
end
@@ -33,7 +33,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase
builder = new_builder_command(builder: { "local" => { }, "remote" => { }, "cache" => { "type" => "gha" } })
assert_equal "multiarch/remote", builder.name
assert_equal \
"git archive --format=tar HEAD | docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch-remote -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile -",
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch-remote -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
builder.push.join(" ")
end
@@ -41,7 +41,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase
builder = new_builder_command(builder: { "local" => { "arch" => "amd64" } })
assert_equal "multiarch", builder.name
assert_equal \
"git archive --format=tar HEAD | docker buildx build --push --platform linux/amd64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile -",
"docker buildx build --push --platform linux/amd64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile .",
builder.push.join(" ")
end
@@ -49,7 +49,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase
builder = new_builder_command(builder: { "remote" => { "arch" => "amd64" }, "cache" => { "type" => "gha" } })
assert_equal "native/remote", builder.name
assert_equal \
"git archive --format=tar HEAD | docker buildx build --push --platform linux/amd64 --builder kamal-app-native-remote -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile -",
"docker buildx build --push --platform linux/amd64 --builder kamal-app-native-remote -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
builder.push.join(" ")
end
@@ -93,26 +93,26 @@ class CommandsBuilderTest < ActiveSupport::TestCase
test "native push with build args" do
builder = new_builder_command(builder: { "multiarch" => false, "args" => { "a" => 1, "b" => 2 } })
assert_equal \
"git archive --format=tar HEAD | docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile - && docker push dhh/app:123 && docker push dhh/app:latest",
"docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile . && docker push dhh/app:123 && docker push dhh/app:latest",
builder.push.join(" ")
end
test "multiarch push with build args" do
builder = new_builder_command(builder: { "args" => { "a" => 1, "b" => 2 } })
assert_equal \
"git archive --format=tar HEAD | docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile -",
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile .",
builder.push.join(" ")
end
test "native push with build secrets" do
builder = new_builder_command(builder: { "multiarch" => false, "secrets" => [ "a", "b" ] })
assert_equal \
"git archive --format=tar HEAD | docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"a\" --secret id=\"b\" --file Dockerfile - && docker push dhh/app:123 && docker push dhh/app:latest",
"docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"a\" --secret id=\"b\" --file Dockerfile . && docker push dhh/app:123 && docker push dhh/app:latest",
builder.push.join(" ")
end
test "build with ssh agent socket" do
builder = new_builder_command(builder: { "ssh" => "default=$SSH_AUTH_SOCK" })
builder = new_builder_command(builder: { "ssh" => 'default=$SSH_AUTH_SOCK' })
assert_equal \
"-t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile --ssh default=$SSH_AUTH_SOCK",
@@ -120,35 +120,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase
end
test "validate image" do
assert_equal "docker inspect -f '{{ .Config.Labels.service }}' dhh/app:123 | grep -x app || (echo \"Image dhh/app:123 is missing the 'service' label\" && exit 1)", new_builder_command.validate_image.join(" ")
end
test "multiarch context build" do
builder = new_builder_command(builder: { "context" => "./foo" })
assert_equal \
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo",
builder.push.join(" ")
end
test "native context build" do
builder = new_builder_command(builder: { "multiarch" => false, "context" => "./foo" })
assert_equal \
"docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo && docker push dhh/app:123 && docker push dhh/app:latest",
builder.push.join(" ")
end
test "cached context build" do
builder = new_builder_command(builder: { "multiarch" => false, "context" => "./foo", "cache" => { "type" => "gha" } })
assert_equal \
"docker buildx build --push -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile ./foo",
builder.push.join(" ")
end
test "remote context build" do
builder = new_builder_command(builder: { "remote" => { "arch" => "amd64" }, "context" => "./foo" })
assert_equal \
"docker buildx build --push --platform linux/amd64 --builder kamal-app-native-remote -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo",
builder.push.join(" ")
assert_equal "docker inspect -f '{{ .Config.Labels.service }}' dhh/app:123 | grep -x app || (echo \"Image dhh/app:123 is missing the `service` label\" && exit 1)", new_builder_command.validate_image.join(" ")
end
private

View File

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

View File

@@ -10,7 +10,7 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase
test "run" do
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.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-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",
new_command.run.join(" ")
end
@@ -18,7 +18,7 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase
@config[:healthcheck] = { "port" => 3001 }
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.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-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",
new_command.run.join(" ")
end
@@ -26,7 +26,7 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase
@destination = "staging"
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.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-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",
new_command.run.join(" ")
end
@@ -34,7 +34,7 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase
@config[:healthcheck] = { "cmd" => "/bin/up" }
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.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-secret.env --env-file .kamal/env/roles/app-web-clear.env --health-cmd \"/bin/up\" --health-interval \"1s\" dhh/app:123",
new_command.run.join(" ")
end
@@ -42,7 +42,7 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere" } } }
@config[:healthcheck] = { "exposed_port" => 4999 }
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.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-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",
new_command.run.join(" ")
end

View File

@@ -10,24 +10,24 @@ class CommandsLockTest < ActiveSupport::TestCase
test "status" do
assert_equal \
"stat .kamal/locks/app-production > /dev/null && cat .kamal/locks/app-production/details | base64 -d",
"stat .kamal/lock-app > /dev/null && cat .kamal/lock-app/details | base64 -d",
new_command.status.join(" ")
end
test "acquire" do
assert_match \
%r{mkdir \.kamal/locks/app-production && echo ".*" > \.kamal/locks/app-production/details}m,
%r{mkdir \.kamal/lock-app && echo ".*" > \.kamal/lock-app/details}m,
new_command.acquire("Hello", "123").join(" ")
end
test "release" do
assert_match \
"rm .kamal/locks/app-production/details && rm -r .kamal/locks/app-production",
"rm .kamal/lock-app/details && rm -r .kamal/lock-app",
new_command.release.join(" ")
end
private
def new_command
Kamal::Commands::Lock.new(Kamal::Configuration.new(@config, version: "123", destination: "production"))
Kamal::Commands::Lock.new(Kamal::Configuration.new(@config, version: "123"))
end
end

View File

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

View File

@@ -18,72 +18,72 @@ class CommandsTraefikTest < ActiveSupport::TestCase
test "run" do
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.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-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\"",
new_command.run.join(" ")
@config[:traefik]["host_port"] = "8080"
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.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-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\"",
new_command.run.join(" ")
@config[:traefik]["publish"] = false
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.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-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\"",
new_command.run.join(" ")
end
test "run with ports configured" do
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.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-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\"",
new_command.run.join(" ")
@config[:traefik]["options"] = {"publish" => %w[9000:9000 9001:9001]}
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.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-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\"",
new_command.run.join(" ")
end
test "run with volumes configured" do
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.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-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\"",
new_command.run.join(" ")
@config[:traefik]["options"] = {"volume" => %w[./letsencrypt/acme.json:/letsencrypt/acme.json] }
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.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-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\"",
new_command.run.join(" ")
end
test "run with several options configured" do
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.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-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\"",
new_command.run.join(" ")
@config[:traefik]["options"] = {"volume" => %w[./letsencrypt/acme.json:/letsencrypt/acme.json], "publish" => %w[8080:8080], "memory" => "512m"}
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.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-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\"",
new_command.run.join(" ")
end
test "run with labels configured" do
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.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-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\"",
new_command.run.join(" ")
@config[:traefik]["labels"] = { "traefik.http.routers.dashboard.service" => "api@internal", "traefik.http.routers.dashboard.middlewares" => "auth" }
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.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-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\"",
new_command.run.join(" ")
end
test "run with env configured" do
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.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-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\"",
new_command.run.join(" ")
@config[:traefik]["env"] = { "secret" => %w[EXAMPLE_API_KEY] }
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.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-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\"",
new_command.run.join(" ")
end
@@ -91,7 +91,7 @@ class CommandsTraefikTest < ActiveSupport::TestCase
@config.delete(:traefik)
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.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-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\"",
new_command.run.join(" ")
end
@@ -99,7 +99,7 @@ class CommandsTraefikTest < ActiveSupport::TestCase
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
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.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-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\"",
new_command.run.join(" ")
end
@@ -107,7 +107,7 @@ class CommandsTraefikTest < ActiveSupport::TestCase
@config[:traefik]["args"]["log.level"] = "ERROR"
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.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-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\"",
new_command.run.join(" ")
end
@@ -138,7 +138,7 @@ class CommandsTraefikTest < ActiveSupport::TestCase
test "traefik logs since 2h" do
assert_equal \
"docker logs traefik --since 2h --timestamps 2>&1",
new_command.logs(since: "2h").join(" ")
new_command.logs(since: '2h').join(" ")
end
test "traefik logs last 10 lines" do
@@ -150,7 +150,7 @@ class CommandsTraefikTest < ActiveSupport::TestCase
test "traefik logs with grep hello!" do
assert_equal \
"docker logs traefik --timestamps 2>&1 | grep 'hello!'",
new_command.logs(grep: "hello!").join(" ")
new_command.logs(grep: 'hello!').join(" ")
end
test "traefik remove container" do
@@ -174,21 +174,29 @@ class CommandsTraefikTest < ActiveSupport::TestCase
test "traefik follow logs with grep hello!" do
assert_equal \
"ssh -t root@1.1.1.1 -p 22 'docker logs traefik --timestamps --tail 10 --follow 2>&1 | grep \"hello!\"'",
new_command.follow_logs(host: @config[:servers].first, grep: "hello!")
new_command.follow_logs(host: @config[:servers].first, grep: 'hello!')
end
test "secrets io" do
test "env_file" do
@config[:traefik]["env"] = { "secret" => %w[EXAMPLE_API_KEY] }
assert_equal "EXAMPLE_API_KEY=456\n", new_command.env.secrets_io.string
assert_equal "EXAMPLE_API_KEY=456\n", new_command.env_file.secret
end
test "host_secret_env_file_path" do
assert_equal ".kamal/env/traefik/traefik-secret.env", new_command.host_secret_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
test "make_env_directory" do
assert_equal "mkdir -p .kamal/env/traefik", new_command.make_env_directory.join(" ")
end
test "remove_env_file" do
assert_equal "rm -f .kamal/env/traefik/traefik.env", new_command.remove_env_file.join(" ")
test "remove_env_files" do
assert_equal "rm -f .kamal/env/traefik/traefik*.env", new_command.remove_env_files.join(" ")
end
private

View File

@@ -20,7 +20,7 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
},
"secret" => [
"MYSQL_ROOT_PASSWORD"
]
],
},
"files" => [
"config/mysql/my.cnf:/etc/mysql/my.cnf",
@@ -107,35 +107,47 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
assert_equal "Specify one of `host`, `hosts` or `roles` for accessory `mysql`", exception.message
end
test "all hosts" do
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4", "1.1.1.5", "1.1.1.6", "1.1.1.7" ], @config.all_hosts
end
test "label args" do
assert_equal ["--label", "service=\"app-mysql\""], @config.accessory(:mysql).label_args
assert_equal ["--label", "service=\"app-redis\"", "--label", "cache=\"true\""], @config.accessory(:redis).label_args
end
test "env args" do
assert_equal [ "--env-file", ".kamal/env/accessories/app-mysql.env", "--env", "MYSQL_ROOT_HOST=\"%\"" ], @config.accessory(:mysql).env_args
assert_equal [ "--env-file", ".kamal/env/accessories/app-redis.env", "--env", "SOMETHING=\"else\"" ], @config.accessory(:redis).env_args
assert_equal \
["--env-file", ".kamal/env/accessories/app-mysql-secret.env", "--env-file", ".kamal/env/accessories/app-mysql-clear.env"],
@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
test "env with secrets" do
test "env file with secret" do
ENV["MYSQL_ROOT_PASSWORD"] = "secret123"
expected_secrets_file = <<~ENV
expected_secret = <<~ENV
MYSQL_ROOT_PASSWORD=secret123
ENV
assert_equal expected_secrets_file, @config.accessory(:mysql).env.secrets_io.string
assert_equal [ "--env-file", ".kamal/env/accessories/app-mysql.env", "--env", "MYSQL_ROOT_HOST=\"%\"" ], @config.accessory(:mysql).env_args
expected_clear = <<~ENV
MYSQL_ROOT_HOST=%
ENV
assert_equal expected_secret, @config.accessory(:mysql).env_file.secret
assert_equal expected_clear, @config.accessory(:mysql).env_file.clear
ensure
ENV["MYSQL_ROOT_PASSWORD"] = nil
end
test "env secrets path" do
assert_equal ".kamal/env/accessories/app-mysql.env", @config.accessory(:mysql).env.secrets_file
test "host_env_directory" do
assert_equal ".kamal/env/accessories", @config.accessory(:mysql).host_env_directory
end
test "host_secret_env_file_path" do
assert_equal ".kamal/env/accessories/app-mysql-secret.env", @config.accessory(:mysql).host_secret_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
test "volume args" do

View File

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

View File

@@ -1,74 +0,0 @@
require "test_helper"
class ConfigurationEnvTest < ActiveSupport::TestCase
require "test_helper"
test "simple" do
assert_config \
config: { "foo" => "bar", "baz" => "haz" },
clear: { "foo" => "bar", "baz" => "haz" },
secrets: {}
end
test "clear" do
assert_config \
config: { "clear" => { "foo" => "bar", "baz" => "haz" } },
clear: { "foo" => "bar", "baz" => "haz" },
secrets: {}
end
test "secret" do
ENV["PASSWORD"] = "hello"
env = Kamal::Configuration::Env.from_config config: { "secret" => [ "PASSWORD" ] }
assert_config \
config: { "secret" => [ "PASSWORD" ] },
clear: {},
secrets: { "PASSWORD" => "hello" }
ensure
ENV.delete "PASSWORD"
end
test "missing secret" do
env = {
"secret" => [ "PASSWORD" ]
}
assert_raises(KeyError) { Kamal::Configuration::Env.from_config(config: { "secret" => [ "PASSWORD" ] }).secrets }
end
test "secret and clear" do
ENV["PASSWORD"] = "hello"
config = {
"secret" => [ "PASSWORD" ],
"clear" => {
"foo" => "bar",
"baz" => "haz"
}
}
assert_config \
config: config,
clear: { "foo" => "bar", "baz" => "haz" },
secrets: { "PASSWORD" => "hello" }
ensure
ENV.delete "PASSWORD"
end
test "stringIO conversion" do
env = {
"foo" => "bar",
"baz" => "haz"
}
assert_equal "foo=bar\nbaz=haz\n", \
StringIO.new(Kamal::EnvFile.new(env)).read
end
private
def assert_config(config:, clear:, secrets:)
env = Kamal::Configuration::Env.from_config config: config
assert_equal clear, env.clear
assert_equal secrets, env.secrets
end
end

View File

@@ -38,11 +38,11 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
end
test "label args" do
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"workers\"", "--label", "destination" ], @config_with_roles.role(:workers).label_args
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"workers\"" ], @config_with_roles.role(:workers).label_args
end
test "special label args for web" do
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"web\"", "--label", "destination", "--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\"" ], @config.role(:web).label_args
assert_equal [ "--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\"" ], @config.role(:web).label_args
end
test "custom labels" do
@@ -66,14 +66,19 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
c[:servers]["beta"] = { "traefik" => "true", "hosts" => [ "1.1.1.5" ] }
})
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"beta\"", "--label", "destination", "--label", "traefik.http.services.app-beta.loadbalancer.server.scheme=\"http\"", "--label", "traefik.http.routers.app-beta.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.routers.app-beta.priority=\"2\"", "--label", "traefik.http.middlewares.app-beta-retry.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app-beta-retry.retry.initialinterval=\"500ms\"", "--label", "traefik.http.routers.app-beta.middlewares=\"app-beta-retry@docker\"" ], config.role(:beta).label_args
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"beta\"", "--label", "traefik.http.services.app-beta.loadbalancer.server.scheme=\"http\"", "--label", "traefik.http.routers.app-beta.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.routers.app-beta.priority=\"2\"", "--label", "traefik.http.middlewares.app-beta-retry.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app-beta-retry.retry.initialinterval=\"500ms\"", "--label", "traefik.http.routers.app-beta.middlewares=\"app-beta-retry@docker\"" ], config.role(:beta).label_args
end
test "env overwritten by role" do
assert_equal "redis://a/b", @config_with_roles.role(:workers).env.clear["REDIS_URL"]
assert_equal "redis://a/b", @config_with_roles.role(:workers).env["REDIS_URL"]
assert_equal "\n", @config_with_roles.role(:workers).env.secrets_io.string
assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], @config_with_roles.role(:workers).env_args
expected_env = <<~ENV
REDIS_URL=redis://a/b
WEB_CONCURRENCY=4
ENV
assert_equal "\n", @config_with_roles.role(:workers).env_file.secret
assert_equal expected_env, @config_with_roles.role(:workers).env_file.clear
end
test "container name" do
@@ -86,7 +91,9 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
end
test "env args" do
assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], @config_with_roles.role(:workers).env_args
assert_equal \
["--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
test "env secret overwritten by role" do
@@ -112,13 +119,18 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
ENV["REDIS_PASSWORD"] = "secret456"
ENV["DB_PASSWORD"] = "secret&\"123"
expected_secrets_file = <<~ENV
expected_secret = <<~ENV
REDIS_PASSWORD=secret456
DB_PASSWORD=secret&\"123
ENV
assert_equal expected_secrets_file, @config_with_roles.role(:workers).env.secrets_io.string
assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], @config_with_roles.role(:workers).env_args
expected_clear = <<~ENV
REDIS_URL=redis://a/b
WEB_CONCURRENCY=4
ENV
assert_equal expected_secret, @config_with_roles.role(:workers).env_file.secret
assert_equal expected_clear, @config_with_roles.role(:workers).env_file.clear
ensure
ENV["REDIS_PASSWORD"] = nil
ENV["DB_PASSWORD"] = nil
@@ -137,12 +149,17 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
ENV["DB_PASSWORD"] = "secret123"
expected_secrets_file = <<~ENV
expected_secret = <<~ENV
DB_PASSWORD=secret123
ENV
assert_equal expected_secrets_file, @config_with_roles.role(:workers).env.secrets_io.string
assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], @config_with_roles.role(:workers).env_args
expected_clear = <<~ENV
REDIS_URL=redis://a/b
WEB_CONCURRENCY=4
ENV
assert_equal expected_secret, @config_with_roles.role(:workers).env_file.secret
assert_equal expected_clear, @config_with_roles.role(:workers).env_file.clear
ensure
ENV["DB_PASSWORD"] = nil
end
@@ -159,12 +176,17 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
ENV["REDIS_PASSWORD"] = "secret456"
expected_secrets_file = <<~ENV
expected_secret = <<~ENV
REDIS_PASSWORD=secret456
ENV
assert_equal expected_secrets_file, @config_with_roles.role(:workers).env.secrets_io.string
assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], @config_with_roles.role(:workers).env_args
expected_clear = <<~ENV
REDIS_URL=redis://a/b
WEB_CONCURRENCY=4
ENV
assert_equal expected_secret, @config_with_roles.role(:workers).env_file.secret
assert_equal expected_clear, @config_with_roles.role(:workers).env_file.clear
ensure
ENV["REDIS_PASSWORD"] = nil
end
@@ -181,29 +203,41 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
@deploy_with_roles[:servers]["workers"]["env"] = {
"clear" => {
"REDIS_URL" => "redis://c/d"
}
"REDIS_URL" => "redis://c/d",
},
}
ENV["REDIS_PASSWORD"] = "secret456"
expected_secrets_file = <<~ENV
expected_secret = <<~ENV
REDIS_PASSWORD=secret456
ENV
assert_equal expected_secrets_file, @config_with_roles.role(:workers).env.secrets_io.string
assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://c/d\"" ], @config_with_roles.role(:workers).env_args
expected_clear = <<~ENV
REDIS_URL=redis://c/d
ENV
assert_equal expected_secret, @config_with_roles.role(:workers).env_file.secret
assert_equal expected_clear, @config_with_roles.role(:workers).env_file.clear
ensure
ENV["REDIS_PASSWORD"] = nil
end
test "env secrets_file" do
assert_equal ".kamal/env/roles/app-workers.env", @config_with_roles.role(:workers).env.secrets_file
test "host_env_directory" do
assert_equal ".kamal/env/roles", @config_with_roles.role(:workers).host_env_directory
end
test "host_secret_env_file_path" do
assert_equal ".kamal/env/roles/app-workers-secret.env", @config_with_roles.role(:workers).host_secret_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
test "uses cord" do
assert @config_with_roles.role(:web).uses_cord?
assert_not @config_with_roles.role(:workers).uses_cord?
assert !@config_with_roles.role(:workers).uses_cord?
end
test "cord host file" do
@@ -227,8 +261,8 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
assert_nil @config_with_roles.role(:workers).asset_volume_args
assert_nil @config_with_roles.role(:web).asset_path
assert_nil @config_with_roles.role(:workers).asset_path
assert_not @config_with_roles.role(:web).assets?
assert_not @config_with_roles.role(:workers).assets?
assert !@config_with_roles.role(:web).assets?
assert !@config_with_roles.role(:workers).assets?
config_with_assets = Kamal::Configuration.new(@deploy_with_roles.dup.tap { |c|
c[:asset_path] = "foo"
@@ -238,7 +272,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
assert_equal ["--volume", "$(pwd)/.kamal/assets/volumes/app-web-12345:foo"], config_with_assets.role(:web).asset_volume_args
assert_nil config_with_assets.role(:workers).asset_volume_args
assert config_with_assets.role(:web).assets?
assert_not config_with_assets.role(:workers).assets?
assert !config_with_assets.role(:workers).assets?
config_with_assets = Kamal::Configuration.new(@deploy_with_roles.dup.tap { |c|
c[:servers]["web"] = { "hosts" => [ "1.1.1.1", "1.1.1.2" ], "asset_path" => "bar" }
@@ -248,7 +282,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
assert_equal ["--volume", "$(pwd)/.kamal/assets/volumes/app-web-12345:bar"], config_with_assets.role(:web).asset_volume_args
assert_nil config_with_assets.role(:workers).asset_volume_args
assert config_with_assets.role(:web).assets?
assert_not config_with_assets.role(:workers).assets?
assert !config_with_assets.role(:workers).assets?
ensure
ENV.delete("VERSION")

View File

@@ -44,7 +44,6 @@ class ConfigurationTest < ActiveSupport::TestCase
test "service name valid" do
assert Kamal::Configuration.new(@deploy.tap { _1[:service] = "hey-app1_primary" }).valid?
assert Kamal::Configuration.new(@deploy.tap { _1[:service] = "MyApp" }).valid?
end
test "service name invalid" do
@@ -104,17 +103,7 @@ class ConfigurationTest < ActiveSupport::TestCase
Kamal::Git.expects(:revision).returns("git-version")
Kamal::Git.expects(:uncommitted_changes).returns("M file\n")
assert_equal "git-version", @config.version
end
test "version from uncommitted context" do
ENV.delete("VERSION")
config = Kamal::Configuration.new(@deploy.tap { |c| c[:builder] = { "context" => "." } })
Kamal::Git.expects(:revision).returns("git-version")
Kamal::Git.expects(:uncommitted_changes).returns("M file\n")
assert_match /^git-version_uncommitted_[0-9a-f]{16}$/, config.version
assert_match /^git-version_uncommitted_[0-9a-f]{16}$/, @config.version
end
test "version from env" do
@@ -251,19 +240,19 @@ class ConfigurationTest < ActiveSupport::TestCase
test "to_h" do
expected_config = \
{ roles: [ "web" ],
hosts: [ "1.1.1.1", "1.1.1.2" ],
primary_host: "1.1.1.1",
version: "missing",
repository: "dhh/app",
absolute_image: "dhh/app:missing",
service_with_version: "app-missing",
ssh_options: { user: "root", port: 22, log_level: :fatal, keepalive: true, keepalive_interval: 30 },
sshkit: {},
volume_args: [ "--volume", "/local/path:/container/path" ],
builder: {},
logging: [ "--log-opt", "max-size=\"10m\"" ],
healthcheck: { "path"=>"/up", "port"=>3000, "max_attempts" => 7, "exposed_port" => 3999, "cord" => "/tmp/kamal-cord", "log_lines" => 50 } }
{ :roles=>["web"],
:hosts=>["1.1.1.1", "1.1.1.2"],
:primary_host=>"1.1.1.1",
:version=>"missing",
:repository=>"dhh/app",
:absolute_image=>"dhh/app:missing",
:service_with_version=>"app-missing",
:ssh_options=>{ :user=>"root", port: 22, log_level: :fatal, keepalive: true, keepalive_interval: 30 },
:sshkit=>{},
:volume_args=>["--volume", "/local/path:/container/path"],
:builder=>{},
:logging=>["--log-opt", "max-size=\"10m\""],
:healthcheck=>{ "path"=>"/up", "port"=>3000, "max_attempts" => 7, "exposed_port" => 3999, "cord" => "/tmp/kamal-cord", "log_lines" => 50 }}
assert_equal expected_config, @config.to_h
end
@@ -311,14 +300,14 @@ class ConfigurationTest < ActiveSupport::TestCase
end
test "primary role" do
assert_equal "web", @config.primary_role.name
assert_equal "web", @config.primary_role
config = Kamal::Configuration.new(@deploy_with_roles.deep_merge({
servers: { "alternate_web" => { "hosts" => [ "1.1.1.4", "1.1.1.5" ] } },
primary_role: "alternate_web" } ))
assert_equal "alternate_web", config.primary_role.name
assert_equal "alternate_web", config.primary_role
assert_equal "1.1.1.4", config.primary_host
assert config.role(:alternate_web).primary?
assert config.role(:alternate_web).running_traefik?
@@ -338,4 +327,13 @@ class ConfigurationTest < ActiveSupport::TestCase
assert_raises(ArgumentError) { Kamal::Configuration.new(@deploy_with_roles.merge(retain_containers: 0)) }
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

View File

@@ -1,76 +0,0 @@
require "test_helper"
class EnvFileTest < ActiveSupport::TestCase
test "to_s" do
env = {
"foo" => "bar",
"baz" => "haz"
}
assert_equal "foo=bar\nbaz=haz\n", \
Kamal::EnvFile.new(env).to_s
end
test "to_str won't escape chinese characters" do
env = {
"foo" => '你好 means hello, "欢迎" means welcome, that\'s simple! 😃 {smile}'
}
assert_equal "foo=你好 means hello, \"欢迎\" means welcome, that's simple! 😃 {smile}\n",
Kamal::EnvFile.new(env).to_s
end
test "to_s won't escape japanese characters" do
env = {
"foo" => 'こんにちは means hello, "ようこそ" means welcome, that\'s simple! 😃 {smile}'
}
assert_equal "foo=こんにちは means hello, \"ようこそ\" means welcome, that's simple! 😃 {smile}\n", \
Kamal::EnvFile.new(env).to_s
end
test "to_s won't escape korean characters" do
env = {
"foo" => '안녕하세요 means hello, "어서 오십시오" means welcome, that\'s simple! 😃 {smile}'
}
assert_equal "foo=안녕하세요 means hello, \"어서 오십시오\" means welcome, that's simple! 😃 {smile}\n", \
Kamal::EnvFile.new(env).to_s
end
test "to_s empty" do
assert_equal "\n", Kamal::EnvFile.new({}).to_s
end
test "to_s escaped newline" do
env = {
"foo" => "hello\\nthere"
}
assert_equal "foo=hello\\\\nthere\n", \
Kamal::EnvFile.new(env).to_s
ensure
ENV.delete "PASSWORD"
end
test "to_s newline" do
env = {
"foo" => "hello\nthere"
}
assert_equal "foo=hello\\nthere\n", \
Kamal::EnvFile.new(env).to_s
ensure
ENV.delete "PASSWORD"
end
test "stringIO conversion" do
env = {
"foo" => "bar",
"baz" => "haz"
}
assert_equal "foo=bar\nbaz=haz\n", \
StringIO.new(Kamal::EnvFile.new(env)).read
end
end

102
test/env_files_test.rb Normal file
View File

@@ -0,0 +1,102 @@
require "test_helper"
class EnvFilesTest < ActiveSupport::TestCase
test "env file simple" do
env = {
"foo" => "bar",
"baz" => "haz"
}
assert_equal "foo=bar\nbaz=haz\n", Kamal::EnvFiles.new(env).clear
assert_equal "\n", Kamal::EnvFiles.new(env).secret
end
test "env file clear" do
env = {
"clear" => {
"foo" => "bar",
"baz" => "haz"
}
}
assert_equal "foo=bar\nbaz=haz\n", Kamal::EnvFiles.new(env).clear
assert_equal "\n", Kamal::EnvFiles.new(env).secret
end
test "env file empty" do
assert_equal "\n", Kamal::EnvFiles.new({}).secret
assert_equal "\n", Kamal::EnvFiles.new({}).clear
end
test "env file secret" do
ENV["PASSWORD"] = "hello"
env = {
"secret" => [ "PASSWORD" ]
}
assert_equal "PASSWORD=hello\n", Kamal::EnvFiles.new(env).secret
assert_equal "\n", Kamal::EnvFiles.new(env).clear
ensure
ENV.delete "PASSWORD"
end
test "env file secret escaped newline" do
ENV["PASSWORD"] = "hello\\nthere"
env = {
"secret" => [ "PASSWORD" ]
}
assert_equal "PASSWORD=hello\\\\nthere\n", Kamal::EnvFiles.new(env).secret
ensure
ENV.delete "PASSWORD"
end
test "env file secret newline" do
ENV["PASSWORD"] = "hello\nthere"
env = {
"secret" => [ "PASSWORD" ]
}
assert_equal "PASSWORD=hello\\nthere\n", Kamal::EnvFiles.new(env).secret
ensure
ENV.delete "PASSWORD"
end
test "env file missing secret" do
env = {
"secret" => [ "PASSWORD" ]
}
assert_raises(KeyError) { Kamal::EnvFiles.new(env).secret }
ensure
ENV.delete "PASSWORD"
end
test "env file secret and clear" do
ENV["PASSWORD"] = "hello"
env = {
"secret" => [ "PASSWORD" ],
"clear" => {
"foo" => "bar",
"baz" => "haz"
}
}
assert_equal "PASSWORD=hello\n", Kamal::EnvFiles.new(env).secret
assert_equal "foo=bar\nbaz=haz\n", Kamal::EnvFiles.new(env).clear
ensure
ENV.delete "PASSWORD"
end
test "stringIO conversion" do
env = {
"foo" => "bar",
"baz" => "haz"
}
assert_equal "foo=bar\nbaz=haz\n", \
StringIO.new(Kamal::EnvFiles.new(env).clear).read
end
end

37
test/fixtures/deploy_push_clear_env.yml vendored Normal file
View File

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

@@ -1,17 +0,0 @@
service: app
image: dhh/app
servers:
web:
- "1.1.1.1"
- "1.1.1.2"
workers:
- "1.1.1.3"
- "1.1.1.4"
registry:
username: user
password: pw
boot:
limit: 1%
wait: 2

View File

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

View File

@@ -31,7 +31,7 @@ class AccessoryTest < IntegrationTest
end
def assert_accessory_not_running(name)
assert_no_match /registry:4443\/busybox:1.36.0 "sh -c 'echo \\"Start/, accessory_details(name)
refute_match /registry:4443\/busybox:1.36.0 "sh -c 'echo \\"Start/, accessory_details(name)
end
def accessory_details(name)

View File

@@ -18,7 +18,7 @@ services:
build:
context: docker/deployer
environment:
- TEST_ID=${TEST_ID:-}
- TEST_ID=${TEST_ID}
volumes:
- ../..:/kamal
- shared:/shared
@@ -50,19 +50,8 @@ services:
volumes:
- shared:/shared
vm3:
privileged: true
build:
context: docker/vm
volumes:
- shared:/shared
load_balancer:
build:
context: docker/load_balancer
ports:
- "12345:80"
depends_on:
- vm1
- vm2
- vm3

View File

@@ -1,6 +1,6 @@
FROM ruby:3.2
WORKDIR /
WORKDIR /app
ENV VERBOSE=true
@@ -17,8 +17,7 @@ RUN echo \
RUN apt-get update --fix-missing && apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
COPY *.sh .
COPY app/ app/
COPY app_with_roles/ app_with_roles/
COPY app/ .
RUN rm -rf /root/.ssh
RUN ln -s /shared/ssh /root/.ssh
@@ -26,8 +25,7 @@ RUN mkdir -p /etc/docker/certs.d/registry:4443 && ln -s /shared/certs/domain.crt
RUN git config --global user.email "deployer@example.com"
RUN git config --global user.name "Deployer"
RUN cd app && git init && echo ".env" >> .gitignore && git add . && git commit -am "Initial version"
RUN cd app_with_roles && git init && echo ".env" >> .gitignore && git add . && git commit -am "Initial version"
RUN git init && echo ".env" >> .gitignore && git add . && git commit -am "Initial version"
HEALTHCHECK --interval=1s CMD pgrep sleep

View File

@@ -1 +1 @@
SECRET_TOKEN='1234 with "中文"'
SECRET_TOKEN=1234

View File

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

View File

@@ -5,8 +5,7 @@ servers:
- vm2
env:
clear:
CLEAR_TOKEN: 4321
HOST_TOKEN: "${HOST_TOKEN}"
CLEAR_TOKEN: '4321'
secret:
- SECRET_TOKEN
asset_path: /usr/share/nginx/html/versions
@@ -34,3 +33,4 @@ accessories:
roles:
- web
stop_wait_time: 1
push_env: clear

View File

@@ -1 +0,0 @@
SECRET_TOKEN='1234 with "中文"'

View File

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

View File

@@ -1,3 +0,0 @@
#!/bin/sh
echo "Deployed!"
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/post-deploy

View File

@@ -1,3 +0,0 @@
#!/bin/sh
echo "Rebooted Traefik on ${KAMAL_HOSTS}"
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/post-traefik-reboot

View File

@@ -1,3 +0,0 @@
#!/bin/sh
echo "About to build and push..."
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-build

View File

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

View File

@@ -1,3 +0,0 @@
#!/bin/sh
echo "Deployed!"
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-deploy

View File

@@ -1,3 +0,0 @@
#!/bin/sh
echo "Rebooting Traefik on ${KAMAL_HOSTS}..."
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-traefik-reboot

View File

@@ -1,9 +0,0 @@
FROM registry:4443/nginx:1-alpine-slim
COPY default.conf /etc/nginx/conf.d/default.conf
ARG COMMIT_SHA
RUN echo $COMMIT_SHA > /usr/share/nginx/html/version
RUN mkdir -p /usr/share/nginx/html/versions && echo "version" > /usr/share/nginx/html/versions/$COMMIT_SHA
RUN mkdir -p /usr/share/nginx/html/versions && echo "hidden" > /usr/share/nginx/html/versions/.hidden

View File

@@ -1,37 +0,0 @@
service: app
image: app
servers:
web:
hosts:
- vm1
- vm2
workers:
hosts:
- vm3
cmd: sleep infinity
asset_path: /usr/share/nginx/html/versions
registry:
server: registry:4443
username: root
password: root
builder:
multiarch: false
args:
COMMIT_SHA: <%= `git rev-parse HEAD` %>
healthcheck:
cmd: wget -qO- http://localhost > /dev/null || exit 1
traefik:
args:
accesslog: true
accesslog.format: json
image: registry:4443/traefik:v2.10
accessories:
busybox:
service: custom-busybox
image: registry:4443/busybox:1.36.0
cmd: sh -c 'echo "Starting busybox..."; trap exit term; while true; do sleep 1; done'
roles:
- web
stop_wait_time: 1

View File

@@ -1,17 +0,0 @@
server {
listen 80;
listen [::]:80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

View File

@@ -1,3 +1,3 @@
#!/bin/bash
cd $1 && git commit -am 'Update rev' --amend
git commit -am 'Update rev' --amend

View File

@@ -7,8 +7,6 @@ RUN apt-get update --fix-missing && apt-get -y install openssh-client openssh-se
RUN mkdir /root/.ssh && ln -s /shared/ssh/id_rsa.pub /root/.ssh/authorized_keys
RUN mkdir -p /etc/docker/certs.d/registry:4443 && ln -s /shared/certs/domain.crt /etc/docker/certs.d/registry:4443/ca.crt
RUN echo "HOST_TOKEN=abcd" >> /etc/environment
COPY boot.sh .
HEALTHCHECK --interval=1s CMD pgrep dockerd

View File

@@ -7,7 +7,6 @@ class IntegrationTest < ActiveSupport::TestCase
docker_compose "up --build -d"
wait_for_healthy
setup_deployer
@app = "app"
end
teardown do
@@ -35,9 +34,8 @@ class IntegrationTest < ActiveSupport::TestCase
result
end
def deployer_exec(*commands, workdir: nil, **options)
workdir ||= "/#{@app}"
docker_compose("exec --workdir #{workdir} deployer #{commands.join(" ")}", **options)
def deployer_exec(*commands, **options)
docker_compose("exec deployer #{commands.join(" ")}", **options)
end
def kamal(*commands, **options)
@@ -74,7 +72,7 @@ class IntegrationTest < ActiveSupport::TestCase
end
def update_app_rev
deployer_exec "./update_app_rev.sh #{@app}", workdir: "/"
deployer_exec "./update_app_rev.sh"
latest_app_version
end
@@ -117,7 +115,7 @@ class IntegrationTest < ActiveSupport::TestCase
end
def setup_deployer
deployer_exec("./setup.sh", workdir: "/") unless $DEPLOYER_SETUP
deployer_exec("./setup.sh") unless $DEPLOYER_SETUP
$DEPLOYER_SETUP = true
end

View File

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

View File

@@ -3,9 +3,11 @@ require_relative "integration_test"
class MainTest < IntegrationTest
test "envify, deploy, redeploy, rollback, details and audit" do
kamal :envify
assert_local_env_file "SECRET_TOKEN='1234 with \"中文\"'"
assert_remote_env_file "SECRET_TOKEN=1234 with \"中文\""
assert_local_env_file "SECRET_TOKEN=1234"
assert_remote_env_file "CLEAR_TOKEN=4321", :clear
assert_remote_env_file "SECRET_TOKEN=1234", :secret
remove_local_env_file
remove_remote_env_file :clear
first_version = latest_app_version
@@ -14,9 +16,7 @@ class MainTest < IntegrationTest
kamal :deploy
assert_app_is_up version: first_version
assert_hooks_ran "pre-connect", "pre-build", "pre-deploy", "post-deploy"
assert_env :CLEAR_TOKEN, "4321", version: first_version
assert_env :HOST_TOKEN, "abcd", version: first_version
assert_env :SECRET_TOKEN, "1234 with \"中文\"", version: first_version
assert_remote_env_file "CLEAR_TOKEN=4321", :clear
second_version = update_app_rev
@@ -42,23 +42,7 @@ class MainTest < IntegrationTest
assert_match /Booted app version #{first_version}.*Booted app version #{second_version}.*Booted app version #{first_version}.*/m, audit
kamal :env, :delete
assert_no_remote_env_file
end
test "app with roles" do
@app = "app_with_roles"
kamal :envify
version = latest_app_version
assert_app_is_down
kamal :deploy
assert_app_is_up version: version
assert_hooks_ran "pre-connect", "pre-build", "pre-deploy", "post-deploy"
assert_container_running host: :vm3, name: "app-workers-#{version}"
assert_no_remote_env_files
end
test "config" do
@@ -79,38 +63,26 @@ class MainTest < IntegrationTest
assert_equal({ "path" => "/up", "port" => 3000, "max_attempts" => 7, "exposed_port" => 3999, "cord"=>"/tmp/kamal-cord", "log_lines" => 50, "cmd"=>"wget -qO- http://localhost > /dev/null || exit 1" }, config[:healthcheck])
end
test "setup and remove" do
# Check remove completes when nothing has been setup yet
kamal :remove, "-y"
assert_no_images_or_containers
kamal :envify
kamal :setup
assert_images_and_containers
kamal :remove, "-y"
assert_no_images_or_containers
end
private
def assert_local_env_file(contents)
assert_equal contents, deployer_exec("cat .env", capture: true)
end
def assert_env(key, value, version:)
assert_equal "#{key}=#{value}", docker_compose("exec vm1 docker exec app-web-#{version} env | grep #{key}", capture: true)
end
def remove_local_env_file
deployer_exec("rm .env")
end
def assert_remote_env_file(contents)
assert_equal contents, docker_compose("exec vm1 cat /root/.kamal/env/roles/app-web.env", capture: true)
def remove_remote_env_file(env_type)
docker_compose("exec vm1 cat /root/.kamal/env/roles/app-web-#{env_type}.env")
end
def assert_no_remote_env_file
assert_equal "nofile", docker_compose("exec vm1 stat /root/.kamal/env/roles/app-web.env 2> /dev/null || echo nofile", capture: true)
def assert_remote_env_file(contents, env_type)
assert_equal contents, docker_compose("exec vm1 cat /root/.kamal/env/roles/app-web-#{env_type}.env", 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
def assert_accumulated_assets(*versions)
@@ -120,26 +92,4 @@ class MainTest < IntegrationTest
assert_equal "200", Net::HTTP.get_response(URI.parse("http://localhost:12345/versions/.hidden")).code
end
def vm1_image_ids
docker_compose("exec vm1 docker image ls -q", capture: true).strip.split("\n")
end
def vm1_container_ids
docker_compose("exec vm1 docker ps -a -q", capture: true).strip.split("\n")
end
def assert_no_images_or_containers
assert vm1_image_ids.empty?
assert vm1_container_ids.empty?
end
def assert_images_and_containers
assert vm1_image_ids.any?
assert vm1_container_ids.any?
end
def assert_container_running(host:, name:)
assert docker_compose("exec #{host} docker ps --filter=name=#{name} -q", capture: true).strip.present?
end
end

View File

@@ -7,13 +7,13 @@ class TraefikTest < IntegrationTest
kamal :traefik, :boot
assert_traefik_running
output = kamal :traefik, :reboot, "-y", "--verbose", capture: true
output = kamal :traefik, :reboot, capture: true
assert_traefik_running
assert_hooks_ran "pre-traefik-reboot", "post-traefik-reboot"
assert_match /Rebooting Traefik on vm1,vm2.../, output
assert_match /Rebooted Traefik on vm1,vm2/, output
output = kamal :traefik, :reboot, "--rolling", "-y", "--verbose", capture: true
output = kamal :traefik, :reboot, :"--rolling", capture: true
assert_traefik_running
assert_hooks_ran "pre-traefik-reboot", "post-traefik-reboot"
assert_match /Rebooting Traefik on vm1.../, output
@@ -56,7 +56,7 @@ class TraefikTest < IntegrationTest
end
def assert_traefik_not_running
assert_no_match /traefik:v2.10 "\/entrypoint.sh/, traefik_details
refute_match /traefik:v2.10 "\/entrypoint.sh/, traefik_details
end
def traefik_details