feat(cicd) — rétention du registre de paquets Gitea : 935 images kadans avaient rempli la carte SD de pi2 #60

Merged
arcodange merged 1 commits from arcodange/retention-paquets-gitea into main 2026-09-19 01:37:38 +02:00
5 changed files with 170 additions and 1 deletions
@@ -45,6 +45,14 @@ gitea:
GITEA__server__SSH_PORT: 2222
GITEA__server__SSH_DOMAIN: "{{ hostvars[groups.gitea[0]]['preferred_ip'] }}"
GITEA__server__SSH_LISTEN_PORT: 22
# Rétention des artefacts de CI (défaut Gitea : 90 jours). Les
# artefacts pesaient 3,3 Go sur la carte SD de pi2 le 2026-09-19.
GITEA__actions__ARTIFACT_RETENTION_DAYS: 14
# Déjà en service, mais posé À LA MAIN dans data/gitea/conf/app.ini :
# un job ATTRIBUÉ à un runner mais coincé derrière sa file (capacity 1)
# était tué au défaut de 10m — failure sans journal, ~650 s (mesuré les
# 30/07 et 03/08). 60m couvre une file de plusieurs jobs longs.
GITEA__actions__ZOMBIE_TASK_TIMEOUT: 60m
GITEA_server__DOMAIN: localhost
GITEA_server__HTTP_PORT: 3000
GITEA_server__ROOT_URL: https://gitea.arcodange.lab/
@@ -69,4 +77,27 @@ gitea:
resources:
limits:
cpus: "1.5"
memory: 1536M
memory: 1536M
# Rétention du registre de paquets — appliquée par le rôle
# arcodange.factory.gitea_packages_retention (playbook 03_cicd.yml), exécutée
# par Gitea chaque nuit (tâche `cleanup_packages`).
#
# Incident 2026-09-18 → 19 : la carte SD de pi2 (117 Go) à 94 %, DiskPressure,
# pgbouncer expulsé et non replanifiable (épinglé sur pi2), la LAPI CrowdSec en
# CrashLoop faute de base, le bouncer Traefik en erreur → kadans.arcodange.fr et
# lien.arcodange.fr en 404. Cause : le registre gardait 935 versions de l'image
# `arcodange/kadans` (61 Go), une par push de CI depuis le 14/07, ~0,9 Go/jour.
#
# ⚠ On ne vise QUE les empreintes `sha256:` sans étiquette des images kadans :
# jamais un nom d'étiquette. `postgres-postgis`, `*-build-cache`, `cms-deps`…
# portent des étiquettes qui servent de BASE à d'autres builds — une règle large
# les emporterait au premier passage. `latest` et les manifestes encore
# référencés par un index étiqueté sont sautés par Gitea lui-même.
# 7 jours = de quoi revenir à l'image d'avant (ArgoCD épingle un digest).
gitea_packages_retention_rules:
- owner: arcodange
type: container
keep_count: 20
remove_days: 7
remove_pattern: '^kadans(-[a-z-]+)?/sha256:[0-9a-f]{64}$'
match_full_name: true
@@ -208,3 +208,12 @@
loop: ["absent", "present"]
loop_control:
loop_var: docker_compose_down_then_up
# Rétention du registre de paquets (règles dans group_vars/gitea/gitea.yml).
# Sur l'hôte de Gitea, où vit aussi sa base : le rôle ne fait que DÉCLARER les
# règles, Gitea les applique chaque nuit — aucun redémarrage de la forge.
- name: Déclarer la rétention du registre de paquets Gitea
hosts: gitea
gather_facts: false
roles:
- arcodange.factory.gitea_packages_retention
@@ -0,0 +1,22 @@
---
# Règles de nettoyage du registre de paquets de Gitea, une par (propriétaire, type).
# Gitea les APPLIQUE lui-même, chaque nuit, dans sa tâche `cleanup_packages`
# (active par défaut, @midnight) — ce rôle ne supprime rien : il les DÉCLARE.
#
# Champs (mêmes noms que la table `package_cleanup_rule` et que le formulaire
# Réglages → Paquets → Règles de nettoyage) :
# owner : utilisateur ou organisation
# type : container, npm, generic…
# keep_count : les N versions les plus récentes de CHAQUE paquet sont gardées
# keep_pattern : regex (RE2, minuscules) — une version qui la vérifie est gardée
# remove_days : seules les versions plus vieilles que N jours partent
# remove_pattern : regex — SEULES les versions qui la vérifient peuvent partir
# match_full_name: les regex voient « <paquet>/<version> » au lieu de « <version> »
# Le moteur de Gitea saute de lui-même, pour les conteneurs, l'étiquette `latest`
# et tout manifeste encore RÉFÉRENCÉ par un autre (les enfants d'un index étiqueté).
gitea_packages_retention_rules: []
# Conteneur PostgreSQL qui porte la base de Gitea (docker compose, même hôte).
gitea_packages_retention_postgres_container: postgres
gitea_packages_retention_postgres_user: postgres
gitea_packages_retention_database: gitea
@@ -0,0 +1,35 @@
-- Déclare (ou met à jour) UNE règle de nettoyage. Variables psql :
-- owner type keep_count keep_pattern remove_days remove_pattern match_full_name
-- Rend « inchangee », « ecrite », ou « proprietaire-absent ».
\set ON_ERROR_STOP 1
with proprietaire as (
select id from "user" where lower_name = lower(:'owner')
), voulue as (
select p.id as owner_id, :'type'::varchar as type, true as enabled,
:'keep_count'::int as keep_count, :'keep_pattern'::varchar as keep_pattern,
:'remove_days'::int as remove_days, :'remove_pattern'::varchar as remove_pattern,
:'match_full_name'::boolean as match_full_name
from proprietaire p
), deja as (
select 1 from package_cleanup_rule r join voulue v using (owner_id, type)
where r.enabled and r.keep_count = v.keep_count and r.keep_pattern = v.keep_pattern
and r.remove_days = v.remove_days and r.remove_pattern = v.remove_pattern
and r.match_full_name = v.match_full_name
), ecrite as (
insert into package_cleanup_rule
(enabled, owner_id, type, keep_count, keep_pattern, remove_days, remove_pattern,
match_full_name, created_unix, updated_unix)
select enabled, owner_id, type, keep_count, keep_pattern, remove_days, remove_pattern,
match_full_name, extract(epoch from now())::bigint, extract(epoch from now())::bigint
from voulue where not exists (select 1 from deja)
on conflict (owner_id, type) do update set
enabled = excluded.enabled, keep_count = excluded.keep_count,
keep_pattern = excluded.keep_pattern, remove_days = excluded.remove_days,
remove_pattern = excluded.remove_pattern, match_full_name = excluded.match_full_name,
updated_unix = excluded.updated_unix
returning 1
)
select case
when not exists (select 1 from proprietaire) then 'proprietaire-absent'
when exists (select 1 from ecrite) then 'ecrite'
else 'inchangee' end;
@@ -0,0 +1,72 @@
---
# Pourquoi la BASE et pas l'API : Gitea 1.27 n'expose AUCUNE route pour les règles
# de nettoyage (le swagger n'a que /packages/{owner}/…). Le seul autre chemin est
# le formulaire web (session + CSRF). La table est petite et stable ; on vérifie
# quand même son schéma avant d'écrire, pour qu'une montée de Gitea qui la change
# fasse ÉCHOUER ce rôle au lieu d'écrire à côté.
- name: Vérifier le schéma de package_cleanup_rule
ansible.builtin.command:
argv:
- docker
- exec
- "{{ gitea_packages_retention_postgres_container }}"
- psql
- -U
- "{{ gitea_packages_retention_postgres_user }}"
- -d
- "{{ gitea_packages_retention_database }}"
- -At
- -c
- >-
select string_agg(column_name, ',' order by column_name)
from information_schema.columns where table_name = 'package_cleanup_rule'
register: gitea_packages_retention_schema
changed_when: false
- name: Exiger les colonnes attendues
ansible.builtin.assert:
that: >-
gitea_packages_retention_schema.stdout.split(',') | sort ==
['created_unix', 'enabled', 'id', 'keep_count', 'keep_pattern', 'match_full_name',
'owner_id', 'remove_days', 'remove_pattern', 'type', 'updated_unix']
fail_msg: >-
package_cleanup_rule a changé de forme ({{ gitea_packages_retention_schema.stdout }}) —
relire le modèle de Gitea avant de réécrire files/regle.sql.
- name: Déclarer les règles de nettoyage
ansible.builtin.command:
argv:
- docker
- exec
- -i
- "{{ gitea_packages_retention_postgres_container }}"
- psql
- -U
- "{{ gitea_packages_retention_postgres_user }}"
- -d
- "{{ gitea_packages_retention_database }}"
- -At
- -v
- "owner={{ regle.owner }}"
- -v
- "type={{ regle.type }}"
- -v
- "keep_count={{ regle.keep_count | default(0) | int }}"
- -v
- "keep_pattern={{ regle.keep_pattern | default('') }}"
- -v
- "remove_days={{ regle.remove_days | default(0) | int }}"
- -v
- "remove_pattern={{ regle.remove_pattern | default('') }}"
- -v
- "match_full_name={{ regle.match_full_name | default(false) | bool | lower }}"
stdin: "{{ lookup('ansible.builtin.file', 'regle.sql') }}"
loop: "{{ gitea_packages_retention_rules }}"
loop_control:
loop_var: regle
label: "{{ regle.owner }}/{{ regle.type }}"
register: gitea_packages_retention_ecriture
changed_when: gitea_packages_retention_ecriture.stdout == 'ecrite'
failed_when: >-
gitea_packages_retention_ecriture.rc != 0
or gitea_packages_retention_ecriture.stdout not in ['ecrite', 'inchangee']