diff --git a/lib/kamal/cli/base.rb b/lib/kamal/cli/base.rb index 9f38241a1..8bbdf198a 100644 --- a/lib/kamal/cli/base.rb +++ b/lib/kamal/cli/base.rb @@ -79,6 +79,8 @@ def mutating run_hook "pre-connect" + ensure_run_directory + acquire_lock begin @@ -167,5 +169,11 @@ def subcommand def first_invocation instance_variable_get("@_invocations").first end + + def ensure_run_directory + on(KAMAL.hosts) do + execute(*KAMAL.server.ensure_run_directory) + end + end end end diff --git a/lib/kamal/cli/lock.rb b/lib/kamal/cli/lock.rb index c30e6f0fe..1e4b52cf2 100644 --- a/lib/kamal/cli/lock.rb +++ b/lib/kamal/cli/lock.rb @@ -2,7 +2,10 @@ class Kamal::Cli::Lock < Kamal::Cli::Base desc "status", "Report lock status" def status handle_missing_lock do - on(KAMAL.primary_host) { puts capture_with_debug(*KAMAL.lock.status) } + on(KAMAL.primary_host) do + execute *KAMAL.server.ensure_run_directory + puts capture_with_debug(*KAMAL.lock.status) + end end end @@ -11,7 +14,10 @@ def status def acquire message = options[:message] raise_if_locked do - on(KAMAL.primary_host) { execute *KAMAL.lock.acquire(message, KAMAL.config.version), verbosity: :debug } + on(KAMAL.primary_host) do + execute *KAMAL.server.ensure_run_directory + execute *KAMAL.lock.acquire(message, KAMAL.config.version), verbosity: :debug + end say "Acquired the deploy lock" end end @@ -19,7 +25,10 @@ def acquire desc "release", "Release the deploy lock" def release handle_missing_lock do - on(KAMAL.primary_host) { execute *KAMAL.lock.release, verbosity: :debug } + on(KAMAL.primary_host) do + execute *KAMAL.server.ensure_run_directory + execute *KAMAL.lock.release, verbosity: :debug + end say "Released the deploy lock" end end diff --git a/lib/kamal/cli/server.rb b/lib/kamal/cli/server.rb index 4cf23a5da..8387d47d6 100644 --- a/lib/kamal/cli/server.rb +++ b/lib/kamal/cli/server.rb @@ -14,6 +14,10 @@ def bootstrap end end + on(KAMAL.hosts) do + execute(*KAMAL.server.ensure_run_directory) + end + if missing.any? raise "Docker is not installed on #{missing.join(", ")} and can't be automatically installed without having root access and the `curl` command available. Install Docker manually: https://docs.docker.com/engine/install/" end diff --git a/lib/kamal/commander.rb b/lib/kamal/commander.rb index 047951643..47e0cf9fd 100644 --- a/lib/kamal/commander.rb +++ b/lib/kamal/commander.rb @@ -116,6 +116,10 @@ def registry @registry ||= Kamal::Commands::Registry.new(config) end + def server + @server ||= Kamal::Commands::Server.new(config) + end + def traefik @traefik ||= Kamal::Commands::Traefik.new(config) end diff --git a/lib/kamal/commands/auditor.rb b/lib/kamal/commands/auditor.rb index 0fefbbcb4..ad9c4d169 100644 --- a/lib/kamal/commands/auditor.rb +++ b/lib/kamal/commands/auditor.rb @@ -19,7 +19,9 @@ def reveal private def audit_log_file - [ "kamal", config.service, config.destination, "audit.log" ].compact.join("-") + file = [ config.service, config.destination, "audit.log" ].compact.join("-") + + "#{config.run_directory}/#{file}" end def audit_tags(**details) diff --git a/lib/kamal/commands/lock.rb b/lib/kamal/commands/lock.rb index 03b9abd27..c5d216ac7 100644 --- a/lib/kamal/commands/lock.rb +++ b/lib/kamal/commands/lock.rb @@ -40,7 +40,7 @@ def stat_lock_dir end def lock_dir - "kamal_lock-#{config.service}" + "#{config.run_directory}/lock-#{config.service}" end def lock_details_file diff --git a/lib/kamal/commands/server.rb b/lib/kamal/commands/server.rb new file mode 100644 index 000000000..5b3ad194d --- /dev/null +++ b/lib/kamal/commands/server.rb @@ -0,0 +1,5 @@ +class Kamal::Commands::Server < Kamal::Commands::Base + def ensure_run_directory + [:mkdir, "-p", config.run_directory] + end +end diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index 28da91efa..2bb8002bd 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -57,6 +57,10 @@ def abbreviated_version Kamal::Utils.abbreviate_version(version) end + def run_directory + raw_config.run_directory || ".kamal" + end + def roles @roles ||= role_names.collect { |role_name| Role.new(role_name, config: self) } diff --git a/test/cli/build_test.rb b/test/cli/build_test.rb index 4e09564cf..35e8b7611 100644 --- a/test/cli/build_test.rb +++ b/test/cli/build_test.rb @@ -20,7 +20,7 @@ class CliBuildTest < CliTestCase end test "push without builder" do - stub_locking + stub_setup SSHKit::Backend::Abstract.any_instance.stubs(:execute) .with(:docker, "--version", "&&", :docker, :buildx, "version") @@ -36,7 +36,7 @@ class CliBuildTest < CliTestCase end test "push with no buildx plugin" do - stub_locking + stub_setup SSHKit::Backend::Abstract.any_instance.stubs(:execute) .with(:docker, "--version", "&&", :docker, :buildx, "version") .raises(SSHKit::Command::Failed.new("no buildx")) @@ -67,7 +67,7 @@ class CliBuildTest < CliTestCase end test "create with error" do - stub_locking + stub_setup SSHKit::Backend::Abstract.any_instance.stubs(:execute) .with { |arg| arg == :docker } .raises(SSHKit::Command::Failed.new("stderr=error")) diff --git a/test/cli/cli_test_case.rb b/test/cli/cli_test_case.rb index e28f96327..8f99f968f 100644 --- a/test/cli/cli_test_case.rb +++ b/test/cli/cli_test_case.rb @@ -27,11 +27,13 @@ def fail_hook(hook) .raises(SSHKit::Command::Failed.new("failed")) end - def stub_locking + def stub_setup SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |arg1, arg2| arg1 == :mkdir && arg2 == "kamal_lock-app" } + .with { |*args| args == [ :mkdir, "-p", ".kamal" ] } SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |arg1, arg2| arg1 == :rm && arg2 == "kamal_lock-app/details" } + .with { |arg1, arg2| arg1 == :mkdir && arg2 == ".kamal/lock-app" } + SSHKit::Backend::Abstract.any_instance.stubs(:execute) + .with { |arg1, arg2| arg1 == :rm && arg2 == ".kamal/lock-app/details" } end def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: nil) diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index 888a66885..18b313744 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -63,11 +63,14 @@ class CliMainTest < CliTestCase Thread.report_on_exception = false SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |*arg| arg[0..1] == [:mkdir, 'kamal_lock-app'] } + .with { |*args| args == [ :mkdir, "-p", ".kamal" ] } + + SSHKit::Backend::Abstract.any_instance.stubs(:execute) + .with { |*arg| arg[0..1] == [:mkdir, ".kamal/lock-app"] } .raises(RuntimeError, "mkdir: cannot create directory ‘kamal_lock-app’: File exists") SSHKit::Backend::Abstract.any_instance.expects(:capture_with_debug) - .with(:stat, 'kamal_lock-app', ">", "/dev/null", "&&", :cat, "kamal_lock-app/details", "|", :base64, "-d") + .with(:stat, ".kamal/lock-app", ">", "/dev/null", "&&", :cat, ".kamal/lock-app/details", "|", :base64, "-d") assert_raises(Kamal::Cli::LockError) do run_command("deploy") @@ -78,7 +81,10 @@ class CliMainTest < CliTestCase Thread.report_on_exception = false SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |*arg| arg[0..1] == [:mkdir, 'kamal_lock-app'] } + .with { |*args| args == [ :mkdir, "-p", ".kamal" ] } + + SSHKit::Backend::Abstract.any_instance.stubs(:execute) + .with { |*arg| arg[0..1] == [:mkdir, ".kamal/lock-app"] } .raises(SocketError, "getaddrinfo: nodename nor servname provided, or not known") assert_raises(SSHKit::Runner::ExecuteError) do @@ -230,7 +236,7 @@ class CliMainTest < CliTestCase test "audit" do run_command("audit").tap do |output| - assert_match /tail -n 50 kamal-app-audit.log on 1.1.1.1/, output + assert_match %r{tail -n 50 \.kamal/app-audit.log on 1.1.1.1}, output assert_match /App Host: 1.1.1.1/, output end end diff --git a/test/cli/server_test.rb b/test/cli/server_test.rb index 377f6cff9..5742d5445 100644 --- a/test/cli/server_test.rb +++ b/test/cli/server_test.rb @@ -3,6 +3,7 @@ class CliServerTest < CliTestCase test "bootstrap already installed" do SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(true).at_least_once + SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:mkdir, "-p", ".kamal").returns("").at_least_once assert_equal "", run_command("bootstrap") end @@ -10,6 +11,7 @@ class CliServerTest < CliTestCase test "bootstrap install as non-root user" do SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(false).at_least_once SSHKit::Backend::Abstract.any_instance.expects(:execute).with('[ "${EUID:-$(id -u)}" -eq 0 ]', raise_on_non_zero_exit: false).returns(false).at_least_once + SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:mkdir, "-p", ".kamal").returns("").at_least_once assert_raise RuntimeError, "Docker is not installed on 1.1.1.1, 1.1.1.3, 1.1.1.4, 1.1.1.2 and can't be automatically installed without having root access and the `curl` command available. Install Docker manually: https://docs.docker.com/engine/install/" do run_command("bootstrap") @@ -20,6 +22,7 @@ class CliServerTest < CliTestCase SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(false).at_least_once SSHKit::Backend::Abstract.any_instance.expects(:execute).with('[ "${EUID:-$(id -u)}" -eq 0 ]', raise_on_non_zero_exit: false).returns(true).at_least_once SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:curl, "-fsSL", "https://get.docker.com", "|", :sh).at_least_once + SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:mkdir, "-p", ".kamal").returns("").at_least_once run_command("bootstrap").tap do |output| ("1.1.1.1".."1.1.1.4").map do |host| diff --git a/test/cli/traefik_test.rb b/test/cli/traefik_test.rb index d5023230c..0edaf1f61 100644 --- a/test/cli/traefik_test.rb +++ b/test/cli/traefik_test.rb @@ -20,7 +20,7 @@ class CliTraefikTest < CliTestCase test "reboot --rolling" do run_command("reboot", "--rolling").tap do |output| - assert_match "Running docker container prune --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output.lines[3] + assert_match "Running docker container prune --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output end end diff --git a/test/commands/auditor_test.rb b/test/commands/auditor_test.rb index 1ea061fba..29b0025d0 100644 --- a/test/commands/auditor_test.rb +++ b/test/commands/auditor_test.rb @@ -21,7 +21,7 @@ class CommandsAuditorTest < ActiveSupport::TestCase :echo, "[#{@recorded_at}] [#{@performer}]", "app removed container", - ">>", "kamal-app-audit.log" + ">>", ".kamal/app-audit.log" ], @auditor.record("app removed container") end @@ -31,7 +31,7 @@ class CommandsAuditorTest < ActiveSupport::TestCase :echo, "[#{@recorded_at}] [#{@performer}] [staging]", "app removed container", - ">>", "kamal-app-staging-audit.log" + ">>", ".kamal/app-staging-audit.log" ], auditor.record("app removed container") end end @@ -42,7 +42,7 @@ class CommandsAuditorTest < ActiveSupport::TestCase :echo, "[#{@recorded_at}] [#{@performer}] [web]", "app removed container", - ">>", "kamal-app-audit.log" + ">>", ".kamal/app-audit.log" ], auditor.record("app removed container") end end @@ -52,7 +52,7 @@ class CommandsAuditorTest < ActiveSupport::TestCase :echo, "[#{@recorded_at}] [#{@performer}] [value]", "app removed container", - ">>", "kamal-app-audit.log" + ">>", ".kamal/app-audit.log" ], @auditor.record("app removed container", detail: "value") end diff --git a/test/commands/lock_test.rb b/test/commands/lock_test.rb index 4ed4fe1f0..dc8981f96 100644 --- a/test/commands/lock_test.rb +++ b/test/commands/lock_test.rb @@ -10,19 +10,19 @@ class CommandsLockTest < ActiveSupport::TestCase test "status" do assert_equal \ - "stat kamal_lock-app > /dev/null && cat kamal_lock-app/details | base64 -d", + "stat .kamal/lock-app > /dev/null && cat .kamal/lock-app/details | base64 -d", new_command.status.join(" ") end test "acquire" do assert_match \ - /mkdir kamal_lock-app && echo ".*" > kamal_lock-app\/details/m, + %r{mkdir \.kamal/lock-app && echo ".*" > \.kamal/lock-app/details}m, new_command.acquire("Hello", "123").join(" ") end test "release" do assert_match \ - "rm kamal_lock-app/details && rm -r kamal_lock-app", + "rm .kamal/lock-app/details && rm -r .kamal/lock-app", new_command.release.join(" ") end diff --git a/test/commands/server_test.rb b/test/commands/server_test.rb new file mode 100644 index 000000000..9e063f77d --- /dev/null +++ b/test/commands/server_test.rb @@ -0,0 +1,23 @@ +require "test_helper" + +class CommandsServerTest < ActiveSupport::TestCase + setup do + @config = { + service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], + traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } + } + end + + test "ensure run directory" do + assert_equal "mkdir -p .kamal", new_command.ensure_run_directory.join(" ") + end + + test "ensure non default run directory" do + assert_equal "mkdir -p /var/run/kamal", new_command(run_directory: "/var/run/kamal").ensure_run_directory.join(" ") + end + + private + def new_command(extra_config = {}) + Kamal::Commands::Server.new(Kamal::Configuration.new(@config.merge(extra_config))) + end +end diff --git a/test/configuration_test.rb b/test/configuration_test.rb index 89aeaf844..615053129 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -283,4 +283,12 @@ class ConfigurationTest < ActiveSupport::TestCase Kamal::Configuration.new(@deploy.tap { |c| c.merge!(minimum_version: "10000.0.0") }) end end + + test "run directory" do + config = Kamal::Configuration.new(@deploy) + assert_equal ".kamal", config.run_directory + + config = Kamal::Configuration.new(@deploy.merge!(run_directory: "/root/kamal")) + assert_equal "/root/kamal", config.run_directory + end end