Commit Graph
86 Commits
Author SHA1 Message Date
arcodange 286f2422a0 feat(system) — câbler earlyoom dans le playbook system
Placé juste après le setup général et avant docker/k3s : le filet mémoire doit
exister avant les consommateurs qu'il est censé borner.
2026-08-15 22:30:14 +02:00
arcodange 9cf7dfd44e feat(system) — earlyoom sur les 3 Pi : tuer net au lieu de thrasher
Les Pi tournent sans swap, et sans swap un dépassement mémoire ne produit pas
forcément d'OOM-kill : le noyau part en thrash et la machine reste vivante mais
inutilisable des heures. Mesuré sur pi1 le 2026-08-15 (load 293, 175 Mio dispo,
sshd incapable de forker, apiserver k3s + traefik affamés, tout
*.arcodange.lab HS pendant ~50 min).

earlyoom surveille MemAvailable en espace utilisateur et déclenche AVANT le
blocage du noyau : SIGTERM sous 10 %, SIGKILL sous 5 %. Choix volontaire du
kill direct plutôt que zram/swap — aucun swap à ajouter, donc aucun conflit
avec le --fail-swap-on de k3s.

Appliqué et vérifié sur pi1, pi2 et pi3 ; rejeu idempotent (changed=0).
2026-08-15 22:29:47 +02:00
arcodange d355c9c24e fix(k3s) — kubelet-arg guillemeté cassait le parsing, + réservation pi2
Le scheduler k8s croyait disposer des 4 cœurs / 7,6 Gi entiers de pi2,
alors que Gitea et Postgres (docker compose nu, hors k3s, PR factory#54)
en consomment une part invisible. `--kubelet-arg="system-reserved=cpu=2,
memory=2Gi"` sur l'agent pi2 corrige ça — réservation informative, pas
d'--enforce-node-allocatable, donc pas de nouvelle éviction.

En le déployant : incident réel. `--kubelet-arg="k=v"` (guillemets
littéraux autour de key=value) ressort en `\=` littéral dans l'ExecStart
que k3s-install.sh régénère — kubelet refuse de démarrer ("unknown flag:
--container-log-max-files\"), boucle de redémarrage jusqu'à NotReady.
C'était déjà le cas pour les DEUX args pré-existants (container-log-max-
files, container-log-max-size), latent depuis des mois parce que
k3s-agent n'avait pas redémarré depuis avril — jamais régénéré par la
version actuelle du script. Mon changement a déclenché le premier
restart réel et l'a fait sortir.

pi2 a été NotReady ~3 min pendant le diagnostic puis la correction en
direct (aucun pod évincé, sous le pod-eviction-timeout par défaut de
5 min — vérifié). Les DEUX occurrences pré-existantes sont corrigées ici
aussi (extra_server_args ET extra_agent_args), pas seulement la mienne :
pi1 (le control-plane) porte le MÊME bug dans sa source, dormant parce
que son k3s.service n'a pas non plus redémarré récemment. Sans cette
PR, le prochain restart de pi1 (reboot, ou un futur run de ce playbook)
aurait cassé l'API server de la même façon.

Le format sans guillemets (`--kubelet-arg=k=v`) traverse la génération
intact — vérifié par la correction en direct sur pi2 (journal confirme
`--system-reserved=cpu=2,memory=2Gi` sans backslash, service stable,
Allocatable descendu de 4 cœurs/8Gi à 2 cœurs/5,6Gi).
2026-08-11 17:05:18 +02:00
arcodange f250817641 fix(pi2) — Gitea et Postgres avaient zéro plafond, plus maintenant
pi2 tournait à load average ~45 (4 cœurs) pendant qu'un push docker
timeoutait vers le registre. Gitea et Postgres tournent en docker compose
nu, hors k3s — invisibles du scheduler ET sans limite (`docker inspect`
mesurait NanoCPUs=0, Memory=0 pour les deux), donc rien ne les empêchait
de se battre à armes égales avec tout le reste du nœud.

Postgres → 1 CPU / 1024M, Gitea → 1.5 CPU / 1536M (Compose v2 honore
`deploy.resources.limits` hors swarm). Valeurs dérivées d'une mesure au
repos (Postgres 3-5 %, Gitea 12 % CPU) avec de la marge pour les pics —
un filet, pas un dimensionnement pour la charge normale.

Appliqué et vérifié en direct sur pi2 : les deux conteneurs ont recréé
avec les nouvelles limites (`docker inspect` confirme), PostGIS survit
au recreate de Postgres (déjà géré par ce playbook), Gitea sert web (200)
et registre (401 attendu, anonyme) normalement après coup.

Le levier complémentaire (kubelet --system-reserved/--kube-reserved sur
pi2, pour que le SCHEDULER k8s sache que cette place est déjà prise) n'est
pas dans cette PR — plus gros, touche system_k3s.yml pour tout le cluster.
2026-08-11 16:25:20 +02:00
arcodangeandClaude Opus 5 f944fe4bbd feat(postgres) — PostGIS, posé par le playbook et non par une image custom
Kadans doit ranger le contour d'un quartier en vraie géométrie
(geometry(MultiPolygon,4326), ST_Contains, index GiST). L'extension
n'existait nulle part : mesuré sur pi2, `pg_available_extensions` ne
rendait AUCUNE ligne `postgis%`.

Arbitrage fondateur (2026-08-08) : on garde `postgres:16.3-alpine` et on
pose l'extension par Ansible, comme le playbook pose déjà les bases et le
rôle pgbouncer. Pas d'image custom.

⚠ POURQUOI LE RECALAGE DE CHEMINS N'EST PAS FACULTATIF — mesuré, arm64.
`apk add postgis` SEUL réussit, et `CREATE EXTENSION postgis` échoue quand
même :

    ERROR: extension "postgis" is not available
    DETAIL: Could not open extension control file
            "/usr/local/share/postgresql/extension/postgis.control"

Le paquet Alpine vise la disposition d'Alpine (/usr/share/postgresql16,
/usr/lib/postgresql16) ; l'image officielle compile le serveur dans
/usr/local. Les fichiers sont là, le serveur regarde ailleurs. Après
recalage : PostGIS 3.4 USE_GEOS=1 USE_PROJ=1, et un polygone lyonnais qui
fait l'aller-retour ST_GeomFromText → ST_AsGeoJSON.

Ne pas « simplifier » en un `apk add` nu : la simulation dit OK,
l'installation dit OK, et l'extension reste inutilisable.

⚠ INSTALLATION PAR CONTENEUR, PAS PAR VOLUME. `apk add` écrit dans la
couche inscriptible : recréer le conteneur efface PostGIS pendant que les
données gardent leurs colonnes géométriques — toute requête spatiale casse
jusqu'au prochain passage du playbook. D'où l'ordre (déploiement compose
PUIS installation), l'idempotence, et surtout la tâche de vérification.

La vérification ne se contente pas d'un code de retour : elle exige que la
base rende USE_GEOS=1 ET un vrai Point GeoJSON avec son SRID. Un bouchon
qui répondrait une chaîne vide passerait un simple `rc == 0` et ne
prouverait rien — un playbook vert sur une extension absente ferait
atterrir le symptôme dans Kadans, des jours plus tard, déguisé en bug
applicatif.

Vérifié sur le conteneur RÉEL sans le modifier : `apk add --simulate`
résout postgis 3.4.2-r2, et `pg_config` y rend bien les deux chemins que
les variables supposent.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01J4UE4AmX5PAMN6c6Q6Fey9
2026-08-08 09:46:08 +02:00
arcodange 0cc8213bff Merge remote-tracking branch 'origin/main' into arcodange/mirror-depots-perso
# Conflicts:
#	ansible/arcodange/factory/inventory/group_vars/all/gitea.yml
2026-07-30 19:55:06 +02:00
CI BotandClaude Opus 5 6bb27b0e5c feat(cicd) — Gitea 1.27.1 et Gitea Runner 2.3.0 : l'image du runner a CHANGÉ DE NOM
⚠ CORRIGE LE PREMIER JET DE CETTE BRANCHE, qui épinglait `gitea/act_runner:0.3.1`.

`gitea/act_runner` est GELÉE à 0.6.1. Le successeur officiel est `gitea/runner`
(binaire renommé `act_runner` → `gitea-runner`), aujourd'hui en **2.3.0**.
Épingler l'ancien nom nous aurait enfermés dans une image morte — trouvé grâce
aux notes de version de Gitea 1.27 signalées par le fondateur.

VÉRIFIÉ AVANT DE BASCULER — c'est un remplacement DIRECT pour ce compose :
  • entrypoint identique : /sbin/tini -- run.sh
  • mêmes variables lues : CONFIG_FILE, GITEA_INSTANCE_URL,
    GITEA_RUNNER_{REGISTRATION_TOKEN,NAME,LABELS}
  • config.yaml compatible : capacity, labels, cache.*, container.force_pull,
    options, valid_volumes, host.workdir_parent — AUCUNE clé utilisée ici n'a
    disparu (comparé au `gitea-runner generate-config` de la 2.3.0)

La 2.3.0 apporte en prime des réglages qui parlent à nos pannes connues :
`health_check.min_free_disk_space_mb` (les images de runner supprimées quand le
disque se remplit, ADR 20260407) et `state_report_interval` (les tâches tuées en
zombie faute de rapport, factory#50).

GITEA 1.25.5 → 1.27.1 : deux versions mineures, migrations de base
IRRÉVERSIBLES. Sauvegardes du jour VÉRIFIÉES avant, pas supposées :
  /mnt/backups/postgres/backup_20260730.sql.gz  13 Mo, gzip -t OK,
      contient « CREATE DATABASE gitea » (pg_dumpall)
  /mnt/backups/gitea/backup_20260730.gitea.gz   1,7 Go, gzip -t OK
⚠ Le backup Gitea utilise `gitea dump --skip-db` : il ne contient PAS la base.
C'est le dump postgres qui la porte — les deux sont nécessaires.

Changements cassants de 1.27 et leur portée ici, vérifiée :
  • workflows réutilisables externes retirés → AUCUN dans front, kadans-api,
    factory (contrôlé programmatiquement, `uses:` au niveau job)
  • nonce CSP pour scripts inline → concerne les templates personnalisés, nous
    n'en avons pas
  • X-Content-Type-Options: nosniff par défaut

Refs arcodange-org/factory#50

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-30 09:03:40 +02:00
CI BotandClaude Opus 5 48e3d6827c fix(cicd) — épingler la version du runner : latest + pull: missing ne rafraîchit JAMAIS
Réponse à « qu'est-ce qui nous empêche d'upgrade des deux côtés ? » : rien.
Le playbook déployait `gitea/act_runner:latest` avec `pull: missing`, c'est-à-dire
la pire combinaison possible — un tag FLOTTANT qui n'est JAMAIS rafraîchi. Chaque
hôte garde donc ce que « latest » voulait dire le jour de son premier pull :

  pi1 : sha256:7bdc8d31…  →  v0.3.1
  pi3 : sha256:0f65fa10…  →  v0.2.13

Deux machines censées être équivalentes, deux versions à trois mineures d'écart.
Effets mesurés : le MÊME job, sur la MÊME image de CI, met 511 s sur pi1 et
397 s sur pi3 (114 s d'écart imputables à la machine) ; et pi3 a mal lu la
définition d'un job dont il dépendait (« 'runs-on' key not defined », puis
« No steps found »).

⚠ POURQUOI PAS `latest` + `pull: always`. `latest` vaut aujourd'hui **0.6.1**
(Docker Hub, 30/04/2026), soit 3 à 4 versions mineures devant tout ce qui est
éprouvé ici. Le runner exécute TOUTE la CI de la forge : une montée subie, non
datée et non choisie s'y paie cher. On épingle donc, et on monte délibérément.

⚠ POURQUOI 0.3.1 ET PAS 0.6.1. 0.3.1 est la version que pi1 exécute DÉJÀ avec
succès sur cette forge. Ce changement aligne donc pi3 VERS LE HAUT, sur du
prouvé, sans saut de quatre versions. Passer ensuite à 0.6.1 devient une
modification d'UNE ligne, datée et reculable — c'est tout l'intérêt de la
variable.

⚠ Et `pull: missing` redevient CORRECT avec un tag épinglé : changer la version
change le tag, donc l'image est absente, donc elle est tirée. Aucun besoin de
`pull: always`, qui interrogerait le registre à chaque passage pour rien.

⚠ NE PAS jouer ce playbook pendant qu'une CI tourne : il recrée les conteneurs
de runner et TUE les jobs en vol (journaux perdus). Vérifier `list_runs` avant —
et se rappeler qu'un merge est un déclencheur.

Refs arcodange-org/factory#50

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-30 08:54:43 +02:00
CI BotandClaude Opus 5 ba791ed055 fix(ci_base_image) — le contexte de build doit être SUR la machine, pas sur le contrôleur
Le playbook 03_cicd est mort sur les deux hôtes :

  "/Users/…/roles/ci_base_image/files/" is not an existing directory

`docker_image_build` s'exécute SUR LA CIBLE : son `path:` est un chemin de la
cible. Je passais `{{ role_path }}/files/`, un chemin du CONTRÔLEUR.

Le motif venait du rôle `playwright`, qui l'emploie LÉGITIMEMENT parce qu'il
construit en local. Recopié pour un build distant, il ne pouvait pas marcher —
et aucune relecture ne l'aurait montré, seule l'exécution le dit.

⚠ L'échec est arrivé AVANT les tâches qui déploient le runner : les deux
runners sont restés `Up 6 days`, rien n'a été cassé. Le seul effet fut un jeton
d'API Gitea créé par le rôle gitea_token, son comportement normal.

Le contexte est désormais déposé sur la machine (`/tmp/ci-base-image`), et la
RECONSTRUCTION DEVIENT CONDITIONNELLE : `never` en régime normal — le playbook
ne rebâtit pas 3,3 Go à chaque passage — mais `always` dès que le Dockerfile a
CHANGÉ sur la machine. Ajouter une bibliothèque devient donc effectif sans avoir
à penser à un drapeau.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-29 23:41:32 +02:00
CI BotandClaude Opus 5 9f438c4968 fix(ci_base_image) — l'image livrait Node 18 : deux défauts que seule sa CONSTRUCTION a montrés
J'avais écrit dans cette PR « je n'ai pas pu construire l'image, sa base vit
derrière le certificat interne ». C'était une SUPPOSITION NON TESTÉE, et elle est
fausse : `docker pull gitea.arcodange.lab/…/runner-images:ubuntu-latest-ca` passe
sans rien configurer. En la construisant vraiment, deux défauts sont sortis — et
aucun n'était visible à la lecture du Dockerfile.

1. `runner-images:ubuntu-latest-ca` LIVRE NODE 18 (v18.20.8). Or `nuxi` importe
   `node:util.styleText`, absent de Node 18 : c'est la raison d'être du
   `container: node:20-bookworm` de la CI de kadans, que son CLAUDE.md interdit
   de retirer. Sans correctif, basculer la CI sur cette image cassait `nuxt build`
   sur un message parlant d'un import introuvable — jamais d'une version de Node.
   → Node 20 installé depuis NodeSource.

2. ET INSTALLER NE SUFFISAIT PAS. Après l'installation, `node --version` rendait
   TOUJOURS v18.20.8 : l'image de base précuit un node pour le toolcache d'act et
   le met EN TÊTE du PATH.

     which node → /opt/acttoolcache/node/18.20.8/arm64/bin/node
     /usr/bin/node --version → v20.20.2   ← le bon, mais il PERD

   → l'entrée 18 du toolcache est retirée ; la résolution retombe sur
     /usr/bin/node. ⚠ Conséquence assumée : `actions/setup-node` ne trouvera plus
     de Node 18 préinstallé — aucun workflow de kadans ne l'utilise, et l'image
     n'est servie qu'aux jobs qui DEMANDENT le label.

Le Dockerfile porte désormais une ASSERTION DE BUILD
(`node --version | grep -q "^v${NODE_MAJOR}\."`) : l'image ne peut plus se
construire si la résolution redevient mauvaise. Et le rôle vérifie la version au
déploiement (`failed_when`), au lieu de la supposer.

MESURES RÉELLES (construite en linux/arm64, l'architecture des runners) :

  TOTAL                              4,58 Go
  ├─ playwright install chromium     1,01 Go
  ├─ playwright install-deps          405 Mo
  ├─ Node 20 (NodeSource)             183 Mo
  └─ bun                              179 Mo

  Vérifié dans l'image : which node → /usr/bin/node v20.20.2 · bun 1.3.14 ·
  chromium-1228 + headless-shell + ffmpeg · /etc/ci-base.versions cohérent.

⚠ Ce que ces chiffres tranchent : le découpage en RUN séparés N'A PAS suffi à
rendre l'image poussable — 1,01 Go pour la plus grosse couche, soit ~4× les
261 Mo que le registre accepte (runner-images:ubuntu-latest-ca). Le build LOCAL
n'est donc pas une préférence, c'est la seule voie. Mesuré, plus supposé.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-29 23:09:14 +02:00
CI BotandClaude Opus 5 fdecabf8ca feat(ci): construire l'image des jobs CI lourds sur chaque machine à runner
Les jobs CI du dépôt kadans réinstallent, à CHAQUE exécution, des choses qui
changent tous les trimestres. Mesuré le 2026-07-29 sur le run 628 (runner
ARM64, 2 vCPU / 3 Gio) :

  npm install -g bun                        →    8,6 s
  playwright install --with-deps chromium   →  104,0 s   (apt-get, à chaque run)
                                               ────────
                                                112,6 s jetées par run, sur le
                                                job qui EST le chemin critique

Le cache `actions/cache` ne peut rien contre ces 104 s : il couvre le NAVIGATEUR
(299 Mo déjà mis en cache), pas ses dépendances SYSTÈME — `--with-deps` relance
apt quoi qu'il arrive.

POURQUOI CONSTRUIRE ICI PLUTÔT QUE POUSSER UNE IMAGE. La tentative de publier
l'image au registre a échoué (kadans#224 puis #225) : 3,81 Go dont une couche
unique de 1,36 Go, `docker push` casse en « connection reset by peer » — 7
couches passent, 3 sont retentées 50 fois puis abandonnées. À titre de
comparaison, runner-images:ubuntu-latest-ca (534,7 Mo, plus grosse couche
261 Mo) passe sans problème : la limite est entre 261 Mo et ~500 Mo par couche.
Construire localement supprime le problème — aucune couche ne traverse le
réseau.

Et c'est bien sur CHAQUE machine : avec `capacity: 1`, le parallélisme vient de
plusieurs Raspberry, et `container:` est résolu par le runner. Un job qui
atterrit là où l'image manque échoue AVANT sa première étape.

Trois choix de conception :

1. L'image hérite de runner-images:ubuntu-latest-ca, donc du certificat de la CA
   interne (step-ca). Repartir de node:20-bookworm obligerait à réinjecter le CA
   à la main, et tout job parlant à gitea.arcodange.lab échouerait en TLS.

2. TROIS `RUN` séparés (bun / dépendances système / navigateur), délibérément.
   La version qui a échoué faisait une couche de 1,36 Go. Ne pas les fusionner
   pour « gagner une couche ».

3. L'image est ÉPINGLÉE par un conteneur factice — ce qui implémente enfin la
   section 1 de docs/adr/20260407-docker-storage-gitea-runner.md, restée à
   l'état de proposition : system_docker.yml n'applique que le data-root sur
   disque externe et les log-opts. Sans épinglage, le ramasse-miettes de Docker
   supprime l'image dès que le disque se remplit — panne déjà constatée sur les
   images de runner elles-mêmes.

Le rôle vérifie sa sortie (`docker run … bun --version`) au lieu de supposer que
le build a suffi, et l'image écrit ses versions dans /etc/ci-base.versions pour
que la CI de kadans puisse les confronter à son bun.lock et échouer FORT sur une
dérive, plutôt que de la découvrir en « Executable doesn't exist ».

Nouveau label runner `ci-node-playwright`. `container.force_pull: false` est
déjà en place et devient REQUIS pour ce label : sans lui, act_runner tenterait
un pull d'une image qui n'est dans aucun registre.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-29 20:23:59 +02:00
arcodangeandClaude Opus 5 e84383ee34 feat(miroirs) — un dépôt personnel n'est pas une organisation
Le rôle gitea_repo ne savait viser qu'un propriétaire : l'organisation, des
deux côtés à la fois. Les dépôts qui vivent sous le compte personnel
`arcodange` ne pouvaient donc pas sortir du homelab — ni être balayés par
gitea_sync, qui n'interroge que /orgs/<org>/repos.

Trois séparations, toutes rétrocompatibles (les défauts reconduisent le
comportement org-vers-org des dix dépôts déjà en miroir) :

- le propriétaire côté Gitea (`gitea_repo_owner`) n'est plus le même objet que
  celui d'en face (`github_owner`, `gitlab_owner`) ;
- un compte personnel n'est pas une organisation : GitHub ne crée pas le dépôt
  au même endroit, d'où `github_owner_is_org` qui route vers POST /user/repos ;
- GitLab devient facultatif (`gitea_mirror_gitlab`). Il ne l'était pas : sa
  création attendait un 201 sans ignore_errors, si bien qu'un échec GitLab
  avortait l'itération — y compris la moitié GitHub, qui n'y était pour rien.

Deux défauts corrigés au passage, tous deux silencieux :

- les trois listages de gitea_sync ne paginaient pas (30 chez GitHub, 20 chez
  GitLab). Sous la taille d'une page tout va bien ; au-delà, la différence
  entre forges désigne de FAUX dépôts manquants et le rôle les « répare » ;
- la migration entrante posait `repo_owner: github_organization` pour désigner
  le propriétaire DANS Gitea.

Et un piège découvert en exécutant : un dépôt GitHub créé vide adopte comme
branche par défaut la PREMIÈRE branche que le miroir lui pousse — `kadans` a
atterri sur `arcodange/adr-ddd-front`. Le rôle réaligne désormais sur la
branche par défaut de Gitea ; le miroir étant asynchrone, l'alignement échoue
au run qui crée le dépôt et réussit au suivant, d'où le failed_when permissif.

Ce qui sort du homelab reste un CHOIX : playbooks/07_mirrors.yml parcourt une
liste explicite et relue (`gitea_mirrored_repos`) plutôt que la différence
automatique entre forges, qui recréerait un dépôt supprimé exprès.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-27 13:34:26 +02:00
arcodange 1ed3154668 Merge pull request 'fix(dns): coredns-custom — importer les blocs *.server à la racine du Corefile (sinon CoreDNS crash)' (#40) from arcodange/coredns-custom-import into main
Reviewed-on: #40
2026-07-24 12:44:33 +02:00
arcodangeandClaude Fable 5 ec49706952 fix(dns): import coredns custom *.server blocks at Corefile root — inside .:53 it crashes CoreDNS
The never-yet-applied k3s_dns.yml placed 'import /etc/coredns/custom/*.server'
INSIDE the .:53 server block. *.server files hold full server blocks
(arcodange.lab:53 {…}), which only parse at Corefile root — inside a block
CoreDNS dies at startup with "Unknown directive 'arcodange.lab:53'"
(CrashLoopBackOff, cluster DNS fully down; lived it on 2026-07-24 while
restoring the expired *.arcodange.lab certificate).

Also restores the stock 'loadbalance' plugin dropped by the playbook.

Context: cluster CoreDNS forwarded to the node's resolv.conf, which lists the
ISP box's IPv6 RDNSS next to the Pi-holes — NXDOMAIN roulette for *.lab names.
That's what left step-issuer unable to reach ssl-ca.arcodange.lab:8443 and let
the 24h wildcard cert expire this morning. The (fixed) playbook pins .lab
resolution to the Pi-holes via the coredns-custom ConfigMap; applied live on
2026-07-24, wildcard renewed, strict TLS verified on gitea/argocd/grafana.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-24 10:13:41 +02:00
arcodangeandClaude Fable 5 961691d6d2 fix(cicd): cap act_runner jobs (3g/2cpu/pids) and capacity 2→1 — a build can no longer take down pi1
Incident 2026-07-23: an uncapped nuxt generate (3.5G RSS) on pi1 starved the
k3s control-plane and traefik (load >150, no swap, no OOM-kill) — every
*.arcodange.lab endpoint went dark, Gitea included, while Gitea itself was
healthy on pi2. Job containers are spawned via the host docker socket, so
cgroup caps on the job container are the only guardrail.

Applied live on pi1+pi3 via 03_cicd.yml on 2026-07-24 (both runners
re-registered; pi3's runner was down and is back in service).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-24 00:09:47 +02:00
arcodange 6ede249da9 🔒 fix(ansible): gate vault auth disable behind vault_oidc_force_reset (default off) (#5)
Co-authored-by: Gabriel Radureau <[email protected]>
Co-committed-by: Gabriel Radureau <[email protected]>
2026-05-06 15:03:33 +02:00
arcodange 9e821e1626 ♻️ refactor(ansible): move gitea secret user-propagation list to inventory (#4)
Co-authored-by: Gabriel Radureau <[email protected]>
Co-committed-by: Gabriel Radureau <[email protected]>
2026-05-06 14:48:05 +02:00
arcodange 69b7e9ddcb Merge remote-tracking branch 'origin/main' 2026-05-06 14:38:01 +02:00
arcodangeandClaude Opus 4.7 069edd72f1 chore(cicd): drop temporary commented-out tasks from 03_cicd.yml
Removes the commented PACKAGES_TOKEN/HOMELAB_CA_CERT blocks and the legacy
"Deploy Argo CD" play that were left behind during the migration to
Helm-based ArgoCD.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-06 14:37:48 +02:00
arcodange a644436746 🔒 fix(ansible): propagate vault_oauth__sh_b64 to user-owned namespaces (arcodange) (#3)
Co-authored-by: Gabriel Radureau <[email protected]>
Co-committed-by: Gabriel Radureau <[email protected]>
2026-05-06 14:18:06 +02:00
arcodangeandClaude Opus 4.7 01f0f37691 chore(ansible): add per-collection ansible.cfg + drop trailing whitespace
ansible/arcodange/factory/ansible.cfg sets collections_path so ansible
commands run from inside the collection directory still find user-installed
collections under ~/.ansible/collections.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-06 12:55:54 +02:00
arcodangeandClaude Opus 4.7 1688fe0dfd fix(crowdsec): clean up Failed pods before Traefik middleware reload
Re-running the role would leave behind crowdsec pods stuck in Failed phase
(typically after a config error on a previous run), which then blocked the
Traefik middleware refresh. Delete them up front so the next reconcile
schedules fresh pods.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-06 12:55:39 +02:00
arcodangeandClaude Opus 4.7 499410a160 feat(cicd): persist gitea act-runner cache + isolate on dedicated docker network
Pins the actcache server to a fixed port (43707) and exposes it, then
mounts /mnt/arcodange/gitea-runner-cache and /mnt/arcodange/gitea-runner-act
into the runner so the actions/cache and act image layer cache survive
container restarts. Moves the runner onto a dedicated `gitea_action_network`
so CI job containers can reach the cache server by name without sharing the
host network.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-06 12:55:34 +02:00
arcodangeandClaude Opus 4.7 e3e0decd98 docs(adr): extend network-architecture ADR with .lab SSL/TLS deep dive
Replaces the placeholder "Success Metrics" section with a detailed
walkthrough of the internal PKI: Step CA provisioners, cert-manager +
StepClusterIssuer wiring, certificate issuance/renewal sequence diagram,
device-trust installation steps, and troubleshooting playbook for the
common stuck-CertificateRequest / Traefik TLS / device-trust failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-06 12:55:27 +02:00
arcodangeandClaude Opus 4.7 1ae28cb944 docs(longhorn): document 2026-04-13 power-cut recovery + add data-recovery tooling
Captures the post-mortem of the April 13 power-cut: incident timeline,
retrospective, and architecture/role diagrams. Adds an ADR explaining why
Longhorn cannot re-associate orphaned replica directories after a nuclear
reinstall (engine-id naming), plus block-device recovery runbooks and the
`playbooks/recover/longhorn_data.yml` automation that wires `merge-longhorn-layers.py`
to rebuild PVCs from raw `volume-head-*.img` chains.

Also extends the k3s_pvc backup to capture Longhorn `volumes`/`settings` CRDs
(needed for the fast-path restore) and rewrites the restore script with a
fallback dir + English messages.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-06 12:55:18 +02:00
arcodangeandClaude Opus 4.7 934b62d922 chore(ansible): use project-local uv venv for ansible runtime deps
Moves the local ansible runtime from a global `uv tool install ansible-core`
(which required remembering `--with kubernetes --with jmespath --with dnspython`)
to a project-managed venv described by `pyproject.toml` + `uv.lock`. Fixes the
"Failed to import the required Python library (kubernetes)" error on localhost.

The localhost inventory entry now derives `ansible_python_interpreter` from
`{{ ansible_playbook_python }}`, so `uv run ansible-playbook` is enough — no
more hardcoded user-specific paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-06 12:35:28 +02:00
arcodangeandClaude Sonnet 4.6 e6fc24c101 fix(dns): harden DNS resilience after power-cut incident
During the 2026-04-13 power cut recovery, DNS resolution failures blocked
Longhorn reinstall. Root causes:
- CoreDNS forwarded to a single hardcoded Pi-hole IP instead of both HA instances
- CoreDNS main Corefile forwarded to /etc/resolv.conf which pointed to itself on pi3
- Pi-hole lacked explicit upstream DNS, relying on DHCP-provided config
- dnsmasq system service conflicted with pihole-FTL on port 53

Changes:
- k3s_dns: forward CoreDNS to both Pi-hole HA instances (pi1 + pi3) dynamically
- k3s_dns: update main CoreDNS Corefile to forward to Pi-holes instead of resolv.conf
- pihole defaults: add explicit upstream DNS servers (8.8.8.8, 1.1.1.1, 8.8.4.4)
- pihole ha_setup: write /etc/dnsmasq.d/99-upstream.conf with explicit upstreams
- rpi: add dnsmasq user to dip group and disable conflicting dnsmasq service on Pi-hole nodes

See docs/adr/20260414-internal-dns-architecture.md for full rationale.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-04-14 10:54:42 +02:00
arcodangeandClaude Sonnet 4.6 355ab11c4d fix(system_docker): fix daemon.json corruption on re-run
Two bugs caused daemon.json to be overwritten with invalid content:
- Invalid `when` condition using unsupported Ansible inline stat syntax,
  causing the existing file read to be silently skipped and docker_config
  to always reset to {}
- Folded scalar `>` in set_fact converted the dict to a Python string
  representation, which to_nice_json serialized as a JSON string instead
  of an object

Fixes identified during 2026-04-13 power cut incident post-mortem.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-04-14 10:52:27 +02:00
arcodange ad70b424cf Add sequence diagram to Docker storage ADR
This commit adds a detailed sequence diagram to the Docker storage optimization ADR, illustrating the workflow for configuring Docker storage, pinning images, and maintaining Longhorn performance.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <[email protected]>
2026-04-08 11:33:03 +02:00
arcodange b299469d00 Consolidate ADRs into docs/adr/
This commit moves Architecture Decision Records (ADRs) from ../../../docs/adr/ to docs/adr/ in the arcodange/factory repository. This centralizes all ADRs in one location for better maintainability and discoverability.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <[email protected]>
2026-04-08 11:09:34 +02:00
arcodange fc9164f11e Update README with detailed playbook execution sequence
This commit updates the README to include a detailed timeline of the playbook execution sequence, organized into sections for system setup, application setup, CI/CD, tools, and backups.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <[email protected]>
2026-04-08 11:04:11 +02:00
arcodange c751b621ba Enable PostgreSQL backup in backup playbook
This commit uncomments the PostgreSQL backup section in the backup playbook to enable regular backups of the PostgreSQL database.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <[email protected]>
2026-04-08 11:04:07 +02:00
arcodangeandMistral Vibe 07a619b274 Fix step-issuer ARM64 compatibility on pi3
The default kube-rbac-proxy image (gcr.io/kubebuilder/kube-rbac-proxy:v0.15.0) is AMD64-only and fails on pi3 (ARM64). This commit overrides the image to use quay.io/brancz/kube-rbac-proxy:v0.15.0, which supports ARM64.

Note: pi2 (ARMv7) may work with AMD64 images, but pi3 (ARM64) requires an ARM64-compatible image.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <[email protected]>
2026-04-08 11:04:03 +02:00
arcodange 9931f81998 Update Docker storage configuration and revoke token task 2026-04-07 19:19:03 +02:00
arcodange 437fd506ed Fix Vault Gitea OIDC setup: remove trailing slash from bound_issuer and pass CA certificate 2026-04-07 19:17:47 +02:00
arcodange 943915be74 gitea act runner: reuse docker images 2026-04-07 09:20:30 +02:00
arcodange 8a82d14797 upgrade gitea version to 1.25.5 2026-04-06 10:55:20 +02:00
arcodange 0285d171ff tweack backup and setup cronjob to fix pg table ownership 2026-03-15 22:14:12 +01:00
arcodange 55d137132f backup k3s volumes 2026-01-23 18:26:28 +01:00
arcodange 451dfa5133 restart traefik when editing crowdsec middleware 2026-01-03 20:08:00 +01:00
arcodange 17e99db641 runner image and setup for gitea workflow with self signed cert 2026-01-03 12:44:27 +01:00
arcodange 5b3c896a25 use self signed cert for internal domain arcodange.lab 2025-12-31 17:38:04 +01:00
arcodange 91219c49f1 use exposed webapp.arcodange.fr instead in gitea cicd 2025-12-23 14:23:12 +01:00
arcodange 1fd47e9d97 install pihole to fix failing duckdns name servers 2025-12-23 14:20:04 +01:00
arcodange 8d6be311ae argocd: add --enable-helm to kustomize ; enable shell from web ui 2025-12-10 13:48:22 +01:00
arcodange 2b4aa30a64 use cache redis with crowdsec traefik bouncer 2025-12-06 15:09:36 +01:00
arcodange cd3c4d86ff install socat package to enable kubectl port-forward 2025-12-06 15:09:12 +01:00
arcodange f4cb04c9c9 configure crowdsec captcha with cloudflare turnstile 2025-12-03 16:45:25 +01:00
arcodange 17a0f23bbb declare gitea external service 2025-12-01 16:22:44 +01:00
arcodange f7bfe2f71d get cloudflared client real ip and fix crowdsec mw 2025-11-29 17:24:51 +01:00