diff --git a/main.sh b/main.sh index 5b44aff..ac8fadf 100755 --- a/main.sh +++ b/main.sh @@ -1090,8 +1090,11 @@ prune() { #{{{ update_diagnostic "system.docker.reclaimed_containers_images" "$reclaimed_containers_images" update_diagnostic "system.docker.prune_filter" "$prune_filter" + # Volumes only. `system prune --volumes` also sweeps every non-running + # container regardless of age, deleting freshly created ECS task containers + # before the agent can start them. local reclaimed_volumes - reclaimed_volumes=$($CONTAINER_ORCHESTRATOR system prune --volumes -f | + reclaimed_volumes=$($CONTAINER_ORCHESTRATOR volume prune -f | cut -d: -f2 | tr -d ' ') update_diagnostic "system.docker.reclaimed_volumes" "$reclaimed_volumes" diff --git a/test/mocks/bin/docker b/test/mocks/bin/docker index 955fe1e..7a1d724 100755 --- a/test/mocks/bin/docker +++ b/test/mocks/bin/docker @@ -4,6 +4,7 @@ # ps -> $MOCK_DOCKER_PS_OUTPUT (default: empty) # inspect -> $MOCK_DOCKER_INSPECT_OUTPUT (default: healthy) # system -> $MOCK_DOCKER_SYSTEM_OUTPUT (default: "Total reclaimed space: 0B") +# volume -> $MOCK_DOCKER_VOLUME_OUTPUT (default: "Total reclaimed space: 0B") # Exit code override: $MOCK_DOCKER_EXIT (default: 0). printf '%s\n' "docker $*" >>"${MOCK_CALL_LOG}" @@ -18,6 +19,9 @@ case "$1" in system) echo "${MOCK_DOCKER_SYSTEM_OUTPUT:-Total reclaimed space: 0B}" ;; + volume) + echo "${MOCK_DOCKER_VOLUME_OUTPUT:-Total reclaimed space: 0B}" + ;; esac exit "${MOCK_DOCKER_EXIT:-0}" diff --git a/test/unit/prune.bats b/test/unit/prune.bats new file mode 100644 index 0000000..ee0b526 --- /dev/null +++ b/test/unit/prune.bats @@ -0,0 +1,65 @@ +#!/usr/bin/env bats +# +# Unit tests for prune(), which the `0 */4 * * *` crontab entry installed by +# setup_cron() invokes on every registered runner. +# +# The container/image sweep is age-guarded with --filter until=4h. The volume +# sweep must never be able to delete a container: an unfiltered +# `docker system prune` removes every non-running container, including one in +# `created` state, which races the ECS agent between docker create and docker +# start and fails the task with CannotStartContainerError / No such container. + +setup() { + load "${BATS_TEST_DIRNAME}/../helpers/load.bash" + load_main + CONTAINER_ORCHESTRATOR="docker" + init_diagnostic_dir +} + +# Every recorded `docker ... prune` invocation, one per line. +prune_calls() { + mock_calls docker | grep -F "prune" || true +} + +@test "prune age-guards the container and image sweep" { + run prune + assert_success + + assert_called docker "system prune -f --filter until=4h" +} + +@test "prune never issues a system prune without an age filter" { + run prune + assert_success + + local unguarded + unguarded="$(prune_calls | grep -F "system prune" | grep -vF -- "--filter" || true)" + assert_equal "$unguarded" "" +} + +@test "prune reclaims volumes without a container-deleting sweep" { + run prune + assert_success + + assert_called docker "volume prune" + + local container_deleting + container_deleting="$(prune_calls | grep -F "system prune --volumes" || true)" + assert_equal "$container_deleting" "" +} + +@test "prune records the filter it actually applied to the guarded sweep" { + run prune + assert_success + + assert_equal "$(jq -r '.system.docker.prune_filter' "$SG_DIAGNOSTIC_FILE")" "until=4h" +} + +@test "prune records reclaimed space for both sweeps" { + run prune + assert_success + + refute [ "$(jq -r '.system.docker.last_prune' "$SG_DIAGNOSTIC_FILE")" = "null" ] + refute [ "$(jq -r '.system.docker.reclaimed_containers_images' "$SG_DIAGNOSTIC_FILE")" = "null" ] + refute [ "$(jq -r '.system.docker.reclaimed_volumes' "$SG_DIAGNOSTIC_FILE")" = "null" ] +}