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
|
# 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
|
## 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
|
**17 tâches `changed` au second passage, contre 924 au rejeu depuis zéro.** La flotte
|
||||||
|
|
|
||||||
|
|
@ -102,13 +102,52 @@
|
||||||
group: "{{ serveur_forgejo_utilisateur }}"
|
group: "{{ serveur_forgejo_utilisateur }}"
|
||||||
mode: "0770"
|
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
|
- name: Deployer app.ini
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: app.ini.j2
|
src: app.ini.j2
|
||||||
dest: "{{ serveur_forgejo_config }}"
|
dest: "{{ serveur_forgejo_config }}"
|
||||||
owner: "{{ serveur_forgejo_utilisateur }}" # Forgejo persiste des secrets générés (oauth2 JWT) → doit pouvoir écrire
|
owner: "{{ serveur_forgejo_utilisateur }}" # Forgejo persiste des secrets générés (oauth2 JWT) → doit pouvoir écrire
|
||||||
group: "{{ serveur_forgejo_utilisateur }}"
|
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
|
no_log: true
|
||||||
notify: Redemarrer forgejo
|
notify: Redemarrer forgejo
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,3 +62,20 @@ DEFAULT_THEME = {{ serveur_forgejo_theme }}
|
||||||
|
|
||||||
[ui.meta]
|
[ui.meta]
|
||||||
DESCRIPTION = {{ serveur_forgejo_meta_description }}
|
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 }}
|
ca_file: {{ serveur_prometheus_metriques_ca }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
static_configs:
|
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)
|
| map('extract', hostvars)
|
||||||
| selectattr('ansible_host', 'defined')
|
| selectattr('ansible_host', 'defined')
|
||||||
| map(attribute='ansible_host')
|
| map(attribute='ansible_host')
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue