Some checks are pending
verifier / verifier (push) Waiting to run
Decision de l'exploitant : la forge du SITE fait autorite pour le genome. Toute autre copie — y compris celle d'ou le moteur a ete pousse jusqu'ici — est un MIROIR. Un ecosysteme se reproduit depuis la forge de son site : c'est de la qu'il clone son moteur, ses plans, ses modeles. Si l'autorite est ailleurs, cette forge devient un cache qu'on croit a jour — et le 2026-08-26 elle etait quatre commits en arriere sans que rien ne le signale, dont le correctif qui desarme le pare-feu Proxmox. UNE AUTORITE QU'ON NE VERIFIE PAS EST UNE AUTORITE QU'ON SUPPOSE. `make genome-etat` confronte, depot par depot, ce que le poste porte a ce que la forge porte. Il REFUSE en cas d'ecart plutot que de le signaler : un ecart connu et tolere redevient un ecart oublie, et la commande qui le corrige tient en trois mots. Il dit aussi quand la copie locale n'est pas propre — des commits pas encore faits sont une autre forme de retard. Mesure au passage, et traitee plutot qu'ignoree : le premier contact avec la forge echoue une fois sur six — poignee TLS expiree, puis cinq reponses de suite. Ce n'est pas le chemin, qui est prouve ; c'est l'acceptation TLS apres un temps d'inactivite. Les deux cibles reessaient. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
270 lines
12 KiB
YAML
270 lines
12 KiB
YAML
---
|
|
# LE RUNNER DU SITE POUSSE LE GENOME SUR LA FORGE DU SITE.
|
|
#
|
|
# POURQUOI CE DETOUR, ET POURQUOI IL EST BON (2026-08-26).
|
|
#
|
|
# La forge du genome vit DANS le site, sur un reseau que le poste de l'exploitant ne route
|
|
# pas — et c'est voulu : le poste n'a de patte que sur l'administration, la frontiere
|
|
# police le reste. Il ne peut donc pas pousser lui-meme.
|
|
#
|
|
# On ne perce pas un chemin pour lui. Le RUNNER existe exactement pour ca : il est dans le
|
|
# site, il a le genome, il a une identite. On lui PORTE les commits dans un `git bundle` —
|
|
# un fichier, verifiable, qui ne demande aucun reseau — et c'est LUI qui pousse.
|
|
#
|
|
# Mesure du 2026-08-26 : la forge du site etait restee quatre commits en arriere du poste,
|
|
# sans que rien ne le signale. Le runner clonait donc un moteur perime — dont, ce jour-la,
|
|
# le correctif qui desarme le pare-feu Proxmox. Un ecosysteme qui se reproduit depuis une
|
|
# forge en retard reproduit ses defauts.
|
|
#
|
|
# CE QUI EST POUSSE SE DERIVE, il ne s'ecrit pas : `serveur_ops_depots` (au plan du site)
|
|
# dit ce que le runner porte. On pousse ceux dont une copie locale existe a cote du
|
|
# moteur, et rien d'autre.
|
|
#
|
|
# make genome-pousser tous les depots portes par le runner
|
|
# make genome-pousser DEPOT=<nom> un seul (le nom du depot sur la forge)
|
|
#
|
|
# L'ECRITURE S'AUTHENTIFIE, LA LECTURE NON. `serveur_ops` clone en anonyme, par doctrine.
|
|
# Pousser est un autre acte : le runner s'y presente avec SA cle — celle que `serveur_ops`
|
|
# lui a generee — declaree en cle de depot, en ecriture, sur ces depots-la seulement.
|
|
# `url` reste en HTTPS anonyme ; seul `pushurl` passe par SSH.
|
|
|
|
- name: Le runner du site pousse le genome sur la forge du site
|
|
hosts: serveur_ops_site
|
|
become: true
|
|
become_user: "{{ serveur_ops_utilisateur | default('setops') }}"
|
|
gather_facts: false
|
|
|
|
vars:
|
|
genome_racine: "{{ serveur_ops_racine | default('/opt/setops') }}"
|
|
genome_depot_demande: "{{ lookup('env', 'SETOPS_DEPOT') | default('', true) }}"
|
|
# Le dossier qui contient le moteur ET ses depots freres, sur le CONTROLEUR.
|
|
genome_freres: "{{ playbook_dir | dirname | dirname | dirname }}"
|
|
# Le dossier du moteur, tel que le plan le nomme — jamais ecrit en dur.
|
|
genome_moteur: "{{ (serveur_ops_depots | selectattr('role', 'eq', 'moteur') | first).dest }}"
|
|
|
|
tasks:
|
|
- name: Exiger de savoir quoi pousser
|
|
ansible.builtin.assert:
|
|
that:
|
|
- (serveur_ops_depots | default([])) | length > 0
|
|
fail_msg: >-
|
|
Ce poste ne déclare aucun dépôt (`serveur_ops_depots`). Rien à pousser — et
|
|
probablement rien à piloter non plus : voir le plan de cette machine.
|
|
|
|
# Ce que le runner porte, restreint a ce dont une copie existe sur le controleur.
|
|
# Un depot declare mais absent d'ici n'est pas une faute : il vit ailleurs.
|
|
- name: Voir lesquels de ces dépôts existent sur le contrôleur
|
|
ansible.builtin.stat:
|
|
path: "{{ genome_freres }}/{{ item.dest }}/.git"
|
|
delegate_to: localhost
|
|
become: false
|
|
loop: "{{ serveur_ops_depots }}"
|
|
loop_control:
|
|
label: "{{ item.dest }}"
|
|
register: genome_locaux
|
|
|
|
- name: Retenir ceux qu'on peut effectivement porter
|
|
ansible.builtin.set_fact:
|
|
genome_a_pousser: >-
|
|
{{ genome_locaux.results
|
|
| selectattr('stat.exists')
|
|
| map(attribute='item') | list
|
|
| selectattr('depot', 'equalto', genome_depot_demande)
|
|
| list
|
|
if genome_depot_demande | length > 0
|
|
else genome_locaux.results | selectattr('stat.exists')
|
|
| map(attribute='item') | list }}
|
|
|
|
- name: Dire ce qui va être poussé
|
|
ansible.builtin.debug:
|
|
msg: >-
|
|
{{ genome_a_pousser | map(attribute='depot') | list
|
|
| ternary(genome_a_pousser | map(attribute='depot') | list,
|
|
'aucun dépôt à pousser') }}
|
|
|
|
- name: Refuser un DEPOT= qui ne désigne rien
|
|
ansible.builtin.assert:
|
|
that: genome_a_pousser | length > 0
|
|
fail_msg: >-
|
|
Aucun dépôt à pousser. {{ 'Le dépôt `' ~ genome_depot_demande ~ '` n est pas
|
|
porté par ce runner, ou n a pas de copie locale à côté du moteur.'
|
|
if genome_depot_demande | length > 0 else
|
|
'Aucun des dépôts déclarés n a de copie locale à côté du moteur.' }}
|
|
|
|
# LA CLE DU RUNNER DOIT ETRE AUTORISEE SUR CE QU'IL POUSSE — sinon la forge refuse,
|
|
# depot par depot, et l'exploitant doit aller cliquer. La cible s'en charge : la
|
|
# declaration est idempotente (422 = deja la), et sa portee est exactement ce qu'on
|
|
# pousse, jamais plus large.
|
|
- name: Lire la clé publique du runner
|
|
ansible.builtin.slurp:
|
|
src: "{{ genome_racine }}/.ssh/id_{{ serveur_ops_cle_type | default('ed25519') }}.pub"
|
|
register: genome_cle
|
|
|
|
- name: Autoriser cette clé en écriture sur les dépôts poussés
|
|
ansible.builtin.uri:
|
|
url: >-
|
|
https://forge.{{ domaine_interne }}/api/v1/repos/{{ serveur_ops_forge_organisation
|
|
| default('genome') }}/{{ item.depot }}/keys
|
|
method: POST
|
|
url_username: forgejo-admin
|
|
url_password: "{{ vault_forgejo_admin }}"
|
|
force_basic_auth: true
|
|
validate_certs: false
|
|
body_format: json
|
|
body:
|
|
title: "runner du site — {{ inventory_hostname }}"
|
|
key: "{{ genome_cle.content | b64decode | trim }}"
|
|
read_only: false
|
|
status_code: [201, 422]
|
|
loop: "{{ genome_a_pousser }}"
|
|
loop_control:
|
|
label: "{{ item.depot }}"
|
|
register: genome_cles
|
|
changed_when: false
|
|
no_log: true
|
|
|
|
- name: Un endroit pour fabriquer les colis, sur le contrôleur
|
|
ansible.builtin.tempfile:
|
|
state: directory
|
|
suffix: -genome
|
|
delegate_to: localhost
|
|
become: false
|
|
register: genome_atelier
|
|
|
|
- name: Un endroit pour déposer les colis
|
|
ansible.builtin.file:
|
|
path: "{{ genome_racine }}/.bundles"
|
|
state: directory
|
|
owner: "{{ serveur_ops_utilisateur | default('setops') }}"
|
|
group: "{{ serveur_ops_utilisateur | default('setops') }}"
|
|
mode: "0700"
|
|
|
|
# LA LOGIQUE GIT VIT DANS `scripts/genome_colis.py`, comme partout ici : le playbook
|
|
# orchestre, le script fait. Il voyage AVEC le moteur — le runner en a donc une copie
|
|
# dès qu'il l'a cloné, et rien de spécial n'est déposé sur la machine.
|
|
- name: Emballer chaque dépôt sur le contrôleur
|
|
# `argv` ET NON `cmd` : le dossier des dépôts frères peut contenir des espaces —
|
|
# c'est le cas ici (« Espace Chezlepro »). Une chaîne `cmd` est découpée sur les
|
|
# blancs, et python recevait « /home/…/Espace » comme nom de fichier.
|
|
ansible.builtin.command:
|
|
argv:
|
|
- python3
|
|
- "{{ genome_freres }}/{{ genome_moteur }}/scripts/genome_colis.py"
|
|
- emballer
|
|
- "{{ genome_freres }}/{{ item.dest }}"
|
|
- "{{ genome_atelier.path }}/{{ item.depot }}.bundle"
|
|
- --branche
|
|
- "{{ item.branche | default('main') }}"
|
|
delegate_to: localhost
|
|
become: false
|
|
loop: "{{ genome_a_pousser }}"
|
|
loop_control:
|
|
label: "{{ item.dest }}"
|
|
changed_when: true
|
|
|
|
- name: Porter les colis jusqu'au runner
|
|
ansible.builtin.copy:
|
|
src: "{{ genome_atelier.path }}/{{ item.depot }}.bundle"
|
|
dest: "{{ genome_racine }}/.bundles/{{ item.depot }}.bundle"
|
|
owner: "{{ serveur_ops_utilisateur | default('setops') }}"
|
|
group: "{{ serveur_ops_utilisateur | default('setops') }}"
|
|
mode: "0600"
|
|
loop: "{{ genome_a_pousser }}"
|
|
loop_control:
|
|
label: "{{ item.depot }}"
|
|
|
|
# L'OUTIL VOYAGE AVEC LE COLIS, il n'est pas supposé déjà là (2026-08-26).
|
|
#
|
|
# Première version : on lançait `scripts/genome_colis.py` depuis le moteur cloné sur
|
|
# le runner. Circulaire — le runner ne reçoit cette version du script qu'APRÈS la
|
|
# poussée qu'elle sert à faire. Et ça se reproduirait à chaque évolution de l'outil :
|
|
# le runner en retard ne saurait jamais rattraper son retard.
|
|
#
|
|
# C'est le contrôleur qui détient la version qui fait autorité. Il la porte.
|
|
- name: Porter l'outil lui-même jusqu'au runner
|
|
ansible.builtin.copy:
|
|
src: "{{ genome_freres }}/{{ genome_moteur }}/scripts/genome_colis.py"
|
|
dest: "{{ genome_racine }}/.bundles/genome_colis.py"
|
|
owner: "{{ serveur_ops_utilisateur | default('setops') }}"
|
|
group: "{{ serveur_ops_utilisateur | default('setops') }}"
|
|
mode: "0700"
|
|
|
|
- name: Le runner vérifie, avance en avance rapide, et pousse
|
|
ansible.builtin.command:
|
|
argv:
|
|
- python3
|
|
- "{{ genome_racine }}/.bundles/genome_colis.py"
|
|
- recevoir
|
|
- "{{ genome_racine }}/{{ item.dest }}"
|
|
- "{{ genome_racine }}/.bundles/{{ item.depot }}.bundle"
|
|
- "git@forge.{{ domaine_interne }}:{{ serveur_ops_forge_organisation | default('genome') }}/{{ item.depot }}.git"
|
|
- --cle
|
|
- "{{ genome_racine }}/.ssh/id_{{ serveur_ops_cle_type | default('ed25519') }}"
|
|
- --branche
|
|
- "{{ item.branche | default('main') }}"
|
|
loop: "{{ genome_a_pousser }}"
|
|
loop_control:
|
|
label: "{{ item.depot }}"
|
|
register: genome_recu
|
|
# `changed_when: false` ET C'EST VOULU. Dans une boucle, ni le registre (pas encore
|
|
# defini) ni `stdout` nu ne sont lisibles ici — les deux ont ete essayes, les deux
|
|
# ont leve. Plutot qu'une condition qui ment ou qui casse, on ne pretend rien : ce
|
|
# qui a change est dit par le VERDICT, plus bas, qui compare la forge au runner et
|
|
# nomme la transition. La verite vient de la forge, pas d'un drapeau de tache.
|
|
changed_when: false
|
|
|
|
- name: Demander à la forge ce qu'elle porte maintenant
|
|
# On interroge son API plutôt que `git ls-remote` : c'est la forge elle-même qui
|
|
# répond, et la lecture est anonyme — aucun justificatif n'entre ici.
|
|
ansible.builtin.uri:
|
|
url: >-
|
|
https://forge.{{ domaine_interne }}/api/v1/repos/{{ serveur_ops_forge_organisation
|
|
| default('genome') }}/{{ item.depot }}/branches/{{ item.branche | default('main') }}
|
|
validate_certs: false
|
|
status_code: [200]
|
|
loop: "{{ genome_a_pousser }}"
|
|
loop_control:
|
|
label: "{{ item.depot }}"
|
|
register: genome_forge
|
|
# LE PREMIER CONTACT AVEC LA FORGE RATE PARFOIS (mesure du 2026-08-26 : une fois sur
|
|
# six, poignee TLS expiree, puis cinq reponses de suite). Ce n'est pas le chemin —
|
|
# il est prouve — c'est l'acceptation TLS apres un temps d'inactivite. On reessaie
|
|
# plutot que de rendre un verdict sur un alea.
|
|
retries: 4
|
|
delay: 3
|
|
until: genome_forge is succeeded
|
|
changed_when: false
|
|
|
|
- name: La forge porte-t-elle exactement ce que le runner porte ?
|
|
ansible.builtin.assert:
|
|
that:
|
|
- genome_forge.results[idx].json.commit.id
|
|
== (genome_recu.results[idx].stdout | from_json).apres
|
|
fail_msg: >-
|
|
{{ item.depot }} : la forge porte
|
|
{{ genome_forge.results[idx].json.commit.id[:8] | default('rien') }} alors que
|
|
le runner porte
|
|
{{ (genome_recu.results[idx].stdout | from_json).apres[:8] }}. La poussée n'a pas
|
|
abouti — ne pas cloner depuis cette forge tant que ce n'est pas réglé.
|
|
success_msg: >-
|
|
{{ item.depot }} :
|
|
{{ (genome_recu.results[idx].stdout | from_json).avant[:7] }} →
|
|
{{ (genome_recu.results[idx].stdout | from_json).apres[:7] }}, et la forge le porte.
|
|
loop: "{{ genome_a_pousser }}"
|
|
loop_control:
|
|
label: "{{ item.depot }}"
|
|
index_var: idx
|
|
|
|
# Le genome n'a pas a trainer en double sur la machine : ce qui compte est dans les
|
|
# depots et sur la forge.
|
|
- name: Retirer les colis
|
|
ansible.builtin.file:
|
|
path: "{{ genome_racine }}/.bundles"
|
|
state: absent
|
|
|
|
- name: Retirer l'atelier du contrôleur
|
|
ansible.builtin.file:
|
|
path: "{{ genome_atelier.path }}"
|
|
state: absent
|
|
delegate_to: localhost
|
|
become: false
|