Compare commits

..

9 Commits

Author SHA1 Message Date
Matthew Kent
5b694a0814 Support passing a short-sha version for rollback when using git.
Only for emergencies.
2023-11-21 15:45:51 -08:00
Donal McBreen
635876bdb9 Merge pull request #523 from rmacklin/fix-error-message-in-pre-build-sample-hook
Fix duplicate error message in pre-build.sample
2023-11-16 08:51:22 +00:00
Donal McBreen
11521517fa Merge pull request #550 from dmitrytrager/feature-name-all-for-accessory-reboot
feature: add NAME=all option for accessory reboot
2023-11-16 08:50:51 +00:00
Donal McBreen
610d9de3fd Merge pull request #580 from happyscribe/feat/no-web
Allow Kamal to run without traefik
2023-11-16 08:44:45 +00:00
Yoel Cabo
7472e5dfa6 Merge remote-tracking branch 'origin/main' into feat/no-web 2023-11-14 12:11:18 +01:00
Yoel Cabo
887b7dd46d Do not invoke healthcheck on deploy when no web role 2023-11-14 11:34:32 +01:00
Yoel Cabo
87cb8c1f71 fix: allow configurations without web roles 2023-11-12 09:39:07 +01:00
dmitrytrager
2d22143a24 feature: add NAME=all option for accessory reboot 2023-10-31 00:13:45 +01:00
Richard Macklin
f96d071222 Fix copy-pasted error message in pre-build.sample
The "No git remote set" error message was appropriate for the previous
block (where it was presumably copy-pasted from), but in this line we
have failed the check that determines if we have a git branch checked
out, so we should output a corresponding error.
2023-10-08 15:14:40 -07:00
16 changed files with 122 additions and 57 deletions

View File

@@ -49,17 +49,21 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
end end
end end
desc "reboot [NAME]", "Reboot existing accessory on host (stop container, remove container, start new container)" desc "reboot [NAME]", "Reboot existing accessory on host (stop container, remove container, start new container; use NAME=all to boot all accessories)"
def reboot(name) def reboot(name)
mutating do mutating do
with_accessory(name) do |accessory| if name == "all"
on(accessory.hosts) do KAMAL.accessory_names.each { |accessory_name| reboot(accessory_name) }
execute *KAMAL.registry.login else
end with_accessory(name) do |accessory|
on(accessory.hosts) do
execute *KAMAL.registry.login
end
stop(name) stop(name)
remove_container(name) remove_container(name)
boot(name, login: false) boot(name, login: false)
end
end end
end end
end end

View File

@@ -3,6 +3,7 @@ class Kamal::Cli::Healthcheck < Kamal::Cli::Base
desc "perform", "Health check current app version" desc "perform", "Health check current app version"
def perform def perform
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 on(KAMAL.primary_host) do
begin begin
execute *KAMAL.healthcheck.run execute *KAMAL.healthcheck.run

View File

@@ -38,8 +38,10 @@ class Kamal::Cli::Main < Kamal::Cli::Base
say "Ensure Traefik is running...", :magenta say "Ensure Traefik is running...", :magenta
invoke "kamal:cli:traefik:boot", [], invoke_options invoke "kamal:cli:traefik:boot", [], invoke_options
say "Ensure app can pass healthcheck...", :magenta if KAMAL.config.role(KAMAL.config.primary_role).running_traefik?
invoke "kamal:cli:healthcheck:perform", [], invoke_options say "Ensure app can pass healthcheck...", :magenta
invoke "kamal:cli:healthcheck:perform", [], invoke_options
end
say "Detect stale containers...", :magenta say "Detect stale containers...", :magenta
invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true) invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
@@ -91,7 +93,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
mutating do mutating do
invoke_options = deploy_options invoke_options = deploy_options
KAMAL.config.version = version KAMAL.config.version = Kamal::Git.used? ? Kamal::Git.resolve_revision(version) : version
old_version = nil old_version = nil
if container_available?(version) if container_available?(version)

View File

@@ -84,13 +84,12 @@ registry:
# limit: 10 # Can also specify as a percentage of total hosts, such as "25%" # limit: 10 # Can also specify as a percentage of total hosts, such as "25%"
# wait: 2 # wait: 2
# Configure the role used to determine the primary_web_host. This host takes # Configure the role used to determine the primary_host. This host takes
# deploy locks, runs health checks during the deploy, and follow logs, etc. # deploy locks, runs health checks during the deploy, and follow logs, etc.
# This role should have traefik enabled.
# #
# Caution: there's no support for role renaming yet, so be careful to cleanup # Caution: there's no support for role renaming yet, so be careful to cleanup
# the previous role on the deployed hosts. # the previous role on the deployed hosts.
# primary_web_role: web # primary_role: web
# Controls if we abort when see a role with no hosts. Disabling this may be # Controls if we abort when see a role with no hosts. Disabling this may be
# useful for more complex deploy configurations. # useful for more complex deploy configurations.

View File

@@ -32,7 +32,7 @@ fi
current_branch=$(git branch --show-current) current_branch=$(git branch --show-current)
if [ -z "$current_branch" ]; then if [ -z "$current_branch" ]; then
echo "No git remote set, aborting..." >&2 echo "Not on a git branch, aborting..." >&2
exit 1 exit 1
fi fi

View File

@@ -24,7 +24,7 @@ class Kamal::Commander
attr_reader :specific_roles, :specific_hosts attr_reader :specific_roles, :specific_hosts
def specific_primary! def specific_primary!
self.specific_hosts = [ config.primary_web_host ] self.specific_hosts = [ config.primary_host ]
end end
def specific_roles=(role_names) def specific_roles=(role_names)
@@ -36,7 +36,7 @@ class Kamal::Commander
end end
def primary_host def primary_host
specific_hosts&.first || specific_roles&.first&.primary_host || config.primary_web_host specific_hosts&.first || specific_roles&.first&.primary_host || config.primary_host
end end
def primary_role def primary_role

View File

@@ -1,7 +1,7 @@
class Kamal::Commands::Healthcheck < Kamal::Commands::Base class Kamal::Commands::Healthcheck < Kamal::Commands::Base
def run def run
web = config.role(config.primary_web_role) primary = config.role(config.primary_role)
docker :run, docker :run,
"--detach", "--detach",
@@ -9,12 +9,12 @@ class Kamal::Commands::Healthcheck < Kamal::Commands::Base
"--publish", "#{exposed_port}:#{config.healthcheck["port"]}", "--publish", "#{exposed_port}:#{config.healthcheck["port"]}",
"--label", "service=#{config.healthcheck_service}", "--label", "service=#{config.healthcheck_service}",
"-e", "KAMAL_CONTAINER_NAME=\"#{config.healthcheck_service}\"", "-e", "KAMAL_CONTAINER_NAME=\"#{config.healthcheck_service}\"",
*web.env_args, *primary.env_args,
*web.health_check_args(cord: false), *primary.health_check_args(cord: false),
*config.volume_args, *config.volume_args,
*web.option_args, *primary.option_args,
config.absolute_image, config.absolute_image,
web.cmd primary.cmd
end end
def status def status

View File

@@ -91,8 +91,8 @@ class Kamal::Configuration
roles.flat_map(&:hosts).uniq roles.flat_map(&:hosts).uniq
end end
def primary_web_host def primary_host
role(primary_web_role)&.primary_host role(primary_role)&.primary_host
end end
def traefik_roles def traefik_roles
@@ -208,8 +208,8 @@ class Kamal::Configuration
raw_config.asset_path raw_config.asset_path
end end
def primary_web_role def primary_role
raw_config.primary_web_role || "web" raw_config.primary_role || "web"
end end
def allow_empty_roles? def allow_empty_roles?
@@ -225,7 +225,7 @@ class Kamal::Configuration
{ {
roles: role_names, roles: role_names,
hosts: all_hosts, hosts: all_hosts,
primary_host: primary_web_host, primary_host: primary_host,
version: version, version: version,
repository: repository, repository: repository,
absolute_image: absolute_image, absolute_image: absolute_image,
@@ -264,16 +264,12 @@ class Kamal::Configuration
raise ArgumentError, "You must specify a password for the registry in config/deploy.yml (or set the ENV variable if that's used)" raise ArgumentError, "You must specify a password for the registry in config/deploy.yml (or set the ENV variable if that's used)"
end end
unless role_names.include?(primary_web_role) unless role_names.include?(primary_role)
raise ArgumentError, "The primary_web_role #{primary_web_role} isn't defined" raise ArgumentError, "The primary_role #{primary_role} isn't defined"
end end
unless traefik_role_names.include?(primary_web_role) if role(primary_role).hosts.empty?
raise ArgumentError, "Role #{primary_web_role} needs to have traefik enabled" raise ArgumentError, "No servers specified for the #{primary_role} primary_role"
end
if role(primary_web_role).hosts.empty?
raise ArgumentError, "No servers specified for the #{primary_web_role} primary_web_role"
end end
unless allow_empty_roles? unless allow_empty_roles?

View File

@@ -93,7 +93,15 @@ class Kamal::Configuration::Role
def running_traefik? def running_traefik?
name.web? || specializations["traefik"] if specializations["traefik"].nil?
primary?
else
specializations["traefik"]
end
end
def primary?
@config.primary_role == name
end end

View File

@@ -13,6 +13,16 @@ module Kamal::Git
`git rev-parse HEAD`.strip `git rev-parse HEAD`.strip
end end
# Attempt to convert a short -> long git sha, or return the original
def resolve_revision(revision)
resolved_rev = `git rev-parse -q --verify #{revision}`.strip
if resolved_rev.empty?
revision
else
resolved_rev
end
end
def uncommitted_changes def uncommitted_changes
`git status --porcelain`.strip `git status --porcelain`.strip
end end

View File

@@ -48,6 +48,18 @@ class CliAccessoryTest < CliTestCase
run_command("reboot", "mysql") run_command("reboot", "mysql")
end end
test "reboot all" do
Kamal::Commands::Registry.any_instance.expects(:login).times(3)
Kamal::Cli::Accessory.any_instance.expects(:stop).with("mysql")
Kamal::Cli::Accessory.any_instance.expects(:remove_container).with("mysql")
Kamal::Cli::Accessory.any_instance.expects(:boot).with("mysql", login: false)
Kamal::Cli::Accessory.any_instance.expects(:stop).with("redis")
Kamal::Cli::Accessory.any_instance.expects(:remove_container).with("redis")
Kamal::Cli::Accessory.any_instance.expects(:boot).with("redis", login: false)
run_command("reboot", "all")
end
test "start" do test "start" do
assert_match "docker container start app-mysql", run_command("start", "mysql") assert_match "docker container start app-mysql", run_command("start", "mysql")
end end

View File

@@ -64,9 +64,19 @@ class CliHealthcheckTest < CliTestCase
end end
assert_match "container not ready (unhealthy)", exception.message assert_match "container not ready (unhealthy)", exception.message
end end
test "raises an exception if primary does not have traefik" do
SSHKit::Backend::Abstract.any_instance.expects(:execute).never
exception = assert_raises do
run_command("perform", config_file: "test/fixtures/deploy_workers_only.yml")
end
assert_equal "The primary host is not configured to run Traefik", exception.message
end
private private
def run_command(*command) def run_command(*command, config_file: "test/fixtures/deploy_with_accessories.yml")
stdouted { Kamal::Cli::Healthcheck.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) } stdouted { Kamal::Cli::Healthcheck.start([*command, "-c", config_file]) }
end end
end end

View File

@@ -122,6 +122,21 @@ class CliMainTest < CliTestCase
refute_match /Running the post-deploy hook.../, output refute_match /Running the post-deploy hook.../, output
end end
end end
test "deploy without healthcheck if primary host doesn't have traefik" do
invoke_options = { "config_file" => "test/fixtures/deploy_workers_only.yml", "version" => "999", "skip_hooks" => false }
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:healthcheck:perform", [], invoke_options).never
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:deliver", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:boot", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true))
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options)
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options)
run_command("deploy", config_file: "deploy_workers_only")
end
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 }

View File

@@ -58,9 +58,9 @@ class ConfigurationTest < ActiveSupport::TestCase
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3" ], @config_with_roles.all_hosts assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3" ], @config_with_roles.all_hosts
end end
test "primary web host" do test "primary host" do
assert_equal "1.1.1.1", @config.primary_web_host assert_equal "1.1.1.1", @config.primary_host
assert_equal "1.1.1.1", @config_with_roles.primary_web_host assert_equal "1.1.1.1", @config_with_roles.primary_host
end end
test "traefik hosts" do test "traefik hosts" do
@@ -289,27 +289,23 @@ class ConfigurationTest < ActiveSupport::TestCase
assert_equal "foo", Kamal::Configuration.new(@deploy.merge!(asset_path: "foo")).asset_path assert_equal "foo", Kamal::Configuration.new(@deploy.merge!(asset_path: "foo")).asset_path
end end
test "primary web role" do test "primary role" do
assert_equal "web", @config.primary_web_role assert_equal "web", @config.primary_role
config = Kamal::Configuration.new(@deploy_with_roles.deep_merge({ config = Kamal::Configuration.new(@deploy_with_roles.deep_merge({
servers: { "alternate_web" => { "hosts" => [ "1.1.1.4", "1.1.1.5" ] , "traefik" => true } }, servers: { "alternate_web" => { "hosts" => [ "1.1.1.4", "1.1.1.5" ] } },
primary_web_role: "alternate_web" } )) primary_role: "alternate_web" } ))
assert_equal "alternate_web", config.primary_web_role
assert_equal "1.1.1.4", config.primary_web_host assert_equal "alternate_web", config.primary_role
assert_equal "1.1.1.4", config.primary_host
assert config.role(:alternate_web).primary?
assert config.role(:alternate_web).running_traefik?
end end
test "primary web role no traefik" do test "primary role missing" do
error = assert_raises(ArgumentError) do error = assert_raises(ArgumentError) do
Kamal::Configuration.new(@deploy_with_roles.merge(primary_web_role: "workers")) Kamal::Configuration.new(@deploy.merge(primary_role: "bar"))
end
assert_match /workers needs to have traefik enabled/, error.message
end
test "primary web role missing" do
error = assert_raises(ArgumentError) do
Kamal::Configuration.new(@deploy.merge(primary_web_role: "bar"))
end end
assert_match /bar isn't defined/, error.message assert_match /bar isn't defined/, error.message
end end

View File

@@ -17,4 +17,4 @@ registry:
server: registry.digitalocean.com server: registry.digitalocean.com
username: user username: user
password: pw password: pw
primary_web_role: web_tokyo primary_role: web_tokyo

12
test/fixtures/deploy_workers_only.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
service: app
image: dhh/app
servers:
workers:
traefik: false
hosts:
- 1.1.1.1
- 1.1.1.2
primary_role: workers
registry:
username: user
password: pw