Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into feat/no-web
Browse files Browse the repository at this point in the history
  • Loading branch information
yoelcabo committed Nov 14, 2023
2 parents 887b7dd + 77a79b2 commit f9214c9
Show file tree
Hide file tree
Showing 29 changed files with 292 additions and 77 deletions.
4 changes: 2 additions & 2 deletions lib/kamal/cli/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ def self.exit_on_failure?() true end
class_option :version, desc: "Run commands against a specific app version"

class_option :primary, type: :boolean, aliases: "-p", desc: "Run commands only on primary host instead of all"
class_option :hosts, aliases: "-h", desc: "Run commands on these hosts instead of all (separate by comma)"
class_option :roles, aliases: "-r", desc: "Run commands on these roles instead of all (separate by comma)"
class_option :hosts, aliases: "-h", desc: "Run commands on these hosts instead of all (separate by comma, supports wildcards with *)"
class_option :roles, aliases: "-r", desc: "Run commands on these roles instead of all (separate by comma, supports wildcards with *)"

class_option :config_file, aliases: "-c", default: "config/deploy.yml", desc: "Path to config file"
class_option :destination, aliases: "-d", desc: "Specify destination to be used for config file (staging -> deploy.staging.yml)"
Expand Down
2 changes: 1 addition & 1 deletion lib/kamal/cli/healthcheck.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ class Kamal::Cli::Healthcheck < Kamal::Cli::Base

desc "perform", "Health check current app version"
def perform
raise "The primary host is not configured to run Traefik" unless KAMAL.config.primary_role.running_traefik?
raise "The primary host is not configured to run Traefik" unless KAMAL.config.role(KAMAL.config.primary_role).running_traefik?
on(KAMAL.primary_host) do
begin
execute *KAMAL.healthcheck.run
Expand Down
2 changes: 1 addition & 1 deletion lib/kamal/cli/main.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def deploy
say "Ensure Traefik is running...", :magenta
invoke "kamal:cli:traefik:boot", [], invoke_options

if KAMAL.config.primary_role.running_traefik?
if KAMAL.config.role(KAMAL.config.primary_role).running_traefik?
say "Ensure app can pass healthcheck...", :magenta
invoke "kamal:cli:healthcheck:perform", [], invoke_options
end
Expand Down
12 changes: 12 additions & 0 deletions lib/kamal/cli/templates/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,15 @@ registry:
# boot:
# limit: 10 # Can also specify as a percentage of total hosts, such as "25%"
# wait: 2

# Configure the role used to determine the primary_host. This host takes
# deploy locks, runs health checks during the deploy, and follow logs, etc.
#
# Caution: there's no support for role renaming yet, so be careful to cleanup
# the previous role on the deployed hosts.
# primary_role: web

# Controls if we abort when see a role with no hosts. Disabling this may be
# useful for more complex deploy configurations.
#
# allow_empty_roles: false
4 changes: 2 additions & 2 deletions lib/kamal/commander.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ def specific_primary!
end

def specific_roles=(role_names)
@specific_roles = config.roles.select { |r| role_names.include?(r.name) } if role_names.present?
@specific_roles = Kamal::Utils.filter_specific_items(role_names, config.roles) if role_names.present?
end

def specific_hosts=(hosts)
@specific_hosts = config.all_hosts & hosts if hosts.present?
@specific_hosts = Kamal::Utils.filter_specific_items(hosts, config.all_hosts) if hosts.present?
end

def primary_host
Expand Down
2 changes: 1 addition & 1 deletion lib/kamal/commands/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def run_over_ssh(*command, host:)
elsif config.ssh.proxy && config.ssh.proxy.is_a?(Net::SSH::Proxy::Command)
cmd << " -o ProxyCommand='#{config.ssh.proxy.command_line_template}'"
end
cmd << " -t #{config.ssh.user}@#{host} '#{command.join(" ")}'"
cmd << " -t #{config.ssh.user}@#{host} -p #{config.ssh.port} '#{command.join(" ")}'"
end
end

Expand Down
10 changes: 5 additions & 5 deletions lib/kamal/commands/healthcheck.rb
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
class Kamal::Commands::Healthcheck < Kamal::Commands::Base

def run
web = config.role(:web)
primary = config.role(config.primary_role)

docker :run,
"--detach",
"--name", container_name_with_version,
"--publish", "#{exposed_port}:#{config.healthcheck["port"]}",
"--label", "service=#{config.healthcheck_service}",
"-e", "KAMAL_CONTAINER_NAME=\"#{config.healthcheck_service}\"",
*web.env_args,
*web.health_check_args(cord: false),
*primary.env_args,
*primary.health_check_args(cord: false),
*config.volume_args,
*web.option_args,
*primary.option_args,
config.absolute_image,
web.cmd
primary.cmd
end

def status
Expand Down
10 changes: 9 additions & 1 deletion lib/kamal/commands/traefik.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base
DEFAULT_ARGS = {
'log.level' => 'DEBUG'
}
DEFAULT_LABELS = {
# These ensure we serve a 502 rather than a 404 if no containers are available
"traefik.http.routers.catchall.entryPoints" => "http",
"traefik.http.routers.catchall.rule" => "PathPrefix(`/`)",
"traefik.http.routers.catchall.service" => "unavailable",
"traefik.http.routers.catchall.priority" => 1,
"traefik.http.services.unavailable.loadbalancer.server.port" => "0"
}

def run
docker :run, "--name traefik",
Expand Down Expand Up @@ -97,7 +105,7 @@ def host_env_directory
end

def labels
config.traefik["labels"] || []
DEFAULT_LABELS.merge(config.traefik["labels"] || {})
end

def image
Expand Down
41 changes: 32 additions & 9 deletions lib/kamal/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ def load_config_files(*files)

def load_config_file(file)
if file.exist?
YAML.load(ERB.new(IO.read(file)).result).symbolize_keys
# Newer Psych doesn't load aliases by default
load_method = YAML.respond_to?(:unsafe_load) ? :unsafe_load : :load
YAML.send(load_method, ERB.new(IO.read(file)).result).symbolize_keys
else
raise "Configuration file not found in #{file}"
end
Expand Down Expand Up @@ -90,17 +92,20 @@ def all_hosts
end

def primary_host
primary_role.primary_host
role(primary_role)&.primary_host
end

def primary_role
role(:web) || roles.first
def traefik_roles
roles.select(&:running_traefik?)
end

def traefik_hosts
roles.select(&:running_traefik?).flat_map(&:hosts).uniq
def traefik_role_names
traefik_roles.flat_map(&:name)
end

def traefik_hosts
traefik_roles.flat_map(&:hosts).uniq
end

def repository
[ raw_config.registry["server"], image ].compact.join("/")
Expand Down Expand Up @@ -203,6 +208,14 @@ def asset_path
raw_config.asset_path
end

def primary_role
raw_config.primary_role || "web"
end

def allow_empty_roles?
raw_config.allow_empty_roles
end


def valid?
ensure_destination_if_required && ensure_required_keys_present && ensure_valid_kamal_version
Expand Down Expand Up @@ -251,9 +264,19 @@ def ensure_required_keys_present
raise ArgumentError, "You must specify a password for the registry in config/deploy.yml (or set the ENV variable if that's used)"
end

roles.each do |role|
if role.hosts.empty?
raise ArgumentError, "No servers specified for the #{role.name} role"
unless role_names.include?(primary_role)
raise ArgumentError, "The primary_role #{primary_role} isn't defined"
end

if role(primary_role).hosts.empty?
raise ArgumentError, "No servers specified for the #{primary_role} primary_role"
end

unless allow_empty_roles?
roles.each do |role|
if role.hosts.empty?
raise ArgumentError, "No servers specified for the #{role.name} role. You can ignore this with allow_empty_roles: true"
end
end
end

Expand Down
11 changes: 10 additions & 1 deletion lib/kamal/configuration/role.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,15 @@ def health_check_interval


def running_traefik?
name.web? || specializations["traefik"]
if specializations["traefik"].nil?
primary?
else
specializations["traefik"]
end
end

def primary?
@config.primary_role == name
end


Expand Down Expand Up @@ -185,6 +193,7 @@ def traefik_labels
"traefik.http.services.#{traefik_service}.loadbalancer.server.scheme" => "http",

"traefik.http.routers.#{traefik_service}.rule" => "PathPrefix(`/`)",
"traefik.http.routers.#{traefik_service}.priority" => "2",
"traefik.http.middlewares.#{traefik_service}-retry.retry.attempts" => "5",
"traefik.http.middlewares.#{traefik_service}-retry.retry.initialinterval" => "500ms",
"traefik.http.routers.#{traefik_service}.middlewares" => "#{traefik_service}-retry@docker"
Expand Down
6 changes: 5 additions & 1 deletion lib/kamal/configuration/ssh.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ def user
config.fetch("user", "root")
end

def port
config.fetch("port", 22)
end

def proxy
if (proxy = config["proxy"])
Net::SSH::Proxy::Jump.new(proxy.include?("@") ? proxy : "root@#{proxy}")
Expand All @@ -18,7 +22,7 @@ def proxy
end

def options
{ user: user, proxy: proxy, logger: logger, keepalive: true, keepalive_interval: 30 }.compact
{ user: user, port: port, proxy: proxy, logger: logger, keepalive: true, keepalive_interval: 30 }.compact
end

def to_h
Expand Down
16 changes: 16 additions & 0 deletions lib/kamal/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,20 @@ def escape_shell_value(value)
.gsub(/`/, '\\\\`')
.gsub(DOLLAR_SIGN_WITHOUT_SHELL_EXPANSION_REGEX, '\$')
end

# Apply a list of host or role filters, including wildcard matches
def filter_specific_items(filters, items)
matches = []

Array(filters).select do |filter|
matches += Array(items).select do |item|
# Only allow * for a wildcard
pattern = Regexp.escape(filter).gsub('\*', '.*')
# items are roles or hosts
(item.respond_to?(:name) ? item.name : item).match(/^#{pattern}$/)
end
end

matches
end
end
2 changes: 1 addition & 1 deletion test/cli/accessory_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ class CliAccessoryTest < CliTestCase

test "logs with follow" do
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
.with("ssh -t root@1.1.1.3 'docker logs app-mysql --timestamps --tail 10 --follow 2>&1'")
.with("ssh -t root@1.1.1.3 -p 22 'docker logs app-mysql --timestamps --tail 10 --follow 2>&1'")

assert_match "docker logs app-mysql --timestamps --tail 10 --follow 2>&1", run_command("logs", "mysql", "--follow")
end
Expand Down
6 changes: 3 additions & 3 deletions test/cli/app_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ class CliAppTest < CliTestCase

test "exec interactive" do
SSHKit::Backend::Abstract.any_instance.expects(:exec)
.with("ssh -t root@1.1.1.1 'docker run -it --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v'")
.with("ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v'")
run_command("exec", "-i", "ruby -v").tap do |output|
assert_match "Get most recent version available as an image...", output
assert_match "Launching interactive command with version latest via SSH from new container on 1.1.1.1...", output
Expand All @@ -181,7 +181,7 @@ class CliAppTest < CliTestCase

test "exec interactive with reuse" do
SSHKit::Backend::Abstract.any_instance.expects(:exec)
.with("ssh -t root@1.1.1.1 'docker exec -it app-web-999 ruby -v'")
.with("ssh -t root@1.1.1.1 -p 22 'docker exec -it app-web-999 ruby -v'")
run_command("exec", "-i", "--reuse", "ruby -v").tap do |output|
assert_match "Get current version of running container...", output
assert_match "Running docker ps --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest --format \"{{.Names}}\" | while read line; do echo ${line#app-web-}; done on 1.1.1.1", output
Expand Down Expand Up @@ -210,7 +210,7 @@ class CliAppTest < CliTestCase

test "logs with follow" do
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
.with("ssh -t root@1.1.1.1 'docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --timestamps --tail 10 --follow 2>&1'")
.with("ssh -t root@1.1.1.1 -p 22 'docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --timestamps --tail 10 --follow 2>&1'")

assert_match "docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --timestamps --tail 10 --follow 2>&1", run_command("logs", "--follow")
end
Expand Down
23 changes: 23 additions & 0 deletions test/cli/main_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,16 @@ class CliMainTest < CliTestCase
end
end

test "config with primary web role override" do
run_command("config", config_file: "deploy_primary_web_role_override").tap do |output|
config = YAML.load(output)

assert_equal ["web_chicago", "web_tokyo"], config[:roles]
assert_equal ["1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4"], config[:hosts]
assert_equal "1.1.1.3", config[:primary_host]
end
end

test "config with destination" do
run_command("config", "-d", "world", config_file: "deploy_for_dest").tap do |output|
config = YAML.load(output)
Expand All @@ -311,6 +321,19 @@ class CliMainTest < CliTestCase
end
end

test "config with aliases" do
run_command("config", config_file: "deploy_with_aliases").tap do |output|
config = YAML.load(output)

assert_equal ["web", "web_tokyo", "workers", "workers_tokyo"], config[:roles]
assert_equal ["1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4"], config[:hosts]
assert_equal "999", config[:version]
assert_equal "registry.digitalocean.com/dhh/app", config[:repository]
assert_equal "registry.digitalocean.com/dhh/app:999", config[:absolute_image]
assert_equal "app-999", config[:service_with_version]
end
end

test "init" do
Pathname.any_instance.expects(:exist?).returns(false).times(3)
Pathname.any_instance.stubs(:mkpath)
Expand Down
6 changes: 3 additions & 3 deletions test/cli/traefik_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class CliTraefikTest < CliTestCase
test "boot" do
run_command("boot").tap do |output|
assert_match "docker login", output
assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output
assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output
end
end

Expand All @@ -14,7 +14,7 @@ class CliTraefikTest < CliTestCase
run_command("reboot").tap do |output|
assert_match "docker container stop traefik", output
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik", output
assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output
assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output
end
end

Expand Down Expand Up @@ -64,7 +64,7 @@ class CliTraefikTest < CliTestCase

test "logs with follow" do
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
.with("ssh -t root@1.1.1.1 'docker logs traefik --timestamps --tail 10 --follow 2>&1'")
.with("ssh -t root@1.1.1.1 -p 22 'docker logs traefik --timestamps --tail 10 --follow 2>&1'")

assert_match "docker logs traefik --timestamps --tail 10 --follow", run_command("logs", "--follow")
end
Expand Down
24 changes: 24 additions & 0 deletions test/commander_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ class CommanderTest < ActiveSupport::TestCase

@kamal.specific_hosts = [ "1.1.1.1", "1.1.1.2" ]
assert_equal [ "1.1.1.1", "1.1.1.2" ], @kamal.hosts

@kamal.specific_hosts = [ "1.1.1.1*" ]
assert_equal [ "1.1.1.1" ], @kamal.hosts

@kamal.specific_hosts = [ "1.1.1.*", "*.1.2.*" ]
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], @kamal.hosts

@kamal.specific_hosts = [ "*" ]
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], @kamal.hosts

@kamal.specific_hosts = [ "*miss" ]
assert_equal [], @kamal.hosts
end

test "filtering hosts by filtering roles" do
Expand All @@ -28,6 +40,18 @@ class CommanderTest < ActiveSupport::TestCase

@kamal.specific_roles = [ "workers" ]
assert_equal [ "workers" ], @kamal.roles.map(&:name)

@kamal.specific_roles = [ "w*" ]
assert_equal [ "web", "workers" ], @kamal.roles.map(&:name)

@kamal.specific_roles = [ "we*", "*orkers" ]
assert_equal [ "web", "workers" ], @kamal.roles.map(&:name)

@kamal.specific_roles = [ "*" ]
assert_equal [ "web", "workers" ], @kamal.roles.map(&:name)

@kamal.specific_roles = [ "*miss" ]
assert_equal [], @kamal.roles.map(&:name)
end

test "filtering roles by filtering hosts" do
Expand Down
Loading

0 comments on commit f9214c9

Please sign in to comment.