- GC CronJob runs 2x daily (02:00 & 05:00 UTC) instead of once (03:00) - Delete incomplete actcache uploads immediately (tmp/ dirs from failed writes) - Reduce actcache retention from 3 days to 1 day - Prevents cargo cache backlog on rust runner under heavy commit load - Incomplete entries were accumulating 900MB+ each, filling 1Gi PVC instantly
153 lines
6.6 KiB
YAML
153 lines
6.6 KiB
YAML
{{- if .Values.gc.enabled }}
|
|
# Garbage-collects DinD Docker images/volumes/build-cache and actcache across
|
|
# ALL forgejo-runner pods. Prevents PVC fill-up that breaks CI runs.
|
|
# Only rendered once (enable in default values.yaml, disable in per-runner overrides).
|
|
apiVersion: v1
|
|
kind: ServiceAccount
|
|
metadata:
|
|
name: runner-gc
|
|
namespace: {{ .Release.Namespace }}
|
|
---
|
|
apiVersion: rbac.authorization.k8s.io/v1
|
|
kind: Role
|
|
metadata:
|
|
name: runner-gc
|
|
namespace: {{ .Release.Namespace }}
|
|
rules:
|
|
- apiGroups: [""]
|
|
resources: ["pods"]
|
|
verbs: ["get", "list"]
|
|
- apiGroups: [""]
|
|
resources: ["pods/exec"]
|
|
verbs: ["create"]
|
|
---
|
|
apiVersion: rbac.authorization.k8s.io/v1
|
|
kind: RoleBinding
|
|
metadata:
|
|
name: runner-gc
|
|
namespace: {{ .Release.Namespace }}
|
|
subjects:
|
|
- kind: ServiceAccount
|
|
name: runner-gc
|
|
namespace: {{ .Release.Namespace }}
|
|
roleRef:
|
|
kind: Role
|
|
name: runner-gc
|
|
apiGroup: rbac.authorization.k8s.io
|
|
---
|
|
apiVersion: batch/v1
|
|
kind: CronJob
|
|
metadata:
|
|
name: forgejo-runner-gc
|
|
namespace: {{ .Release.Namespace }}
|
|
labels:
|
|
app: forgejo-runner-gc
|
|
spec:
|
|
schedule: "0 2,5 * * *" # Run at 02:00 and 05:00 UTC (more frequent for heavy build workloads)
|
|
concurrencyPolicy: Forbid
|
|
successfulJobsHistoryLimit: 3
|
|
failedJobsHistoryLimit: 3
|
|
jobTemplate:
|
|
spec:
|
|
backoffLimit: 1
|
|
activeDeadlineSeconds: 900
|
|
template:
|
|
spec:
|
|
serviceAccountName: runner-gc
|
|
restartPolicy: Never
|
|
tolerations:
|
|
- key: node-role.kubernetes.io/control-plane
|
|
operator: Exists
|
|
effect: NoSchedule
|
|
containers:
|
|
- name: gc
|
|
image: {{ .Values.gc.image }}
|
|
command:
|
|
- sh
|
|
- -c
|
|
- |
|
|
set -e
|
|
|
|
# Iterate all forgejo-runner pods (golang, rust, node)
|
|
PODS=$(kubectl -n {{ .Release.Namespace }} get pod \
|
|
-l app -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.labels.app}{"\n"}{end}' \
|
|
| grep 'forgejo-runner-' | awk '{print $1}')
|
|
|
|
if [ -z "$PODS" ]; then
|
|
echo "no forgejo-runner pods found, skipping"
|
|
exit 0
|
|
fi
|
|
|
|
for POD in $PODS; do
|
|
echo "===== $POD ====="
|
|
|
|
# 1. Docker image prune (DinD sidecar)
|
|
echo "[docker] before:"
|
|
kubectl -n {{ .Release.Namespace }} exec "$POD" -c dind -- docker system df 2>/dev/null || true
|
|
|
|
echo "[docker] pruning non-latest images older than {{ .Values.gc.pruneAge }}..."
|
|
# Keep :latest tagged images, delete all others older than pruneAge.
|
|
# docker image prune can't filter by tag, so we list and selectively rmi.
|
|
kubectl -n {{ .Release.Namespace }} exec "$POD" -c dind -- \
|
|
sh -c '
|
|
# Remove dangling (untagged) images older than {{ .Values.gc.pruneAge }}
|
|
docker image prune -f --filter "until={{ .Values.gc.pruneAge }}" 2>/dev/null
|
|
|
|
# Remove tagged non-latest images older than {{ .Values.gc.pruneAge }}
|
|
CUTOFF=$(date -d "-{{ .Values.gc.pruneAgeHours }} hours" +%s 2>/dev/null || date -v-{{ .Values.gc.pruneAgeHours }}H +%s)
|
|
docker images --format "{{"{{"}} .Repository {{"}}"}}:{{"{{"}} .Tag {{"}}"}} {{"{{"}} .CreatedAt {{"}}"}}" | while read -r IMAGE_TAG CREATED_REST; do
|
|
TAG=$(echo "$IMAGE_TAG" | rev | cut -d: -f1 | rev)
|
|
# Skip latest-tagged images
|
|
if [ "$TAG" = "latest" ]; then
|
|
echo "[keep] $IMAGE_TAG (latest)"
|
|
continue
|
|
fi
|
|
# Check image age via inspect
|
|
CREATED_TS=$(docker inspect --format="{{"{{"}} .Created {{"}}"}}" "$IMAGE_TAG" 2>/dev/null | head -1)
|
|
if [ -z "$CREATED_TS" ]; then continue; fi
|
|
IMAGE_EPOCH=$(date -d "$CREATED_TS" +%s 2>/dev/null || date -jf "%Y-%m-%dT%H:%M:%S" "$(echo $CREATED_TS | cut -dT -f1-2 | cut -d. -f1)" +%s 2>/dev/null || echo 0)
|
|
if [ "$IMAGE_EPOCH" -lt "$CUTOFF" ] 2>/dev/null; then
|
|
echo "[delete] $IMAGE_TAG (older than {{ .Values.gc.pruneAge }})"
|
|
docker rmi -f "$IMAGE_TAG" 2>/dev/null || true
|
|
else
|
|
echo "[keep] $IMAGE_TAG (recent)"
|
|
fi
|
|
done
|
|
' 2>/dev/null || true
|
|
|
|
echo "[docker] pruning build cache unused >{{ .Values.gc.pruneAge }}..."
|
|
kubectl -n {{ .Release.Namespace }} exec "$POD" -c dind -- \
|
|
docker builder prune -af --filter "until={{ .Values.gc.pruneAge }}" 2>/dev/null || true
|
|
|
|
echo "[docker] pruning dangling volumes..."
|
|
kubectl -n {{ .Release.Namespace }} exec "$POD" -c dind -- \
|
|
docker volume prune -af 2>/dev/null || true
|
|
|
|
echo "[docker] after:"
|
|
kubectl -n {{ .Release.Namespace }} exec "$POD" -c dind -- docker system df 2>/dev/null || true
|
|
|
|
# 2. Actcache cleanup (runner container)
|
|
echo "[actcache] cleaning incomplete and stale cache entries..."
|
|
kubectl -n {{ .Release.Namespace }} exec "$POD" -c runner -- \
|
|
sh -c '
|
|
# Delete incomplete/partial cache uploads immediately (tmp dirs)
|
|
find /data/.cache/actcache/cache -name "tmp" -type d -exec rm -rf {} + 2>/dev/null || true
|
|
# Delete cache entries not accessed in last {{ .Values.gc.actcacheMaxAgeDays }} day(s)
|
|
find /data/.cache/actcache/cache -type f -mtime +{{ .Values.gc.actcacheMaxAgeDays }} -delete 2>/dev/null || true
|
|
# Clean up empty directories
|
|
find /data/.cache/actcache/cache -type d -empty -delete 2>/dev/null || true
|
|
echo "actcache size: $(du -sh /data/.cache/actcache/cache 2>/dev/null | cut -f1)"
|
|
' || true
|
|
|
|
echo ""
|
|
done
|
|
echo "GC complete"
|
|
resources:
|
|
requests:
|
|
cpu: 50m
|
|
memory: 64Mi
|
|
limits:
|
|
cpu: 250m
|
|
memory: 128Mi
|
|
{{- end }}
|