idempotence : Prometheus trie ses cibles, Forgejo conserve son secret JWT
Prometheus : intersect rend un ENSEMBLE, dont l'ordre d'iteration n'est pas stable d'un processus a l'autre. Le fichier se rendait differemment a chaque passage — memes cibles, ordre different — et le service redemarrait pour rien. Trie sur les hotes. Forgejo : JWT_SECRET est genere par le service et ajoute par lui a la fin d'app.ini. Le gabarit ne le portait pas, donc chaque rendu l'EFFACAIT et Forgejo en generait un nouveau. Ce n'etait pas du bruit : chaque deploiement invalidait les jetons OAuth2 emis par la forge. Le role le relit et le repose ; meme empreinte avant/apres, changed=0 aux 2e et 3e passages. Trois erreurs de methode de ma part dans cette enquete : - conclu « diff vide donc contenu identique » alors que no_log masquait le diff - applique un replace sur le gabarit SANS verifier qu'il avait pris (la section [oauth2] n'existait pas), affiche un succes, et interprete trois passages sur cette base - garde une expression Jinja indiagnosticable en place a cause de no_log Verifier l'effet, pas l'intention — un replace qui ne trouve rien reussit silencieusement, exactement comme kcadm -s sur une map. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
fba0df26c3
commit
ffb515cf3c
4 changed files with 96 additions and 2 deletions
33
CHANGELOG.md
33
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
Loading…
Reference in a new issue