Compare commits

..

28 Commits

Author SHA1 Message Date
Donal McBreen
10f3d33cb5 WIP 2024-09-23 15:18:31 +01:00
Donal McBreen
9fd4001a88 Bind to 127.0.0.1 2024-09-23 15:18:31 +01:00
Donal McBreen
6aa707e233 Use local registry for app images
Allow applications to be deployed without needing to set up a repository
in a remote Docker registry.

If the registry server starts with `localhost`, Kamal will start a local
docker registry on that port and push the app image to it.

Then when pulling the image onto the servers, we use net-ssh to forward
the that port from the app server to the deployment server.

This will allow the deployment server to pull the image from the
registry as if it were local, meaning we don't need to set up a cert.
2024-09-23 15:18:28 +01:00
David Heinemeier Hansson
e71bfcbadd Bump version for 2.0.0.rc2 2024-09-20 15:41:26 -07:00
David Heinemeier Hansson
567309596a Make the skip of timestamps a boolean 2024-09-20 12:50:46 -07:00
David Heinemeier Hansson
b89ec2bf63 Bump version for 2.0.0.rc1 2024-09-20 11:08:45 -07:00
David Heinemeier Hansson
3172adca30 Merge pull request #958 from basecamp/optional-timestamps
Add option to skip timestamps on logging output
2024-09-20 18:01:14 +02:00
David Heinemeier Hansson
04d21f45bb Fix test 2024-09-20 08:45:40 -07:00
David Heinemeier Hansson
eabd57350c Fix tests 2024-09-20 08:33:14 -07:00
David Heinemeier Hansson
487f6f5f53 Fix excess spacing 2024-09-20 08:31:56 -07:00
David Heinemeier Hansson
d98500982d Update tests 2024-09-20 08:19:38 -07:00
David Heinemeier Hansson
8693e968c1 Timestamps now default on for app logs too 2024-09-20 08:17:19 -07:00
David Heinemeier Hansson
6ab5fc9459 Allow timestamps on/off for app logging too 2024-09-20 08:04:28 -07:00
David Heinemeier Hansson
6fc2915884 Merge branch 'main' into optional-timestamps 2024-09-20 07:58:49 -07:00
David Heinemeier Hansson
afa6898a82 Fix pipe 2024-09-20 07:58:38 -07:00
David Heinemeier Hansson
384b36d158 Add option to skip timestamps on logging output
So it is easier to follow live when you are doing debugging, especially
early days app setup when you are the only user.
2024-09-20 07:42:31 -07:00
Donal McBreen
6df169a4fb Doc updates 2024-09-20 15:27:10 +01:00
Donal McBreen
ab109afc52 Merge pull request #957 from basecamp/numeric-timeouts
Response timeout should be a number
2024-09-20 09:50:05 +01:00
Donal McBreen
a6a48c456c Response timeout should be a number
Kamal will append the `s` for the duration when talking to kamal-proxy
so no need to have it in the config.
2024-09-20 09:26:06 +01:00
David Heinemeier Hansson
a4e5dbe5d4 Bump version for 2.0.0.beta2 2024-09-19 11:37:22 -07:00
Donal McBreen
56e90906b1 Merge pull request #954 from basecamp/two-app-integration-test
Integration test two apps
2024-09-19 16:36:45 +01:00
Donal McBreen
6e65968bdc Integration test two apps
Use localhost for app_with_roles and 127.0.0.1 for app. Confirm we can
deploy both and the respond to requests. Ensure the proxy is removed
once both have been removed.
2024-09-19 16:25:09 +01:00
Donal McBreen
85f1e14b97 Merge pull request #953 from basecamp/inherit-env-for-secrets
Avoid setting env via SSHKit
2024-09-19 15:18:31 +01:00
Donal McBreen
2c829a4824 Avoid setting env via SSHKit
SSHKit puts the env in the command, so leaks them in process listings.
2024-09-19 15:09:17 +01:00
Donal McBreen
45a58f7e15 Merge pull request #952 from basecamp/app-exec-in-kamal-network
Run app exec in the kamal network
2024-09-19 14:46:58 +01:00
Donal McBreen
834b343ded Run app exec in the kamal network
All other containers run in the kamal network, so let's add app exec-ed
containers as well.
2024-09-19 14:29:33 +01:00
Donal McBreen
9fe1821cae Merge pull request #951 from basecamp/proxy-config-ownership
Fix /home/kamal-proxy/.config/kamal-proxy ownership
2024-09-19 12:57:10 +01:00
Donal McBreen
1d7c9fec1d Fix /home/kamal-proxy/.config/kamal-proxy ownership
1. Update to kamal-proxy 0.4.0 which creates and chowns
/home/kamal-proxy/.config/kamal-proxy to kamal-proxy
2. Use a docker volume rather than mapping in a directory, so docker
keeps it owned by the correct user
2024-09-19 12:25:57 +01:00
45 changed files with 471 additions and 264 deletions

View File

@@ -1,7 +1,7 @@
PATH PATH
remote: . remote: .
specs: specs:
kamal (2.0.0.beta1) kamal (2.0.0.rc2)
activesupport (>= 7.0) activesupport (>= 7.0)
base64 (~> 0.2) base64 (~> 0.2)
bcrypt_pbkdf (~> 1.0) bcrypt_pbkdf (~> 1.0)
@@ -9,6 +9,7 @@ PATH
dotenv (~> 3.1) dotenv (~> 3.1)
ed25519 (~> 1.2) ed25519 (~> 1.2)
net-ssh (~> 7.0) net-ssh (~> 7.0)
net-ssh-gateway
sshkit (>= 1.23.0, < 2.0) sshkit (>= 1.23.0, < 2.0)
thor (~> 1.3) thor (~> 1.3)
zeitwerk (~> 2.5) zeitwerk (~> 2.5)
@@ -79,6 +80,8 @@ GEM
net-sftp (4.0.0) net-sftp (4.0.0)
net-ssh (>= 5.0.0, < 8.0.0) net-ssh (>= 5.0.0, < 8.0.0)
net-ssh (7.2.3) net-ssh (7.2.3)
net-ssh-gateway (2.0.0)
net-ssh (>= 4.0.0)
nokogiri (1.16.7-arm64-darwin) nokogiri (1.16.7-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.7-x86_64-darwin) nokogiri (1.16.7-x86_64-darwin)

View File

@@ -14,6 +14,7 @@ Gem::Specification.new do |spec|
spec.add_dependency "activesupport", ">= 7.0" spec.add_dependency "activesupport", ">= 7.0"
spec.add_dependency "sshkit", ">= 1.23.0", "< 2.0" spec.add_dependency "sshkit", ">= 1.23.0", "< 2.0"
spec.add_dependency "net-ssh", "~> 7.0" spec.add_dependency "net-ssh", "~> 7.0"
spec.add_dependency "net-ssh-gateway"
spec.add_dependency "thor", "~> 1.3" spec.add_dependency "thor", "~> 1.3"
spec.add_dependency "dotenv", "~> 3.1" spec.add_dependency "dotenv", "~> 3.1"
spec.add_dependency "zeitwerk", "~> 2.5" spec.add_dependency "zeitwerk", "~> 2.5"

View File

@@ -147,23 +147,25 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)" option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
option :grep_options, aliases: "-o", desc: "Additional options supplied to grep" option :grep_options, aliases: "-o", desc: "Additional options supplied to grep"
option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)" option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
def logs(name) def logs(name)
with_accessory(name) do |accessory, hosts| with_accessory(name) do |accessory, hosts|
grep = options[:grep] grep = options[:grep]
grep_options = options[:grep_options] grep_options = options[:grep_options]
timestamps = !options[:skip_timestamps]
if options[:follow] if options[:follow]
run_locally do run_locally do
info "Following logs on #{hosts}..." info "Following logs on #{hosts}..."
info accessory.follow_logs(grep: grep, grep_options: grep_options) info accessory.follow_logs(timestamps: timestamps, grep: grep, grep_options: grep_options)
exec accessory.follow_logs(grep: grep, grep_options: grep_options) exec accessory.follow_logs(timestamps: timestamps, grep: grep, grep_options: grep_options)
end end
else else
since = options[:since] since = options[:since]
lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
on(hosts) do on(hosts) do
puts capture_with_info(*accessory.logs(since: since, lines: lines, grep: grep, grep_options: grep_options)) puts capture_with_info(*accessory.logs(timestamps: timestamps, since: since, lines: lines, grep: grep, grep_options: grep_options))
end end
end end
end end
@@ -273,7 +275,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
def prepare(name) def prepare(name)
with_accessory(name) do |accessory, hosts| with_accessory(name) do |accessory, hosts|
on(hosts) do on(hosts) do
execute *KAMAL.registry.login execute *KAMAL.registry.login unless KAMAL.config.registry.local?
execute *KAMAL.docker.create_network execute *KAMAL.docker.create_network
rescue SSHKit::Command::Failed => e rescue SSHKit::Command::Failed => e
raise unless e.message.include?("already exists") raise unless e.message.include?("already exists")

View File

@@ -188,12 +188,14 @@ class Kamal::Cli::App < Kamal::Cli::Base
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)" option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
option :grep_options, aliases: "-o", desc: "Additional options supplied to grep" option :grep_options, aliases: "-o", desc: "Additional options supplied to grep"
option :follow, aliases: "-f", desc: "Follow log on primary server (or specific host set by --hosts)" option :follow, aliases: "-f", desc: "Follow log on primary server (or specific host set by --hosts)"
option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
def logs def logs
# FIXME: Catch when app containers aren't running # FIXME: Catch when app containers aren't running
grep = options[:grep] grep = options[:grep]
grep_options = options[:grep_options] grep_options = options[:grep_options]
since = options[:since] since = options[:since]
timestamps = !options[:skip_timestamps]
if options[:follow] if options[:follow]
lines = options[:lines].presence || ((since || grep) ? nil : 10) # Default to 10 lines if since or grep isn't set lines = options[:lines].presence || ((since || grep) ? nil : 10) # Default to 10 lines if since or grep isn't set
@@ -205,8 +207,8 @@ class Kamal::Cli::App < Kamal::Cli::Base
role = KAMAL.roles_on(KAMAL.primary_host).first role = KAMAL.roles_on(KAMAL.primary_host).first
app = KAMAL.app(role: role, host: host) app = KAMAL.app(role: role, host: host)
info app.follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep, grep_options: grep_options) info app.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options)
exec app.follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep, grep_options: grep_options) exec app.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options)
end end
else else
lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
@@ -216,7 +218,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
roles.each do |role| roles.each do |role|
begin begin
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).logs(since: since, lines: lines, grep: grep, grep_options: grep_options)) puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).logs(timestamps: timestamps, since: since, lines: lines, grep: grep, grep_options: grep_options))
rescue SSHKit::Command::Failed rescue SSHKit::Command::Failed
puts_by_host host, "Nothing found" puts_by_host host, "Nothing found"
end end

View File

@@ -135,8 +135,10 @@ module Kamal::Cli
details = { hosts: KAMAL.hosts.join(","), command: command, subcommand: subcommand } details = { hosts: KAMAL.hosts.join(","), command: command, subcommand: subcommand }
say "Running the #{hook} hook...", :magenta say "Running the #{hook} hook...", :magenta
run_locally do with_env KAMAL.hook.env(**details, **extra_details) do
execute *KAMAL.hook.run(hook, **details, **extra_details) run_locally do
execute *KAMAL.hook.run(hook)
end
rescue SSHKit::Command::Failed => e rescue SSHKit::Command::Failed => e
raise HookError.new("Hook `#{hook}` failed:\n#{e.message}") raise HookError.new("Hook `#{hook}` failed:\n#{e.message}")
end end
@@ -183,5 +185,14 @@ module Kamal::Cli
execute(*KAMAL.server.ensure_run_directory) execute(*KAMAL.server.ensure_run_directory)
end end
end end
def with_env(env)
current_env = ENV.to_h.dup
ENV.update(env)
yield
ensure
ENV.clear
ENV.update(current_env)
end
end end
end end

View File

@@ -1,4 +1,5 @@
require "uri" require "uri"
require "net/ssh"
class Kamal::Cli::Build < Kamal::Cli::Base class Kamal::Cli::Build < Kamal::Cli::Base
class BuildError < StandardError; end class BuildError < StandardError; end
@@ -30,34 +31,38 @@ class Kamal::Cli::Build < Kamal::Cli::Base
say "Building with uncommitted changes:\n #{uncommitted_changes}", :yellow say "Building with uncommitted changes:\n #{uncommitted_changes}", :yellow
end end
run_locally do with_env(KAMAL.config.builder.secrets) do
begin run_locally do
execute *KAMAL.builder.inspect_builder begin
rescue SSHKit::Command::Failed => e execute *KAMAL.builder.inspect_builder
if e.message =~ /(context not found|no builder|no compatible builder|does not exist)/ rescue SSHKit::Command::Failed => e
warn "Missing compatible builder, so creating a new one first" if e.message =~ /(context not found|no builder|no compatible builder|does not exist)/
begin warn "Missing compatible builder, so creating a new one first"
cli.remove begin
rescue SSHKit::Command::Failed cli.remove
raise unless e.message =~ /(context not found|no builder|does not exist)/ rescue SSHKit::Command::Failed
raise unless e.message =~ /(context not found|no builder|does not exist)/
end
cli.create
else
raise
end end
cli.create
else
raise
end end
end
# Get the command here to ensure the Dir.chdir doesn't interfere with it # Get the command here to ensure the Dir.chdir doesn't interfere with it
push = KAMAL.builder.push push = KAMAL.builder.push
KAMAL.with_verbosity(:debug) do KAMAL.with_verbosity(:debug) do
Dir.chdir(KAMAL.config.builder.build_directory) { execute *push, env: KAMAL.config.builder.secrets } Dir.chdir(KAMAL.config.builder.build_directory) { execute *push }
end
end end
end end
end end
desc "pull", "Pull app image from registry onto servers" desc "pull", "Pull app image from registry onto servers"
def pull def pull
tunnels = Kamal::Cli::Tunnel::RemotePorts.new(KAMAL.hosts, KAMAL.config.registry.local_port).tap(&:open) if KAMAL.config.registry.local?
if (first_hosts = mirror_hosts).any? if (first_hosts = mirror_hosts).any?
#  Pull on a single host per mirror first to seed them #  Pull on a single host per mirror first to seed them
say "Pulling image on #{first_hosts.join(", ")} to seed the #{"mirror".pluralize(first_hosts.count)}...", :magenta say "Pulling image on #{first_hosts.join(", ")} to seed the #{"mirror".pluralize(first_hosts.count)}...", :magenta
@@ -67,6 +72,8 @@ class Kamal::Cli::Build < Kamal::Cli::Base
else else
pull_on_hosts(KAMAL.hosts) pull_on_hosts(KAMAL.hosts)
end end
ensure
tunnels&.close
end end
desc "create", "Create a build setup" desc "create", "Create a build setup"
@@ -150,7 +157,7 @@ class Kamal::Cli::Build < Kamal::Cli::Base
end end
def pull_on_hosts(hosts) def pull_on_hosts(hosts)
on(hosts) do on(hosts) do |host|
execute *KAMAL.auditor.record("Pulled image with version #{KAMAL.config.version}"), verbosity: :debug 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.clean, raise_on_non_zero_exit: false
execute *KAMAL.builder.pull execute *KAMAL.builder.pull

View File

@@ -22,7 +22,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
invoke_options = deploy_options invoke_options = deploy_options
say "Log into image registry...", :magenta say "Log into image registry...", :magenta
invoke "kamal:cli:registry:login", [], invoke_options.merge(skip_local: options[:skip_push]) invoke "kamal:cli:registry:setup", [], invoke_options.merge(skip_local: options[:skip_push])
if options[:skip_push] if options[:skip_push]
say "Pull app image...", :magenta say "Pull app image...", :magenta
@@ -48,7 +48,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
end end
end end
run_hook "post-deploy", secrets: true, runtime: runtime.round run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s
end end
desc "redeploy", "Deploy app to servers without bootstrapping servers, starting kamal-proxy, pruning, and registry login" desc "redeploy", "Deploy app to servers without bootstrapping servers, starting kamal-proxy, pruning, and registry login"
@@ -75,7 +75,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
end end
end end
run_hook "post-deploy", secrets: true, runtime: runtime.round run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s
end end
desc "rollback [VERSION]", "Rollback app to VERSION" desc "rollback [VERSION]", "Rollback app to VERSION"
@@ -99,7 +99,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
end end
end end
run_hook "post-deploy", secrets: true, runtime: runtime.round if rolled_back run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s if rolled_back
end end
desc "details", "Show details about all containers" desc "details", "Show details about all containers"
@@ -184,7 +184,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
invoke "kamal:cli:app:remove", [], options.without(:confirmed) invoke "kamal:cli:app:remove", [], options.without(:confirmed)
invoke "kamal:cli:proxy:remove", [], options.without(:confirmed) invoke "kamal:cli:proxy:remove", [], options.without(:confirmed)
invoke "kamal:cli:accessory:remove", [ "all" ], options invoke "kamal:cli:accessory:remove", [ "all" ], options
invoke "kamal:cli:registry:logout", [], options.without(:confirmed).merge(skip_local: true) invoke "kamal:cli:registry:remove", [], options.without(:confirmed).merge(skip_local: true)
end end
end end
end end

View File

@@ -9,7 +9,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
end end
on(KAMAL.proxy_hosts) do |host| on(KAMAL.proxy_hosts) do |host|
execute *KAMAL.registry.login execute *KAMAL.registry.login unless KAMAL.config.registry.local?
version = capture_with_info(*KAMAL.proxy.version).strip.presence version = capture_with_info(*KAMAL.proxy.version).strip.presence
@@ -33,7 +33,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
run_hook "pre-proxy-reboot", hosts: host_list run_hook "pre-proxy-reboot", hosts: host_list
on(hosts) do |host| on(hosts) do |host|
execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug
execute *KAMAL.registry.login execute *KAMAL.registry.login unless KAMAL.config.registry.local?
"Stopping and removing Traefik on #{host}, if running..." "Stopping and removing Traefik on #{host}, if running..."
execute *KAMAL.proxy.cleanup_traefik execute *KAMAL.proxy.cleanup_traefik
@@ -76,7 +76,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
run_hook "pre-proxy-reboot", hosts: host_list run_hook "pre-proxy-reboot", hosts: host_list
on(hosts) do |host| on(hosts) do |host|
execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug
execute *KAMAL.registry.login execute *KAMAL.registry.login unless KAMAL.config.registry.local?
"Stopping and removing Traefik on #{host}, if running..." "Stopping and removing Traefik on #{host}, if running..."
execute *KAMAL.proxy.cleanup_traefik execute *KAMAL.proxy.cleanup_traefik
@@ -140,21 +140,23 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server" option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)" option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)" option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
def logs def logs
grep = options[:grep] grep = options[:grep]
timestamps = !options[:skip_timestamps]
if options[:follow] if options[:follow]
run_locally do run_locally do
info "Following logs on #{KAMAL.primary_host}..." info "Following logs on #{KAMAL.primary_host}..."
info KAMAL.proxy.follow_logs(host: KAMAL.primary_host, grep: grep) info KAMAL.proxy.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, grep: grep)
exec KAMAL.proxy.follow_logs(host: KAMAL.primary_host, grep: grep) exec KAMAL.proxy.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, grep: grep)
end end
else else
since = options[:since] since = options[:since]
lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
on(KAMAL.proxy_hosts) do |host| on(KAMAL.proxy_hosts) do |host|
puts_by_host host, capture(*KAMAL.proxy.logs(since: since, lines: lines, grep: grep)), type: "Proxy" puts_by_host host, capture(*KAMAL.proxy.logs(timestamps: timestamps, since: since, lines: lines, grep: grep)), type: "Proxy"
end end
end end
end end
@@ -167,7 +169,6 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
stop stop
remove_container remove_container
remove_image remove_image
remove_host_directory
end end
end end
end end
@@ -192,16 +193,6 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
end end
end end
desc "remove_host_directory", "Remove proxy directory from servers", hide: true
def remove_host_directory
with_lock do
on(KAMAL.proxy_hosts) do
execute *KAMAL.auditor.record("Removed #{KAMAL.config.proxy_directory}"), verbosity: :debug
execute *KAMAL.proxy.remove_host_directory, raise_on_non_zero_exit: false
end
end
end
private private
def removal_allowed?(force) def removal_allowed?(force)
on(KAMAL.proxy_hosts) do |host| on(KAMAL.proxy_hosts) do |host|

View File

@@ -1,17 +1,25 @@
class Kamal::Cli::Registry < Kamal::Cli::Base class Kamal::Cli::Registry < Kamal::Cli::Base
desc "login", "Log in to registry locally and remotely" desc "login", "Setup local registry or log in to remote registry locally and remotely"
option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login" option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login" option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
def login def setup
run_locally { execute *KAMAL.registry.login } unless options[:skip_local] if KAMAL.registry.local?
on(KAMAL.hosts) { execute *KAMAL.registry.login } unless options[:skip_remote] run_locally { execute *KAMAL.registry.setup } unless options[:skip_local]
else
run_locally { execute *KAMAL.registry.login } unless options[:skip_local]
on(KAMAL.hosts) { execute *KAMAL.registry.login } unless options[:skip_remote]
end
end end
desc "logout", "Log out of registry locally and remotely" desc "remove", "Remove local registry or log out of remote registry locally and remotely"
option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login" option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login" option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
def logout def remove
run_locally { execute *KAMAL.registry.logout } unless options[:skip_local] if KAMAL.registry.local?
on(KAMAL.hosts) { execute *KAMAL.registry.logout } unless options[:skip_remote] run_locally { execute *KAMAL.registry.remove, raise_on_non_zero_exit: false } unless options[:skip_local]
else
run_locally { execute *KAMAL.registry.logout } unless options[:skip_local]
on(KAMAL.hosts) { execute *KAMAL.registry.logout } unless options[:skip_remote]
end
end end
end end

View File

@@ -0,0 +1,66 @@
Signal.trap "SIGPROF" do
Thread.list.each do |thread|
puts thread.name
puts thread.backtrace.map { |bt| " #{bt}" }
puts
end
end
require "concurrent/map"
class Kamal::Cli::Tunnel::RemotePorts
attr_reader :hosts, :port
def initialize(hosts, port)
@hosts = hosts
@port = port
@open = false
end
def open
@open = true
@opened = Concurrent::Map.new
@threads = hosts.map do |host|
Thread.new do
Net::SSH.start(host, KAMAL.config.ssh.user) do |ssh|
forwarding = nil
ssh.forward.remote(port, "localhost", port, "localhost") do |actual_remote_port|
forwarding = !!actual_remote_port
:no_exception # will yield the exception on my own thread
end
ssh.loop { forwarding.nil? }
if forwarding
@opened[host] = true
ssh.loop(0.1) { @open }
ssh.forward.cancel_remote(port, "localhost")
ssh.loop(0.1) { ssh.forward.active_remotes.include?([ port, "localhost" ]) }
else
@opened[host] = false
end
end
rescue => e
@opened[host] = false
puts e.message
puts e.backtrace
end
end
loop do
break if @opened.size == hosts.size
sleep 0.1
end
raise "Could not open tunnels on #{opened.reject { |k, v| v }.join(", ")}" unless @opened.values.all?
end
def close
p "Closing"
@open = false
p "Joining"
@threads.each(&:join)
p "Joined"
end
end

View File

@@ -39,16 +39,16 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
end end
def logs(since: nil, lines: nil, grep: nil, grep_options: nil) def logs(timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
pipe \ pipe \
docker(:logs, service_name, (" --since #{since}" if since), (" --tail #{lines}" if lines), "--timestamps", "2>&1"), docker(:logs, service_name, (" --since #{since}" if since), (" --tail #{lines}" if lines), ("--timestamps" if timestamps), "2>&1"),
("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep) ("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep)
end end
def follow_logs(grep: nil, grep_options: nil) def follow_logs(timestamps: true, grep: nil, grep_options: nil)
run_over_ssh \ run_over_ssh \
pipe \ pipe \
docker(:logs, service_name, "--timestamps", "--tail", "10", "--follow", "2>&1"), docker(:logs, service_name, ("--timestamps" if timestamps), "--tail", "10", "--follow", "2>&1"),
(%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep) (%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep)
end end

View File

@@ -11,6 +11,7 @@ module Kamal::Commands::App::Execution
docker :run, docker :run,
("-it" if interactive), ("-it" if interactive),
"--rm", "--rm",
"--network", "kamal",
*role&.env_args(host), *role&.env_args(host),
*argumentize("--env", env), *argumentize("--env", env),
*config.volume_args, *config.volume_args,

View File

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

View File

@@ -1,9 +1,12 @@
class Kamal::Commands::Hook < Kamal::Commands::Base class Kamal::Commands::Hook < Kamal::Commands::Base
def run(hook, secrets: false, **details) def run(hook)
env = tags(**details).env [ hook_file(hook) ]
env.merge!(config.secrets.to_h) if secrets end
[ hook_file(hook), env: env ] def env(secrets: false, **details)
tags(**details).env.tap do |env|
env.merge!(config.secrets.to_h) if secrets
end
end end
def hook_exists?(hook) def hook_exists?(hook)

View File

@@ -8,8 +8,7 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
"--detach", "--detach",
"--restart", "unless-stopped", "--restart", "unless-stopped",
*config.proxy_publish_args, *config.proxy_publish_args,
"--volume", "/var/run/docker.sock:/var/run/docker.sock", "--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy",
*config.proxy_config_volume.docker_args,
*config.logging_args, *config.logging_args,
config.proxy_image config.proxy_image
end end
@@ -36,15 +35,15 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
[ :cut, "-d:", "-f2" ] [ :cut, "-d:", "-f2" ]
end end
def logs(since: nil, lines: nil, grep: nil, grep_options: nil) def logs(timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
pipe \ pipe \
docker(:logs, container_name, (" --since #{since}" if since), (" --tail #{lines}" if lines), "--timestamps", "2>&1"), docker(:logs, container_name, ("--since #{since}" if since), ("--tail #{lines}" if lines), ("--timestamps" if timestamps), "2>&1"),
("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep) ("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep)
end end
def follow_logs(host:, grep: nil, grep_options: nil) def follow_logs(host:, timestamps: true, grep: nil, grep_options: nil)
run_over_ssh pipe( run_over_ssh pipe(
docker(:logs, container_name, "--timestamps", "--tail", "10", "--follow", "2>&1"), docker(:logs, container_name, ("--timestamps" if timestamps), "--tail", "10", "--follow", "2>&1"),
(%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep) (%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep)
).join(" "), host: host ).join(" "), host: host
end end
@@ -57,10 +56,6 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=kamal-proxy" docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=kamal-proxy"
end end
def remove_host_directory
remove_directory config.proxy_directory
end
def cleanup_traefik def cleanup_traefik
chain \ chain \
docker(:container, :stop, "traefik"), docker(:container, :stop, "traefik"),

View File

@@ -1,5 +1,6 @@
class Kamal::Commands::Registry < Kamal::Commands::Base class Kamal::Commands::Registry < Kamal::Commands::Base
delegate :registry, to: :config delegate :registry, to: :config
delegate :local?, :local_port, to: :registry
def login def login
docker :login, docker :login,
@@ -11,4 +12,26 @@ class Kamal::Commands::Registry < Kamal::Commands::Base
def logout def logout
docker :logout, registry.server docker :logout, registry.server
end end
def setup
combine \
docker(:start, "kamal-docker-registry"),
docker(:run, "--detach", "-p", "127.0.0.1:#{local_port}:5000", "--name", "kamal-docker-registry", "registry:2"),
by: "||"
end
def remove
combine \
docker(:stop, "kamal-docker-registry"),
docker(:rm, "kamal-docker-registry"),
by: "&&"
end
def logout
docker :logout, registry.server
end
def tunnel(host)
run_over_ssh "-R", "#{local_port}:localhost:#{local_port}", host: host
end
end end

View File

@@ -14,7 +14,7 @@ class Kamal::Configuration
include Validation include Validation
PROXY_MINIMUM_VERSION = "v0.3.0" PROXY_MINIMUM_VERSION = "v0.4.0"
PROXY_HTTP_PORT = 80 PROXY_HTTP_PORT = 80
PROXY_HTTPS_PORT = 443 PROXY_HTTPS_PORT = 443
@@ -216,10 +216,6 @@ class Kamal::Configuration
File.join apps_directory, [ service, destination ].compact.join("-") File.join apps_directory, [ service, destination ].compact.join("-")
end end
def proxy_directory
File.join run_directory, "proxy"
end
def env_directory def env_directory
File.join app_directory, "env" File.join app_directory, "env"
end end
@@ -262,12 +258,6 @@ class Kamal::Configuration
"kamal-proxy" "kamal-proxy"
end end
def proxy_config_volume
Kamal::Configuration::Volume.new \
host_path: File.join(proxy_directory, "config"),
container_path: "/home/kamal-proxy/.config/kamal-proxy"
end
def to_h def to_h
{ {

View File

@@ -2,10 +2,6 @@
# #
# The builder configuration controls how the application is built with `docker build` # The builder configuration controls how the application is built with `docker build`
# #
# If no configuration is specified, Kamal will:
# 1. Create a buildx context called `kamal-local-docker-container`, using the docker-container driver
# 2. Use `docker build` to build a multiarch image for linux/amd64,linux/arm64 with that context
#
# See https://kamal-deploy.org/docs/configuration/builder-examples/ for more information # See https://kamal-deploy.org/docs/configuration/builder-examples/ for more information
# Builder options # Builder options
@@ -78,7 +74,7 @@ builder:
# Build secrets # Build secrets
# #
# Values are read from the .kamal/secrets. # Values are read from .kamal/secrets.
# #
secrets: secrets:
- SECRET1 - SECRET1

View File

@@ -36,6 +36,8 @@ image: my-image
labels: labels:
my-label: my-value my-label: my-value
# Volumes
#
# Additional volumes to mount into the container # Additional volumes to mount into the container
volumes: volumes:
- /path/on/host:/path/in/container:ro - /path/on/host:/path/in/container:ro
@@ -58,7 +60,7 @@ servers:
env: env:
... ...
# Asset Bridging # Asset Path
# #
# Used for asset bridging across deployments, default to `nil` # Used for asset bridging across deployments, default to `nil`
# #
@@ -74,6 +76,8 @@ env:
# To configure this, set the path to the assets: # To configure this, set the path to the assets:
asset_path: /path/to/assets asset_path: /path/to/assets
# Hooks path
#
# Path to hooks, defaults to `.kamal/hooks` # Path to hooks, defaults to `.kamal/hooks`
# See https://kamal-deploy.org/docs/hooks for more information # See https://kamal-deploy.org/docs/hooks for more information
hooks_path: /user_home/kamal/hooks hooks_path: /user_home/kamal/hooks
@@ -83,7 +87,7 @@ hooks_path: /user_home/kamal/hooks
# Whether deployments require a destination to be specified, defaults to `false` # Whether deployments require a destination to be specified, defaults to `false`
require_destination: true require_destination: true
# The primary role # Primary role
# #
# This defaults to `web`, but if you have no web role, you can change this # This defaults to `web`, but if you have no web role, you can change this
primary_role: workers primary_role: workers

View File

@@ -12,11 +12,16 @@ env:
DATABASE_HOST: mysql-db1 DATABASE_HOST: mysql-db1
DATABASE_PORT: 3306 DATABASE_PORT: 3306
# Using .kamal/secrets file to load required environment variables # Secrets
# #
# Kamal uses dotenv to automatically load environment variables set in the .kamal/secrets file. # Kamal uses dotenv to automatically load environment variables set in the `.kamal/secrets` file.
# #
# This file can be used to set variables like KAMAL_REGISTRY_PASSWORD or database passwords. # If you are using destinations, secrets will instead be read from `.kamal/secrets-<DESTINATION>` if
# it exists.
#
# Common secrets across all destinations can be set in `.kamal/secrets-common`.
#
# This file can be used to set variables like `KAMAL_REGISTRY_PASSWORD` or database passwords.
# You can use variable or command substitution in the secrets file. # You can use variable or command substitution in the secrets file.
# #
# ``` # ```
@@ -24,6 +29,14 @@ env:
# RAILS_MASTER_KEY=$(cat config/master.key) # RAILS_MASTER_KEY=$(cat config/master.key)
# ``` # ```
# #
# You can also use [secret helpers](../commands/secrets) for some common password managers.
# ```
# SECRETS=$(kamal secrets fetch ...)
#
# REGISTRY_PASSWORD=$(kamal secrets extract REGISTRY_PASSWORD $SECRETS)
# DB_PASSWORD=$(kamal secrets extract DB_PASSWORD $SECRETS)
# ```
#
# If you store secrets directly in .kamal/secrets, ensure that it is not checked into version control. # If you store secrets directly in .kamal/secrets, ensure that it is not checked into version control.
# #
# To pass the secrets you should list them under the `secret` key. When you do this the # To pass the secrets you should list them under the `secret` key. When you do this the

View File

@@ -47,7 +47,7 @@ proxy:
# Response timeout # Response timeout
# #
# How long to wait for requests to complete before timing out, defaults to 30 seconds # How long to wait for requests to complete before timing out, defaults to 30 seconds
response_timeout: 10s response_timeout: 10
# Healthcheck # Healthcheck
# #
@@ -91,7 +91,7 @@ proxy:
# Forward headers # Forward headers
# #
# Whether to forward the X-Forwarded-For and X-Forwarded-Proto headers (defaults to false) # Whether to forward the X-Forwarded-For and X-Forwarded-Proto headers.
# #
# If you are behind a trusted proxy, you can set this to true to forward the headers. # If you are behind a trusted proxy, you can set this to true to forward the headers.
# #

View File

@@ -26,8 +26,12 @@ servers:
# #
# When there are other options to set, the list of hosts goes under the `hosts` key # When there are other options to set, the list of hosts goes under the `hosts` key
# #
# By default only the primary role uses a proxy, but you can set `proxy` to change # By default only the primary role uses a proxy.
# it. #
# For other roles, you can set it to `proxy: true` enable it and inherit the root proxy
# configuration or provide a map of options to override the root configuration.
#
# For the primary role, you can set `proxy: false` to disable the proxy.
# #
# You can also set a custom cmd to run in the container, and overwrite other settings # You can also set a custom cmd to run in the container, and overwrite other settings
# from the root configuration. # from the root configuration.

View File

@@ -21,6 +21,14 @@ class Kamal::Configuration::Registry
lookup("password") lookup("password")
end end
def local?
server&.match?("^localhost[:$]")
end
def local_port
local? ? (server.split(":").last.to_i || 80) : nil
end
private private
def lookup(key) def lookup(key)
if registry_config[key].is_a?(Array) if registry_config[key].is_a?(Array)

View File

@@ -15,10 +15,12 @@ class Kamal::Configuration::Validator::Registry < Kamal::Configuration::Validato
with_context(key) do with_context(key) do
value = config[key] value = config[key]
error "is required" unless value.present? unless config["server"]&.match?("^localhost[:$]")
error "is required" unless value.present?
unless value.is_a?(String) || (value.is_a?(Array) && value.size == 1 && value.first.is_a?(String)) unless value.is_a?(String) || (value.is_a?(Array) && value.size == 1 && value.first.is_a?(String))
error "should be a string or an array with one string (for secret lookup)" error "should be a string or an array with one string (for secret lookup)"
end
end end
end end
end end

View File

@@ -1,3 +1,3 @@
module Kamal module Kamal
VERSION = "2.0.0.beta1" VERSION = "2.0.0.rc2"
end end

View File

@@ -263,13 +263,13 @@ class CliAppTest < CliTestCase
test "exec" do test "exec" do
run_command("exec", "ruby -v").tap do |output| run_command("exec", "ruby -v").tap do |output|
assert_match "docker run --rm --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v", output assert_match "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v", output
end end
end end
test "exec separate arguments" do test "exec separate arguments" do
run_command("exec", "ruby", " -v").tap do |output| run_command("exec", "ruby", " -v").tap do |output|
assert_match "docker run --rm --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v", output assert_match "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v", output
end end
end end
@@ -282,7 +282,7 @@ class CliAppTest < CliTestCase
test "exec interactive" do test "exec interactive" do
SSHKit::Backend::Abstract.any_instance.expects(:exec) SSHKit::Backend::Abstract.any_instance.expects(:exec)
.with("ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v'") .with("ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v'")
run_command("exec", "-i", "ruby -v").tap do |output| run_command("exec", "-i", "ruby -v").tap do |output|
assert_match "Get most recent version available as an image...", output assert_match "Get most recent version available as an image...", output
assert_match "Launching interactive command with version latest via SSH from new container on 1.1.1.1...", output assert_match "Launching interactive command with version latest via SSH from new container on 1.1.1.1...", output
@@ -315,11 +315,11 @@ class CliAppTest < CliTestCase
SSHKit::Backend::Abstract.any_instance.stubs(:exec) SSHKit::Backend::Abstract.any_instance.stubs(:exec)
.with("ssh -t root@1.1.1.1 'sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1| xargs docker logs --timestamps --tail 10 2>&1'") .with("ssh -t root@1.1.1.1 'sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1| xargs docker logs --timestamps --tail 10 2>&1'")
assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --tail 100 2>&1", run_command("logs") assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --tail 100 2>&1", run_command("logs")
assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs 2>&1 | grep 'hey'", run_command("logs", "--grep", "hey") assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'hey'", run_command("logs", "--grep", "hey")
assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs 2>&1 | grep 'hey' -C 2", run_command("logs", "--grep", "hey", "--grep-options", "-C 2") assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'hey' -C 2", run_command("logs", "--grep", "hey", "--grep-options", "-C 2")
end end
test "logs with follow" do test "logs with follow" do

View File

@@ -49,7 +49,7 @@ class CliBuildTest < CliTestCase
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :submodule, :update, "--init") SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :submodule, :update, "--init")
SSHKit::Backend::Abstract.any_instance.expects(:execute) SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".", env: {}) .with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".")
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:git, "-C", anything, :"rev-parse", :HEAD) .with(:git, "-C", anything, :"rev-parse", :HEAD)
@@ -140,7 +140,7 @@ class CliBuildTest < CliTestCase
.returns("") .returns("")
SSHKit::Backend::Abstract.any_instance.expects(:execute) SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".", env: {}) .with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".")
run_command("push").tap do |output| run_command("push").tap do |output|
assert_match /WARN Missing compatible builder, so creating a new one first/, output assert_match /WARN Missing compatible builder, so creating a new one first/, output

View File

@@ -41,27 +41,7 @@ class CliTestCase < ActiveSupport::TestCase
end end
def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: false, secrets: false) def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: false, secrets: false)
whoami = `whoami`.chomp assert_match %r{usr/bin/env\s\.kamal/hooks/#{hook}}, output
performer = Kamal::Git.email.presence || whoami
service = service_version.split("@").first
assert_match "Running the #{hook} hook...\n", output
expected = %r{Running\s/usr/bin/env\s\.kamal/hooks/#{hook}\sas\s#{whoami}@localhost\n\s
DEBUG\s\[[0-9a-f]*\]\sCommand:\s\(\sexport\s
KAMAL_RECORDED_AT=\"\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ\"\s
KAMAL_PERFORMER=\"#{performer}\"\s
KAMAL_VERSION=\"#{version}\"\s
KAMAL_SERVICE_VERSION=\"#{service_version}\"\s
KAMAL_SERVICE=\"#{service}\"\s
KAMAL_HOSTS=\"#{hosts}\"\s
KAMAL_COMMAND=\"#{command}\"\s
#{"KAMAL_SUBCOMMAND=\\\"#{subcommand}\\\"\\s" if subcommand}
#{"KAMAL_RUNTIME=\\\"\\d+\\\"\\s" if runtime}
#{"DB_PASSWORD=\"secret\"\\s" if secrets}
;\s/usr/bin/env\s\.kamal/hooks/#{hook} }x
assert_match expected, output
end end
def with_argv(*argv) def with_argv(*argv)

View File

@@ -22,7 +22,7 @@ class CliMainTest < CliTestCase
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:server:bootstrap", [], invoke_options) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:server:bootstrap", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:boot", [ "all" ], invoke_options) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:boot", [ "all" ], invoke_options)
# deploy # deploy
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options.merge(skip_local: true)) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:setup", [], invoke_options.merge(skip_local: true))
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:pull", [], invoke_options) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:pull", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], invoke_options) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], 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:stale_containers", [], invoke_options.merge(stop: true))
@@ -46,7 +46,7 @@ class CliMainTest < CliTestCase
with_test_secrets("secrets" => "DB_PASSWORD=secret") do with_test_secrets("secrets" => "DB_PASSWORD=secret") do
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false, "verbose" => true } invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false, "verbose" => true }
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options.merge(skip_local: false)) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:setup", [], invoke_options.merge(skip_local: 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:build:deliver", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], invoke_options) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], 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:stale_containers", [], invoke_options.merge(stop: true))
@@ -72,7 +72,7 @@ class CliMainTest < CliTestCase
test "deploy with skip_push" do test "deploy with skip_push" do
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false } invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options.merge(skip_local: true)) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:setup", [], invoke_options.merge(skip_local: true))
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:pull", [], invoke_options) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:pull", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], invoke_options) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], 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:stale_containers", [], invoke_options.merge(stop: true))
@@ -159,7 +159,7 @@ class CliMainTest < CliTestCase
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false, :skip_local => false } invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false, :skip_local => false }
Kamal::Cli::Main.any_instance.expects(:invoke) Kamal::Cli::Main.any_instance.expects(:invoke)
.with("kamal:cli:registry:login", [], invoke_options.merge(skip_local: false)) .with("kamal:cli:registry:setup", [], invoke_options.merge(skip_local: false))
.raises(RuntimeError) .raises(RuntimeError)
assert_not KAMAL.holding_lock? assert_not KAMAL.holding_lock?
@@ -172,7 +172,7 @@ class CliMainTest < CliTestCase
test "deploy with skipped hooks" do test "deploy with skipped hooks" do
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => true } invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => true }
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options.merge(skip_local: false)) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:setup", [], invoke_options.merge(skip_local: 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:build:deliver", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], invoke_options) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], 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:stale_containers", [], invoke_options.merge(stop: true))
@@ -187,7 +187,7 @@ class CliMainTest < CliTestCase
test "deploy with missing secrets" do test "deploy with missing secrets" do
invoke_options = { "config_file" => "test/fixtures/deploy_with_secrets.yml", "version" => "999", "skip_hooks" => false } 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.merge(skip_local: false)) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:setup", [], invoke_options.merge(skip_local: 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:build:deliver", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], invoke_options) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], 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:stale_containers", [], invoke_options.merge(stop: true))
@@ -289,6 +289,16 @@ class CliMainTest < CliTestCase
end end
end end
test "remove" do
options = { "config_file" => "test/fixtures/deploy_simple.yml", "skip_hooks" => false, "confirmed" => true }
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:remove", [], options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:remove", [], options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:remove", [ "all" ], options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:remove", [], options.merge(skip_local: true))
run_command("remove", "-y")
end
test "details" do test "details" do
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:details") Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:details")
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:details") Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:details")

View File

@@ -4,7 +4,7 @@ class CliProxyTest < CliTestCase
test "boot" do test "boot" do
run_command("boot").tap do |output| run_command("boot").tap do |output|
assert_match "docker login", output assert_match "docker login", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}", output assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}", output
end end
end end
@@ -18,7 +18,7 @@ class CliProxyTest < CliTestCase
exception = assert_raises do exception = assert_raises do
run_command("boot").tap do |output| run_command("boot").tap do |output|
assert_match "docker login", output assert_match "docker login", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}", output assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}", output
end end
end end
@@ -36,7 +36,7 @@ class CliProxyTest < CliTestCase
run_command("boot").tap do |output| run_command("boot").tap do |output|
assert_match "docker login", output assert_match "docker login", output
assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}", output assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}", output
end end
ensure ensure
Thread.report_on_exception = false Thread.report_on_exception = false
@@ -57,13 +57,13 @@ class CliProxyTest < CliTestCase
assert_match "docker container stop kamal-proxy on 1.1.1.1", output assert_match "docker container stop kamal-proxy on 1.1.1.1", output
assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image} on 1.1.1.1", output assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image} on 1.1.1.1", output
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\" on 1.1.1.1", output assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\" on 1.1.1.1", output
assert_match "docker container stop kamal-proxy on 1.1.1.2", output assert_match "docker container stop kamal-proxy on 1.1.1.2", output
assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.2", output assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.2", output
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.2", output assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.2", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image} on 1.1.1.2", output assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image} on 1.1.1.2", output
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\" on 1.1.1.2", output assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\" on 1.1.1.2", output
end end
end end
@@ -111,11 +111,11 @@ class CliProxyTest < CliTestCase
test "logs" do test "logs" do
SSHKit::Backend::Abstract.any_instance.stubs(:capture) SSHKit::Backend::Abstract.any_instance.stubs(:capture)
.with(:docker, :logs, "kamal-proxy", " --tail 100", "--timestamps", "2>&1") .with(:docker, :logs, "kamal-proxy", "--tail 100", "--timestamps", "2>&1")
.returns("Log entry") .returns("Log entry")
SSHKit::Backend::Abstract.any_instance.stubs(:capture) SSHKit::Backend::Abstract.any_instance.stubs(:capture)
.with(:docker, :logs, "proxy", " --tail 100", "--timestamps", "2>&1") .with(:docker, :logs, "proxy", "--tail 100", "--timestamps", "2>&1")
.returns("Log entry") .returns("Log entry")
run_command("logs").tap do |output| run_command("logs").tap do |output|
@@ -136,7 +136,6 @@ class CliProxyTest < CliTestCase
assert_match "/usr/bin/env ls .kamal/apps | wc -l", output assert_match "/usr/bin/env ls .kamal/apps | wc -l", output
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy", output assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy", output
assert_match "docker image prune --all --force --filter label=org.opencontainers.image.title=kamal-proxy", output assert_match "docker image prune --all --force --filter label=org.opencontainers.image.title=kamal-proxy", output
assert_match "/usr/bin/env rm -r .kamal/proxy", output
end end
end end
@@ -176,12 +175,6 @@ class CliProxyTest < CliTestCase
end end
end end
test "remove_host_directory" do
run_command("remove_host_directory").tap do |output|
assert_match "/usr/bin/env rm -r .kamal/proxy", output
end
end
test "upgrade" do test "upgrade" do
Object.any_instance.stubs(:sleep) Object.any_instance.stubs(:sleep)
@@ -205,7 +198,7 @@ class CliProxyTest < CliTestCase
assert_match "/usr/bin/env mkdir -p .kamal", output assert_match "/usr/bin/env mkdir -p .kamal", output
assert_match "docker network create kamal", output assert_match "docker network create kamal", output
assert_match "docker login -u [REDACTED] -p [REDACTED]", output assert_match "docker login -u [REDACTED] -p [REDACTED]", output
assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", output assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", output
assert_match "/usr/bin/env mkdir -p .kamal", output assert_match "/usr/bin/env mkdir -p .kamal", output
assert_match %r{docker rename app-web-latest app-web-latest_replaced_.*}, output assert_match %r{docker rename app-web-latest app-web-latest_replaced_.*}, output
assert_match "/usr/bin/env mkdir -p .kamal/apps/app/env/roles", output assert_match "/usr/bin/env mkdir -p .kamal/apps/app/env/roles", output

View File

@@ -1,50 +1,62 @@
require_relative "cli_test_case" require_relative "cli_test_case"
class CliRegistryTest < CliTestCase class CliRegistryTest < CliTestCase
test "login" do test "setup" do
run_command("login").tap do |output| run_command("setup").tap do |output|
assert_match /docker login -u \[REDACTED\] -p \[REDACTED\] as .*@localhost/, output assert_match /docker login -u \[REDACTED\] -p \[REDACTED\] as .*@localhost/, output
assert_match /docker login -u \[REDACTED\] -p \[REDACTED\] on 1.1.1.\d/, output assert_match /docker login -u \[REDACTED\] -p \[REDACTED\] on 1.1.1.\d/, output
end end
end end
test "login skip local" do test "setup skip local" do
run_command("login", "-L").tap do |output| run_command("setup", "-L").tap do |output|
assert_no_match /docker login -u \[REDACTED\] -p \[REDACTED\] as .*@localhost/, output assert_no_match /docker login -u \[REDACTED\] -p \[REDACTED\] as .*@localhost/, output
assert_match /docker login -u \[REDACTED\] -p \[REDACTED\] on 1.1.1.\d/, output assert_match /docker login -u \[REDACTED\] -p \[REDACTED\] on 1.1.1.\d/, output
end end
end end
test "login skip remote" do test "setup skip remote" do
run_command("login", "-R").tap do |output| run_command("setup", "-R").tap do |output|
assert_match /docker login -u \[REDACTED\] -p \[REDACTED\] as .*@localhost/, output assert_match /docker login -u \[REDACTED\] -p \[REDACTED\] as .*@localhost/, output
assert_no_match /docker login -u \[REDACTED\] -p \[REDACTED\] on 1.1.1.\d/, output assert_no_match /docker login -u \[REDACTED\] -p \[REDACTED\] on 1.1.1.\d/, output
end end
end end
test "logout" do test "remove" do
run_command("logout").tap do |output| run_command("remove").tap do |output|
assert_match /docker logout as .*@localhost/, output assert_match /docker logout as .*@localhost/, output
assert_match /docker logout on 1.1.1.\d/, output assert_match /docker logout on 1.1.1.\d/, output
end end
end end
test "logout skip local" do test "remove skip local" do
run_command("logout", "-L").tap do |output| run_command("remove", "-L").tap do |output|
assert_no_match /docker logout as .*@localhost/, output assert_no_match /docker logout as .*@localhost/, output
assert_match /docker logout on 1.1.1.\d/, output assert_match /docker logout on 1.1.1.\d/, output
end end
end end
test "logout skip remote" do test "remove skip remote" do
run_command("logout", "-R").tap do |output| run_command("remove", "-R").tap do |output|
assert_match /docker logout as .*@localhost/, output assert_match /docker logout as .*@localhost/, output
assert_no_match /docker logout on 1.1.1.\d/, output assert_no_match /docker logout on 1.1.1.\d/, output
end end
end end
test "setup local registry" do
run_command("setup", fixture: :with_local_registry).tap do |output|
assert_match /docker start kamal-docker-registry || docker run --detach -p 127.0.0.1:5000:5000 --name kamal-docker-registry registry:2 as .*@localhost/, output
end
end
test "remove local registry" do
run_command("remove", fixture: :with_local_registry).tap do |output|
assert_match /docker stop kamal-docker-registry && docker rm kamal-docker-registry as .*@localhost/, output
end
end
private private
def run_command(*command) def run_command(*command, fixture: :with_accessories)
stdouted { Kamal::Cli::Registry.start([ *command, "-c", "test/fixtures/deploy_with_accessories.yml" ]) } stdouted { Kamal::Cli::Registry.start([ *command, "-c", "test/fixtures/deploy_#{fixture}.yml" ]) }
end end
end end

View File

@@ -130,12 +130,20 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
assert_equal \ assert_equal \
"docker logs app-mysql --since 5m --tail 100 --timestamps 2>&1 | grep 'thing' -C 2", "docker logs app-mysql --since 5m --tail 100 --timestamps 2>&1 | grep 'thing' -C 2",
new_command(:mysql).logs(since: "5m", lines: 100, grep: "thing", grep_options: "-C 2").join(" ") new_command(:mysql).logs(since: "5m", lines: 100, grep: "thing", grep_options: "-C 2").join(" ")
assert_equal \
"docker logs app-mysql --since 5m --tail 100 2>&1 | grep 'thing' -C 2",
new_command(:mysql).logs(timestamps: false, since: "5m", lines: 100, grep: "thing", grep_options: "-C 2").join(" ")
end end
test "follow logs" do test "follow logs" do
assert_equal \ assert_equal \
"ssh -t root@1.1.1.5 -p 22 'docker logs app-mysql --timestamps --tail 10 --follow 2>&1'", "ssh -t root@1.1.1.5 -p 22 'docker logs app-mysql --timestamps --tail 10 --follow 2>&1'",
new_command(:mysql).follow_logs new_command(:mysql).follow_logs
assert_equal \
"ssh -t root@1.1.1.5 -p 22 'docker logs app-mysql --tail 10 --follow 2>&1'",
new_command(:mysql).follow_logs(timestamps: false)
end end
test "remove container" do test "remove container" do

View File

@@ -129,49 +129,49 @@ class CommandsAppTest < ActiveSupport::TestCase
test "logs" do test "logs" do
assert_equal \ assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs 2>&1", "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1",
new_command.logs.join(" ") new_command.logs.join(" ")
end end
test "logs with since" do test "logs with since" do
assert_equal \ assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --since 5m 2>&1", "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1",
new_command.logs(since: "5m").join(" ") new_command.logs(since: "5m").join(" ")
end end
test "logs with lines" do test "logs with lines" do
assert_equal \ assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --tail 100 2>&1", "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --tail 100 2>&1",
new_command.logs(lines: "100").join(" ") new_command.logs(lines: "100").join(" ")
end end
test "logs with since and lines" do test "logs with since and lines" do
assert_equal \ assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --since 5m --tail 100 2>&1", "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m --tail 100 2>&1",
new_command.logs(since: "5m", lines: "100").join(" ") new_command.logs(since: "5m", lines: "100").join(" ")
end end
test "logs with grep" do test "logs with grep" do
assert_equal \ assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs 2>&1 | grep 'my-id'", "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'my-id'",
new_command.logs(grep: "my-id").join(" ") new_command.logs(grep: "my-id").join(" ")
end end
test "logs with grep and grep options" do test "logs with grep and grep options" do
assert_equal \ assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs 2>&1 | grep 'my-id' -C 2", "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'my-id' -C 2",
new_command.logs(grep: "my-id", grep_options: "-C 2").join(" ") new_command.logs(grep: "my-id", grep_options: "-C 2").join(" ")
end end
test "logs with since, grep and grep options" do test "logs with since, grep and grep options" do
assert_equal \ assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --since 5m 2>&1 | grep 'my-id' -C 2", "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1 | grep 'my-id' -C 2",
new_command.logs(since: "5m", grep: "my-id", grep_options: "-C 2").join(" ") new_command.logs(since: "5m", grep: "my-id", grep_options: "-C 2").join(" ")
end end
test "logs with since and grep" do test "logs with since and grep" do
assert_equal \ assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --since 5m 2>&1 | grep 'my-id'", "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1 | grep 'my-id'",
new_command.logs(since: "5m", grep: "my-id").join(" ") new_command.logs(since: "5m", grep: "my-id").join(" ")
end end
@@ -191,18 +191,22 @@ class CommandsAppTest < ActiveSupport::TestCase
assert_equal \ assert_equal \
"ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --tail 123 --follow 2>&1 | grep \"Completed\"'", "ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --tail 123 --follow 2>&1 | grep \"Completed\"'",
new_command.follow_logs(host: "app-1", lines: 123, grep: "Completed") new_command.follow_logs(host: "app-1", lines: 123, grep: "Completed")
assert_equal \
"ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --tail 123 --follow 2>&1 | grep \"Completed\"'",
new_command.follow_logs(host: "app-1", timestamps: false, lines: 123, grep: "Completed")
end end
test "execute in new container" do test "execute in new container" do
assert_equal \ assert_equal \
"docker run --rm --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails db:setup", "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails db:setup",
new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ") new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ")
end end
test "execute in new container with env" do test "execute in new container with env" do
assert_equal \ assert_equal \
"docker run --rm --env-file .kamal/apps/app/env/roles/web.env --env foo=\"bar\" dhh/app:999 bin/rails db:setup", "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --env foo=\"bar\" dhh/app:999 bin/rails db:setup",
new_command.execute_in_new_container("bin/rails", "db:setup", env: { "foo" => "bar" }).join(" ") new_command.execute_in_new_container("bin/rails", "db:setup", env: { "foo" => "bar" }).join(" ")
end end
@@ -211,14 +215,14 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } } @config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } }
assert_equal \ assert_equal \
"docker run --rm --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails db:setup", "docker run --rm --network kamal --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails db:setup",
new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ") new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ")
end end
test "execute in new container with custom options" do test "execute in new container with custom options" do
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } } @config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } }
assert_equal \ assert_equal \
"docker run --rm --env-file .kamal/apps/app/env/roles/web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails db:setup", "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails db:setup",
new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ") new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ")
end end
@@ -235,7 +239,7 @@ class CommandsAppTest < ActiveSupport::TestCase
end end
test "execute in new container over ssh" do test "execute in new container over ssh" do
assert_match %r{docker run -it --rm --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails c}, assert_match %r{docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails c},
new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {}) new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
end end
@@ -243,13 +247,13 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:servers] = [ { "1.1.1.1" => "tag1" } ] @config[:servers] = [ { "1.1.1.1" => "tag1" } ]
@config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } } @config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } }
assert_equal "ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails c'", assert_equal "ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --network kamal --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails c'",
new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {}) new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
end end
test "execute in new container with custom options over ssh" do test "execute in new container with custom options over ssh" do
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } } @config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } }
assert_match %r{docker run -it --rm --env-file .kamal/apps/app/env/roles/web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c}, assert_match %r{docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c},
new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {}) new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
end end

View File

@@ -16,41 +16,34 @@ class CommandsHookTest < ActiveSupport::TestCase
end end
test "run" do test "run" do
assert_equal [ assert_equal [ ".kamal/hooks/foo" ], new_command.run("foo")
".kamal/hooks/foo", end
{ env: {
"KAMAL_RECORDED_AT" => @recorded_at, test "env" do
"KAMAL_PERFORMER" => @performer, assert_equal ({
"KAMAL_VERSION" => "123", "KAMAL_RECORDED_AT" => @recorded_at,
"KAMAL_SERVICE_VERSION" => "app@123", "KAMAL_PERFORMER" => @performer,
"KAMAL_SERVICE" => "app" } } "KAMAL_VERSION" => "123",
], new_command.run("foo") "KAMAL_SERVICE_VERSION" => "app@123",
"KAMAL_SERVICE" => "app"
}), new_command.env
end end
test "run with custom hooks_path" do test "run with custom hooks_path" do
assert_equal [ assert_equal [ "custom/hooks/path/foo" ], new_command(hooks_path: "custom/hooks/path").run("foo")
"custom/hooks/path/foo",
{ env: {
"KAMAL_RECORDED_AT" => @recorded_at,
"KAMAL_PERFORMER" => @performer,
"KAMAL_VERSION" => "123",
"KAMAL_SERVICE_VERSION" => "app@123",
"KAMAL_SERVICE" => "app" } }
], new_command(hooks_path: "custom/hooks/path").run("foo")
end end
test "hook with secrets" do test "env with secrets" do
with_test_secrets("secrets" => "DB_PASSWORD=secret") do with_test_secrets("secrets" => "DB_PASSWORD=secret") do
assert_equal [ assert_equal (
".kamal/hooks/foo", {
{ env: {
"KAMAL_RECORDED_AT" => @recorded_at, "KAMAL_RECORDED_AT" => @recorded_at,
"KAMAL_PERFORMER" => @performer, "KAMAL_PERFORMER" => @performer,
"KAMAL_VERSION" => "123", "KAMAL_VERSION" => "123",
"KAMAL_SERVICE_VERSION" => "app@123", "KAMAL_SERVICE_VERSION" => "app@123",
"KAMAL_SERVICE" => "app", "KAMAL_SERVICE" => "app",
"DB_PASSWORD" => "secret" } } "DB_PASSWORD" => "secret" }
], new_command(env: { "secret" => [ "DB_PASSWORD" ] }).run("foo", secrets: true) ), new_command.env(secrets: true)
end end
end end

View File

@@ -15,13 +15,13 @@ class CommandsProxyTest < ActiveSupport::TestCase
test "run" do test "run" do
assert_equal \ assert_equal \
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}", "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}",
new_command.run.join(" ") new_command.run.join(" ")
end end
test "run with ports configured" do test "run with ports configured" do
assert_equal \ assert_equal \
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}", "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}",
new_command.run.join(" ") new_command.run.join(" ")
end end
@@ -29,7 +29,7 @@ class CommandsProxyTest < ActiveSupport::TestCase
@config.delete(:proxy) @config.delete(:proxy)
assert_equal \ assert_equal \
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}", "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}",
new_command.run.join(" ") new_command.run.join(" ")
end end
@@ -37,7 +37,7 @@ class CommandsProxyTest < ActiveSupport::TestCase
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } } @config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
assert_equal \ assert_equal \
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" #{KAMAL.config.proxy_image}", "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}",
new_command.run.join(" ") new_command.run.join(" ")
end end
@@ -67,16 +67,22 @@ class CommandsProxyTest < ActiveSupport::TestCase
test "proxy logs since 2h" do test "proxy logs since 2h" do
assert_equal \ assert_equal \
"docker logs kamal-proxy --since 2h --timestamps 2>&1", "docker logs kamal-proxy --since 2h --timestamps 2>&1",
new_command.logs(since: "2h").join(" ") new_command.logs(since: "2h").join(" ")
end end
test "proxy logs last 10 lines" do test "proxy logs last 10 lines" do
assert_equal \ assert_equal \
"docker logs kamal-proxy --tail 10 --timestamps 2>&1", "docker logs kamal-proxy --tail 10 --timestamps 2>&1",
new_command.logs(lines: 10).join(" ") new_command.logs(lines: 10).join(" ")
end end
test "proxy logs without timestamps" do
assert_equal \
"docker logs kamal-proxy 2>&1",
new_command.logs(timestamps: false).join(" ")
end
test "proxy logs with grep hello!" do test "proxy logs with grep hello!" do
assert_equal \ assert_equal \
"docker logs kamal-proxy --timestamps 2>&1 | grep 'hello!'", "docker logs kamal-proxy --timestamps 2>&1 | grep 'hello!'",

View File

@@ -55,6 +55,15 @@ class CommandsRegistryTest < ActiveSupport::TestCase
registry.logout.join(" ") registry.logout.join(" ")
end end
test "registry setup" do
@config[:registry] = { "server" => "localhost:5000" }
assert_equal "docker start kamal-docker-registry || docker run --detach -p 5000:5000 --name kamal-docker-registry registry:2", registry.setup.join(" ")
end
test "registry remove" do
assert_equal "docker stop kamal-docker-registry && docker rm kamal-docker-registry", registry.remove.join(" ")
end
private private
def registry def registry
Kamal::Commands::Registry.new Kamal::Configuration.new(@config) Kamal::Commands::Registry.new Kamal::Configuration.new(@config)

View File

@@ -0,0 +1,37 @@
service: app
image: dhh/app
servers:
web:
- "1.1.1.1"
- "1.1.1.2"
workers:
- "1.1.1.3"
- "1.1.1.4"
registry:
server: localhost:5000
builder:
arch: amd64
accessories:
mysql:
image: mysql:5.7
host: 1.1.1.3
port: 3306
env:
clear:
MYSQL_ROOT_HOST: '%'
secret:
- MYSQL_ROOT_PASSWORD
files:
- test/fixtures/files/my.cnf:/etc/mysql/my.cnf
directories:
- data:/var/lib/mysql
redis:
image: redis:latest
roles:
- web
port: 6379
directories:
- data:/data
readiness_delay: 0

View File

@@ -27,14 +27,14 @@ class AppTest < IntegrationTest
images = kamal :app, :images, capture: true images = kamal :app, :images, capture: true
assert_match "App Host: vm1", images assert_match "App Host: vm1", images
assert_match "App Host: vm2", images assert_match "App Host: vm2", images
assert_match /registry:4443\/app\s+#{latest_app_version}/, images assert_match /localhost:5000\/app\s+#{latest_app_version}/, images
assert_match /registry:4443\/app\s+latest/, images assert_match /localhost:5000\/app\s+latest/, images
containers = kamal :app, :containers, capture: true containers = kamal :app, :containers, capture: true
assert_match "App Host: vm1", containers assert_match "App Host: vm1", containers
assert_match "App Host: vm2", containers assert_match "App Host: vm2", containers
assert_match "registry:4443/app:#{latest_app_version}", containers assert_match "localhost:5000/app:#{latest_app_version}", containers
assert_match "registry:4443/app:latest", containers assert_match "localhost:5000/app:latest", containers
exec_output = kamal :app, :exec, :ps, capture: true exec_output = kamal :app, :exec, :ps, capture: true
assert_match "App Host: vm1", exec_output assert_match "App Host: vm1", exec_output

View File

@@ -9,7 +9,7 @@ class BrokenDeployTest < IntegrationTest
kamal :deploy kamal :deploy
assert_app_is_up version: first_version assert_app_is_up version: first_version
assert_container_running host: :vm3, name: "app-workers-#{first_version}" assert_container_running host: :vm3, name: "app_with_roles-workers-#{first_version}"
second_version = break_app second_version = break_app
@@ -17,8 +17,8 @@ class BrokenDeployTest < IntegrationTest
assert_failed_deploy output assert_failed_deploy output
assert_app_is_up version: first_version assert_app_is_up version: first_version
assert_container_running host: :vm3, name: "app-workers-#{first_version}" assert_container_running host: :vm3, name: "app_with_roles-workers-#{first_version}"
assert_container_not_running host: :vm3, name: "app-workers-#{second_version}" assert_container_not_running host: :vm3, name: "app_with_roles-workers-#{second_version}"
end end
private private

View File

@@ -23,11 +23,10 @@ asset_path: /usr/share/nginx/html/versions
deploy_timeout: 2 deploy_timeout: 2
drain_timeout: 2 drain_timeout: 2
readiness_delay: 0 readiness_delay: 0
proxy:
host: 127.0.0.1
registry: registry:
server: registry:4443 server: localhost:5000
username: root
password: root
builder: builder:
driver: docker driver: docker
arch: <%= Kamal::Utils.docker_arch %> arch: <%= Kamal::Utils.docker_arch %>

View File

@@ -1,5 +1,5 @@
service: app service: app_with_roles
image: app image: app_with_roles
servers: servers:
web: web:
hosts: hosts:
@@ -14,6 +14,7 @@ drain_timeout: 2
readiness_delay: 0 readiness_delay: 0
proxy: proxy:
host: localhost
healthcheck: healthcheck:
interval: 1 interval: 1
timeout: 1 timeout: 1

View File

@@ -8,6 +8,8 @@ server {
location / { location / {
proxy_pass http://loadbalancer; proxy_pass http://loadbalancer;
proxy_set_header Host $host;
proxy_connect_timeout 10; proxy_connect_timeout 10;
proxy_send_timeout 10; proxy_send_timeout 10;
proxy_read_timeout 10; proxy_read_timeout 10;

View File

@@ -50,8 +50,8 @@ class IntegrationTest < ActiveSupport::TestCase
assert_equal "502", response.code assert_equal "502", response.code
end end
def assert_app_is_up(version: nil) def assert_app_is_up(version: nil, app: @app)
response = app_response response = app_response(app: app)
debug_response_code(response, "200") debug_response_code(response, "200")
assert_equal "200", response.code assert_equal "200", response.code
assert_app_version(version, response) if version assert_app_version(version, response) if version
@@ -69,8 +69,8 @@ class IntegrationTest < ActiveSupport::TestCase
assert_equal up_times, up_count assert_equal up_times, up_count
end end
def app_response def app_response(app: @app)
Net::HTTP.get_response(URI.parse("http://localhost:12345/version")) Net::HTTP.get_response(URI.parse("http://#{app_host(app)}:12345/version"))
end end
def update_app_rev def update_app_rev
@@ -153,11 +153,26 @@ class IntegrationTest < ActiveSupport::TestCase
assert_directory_removed("./kamal/apps/#{@app}") assert_directory_removed("./kamal/apps/#{@app}")
end end
def assert_proxy_directory_removed
assert_directory_removed("./kamal/proxy")
end
def assert_directory_removed(directory) def assert_directory_removed(directory)
assert docker_compose("exec vm1 ls #{directory} | wc -l", capture: true).strip == "0" assert docker_compose("exec vm1 ls #{directory} | wc -l", capture: true).strip == "0"
end end
def assert_proxy_running
assert_container_running(host: "vm1", name: "kamal-proxy")
end
def assert_proxy_not_running
assert_container_not_running(host: "vm1", name: "kamal-proxy")
end
def app_host(app = @app)
case app
when "app"
"127.0.0.1"
when "app_with_roles"
"localhost"
else
raise "Unknown app: #{app}"
end
end
end end

View File

@@ -29,7 +29,7 @@ class MainTest < IntegrationTest
assert_match /App Host: vm1/, details assert_match /App Host: vm1/, details
assert_match /App Host: vm2/, details assert_match /App Host: vm2/, details
assert_match /basecamp\/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}/, details assert_match /basecamp\/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}/, details
assert_match /registry:4443\/app:#{first_version}/, details assert_match /localhost:5000\/app:#{first_version}/, details
audit = kamal :audit, capture: true audit = kamal :audit, capture: true
assert_match /Booted app version #{first_version}.*Booted app version #{second_version}.*Booted app version #{first_version}.*/m, audit assert_match /Booted app version #{first_version}.*Booted app version #{second_version}.*Booted app version #{first_version}.*/m, audit
@@ -46,13 +46,13 @@ class MainTest < IntegrationTest
assert_app_is_up version: version assert_app_is_up version: version
assert_hooks_ran "pre-connect", "pre-build", "pre-deploy", "post-deploy" assert_hooks_ran "pre-connect", "pre-build", "pre-deploy", "post-deploy"
assert_container_running host: :vm3, name: "app-workers-#{version}" assert_container_running host: :vm3, name: "app_with_roles-workers-#{version}"
second_version = update_app_rev second_version = update_app_rev
kamal :redeploy kamal :redeploy
assert_app_is_up version: second_version assert_app_is_up version: second_version
assert_container_running host: :vm3, name: "app-workers-#{second_version}" assert_container_running host: :vm3, name: "app_with_roles-workers-#{second_version}"
end end
test "config" do test "config" do
@@ -63,8 +63,8 @@ class MainTest < IntegrationTest
assert_equal [ "vm1", "vm2" ], config[:hosts] assert_equal [ "vm1", "vm2" ], config[:hosts]
assert_equal "vm1", config[:primary_host] assert_equal "vm1", config[:primary_host]
assert_equal version, config[:version] assert_equal version, config[:version]
assert_equal "registry:4443/app", config[:repository] assert_equal "localhost:5000/app", config[:repository]
assert_equal "registry:4443/app:#{version}", config[:absolute_image] assert_equal "localhost:5000/app:#{version}", config[:absolute_image]
assert_equal "app-#{version}", config[:service_with_version] assert_equal "app-#{version}", config[:service_with_version]
assert_equal [], config[:volume_args] assert_equal [], config[:volume_args]
assert_equal({ user: "root", port: 22, keepalive: true, keepalive_interval: 30, log_level: :fatal }, config[:ssh_options]) assert_equal({ user: "root", port: 22, keepalive: true, keepalive_interval: 30, log_level: :fatal }, config[:ssh_options])
@@ -98,7 +98,29 @@ class MainTest < IntegrationTest
kamal :remove, "-y" kamal :remove, "-y"
assert_no_images_or_containers assert_no_images_or_containers
assert_app_directory_removed assert_app_directory_removed
assert_proxy_directory_removed end
test "two apps" do
@app = "app"
kamal :deploy
app1_version = latest_app_version
@app = "app_with_roles"
kamal :deploy
app2_version = latest_app_version
assert_app_is_up version: app1_version, app: "app"
assert_app_is_up version: app2_version, app: "app_with_roles"
@app = "app"
kamal :remove, "-y"
assert_app_directory_removed
assert_proxy_running
@app = "app_with_roles"
kamal :remove, "-y"
assert_app_directory_removed
assert_proxy_not_running
end end
private private
@@ -116,21 +138,21 @@ class MainTest < IntegrationTest
end end
def assert_env(key, value, vm:, version:) def assert_env(key, value, vm:, version:)
assert_equal "#{key}=#{value}", docker_compose("exec #{vm} docker exec app-web-#{version} env | grep #{key}", capture: true) assert_equal "#{key}=#{value}", docker_compose("exec #{vm} docker exec #{@app}-web-#{version} env | grep #{key}", capture: true)
end end
def assert_no_env(key, vm:, version:) def assert_no_env(key, vm:, version:)
assert_raises(RuntimeError, /exit 1/) do assert_raises(RuntimeError, /exit 1/) do
docker_compose("exec #{vm} docker exec app-web-#{version} env | grep #{key}", capture: true) docker_compose("exec #{vm} docker exec #{@app}-web-#{version} env | grep #{key}", capture: true)
end end
end end
def assert_accumulated_assets(*versions) def assert_accumulated_assets(*versions)
versions.each do |version| versions.each do |version|
assert_equal "200", Net::HTTP.get_response(URI.parse("http://localhost:12345/versions/#{version}")).code assert_equal "200", Net::HTTP.get_response(URI.parse("http://#{app_host}:12345/versions/#{version}")).code
end end
assert_equal "200", Net::HTTP.get_response(URI.parse("http://localhost:12345/versions/.hidden")).code assert_equal "200", Net::HTTP.get_response(URI.parse("http://#{app_host}:12345/versions/.hidden")).code
end end
def vm1_image_ids def vm1_image_ids

View File

@@ -48,19 +48,5 @@ class ProxyTest < IntegrationTest
kamal :proxy, :remove kamal :proxy, :remove
assert_proxy_not_running assert_proxy_not_running
assert_proxy_directory_removed
end end
private
def assert_proxy_running
assert_match /basecamp\/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION} \"kamal-proxy run\"/, proxy_details
end
def assert_proxy_not_running
assert_no_match /basecamp\/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION} \"kamal-proxy run\"/, proxy_details
end
def proxy_details
kamal :proxy, :details, capture: true
end
end end