Compare commits
125 Commits
v0.16.0
...
return-502
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e25d8a012 | ||
|
|
24a2f51641 | ||
|
|
8f53104d00 | ||
|
|
78fc91f2ec | ||
|
|
dd748fac8c | ||
|
|
b732b2dd55 | ||
|
|
e3254b2aa8 | ||
|
|
e9269d2ee8 | ||
|
|
d2214b43b7 | ||
|
|
370481921e | ||
|
|
aa23f26330 | ||
|
|
f4933d83bf | ||
|
|
6c36c82153 | ||
|
|
8ca04032a1 | ||
|
|
2fb22c934b | ||
|
|
f6662c7a8f | ||
|
|
645f5ab72d | ||
|
|
8dca65f48f | ||
|
|
83a2d52ff4 | ||
|
|
1a2796a7d0 | ||
|
|
d80fdf8468 | ||
|
|
90fefc419f | ||
|
|
8671963719 | ||
|
|
a03ffd5b92 | ||
|
|
0861730e0e | ||
|
|
6b0f93a564 | ||
|
|
e6371faf4f | ||
|
|
e95a9b4fa2 | ||
|
|
e5886a1a8e | ||
|
|
ec8192b160 | ||
|
|
2da03a220d | ||
|
|
cfbfb37e23 | ||
|
|
ff4d025840 | ||
|
|
59ac59d351 | ||
|
|
3df87520db | ||
|
|
85ce65a4ce | ||
|
|
12a82a6c58 | ||
|
|
b2d2a254d7 | ||
|
|
62cdf31ae2 | ||
|
|
0dcebe7d34 | ||
|
|
32a5c157b9 | ||
|
|
97cea8950d | ||
|
|
873be0b76b | ||
|
|
3a8eb0cf7d | ||
|
|
e9ef13d06d | ||
|
|
f648fe6c3f | ||
|
|
46895d0b08 | ||
|
|
431ca9e809 | ||
|
|
6b5c5f0650 | ||
|
|
d303fcc621 | ||
|
|
3ae855ef28 | ||
|
|
76a3086569 | ||
|
|
07646bc020 | ||
|
|
880b8b267a | ||
|
|
37e5c48a27 | ||
|
|
deb67386fa | ||
|
|
81d74e4a9d | ||
|
|
39c13dcc18 | ||
|
|
e7314a0eea | ||
|
|
168c6e2da3 | ||
|
|
564765862b | ||
|
|
3c12d1799c | ||
|
|
60835d13a8 | ||
|
|
892cf0e66b | ||
|
|
8ddc484ce6 | ||
|
|
0e021e3c57 | ||
|
|
fb0aeec27e | ||
|
|
a367819a1c | ||
|
|
0afe289a20 | ||
|
|
bf6af46ac3 | ||
|
|
df2b76aee1 | ||
|
|
70a3c7195a | ||
|
|
c651de177f | ||
|
|
7b42daa9fb | ||
|
|
9d49b3e391 | ||
|
|
2c5ab054db | ||
|
|
66291a2aea | ||
|
|
d96e086945 | ||
|
|
8424458174 | ||
|
|
6a3b0249fe | ||
|
|
dfc2803714 | ||
|
|
ade90bc051 | ||
|
|
daa53f5831 | ||
|
|
50a4f83db6 | ||
|
|
00cb7d99d8 | ||
|
|
fb74910dc8 | ||
|
|
26dcd75423 | ||
|
|
afb9b0bbe2 | ||
|
|
718776eb72 | ||
|
|
9d35793287 | ||
|
|
0b439362da | ||
|
|
2962f545b9 | ||
|
|
cd02510d0f | ||
|
|
cccf79ed94 | ||
|
|
aa9999809c | ||
|
|
6263bf96ba | ||
|
|
9a539ffc86 | ||
|
|
8a41d15b69 | ||
|
|
94bf090657 | ||
|
|
adc7173cf2 | ||
|
|
fd6bf5324a | ||
|
|
c2b2f7ea33 | ||
|
|
bbcc90e4d1 | ||
|
|
84f78cd9f9 | ||
|
|
787688ea08 | ||
|
|
bcfa1d83e8 | ||
|
|
9363b6a464 | ||
|
|
338fd4e493 | ||
|
|
eb3cb81a79 | ||
|
|
556f7f5a37 | ||
|
|
c2ec04f8c1 | ||
|
|
519659b84c | ||
|
|
560d0698ac | ||
|
|
f40e8e9af1 | ||
|
|
1ab7405e36 | ||
|
|
aeadd7c11f | ||
|
|
d0fbf538d3 | ||
|
|
cfe77934e8 | ||
|
|
3f6ca1648e | ||
|
|
7c6d302baa | ||
|
|
b8eb50b982 | ||
|
|
d981c3c968 | ||
|
|
416860d9b0 | ||
|
|
33d5d7e9a2 | ||
|
|
99c1102a3a |
18
.github/workflows/docker-publish.yml
vendored
18
.github/workflows/docker-publish.yml
vendored
@@ -1,6 +1,12 @@
|
||||
name: Docker
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tagInput:
|
||||
description: 'Tag'
|
||||
required: true
|
||||
|
||||
release:
|
||||
types: [created]
|
||||
tags:
|
||||
@@ -29,6 +35,14 @@ jobs:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Determine version tag
|
||||
id: version-tag
|
||||
run: |
|
||||
INPUT_VALUE="${{ github.event.inputs.tagInput }}"
|
||||
if [ -z "$INPUT_VALUE" ]; then
|
||||
INPUT_VALUE="${{ github.ref_name }}"
|
||||
fi
|
||||
echo "::set-output name=value::$INPUT_VALUE"
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
@@ -37,5 +51,5 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/mrsked/mrsk:latest
|
||||
ghcr.io/mrsked/mrsk:${{ github.ref_name }}
|
||||
ghcr.io/basecamp/kamal:latest
|
||||
ghcr.io/basecamp/kamal:${{ steps.version-tag.outputs.value }}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
kamal (0.16.0)
|
||||
kamal (1.1.0)
|
||||
activesupport (>= 7.0)
|
||||
bcrypt_pbkdf (~> 1.0)
|
||||
concurrent-ruby (~> 1.2)
|
||||
|
||||
@@ -5,10 +5,9 @@ Gem::Specification.new do |spec|
|
||||
spec.version = Kamal::VERSION
|
||||
spec.authors = [ "David Heinemeier Hansson" ]
|
||||
spec.email = "dhh@hey.com"
|
||||
spec.homepage = "https://github.com/rails/kamal"
|
||||
spec.homepage = "https://github.com/basecamp/kamal"
|
||||
spec.summary = "Deploy web apps in containers to servers running Docker with zero downtime."
|
||||
spec.license = "MIT"
|
||||
|
||||
spec.files = Dir["lib/**/*", "MIT-LICENSE", "README.md"]
|
||||
spec.executables = %w[ kamal ]
|
||||
|
||||
|
||||
@@ -9,31 +9,56 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
||||
|
||||
on(KAMAL.hosts) do
|
||||
execute *KAMAL.auditor.record("Tagging #{KAMAL.config.absolute_image} as the latest image"), verbosity: :debug
|
||||
execute *KAMAL.app.tag_current_as_latest
|
||||
execute *KAMAL.app.tag_current_image_as_latest
|
||||
|
||||
KAMAL.roles_on(host).each do |role|
|
||||
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|
|
||||
roles = KAMAL.roles_on(host)
|
||||
|
||||
roles.each do |role|
|
||||
KAMAL.roles_on(host).each do |role|
|
||||
app = KAMAL.app(role: role)
|
||||
auditor = KAMAL.auditor(role: role)
|
||||
role_config = KAMAL.config.role(role)
|
||||
|
||||
if capture_with_info(*app.container_id_for_version(version, only_running: true), raise_on_non_zero_exit: false).present?
|
||||
if capture_with_info(*app.container_id_for_version(version), raise_on_non_zero_exit: false).present?
|
||||
tmp_version = "#{version}_replaced_#{SecureRandom.hex(8)}"
|
||||
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
|
||||
|
||||
old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
|
||||
execute *app.start_or_run(hostname: "#{host}-#{SecureRandom.hex(6)}")
|
||||
execute *app.run(hostname: "#{host}-#{SecureRandom.hex(6)}")
|
||||
|
||||
Kamal::Utils::HealthcheckPoller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
|
||||
Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
|
||||
|
||||
execute *app.stop(version: old_version), raise_on_non_zero_exit: false if old_version.present?
|
||||
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
|
||||
|
||||
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
|
||||
@@ -90,14 +115,16 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
||||
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: "web").execute_in_existing_container_over_ssh(cmd, host: KAMAL.primary_host) }
|
||||
run_locally { exec KAMAL.app(role: KAMAL.primary_role).execute_in_existing_container_over_ssh(cmd, host: KAMAL.primary_host) }
|
||||
end
|
||||
|
||||
when options[:interactive]
|
||||
say "Get most recent version available as an image...", :magenta unless options[:version]
|
||||
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 { exec KAMAL.app(role: KAMAL.primary_host.roles.first).execute_in_new_container_over_ssh(cmd, host: KAMAL.primary_host) }
|
||||
run_locally do
|
||||
exec KAMAL.app(role: KAMAL.primary_role).execute_in_new_container_over_ssh(cmd, host: KAMAL.primary_host)
|
||||
end
|
||||
end
|
||||
|
||||
when options[:reuse]
|
||||
@@ -120,8 +147,12 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
||||
using_version(version_or_latest) do |version|
|
||||
say "Launching command with version #{version} from new container...", :magenta
|
||||
on(KAMAL.hosts) do |host|
|
||||
roles = KAMAL.roles_on(host)
|
||||
|
||||
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.execute_in_new_container(cmd))
|
||||
puts_by_host host, capture_with_info(*KAMAL.app(role: role).execute_in_new_container(cmd))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -24,6 +24,7 @@ module Kamal::Cli
|
||||
|
||||
def initialize(*)
|
||||
super
|
||||
@original_env = ENV.to_h.dup
|
||||
load_envs
|
||||
initialize_commander(options_with_subcommand_class_options)
|
||||
end
|
||||
@@ -37,6 +38,12 @@ module Kamal::Cli
|
||||
end
|
||||
end
|
||||
|
||||
def reload_envs
|
||||
ENV.clear
|
||||
ENV.update(@original_env)
|
||||
load_envs
|
||||
end
|
||||
|
||||
def options_with_subcommand_class_options
|
||||
options.merge(@_initializer.last[:class_options] || {})
|
||||
end
|
||||
@@ -75,10 +82,10 @@ module Kamal::Cli
|
||||
def mutating
|
||||
return yield if KAMAL.holding_lock?
|
||||
|
||||
KAMAL.config.ensure_env_available
|
||||
|
||||
run_hook "pre-connect"
|
||||
|
||||
ensure_run_directory
|
||||
|
||||
acquire_lock
|
||||
|
||||
begin
|
||||
@@ -167,5 +174,11 @@ module Kamal::Cli
|
||||
def first_invocation
|
||||
instance_variable_get("@_invocations").first
|
||||
end
|
||||
|
||||
def ensure_run_directory
|
||||
on(KAMAL.hosts) do
|
||||
execute(*KAMAL.server.ensure_run_directory)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
require "uri"
|
||||
|
||||
class Kamal::Cli::Build < Kamal::Cli::Base
|
||||
class BuildError < StandardError; end
|
||||
|
||||
@@ -17,7 +19,7 @@ class Kamal::Cli::Build < Kamal::Cli::Base
|
||||
verify_local_dependencies
|
||||
run_hook "pre-build"
|
||||
|
||||
if (uncommitted_changes = Kamal::Utils.uncommitted_changes).present?
|
||||
if (uncommitted_changes = Kamal::Git.uncommitted_changes).present?
|
||||
say "The following paths have uncommitted changes:\n #{uncommitted_changes}", :yellow
|
||||
end
|
||||
|
||||
@@ -48,6 +50,7 @@ class Kamal::Cli::Build < Kamal::Cli::Base
|
||||
execute *KAMAL.auditor.record("Pulled image with version #{KAMAL.config.version}"), verbosity: :debug
|
||||
execute *KAMAL.builder.clean, raise_on_non_zero_exit: false
|
||||
execute *KAMAL.builder.pull
|
||||
execute *KAMAL.builder.validate_image
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -55,6 +58,10 @@ class Kamal::Cli::Build < Kamal::Cli::Base
|
||||
desc "create", "Create a build setup"
|
||||
def create
|
||||
mutating do
|
||||
if (remote_host = KAMAL.config.builder.remote_host)
|
||||
connect_to_remote_host(remote_host)
|
||||
end
|
||||
|
||||
run_locally do
|
||||
begin
|
||||
debug "Using builder: #{KAMAL.builder.name}"
|
||||
@@ -103,4 +110,14 @@ class Kamal::Cli::Build < Kamal::Cli::Base
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def connect_to_remote_host(remote_host)
|
||||
remote_uri = URI.parse(remote_host)
|
||||
if remote_uri.scheme == "ssh"
|
||||
options = { user: remote_uri.user, port: remote_uri.port }.compact
|
||||
on(remote_uri.host, options) do
|
||||
execute "true"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
56
lib/kamal/cli/env.rb
Normal file
56
lib/kamal/cli/env.rb
Normal file
@@ -0,0 +1,56 @@
|
||||
require "tempfile"
|
||||
|
||||
class Kamal::Cli::Env < Kamal::Cli::Base
|
||||
desc "push", "Push the env file to the remote hosts"
|
||||
def push
|
||||
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! StringIO.new(role_config.env_file), role_config.host_env_file_path, mode: 400
|
||||
end
|
||||
end
|
||||
|
||||
on(KAMAL.traefik_hosts) do
|
||||
execute *KAMAL.traefik.make_env_directory
|
||||
upload! StringIO.new(KAMAL.traefik.env_file), KAMAL.traefik.host_env_file_path, mode: 400
|
||||
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! StringIO.new(accessory_config.env_file), accessory_config.host_env_file_path, mode: 400
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc "delete", "Delete the env file 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|
|
||||
role_config = KAMAL.config.role(role)
|
||||
execute *KAMAL.app(role: role).remove_env_file
|
||||
end
|
||||
end
|
||||
|
||||
on(KAMAL.traefik_hosts) do
|
||||
execute *KAMAL.traefik.remove_env_file
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -6,8 +6,8 @@ class Kamal::Cli::Healthcheck < Kamal::Cli::Base
|
||||
on(KAMAL.primary_host) do
|
||||
begin
|
||||
execute *KAMAL.healthcheck.run
|
||||
Kamal::Utils::HealthcheckPoller.wait_for_healthy { capture_with_info(*KAMAL.healthcheck.status) }
|
||||
rescue Kamal::Utils::HealthcheckPoller::HealthcheckError => e
|
||||
Poller.wait_for_healthy { capture_with_info(*KAMAL.healthcheck.status) }
|
||||
rescue Poller::HealthcheckError => e
|
||||
error capture_with_info(*KAMAL.healthcheck.logs)
|
||||
error capture_with_pretty_json(*KAMAL.healthcheck.container_health_log)
|
||||
raise
|
||||
|
||||
64
lib/kamal/cli/healthcheck/poller.rb
Normal file
64
lib/kamal/cli/healthcheck/poller.rb
Normal file
@@ -0,0 +1,64 @@
|
||||
module Kamal::Cli::Healthcheck::Poller
|
||||
extend self
|
||||
|
||||
TRAEFIK_UPDATE_DELAY = 5
|
||||
|
||||
class HealthcheckError < StandardError; end
|
||||
|
||||
def wait_for_healthy(pause_after_ready: false, &block)
|
||||
attempt = 1
|
||||
max_attempts = KAMAL.config.healthcheck["max_attempts"]
|
||||
|
||||
begin
|
||||
case status = block.call
|
||||
when "healthy"
|
||||
sleep TRAEFIK_UPDATE_DELAY if pause_after_ready
|
||||
when "running" # No health check configured
|
||||
sleep KAMAL.config.readiness_delay if pause_after_ready
|
||||
else
|
||||
raise HealthcheckError, "container not ready (#{status})"
|
||||
end
|
||||
rescue HealthcheckError => e
|
||||
if attempt <= max_attempts
|
||||
info "#{e.message}, retrying in #{attempt}s (attempt #{attempt}/#{max_attempts})..."
|
||||
sleep attempt
|
||||
attempt += 1
|
||||
retry
|
||||
else
|
||||
raise
|
||||
end
|
||||
end
|
||||
|
||||
info "Container is healthy!"
|
||||
end
|
||||
|
||||
def wait_for_unhealthy(pause_after_ready: false, &block)
|
||||
attempt = 1
|
||||
max_attempts = KAMAL.config.healthcheck["max_attempts"]
|
||||
|
||||
begin
|
||||
case status = block.call
|
||||
when "unhealthy"
|
||||
sleep TRAEFIK_UPDATE_DELAY if pause_after_ready
|
||||
else
|
||||
raise HealthcheckError, "container not unhealthy (#{status})"
|
||||
end
|
||||
rescue HealthcheckError => e
|
||||
if attempt <= max_attempts
|
||||
info "#{e.message}, retrying in #{attempt}s (attempt #{attempt}/#{max_attempts})..."
|
||||
sleep attempt
|
||||
attempt += 1
|
||||
retry
|
||||
else
|
||||
raise
|
||||
end
|
||||
end
|
||||
|
||||
info "Container is unhealthy!"
|
||||
end
|
||||
|
||||
private
|
||||
def info(message)
|
||||
SSHKit.config.output.info(message)
|
||||
end
|
||||
end
|
||||
@@ -2,7 +2,10 @@ class Kamal::Cli::Lock < Kamal::Cli::Base
|
||||
desc "status", "Report lock status"
|
||||
def status
|
||||
handle_missing_lock do
|
||||
on(KAMAL.primary_host) { puts capture_with_debug(*KAMAL.lock.status) }
|
||||
on(KAMAL.primary_host) do
|
||||
execute *KAMAL.server.ensure_run_directory
|
||||
puts capture_with_debug(*KAMAL.lock.status)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,7 +14,10 @@ class Kamal::Cli::Lock < Kamal::Cli::Base
|
||||
def acquire
|
||||
message = options[:message]
|
||||
raise_if_locked do
|
||||
on(KAMAL.primary_host) { execute *KAMAL.lock.acquire(message, KAMAL.config.version), verbosity: :debug }
|
||||
on(KAMAL.primary_host) do
|
||||
execute *KAMAL.server.ensure_run_directory
|
||||
execute *KAMAL.lock.acquire(message, KAMAL.config.version), verbosity: :debug
|
||||
end
|
||||
say "Acquired the deploy lock"
|
||||
end
|
||||
end
|
||||
@@ -19,7 +25,10 @@ class Kamal::Cli::Lock < Kamal::Cli::Base
|
||||
desc "release", "Release the deploy lock"
|
||||
def release
|
||||
handle_missing_lock do
|
||||
on(KAMAL.primary_host) { execute *KAMAL.lock.release, verbosity: :debug }
|
||||
on(KAMAL.primary_host) do
|
||||
execute *KAMAL.server.ensure_run_directory
|
||||
execute *KAMAL.lock.release, verbosity: :debug
|
||||
end
|
||||
say "Released the deploy lock"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
class Kamal::Cli::Main < Kamal::Cli::Base
|
||||
desc "setup", "Setup all accessories and deploy app to servers"
|
||||
desc "setup", "Setup all accessories, push the env, and deploy app to servers"
|
||||
def setup
|
||||
print_runtime do
|
||||
mutating do
|
||||
say "Ensure Docker is installed...", :magenta
|
||||
invoke "kamal:cli:server:bootstrap"
|
||||
|
||||
say "Push env files...", :magenta
|
||||
invoke "kamal:cli:env:push"
|
||||
|
||||
invoke "kamal:cli:accessory:boot", [ "all" ]
|
||||
deploy
|
||||
end
|
||||
@@ -37,7 +42,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
||||
invoke "kamal:cli:healthcheck:perform", [], invoke_options
|
||||
|
||||
say "Detect stale containers...", :magenta
|
||||
invoke "kamal:cli:app:stale_containers", [], invoke_options
|
||||
invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
|
||||
|
||||
invoke "kamal:cli:app:boot", [], invoke_options
|
||||
|
||||
@@ -70,7 +75,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
||||
invoke "kamal:cli:healthcheck:perform", [], invoke_options
|
||||
|
||||
say "Detect stale containers...", :magenta
|
||||
invoke "kamal:cli:app:stale_containers", [], invoke_options
|
||||
invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
|
||||
|
||||
invoke "kamal:cli:app:boot", [], invoke_options
|
||||
end
|
||||
@@ -165,6 +170,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
||||
end
|
||||
|
||||
desc "envify", "Create .env by evaluating .env.erb (or .env.staging.erb -> .env.staging when using -d staging)"
|
||||
option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip .env file push"
|
||||
def envify
|
||||
if destination = options[:destination]
|
||||
env_template_path = ".env.#{destination}.erb"
|
||||
@@ -174,7 +180,12 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
||||
env_path = ".env"
|
||||
end
|
||||
|
||||
File.write(env_path, ERB.new(File.read(env_template_path)).result, perm: 0600)
|
||||
File.write(env_path, ERB.new(File.read(env_template_path), trim_mode: "-").result, perm: 0600)
|
||||
|
||||
unless options[:skip_push]
|
||||
reload_envs
|
||||
invoke "kamal:cli:env:push", options
|
||||
end
|
||||
end
|
||||
|
||||
desc "remove", "Remove Traefik, app, accessories, and registry session from servers"
|
||||
@@ -204,6 +215,9 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
||||
desc "build", "Build application image"
|
||||
subcommand "build", Kamal::Cli::Build
|
||||
|
||||
desc "env", "Manage environment files"
|
||||
subcommand "env", Kamal::Cli::Env
|
||||
|
||||
desc "healthcheck", "Healthcheck application"
|
||||
subcommand "healthcheck", Kamal::Cli::Healthcheck
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ class Kamal::Cli::Prune < Kamal::Cli::Base
|
||||
end
|
||||
end
|
||||
|
||||
desc "images", "Prune dangling images"
|
||||
desc "images", "Prune unused images"
|
||||
def images
|
||||
mutating do
|
||||
on(KAMAL.hosts) do
|
||||
@@ -23,7 +23,8 @@ class Kamal::Cli::Prune < Kamal::Cli::Base
|
||||
mutating do
|
||||
on(KAMAL.hosts) do
|
||||
execute *KAMAL.auditor.record("Pruned containers"), verbosity: :debug
|
||||
execute *KAMAL.prune.containers
|
||||
execute *KAMAL.prune.app_containers
|
||||
execute *KAMAL.prune.healthcheck_containers
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,6 +12,8 @@ class Kamal::Cli::Server < Kamal::Cli::Base
|
||||
missing << host
|
||||
end
|
||||
end
|
||||
|
||||
execute(*KAMAL.server.ensure_run_directory)
|
||||
end
|
||||
|
||||
if missing.any?
|
||||
|
||||
@@ -19,6 +19,7 @@ registry:
|
||||
- KAMAL_REGISTRY_PASSWORD
|
||||
|
||||
# Inject ENV variables into containers (secrets come from .env).
|
||||
# Remember to run `kamal env push` after making changes!
|
||||
# env:
|
||||
# clear:
|
||||
# DB_HOST: 192.168.0.2
|
||||
@@ -52,7 +53,7 @@ registry:
|
||||
# - MYSQL_ROOT_PASSWORD
|
||||
# files:
|
||||
# - config/mysql/production.cnf:/etc/mysql/my.cnf
|
||||
# - db/production.sql.erb:/docker-entrypoint-initdb.d/setup.sql
|
||||
# - db/production.sql:/docker-entrypoint-initdb.d/setup.sql
|
||||
# directories:
|
||||
# - data:/var/lib/mysql
|
||||
# redis:
|
||||
@@ -72,3 +73,13 @@ registry:
|
||||
# healthcheck:
|
||||
# path: /healthz
|
||||
# port: 4000
|
||||
|
||||
# Bridge fingerprinted assets, like JS and CSS, between versions to avoid
|
||||
# hitting 404 on in-flight requests. Combines all files from new and old
|
||||
# version inside the asset_path.
|
||||
# asset_path: /rails/public/assets
|
||||
|
||||
# Configure rolling deploys by setting a wait time between batches of restarts.
|
||||
# boot:
|
||||
# limit: 10 # Can also specify as a percentage of total hosts, such as "25%"
|
||||
# wait: 2
|
||||
|
||||
@@ -39,6 +39,10 @@ class Kamal::Commander
|
||||
specific_hosts&.first || specific_roles&.first&.primary_host || config.primary_web_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?
|
||||
@@ -51,14 +55,6 @@ class Kamal::Commander
|
||||
end
|
||||
end
|
||||
|
||||
def boot_strategy
|
||||
if config.boot.limit.present?
|
||||
{ in: :groups, limit: config.boot.limit, wait: config.boot.wait }
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
|
||||
def roles_on(host)
|
||||
roles.select { |role| role.hosts.include?(host.to_s) }.map(&:name)
|
||||
end
|
||||
@@ -75,6 +71,10 @@ class Kamal::Commander
|
||||
config.accessories&.collect(&:name) || []
|
||||
end
|
||||
|
||||
def accessories_on(host)
|
||||
config.accessories.select { |accessory| accessory.hosts.include?(host.to_s) }.map(&:name)
|
||||
end
|
||||
|
||||
|
||||
def app(role: nil)
|
||||
Kamal::Commands::App.new(config, role: role)
|
||||
@@ -116,10 +116,15 @@ class Kamal::Commander
|
||||
@registry ||= Kamal::Commands::Registry.new(config)
|
||||
end
|
||||
|
||||
def server
|
||||
@server ||= Kamal::Commands::Server.new(config)
|
||||
end
|
||||
|
||||
def traefik
|
||||
@traefik ||= Kamal::Commands::Traefik.new(config)
|
||||
end
|
||||
|
||||
|
||||
def with_verbosity(level)
|
||||
old_level = self.verbosity
|
||||
|
||||
@@ -132,6 +137,14 @@ class Kamal::Commander
|
||||
SSHKit.config.output_verbosity = old_level
|
||||
end
|
||||
|
||||
def boot_strategy
|
||||
if config.boot.limit.present?
|
||||
{ in: :groups, limit: config.boot.limit, wait: config.boot.wait }
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
|
||||
def holding_lock?
|
||||
self.holding_lock
|
||||
end
|
||||
|
||||
@@ -86,14 +86,6 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
||||
end
|
||||
end
|
||||
|
||||
def make_directory_for(remote_file)
|
||||
make_directory Pathname.new(remote_file).dirname.to_s
|
||||
end
|
||||
|
||||
def make_directory(path)
|
||||
[ :mkdir, "-p", path ]
|
||||
end
|
||||
|
||||
def remove_service_directory
|
||||
[ :rm, "-rf", service_name ]
|
||||
end
|
||||
@@ -106,6 +98,14 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
||||
docker :image, :rm, "--force", image
|
||||
end
|
||||
|
||||
def make_env_directory
|
||||
make_directory accessory_config.host_env_directory
|
||||
end
|
||||
|
||||
def remove_env_file
|
||||
[:rm, "-f", accessory_config.host_env_file_path]
|
||||
end
|
||||
|
||||
private
|
||||
def service_filter
|
||||
[ "--filter", "label=service=#{service_name}" ]
|
||||
|
||||
@@ -1,34 +1,33 @@
|
||||
class Kamal::Commands::App < Kamal::Commands::Base
|
||||
include Assets, Containers, Cord, Execution, Images, Logging
|
||||
|
||||
ACTIVE_DOCKER_STATUSES = [ :running, :restarting ]
|
||||
|
||||
attr_reader :role
|
||||
attr_reader :role, :role_config
|
||||
|
||||
def initialize(config, role: nil)
|
||||
super(config)
|
||||
@role = role
|
||||
end
|
||||
|
||||
def start_or_run(hostname: nil)
|
||||
combine start, run(hostname: hostname), by: "||"
|
||||
@role_config = config.role(self.role)
|
||||
end
|
||||
|
||||
def run(hostname: nil)
|
||||
role = config.role(self.role)
|
||||
|
||||
docker :run,
|
||||
"--detach",
|
||||
"--restart unless-stopped",
|
||||
"--name", container_name,
|
||||
*(["--hostname", hostname] if hostname),
|
||||
"-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
|
||||
*role.env_args,
|
||||
*role.health_check_args,
|
||||
"-e", "KAMAL_VERSION=\"#{config.version}\"",
|
||||
*role_config.env_args,
|
||||
*role_config.health_check_args,
|
||||
*config.logging_args,
|
||||
*config.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
|
||||
@@ -50,53 +49,6 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
||||
end
|
||||
|
||||
|
||||
def logs(since: nil, lines: nil, grep: nil)
|
||||
pipe \
|
||||
current_running_container_id,
|
||||
"xargs docker logs#{" --since #{since}" if since}#{" --tail #{lines}" if lines} 2>&1",
|
||||
("grep '#{grep}'" if grep)
|
||||
end
|
||||
|
||||
def follow_logs(host:, grep: nil)
|
||||
run_over_ssh \
|
||||
pipe(
|
||||
current_running_container_id,
|
||||
"xargs docker logs --timestamps --tail 10 --follow 2>&1",
|
||||
(%(grep "#{grep}") if grep)
|
||||
),
|
||||
host: host
|
||||
end
|
||||
|
||||
|
||||
def execute_in_existing_container(*command, interactive: false)
|
||||
docker :exec,
|
||||
("-it" if interactive),
|
||||
container_name,
|
||||
*command
|
||||
end
|
||||
|
||||
def execute_in_new_container(*command, interactive: false)
|
||||
role = config.role(self.role)
|
||||
|
||||
docker :run,
|
||||
("-it" if interactive),
|
||||
"--rm",
|
||||
*config.env_args,
|
||||
*config.volume_args,
|
||||
*role&.option_args,
|
||||
config.absolute_image,
|
||||
*command
|
||||
end
|
||||
|
||||
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:)
|
||||
run_over_ssh execute_in_new_container(*command, interactive: true), host: host
|
||||
end
|
||||
|
||||
|
||||
def current_running_container_id
|
||||
docker :ps, "--quiet", *filter_args(statuses: ACTIVE_DOCKER_STATUSES), "--latest"
|
||||
end
|
||||
@@ -112,47 +64,22 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
||||
def list_versions(*docker_args, statuses: nil)
|
||||
pipe \
|
||||
docker(:ps, *filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'),
|
||||
%(while read line; do echo ${line##{service_role_dest}-}; done) # Extract SHA from "service-role-dest-SHA"
|
||||
%(while read line; do echo ${line##{role_config.container_prefix}-}; done) # Extract SHA from "service-role-dest-SHA"
|
||||
end
|
||||
|
||||
def list_containers
|
||||
docker :container, :ls, "--all", *filter_args
|
||||
|
||||
def make_env_directory
|
||||
make_directory role_config.host_env_directory
|
||||
end
|
||||
|
||||
def list_container_names
|
||||
[ *list_containers, "--format", "'{{ .Names }}'" ]
|
||||
end
|
||||
|
||||
def remove_container(version:)
|
||||
pipe \
|
||||
container_id_for(container_name: container_name(version)),
|
||||
xargs(docker(:container, :rm))
|
||||
end
|
||||
|
||||
def rename_container(version:, new_version:)
|
||||
docker :rename, container_name(version), container_name(new_version)
|
||||
end
|
||||
|
||||
def remove_containers
|
||||
docker :container, :prune, "--force", *filter_args
|
||||
end
|
||||
|
||||
def list_images
|
||||
docker :image, :ls, config.repository
|
||||
end
|
||||
|
||||
def remove_images
|
||||
docker :image, :prune, "--all", "--force", *filter_args
|
||||
end
|
||||
|
||||
def tag_current_as_latest
|
||||
docker :tag, config.absolute_image, config.latest_image
|
||||
def remove_env_file
|
||||
[ :rm, "-f", role_config.host_env_file_path ]
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
def container_name(version = nil)
|
||||
[ config.service, role, config.destination, version || config.version ].compact.join("-")
|
||||
[ role_config.container_prefix, version || config.version ].compact.join("-")
|
||||
end
|
||||
|
||||
def filter_args(statuses: nil)
|
||||
|
||||
51
lib/kamal/commands/app/assets.rb
Normal file
51
lib/kamal/commands/app/assets.rb
Normal file
@@ -0,0 +1,51 @@
|
||||
module Kamal::Commands::App::Assets
|
||||
def extract_assets
|
||||
asset_container = "#{role_config.container_prefix}-assets"
|
||||
|
||||
combine \
|
||||
make_directory(role_config.asset_extracted_path),
|
||||
[*docker(:stop, "-t 1", asset_container, "2> /dev/null"), "|| true"],
|
||||
docker(:run, "--name", asset_container, "--detach", "--rm", config.latest_image, "sleep 1000000"),
|
||||
docker(:cp, "-L", "#{asset_container}:#{role_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_config.asset_extracted_path(config.version), role_config.asset_volume.host_path
|
||||
if old_version.present?
|
||||
old_extracted_path, old_volume_path = role_config.asset_extracted_path(old_version), role_config.asset_volume(old_version).host_path
|
||||
end
|
||||
|
||||
commands = [make_directory(new_volume_path), copy_contents(new_extracted_path, new_volume_path)]
|
||||
|
||||
if old_version.present?
|
||||
commands << copy_contents(new_extracted_path, old_volume_path, continue_on_error: true)
|
||||
commands << copy_contents(old_extracted_path, new_volume_path, continue_on_error: true)
|
||||
end
|
||||
|
||||
chain *commands
|
||||
end
|
||||
|
||||
def clean_up_assets
|
||||
chain \
|
||||
find_and_remove_older_siblings(role_config.asset_extracted_path),
|
||||
find_and_remove_older_siblings(role_config.asset_volume_path)
|
||||
end
|
||||
|
||||
private
|
||||
def find_and_remove_older_siblings(path)
|
||||
[
|
||||
:find,
|
||||
Pathname.new(path).dirname.to_s,
|
||||
"-maxdepth 1",
|
||||
"-name", "'#{role_config.container_prefix}-*'",
|
||||
"!", "-name", Pathname.new(path).basename.to_s,
|
||||
"-exec rm -rf \"{}\" +"
|
||||
]
|
||||
end
|
||||
|
||||
def copy_contents(source, destination, continue_on_error: false)
|
||||
[ :cp, "-rnT", "#{source}", destination, *("|| true" if continue_on_error)]
|
||||
end
|
||||
end
|
||||
23
lib/kamal/commands/app/containers.rb
Normal file
23
lib/kamal/commands/app/containers.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
module Kamal::Commands::App::Containers
|
||||
def list_containers
|
||||
docker :container, :ls, "--all", *filter_args
|
||||
end
|
||||
|
||||
def list_container_names
|
||||
[ *list_containers, "--format", "'{{ .Names }}'" ]
|
||||
end
|
||||
|
||||
def remove_container(version:)
|
||||
pipe \
|
||||
container_id_for(container_name: container_name(version)),
|
||||
xargs(docker(:container, :rm))
|
||||
end
|
||||
|
||||
def rename_container(version:, new_version:)
|
||||
docker :rename, container_name(version), container_name(new_version)
|
||||
end
|
||||
|
||||
def remove_containers
|
||||
docker :container, :prune, "--force", *filter_args
|
||||
end
|
||||
end
|
||||
22
lib/kamal/commands/app/cord.rb
Normal file
22
lib/kamal/commands/app/cord.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
module Kamal::Commands::App::Cord
|
||||
def cord(version:)
|
||||
pipe \
|
||||
docker(:inspect, "-f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}'", container_name(version)),
|
||||
[:awk, "'$2 == \"#{role_config.cord_volume.container_path}\" {print $1}'"]
|
||||
end
|
||||
|
||||
def tie_cord(cord)
|
||||
create_empty_file(cord)
|
||||
end
|
||||
|
||||
def cut_cord(cord)
|
||||
remove_directory(cord)
|
||||
end
|
||||
|
||||
private
|
||||
def create_empty_file(file)
|
||||
chain \
|
||||
make_directory_for(file),
|
||||
[:touch, file]
|
||||
end
|
||||
end
|
||||
27
lib/kamal/commands/app/execution.rb
Normal file
27
lib/kamal/commands/app/execution.rb
Normal file
@@ -0,0 +1,27 @@
|
||||
module Kamal::Commands::App::Execution
|
||||
def execute_in_existing_container(*command, interactive: false)
|
||||
docker :exec,
|
||||
("-it" if interactive),
|
||||
container_name,
|
||||
*command
|
||||
end
|
||||
|
||||
def execute_in_new_container(*command, interactive: false)
|
||||
docker :run,
|
||||
("-it" if interactive),
|
||||
"--rm",
|
||||
*role_config&.env_args,
|
||||
*config.volume_args,
|
||||
*role_config&.option_args,
|
||||
config.absolute_image,
|
||||
*command
|
||||
end
|
||||
|
||||
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:)
|
||||
run_over_ssh execute_in_new_container(*command, interactive: true), host: host
|
||||
end
|
||||
end
|
||||
13
lib/kamal/commands/app/images.rb
Normal file
13
lib/kamal/commands/app/images.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
module Kamal::Commands::App::Images
|
||||
def list_images
|
||||
docker :image, :ls, config.repository
|
||||
end
|
||||
|
||||
def remove_images
|
||||
docker :image, :prune, "--all", "--force", *filter_args
|
||||
end
|
||||
|
||||
def tag_current_image_as_latest
|
||||
docker :tag, config.absolute_image, config.latest_image
|
||||
end
|
||||
end
|
||||
18
lib/kamal/commands/app/logging.rb
Normal file
18
lib/kamal/commands/app/logging.rb
Normal file
@@ -0,0 +1,18 @@
|
||||
module Kamal::Commands::App::Logging
|
||||
def logs(since: nil, lines: nil, grep: nil)
|
||||
pipe \
|
||||
current_running_container_id,
|
||||
"xargs docker logs#{" --since #{since}" if since}#{" --tail #{lines}" if lines} 2>&1",
|
||||
("grep '#{grep}'" if grep)
|
||||
end
|
||||
|
||||
def follow_logs(host:, grep: nil)
|
||||
run_over_ssh \
|
||||
pipe(
|
||||
current_running_container_id,
|
||||
"xargs docker logs --timestamps --tail 10 --follow 2>&1",
|
||||
(%(grep "#{grep}") if grep)
|
||||
),
|
||||
host: host
|
||||
end
|
||||
end
|
||||
@@ -19,7 +19,9 @@ class Kamal::Commands::Auditor < Kamal::Commands::Base
|
||||
|
||||
private
|
||||
def audit_log_file
|
||||
[ "kamal", config.service, config.destination, "audit.log" ].compact.join("-")
|
||||
file = [ config.service, config.destination, "audit.log" ].compact.join("-")
|
||||
|
||||
"#{config.run_directory}/#{file}"
|
||||
end
|
||||
|
||||
def audit_tags(**details)
|
||||
|
||||
@@ -26,6 +26,18 @@ module Kamal::Commands
|
||||
docker :container, :ls, *("--all" unless only_running), "--filter", "name=^#{container_name}$", "--quiet"
|
||||
end
|
||||
|
||||
def make_directory_for(remote_file)
|
||||
make_directory Pathname.new(remote_file).dirname.to_s
|
||||
end
|
||||
|
||||
def make_directory(path)
|
||||
[ :mkdir, "-p", path ]
|
||||
end
|
||||
|
||||
def remove_directory(path)
|
||||
[ :rm, "-r", path ]
|
||||
end
|
||||
|
||||
private
|
||||
def combine(*commands, by: "&&")
|
||||
commands
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
require "active_support/core_ext/string/filters"
|
||||
|
||||
class Kamal::Commands::Builder < Kamal::Commands::Base
|
||||
delegate :create, :remove, :push, :clean, :pull, :info, to: :target
|
||||
delegate :create, :remove, :push, :clean, :pull, :info, :validate_image, to: :target
|
||||
|
||||
def name
|
||||
target.class.to_s.remove("Kamal::Commands::Builder::").underscore.inquiry
|
||||
|
||||
@@ -21,6 +21,12 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
||||
config.builder.context
|
||||
end
|
||||
|
||||
def validate_image
|
||||
pipe \
|
||||
docker(:inspect, "-f", "'{{ .Config.Labels.service }}'", config.absolute_image),
|
||||
[:grep, "-x", config.service, "||", "(echo \"Image #{config.absolute_image} is missing the `service` label\" && exit 1)"]
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
def build_tags
|
||||
|
||||
@@ -16,6 +16,6 @@ class Kamal::Commands::Docker < Kamal::Commands::Base
|
||||
|
||||
# Do we have superuser access to install Docker and start system services?
|
||||
def superuser?
|
||||
[ '[ "${EUID:-$(id -u)}" -eq 0 ]' ]
|
||||
[ '[ "${EUID:-$(id -u)}" -eq 0 ] || command -v sudo >/dev/null || command -v su >/dev/null' ]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
class Kamal::Commands::Healthcheck < Kamal::Commands::Base
|
||||
EXPOSED_PORT = 3999
|
||||
|
||||
def run
|
||||
web = config.role(:web)
|
||||
@@ -7,11 +6,11 @@ class Kamal::Commands::Healthcheck < Kamal::Commands::Base
|
||||
docker :run,
|
||||
"--detach",
|
||||
"--name", container_name_with_version,
|
||||
"--publish", "#{EXPOSED_PORT}:#{config.healthcheck["port"]}",
|
||||
"--label", "service=#{container_name}",
|
||||
"-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
|
||||
"--publish", "#{exposed_port}:#{config.healthcheck["port"]}",
|
||||
"--label", "service=#{config.healthcheck_service}",
|
||||
"-e", "KAMAL_CONTAINER_NAME=\"#{config.healthcheck_service}\"",
|
||||
*web.env_args,
|
||||
*web.health_check_args,
|
||||
*web.health_check_args(cord: false),
|
||||
*config.volume_args,
|
||||
*web.option_args,
|
||||
config.absolute_image,
|
||||
@@ -27,7 +26,7 @@ class Kamal::Commands::Healthcheck < Kamal::Commands::Base
|
||||
end
|
||||
|
||||
def logs
|
||||
pipe container_id, xargs(docker(:logs, "--tail", 50, "2>&1"))
|
||||
pipe container_id, xargs(docker(:logs, "--tail", log_lines, "2>&1"))
|
||||
end
|
||||
|
||||
def stop
|
||||
@@ -39,12 +38,8 @@ class Kamal::Commands::Healthcheck < Kamal::Commands::Base
|
||||
end
|
||||
|
||||
private
|
||||
def container_name
|
||||
[ "healthcheck", config.service, config.destination ].compact.join("-")
|
||||
end
|
||||
|
||||
def container_name_with_version
|
||||
"#{container_name}-#{config.version}"
|
||||
"#{config.healthcheck_service}-#{config.version}"
|
||||
end
|
||||
|
||||
def container_id
|
||||
@@ -52,6 +47,14 @@ class Kamal::Commands::Healthcheck < Kamal::Commands::Base
|
||||
end
|
||||
|
||||
def health_url
|
||||
"http://localhost:#{EXPOSED_PORT}#{config.healthcheck["path"]}"
|
||||
"http://localhost:#{exposed_port}#{config.healthcheck["path"]}"
|
||||
end
|
||||
|
||||
def exposed_port
|
||||
config.healthcheck["exposed_port"]
|
||||
end
|
||||
|
||||
def log_lines
|
||||
config.healthcheck["log_lines"]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -40,7 +40,7 @@ class Kamal::Commands::Lock < Kamal::Commands::Base
|
||||
end
|
||||
|
||||
def lock_dir
|
||||
"kamal_lock-#{config.service}"
|
||||
"#{config.run_directory}/lock-#{config.service}"
|
||||
end
|
||||
|
||||
def lock_details_file
|
||||
@@ -56,7 +56,7 @@ class Kamal::Commands::Lock < Kamal::Commands::Base
|
||||
end
|
||||
|
||||
def locked_by
|
||||
`git config user.name`.strip
|
||||
Kamal::Git.user_name
|
||||
rescue Errno::ENOENT
|
||||
"Unknown"
|
||||
end
|
||||
|
||||
@@ -3,7 +3,7 @@ require "active_support/core_ext/numeric/time"
|
||||
|
||||
class Kamal::Commands::Prune < Kamal::Commands::Base
|
||||
def dangling_images
|
||||
docker :image, :prune, "--force", "--filter", "label=service=#{config.service}", "--filter", "dangling=true"
|
||||
docker :image, :prune, "--force", "--filter", "label=service=#{config.service}"
|
||||
end
|
||||
|
||||
def tagged_images
|
||||
@@ -13,13 +13,17 @@ class Kamal::Commands::Prune < Kamal::Commands::Base
|
||||
"while read image tag; do docker rmi $tag; done"
|
||||
end
|
||||
|
||||
def containers(keep_last: 5)
|
||||
def app_containers(keep_last: 5)
|
||||
pipe \
|
||||
docker(:ps, "-q", "-a", *service_filter, *stopped_containers_filters),
|
||||
"tail -n +#{keep_last + 1}",
|
||||
"while read container_id; do docker rm $container_id; done"
|
||||
end
|
||||
|
||||
def healthcheck_containers
|
||||
docker :container, :prune, "--force", *healthcheck_service_filter
|
||||
end
|
||||
|
||||
private
|
||||
def stopped_containers_filters
|
||||
[ "created", "exited", "dead" ].flat_map { |status| ["--filter", "status=#{status}"] }
|
||||
@@ -35,4 +39,8 @@ class Kamal::Commands::Prune < Kamal::Commands::Base
|
||||
def service_filter
|
||||
[ "--filter", "label=service=#{config.service}" ]
|
||||
end
|
||||
|
||||
def healthcheck_service_filter
|
||||
[ "--filter", "label=service=#{config.healthcheck_service}" ]
|
||||
end
|
||||
end
|
||||
|
||||
5
lib/kamal/commands/server.rb
Normal file
5
lib/kamal/commands/server.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class Kamal::Commands::Server < Kamal::Commands::Base
|
||||
def ensure_run_directory
|
||||
[:mkdir, "-p", config.run_directory]
|
||||
end
|
||||
end
|
||||
@@ -1,17 +1,25 @@
|
||||
class Kamal::Commands::Traefik < Kamal::Commands::Base
|
||||
delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils
|
||||
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||
|
||||
DEFAULT_IMAGE = "traefik:v2.9"
|
||||
CONTAINER_PORT = 80
|
||||
DEFAULT_ARGS = {
|
||||
'log.level' => 'DEBUG'
|
||||
}
|
||||
DEFAULT_LABELS = {
|
||||
# These ensure we serve a 502 rather than a 404 if no containers are available
|
||||
"traefik.http.routers.catchall.entryPoints" => "http",
|
||||
"traefik.http.routers.catchall.rule" => "PathPrefix(`/`)",
|
||||
"traefik.http.routers.catchall.service" => "unavailable",
|
||||
"traefik.http.routers.catchall.priority" => 1,
|
||||
"traefik.http.services.unavailable.loadbalancer.server.port" => "0"
|
||||
}
|
||||
|
||||
def run
|
||||
docker :run, "--name traefik",
|
||||
"--detach",
|
||||
"--restart", "unless-stopped",
|
||||
"--publish", port,
|
||||
*publish_args,
|
||||
"--volume", "/var/run/docker.sock:/var/run/docker.sock",
|
||||
*env_args,
|
||||
*config.logging_args,
|
||||
@@ -63,23 +71,41 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base
|
||||
"#{host_port}:#{CONTAINER_PORT}"
|
||||
end
|
||||
|
||||
def env_file
|
||||
Kamal::EnvFile.new(config.traefik.fetch("env", {}))
|
||||
end
|
||||
|
||||
def host_env_file_path
|
||||
File.join host_env_directory, "traefik.env"
|
||||
end
|
||||
|
||||
def make_env_directory
|
||||
make_directory(host_env_directory)
|
||||
end
|
||||
|
||||
def remove_env_file
|
||||
[:rm, "-f", host_env_file_path]
|
||||
end
|
||||
|
||||
private
|
||||
def publish_args
|
||||
argumentize "--publish", port unless config.traefik["publish"] == false
|
||||
end
|
||||
|
||||
def label_args
|
||||
argumentize "--label", labels
|
||||
end
|
||||
|
||||
def env_args
|
||||
env_config = config.traefik["env"] || {}
|
||||
|
||||
if env_config.present?
|
||||
argumentize_env_with_secrets(env_config)
|
||||
else
|
||||
[]
|
||||
argumentize "--env-file", host_env_file_path
|
||||
end
|
||||
|
||||
def host_env_directory
|
||||
File.join config.host_env_directory, "traefik"
|
||||
end
|
||||
|
||||
def labels
|
||||
config.traefik["labels"] || []
|
||||
DEFAULT_LABELS.merge(config.traefik["labels"] || {})
|
||||
end
|
||||
|
||||
def image
|
||||
|
||||
@@ -7,10 +7,9 @@ require "net/ssh/proxy/jump"
|
||||
|
||||
class Kamal::Configuration
|
||||
delegate :service, :image, :servers, :env, :labels, :registry, :stop_wait_time, :hooks_path, to: :raw_config, allow_nil: true
|
||||
delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils
|
||||
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||
|
||||
attr_accessor :destination
|
||||
attr_accessor :raw_config
|
||||
attr_reader :destination, :raw_config
|
||||
|
||||
class << self
|
||||
def create_from(config_file:, destination: nil, version: nil)
|
||||
@@ -54,7 +53,18 @@ class Kamal::Configuration
|
||||
end
|
||||
|
||||
def abbreviated_version
|
||||
Kamal::Utils.abbreviate_version(version)
|
||||
if version
|
||||
# Don't abbreviate <sha>_uncommitted_<etc>
|
||||
if version.include?("_")
|
||||
version
|
||||
else
|
||||
version[0...7]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def minimum_version
|
||||
raw_config.minimum_version
|
||||
end
|
||||
|
||||
|
||||
@@ -87,10 +97,6 @@ class Kamal::Configuration
|
||||
roles.select(&:running_traefik?).flat_map(&:hosts).uniq
|
||||
end
|
||||
|
||||
def boot
|
||||
Kamal::Configuration::Boot.new(config: self)
|
||||
end
|
||||
|
||||
|
||||
def repository
|
||||
[ raw_config.registry["server"], image ].compact.join("/")
|
||||
@@ -108,14 +114,10 @@ class Kamal::Configuration
|
||||
"#{service}-#{version}"
|
||||
end
|
||||
|
||||
def require_destination?
|
||||
raw_config.require_destination
|
||||
end
|
||||
|
||||
def env_args
|
||||
if raw_config.env.present?
|
||||
argumentize_env_with_secrets(raw_config.env)
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def volume_args
|
||||
if raw_config.volumes.present?
|
||||
@@ -135,6 +137,18 @@ class Kamal::Configuration
|
||||
end
|
||||
|
||||
|
||||
def boot
|
||||
Kamal::Configuration::Boot.new(config: self)
|
||||
end
|
||||
|
||||
def builder
|
||||
Kamal::Configuration::Builder.new(config: self)
|
||||
end
|
||||
|
||||
def traefik
|
||||
raw_config.traefik || {}
|
||||
end
|
||||
|
||||
def ssh
|
||||
Kamal::Configuration::Ssh.new(config: self)
|
||||
end
|
||||
@@ -145,22 +159,51 @@ class Kamal::Configuration
|
||||
|
||||
|
||||
def healthcheck
|
||||
{ "path" => "/up", "port" => 3000, "max_attempts" => 7 }.merge(raw_config.healthcheck || {})
|
||||
{ "path" => "/up", "port" => 3000, "max_attempts" => 7, "exposed_port" => 3999, "cord" => "/tmp/kamal-cord", "log_lines" => 50 }.merge(raw_config.healthcheck || {})
|
||||
end
|
||||
|
||||
def healthcheck_service
|
||||
[ "healthcheck", service, destination ].compact.join("-")
|
||||
end
|
||||
|
||||
def readiness_delay
|
||||
raw_config.readiness_delay || 7
|
||||
end
|
||||
|
||||
def minimum_version
|
||||
raw_config.minimum_version
|
||||
def run_id
|
||||
@run_id ||= SecureRandom.hex(16)
|
||||
end
|
||||
|
||||
|
||||
def run_directory
|
||||
raw_config.run_directory || ".kamal"
|
||||
end
|
||||
|
||||
def run_directory_as_docker_volume
|
||||
if Pathname.new(run_directory).absolute?
|
||||
run_directory
|
||||
else
|
||||
File.join "$(pwd)", run_directory
|
||||
end
|
||||
end
|
||||
|
||||
def hooks_path
|
||||
raw_config.hooks_path || ".kamal/hooks"
|
||||
end
|
||||
|
||||
def host_env_directory
|
||||
"#{run_directory}/env"
|
||||
end
|
||||
|
||||
def asset_path
|
||||
raw_config.asset_path
|
||||
end
|
||||
|
||||
|
||||
def valid?
|
||||
ensure_required_keys_present && ensure_valid_kamal_version
|
||||
ensure_destination_if_required && ensure_required_keys_present && ensure_valid_kamal_version
|
||||
end
|
||||
|
||||
|
||||
def to_h
|
||||
{
|
||||
roles: role_names,
|
||||
@@ -170,7 +213,6 @@ class Kamal::Configuration
|
||||
repository: repository,
|
||||
absolute_image: absolute_image,
|
||||
service_with_version: service_with_version,
|
||||
env_args: env_args,
|
||||
volume_args: volume_args,
|
||||
ssh_options: ssh.to_h,
|
||||
sshkit: sshkit.to_h,
|
||||
@@ -181,28 +223,17 @@ class Kamal::Configuration
|
||||
}.compact
|
||||
end
|
||||
|
||||
def traefik
|
||||
raw_config.traefik || {}
|
||||
end
|
||||
|
||||
def hooks_path
|
||||
raw_config.hooks_path || ".kamal/hooks"
|
||||
private
|
||||
# Will raise ArgumentError if any required config keys are missing
|
||||
def ensure_destination_if_required
|
||||
if require_destination? && destination.nil?
|
||||
raise ArgumentError, "You must specify a destination"
|
||||
end
|
||||
|
||||
def builder
|
||||
Kamal::Configuration::Builder.new(config: self)
|
||||
end
|
||||
|
||||
# Will raise KeyError if any secret ENVs are missing
|
||||
def ensure_env_available
|
||||
env_args
|
||||
roles.each(&:env_args)
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
# Will raise ArgumentError if any required config keys are missing
|
||||
def ensure_required_keys_present
|
||||
%i[ service image registry servers ].each do |key|
|
||||
raise ArgumentError, "Missing required configuration for #{key}" unless raw_config[key].present?
|
||||
@@ -240,10 +271,8 @@ class Kamal::Configuration
|
||||
|
||||
def git_version
|
||||
@git_version ||=
|
||||
if system("git rev-parse")
|
||||
uncommitted_suffix = Kamal::Utils.uncommitted_changes.present? ? "_uncommitted_#{SecureRandom.hex(8)}" : ""
|
||||
|
||||
"#{`git rev-parse HEAD`.strip}#{uncommitted_suffix}"
|
||||
if Kamal::Git.used?
|
||||
[ 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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class Kamal::Configuration::Accessory
|
||||
delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils
|
||||
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||
|
||||
attr_accessor :name, :specifics
|
||||
|
||||
@@ -45,8 +45,20 @@ class Kamal::Configuration::Accessory
|
||||
specifics["env"] || {}
|
||||
end
|
||||
|
||||
def env_file
|
||||
Kamal::EnvFile.new(env)
|
||||
end
|
||||
|
||||
def host_env_directory
|
||||
File.join config.host_env_directory, "accessories"
|
||||
end
|
||||
|
||||
def host_env_file_path
|
||||
File.join host_env_directory, "#{service_name}.env"
|
||||
end
|
||||
|
||||
def env_args
|
||||
argumentize_env_with_secrets env
|
||||
argumentize "--env-file", host_env_file_path
|
||||
end
|
||||
|
||||
def files
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class Kamal::Configuration::Role
|
||||
delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils
|
||||
CORD_FILE = "cord"
|
||||
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||
|
||||
attr_accessor :name
|
||||
|
||||
@@ -15,48 +16,6 @@ class Kamal::Configuration::Role
|
||||
@hosts ||= extract_hosts_from_config
|
||||
end
|
||||
|
||||
def labels
|
||||
default_labels.merge(traefik_labels).merge(custom_labels)
|
||||
end
|
||||
|
||||
def label_args
|
||||
argumentize "--label", labels
|
||||
end
|
||||
|
||||
def env
|
||||
if config.env && config.env["secret"]
|
||||
merged_env_with_secrets
|
||||
else
|
||||
merged_env
|
||||
end
|
||||
end
|
||||
|
||||
def env_args
|
||||
argumentize_env_with_secrets env
|
||||
end
|
||||
|
||||
def health_check_args
|
||||
if health_check_cmd.present?
|
||||
optionize({ "health-cmd" => health_check_cmd, "health-interval" => health_check_interval })
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def health_check_cmd
|
||||
options = specializations["healthcheck"] || {}
|
||||
options = config.healthcheck.merge(options) if running_traefik?
|
||||
|
||||
options["cmd"] || http_health_check(port: options["port"], path: options["path"])
|
||||
end
|
||||
|
||||
def health_check_interval
|
||||
options = specializations["healthcheck"] || {}
|
||||
options = config.healthcheck.merge(options) if running_traefik?
|
||||
|
||||
options["interval"] || "1s"
|
||||
end
|
||||
|
||||
def cmd
|
||||
specializations["cmd"]
|
||||
end
|
||||
@@ -69,10 +28,136 @@ class Kamal::Configuration::Role
|
||||
end
|
||||
end
|
||||
|
||||
def labels
|
||||
default_labels.merge(traefik_labels).merge(custom_labels)
|
||||
end
|
||||
|
||||
def label_args
|
||||
argumentize "--label", labels
|
||||
end
|
||||
|
||||
|
||||
def env
|
||||
if config.env && config.env["secret"]
|
||||
merged_env_with_secrets
|
||||
else
|
||||
merged_env
|
||||
end
|
||||
end
|
||||
|
||||
def env_file
|
||||
Kamal::EnvFile.new(env)
|
||||
end
|
||||
|
||||
def host_env_directory
|
||||
File.join config.host_env_directory, "roles"
|
||||
end
|
||||
|
||||
def host_env_file_path
|
||||
File.join host_env_directory, "#{[config.service, name, config.destination].compact.join("-")}.env"
|
||||
end
|
||||
|
||||
def env_args
|
||||
argumentize "--env-file", host_env_file_path
|
||||
end
|
||||
|
||||
def asset_volume_args
|
||||
asset_volume&.docker_args
|
||||
end
|
||||
|
||||
|
||||
def health_check_args(cord: true)
|
||||
if health_check_cmd.present?
|
||||
if cord && uses_cord?
|
||||
optionize({ "health-cmd" => health_check_cmd_with_cord, "health-interval" => health_check_interval })
|
||||
.concat(cord_volume.docker_args)
|
||||
else
|
||||
optionize({ "health-cmd" => health_check_cmd, "health-interval" => health_check_interval })
|
||||
end
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def health_check_cmd
|
||||
health_check_options["cmd"] || http_health_check(port: health_check_options["port"], path: health_check_options["path"])
|
||||
end
|
||||
|
||||
def health_check_cmd_with_cord
|
||||
"(#{health_check_cmd}) && (stat #{cord_container_file} > /dev/null || exit 1)"
|
||||
end
|
||||
|
||||
def health_check_interval
|
||||
health_check_options["interval"] || "1s"
|
||||
end
|
||||
|
||||
|
||||
def running_traefik?
|
||||
name.web? || specializations["traefik"]
|
||||
end
|
||||
|
||||
|
||||
def uses_cord?
|
||||
running_traefik? && cord_volume && health_check_cmd.present?
|
||||
end
|
||||
|
||||
def cord_host_directory
|
||||
File.join config.run_directory_as_docker_volume, "cords", [container_prefix, config.run_id].join("-")
|
||||
end
|
||||
|
||||
def cord_volume
|
||||
if (cord = health_check_options["cord"])
|
||||
@cord_volume ||= Kamal::Configuration::Volume.new \
|
||||
host_path: File.join(config.run_directory, "cords", [container_prefix, config.run_id].join("-")),
|
||||
container_path: cord
|
||||
end
|
||||
end
|
||||
|
||||
def cord_host_file
|
||||
File.join cord_volume.host_path, CORD_FILE
|
||||
end
|
||||
|
||||
def cord_container_directory
|
||||
health_check_options.fetch("cord", nil)
|
||||
end
|
||||
|
||||
def cord_container_file
|
||||
File.join cord_volume.container_path, CORD_FILE
|
||||
end
|
||||
|
||||
|
||||
def container_name(version = nil)
|
||||
[ container_prefix, version || config.version ].compact.join("-")
|
||||
end
|
||||
|
||||
def container_prefix
|
||||
[ config.service, name, config.destination ].compact.join("-")
|
||||
end
|
||||
|
||||
|
||||
def asset_path
|
||||
specializations["asset_path"] || config.asset_path
|
||||
end
|
||||
|
||||
def assets?
|
||||
asset_path.present? && running_traefik?
|
||||
end
|
||||
|
||||
def asset_volume(version = nil)
|
||||
if assets?
|
||||
Kamal::Configuration::Volume.new \
|
||||
host_path: asset_volume_path(version), container_path: asset_path
|
||||
end
|
||||
end
|
||||
|
||||
def asset_extracted_path(version = nil)
|
||||
File.join config.run_directory, "assets", "extracted", container_name(version)
|
||||
end
|
||||
|
||||
def asset_volume_path(version = nil)
|
||||
File.join config.run_directory, "assets", "volumes", container_name(version)
|
||||
end
|
||||
|
||||
private
|
||||
attr_accessor :config
|
||||
|
||||
@@ -100,6 +185,7 @@ class Kamal::Configuration::Role
|
||||
"traefik.http.services.#{traefik_service}.loadbalancer.server.scheme" => "http",
|
||||
|
||||
"traefik.http.routers.#{traefik_service}.rule" => "PathPrefix(`/`)",
|
||||
"traefik.http.routers.#{traefik_service}.priority" => "2",
|
||||
"traefik.http.middlewares.#{traefik_service}-retry.retry.attempts" => "5",
|
||||
"traefik.http.middlewares.#{traefik_service}-retry.retry.initialinterval" => "500ms",
|
||||
"traefik.http.routers.#{traefik_service}.middlewares" => "#{traefik_service}-retry@docker"
|
||||
@@ -152,4 +238,12 @@ class Kamal::Configuration::Role
|
||||
def http_health_check(port:, path:)
|
||||
"curl -f #{URI.join("http://localhost:#{port}", path)} || exit 1" if path.present? || port.present?
|
||||
end
|
||||
|
||||
def health_check_options
|
||||
@health_check_options ||= begin
|
||||
options = specializations["healthcheck"] || {}
|
||||
options = config.healthcheck.merge(options) if running_traefik?
|
||||
options
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,7 +18,7 @@ class Kamal::Configuration::Ssh
|
||||
end
|
||||
|
||||
def options
|
||||
{ user: user, proxy: proxy, auth_methods: [ "publickey" ], logger: logger, keepalive: true, keepalive_interval: 30 }.compact
|
||||
{ user: user, proxy: proxy, logger: logger, keepalive: true, keepalive_interval: 30 }.compact
|
||||
end
|
||||
|
||||
def to_h
|
||||
|
||||
22
lib/kamal/configuration/volume.rb
Normal file
22
lib/kamal/configuration/volume.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
class Kamal::Configuration::Volume
|
||||
attr_reader :host_path, :container_path
|
||||
delegate :argumentize, to: Kamal::Utils
|
||||
|
||||
def initialize(host_path:, container_path:)
|
||||
@host_path = host_path
|
||||
@container_path = container_path
|
||||
end
|
||||
|
||||
def docker_args
|
||||
argumentize "--volume", "#{host_path_for_docker_volume}:#{container_path}"
|
||||
end
|
||||
|
||||
private
|
||||
def host_path_for_docker_volume
|
||||
if Pathname.new(host_path).absolute?
|
||||
host_path
|
||||
else
|
||||
File.join "$(pwd)", host_path
|
||||
end
|
||||
end
|
||||
end
|
||||
41
lib/kamal/env_file.rb
Normal file
41
lib/kamal/env_file.rb
Normal file
@@ -0,0 +1,41 @@
|
||||
# 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|
|
||||
if (secrets = @env["secret"]).present?
|
||||
@env.fetch("secret", @env)&.each do |key|
|
||||
contents << docker_env_file_line(key, ENV.fetch(key))
|
||||
end
|
||||
|
||||
@env["clear"]&.each do |key, value|
|
||||
contents << docker_env_file_line(key, value)
|
||||
end
|
||||
else
|
||||
@env.fetch("clear", @env)&.each do |key, value|
|
||||
contents << docker_env_file_line(key, value)
|
||||
end
|
||||
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.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
|
||||
end
|
||||
19
lib/kamal/git.rb
Normal file
19
lib/kamal/git.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
module Kamal::Git
|
||||
extend self
|
||||
|
||||
def used?
|
||||
system("git rev-parse")
|
||||
end
|
||||
|
||||
def user_name
|
||||
`git config user.name`.strip
|
||||
end
|
||||
|
||||
def revision
|
||||
`git rev-parse HEAD`.strip
|
||||
end
|
||||
|
||||
def uncommitted_changes
|
||||
`git status --porcelain`.strip
|
||||
end
|
||||
end
|
||||
@@ -16,16 +16,6 @@ module Kamal::Utils
|
||||
end
|
||||
end
|
||||
|
||||
# Return a list of shell arguments using the same named argument against the passed attributes,
|
||||
# but redacts and expands secrets.
|
||||
def argumentize_env_with_secrets(env)
|
||||
if (secrets = env["secret"]).present?
|
||||
argumentize("-e", secrets.to_h { |key| [ key, ENV.fetch(key) ] }, sensitive: true) + argumentize("-e", env["clear"])
|
||||
else
|
||||
argumentize "-e", env.fetch("clear", env)
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a list of shell-dashed option arguments. If the value is true, it's treated like a value-less option.
|
||||
def optionize(args, with: nil)
|
||||
options = if with
|
||||
@@ -62,39 +52,10 @@ module Kamal::Utils
|
||||
end
|
||||
end
|
||||
|
||||
def unredacted(value)
|
||||
case
|
||||
when value.respond_to?(:unredacted)
|
||||
value.unredacted
|
||||
when value.respond_to?(:transform_values)
|
||||
value.transform_values { |value| unredacted value }
|
||||
when value.respond_to?(:map)
|
||||
value.map { |element| unredacted element }
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
# Escape a value to make it safe for shell use.
|
||||
def escape_shell_value(value)
|
||||
value.to_s.dump
|
||||
.gsub(/`/, '\\\\`')
|
||||
.gsub(DOLLAR_SIGN_WITHOUT_SHELL_EXPANSION_REGEX, '\$')
|
||||
end
|
||||
|
||||
# Abbreviate a git revhash for concise display
|
||||
def abbreviate_version(version)
|
||||
if version
|
||||
# Don't abbreviate <sha>_uncommitted_<etc>
|
||||
if version.include?("_")
|
||||
version
|
||||
else
|
||||
version[0...7]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def uncommitted_changes
|
||||
`git status --porcelain`.strip
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
class Kamal::Utils::HealthcheckPoller
|
||||
TRAEFIK_HEALTHY_DELAY = 2
|
||||
|
||||
class HealthcheckError < StandardError; end
|
||||
|
||||
class << self
|
||||
def wait_for_healthy(pause_after_ready: false, &block)
|
||||
attempt = 1
|
||||
max_attempts = KAMAL.config.healthcheck["max_attempts"]
|
||||
|
||||
begin
|
||||
case status = block.call
|
||||
when "healthy"
|
||||
sleep TRAEFIK_HEALTHY_DELAY if pause_after_ready
|
||||
when "running" # No health check configured
|
||||
sleep KAMAL.config.readiness_delay if pause_after_ready
|
||||
else
|
||||
raise HealthcheckError, "container not ready (#{status})"
|
||||
end
|
||||
rescue HealthcheckError => e
|
||||
if attempt <= max_attempts
|
||||
info "#{e.message}, retrying in #{attempt}s (attempt #{attempt}/#{max_attempts})..."
|
||||
sleep attempt
|
||||
attempt += 1
|
||||
retry
|
||||
else
|
||||
raise
|
||||
end
|
||||
end
|
||||
|
||||
info "Container is healthy!"
|
||||
end
|
||||
|
||||
private
|
||||
def info(message)
|
||||
SSHKit.config.output.info(message)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,5 @@
|
||||
require "active_support/core_ext/module/delegation"
|
||||
require "sshkit"
|
||||
|
||||
class Kamal::Utils::Sensitive
|
||||
# So SSHKit knows to redact these values.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
module Kamal
|
||||
VERSION = "0.16.0"
|
||||
VERSION = "1.1.0"
|
||||
end
|
||||
|
||||
@@ -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 -e [REDACTED] -e 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.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 -e [REDACTED] -e 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 --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 --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.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.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
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -11,10 +11,11 @@ class CliAppTest < CliTestCase
|
||||
end
|
||||
|
||||
test "boot will rename if same version is already running" do
|
||||
Object.any_instance.stubs(:sleep)
|
||||
run_command("details") # Preheat Kamal const
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :container, :ls, "--filter", "name=^app-web-latest$", "--quiet", raise_on_non_zero_exit: false)
|
||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", raise_on_non_zero_exit: false)
|
||||
.returns("12345678") # running version
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
@@ -25,6 +26,14 @@ class CliAppTest < CliTestCase
|
||||
.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)
|
||||
.returns("cordfile") # old version
|
||||
|
||||
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}}'")
|
||||
.returns("unhealthy") # old version unhealthy
|
||||
|
||||
run_command("boot").tap do |output|
|
||||
assert_match /Renaming container .* to .* as already deployed on 1.1.1.1/, output # Rename
|
||||
assert_match /docker rename app-web-latest app-web-latest_replaced_[0-9a-f]{16}/, output
|
||||
@@ -46,8 +55,6 @@ class CliAppTest < CliTestCase
|
||||
end
|
||||
|
||||
test "boot errors leave lock in place" do
|
||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999" }
|
||||
|
||||
Kamal::Cli::App.any_instance.expects(:using_version).raises(RuntimeError)
|
||||
|
||||
assert !KAMAL.holding_lock?
|
||||
@@ -57,6 +64,34 @@ class CliAppTest < CliTestCase
|
||||
assert KAMAL.holding_lock?
|
||||
end
|
||||
|
||||
test "boot with assets" do
|
||||
Object.any_instance.stubs(:sleep)
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", raise_on_non_zero_exit: false)
|
||||
.returns("12345678") # running version
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
|
||||
.returns("running") # health check
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.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)
|
||||
.returns("") # old version
|
||||
|
||||
run_command("boot", config: :with_assets).tap do |output|
|
||||
assert_match "docker tag dhh/app:latest dhh/app:latest", output
|
||||
assert_match "/usr/bin/env mkdir -p .kamal/assets/volumes/app-web-latest ; cp -rnT .kamal/assets/extracted/app-web-latest .kamal/assets/volumes/app-web-latest ; cp -rnT .kamal/assets/extracted/app-web-latest .kamal/assets/volumes/app-web-123 || true ; cp -rnT .kamal/assets/extracted/app-web-123 .kamal/assets/volumes/app-web-latest || true", output
|
||||
assert_match "/usr/bin/env mkdir -p .kamal/assets/extracted/app-web-latest && docker stop -t 1 app-web-assets 2> /dev/null || true && 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-latest && docker stop -t 1 app-web-assets", output
|
||||
assert_match /docker run --detach --restart unless-stopped --name app-web-latest --hostname 1.1.1.1-[0-9a-f]{12} /, output
|
||||
assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output
|
||||
assert_match "/usr/bin/env find .kamal/assets/extracted -maxdepth 1 -name 'app-web-*' ! -name app-web-latest -exec rm -rf \"{}\" + ; find .kamal/assets/volumes -maxdepth 1 -name 'app-web-*' ! -name app-web-latest -exec rm -rf \"{}\" +", output
|
||||
end
|
||||
end
|
||||
|
||||
test "start" do
|
||||
run_command("start").tap do |output|
|
||||
assert_match "docker start app-web-999", output
|
||||
@@ -124,7 +159,7 @@ class CliAppTest < CliTestCase
|
||||
|
||||
test "exec" do
|
||||
run_command("exec", "ruby -v").tap do |output|
|
||||
assert_match "docker run --rm dhh/app:latest ruby -v", output
|
||||
assert_match "docker run --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v", output
|
||||
end
|
||||
end
|
||||
|
||||
@@ -135,6 +170,25 @@ class CliAppTest < CliTestCase
|
||||
end
|
||||
end
|
||||
|
||||
test "exec interactive" do
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:exec)
|
||||
.with("ssh -t root@1.1.1.1 'docker run -it --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v'")
|
||||
run_command("exec", "-i", "ruby -v").tap do |output|
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
test "exec interactive with reuse" do
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:exec)
|
||||
.with("ssh -t root@1.1.1.1 '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 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
|
||||
|
||||
test "containers" do
|
||||
run_command("containers").tap do |output|
|
||||
assert_match "docker container ls --all --filter label=service=app", output
|
||||
@@ -180,10 +234,16 @@ class CliAppTest < CliTestCase
|
||||
end
|
||||
|
||||
def stub_running
|
||||
Object.any_instance.stubs(:sleep)
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
|
||||
.returns("running") # health check
|
||||
|
||||
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}}'")
|
||||
.returns("unhealthy") # health check
|
||||
end
|
||||
end
|
||||
|
||||
@@ -20,7 +20,7 @@ class CliBuildTest < CliTestCase
|
||||
end
|
||||
|
||||
test "push without builder" do
|
||||
stub_locking
|
||||
stub_setup
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.with(:docker, "--version", "&&", :docker, :buildx, "version")
|
||||
|
||||
@@ -36,7 +36,7 @@ class CliBuildTest < CliTestCase
|
||||
end
|
||||
|
||||
test "push with no buildx plugin" do
|
||||
stub_locking
|
||||
stub_setup
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.with(:docker, "--version", "&&", :docker, :buildx, "version")
|
||||
.raises(SSHKit::Command::Failed.new("no buildx"))
|
||||
@@ -57,6 +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
|
||||
end
|
||||
end
|
||||
|
||||
@@ -66,8 +67,16 @@ class CliBuildTest < CliTestCase
|
||||
end
|
||||
end
|
||||
|
||||
test "create remote" do
|
||||
run_command("create", fixture: :with_remote_builder).tap do |output|
|
||||
assert_match "Running /usr/bin/env true on 1.1.1.5", output
|
||||
assert_match "docker context create kamal-app-native-remote-amd64 --description 'kamal-app-native-remote amd64 native host' --docker 'host=ssh://app@1.1.1.5'", output
|
||||
assert_match "docker buildx create --name kamal-app-native-remote kamal-app-native-remote-amd64 --platform linux/amd64", output
|
||||
end
|
||||
end
|
||||
|
||||
test "create with error" do
|
||||
stub_locking
|
||||
stub_setup
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.with { |arg| arg == :docker }
|
||||
.raises(SSHKit::Command::Failed.new("stderr=error"))
|
||||
@@ -95,8 +104,8 @@ class CliBuildTest < CliTestCase
|
||||
end
|
||||
|
||||
private
|
||||
def run_command(*command)
|
||||
stdouted { Kamal::Cli::Build.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) }
|
||||
def run_command(*command, fixture: :with_accessories)
|
||||
stdouted { Kamal::Cli::Build.start([*command, "-c", "test/fixtures/deploy_#{fixture}.yml"]) }
|
||||
end
|
||||
|
||||
def stub_dependency_checks
|
||||
|
||||
@@ -27,11 +27,13 @@ class CliTestCase < ActiveSupport::TestCase
|
||||
.raises(SSHKit::Command::Failed.new("failed"))
|
||||
end
|
||||
|
||||
def stub_locking
|
||||
def stub_setup
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.with { |arg1, arg2| arg1 == :mkdir && arg2 == "kamal_lock-app" }
|
||||
.with { |*args| args == [ :mkdir, "-p", ".kamal" ] }
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.with { |arg1, arg2| arg1 == :rm && arg2 == "kamal_lock-app/details" }
|
||||
.with { |arg1, arg2| arg1 == :mkdir && arg2 == ".kamal/lock-app" }
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.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: nil)
|
||||
|
||||
38
test/cli/env_test.rb
Normal file
38
test/cli/env_test.rb
Normal file
@@ -0,0 +1,38 @@
|
||||
require_relative "cli_test_case"
|
||||
|
||||
class CliEnvTest < CliTestCase
|
||||
test "push" do
|
||||
run_command("push").tap do |output|
|
||||
assert_match "Running /usr/bin/env mkdir -p .kamal/env/roles on 1.1.1.1", output
|
||||
assert_match "Running /usr/bin/env mkdir -p .kamal/env/traefik on 1.1.1.1", output
|
||||
assert_match "Running /usr/bin/env mkdir -p .kamal/env/accessories on 1.1.1.1", output
|
||||
assert_match "Running /usr/bin/env mkdir -p .kamal/env/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
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def run_command(*command)
|
||||
stdouted { Kamal::Cli::Env.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) }
|
||||
end
|
||||
end
|
||||
@@ -5,12 +5,13 @@ class CliHealthcheckTest < CliTestCase
|
||||
# Prevent expected failures from outputting to terminal
|
||||
Thread.report_on_exception = false
|
||||
|
||||
Kamal::Utils::HealthcheckPoller.stubs(:sleep) # No sleeping when retrying
|
||||
Kamal::Cli::Healthcheck::Poller.stubs(:sleep) # No sleeping when retrying
|
||||
Kamal::Configuration.any_instance.stubs(:run_id).returns("12345678901234567890123456789012")
|
||||
|
||||
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\"", "--health-cmd", "\"curl -f http://localhost:3000/up || exit 1\"", "--health-interval", "\"1s\"", "dhh/app:999")
|
||||
.with(:docker, :run, "--detach", "--name", "healthcheck-app-999", "--publish", "3999:3000", "--label", "service=healthcheck-app", "-e", "KAMAL_CONTAINER_NAME=\"healthcheck-app\"", "--env-file", ".kamal/env/roles/app-web.env", "--health-cmd", "\"curl -f http://localhost:3000/up || exit 1\"", "--health-interval", "\"1s\"", "dhh/app:999")
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :container, :rm, raise_on_non_zero_exit: false)
|
||||
|
||||
@@ -34,12 +35,12 @@ class CliHealthcheckTest < CliTestCase
|
||||
# Prevent expected failures from outputting to terminal
|
||||
Thread.report_on_exception = false
|
||||
|
||||
Kamal::Utils::HealthcheckPoller.stubs(:sleep) # No sleeping when retrying
|
||||
Kamal::Cli::Healthcheck::Poller.stubs(:sleep) # No sleeping when retrying
|
||||
|
||||
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\"", "--health-cmd", "\"curl -f http://localhost:3000/up || exit 1\"", "--health-interval", "\"1s\"", "dhh/app:999")
|
||||
.with(:docker, :run, "--detach", "--name", "healthcheck-app-999", "--publish", "3999:3000", "--label", "service=healthcheck-app", "-e", "KAMAL_CONTAINER_NAME=\"healthcheck-app\"", "--env-file", ".kamal/env/roles/app-web.env", "--health-cmd", "\"curl -f http://localhost:3000/up || exit 1\"", "--health-interval", "\"1s\"", "dhh/app:999")
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :container, :rm, raise_on_non_zero_exit: false)
|
||||
|
||||
|
||||
@@ -2,19 +2,19 @@ require_relative "cli_test_case"
|
||||
|
||||
class CliLockTest < CliTestCase
|
||||
test "status" do
|
||||
run_command("status") do |output|
|
||||
assert_match "stat lock", output
|
||||
run_command("status").tap do |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
|
||||
|
||||
test "release" do
|
||||
run_command("release") do |output|
|
||||
assert_match "rm -rf lock", output
|
||||
run_command("release").tap do |output|
|
||||
assert_match "Released the deploy lock", output
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def run_command(*command)
|
||||
stdouted { Kamal::Cli::Lock.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) }
|
||||
stdouted { Kamal::Cli::Lock.start([*command, "-v", "-c", "test/fixtures/deploy_with_accessories.yml"]) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,6 +3,7 @@ require_relative "cli_test_case"
|
||||
class CliMainTest < CliTestCase
|
||||
test "setup" do
|
||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:server:bootstrap")
|
||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:env:push")
|
||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:boot", [ "all" ])
|
||||
Kamal::Cli::Main.any_instance.expects(:deploy)
|
||||
|
||||
@@ -16,7 +17,7 @@ class CliMainTest < CliTestCase
|
||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:deliver", [], invoke_options)
|
||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:boot", [], invoke_options)
|
||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:healthcheck:perform", [], invoke_options)
|
||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], 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)
|
||||
|
||||
@@ -43,7 +44,7 @@ class CliMainTest < CliTestCase
|
||||
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)
|
||||
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)
|
||||
|
||||
@@ -63,11 +64,14 @@ class CliMainTest < CliTestCase
|
||||
Thread.report_on_exception = false
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.with { |*arg| arg[0..1] == [:mkdir, 'kamal_lock-app'] }
|
||||
.with { |*args| args == [ :mkdir, "-p", ".kamal" ] }
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.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_lock-app', ">", "/dev/null", "&&", :cat, "kamal_lock-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")
|
||||
@@ -78,7 +82,10 @@ class CliMainTest < CliTestCase
|
||||
Thread.report_on_exception = false
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.with { |*arg| arg[0..1] == [:mkdir, 'kamal_lock-app'] }
|
||||
.with { |*args| args == [ :mkdir, "-p", ".kamal" ] }
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.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
|
||||
@@ -107,7 +114,7 @@ class CliMainTest < CliTestCase
|
||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:deliver", [], invoke_options)
|
||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:boot", [], invoke_options)
|
||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:healthcheck:perform", [], invoke_options)
|
||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], 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)
|
||||
|
||||
@@ -117,17 +124,25 @@ class CliMainTest < CliTestCase
|
||||
end
|
||||
|
||||
test "deploy with missing secrets" do
|
||||
assert_raises(KeyError) do
|
||||
invoke_options = { "config_file" => "test/fixtures/deploy_with_secrets.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: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("deploy", config_file: "deploy_with_secrets")
|
||||
end
|
||||
end
|
||||
|
||||
test "redeploy" 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:build:deliver", [], 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)
|
||||
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)
|
||||
@@ -149,7 +164,7 @@ class CliMainTest < CliTestCase
|
||||
|
||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:pull", [], 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)
|
||||
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)
|
||||
|
||||
run_command("redeploy", "--skip_push").tap do |output|
|
||||
@@ -170,9 +185,10 @@ 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, "--filter", "name=^app-#{role}-123$", "--quiet", raise_on_non_zero_exit: false)
|
||||
.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")
|
||||
@@ -185,14 +201,21 @@ class CliMainTest < CliTestCase
|
||||
.returns("running").at_least_once # health check
|
||||
end
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :inspect, "-f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}'", "app-web-version-to-rollback", "|", :awk, "'$2 == \"/tmp/kamal-cord\" {print $1}'", :raise_on_non_zero_exit => false)
|
||||
.returns("corddirectory").at_least_once # health check
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-version-to-rollback$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
|
||||
.returns("unhealthy").at_least_once # health check
|
||||
|
||||
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
||||
hook_variables = { version: 123, service_version: "app@123", hosts: "1.1.1.1,1.1.1.2,1.1.1.3,1.1.1.4", command: "rollback" }
|
||||
|
||||
run_command("rollback", "123", config_file: "deploy_with_accessories").tap do |output|
|
||||
assert_match "Start container with version 123", output
|
||||
assert_hook_ran "pre-deploy", output, **hook_variables
|
||||
assert_match "docker tag dhh/app:123 dhh/app:latest", output
|
||||
assert_match "docker start app-web-123", 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: "0"
|
||||
end
|
||||
@@ -201,10 +224,10 @@ class CliMainTest < CliTestCase
|
||||
test "rollback without old version" do
|
||||
Kamal::Cli::Main.any_instance.stubs(:container_available?).returns(true)
|
||||
|
||||
Kamal::Utils::HealthcheckPoller.stubs(:sleep)
|
||||
Kamal::Cli::Healthcheck::Poller.stubs(:sleep)
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :container, :ls, "--filter", "name=^app-web-123$", "--quiet", raise_on_non_zero_exit: false)
|
||||
.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(: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)
|
||||
@@ -214,8 +237,7 @@ class CliMainTest < CliTestCase
|
||||
.returns("running").at_least_once # health check
|
||||
|
||||
run_command("rollback", "123").tap do |output|
|
||||
assert_match "Start container with version 123", output
|
||||
assert_match "docker start app-web-123 || docker run --detach --restart unless-stopped --name app-web-123", output
|
||||
assert_match "docker run --detach --restart unless-stopped --name app-web-123", output
|
||||
assert_no_match "docker stop", output
|
||||
end
|
||||
end
|
||||
@@ -230,7 +252,7 @@ class CliMainTest < CliTestCase
|
||||
|
||||
test "audit" do
|
||||
run_command("audit").tap do |output|
|
||||
assert_match /tail -n 50 kamal-app-audit.log on 1.1.1.1/, output
|
||||
assert_match %r{tail -n 50 \.kamal/app-audit.log on 1.1.1.1}, output
|
||||
assert_match /App Host: 1.1.1.1/, output
|
||||
end
|
||||
end
|
||||
@@ -332,11 +354,33 @@ class CliMainTest < CliTestCase
|
||||
run_command("envify")
|
||||
end
|
||||
|
||||
test "envify with destination" do
|
||||
File.expects(:read).with(".env.staging.erb").returns("HELLO=<%= 'world' %>")
|
||||
File.expects(:write).with(".env.staging", "HELLO=world", perm: 0600)
|
||||
test "envify with blank line trimming" do
|
||||
file = <<~EOF
|
||||
HELLO=<%= 'world' %>
|
||||
<% if true -%>
|
||||
KEY=value
|
||||
<% end -%>
|
||||
EOF
|
||||
|
||||
run_command("envify", "-d", "staging")
|
||||
File.expects(:read).with(".env.erb").returns(file.strip)
|
||||
File.expects(:write).with(".env", "HELLO=world\nKEY=value\n", perm: 0600)
|
||||
|
||||
run_command("envify")
|
||||
end
|
||||
|
||||
test "envify with destination" do
|
||||
File.expects(:read).with(".env.world.erb").returns("HELLO=<%= 'world' %>")
|
||||
File.expects(:write).with(".env.world", "HELLO=world", perm: 0600)
|
||||
|
||||
run_command("envify", "-d", "world", config_file: "deploy_for_dest")
|
||||
end
|
||||
|
||||
test "envify with skip_push" do
|
||||
File.expects(:read).with(".env.erb").returns("HELLO=<%= 'world' %>")
|
||||
File.expects(:write).with(".env", "HELLO=world", perm: 0600)
|
||||
|
||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:env:push").never
|
||||
run_command("envify", "--skip-push")
|
||||
end
|
||||
|
||||
test "remove with confirmation" do
|
||||
|
||||
@@ -10,7 +10,7 @@ class CliPruneTest < CliTestCase
|
||||
|
||||
test "images" do
|
||||
run_command("images").tap do |output|
|
||||
assert_match "docker image prune --force --filter label=service=app --filter dangling=true on 1.1.1.", output
|
||||
assert_match "docker image prune --force --filter label=service=app on 1.1.1.", output
|
||||
assert_match "docker image ls --filter label=service=app --format '{{.ID}} {{.Repository}}:{{.Tag}}' | grep -v -w \"$(docker container ls -a --format '{{.Image}}\\|' --filter label=service=app | tr -d '\\n')dhh/app:latest\\|dhh/app:<none>\" | while read image tag; do docker rmi $tag; done on 1.1.1.", output
|
||||
end
|
||||
end
|
||||
@@ -18,6 +18,7 @@ class CliPruneTest < CliTestCase
|
||||
test "containers" do
|
||||
run_command("containers").tap do |output|
|
||||
assert_match /docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +6 | while read container_id; do docker rm $container_id; done on 1.1.1.\d/, output
|
||||
assert_match /docker container prune --force --filter label=service=healthcheck-app on 1.1.1.\d/, output
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -3,13 +3,15 @@ require_relative "cli_test_case"
|
||||
class CliServerTest < CliTestCase
|
||||
test "bootstrap already installed" do
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(true).at_least_once
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:mkdir, "-p", ".kamal").returns("").at_least_once
|
||||
|
||||
assert_equal "", run_command("bootstrap")
|
||||
end
|
||||
|
||||
test "bootstrap install as non-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 ]', 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(false).at_least_once
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:mkdir, "-p", ".kamal").returns("").at_least_once
|
||||
|
||||
assert_raise RuntimeError, "Docker is not installed on 1.1.1.1, 1.1.1.3, 1.1.1.4, 1.1.1.2 and can't be automatically installed without having root access and the `curl` command available. Install Docker manually: https://docs.docker.com/engine/install/" do
|
||||
run_command("bootstrap")
|
||||
@@ -18,8 +20,9 @@ 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 ]', raise_on_non_zero_exit: false).returns(true).at_least_once
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with('[ "${EUID:-$(id -u)}" -eq 0 ] || command -v sudo >/dev/null || command -v su >/dev/null', raise_on_non_zero_exit: false).returns(true).at_least_once
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:curl, "-fsSL", "https://get.docker.com", "|", :sh).at_least_once
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:mkdir, "-p", ".kamal").returns("").at_least_once
|
||||
|
||||
run_command("bootstrap").tap do |output|
|
||||
("1.1.1.1".."1.1.1.4").map do |host|
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 --log-opt max-size=\"10m\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output
|
||||
assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output
|
||||
end
|
||||
end
|
||||
|
||||
@@ -14,13 +14,15 @@ class CliTraefikTest < CliTestCase
|
||||
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 --log-opt max-size=\"10m\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output
|
||||
assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output
|
||||
end
|
||||
end
|
||||
|
||||
test "reboot --rolling" do
|
||||
Object.any_instance.stubs(:sleep)
|
||||
|
||||
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.lines[3]
|
||||
assert_match "Running docker container prune --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -49,6 +49,12 @@ class CommanderTest < ActiveSupport::TestCase
|
||||
assert_equal "1.1.1.3", @kamal.primary_host
|
||||
end
|
||||
|
||||
test "primary_role" do
|
||||
assert_equal "web", @kamal.primary_role
|
||||
@kamal.specific_roles = "workers"
|
||||
assert_equal "workers", @kamal.primary_role
|
||||
end
|
||||
|
||||
test "roles_on" do
|
||||
assert_equal [ "web" ], @kamal.roles_on("1.1.1.1")
|
||||
assert_equal [ "workers" ], @kamal.roles_on("1.1.1.3")
|
||||
|
||||
@@ -49,15 +49,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 -e MYSQL_ROOT_PASSWORD=\"secret123\" -e 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.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 -e 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.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 app-busybox --detach --restart unless-stopped --log-opt max-size=\"10m\" --label service=\"app-busybox\" busybox:latest",
|
||||
"docker run --name app-busybox --detach --restart unless-stopped --log-opt max-size=\"10m\" --env-file .kamal/env/accessories/app-busybox.env --label service=\"app-busybox\" busybox:latest",
|
||||
new_command(:busybox).run.join(" ")
|
||||
end
|
||||
|
||||
@@ -65,7 +65,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
||||
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
|
||||
|
||||
assert_equal \
|
||||
"docker run --name app-busybox --detach --restart unless-stopped --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app-busybox\" busybox:latest",
|
||||
"docker run --name app-busybox --detach --restart unless-stopped --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --env-file .kamal/env/accessories/app-busybox.env --label service=\"app-busybox\" busybox:latest",
|
||||
new_command(:busybox).run.join(" ")
|
||||
end
|
||||
|
||||
@@ -90,7 +90,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
||||
|
||||
test "execute in new container" do
|
||||
assert_equal \
|
||||
"docker run --rm -e MYSQL_ROOT_PASSWORD=\"secret123\" -e MYSQL_ROOT_HOST=\"%\" private.registry/mysql:8.0 mysql -u root",
|
||||
"docker run --rm --env-file .kamal/env/accessories/app-mysql.env private.registry/mysql:8.0 mysql -u root",
|
||||
new_command(:mysql).execute_in_new_container("mysql", "-u", "root").join(" ")
|
||||
end
|
||||
|
||||
@@ -102,7 +102,7 @@ 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 -e MYSQL_ROOT_PASSWORD=\"secret123\" -e 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.env private.registry/mysql:8.0 mysql -u root|,
|
||||
new_command(:mysql).execute_in_new_container_over_ssh("mysql", "-u", "root")
|
||||
end
|
||||
end
|
||||
@@ -144,6 +144,14 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
||||
new_command(:mysql).remove_image.join(" ")
|
||||
end
|
||||
|
||||
test "make_env_directory" do
|
||||
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(" ")
|
||||
end
|
||||
|
||||
private
|
||||
def new_command(accessory)
|
||||
Kamal::Commands::Accessory.new(Kamal::Configuration.new(@config), name: accessory)
|
||||
|
||||
@@ -3,6 +3,7 @@ require "test_helper"
|
||||
class CommandsAppTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
ENV["RAILS_MASTER_KEY"] = "456"
|
||||
Kamal::Configuration.any_instance.stubs(:run_id).returns("12345678901234567890123456789012")
|
||||
|
||||
@config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], env: { "secret" => [ "RAILS_MASTER_KEY" ] } }
|
||||
end
|
||||
@@ -13,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 RAILS_MASTER_KEY=\"456\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --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.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/up || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||
new_command.run.join(" ")
|
||||
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 RAILS_MASTER_KEY=\"456\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --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.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||
"docker run --detach --restart unless-stopped --name app-web-999 --hostname myhost -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/up || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||
new_command.run(hostname: "myhost").join(" ")
|
||||
end
|
||||
|
||||
@@ -27,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 RAILS_MASTER_KEY=\"456\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --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.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/up || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||
new_command.run.join(" ")
|
||||
end
|
||||
|
||||
@@ -35,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 RAILS_MASTER_KEY=\"456\" --health-cmd \"curl -f http://localhost:3000/healthz || exit 1\" --health-interval \"1s\" --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.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/healthz || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||
new_command.run.join(" ")
|
||||
end
|
||||
|
||||
@@ -43,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 RAILS_MASTER_KEY=\"456\" --health-cmd \"/bin/up\" --health-interval \"1s\" --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.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(/bin/up) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||
new_command.run.join(" ")
|
||||
end
|
||||
|
||||
@@ -51,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 RAILS_MASTER_KEY=\"456\" --health-cmd \"/bin/healthy\" --health-interval \"1s\" --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.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(/bin/healthy) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||
new_command.run.join(" ")
|
||||
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 RAILS_MASTER_KEY=\"456\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"jobs\" --mount \"somewhere\" --cap-add dhh/app:999 bin/jobs",
|
||||
"docker run --detach --restart unless-stopped --name app-jobs-999 -e KAMAL_CONTAINER_NAME=\"app-jobs-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-jobs.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"jobs\" --mount \"somewhere\" --cap-add dhh/app:999 bin/jobs",
|
||||
new_command(role: "jobs").run.join(" ")
|
||||
end
|
||||
|
||||
@@ -66,7 +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 RAILS_MASTER_KEY=\"456\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --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.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/up || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||
new_command.run.join(" ")
|
||||
end
|
||||
|
||||
@@ -83,18 +84,6 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
new_command.start.join(" ")
|
||||
end
|
||||
|
||||
test "start_or_run" do
|
||||
assert_equal \
|
||||
"docker start app-web-999 || docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --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.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.start_or_run.join(" ")
|
||||
end
|
||||
|
||||
test "start_or_run with hostname" do
|
||||
assert_equal \
|
||||
"docker start app-web-999 || docker run --detach --restart unless-stopped --name app-web-999 --hostname myhost -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --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.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.start_or_run(hostname: "myhost").join(" ")
|
||||
end
|
||||
|
||||
test "stop" do
|
||||
assert_equal \
|
||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker stop",
|
||||
@@ -167,14 +156,14 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
|
||||
test "execute in new container" do
|
||||
assert_equal \
|
||||
"docker run --rm -e RAILS_MASTER_KEY=\"456\" dhh/app:999 bin/rails db:setup",
|
||||
"docker run --rm --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails db:setup",
|
||||
new_command.execute_in_new_container("bin/rails", "db:setup").join(" ")
|
||||
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 -e RAILS_MASTER_KEY=\"456\" --mount \"somewhere\" --cap-add dhh/app:999 bin/rails db:setup",
|
||||
"docker run --rm --env-file .kamal/env/roles/app-web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails db:setup",
|
||||
new_command.execute_in_new_container("bin/rails", "db:setup").join(" ")
|
||||
end
|
||||
|
||||
@@ -185,13 +174,13 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
test "execute in new container over ssh" do
|
||||
assert_match %r|docker run -it --rm -e RAILS_MASTER_KEY=\"456\" dhh/app:999 bin/rails c|,
|
||||
assert_match %r|docker run -it --rm --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails c|,
|
||||
new_command.execute_in_new_container_over_ssh("bin/rails", "c", host: "app-1")
|
||||
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 -e RAILS_MASTER_KEY=\"456\" --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c|,
|
||||
assert_match %r|docker run -it --rm --env-file .kamal/env/roles/app-web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c|,
|
||||
new_command.execute_in_new_container_over_ssh("bin/rails", "c", host: "app-1")
|
||||
end
|
||||
|
||||
@@ -328,14 +317,67 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
new_command.remove_images.join(" ")
|
||||
end
|
||||
|
||||
test "tag_current_as_latest" do
|
||||
test "tag_current_image_as_latest" do
|
||||
assert_equal \
|
||||
"docker tag dhh/app:999 dhh/app:latest",
|
||||
new_command.tag_current_as_latest.join(" ")
|
||||
new_command.tag_current_image_as_latest.join(" ")
|
||||
end
|
||||
|
||||
test "make_env_directory" do
|
||||
assert_equal "mkdir -p .kamal/env/roles", new_command.make_env_directory.join(" ")
|
||||
end
|
||||
|
||||
test "remove_env_file" do
|
||||
assert_equal "rm -f .kamal/env/roles/app-web.env", new_command.remove_env_file.join(" ")
|
||||
end
|
||||
|
||||
test "cord" do
|
||||
assert_equal "docker inspect -f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}' app-web-123 | awk '$2 == \"/tmp/kamal-cord\" {print $1}'", new_command.cord(version: 123).join(" ")
|
||||
end
|
||||
|
||||
test "tie cord" do
|
||||
assert_equal "mkdir -p . ; touch cordfile", new_command.tie_cord("cordfile").join(" ")
|
||||
assert_equal "mkdir -p corddir ; touch corddir/cordfile", new_command.tie_cord("corddir/cordfile").join(" ")
|
||||
assert_equal "mkdir -p /corddir ; touch /corddir/cordfile", new_command.tie_cord("/corddir/cordfile").join(" ")
|
||||
end
|
||||
|
||||
test "cut cord" do
|
||||
assert_equal "rm -r corddir", new_command.cut_cord("corddir").join(" ")
|
||||
end
|
||||
|
||||
test "extract assets" do
|
||||
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: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
|
||||
end
|
||||
|
||||
test "sync asset volumes" do
|
||||
assert_equal [
|
||||
:mkdir, "-p", ".kamal/assets/volumes/app-web-999", ";",
|
||||
:cp, "-rnT", ".kamal/assets/extracted/app-web-999", ".kamal/assets/volumes/app-web-999"
|
||||
], new_command(asset_path: "/public/assets").sync_asset_volumes
|
||||
|
||||
assert_equal [
|
||||
: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",
|
||||
], new_command(asset_path: "/public/assets").sync_asset_volumes(old_version: 998)
|
||||
end
|
||||
|
||||
test "clean up assets" do
|
||||
assert_equal [
|
||||
:find, ".kamal/assets/extracted", "-maxdepth 1", "-name", "'app-web-*'", "!", "-name", "app-web-999", "-exec rm -rf \"{}\" +", ";",
|
||||
:find, ".kamal/assets/volumes", "-maxdepth 1", "-name", "'app-web-*'", "!", "-name", "app-web-999", "-exec rm -rf \"{}\" +"
|
||||
], new_command(asset_path: "/public/assets").clean_up_assets
|
||||
end
|
||||
|
||||
private
|
||||
def new_command(role: "web")
|
||||
Kamal::Commands::App.new(Kamal::Configuration.new(@config, destination: @destination, version: "999"), role: role)
|
||||
def new_command(role: "web", **additional_config)
|
||||
Kamal::Commands::App.new(Kamal::Configuration.new(@config.merge(additional_config), destination: @destination, version: "999"), role: role)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -21,7 +21,7 @@ class CommandsAuditorTest < ActiveSupport::TestCase
|
||||
:echo,
|
||||
"[#{@recorded_at}] [#{@performer}]",
|
||||
"app removed container",
|
||||
">>", "kamal-app-audit.log"
|
||||
">>", ".kamal/app-audit.log"
|
||||
], @auditor.record("app removed container")
|
||||
end
|
||||
|
||||
@@ -31,7 +31,7 @@ class CommandsAuditorTest < ActiveSupport::TestCase
|
||||
:echo,
|
||||
"[#{@recorded_at}] [#{@performer}] [staging]",
|
||||
"app removed container",
|
||||
">>", "kamal-app-staging-audit.log"
|
||||
">>", ".kamal/app-staging-audit.log"
|
||||
], auditor.record("app removed container")
|
||||
end
|
||||
end
|
||||
@@ -42,7 +42,7 @@ class CommandsAuditorTest < ActiveSupport::TestCase
|
||||
:echo,
|
||||
"[#{@recorded_at}] [#{@performer}] [web]",
|
||||
"app removed container",
|
||||
">>", "kamal-app-audit.log"
|
||||
">>", ".kamal/app-audit.log"
|
||||
], auditor.record("app removed container")
|
||||
end
|
||||
end
|
||||
@@ -52,7 +52,7 @@ class CommandsAuditorTest < ActiveSupport::TestCase
|
||||
:echo,
|
||||
"[#{@recorded_at}] [#{@performer}] [value]",
|
||||
"app removed container",
|
||||
">>", "kamal-app-audit.log"
|
||||
">>", ".kamal/app-audit.log"
|
||||
], @auditor.record("app removed container", detail: "value")
|
||||
end
|
||||
|
||||
|
||||
@@ -103,6 +103,10 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
||||
builder.push.join(" ")
|
||||
end
|
||||
|
||||
test "validate image" do
|
||||
assert_equal "docker inspect -f '{{ .Config.Labels.service }}' dhh/app:123 | grep -x app || (echo \"Image dhh/app:123 is missing the `service` label\" && exit 1)", new_builder_command.validate_image.join(" ")
|
||||
end
|
||||
|
||||
private
|
||||
def new_builder_command(additional_config = {})
|
||||
Kamal::Commands::Builder.new(Kamal::Configuration.new(@config.merge(additional_config), version: "123"))
|
||||
|
||||
@@ -21,6 +21,6 @@ class CommandsDockerTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
test "superuser?" do
|
||||
assert_equal '[ "${EUID:-$(id -u)}" -eq 0 ]', @docker.superuser?.join(" ")
|
||||
assert_equal '[ "${EUID:-$(id -u)}" -eq 0 ] || command -v sudo >/dev/null || command -v su >/dev/null', @docker.superuser?.join(" ")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" dhh/app:123",
|
||||
"docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app -e KAMAL_CONTAINER_NAME=\"healthcheck-app\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" dhh/app:123",
|
||||
new_command.run.join(" ")
|
||||
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\" --health-cmd \"curl -f http://localhost:3001/up || exit 1\" --health-interval \"1s\" dhh/app:123",
|
||||
"docker run --detach --name healthcheck-app-123 --publish 3999:3001 --label service=healthcheck-app -e KAMAL_CONTAINER_NAME=\"healthcheck-app\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3001/up || exit 1\" --health-interval \"1s\" dhh/app:123",
|
||||
new_command.run.join(" ")
|
||||
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\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" dhh/app:123",
|
||||
"docker run --detach --name healthcheck-app-staging-123 --publish 3999:3000 --label service=healthcheck-app-staging -e KAMAL_CONTAINER_NAME=\"healthcheck-app-staging\" --env-file .kamal/env/roles/app-web-staging.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" dhh/app:123",
|
||||
new_command.run.join(" ")
|
||||
end
|
||||
|
||||
@@ -34,14 +34,15 @@ 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\" --health-cmd \"/bin/up\" --health-interval \"1s\" dhh/app:123",
|
||||
"docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app -e KAMAL_CONTAINER_NAME=\"healthcheck-app\" --env-file .kamal/env/roles/app-web.env --health-cmd \"/bin/up\" --health-interval \"1s\" dhh/app:123",
|
||||
new_command.run.join(" ")
|
||||
end
|
||||
|
||||
test "run with custom options" do
|
||||
@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 3999:3000 --label service=healthcheck-app -e KAMAL_CONTAINER_NAME=\"healthcheck-app\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --mount \"somewhere\" dhh/app:123",
|
||||
"docker run --detach --name healthcheck-app-123 --publish 4999:3000 --label service=healthcheck-app -e KAMAL_CONTAINER_NAME=\"healthcheck-app\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --mount \"somewhere\" dhh/app:123",
|
||||
new_command.run.join(" ")
|
||||
end
|
||||
|
||||
@@ -91,6 +92,13 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase
|
||||
new_command.logs.join(" ")
|
||||
end
|
||||
|
||||
test "logs with custom lines number" do
|
||||
@config[:healthcheck] = { "log_lines" => 150 }
|
||||
assert_equal \
|
||||
"docker container ls --all --filter name=^healthcheck-app-123$ --quiet | xargs docker logs --tail 150 2>&1",
|
||||
new_command.logs.join(" ")
|
||||
end
|
||||
|
||||
test "logs with destination" do
|
||||
@destination = "staging"
|
||||
|
||||
|
||||
@@ -10,19 +10,19 @@ class CommandsLockTest < ActiveSupport::TestCase
|
||||
|
||||
test "status" do
|
||||
assert_equal \
|
||||
"stat kamal_lock-app > /dev/null && cat kamal_lock-app/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 \
|
||||
/mkdir kamal_lock-app && echo ".*" > kamal_lock-app\/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_lock-app/details && rm -r kamal_lock-app",
|
||||
"rm .kamal/lock-app/details && rm -r .kamal/lock-app",
|
||||
new_command.release.join(" ")
|
||||
end
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ class CommandsPruneTest < ActiveSupport::TestCase
|
||||
|
||||
test "dangling images" do
|
||||
assert_equal \
|
||||
"docker image prune --force --filter label=service=app --filter dangling=true",
|
||||
"docker image prune --force --filter label=service=app",
|
||||
new_command.dangling_images.join(" ")
|
||||
end
|
||||
|
||||
@@ -20,10 +20,16 @@ class CommandsPruneTest < ActiveSupport::TestCase
|
||||
new_command.tagged_images.join(" ")
|
||||
end
|
||||
|
||||
test "containers" do
|
||||
test "app containers" do
|
||||
assert_equal \
|
||||
"docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +6 | while read container_id; do docker rm $container_id; done",
|
||||
new_command.containers.join(" ")
|
||||
new_command.app_containers.join(" ")
|
||||
end
|
||||
|
||||
test "healthcheck containers" do
|
||||
assert_equal \
|
||||
"docker container prune --force --filter label=service=healthcheck-app",
|
||||
new_command.healthcheck_containers.join(" ")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
23
test/commands/server_test.rb
Normal file
23
test/commands/server_test.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
require "test_helper"
|
||||
|
||||
class CommandsServerTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@config = {
|
||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ],
|
||||
traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
|
||||
}
|
||||
end
|
||||
|
||||
test "ensure run directory" do
|
||||
assert_equal "mkdir -p .kamal", new_command.ensure_run_directory.join(" ")
|
||||
end
|
||||
|
||||
test "ensure non default run directory" do
|
||||
assert_equal "mkdir -p /var/run/kamal", new_command(run_directory: "/var/run/kamal").ensure_run_directory.join(" ")
|
||||
end
|
||||
|
||||
private
|
||||
def new_command(extra_config = {})
|
||||
Kamal::Commands::Server.new(Kamal::Configuration.new(@config.merge(extra_config)))
|
||||
end
|
||||
end
|
||||
@@ -18,67 +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 --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
new_command.run.join(" ")
|
||||
|
||||
@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 --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
"docker run --name traefik --detach --restart unless-stopped --publish 8080:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
new_command.run.join(" ")
|
||||
|
||||
@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\"",
|
||||
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 --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
new_command.run.join(" ")
|
||||
|
||||
@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 --log-opt max-size=\"10m\" --publish \"9000:9000\" --publish \"9001:9001\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --publish \"9000:9000\" --publish \"9001:9001\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
new_command.run.join(" ")
|
||||
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 --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
new_command.run.join(" ")
|
||||
|
||||
@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 --log-opt max-size=\"10m\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
new_command.run.join(" ")
|
||||
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 --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
new_command.run.join(" ")
|
||||
|
||||
@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 --log-opt max-size=\"10m\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" --publish \"8080:8080\" --memory \"512m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" --publish \"8080:8080\" --memory \"512m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
new_command.run.join(" ")
|
||||
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 --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
new_command.run.join(" ")
|
||||
|
||||
@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 --log-opt max-size=\"10m\" --label traefik.http.routers.dashboard.service=\"api@internal\" --label traefik.http.routers.dashboard.middlewares=\"auth\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" --label traefik.http.routers.dashboard.service=\"api@internal\" --label traefik.http.routers.dashboard.middlewares=\"auth\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
new_command.run.join(" ")
|
||||
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 --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
new_command.run.join(" ")
|
||||
|
||||
@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 -e EXAMPLE_API_KEY=\"456\" --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
new_command.run.join(" ")
|
||||
end
|
||||
|
||||
@@ -86,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 --log-opt max-size=\"10m\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"",
|
||||
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"",
|
||||
new_command.run.join(" ")
|
||||
end
|
||||
|
||||
@@ -94,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 --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
new_command.run.join(" ")
|
||||
end
|
||||
|
||||
@@ -102,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 --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"ERROR\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"ERROR\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
new_command.run.join(" ")
|
||||
end
|
||||
|
||||
@@ -172,6 +177,24 @@ class CommandsTraefikTest < ActiveSupport::TestCase
|
||||
new_command.follow_logs(host: @config[:servers].first, grep: 'hello!')
|
||||
end
|
||||
|
||||
test "env_file" do
|
||||
@config[:traefik]["env"] = { "secret" => %w[EXAMPLE_API_KEY] }
|
||||
|
||||
assert_equal "EXAMPLE_API_KEY=456\n", new_command.env_file.to_s
|
||||
end
|
||||
|
||||
test "host_env_file_path" do
|
||||
assert_equal ".kamal/env/traefik/traefik.env", new_command.host_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(" ")
|
||||
end
|
||||
|
||||
private
|
||||
def new_command
|
||||
Kamal::Commands::Traefik.new(Kamal::Configuration.new(@config, version: "123"))
|
||||
|
||||
@@ -110,19 +110,30 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
|
||||
assert_equal ["--label", "service=\"app-redis\"", "--label", "cache=\"true\""], @config.accessory(:redis).label_args
|
||||
end
|
||||
|
||||
test "env args with secret" do
|
||||
test "env args" do
|
||||
assert_equal ["--env-file", ".kamal/env/accessories/app-mysql.env"], @config.accessory(:mysql).env_args
|
||||
assert_equal ["--env-file", ".kamal/env/accessories/app-redis.env"], @config.accessory(:redis).env_args
|
||||
end
|
||||
|
||||
test "env file with secret" do
|
||||
ENV["MYSQL_ROOT_PASSWORD"] = "secret123"
|
||||
|
||||
@config.accessory(:mysql).env_args.tap do |env_args|
|
||||
assert_equal ["-e", "MYSQL_ROOT_PASSWORD=\"secret123\"", "-e", "MYSQL_ROOT_HOST=\"%\""], Kamal::Utils.unredacted(env_args)
|
||||
assert_equal ["-e", "MYSQL_ROOT_PASSWORD=[REDACTED]", "-e", "MYSQL_ROOT_HOST=\"%\""], Kamal::Utils.redacted(env_args)
|
||||
end
|
||||
expected = <<~ENV
|
||||
MYSQL_ROOT_PASSWORD=secret123
|
||||
MYSQL_ROOT_HOST=%
|
||||
ENV
|
||||
|
||||
assert_equal expected, @config.accessory(:mysql).env_file.to_s
|
||||
ensure
|
||||
ENV["MYSQL_ROOT_PASSWORD"] = nil
|
||||
end
|
||||
|
||||
test "env args without secret" do
|
||||
assert_equal ["-e", "SOMETHING=\"else\""], @config.accessory(:redis).env_args
|
||||
test "host_env_directory" do
|
||||
assert_equal ".kamal/env/accessories", @config.accessory(:mysql).host_env_directory
|
||||
end
|
||||
|
||||
test "host_env_file_path" do
|
||||
assert_equal ".kamal/env/accessories/app-mysql.env", @config.accessory(:mysql).host_env_file_path
|
||||
end
|
||||
|
||||
test "volume args" do
|
||||
|
||||
@@ -42,7 +42,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
test "special label args for web" do
|
||||
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.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,12 +66,31 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
|
||||
c[:servers]["beta"] = { "traefik" => "true", "hosts" => [ "1.1.1.5" ] }
|
||||
})
|
||||
|
||||
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.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["REDIS_URL"]
|
||||
assert_equal ["-e", "REDIS_URL=\"redis://a/b\"", "-e", "WEB_CONCURRENCY=\"4\""], @config_with_roles.role(:workers).env_args
|
||||
|
||||
expected_env = <<~ENV
|
||||
REDIS_URL=redis://a/b
|
||||
WEB_CONCURRENCY=4
|
||||
ENV
|
||||
|
||||
assert_equal expected_env, @config_with_roles.role(:workers).env_file.to_s
|
||||
end
|
||||
|
||||
test "container name" do
|
||||
ENV["VERSION"] = "12345"
|
||||
|
||||
assert_equal "app-workers-12345", @config_with_roles.role(:workers).container_name
|
||||
assert_equal "app-web-12345", @config_with_roles.role(:web).container_name
|
||||
ensure
|
||||
ENV.delete("VERSION")
|
||||
end
|
||||
|
||||
test "env args" do
|
||||
assert_equal ["--env-file", ".kamal/env/roles/app-workers.env"], @config_with_roles.role(:workers).env_args
|
||||
end
|
||||
|
||||
test "env secret overwritten by role" do
|
||||
@@ -97,10 +116,14 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
|
||||
ENV["REDIS_PASSWORD"] = "secret456"
|
||||
ENV["DB_PASSWORD"] = "secret&\"123"
|
||||
|
||||
@config_with_roles.role(:workers).env_args.tap do |env_args|
|
||||
assert_equal ["-e", "REDIS_PASSWORD=\"secret456\"", "-e", "DB_PASSWORD=\"secret&\\\"123\"", "-e", "REDIS_URL=\"redis://a/b\"", "-e", "WEB_CONCURRENCY=\"4\""], Kamal::Utils.unredacted(env_args)
|
||||
assert_equal ["-e", "REDIS_PASSWORD=[REDACTED]", "-e", "DB_PASSWORD=[REDACTED]", "-e", "REDIS_URL=\"redis://a/b\"", "-e", "WEB_CONCURRENCY=\"4\""], Kamal::Utils.redacted(env_args)
|
||||
end
|
||||
expected = <<~ENV
|
||||
REDIS_PASSWORD=secret456
|
||||
DB_PASSWORD=secret&\"123
|
||||
REDIS_URL=redis://a/b
|
||||
WEB_CONCURRENCY=4
|
||||
ENV
|
||||
|
||||
assert_equal expected, @config_with_roles.role(:workers).env_file.to_s
|
||||
ensure
|
||||
ENV["REDIS_PASSWORD"] = nil
|
||||
ENV["DB_PASSWORD"] = nil
|
||||
@@ -119,10 +142,13 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
|
||||
|
||||
ENV["DB_PASSWORD"] = "secret123"
|
||||
|
||||
@config_with_roles.role(:workers).env_args.tap do |env_args|
|
||||
assert_equal ["-e", "DB_PASSWORD=\"secret123\"", "-e", "REDIS_URL=\"redis://a/b\"", "-e", "WEB_CONCURRENCY=\"4\""], Kamal::Utils.unredacted(env_args)
|
||||
assert_equal ["-e", "DB_PASSWORD=[REDACTED]", "-e", "REDIS_URL=\"redis://a/b\"", "-e", "WEB_CONCURRENCY=\"4\""], Kamal::Utils.redacted(env_args)
|
||||
end
|
||||
expected = <<~ENV
|
||||
DB_PASSWORD=secret123
|
||||
REDIS_URL=redis://a/b
|
||||
WEB_CONCURRENCY=4
|
||||
ENV
|
||||
|
||||
assert_equal expected, @config_with_roles.role(:workers).env_file.to_s
|
||||
ensure
|
||||
ENV["DB_PASSWORD"] = nil
|
||||
end
|
||||
@@ -139,11 +165,91 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
|
||||
|
||||
ENV["REDIS_PASSWORD"] = "secret456"
|
||||
|
||||
@config_with_roles.role(:workers).env_args.tap do |env_args|
|
||||
assert_equal ["-e", "REDIS_PASSWORD=\"secret456\"", "-e", "REDIS_URL=\"redis://a/b\"", "-e", "WEB_CONCURRENCY=\"4\""], Kamal::Utils.unredacted(env_args)
|
||||
assert_equal ["-e", "REDIS_PASSWORD=[REDACTED]", "-e", "REDIS_URL=\"redis://a/b\"", "-e", "WEB_CONCURRENCY=\"4\""], Kamal::Utils.redacted(env_args)
|
||||
end
|
||||
expected = <<~ENV
|
||||
REDIS_PASSWORD=secret456
|
||||
REDIS_URL=redis://a/b
|
||||
WEB_CONCURRENCY=4
|
||||
ENV
|
||||
|
||||
assert_equal expected, @config_with_roles.role(:workers).env_file.to_s
|
||||
ensure
|
||||
ENV["REDIS_PASSWORD"] = nil
|
||||
end
|
||||
|
||||
test "host_env_directory" do
|
||||
assert_equal ".kamal/env/roles", @config_with_roles.role(:workers).host_env_directory
|
||||
end
|
||||
|
||||
test "host_env_file_path" do
|
||||
assert_equal ".kamal/env/roles/app-workers.env", @config_with_roles.role(:workers).host_env_file_path
|
||||
end
|
||||
|
||||
test "uses cord" do
|
||||
assert @config_with_roles.role(:web).uses_cord?
|
||||
assert !@config_with_roles.role(:workers).uses_cord?
|
||||
end
|
||||
|
||||
test "cord host file" do
|
||||
assert_match %r{.kamal/cords/app-web-[0-9a-f]{32}/cord}, @config_with_roles.role(:web).cord_host_file
|
||||
end
|
||||
|
||||
test "cord volume" do
|
||||
assert_equal "/tmp/kamal-cord", @config_with_roles.role(:web).cord_volume.container_path
|
||||
assert_match %r{.kamal/cords/app-web-[0-9a-f]{32}}, @config_with_roles.role(:web).cord_volume.host_path
|
||||
assert_equal "--volume", @config_with_roles.role(:web).cord_volume.docker_args[0]
|
||||
assert_match %r{\$\(pwd\)/.kamal/cords/app-web-[0-9a-f]{32}:/tmp/kamal-cord}, @config_with_roles.role(:web).cord_volume.docker_args[1]
|
||||
end
|
||||
|
||||
test "cord container file" do
|
||||
assert_equal "/tmp/kamal-cord/cord", @config_with_roles.role(:web).cord_container_file
|
||||
end
|
||||
|
||||
test "asset path and volume args" do
|
||||
ENV["VERSION"] = "12345"
|
||||
assert_nil @config_with_roles.role(:web).asset_volume_args
|
||||
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 !@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"
|
||||
})
|
||||
assert_equal "foo", config_with_assets.role(:web).asset_path
|
||||
assert_equal "foo", config_with_assets.role(:workers).asset_path
|
||||
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 !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" }
|
||||
})
|
||||
assert_equal "bar", config_with_assets.role(:web).asset_path
|
||||
assert_nil config_with_assets.role(:workers).asset_path
|
||||
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 !config_with_assets.role(:workers).assets?
|
||||
|
||||
ensure
|
||||
ENV.delete("VERSION")
|
||||
end
|
||||
|
||||
test "asset extracted path" do
|
||||
ENV["VERSION"] = "12345"
|
||||
assert_equal ".kamal/assets/extracted/app-web-12345", @config_with_roles.role(:web).asset_extracted_path
|
||||
assert_equal ".kamal/assets/extracted/app-workers-12345", @config_with_roles.role(:workers).asset_extracted_path
|
||||
ensure
|
||||
ENV.delete("VERSION")
|
||||
end
|
||||
|
||||
test "asset volume path" do
|
||||
ENV["VERSION"] = "12345"
|
||||
assert_equal ".kamal/assets/volumes/app-web-12345", @config_with_roles.role(:web).asset_volume_path
|
||||
assert_equal ".kamal/assets/volumes/app-workers-12345", @config_with_roles.role(:workers).asset_volume_path
|
||||
ensure
|
||||
ENV.delete("VERSION")
|
||||
end
|
||||
end
|
||||
|
||||
13
test/configuration/volume_test.rb
Normal file
13
test/configuration/volume_test.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
require "test_helper"
|
||||
|
||||
class ConfigurationVolumeTest < ActiveSupport::TestCase
|
||||
test "docker args absolute" do
|
||||
volume = Kamal::Configuration::Volume.new(host_path: "/root/foo/bar", container_path: "/assets")
|
||||
assert_equal ["--volume", "/root/foo/bar:/assets"], volume.docker_args
|
||||
end
|
||||
|
||||
test "docker args relative" do
|
||||
volume = Kamal::Configuration::Volume.new(host_path: "foo/bar", container_path: "/assets")
|
||||
assert_equal ["--volume", "$(pwd)/foo/bar:/assets"], volume.docker_args
|
||||
end
|
||||
end
|
||||
@@ -75,7 +75,7 @@ class ConfigurationTest < ActiveSupport::TestCase
|
||||
test "version no git repo" do
|
||||
ENV.delete("VERSION")
|
||||
|
||||
@config.expects(:system).with("git rev-parse").returns(nil)
|
||||
Kamal::Git.expects(:used?).returns(nil)
|
||||
error = assert_raises(RuntimeError) { @config.version}
|
||||
assert_match /no git repository found/, error.message
|
||||
end
|
||||
@@ -83,16 +83,16 @@ class ConfigurationTest < ActiveSupport::TestCase
|
||||
test "version from git committed" do
|
||||
ENV.delete("VERSION")
|
||||
|
||||
@config.expects(:`).with("git rev-parse HEAD").returns("git-version")
|
||||
Kamal::Utils.expects(:uncommitted_changes).returns("")
|
||||
Kamal::Git.expects(:revision).returns("git-version")
|
||||
Kamal::Git.expects(:uncommitted_changes).returns("")
|
||||
assert_equal "git-version", @config.version
|
||||
end
|
||||
|
||||
test "version from git uncommitted" do
|
||||
ENV.delete("VERSION")
|
||||
|
||||
@config.expects(:`).with("git rev-parse HEAD").returns("git-version")
|
||||
Kamal::Utils.expects(:uncommitted_changes).returns("M file\n")
|
||||
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
|
||||
end
|
||||
|
||||
@@ -124,50 +124,8 @@ class ConfigurationTest < ActiveSupport::TestCase
|
||||
assert_equal "app-missing", @config.service_with_version
|
||||
end
|
||||
|
||||
test "env args" do
|
||||
assert_equal [ "-e", "REDIS_URL=\"redis://x/y\"" ], @config.env_args
|
||||
end
|
||||
|
||||
test "env args with clear and secrets" do
|
||||
ENV["PASSWORD"] = "secret123"
|
||||
|
||||
config = Kamal::Configuration.new(@deploy.tap { |c| c.merge!({
|
||||
env: { "clear" => { "PORT" => "3000" }, "secret" => [ "PASSWORD" ] }
|
||||
}) })
|
||||
|
||||
assert_equal [ "-e", "PASSWORD=\"secret123\"", "-e", "PORT=\"3000\"" ], Kamal::Utils.unredacted(config.env_args)
|
||||
assert_equal [ "-e", "PASSWORD=[REDACTED]", "-e", "PORT=\"3000\"" ], Kamal::Utils.redacted(config.env_args)
|
||||
ensure
|
||||
ENV["PASSWORD"] = nil
|
||||
end
|
||||
|
||||
test "env args with only clear" do
|
||||
config = Kamal::Configuration.new(@deploy.tap { |c| c.merge!({
|
||||
env: { "clear" => { "PORT" => "3000" } }
|
||||
}) })
|
||||
|
||||
assert_equal [ "-e", "PORT=\"3000\"" ], config.env_args
|
||||
end
|
||||
|
||||
test "env args with only secrets" do
|
||||
ENV["PASSWORD"] = "secret123"
|
||||
|
||||
config = Kamal::Configuration.new(@deploy.tap { |c| c.merge!({
|
||||
env: { "secret" => [ "PASSWORD" ] }
|
||||
}) })
|
||||
|
||||
assert_equal [ "-e", "PASSWORD=\"secret123\"" ], Kamal::Utils.unredacted(config.env_args)
|
||||
assert_equal [ "-e", "PASSWORD=[REDACTED]" ], Kamal::Utils.redacted(config.env_args)
|
||||
ensure
|
||||
ENV["PASSWORD"] = nil
|
||||
end
|
||||
|
||||
test "env args with missing secret" do
|
||||
assert_raises(KeyError) do
|
||||
config = Kamal::Configuration.new(@deploy.tap { |c| c.merge!({
|
||||
env: { "secret" => [ "PASSWORD" ] }
|
||||
}) }).ensure_env_available
|
||||
end
|
||||
test "healthcheck service" do
|
||||
assert_equal "healthcheck-app", @config.healthcheck_service
|
||||
end
|
||||
|
||||
test "valid config" do
|
||||
@@ -248,6 +206,18 @@ class ConfigurationTest < ActiveSupport::TestCase
|
||||
end
|
||||
end
|
||||
|
||||
test "destination required" do
|
||||
dest_config_file = Pathname.new(File.expand_path("fixtures/deploy_for_required_dest.yml", __dir__))
|
||||
|
||||
assert_raises(ArgumentError) do
|
||||
config = Kamal::Configuration.create_from config_file: dest_config_file
|
||||
end
|
||||
|
||||
assert_nothing_raised do
|
||||
config = Kamal::Configuration.create_from config_file: dest_config_file, destination: "world"
|
||||
end
|
||||
end
|
||||
|
||||
test "to_h" do
|
||||
expected_config = \
|
||||
{ :roles=>["web"],
|
||||
@@ -257,13 +227,12 @@ class ConfigurationTest < ActiveSupport::TestCase
|
||||
:repository=>"dhh/app",
|
||||
:absolute_image=>"dhh/app:missing",
|
||||
:service_with_version=>"app-missing",
|
||||
:env_args=>["-e", "REDIS_URL=\"redis://x/y\""],
|
||||
:ssh_options=>{ :user=>"root", :auth_methods=>["publickey"], log_level: :fatal, keepalive: true, keepalive_interval: 30 },
|
||||
:ssh_options=>{ :user=>"root", 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 }}
|
||||
: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
|
||||
@@ -283,4 +252,30 @@ class ConfigurationTest < ActiveSupport::TestCase
|
||||
Kamal::Configuration.new(@deploy.tap { |c| c.merge!(minimum_version: "10000.0.0") })
|
||||
end
|
||||
end
|
||||
|
||||
test "run directory" do
|
||||
config = Kamal::Configuration.new(@deploy)
|
||||
assert_equal ".kamal", config.run_directory
|
||||
|
||||
config = Kamal::Configuration.new(@deploy.merge!(run_directory: "/root/kamal"))
|
||||
assert_equal "/root/kamal", config.run_directory
|
||||
end
|
||||
|
||||
test "run directory as docker volume" do
|
||||
config = Kamal::Configuration.new(@deploy)
|
||||
assert_equal "$(pwd)/.kamal", config.run_directory_as_docker_volume
|
||||
|
||||
config = Kamal::Configuration.new(@deploy.merge!(run_directory: "/root/kamal"))
|
||||
assert_equal "/root/kamal", config.run_directory_as_docker_volume
|
||||
end
|
||||
|
||||
test "run id" do
|
||||
SecureRandom.expects(:hex).with(16).returns("09876543211234567890098765432112")
|
||||
assert_equal "09876543211234567890098765432112", @config.run_id
|
||||
end
|
||||
|
||||
test "asset path" do
|
||||
assert_nil @config.asset_path
|
||||
assert_equal "foo", Kamal::Configuration.new(@deploy.merge!(asset_path: "foo")).asset_path
|
||||
end
|
||||
end
|
||||
|
||||
102
test/env_file_test.rb
Normal file
102
test/env_file_test.rb
Normal file
@@ -0,0 +1,102 @@
|
||||
require "test_helper"
|
||||
|
||||
class EnvFileTest < ActiveSupport::TestCase
|
||||
test "env file simple" do
|
||||
env = {
|
||||
"foo" => "bar",
|
||||
"baz" => "haz"
|
||||
}
|
||||
|
||||
assert_equal "foo=bar\nbaz=haz\n", \
|
||||
Kamal::EnvFile.new(env).to_s
|
||||
end
|
||||
|
||||
test "env file clear" do
|
||||
env = {
|
||||
"clear" => {
|
||||
"foo" => "bar",
|
||||
"baz" => "haz"
|
||||
}
|
||||
}
|
||||
|
||||
assert_equal "foo=bar\nbaz=haz\n", \
|
||||
Kamal::EnvFile.new(env).to_s
|
||||
end
|
||||
|
||||
test "env file empty" do
|
||||
assert_equal "\n", Kamal::EnvFile.new({}).to_s
|
||||
end
|
||||
|
||||
test "env file secret" do
|
||||
ENV["PASSWORD"] = "hello"
|
||||
env = {
|
||||
"secret" => [ "PASSWORD" ]
|
||||
}
|
||||
|
||||
assert_equal "PASSWORD=hello\n", \
|
||||
Kamal::EnvFile.new(env).to_s
|
||||
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::EnvFile.new(env).to_s
|
||||
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::EnvFile.new(env).to_s
|
||||
ensure
|
||||
ENV.delete "PASSWORD"
|
||||
end
|
||||
|
||||
test "env file missing secret" do
|
||||
env = {
|
||||
"secret" => [ "PASSWORD" ]
|
||||
}
|
||||
|
||||
assert_raises(KeyError) { Kamal::EnvFile.new(env).to_s }
|
||||
|
||||
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\nfoo=bar\nbaz=haz\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
|
||||
5
test/fixtures/deploy_for_required_dest.world.yml
vendored
Normal file
5
test/fixtures/deploy_for_required_dest.world.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
servers:
|
||||
- 1.1.1.1
|
||||
- 1.1.1.2
|
||||
env:
|
||||
REDIS_URL: redis://x/y
|
||||
7
test/fixtures/deploy_for_required_dest.yml
vendored
Normal file
7
test/fixtures/deploy_for_required_dest.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
service: app
|
||||
image: dhh/app
|
||||
registry:
|
||||
server: registry.digitalocean.com
|
||||
username: <%= "my-user" %>
|
||||
password: <%= "my-password" %>
|
||||
require_destination: true
|
||||
9
test/fixtures/deploy_with_assets.yml
vendored
Normal file
9
test/fixtures/deploy_with_assets.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
service: app
|
||||
image: dhh/app
|
||||
servers:
|
||||
- "1.1.1.1"
|
||||
- "1.1.1.2"
|
||||
registry:
|
||||
username: user
|
||||
password: pw
|
||||
asset_path: /public/assets
|
||||
41
test/fixtures/deploy_with_remote_builder.yml
vendored
Normal file
41
test/fixtures/deploy_with_remote_builder.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
builder:
|
||||
remote:
|
||||
arch: amd64
|
||||
host: ssh://app@1.1.1.5
|
||||
13
test/git_test.rb
Normal file
13
test/git_test.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
require "test_helper"
|
||||
|
||||
class GitTest < ActiveSupport::TestCase
|
||||
test "uncommitted changes exist" do
|
||||
Kamal::Git.expects(:`).with("git status --porcelain").returns("M file\n")
|
||||
assert_equal "M file", Kamal::Git.uncommitted_changes
|
||||
end
|
||||
|
||||
test "uncommitted changes do not exist" do
|
||||
Kamal::Git.expects(:`).with("git status --porcelain").returns("")
|
||||
assert_equal "", Kamal::Git.uncommitted_changes
|
||||
end
|
||||
end
|
||||
@@ -2,6 +2,8 @@ require_relative "integration_test"
|
||||
|
||||
class AccessoryTest < IntegrationTest
|
||||
test "boot, stop, start, restart, logs, remove" do
|
||||
kamal :envify
|
||||
|
||||
kamal :accessory, :boot, :busybox
|
||||
assert_accessory_running :busybox
|
||||
|
||||
@@ -19,6 +21,8 @@ class AccessoryTest < IntegrationTest
|
||||
|
||||
kamal :accessory, :remove, :busybox, "-y"
|
||||
assert_accessory_not_running :busybox
|
||||
|
||||
kamal :env, :delete
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -2,14 +2,15 @@ require_relative "integration_test"
|
||||
|
||||
class AppTest < IntegrationTest
|
||||
test "stop, start, boot, logs, images, containers, exec, remove" do
|
||||
kamal :envify
|
||||
|
||||
kamal :deploy
|
||||
|
||||
assert_app_is_up
|
||||
|
||||
kamal :app, :stop
|
||||
|
||||
# traefik is up and returns 404s when it can't match a route
|
||||
assert_app_not_found
|
||||
assert_app_is_down
|
||||
|
||||
kamal :app, :start
|
||||
|
||||
@@ -49,7 +50,6 @@ class AppTest < IntegrationTest
|
||||
|
||||
kamal :app, :remove
|
||||
|
||||
# traefik is up and returns 404s when it can't match a route
|
||||
assert_app_not_found
|
||||
assert_app_is_down
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,6 +2,8 @@ FROM ruby:3.2
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV VERBOSE=true
|
||||
|
||||
RUN apt-get update --fix-missing && apt-get install -y ca-certificates openssh-client curl gnupg docker.io
|
||||
|
||||
RUN install -m 0755 -d /etc/apt/keyrings
|
||||
@@ -23,7 +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 git init && 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
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "About to lock..."
|
||||
if [ "$MRSK_HOSTS" != "vm1,vm2" ]; then
|
||||
echo "Expected hosts to be 'vm1,vm2', got $MRSK_HOSTS"
|
||||
if [ "$KAMAL_HOSTS" != "vm1,vm2" ]; then
|
||||
echo "Expected hosts to be 'vm1,vm2', got $KAMAL_HOSTS"
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-connect
|
||||
@@ -4,4 +4,6 @@ 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
|
||||
|
||||
|
||||
@@ -3,6 +3,13 @@ image: app
|
||||
servers:
|
||||
- vm1
|
||||
- vm2
|
||||
env:
|
||||
clear:
|
||||
CLEAR_TOKEN: '4321'
|
||||
secret:
|
||||
- SECRET_TOKEN
|
||||
asset_path: /usr/share/nginx/html/versions
|
||||
|
||||
registry:
|
||||
server: registry:4443
|
||||
username: root
|
||||
@@ -12,7 +19,7 @@ builder:
|
||||
args:
|
||||
COMMIT_SHA: <%= `git rev-parse HEAD` %>
|
||||
healthcheck:
|
||||
cmd: wget -qO- http://localhost > /dev/null
|
||||
cmd: wget -qO- http://localhost > /dev/null || exit 1
|
||||
traefik:
|
||||
args:
|
||||
accesslog: true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM ubuntu:22.10
|
||||
FROM ubuntu:22.04
|
||||
|
||||
WORKDIR /work
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM ubuntu:22.10
|
||||
FROM ubuntu:22.04
|
||||
|
||||
WORKDIR /work
|
||||
|
||||
|
||||
@@ -55,12 +55,6 @@ class IntegrationTest < ActiveSupport::TestCase
|
||||
assert_app_version(version, response) if version
|
||||
end
|
||||
|
||||
def assert_app_not_found
|
||||
response = app_response
|
||||
debug_response_code(response, "404")
|
||||
assert_equal "404", response.code
|
||||
end
|
||||
|
||||
def wait_for_app_to_be_up(timeout: 20, up_count: 3)
|
||||
timeout_at = Time.now + timeout
|
||||
up_times = 0
|
||||
|
||||
@@ -2,6 +2,8 @@ require_relative "integration_test"
|
||||
|
||||
class LockTest < IntegrationTest
|
||||
test "acquire, release, status" do
|
||||
kamal :envify
|
||||
|
||||
kamal :lock, :acquire, "-m 'Integration Tests'"
|
||||
|
||||
status = kamal :lock, :status, capture: true
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
require_relative "integration_test"
|
||||
|
||||
class MainTest < IntegrationTest
|
||||
test "deploy, redeploy, rollback, details and audit" do
|
||||
test "envify, deploy, redeploy, rollback, details and audit" do
|
||||
kamal :envify
|
||||
assert_local_env_file "SECRET_TOKEN=1234"
|
||||
assert_remote_env_file "SECRET_TOKEN=1234\nCLEAR_TOKEN=4321"
|
||||
remove_local_env_file
|
||||
|
||||
first_version = latest_app_version
|
||||
|
||||
assert_app_is_down
|
||||
@@ -16,6 +21,8 @@ class MainTest < IntegrationTest
|
||||
assert_app_is_up version: second_version
|
||||
assert_hooks_ran "pre-connect", "pre-build", "pre-deploy", "post-deploy"
|
||||
|
||||
assert_accumulated_assets first_version, second_version
|
||||
|
||||
kamal :rollback, first_version
|
||||
assert_hooks_ran "pre-connect", "pre-deploy", "post-deploy"
|
||||
assert_app_is_up version: first_version
|
||||
@@ -30,12 +37,9 @@ class MainTest < IntegrationTest
|
||||
|
||||
audit = kamal :audit, capture: true
|
||||
assert_match /Booted app version #{first_version}.*Booted app version #{second_version}.*Booted app version #{first_version}.*/m, audit
|
||||
end
|
||||
|
||||
test "envify" do
|
||||
kamal :envify
|
||||
|
||||
assert_equal "SECRET_TOKEN=1234", deployer_exec("cat .env", capture: true)
|
||||
kamal :env, :delete
|
||||
assert_no_remote_env_file
|
||||
end
|
||||
|
||||
test "config" do
|
||||
@@ -49,11 +53,35 @@ class MainTest < IntegrationTest
|
||||
assert_equal "registry:4443/app", config[:repository]
|
||||
assert_equal "registry:4443/app:#{version}", config[:absolute_image]
|
||||
assert_equal "app-#{version}", config[:service_with_version]
|
||||
assert_equal [], config[:env_args]
|
||||
assert_equal [], config[:volume_args]
|
||||
assert_equal({ user: "root", auth_methods: [ "publickey" ], keepalive: true, keepalive_interval: 30, log_level: :fatal }, config[:ssh_options])
|
||||
assert_equal({ user: "root", keepalive: true, keepalive_interval: 30, log_level: :fatal }, config[:ssh_options])
|
||||
assert_equal({ "multiarch" => false, "args" => { "COMMIT_SHA" => version } }, config[:builder])
|
||||
assert_equal [ "--log-opt", "max-size=\"10m\"" ], config[:logging]
|
||||
assert_equal({ "path" => "/up", "port" => 3000, "max_attempts" => 7, "cmd" => "wget -qO- http://localhost > /dev/null" }, config[:healthcheck])
|
||||
assert_equal({ "path" => "/up", "port" => 3000, "max_attempts" => 7, "exposed_port" => 3999, "cord"=>"/tmp/kamal-cord", "log_lines" => 50, "cmd"=>"wget -qO- http://localhost > /dev/null || exit 1" }, config[:healthcheck])
|
||||
end
|
||||
|
||||
private
|
||||
def assert_local_env_file(contents)
|
||||
assert_equal contents, deployer_exec("cat .env", 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)
|
||||
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)
|
||||
end
|
||||
|
||||
def assert_accumulated_assets(*versions)
|
||||
versions.each do |version|
|
||||
assert_equal "200", Net::HTTP.get_response(URI.parse("http://localhost:12345/versions/#{version}")).code
|
||||
end
|
||||
|
||||
assert_equal "200", Net::HTTP.get_response(URI.parse("http://localhost:12345/versions/.hidden")).code
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,6 +2,8 @@ require_relative "integration_test"
|
||||
|
||||
class TraefikTest < IntegrationTest
|
||||
test "boot, reboot, stop, start, restart, logs, remove" do
|
||||
kamal :envify
|
||||
|
||||
kamal :traefik, :boot
|
||||
assert_traefik_running
|
||||
|
||||
@@ -33,6 +35,8 @@ class TraefikTest < IntegrationTest
|
||||
|
||||
kamal :traefik, :remove
|
||||
assert_traefik_not_running
|
||||
|
||||
kamal :env, :delete
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -11,15 +11,6 @@ class UtilsTest < ActiveSupport::TestCase
|
||||
Kamal::Utils.argumentize("--label", { foo: "bar" }, sensitive: true).last
|
||||
end
|
||||
|
||||
test "argumentize_env_with_secrets" do
|
||||
ENV.expects(:fetch).with("FOO").returns("secret")
|
||||
|
||||
args = Kamal::Utils.argumentize_env_with_secrets({ "secret" => [ "FOO" ], "clear" => { BAZ: "qux" } })
|
||||
|
||||
assert_equal [ "-e", "FOO=[REDACTED]", "-e", "BAZ=\"qux\"" ], Kamal::Utils.redacted(args)
|
||||
assert_equal [ "-e", "FOO=\"secret\"", "-e", "BAZ=\"qux\"" ], Kamal::Utils.unredacted(args)
|
||||
end
|
||||
|
||||
test "optionize" do
|
||||
assert_equal [ "--foo", "\"bar\"", "--baz", "\"qux\"", "--quux" ], \
|
||||
Kamal::Utils.optionize({ foo: "bar", baz: "qux", quux: true })
|
||||
@@ -61,14 +52,4 @@ class UtilsTest < ActiveSupport::TestCase
|
||||
assert_equal "\"https://example.com/\\$2\"",
|
||||
Kamal::Utils.escape_shell_value("https://example.com/$2")
|
||||
end
|
||||
|
||||
test "uncommitted changes exist" do
|
||||
Kamal::Utils.expects(:`).with("git status --porcelain").returns("M file\n")
|
||||
assert_equal "M file", Kamal::Utils.uncommitted_changes
|
||||
end
|
||||
|
||||
test "uncommitted changes do not exist" do
|
||||
Kamal::Utils.expects(:`).with("git status --porcelain").returns("")
|
||||
assert_equal "", Kamal::Utils.uncommitted_changes
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user