diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ab38d3..e2435bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,38 @@ # CHANGELOG — Set-OPS +## 2026-08-09 — Les deux dernières tâches non idempotentes, et ce qu'elles cachaient + +**Prometheus — une liste non ordonnée.** `intersect` rend un *ensemble*, dont l'ordre +d'itération n'est pas stable d'un processus Python à l'autre. Le fichier de configuration +se rendait donc différemment à chaque passage — mêmes quatorze cibles, ordre différent — et +Prometheus redémarrait pour rien. Trié sur les hôtes : ordre déterministe **et** lisible. +Second passage à `changed=0`. + +**Forgejo — le dépôt faisait tourner un secret du service.** `JWT_SECRET` est généré par +Forgejo au premier démarrage et ajouté par lui à la fin d'`app.ini`. Le gabarit ne le +portait pas : **chaque rendu l'effaçait**, Forgejo en générait un nouveau au redémarrage, +et le passage suivant recommençait. + +Ce n'était donc pas du bruit : **chaque déploiement invalidait les jetons OAuth2 émis par +la forge.** Le rôle relit maintenant le secret avant de rendre et le repose. Vérifié : même +empreinte avant et après un déploiement, `changed=0` aux deuxième et troisième passages. + +**Trois erreurs de méthode de ma part, dans cette seule enquête**, et elles méritent +d'être écrites : + +1. J'ai conclu « le diff est vide, donc le contenu est identique » — alors que `no_log` + masquait le diff. Toute mon hypothèse sur le mode `0640` reposait là-dessus. +2. J'ai appliqué un `str.replace` sur le gabarit **sans vérifier qu'il avait pris**. La + section `[oauth2]` n'existait pas : le remplacement n'a rien fait, j'ai affiché un + message de succès, et j'ai interprété trois passages d'essai sur cette base. +3. J'ai gardé une expression Jinja qui fonctionnait en isolation mais échouait sur l'hôte, + sans pouvoir la diagnostiquer parce que `no_log` — indispensable, c'est un secret — + masquait l'erreur. Remplacée par une forme plus simple. + +La leçon commune est celle de la journée, retournée contre moi : **vérifier l'effet, pas +l'intention.** Un `replace` qui ne trouve rien réussit silencieusement, exactement comme +`kcadm -s` sur une map. + ## 2026-08-09 — Le passage d'idempotence trouve une panne, pas une imperfection **17 tâches `changed` au second passage, contre 924 au rejeu depuis zéro.** La flotte diff --git a/roles/serveur_forgejo/tasks/main.yml b/roles/serveur_forgejo/tasks/main.yml index de40596..19df804 100644 --- a/roles/serveur_forgejo/tasks/main.yml +++ b/roles/serveur_forgejo/tasks/main.yml @@ -102,13 +102,52 @@ 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 }}" - mode: "0640" + # 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 diff --git a/roles/serveur_forgejo/templates/app.ini.j2 b/roles/serveur_forgejo/templates/app.ini.j2 index ea1d31b..0585299 100644 --- a/roles/serveur_forgejo/templates/app.ini.j2 +++ b/roles/serveur_forgejo/templates/app.ini.j2 @@ -62,3 +62,20 @@ DEFAULT_THEME = {{ serveur_forgejo_theme }} [ui.meta] DESCRIPTION = {{ serveur_forgejo_meta_description }} + +; --- Secret JWT : appartient au SERVICE, relu par le role ------------------------- +; Forgejo genere ce secret au premier demarrage et l'ajoute lui-meme a la fin du +; fichier. Le gabarit ne le portait pas : chaque rendu l'EFFACAIT donc, Forgejo en +; generait un nouveau au redemarrage, et le passage suivant recommencait — autrement dit, +; chaque deploiement faisait tourner le secret JWT de la forge et invalidait les jetons +; qu'elle avait emis. Trouve par le passage d'idempotence du 2026-08-09 : seule ligne +; differente entre deux rendus. +; +; Le role le RELIT avant de rendre (tache « Relire le secret JWT ») et le repose ici. +; Vide au premier deploiement : Forgejo le genere alors, et les rendus suivants le +; conservent. +[oauth2] +{% if serveur_forgejo_jwt_secret | default("") %} +JWT_SECRET = {{ serveur_forgejo_jwt_secret }} +{% endif %} + diff --git a/roles/serveur_prometheus/templates/prometheus.yml.j2 b/roles/serveur_prometheus/templates/prometheus.yml.j2 index e1fedcc..776af4b 100644 --- a/roles/serveur_prometheus/templates/prometheus.yml.j2 +++ b/roles/serveur_prometheus/templates/prometheus.yml.j2 @@ -14,7 +14,12 @@ scrape_configs: ca_file: {{ serveur_prometheus_metriques_ca }} {% endif %} static_configs: - - targets: {{ (groups.get(serveur_prometheus_groupe_metriques, []) | intersect(groups.get('hotes_actifs', []))) + {# `intersect` rend un ENSEMBLE : son ordre d'iteration n'est pas stable d'un + processus Python a l'autre. Sans `sort`, ce fichier se rendait differemment a + chaque passage — memes quatorze cibles, ordre different — et Prometheus etait + redemarre pour rien. Trouve par le passage d'idempotence du 2026-08-09. + On trie sur les HOTES : ordre deterministe et lisible par un humain. #} + - targets: {{ (groups.get(serveur_prometheus_groupe_metriques, []) | intersect(groups.get('hotes_actifs', [])) | sort) | map('extract', hostvars) | selectattr('ansible_host', 'defined') | map(attribute='ansible_host')