Skip to content

Commit fe095fd

Browse files
thardeckp-se
andauthored
Free disk space before the multi-cluster e2e job (#5645) (#5679)
e2e-fleet-mc-test creates three k3d clusters, which are six containers sharing the runner's root filesystem. Every kubelet measures that same disk, so they all declare DiskPressure within seconds of each other, and k3s evicts at nodefs.available<5%. Once the nodes carry the disk-pressure taint, helm upgrade --install --atomic --timeout=10m never gets its pods and the install fails for the full ten minutes, three attempts in a row. Runners are not the same on both sides: public CI gets 4 CPUs, 15.6 GiB of RAM and a 144 GB filesystem, a private repository gets 2 CPUs, 7.75 GiB and 72 GB. On the small runner the filesystem arrives 82-85% full, and the job spends about 11.5 GiB of what is left: 6.9 GB in /var/lib/docker, most of it the k3d node volumes that image import fills, plus 2.4 GB of Go caches. Instrumenting a run showed it finishing with 4.1 GiB available against a 3.58 GiB floor, passing by half a gigabyte, which is why it passed on one commit and died on the next. The android SDK, dotnet, ghcup, swift and the CodeQL toolcache come to 25.6 GB that Fleet's e2e never touches. Removing them takes six seconds and leaves the job finishing at 28.0 GiB instead of 4.1, moving it from 1.15x the eviction floor to 7.8x. Deploy Fleet, which had been timing out three times at ten minutes, completes in 39 seconds. Deleting system directories from a script that lives in a repository deserves care, so it refuses to run anywhere except an ephemeral GitHub-hosted runner, checking the Actions environment, GitHub's prebuilt image marker and the runner work directory. A workstation, a container or a persistent self-hosted machine gets a description of what would have been removed and nothing else. FLEET_FORCE_DISK_PRUNE=yes overrides that deliberately, DRY_RUN=1 lists the paths with their sizes, and each path is separately validated as an absolute, literal, at least three levels deep directory that is not a symlink and is not a system directory, so editing the list carelessly later cannot turn it into rm -rf /usr. Co-authored-by: Patrick Seidensal <9606095+p-se@users.noreply.github.com>
1 parent d079615 commit fe095fd

2 files changed

Lines changed: 111 additions & 0 deletions

File tree

.github/scripts/free-disk-space.sh

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/bin/bash
2+
3+
# Removes preinstalled tooling that Fleet's e2e jobs never touch.
4+
#
5+
# The multi-cluster job runs three k3d clusters as six containers sharing the
6+
# runner's root filesystem. On a private repository that filesystem is 72 GB
7+
# and arrives 82% full, leaving ~13.5 GiB for a job that consumes ~11.5 GiB, so
8+
# it finishes within half a gigabyte of the 5% threshold at which kubelet
9+
# declares DiskPressure, taints the nodes NoSchedule and evicts pods.
10+
#
11+
# Measured contents of these paths on ubuntu24/20260816.277, 25.6 GB in total.
12+
# Removal happens in parallel because the runner has two cores.
13+
#
14+
# THIS SCRIPT DELETES SYSTEM DIRECTORIES. It only does so on an ephemeral
15+
# GitHub-hosted runner, where the machine is discarded at the end of the job.
16+
# Anywhere else it refuses and exits without touching anything. Set
17+
# FLEET_FORCE_DISK_PRUNE=yes to override that, and DRY_RUN=1 to list what
18+
# would be removed.
19+
#
20+
# This script never fails the job.
21+
22+
set -u
23+
24+
PRUNE=(
25+
/usr/local/lib/android # 11.07 GB
26+
/usr/share/dotnet # 5.66 GB
27+
/usr/local/.ghcup # 3.74 GB
28+
/usr/share/swift # 3.37 GB
29+
/opt/hostedtoolcache/CodeQL # 1.73 GB
30+
)
31+
32+
# Paths that must never be handed to rm, whatever PRUNE grows into later.
33+
PROTECTED=(
34+
/ /bin /boot /dev /etc /home /lib /lib64 /opt /proc /root /run /sbin /srv
35+
/sys /tmp /usr /usr/bin /usr/lib /usr/local /usr/local/bin /usr/local/lib
36+
/usr/sbin /usr/share /var /opt/hostedtoolcache
37+
)
38+
39+
avail() { df -Pk / | awk 'NR==2 {print $4}'; }
40+
gib() { awk -v k="$1" 'BEGIN {printf "%.2f GiB", k/1048576}'; }
41+
42+
# An ephemeral GitHub-hosted runner, not someone's workstation and not a
43+
# persistent self-hosted machine. Every one of these is set by the Actions
44+
# runner itself; ImageOS exists only on GitHub's prebuilt images.
45+
on_disposable_runner() {
46+
[ "${GITHUB_ACTIONS:-}" = "true" ] &&
47+
[ "${RUNNER_ENVIRONMENT:-}" = "github-hosted" ] &&
48+
[ -n "${ImageOS:-}" ] &&
49+
[ -d /home/runner/work ]
50+
}
51+
52+
# Refuses anything that is not a deep, absolute, literal directory path.
53+
safe_to_remove() {
54+
local path="$1" protected
55+
56+
case "$path" in
57+
/*) ;;
58+
*) echo "refusing relative path: $path" >&2; return 1 ;;
59+
esac
60+
case "$path" in
61+
*..*|*"*"*|*"?"*) echo "refusing wildcard or traversal: $path" >&2; return 1 ;;
62+
esac
63+
# At least three components deep, so /usr or /opt/foo can never match.
64+
case "$path" in
65+
/*/*/*) ;;
66+
*) echo "refusing shallow path: $path" >&2; return 1 ;;
67+
esac
68+
for protected in "${PROTECTED[@]}"; do
69+
if [ "$path" = "$protected" ]; then
70+
echo "refusing protected path: $path" >&2
71+
return 1
72+
fi
73+
done
74+
[ -d "$path" ] || return 1
75+
[ -L "$path" ] && { echo "refusing symlink: $path" >&2; return 1; }
76+
return 0
77+
}
78+
79+
echo "::group::Free disk space"
80+
81+
if ! on_disposable_runner && [ "${FLEET_FORCE_DISK_PRUNE:-}" != "yes" ]; then
82+
echo "Not running on an ephemeral GitHub-hosted runner, skipping."
83+
echo "This script deletes system directories and would have removed:"
84+
printf ' %s\n' "${PRUNE[@]}"
85+
echo "Set FLEET_FORCE_DISK_PRUNE=yes if that is genuinely what you want."
86+
echo "::endgroup::"
87+
exit 0
88+
fi
89+
90+
before=$(avail)
91+
echo "available before: $(gib "$before")"
92+
93+
for path in "${PRUNE[@]}"; do
94+
safe_to_remove "$path" || continue
95+
if [ "${DRY_RUN:-}" = "1" ]; then
96+
echo "would remove $path ($(sudo du -sxm "$path" 2>/dev/null | cut -f1) MB)"
97+
continue
98+
fi
99+
echo "removing $path"
100+
sudo rm -rf "$path" &
101+
done
102+
wait
103+
104+
after=$(avail)
105+
echo "available after: $(gib "$after")"
106+
echo "reclaimed: $(gib $((after - before)))"
107+
df -h /
108+
echo "::endgroup::"

.github/workflows/e2e-multicluster-ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ jobs:
2929
-
3030
name: Install Ginkgo CLI
3131
run: go install github.com/onsi/ginkgo/v2/ginkgo
32+
-
33+
name: Free Disk Space
34+
run: ./.github/scripts/free-disk-space.sh
3235
-
3336
name: Cache crust-gather CLI
3437
id: cache-crust

0 commit comments

Comments
 (0)