Set-OPS-Public/roles/serveur_forgejo/tasks/main.yml
Daniel Allaire 8a9da0c31a restauration : la reconstruction remet l'etat de l'incarnation precedente
Chaque role proprietaire (AC, bases, annuaire, Nextcloud, rspamd/DKIM,
courriel, forge, web) remet son etat au moment ou il le creerait neuf,
depuis le dernier instantane anterieur a la naissance de la machine.
La sauvegarde refuse de deposer tant qu'un etat d'avant attend.
Outil de noeud setops-restaurer ; cibles sauvegarder-maintenant,
restauration-etat, restauration-renoncer ; test_restauration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 02:39:22 -04:00

624 lines
27 KiB
YAML

---
# L'URL PUBLIQUE de l'IdP se derive de l'exposition declaree au plan, elle ne se
# fabrique pas : `keycloak.<domaine>` n'est publie nulle part (voir resoudre_idp).
#
# ON NE RESOUT PAS UN FOURNISSEUR QU'ON N'UTILISERA PAS (2026-08-25).
#
# Cette resolution partait INCONDITIONNELLEMENT, y compris quand OIDC est desactive.
# `resoudre_idp` lit les registres du plan (`applications.yml`, `domaines.yml`) et exige
# que l'IdP y declare une exposition. Sur une forge SANS Keycloak — celle du site, qui
# porte le genome et n'a pas d'annuaire — il n'y a ni Keycloak ni plan : le role echouait
# sur `'setops_plan_dir' is undefined`, un message qui ne dit rien de la vraie cause.
#
# `serveur_forgejo_oidc_actif` derive deja de la presence du groupe `serveur_keycloak`.
# Il suffisait de s'en servir ici aussi : la resolution suit l'usage.
- name: Resoudre le fournisseur d'identite (role partage)
ansible.builtin.include_role:
name: resoudre_idp
vars:
resoudre_idp_realm: "{{ serveur_forgejo_oidc_realm }}"
when: serveur_forgejo_oidc_actif | bool
- name: Refuser de fermer la connexion locale sur un Forgejo anterieur a la v10
# `ENABLE_INTERNAL_SIGNIN` a ete ajoute en v10 (suivi amont, issue 7476). Sur une
# version anterieure il est ignore EN SILENCE : le role croirait avoir ferme la porte
# alors qu'elle reste ouverte. On refuse plutot que de laisser croire.
ansible.builtin.assert:
that:
- serveur_forgejo_version is version('10.0.0', '>=')
fail_msg: >-
serveur_forgejo_connexion_locale=false exige Forgejo >= 10.0.0
(ici {{ serveur_forgejo_version }}) : le reglage serait ignore sans erreur.
Mettre a jour, ou assumer la connexion locale en la passant a true.
when: not (serveur_forgejo_connexion_locale | bool)
- name: Exiger les secrets Forgejo (Vault)
ansible.builtin.assert:
that:
- serveur_forgejo_secret_key | length > 0
- serveur_forgejo_internal_token | length > 0
- serveur_forgejo_admin_password | length > 0
fail_msg: "secret_key, internal_token et admin_password sont requis (Ansible Vault)."
- name: Refuser une base de donnees inconnue
# Une valeur mal orthographiee ne doit pas retomber en silence sur le defaut : elle
# ferait deployer PostgreSQL a qui croyait choisir SQLite, et l'ecart se lirait au
# premier demarrage, pas ici.
ansible.builtin.assert:
that:
- serveur_forgejo_bd in ['postgres', 'sqlite']
fail_msg: >-
serveur_forgejo_bd={{ serveur_forgejo_bd }} : valeurs acceptees `postgres` ou
`sqlite`.
- name: Resoudre la base de donnees depuis le registre (role partage)
ansible.builtin.include_role:
name: resoudre_base
vars:
resoudre_base_groupe: "{{ serveur_forgejo_groupe }}"
when: serveur_forgejo_bd == 'postgres'
- name: Adopter les facts de base pour Forgejo
ansible.builtin.set_fact:
serveur_forgejo_entree: "{{ resoudre_base_entree }}"
serveur_forgejo_db_password: "{{ resoudre_base_db_password }}"
serveur_forgejo_db_host: "{{ resoudre_base_db_host }}"
serveur_forgejo_db_port: "{{ resoudre_base_db_port }}"
no_log: true
when: serveur_forgejo_bd == 'postgres'
- name: Creer l'utilisateur systeme git
ansible.builtin.user:
name: "{{ serveur_forgejo_utilisateur }}"
system: true
shell: /bin/bash
home: "/home/{{ serveur_forgejo_utilisateur }}"
create_home: true
comment: "Git Version Control"
# Le chemin de destination PORTE la version : `forgejo-10.0.0` ne peut pas designer un
# autre contenu demain. Le binaire est donc immuable une fois pose, et le retelecharger
# n'a aucun sens — or `get_url` sans garde recontacte le serveur distant a CHAQUE
# deploiement, pour 106 Mo deja presents.
#
# Constate le 2026-08-09 pendant le passage d'idempotence : « Connection failure: The
# read operation timed out ». Un deploiement de flotte echouait parce qu'un serveur tiers
# etait lent — sur une plateforme qui se veut souveraine, c'est une dependance de trop
# sur le chemin critique.
# LE ROLE QUI CREE LE GROUPE REVENDIQUE LA CLE (2026-09-12).
#
# `client_pki` (couche `pki_client`) pose les droits de la cle d'hote pour le groupe qui
# doit la lire — ici `git`. Mais ce groupe est cree JUSTE AU-DESSUS, dans une couche
# POSTERIEURE. Au premier jour d'un site, l'ordre se mord la queue :
#
# client_pki a besoin du groupe git <- cree par ce role
# forgejo a besoin de la cle lisible <- posee par client_pki
#
# Mesure sur deux reconstructions completes : le passage 1 laisse la cle en `root:root`,
# Forgejo ne demarre pas, et l'attente du premier demarrage coute 300 SECONDES pour rien.
# Il fallait un passage entier de plus.
#
# AUCUN ORDRE DE COUCHES NE DENOUE CE CYCLE — les certificats doivent venir tot, tout le
# monde en depend. Ce qui se deplace, c'est la PROPRIETE DU GESTE : le role qui cree le
# groupe est celui qui peut, a cet instant, donner l'acces.
#
# `client_pki` garde la sienne et la REPOSE a chaque passage — `step` reecrit la cle a
# chaque renouvellement, et ne regler les droits qu'ici les perdrait des le premier.
# Les deux taches se recouvrent, et c'est voulu : l'une amorce, l'autre entretient.
- name: Donner acces a la cle d'hote au groupe git (amorcage)
ansible.builtin.file:
path: "{{ client_pki_cle | default('/etc/step/certs/' ~ (ansible_fqdn | default(ansible_hostname)) ~ '.key') }}"
owner: root
group: "{{ serveur_forgejo_utilisateur }}"
mode: "{{ client_pki_cle_mode | default('0640') }}"
failed_when: false # pas encore de certificat : `client_pki` passera apres
- name: Le binaire de cette version est-il deja pose ?
ansible.builtin.stat:
path: "/usr/local/bin/forgejo-{{ serveur_forgejo_version }}"
register: serveur_forgejo_binaire_present
# Le controleur telecharge, puis pousse par SSH — la cible ne tire jamais d'Internet.
# Motif explique en detail dans `roles/serveur_nextcloud/tasks/installer.yml`. C'est
# precisement ce binaire qui a fait tomber un deploiement de flotte le 2026-08-09, sur un
# « read operation timed out » venu d'un serveur tiers.
- name: Cache d artefacts du contrôleur
ansible.builtin.file:
path: "{{ serveur_forgejo_cache_local }}"
state: directory
mode: "0700"
delegate_to: localhost
become: false
when: not serveur_forgejo_binaire_present.stat.exists
# --- LE DEPOT DU SITE D'ABORD, L'INTERNET ENSUITE (2026-09-12) ----------------
#
# Le site tient ces artefacts dans son cache (`serveur_artefacts`, LocalDirs). Un runner
# qui les y trouve ne sort pas du tout : c'est le meme principe que le cache apt, applique
# aux publications qui ne vivent dans aucun depot apt.
#
# PLACEE AVANT LE `stat` DU CACHE DU CONTROLEUR, ET C'EST TOUT LE MONTAGE : si le depot
# sert le fichier, le `stat` qui suit le voit, et la tache de telechargement amont se
# saute d'elle-meme. Aucune tache existante n'a change.
#
# `failed_when: false` — le depot est une COMMODITE. Site sans depot, depot qui n'a pas
# encore cette version, cache du site tout neuf : on retombe sur l'amont, comme avant.
- name: Prendre le binaire Forgejo au dépôt du site, avant de sortir sur Internet
ansible.builtin.get_url:
url: "{{ setops_depot_binaires }}/forgejo-{{ serveur_forgejo_version }}"
dest: "{{ serveur_forgejo_cache_local }}/forgejo-{{ serveur_forgejo_version }}"
mode: "0644"
timeout: 30
delegate_to: localhost
become: false
failed_when: false
when:
- setops_depot_binaires | default('') | length > 0
- not serveur_forgejo_binaire_present.stat.exists
- name: L artefact est-il déjà dans le cache du contrôleur ?
ansible.builtin.stat:
path: "{{ serveur_forgejo_cache_local }}/forgejo-{{ serveur_forgejo_version }}"
register: serveur_forgejo_cache_present
delegate_to: localhost
become: false
when: not serveur_forgejo_binaire_present.stat.exists
# Un serveur tiers intermittent ne doit pas arreter un deploiement de quarante minutes
# (mesure du 2026-08-23 : la meme URL pend, puis rend 200 en 0,48 s au second essai).
# Defaut de `get_url` : 10 s et aucune reprise.
- name: Télécharger le binaire Forgejo dans le cache du contrôleur (une seule fois)
ansible.builtin.get_url:
url: "{{ serveur_forgejo_url }}"
dest: "{{ serveur_forgejo_cache_local }}/forgejo-{{ serveur_forgejo_version }}"
mode: "0644"
timeout: 30
delegate_to: localhost
become: false
when:
- not serveur_forgejo_binaire_present.stat.exists
- not (serveur_forgejo_cache_present.stat.exists | default(false))
register: serveur_forgejo_bin
retries: 5
delay: 6
until: serveur_forgejo_bin is succeeded
# VERIFIER LA SIGNATURE AVANT DE POSER UN BINAIRE EXECUTABLE SUR UN HOTE.
#
# C'est un binaire, pas une archive de donnees : ce qu'on depose ici s'execute en tant que
# service. La verification tourne sur le CONTROLEUR, contre la cle versionnee du depot et
# l'empreinte PRIMAIRE epinglee — voir `defaults/main.yml` pour la raison de la primaire.
# Un serveur tiers intermittent ne doit pas arreter un deploiement de quarante minutes
# (mesure du 2026-08-23 : la meme URL pend, puis rend 200 en 0,48 s au second essai).
# Defaut de `get_url` : 10 s et aucune reprise.
# LA SIGNATURE AUSSI VIENT DU DEPOT QUAND IL L'A (2026-09-12). Elle pese 228 octets,
# mais elle est demandee a CHAQUE passage ou la cible n'a pas encore le binaire : sans
# elle ici, un ecosysteme entierement servi par le site sortirait quand meme sur Internet
# pour un quart de kilo-octet. Une sortie reste une sortie.
- name: Prendre la signature au dépôt du site, avant de sortir sur Internet
ansible.builtin.get_url:
url: "{{ setops_depot_binaires }}/forgejo-{{ serveur_forgejo_version }}.asc"
dest: "{{ serveur_forgejo_cache_local }}/forgejo-{{ serveur_forgejo_version }}.asc"
mode: "0644"
timeout: 30
force: true
delegate_to: localhost
become: false
failed_when: false
when:
- setops_depot_binaires | default('') | length > 0
- not serveur_forgejo_binaire_present.stat.exists
# ON MESURE LE FICHIER, PAS LE VERDICT DE LA TACHE. `failed_when: false` REECRIT le
# verdict : la tache n'est plus jamais `failed`, donc une garde posee dessus est toujours
# vraie et ne garde rien. Le depot connait cette marche — elle a deja declare « joignable »
# un amont mesure muet la seconde d'avant. Ce qui compte ici, c'est qu'un fichier soit la.
- name: La signature est-elle arrivée dans le cache du contrôleur ?
ansible.builtin.stat:
path: "{{ serveur_forgejo_cache_local }}/forgejo-{{ serveur_forgejo_version }}.asc"
register: serveur_forgejo_asc_present
delegate_to: localhost
become: false
- name: Récupérer la signature détachée du binaire
ansible.builtin.get_url:
url: "{{ serveur_forgejo_signature_url }}"
dest: "{{ serveur_forgejo_cache_local }}/forgejo-{{ serveur_forgejo_version }}.asc"
mode: "0644"
timeout: 30
force: true
delegate_to: localhost
become: false
when:
- not serveur_forgejo_binaire_present.stat.exists
# Le depot a servi : ne pas ressortir pour ecraser ce qu'on a deja.
- not (serveur_forgejo_asc_present.stat.exists and (serveur_forgejo_asc_present.stat.size | default(0)) > 0)
register: serveur_forgejo_sig
retries: 5
delay: 6
until: serveur_forgejo_sig is succeeded
- name: Vérifier la signature du binaire contre l empreinte épinglée
ansible.builtin.command:
argv:
- python3
- "{{ role_path }}/../../scripts/verifier_signature.py"
- --artefact
- "{{ serveur_forgejo_cache_local }}/forgejo-{{ serveur_forgejo_version }}"
- --signature
- "{{ serveur_forgejo_cache_local }}/forgejo-{{ serveur_forgejo_version }}.asc"
- --cle
- "{{ role_path }}/files/forgejo-release.asc"
- --empreinte
- "{{ serveur_forgejo_signature_empreinte }}"
delegate_to: localhost
become: false
changed_when: false
when: not serveur_forgejo_binaire_present.stat.exists
- name: Déposer le binaire Forgejo sur l hôte depuis le cache
ansible.builtin.copy:
src: "{{ serveur_forgejo_cache_local }}/forgejo-{{ serveur_forgejo_version }}"
dest: "/usr/local/bin/forgejo-{{ serveur_forgejo_version }}"
owner: root
group: root
mode: "0755"
when: not serveur_forgejo_binaire_present.stat.exists
- name: Activer la version courante du binaire
ansible.builtin.file:
src: "/usr/local/bin/forgejo-{{ serveur_forgejo_version }}"
dest: "{{ serveur_forgejo_binaire }}"
state: link
notify: Redemarrer forgejo
# LES DEPOTS D'UNE INCARNATION PRECEDENTE (2026-09-30). Avant que le role ne cree
# l'arborescence : ensuite, le repertoire ne serait plus vierge. La base, elle, a deja
# ete remise par `serveur_postgresql` (ou vit dans ces fichiers, en SQLite). La
# configuration revient avec le secret JWT que le role relit plus bas.
- name: Restauration — les donnees de Forgejo sont-elles vierges ?
ansible.builtin.find:
paths: "{{ serveur_forgejo_data }}"
file_type: any
register: serveur_forgejo_data_initial
- name: Restauration — serveur_forgejo d'une incarnation precedente ?
ansible.builtin.include_role:
name: client_backup
tasks_from: restaurer.yml
vars:
client_backup_restaurer_jeu: serveur_forgejo
client_backup_restaurer_vierge: "{{ serveur_forgejo_data_initial.matched | default(1) == 0 }}"
- name: Restauration — remettre serveur_forgejo
changed_when: true
ansible.builtin.command:
argv: >-
{{ ['/usr/local/sbin/setops-restaurer', 'fichiers',
'--instantane', client_backup_restauration.instantane]
+ client_backup_restauration.chemins }}
when: client_backup_restauration.etat == 'a_restaurer'
# restic rend les proprietaires NUMERIQUES ; `git` n'a pas forcement le meme uid que sur
# la machine d'avant.
- name: Restauration — rendre les donnees au service
ansible.builtin.file:
path: "{{ item.chemin }}"
owner: "{{ item.proprietaire }}"
group: "{{ serveur_forgejo_utilisateur }}"
recurse: true
loop:
- { chemin: "{{ serveur_forgejo_data }}", proprietaire: "{{ serveur_forgejo_utilisateur }}" }
- { chemin: "{{ serveur_forgejo_config_dir }}", proprietaire: root }
when: client_backup_restauration.etat == 'a_restaurer'
- name: Restauration — acter serveur_forgejo
ansible.builtin.include_role:
name: client_backup
tasks_from: acter.yml
vars:
client_backup_acter_etat: restaure
when: client_backup_restauration.etat == 'a_restaurer'
- name: Creer les repertoires de donnees
ansible.builtin.file:
path: "{{ serveur_forgejo_data }}/{{ item }}"
state: directory
owner: "{{ serveur_forgejo_utilisateur }}"
group: "{{ serveur_forgejo_utilisateur }}"
mode: "0750"
loop:
- ""
- custom
- data
- log
- name: Creer le repertoire de configuration
ansible.builtin.file:
path: "{{ serveur_forgejo_config_dir }}"
state: directory
owner: root
group: "{{ serveur_forgejo_utilisateur }}"
mode: "0770"
# JWT_SECRET appartient au SERVICE, pas au depot : Forgejo le genere au premier
# demarrage et le persiste dans `app.ini`. Le gabarit ne le portait pas — donc chaque
# rendu l'EFFACAIT, Forgejo en generait un nouveau au redemarrage, et le passage suivant
# recommencait. Autrement dit : chaque deploiement faisait tourner le secret JWT de la
# forge, invalidant les jetons qu'elle avait emis. Trouve par le passage d'idempotence du
# 2026-08-09 — seule ligne differente entre deux rendus.
#
# On le RELIT donc avant de rendre, et on le repose tel quel. Le depot cesse de disputer
# au service une valeur qui lui appartient.
- name: Relire le secret JWT genere par Forgejo (lui appartient)
ansible.builtin.slurp:
src: "{{ serveur_forgejo_config }}"
register: serveur_forgejo_app_ini
failed_when: false
no_log: true
- name: Retenir le secret JWT (vide au premier deploiement)
ansible.builtin.set_fact:
# Sans groupe de capture : on selectionne la ligne, puis on retire le prefixe. La
# forme avec `regex_search(..., '\\1', multiline=True)` fonctionnait pourtant en
# isolation — mais elle echouait sur l'hote, et `no_log` (indispensable ici, c'est un
# secret) empechait de voir pourquoi. Une expression qu'on ne peut pas diagnostiquer
# en place doit ceder a une expression plus simple.
serveur_forgejo_jwt_secret: >-
{{ (serveur_forgejo_app_ini.content | default('') | b64decode).splitlines()
| select('match', '^JWT_SECRET *=')
| map('regex_replace', '^JWT_SECRET *= *', '')
| first | default('', true) | trim }}
no_log: true
- name: Deployer app.ini
ansible.builtin.template:
src: app.ini.j2
dest: "{{ serveur_forgejo_config }}"
owner: "{{ serveur_forgejo_utilisateur }}" # Forgejo persiste des secrets générés (oauth2 JWT) → doit pouvoir écrire
group: "{{ serveur_forgejo_utilisateur }}"
# 0600, et non 0640 : Forgejo REECRIT ce fichier lui-meme (il y persiste des secrets
# generes) et le repose systematiquement en 0600. Le role remettait 0640 a chaque
# passage, Forgejo le ramenait a 0600 — deux proprietaires pour un fichier, en
# desaccord, et un `changed` perpetuel qui redemarrait le service pour rien.
# Trouve par le passage d'idempotence du 2026-08-09 : contenu identique, diff VIDE,
# seul le mode differait — c'est ce qui rendait la cause invisible.
#
# Le desaccord n'avait aucun effet utile : le groupe est `git`, c'est-a-dire le
# service lui-meme. On s'aligne sur le plus strict, qui est aussi le sien.
mode: "0600"
no_log: true
notify: Redemarrer forgejo
- name: Deployer l'identite visuelle (logo, favicon, accent aurore, accueil)
ansible.builtin.copy:
src: custom/
dest: "{{ serveur_forgejo_data }}/custom/"
owner: "{{ serveur_forgejo_utilisateur }}"
group: "{{ serveur_forgejo_utilisateur }}"
mode: "0644"
directory_mode: "0755"
when: serveur_forgejo_branding | bool
notify: Redemarrer forgejo
- name: Deployer l'unite systemd forgejo
ansible.builtin.template:
src: forgejo.service.j2
dest: /etc/systemd/system/forgejo.service
owner: root
group: root
mode: "0644"
notify: Redemarrer forgejo
- name: Activer et demarrer Forgejo
when: not ansible_check_mode
ansible.builtin.systemd:
name: "{{ serveur_forgejo_service }}"
enabled: true
state: started
daemon_reload: true
# LAISSER LE PREMIER DEMARRAGE FINIR AVANT DE L'INTERROMPRE (2026-09-02).
#
# Au TOUT PREMIER demarrage, Forgejo cree son schema et deroule ses migrations — plusieurs
# dizaines de secondes. Le `flush_handlers` ci-dessous le REDEMARRE parce qu'`app.ini`
# vient de changer. Redemarre au milieu, il laisse une base a moitie faite : la version
# cible est deja inscrite dans `version`, le schema ne l'est pas.
#
# Il ne s'en releve jamais seul. La migration suivante cherche une table qui n'a jamais
# ete creee et echoue en boucle :
#
# migration[v14a_add-foreign-keys-collaboration] ... failure to delete inconsistent
# records before foreign key sync: la relation « collaboration » n'existe pas
#
# ORM engine initialization attempt #1/10, #2/10... indefiniment. Le service reste
# `active` — il n'a pas plante, il reessaie — et n'ecoute jamais son port.
#
# CE QUI RENDAIT CE DEFAUT INVISIBLE : aux deploiements SUIVANTS la base est deja migree,
# le premier demarrage est instantane, et le redemarrage ne tombe au milieu de rien. Il
# ne se manifeste que sur une base VIERGE — donc a la reconstruction, et pas a chaque
# fois : c'est une course. Il a fallu deux reconstructions completes pour le voir.
- name: Attendre la fin du PREMIER demarrage avant tout redemarrage
ansible.builtin.wait_for:
host: 127.0.0.1
port: "{{ serveur_forgejo_http_port }}"
timeout: 300
when: not ansible_check_mode
- name: Appliquer les redemarrages Forgejo (config a jour) avant admin/OAuth2
ansible.builtin.meta: flush_handlers
- name: Attendre que Forgejo reponde (apres redemarrage)
ansible.builtin.wait_for:
host: 127.0.0.1
port: "{{ serveur_forgejo_http_port }}"
timeout: 120
when: not ansible_check_mode
- name: Creer le compte administrateur (une fois)
# `--must-change-password=false` : SANS LUI, LE COMPTE EST INUTILISABLE (2026-08-23,
# premiere forge d'un ecosysteme). Forgejo exige par defaut un changement de mot de passe
# au premier acces, et refuse TOUTE requete d'API tant qu'il n'a pas eu lieu :
# « You must change your password ». Or ce role desactive la connexion locale
# (`serveur_forgejo_connexion_locale: false`, SSO d'abord) — il n'existait donc aucun
# chemin pour effectuer ce changement. Le compte de secours ne secourait rien.
#
# Le mot de passe vient de la voute et tourne par empreinte (voir plus bas) : exiger un
# changement manuel en plus ferait diverger la voute de la realite, ce que ce role
# s'emploie justement a empecher.
ansible.builtin.shell:
cmd: >-
{{ serveur_forgejo_binaire }} admin user create
--admin --username {{ serveur_forgejo_admin_user }}
--email {{ serveur_forgejo_admin_email }}
--password '{{ serveur_forgejo_admin_password }}'
--must-change-password=false
--config {{ serveur_forgejo_config }}
&& touch {{ serveur_forgejo_data }}/.admin-created
creates: "{{ serveur_forgejo_data }}/.admin-created"
become: true
become_user: "{{ serveur_forgejo_utilisateur }}"
environment:
GITEA_WORK_DIR: "{{ serveur_forgejo_data }}"
no_log: true
# ON VERIFIE L'ETAT, ON NE FAIT PAS CONFIANCE AU DRAPEAU (2026-08-25).
#
# La creation passe deja `--must-change-password=false` — corrige le 2026-08-23, sur la
# premiere forge d'un ecosysteme. **Forgejo 16 l'a ignore** : le compte de la forge du SITE
# est ne avec le drapeau POSE malgre l'option.
#
# La panne qui en resulte n'accuse rien de juste : toute requete d'API rend **403**, pas
# 401. Le jeton est valide, l'identite reconnue, la requete rejetee quand meme — on
# cherche un scope, un droit, une politique, et la cause est un drapeau sur le compte.
#
# D'ou cette tache : elle ne demande pas a la creation de bien se comporter, elle CONSTATE
# l'etat voulu et le retablit. Idempotente, et sans effet quand la creation a tenu parole.
- name: Le compte d'administration ne doit exiger aucun changement de mot de passe
ansible.builtin.command:
cmd: >-
{{ serveur_forgejo_binaire }} admin user must-change-password --unset
{{ serveur_forgejo_admin_user }}
--config {{ serveur_forgejo_config }}
become: true
become_user: "{{ serveur_forgejo_utilisateur }}"
environment:
FORGEJO_WORK_DIR: "{{ serveur_forgejo_data }}"
register: forgejo_mcp
changed_when: "'Updated 1 users' in (forgejo_mcp.stdout | default(''))"
failed_when:
- forgejo_mcp.rc != 0
- "'not found' not in (forgejo_mcp.stderr | default(''))"
- name: Configurer la source OAuth2 SSO (Keycloak, idempotent)
ansible.builtin.shell:
executable: /bin/bash
cmd: |
set -euo pipefail
FJ="{{ serveur_forgejo_binaire }} --config {{ serveur_forgejo_config }}"
# Les reglages de GROUPE sont RECONCILIES, pas seulement poses a la creation :
# `add-oauth` une fois puis plus rien laissait un cablage perime vivre
# indefiniment — c'est ce qui est arrive a la federation LDAP de Keycloak, qui
# a pointe un hote inexistant pendant des semaines (2026-08-07).
OPTS_GROUPE=""
{% if serveur_forgejo_oidc_groupe_admin | length > 0 %}
OPTS_GROUPE="--group-claim-name {{ serveur_forgejo_oidc_groupe_claim }} --admin-group {{ serveur_forgejo_oidc_groupe_admin }}"
{% endif %}
ID=$($FJ admin auth list 2>/dev/null \
| awk -v n="{{ serveur_forgejo_oidc_nom }}" '$2 == n {print $1}' | head -1)
if [ -z "$ID" ]; then
$FJ admin auth add-oauth \
--name "{{ serveur_forgejo_oidc_nom }}" \
--provider openidConnect \
--key "{{ serveur_forgejo_oidc_client_id }}" \
--secret "$OIDC_SECRET" \
--auto-discover-url "{{ serveur_forgejo_oidc_discovery }}" \
$OPTS_GROUPE >/dev/null
echo SETOPS_CHANGED
else
# `update-oauth` est idempotent cote Forgejo : il reecrit les memes valeurs
# sans effet de bord. On ne peut pas comparer avant/apres (la CLI n'expose
# pas le detail d'une source), donc on ne signale PAS `changed`.
# `--secret` fait partie de la reconciliation : sans lui, faire tourner
# `vault_forgejo_oidc` laissait Forgejo sur l'ancienne valeur.
$FJ admin auth update-oauth --id "$ID" \
--auto-discover-url "{{ serveur_forgejo_oidc_discovery }}" \
--secret "$OIDC_SECRET" \
$OPTS_GROUPE >/dev/null
echo SETOPS_OK
fi
environment:
OIDC_SECRET: "{{ serveur_forgejo_oidc_client_secret }}"
GITEA_WORK_DIR: "{{ serveur_forgejo_data }}"
become: true
become_user: "{{ serveur_forgejo_utilisateur }}"
register: serveur_forgejo_oauth
changed_when: "'SETOPS_CHANGED' in serveur_forgejo_oauth.stdout"
no_log: true
when:
- serveur_forgejo_oidc_actif | bool
- not ansible_check_mode
# --- ROTATION DU COMPTE DE SECOURS -------------------------------------------
# `admin user create` porte `creates: .admin-created` : il ne tourne qu'UNE fois.
# Changer la voute ne changeait donc rien, et la voute se mettait a mentir. Meme
# raisonnement que pour Grafana — idempotence par EMPREINTE du secret applique.
- name: Empreinte du mot de passe admin en place
ansible.builtin.slurp:
src: "{{ serveur_forgejo_marqueur_admin }}"
register: serveur_forgejo_marque
failed_when: false
changed_when: false
no_log: true
- name: Appliquer le mot de passe du compte de secours (rotation)
ansible.builtin.command:
argv:
- "{{ serveur_forgejo_binaire }}"
- "--config"
- "{{ serveur_forgejo_config }}"
- admin
- user
- change-password
- "--username"
- "{{ serveur_forgejo_admin_user }}"
- "--password"
- "{{ serveur_forgejo_admin_password }}"
environment:
GITEA_WORK_DIR: "{{ serveur_forgejo_data }}"
become: true
become_user: "{{ serveur_forgejo_utilisateur }}"
changed_when: true
no_log: true
when: >-
(serveur_forgejo_marque.content | default('') | b64decode | trim)
!= (serveur_forgejo_admin_password | hash('sha256'))
- name: Retenir l'empreinte appliquee
ansible.builtin.copy:
content: "{{ serveur_forgejo_admin_password | hash('sha256') }}\n"
dest: "{{ serveur_forgejo_marqueur_admin }}"
owner: root
group: root
mode: "0600"
no_log: true
# --- Sonde de supervision (docs/supervision-conception.md) --------------------------
# Le role qui possede la verite depose sa propre sonde ; le porteur (`client_sante`) la
# fait tourner et pousse le verdict, sans savoir ce qu'elle mesure.
- name: Assurer le repertoire des sondes de supervision
ansible.builtin.file:
path: /usr/local/lib/setops/sondes
state: directory
owner: root
group: root
mode: "0755"
- name: Deposer la sonde « forge »
ansible.builtin.template:
src: sonde-forge.sh.j2
dest: /usr/local/lib/setops/sondes/forge.sh
owner: root
group: root
mode: "0750"