Skip to content

Commit

Permalink
Configurable Kamal directory
Browse files Browse the repository at this point in the history
To avoid polluting the default SSH directory with lots of Kamal config,
we'll default to putting them in a `kamal` sub directory.

But also make the directory configurable with the `run_directory` key,
so for example you can set it as `/var/run/kamal/`

The directory is created during bootstrap or before any command that
will need to access a file.
  • Loading branch information
djmb committed Aug 28, 2023
1 parent 9363b6a commit bcfa1d8
Show file tree
Hide file tree
Showing 17 changed files with 101 additions and 23 deletions.
8 changes: 8 additions & 0 deletions lib/kamal/cli/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ def mutating

run_hook "pre-connect"

ensure_run_directory

acquire_lock

begin
Expand Down Expand Up @@ -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
15 changes: 12 additions & 3 deletions lib/kamal/cli/lock.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -11,15 +14,21 @@ 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

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
Expand Down
4 changes: 4 additions & 0 deletions lib/kamal/cli/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions lib/kamal/commander.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion lib/kamal/commands/auditor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion lib/kamal/commands/lock.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions lib/kamal/commands/server.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Kamal::Commands::Server < Kamal::Commands::Base
def ensure_run_directory
[:mkdir, "-p", config.run_directory]
end
end
4 changes: 4 additions & 0 deletions lib/kamal/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand Down
6 changes: 3 additions & 3 deletions test/cli/build_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -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"))
Expand Down Expand Up @@ -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"))
Expand Down
8 changes: 5 additions & 3 deletions test/cli/cli_test_case.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 10 additions & 4 deletions test/cli/main_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions test/cli/server_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
class CliServerTest < CliTestCase
test "bootstrap already installed" do
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(true).at_least_once
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:mkdir, "-p", "kamal").returns("").at_least_once

assert_equal "", run_command("bootstrap")
end

test "bootstrap install as non-root user" do
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(false).at_least_once
SSHKit::Backend::Abstract.any_instance.expects(:execute).with('[ "${EUID:-$(id -u)}" -eq 0 ]', raise_on_non_zero_exit: false).returns(false).at_least_once
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(: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")
Expand All @@ -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|
Expand Down
2 changes: 1 addition & 1 deletion test/cli/traefik_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 4 additions & 4 deletions test/commands/auditor_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

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

Expand Down
6 changes: 3 additions & 3 deletions test/commands/lock_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
23 changes: 23 additions & 0 deletions test/commands/server_test.rb
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions test/configuration_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit bcfa1d8

Please sign in to comment.