diff --git a/ansible/arcodange/factory/inventory/group_vars/gitea/gitea.yml b/ansible/arcodange/factory/inventory/group_vars/gitea/gitea.yml index 049950b..13cb5fb 100644 --- a/ansible/arcodange/factory/inventory/group_vars/gitea/gitea.yml +++ b/ansible/arcodange/factory/inventory/group_vars/gitea/gitea.yml @@ -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 \ No newline at end of file + 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 diff --git a/ansible/arcodange/factory/playbooks/03_cicd.yml b/ansible/arcodange/factory/playbooks/03_cicd.yml index 728e62f..78b30e7 100644 --- a/ansible/arcodange/factory/playbooks/03_cicd.yml +++ b/ansible/arcodange/factory/playbooks/03_cicd.yml @@ -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 diff --git a/ansible/arcodange/factory/roles/gitea_packages_retention/defaults/main.yml b/ansible/arcodange/factory/roles/gitea_packages_retention/defaults/main.yml new file mode 100644 index 0000000..1613e51 --- /dev/null +++ b/ansible/arcodange/factory/roles/gitea_packages_retention/defaults/main.yml @@ -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 « / » au lieu de « » +# 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 diff --git a/ansible/arcodange/factory/roles/gitea_packages_retention/files/regle.sql b/ansible/arcodange/factory/roles/gitea_packages_retention/files/regle.sql new file mode 100644 index 0000000..a682e03 --- /dev/null +++ b/ansible/arcodange/factory/roles/gitea_packages_retention/files/regle.sql @@ -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; diff --git a/ansible/arcodange/factory/roles/gitea_packages_retention/tasks/main.yml b/ansible/arcodange/factory/roles/gitea_packages_retention/tasks/main.yml new file mode 100644 index 0000000..ec39471 --- /dev/null +++ b/ansible/arcodange/factory/roles/gitea_packages_retention/tasks/main.yml @@ -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']