From 6892abb4be2ebf7fd43bfea13856d09f82dc4511 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Mon, 15 Jan 2024 14:17:43 +0000 Subject: [PATCH] Config the number of containers to keep By default we keep 5 containers around for rollback. The containers don't take much space, but the images for them can. Make the number of containers to retain configurable, either in the config with the `retain_containers` setting on the command line with the `--retain` option. --- lib/kamal/cli/prune.rb | 8 ++++++-- lib/kamal/commands/prune.rb | 4 ++-- lib/kamal/configuration.rb | 12 +++++++++++- test/cli/prune_test.rb | 9 +++++++++ test/commands/prune_test.rb | 6 +++++- test/configuration_test.rb | 10 +++++++++- 6 files changed, 42 insertions(+), 7 deletions(-) diff --git a/lib/kamal/cli/prune.rb b/lib/kamal/cli/prune.rb index 236c7d55..498e4ec4 100644 --- a/lib/kamal/cli/prune.rb +++ b/lib/kamal/cli/prune.rb @@ -18,12 +18,16 @@ class Kamal::Cli::Prune < Kamal::Cli::Base end end - desc "containers", "Prune all stopped containers, except the last 5" + desc "containers", "Prune all stopped containers, except the last n (default 5)" + option :retain, type: :numeric, default: nil, desc: "Number of containers to retain" def containers + retain = options.fetch(:retain, KAMAL.config.retain_containers) + raise "retain must be at least 1" if retain < 1 + mutating do on(KAMAL.hosts) do execute *KAMAL.auditor.record("Pruned containers"), verbosity: :debug - execute *KAMAL.prune.app_containers + execute *KAMAL.prune.app_containers(retain: retain) execute *KAMAL.prune.healthcheck_containers end end diff --git a/lib/kamal/commands/prune.rb b/lib/kamal/commands/prune.rb index f9f37b24..40f7dfc4 100644 --- a/lib/kamal/commands/prune.rb +++ b/lib/kamal/commands/prune.rb @@ -13,10 +13,10 @@ class Kamal::Commands::Prune < Kamal::Commands::Base "while read image tag; do docker rmi $tag; done" end - def app_containers(keep_last: 5) + def app_containers(retain:) pipe \ docker(:ps, "-q", "-a", *service_filter, *stopped_containers_filters), - "tail -n +#{keep_last + 1}", + "tail -n +#{retain + 1}", "while read container_id; do docker rm $container_id; done" end diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index e5ecbb53..a3b074d5 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -127,6 +127,10 @@ class Kamal::Configuration raw_config.require_destination end + def retain_containers + raw_config.retain_containers || 5 + end + def volume_args if raw_config.volumes.present? @@ -218,7 +222,7 @@ class Kamal::Configuration def valid? - ensure_destination_if_required && ensure_required_keys_present && ensure_valid_kamal_version + ensure_destination_if_required && ensure_required_keys_present && ensure_valid_kamal_version && ensure_retain_containers_valid end def to_h @@ -291,6 +295,12 @@ class Kamal::Configuration true end + def ensure_retain_containers_valid + raise ArgumentError, "Must retain at least 1 container" if retain_containers < 1 + + true + end + def role_names raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort diff --git a/test/cli/prune_test.rb b/test/cli/prune_test.rb index 77c3ebd4..11acc469 100644 --- a/test/cli/prune_test.rb +++ b/test/cli/prune_test.rb @@ -20,6 +20,15 @@ class CliPruneTest < CliTestCase assert_match /docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +6 | while read container_id; do docker rm $container_id; done on 1.1.1.\d/, output assert_match /docker container prune --force --filter label=service=healthcheck-app on 1.1.1.\d/, output end + + run_command("containers", "--retain", "10").tap do |output| + assert_match /docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +11 | while read container_id; do docker rm $container_id; done on 1.1.1.\d/, output + assert_match /docker container prune --force --filter label=service=healthcheck-app on 1.1.1.\d/, output + end + + assert_raises(RuntimeError, "retain must be at least 1") do + run_command("containers", "--retain", "0") + end end private diff --git a/test/commands/prune_test.rb b/test/commands/prune_test.rb index 00db4ccc..c4a56a9a 100644 --- a/test/commands/prune_test.rb +++ b/test/commands/prune_test.rb @@ -23,7 +23,11 @@ class CommandsPruneTest < ActiveSupport::TestCase test "app containers" do assert_equal \ "docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +6 | while read container_id; do docker rm $container_id; done", - new_command.app_containers.join(" ") + new_command.app_containers(retain: 5).join(" ") + + assert_equal \ + "docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +4 | while read container_id; do docker rm $container_id; done", + new_command.app_containers(retain: 3).join(" ") end test "healthcheck containers" do diff --git a/test/configuration_test.rb b/test/configuration_test.rb index 392b6afb..fbc87a91 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -299,7 +299,7 @@ class ConfigurationTest < ActiveSupport::TestCase 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).primary? assert config.role(:alternate_web).running_traefik? end @@ -309,4 +309,12 @@ class ConfigurationTest < ActiveSupport::TestCase end assert_match /bar isn't defined/, error.message end + + test "retain_containers" do + assert_equal 5, @config.retain_containers + config = Kamal::Configuration.new(@deploy_with_roles.merge(retain_containers: 2)) + assert_equal 2, config.retain_containers + + assert_raises(ArgumentError) { Kamal::Configuration.new(@deploy_with_roles.merge(retain_containers: 0)) } + end end