{{- 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: {{ .Values.gc.schedule | quote }} 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 }}