diff --git a/lib/kamal/cli/app.rb b/lib/kamal/cli/app.rb index ed1870bae..39272c1fe 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -198,6 +198,33 @@ def stale_containers end end + desc "deleted_roles", "Detect app deleted roles" + option :stop, aliases: "-s", type: :boolean, default: false, desc: "Stop the deleted roles containers" + def deleted_roles + stop = options[:stop] + role_regex = /#{Regexp.escape(KAMAL.config.service)}-(.+)-#{Regexp.escape(KAMAL.config.destination.to_s)}/ + + with_lock_if_stopping do + on(KAMAL.app_hosts) do |host| + valid_roles = KAMAL.roles_on(host).map(&:name) + + app = KAMAL.app(host: host) + all_containers = capture_with_info(*app.all_containers, raise_on_non_zero_exit: false).split("\n") + all_containers.each do |container| + role = container.match(role_regex)[1] + next if valid_roles.include?(role) + + if stop + puts_by_host host, "Stopping stale container for deleted role #{role}" + execute :docker, :stop, container + else + puts_by_host host, "Detected container for deleted role #{role} (use `kamal app deleted_roles --stop` to stop)" + end + end + end + end + end + desc "images", "Show app images on servers" def images on(KAMAL.app_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.app.list_images) } diff --git a/lib/kamal/cli/main.rb b/lib/kamal/cli/main.rb index 4af6d8788..4b6cb0931 100644 --- a/lib/kamal/cli/main.rb +++ b/lib/kamal/cli/main.rb @@ -38,6 +38,7 @@ def deploy(boot_accessories: false) say "Detect stale containers...", :magenta invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true) + invoke "kamal:cli:app:deleted_roles", [], invoke_options.merge(stop: true) invoke "kamal:cli:app:boot", [], invoke_options diff --git a/lib/kamal/commands/app.rb b/lib/kamal/commands/app.rb index d80684fb0..8d4878473 100644 --- a/lib/kamal/commands/app.rb +++ b/lib/kamal/commands/app.rb @@ -75,6 +75,10 @@ def ensure_env_directory make_directory role.env_directory end + def all_containers + docker :ps, *image_filter_args, "--format", '"{{.Names}}"' + end + private def latest_image_id docker :image, :ls, *argumentize("--filter", "reference=#{config.latest_image}"), "--format", "'{{.ID}}'" diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index af673ce1a..ae7ff751a 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -264,6 +264,28 @@ class CliAppTest < CliTestCase end end + test "deleted_roles" do + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :ps, "--filter", "label=service=app", "--format", "\"{{.Names}}\"", raise_on_non_zero_exit: false) + .returns("app-deleted-role--version\napp-deleted-role-2--version\n") + + + run_command("deleted_roles").tap do |output| + assert_match /Detected container for deleted role deleted-role/, output + end + end + + test "stop deleted_roles" do + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :ps, "--filter", "label=service=app", "--format", "\"{{.Names}}\"", raise_on_non_zero_exit: false) + .returns("app-deleted-role--version\napp-deleted-role-2--version\n") + + + run_command("deleted_roles", "--stop").tap do |output| + assert_match /Stopping stale container for deleted role deleted-role/, output + end + end + test "details" do run_command("details").tap do |output| assert_match "docker ps --filter label=service=app --filter label=destination= --filter label=role=web", output diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index d37c9352b..869a90506 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -24,6 +24,7 @@ class CliMainTest < CliTestCase 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:app:stale_containers", [], invoke_options.merge(stop: true)) + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:deleted_roles", [], 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) @@ -46,6 +47,7 @@ class CliMainTest < CliTestCase 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:app:stale_containers", [], invoke_options.merge(stop: true)) + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:deleted_roles", [], 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) @@ -69,6 +71,7 @@ class CliMainTest < CliTestCase 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:app:stale_containers", [], invoke_options.merge(stop: true)) + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:deleted_roles", [], 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) @@ -167,6 +170,7 @@ class CliMainTest < CliTestCase 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:app:stale_containers", [], invoke_options.merge(stop: true)) + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:deleted_roles", [], 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) @@ -181,6 +185,7 @@ class CliMainTest < CliTestCase 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:app:stale_containers", [], invoke_options.merge(stop: true)) + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:deleted_roles", [], 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)