Publication atomique du catalogue par lien symbolique, droits du volume, repli sur le catalogue de l'image
build / Garde-fou (pull_request) Successful in 11s
build / Images Harbor (catalog-sync, Dockerfile.catalog-sync) (pull_request) Skipped
build / Images Harbor (web, Dockerfile) (pull_request) Skipped
build / Images Harbor (catalog-sync, Dockerfile.catalog-sync) (push) Successful in 9m49s
build / Garde-fou (push) Successful in 10s
build / Images Harbor (web, Dockerfile) (push) Successful in 11m25s

This commit is contained in:
hcornet committed 2026-09-10 13:39:42 +02:00
1 parent c51a6379bd
commit 5f76b08ae6
9 files changed
+126 -38

No files matched your search

+2 -1
View File
@@ -11,10 +11,11 @@ TZ=Europe/Paris
GUNICORN_WORKERS=2
# Catalogue synchronise depuis Git (laisser vide pour n'utiliser que celui de l'image)
CATALOG_REPO=https://gitea.tips-of-mine.com/hubert/enclume-catalogue.git
CATALOG_REPO=https://gitea.tips-of-mine.com/Tips-Of-Mine/Enclume.git
CATALOG_BRANCH=main
CATALOG_SUBDIR=catalog
SYNC_INTERVAL=300
CATALOG_SUBDIR=catalog
# Base de donnees (chantier 2)
POSTGRES_PASSWORD=a-remplacer
+6 -4
View File
@@ -4,19 +4,21 @@
# image de l'application.
FROM alpine:3.20
RUN apk add --no-cache git curl tini \
RUN apk add --no-cache git curl tini su-exec \
&& addgroup -g 10001 enclume \
&& adduser -u 10001 -G enclume -D -H enclume
COPY docker/catalog-sync.sh /usr/local/bin/catalog-sync
RUN chmod +x /usr/local/bin/catalog-sync
COPY docker/catalog-sync-entrypoint.sh /usr/local/bin/entrypoint
RUN chmod +x /usr/local/bin/catalog-sync /usr/local/bin/entrypoint
ENV CATALOG_REPO="" \
CATALOG_BRANCH=main \
CATALOG_SUBDIR=catalog \
CATALOG_DEST=/catalogue \
SYNC_INTERVAL=300 \
REVISIONS_CONSERVEES=3 \
RELOAD_URL=http://web:8080/recharger
USER enclume
ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/catalog-sync"]
# Demarre en root pour ajuster le volume, puis bascule sur l'utilisateur enclume.
ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/entrypoint"]
+18 -8
View File
@@ -13,12 +13,14 @@ services:
image: ${ENCLUME_REGISTRY:-registry.tips-of-mine.com}/enclume/web:${ENCLUME_TAG:?definir ENCLUME_TAG dans .env}
restart: unless-stopped
environment:
ENCLUME_CATALOG: /catalogue
# catalog-sync publie chaque revision a cote et fait basculer ce lien.
# Si le volume est vide, l'application se rabat sur le catalogue de l'image.
ENCLUME_CATALOG: /catalogue/actuel
GUNICORN_WORKERS: ${GUNICORN_WORKERS:-2}
TZ: ${TZ:-Europe/Paris}
volumes:
- catalogue:/catalogue:ro
networks: [interne, traefik]
networks: [back_network_enclume, db_network_enclume, traefik_front_network]
read_only: true
tmpfs:
- /tmp:size=32m,mode=1777
@@ -32,7 +34,7 @@ services:
options: {max-size: "10m", max-file: "3"}
labels:
traefik.enable: "true"
traefik.docker.network: traefik
traefik.docker.network: traefik_front_network
traefik.http.routers.enclume.rule: Host(`${ENCLUME_DOMAINE:?definir ENCLUME_DOMAINE}`)
traefik.http.routers.enclume.entrypoints: websecure
traefik.http.routers.enclume.tls.certresolver: ${TRAEFIK_RESOLVER:-cloudflare}
@@ -57,7 +59,7 @@ services:
RELOAD_URL: http://web:8080/recharger
volumes:
- catalogue:/catalogue
networks: [interne]
networks: [back_network_enclume]
security_opt: ["no-new-privileges:true"]
deploy:
resources:
@@ -78,7 +80,7 @@ services:
TZ: ${TZ:-Europe/Paris}
volumes:
- donnees:/var/lib/postgresql/data
networks: [interne]
networks: [db_network_enclume]
security_opt: ["no-new-privileges:true"]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U enclume -d enclume"]
@@ -94,7 +96,15 @@ volumes:
donnees:
networks:
interne:
internal: true
traefik:
# Reseau du reverse proxy existant.
traefik_front_network:
external: true
# catalog-sync doit joindre Gitea : ce reseau a donc un acces sortant.
back_network_enclume:
driver: bridge
attachable: true
# La base n'a aucune raison de sortir : elle reste isolee avec l'application.
db_network_enclume:
internal: true
+14
View File
@@ -0,0 +1,14 @@
#!/bin/sh
# Le volume partage appartient a root a sa creation. On l'ajuste en root, puis
# on abandonne les privileges avant de lancer la boucle de synchronisation.
set -eu
: "${CATALOG_DEST:=/catalogue}"
if [ "$(id -u)" = "0" ]; then
mkdir -p "$CATALOG_DEST"
chown enclume:enclume "$CATALOG_DEST"
exec su-exec enclume:enclume /usr/local/bin/catalog-sync "$@"
fi
exec /usr/local/bin/catalog-sync "$@"
+48 -20
View File
@@ -1,42 +1,70 @@
#!/bin/sh
# Clone ou met a jour le depot du catalogue, publie son contenu dans le volume
# partage, puis notifie l'application. Boucle jusqu'a l'arret du conteneur.
#
# La publication passe par un lien symbolique : on ne peut pas renommer le
# point de montage lui-meme, mais on peut faire basculer un lien a l'interieur,
# et cette bascule est atomique. L'application lit CATALOG_DEST/actuel.
set -eu
if [ -z "${CATALOG_REPO}" ]; then
echo "catalog-sync : CATALOG_REPO non defini, rien a synchroniser." >&2
exit 0
: "${CATALOG_DEST:=/catalogue}"
: "${CATALOG_BRANCH:=main}"
: "${CATALOG_SUBDIR:=catalog}"
: "${SYNC_INTERVAL:=300}"
: "${REVISIONS_CONSERVEES:=3}"
if [ -z "${CATALOG_REPO:-}" ]; then
echo "catalog-sync : CATALOG_REPO non defini. L'application utilisera le catalogue de son image."
# On reste en vie sans rien faire : le conteneur ne doit pas boucler en redemarrage.
while true; do sleep 3600; done
fi
TRAVAIL=/tmp/depot
sync_une_fois() {
if [ -d "$TRAVAIL/.git" ]; then
git -C "$TRAVAIL" fetch --quiet --depth 1 origin "$CATALOG_BRANCH"
git -C "$TRAVAIL" reset --quiet --hard "origin/$CATALOG_BRANCH"
git -C "$TRAVAIL" fetch --quiet --depth 1 origin "$CATALOG_BRANCH" || return 1
git -C "$TRAVAIL" reset --quiet --hard "origin/$CATALOG_BRANCH" || return 1
else
git clone --quiet --depth 1 --branch "$CATALOG_BRANCH" "$CATALOG_REPO" "$TRAVAIL"
rm -rf "$TRAVAIL"
git clone --quiet --depth 1 --branch "$CATALOG_BRANCH" "$CATALOG_REPO" "$TRAVAIL" || return 1
fi
SOURCE="$TRAVAIL/$CATALOG_SUBDIR"
[ -d "$SOURCE" ] || { echo "catalog-sync : $CATALOG_SUBDIR absent du depot." >&2; return 1; }
if [ ! -d "$SOURCE" ]; then
echo "catalog-sync : le repertoire $CATALOG_SUBDIR est absent du depot." >&2
return 1
fi
# Publication atomique : on ecrit a cote, puis on bascule.
NEUF="${CATALOG_DEST}.neuf"
rm -rf "$NEUF"
mkdir -p "$NEUF"
cp -a "$SOURCE"/. "$NEUF"/
rm -rf "${CATALOG_DEST}.ancien"
[ -d "$CATALOG_DEST" ] && mv "$CATALOG_DEST" "${CATALOG_DEST}.ancien"
mv "$NEUF" "$CATALOG_DEST"
rm -rf "${CATALOG_DEST}.ancien"
REVISION=$(git -C "$TRAVAIL" rev-parse --short HEAD) || return 1
CIBLE="$CATALOG_DEST/rev-$REVISION"
REVISION=$(git -C "$TRAVAIL" rev-parse --short HEAD)
echo "catalog-sync : catalogue a jour ($REVISION)"
curl -fsS -X POST "$RELOAD_URL" >/dev/null 2>&1 || echo "catalog-sync : rechargement non confirme"
if [ -L "$CATALOG_DEST/actuel" ] && [ "$(readlink "$CATALOG_DEST/actuel")" = "$CIBLE" ]; then
return 0 # deja publie, rien a faire
fi
rm -rf "$CIBLE.partiel" "$CIBLE"
mkdir -p "$CIBLE.partiel"
cp -a "$SOURCE"/. "$CIBLE.partiel"/ || return 1
mv "$CIBLE.partiel" "$CIBLE" || return 1
# Bascule atomique du lien, puis menage des anciennes revisions.
ln -sfn "$CIBLE" "$CATALOG_DEST/actuel.neuf"
mv -f "$CATALOG_DEST/actuel.neuf" "$CATALOG_DEST/actuel" || return 1
ls -1dt "$CATALOG_DEST"/rev-* 2>/dev/null | tail -n +$((REVISIONS_CONSERVEES + 1)) | while read -r vieux; do
rm -rf "$vieux"
done
echo "catalog-sync : catalogue publie ($REVISION)"
if [ -n "${RELOAD_URL:-}" ]; then
curl -fsS -X POST "$RELOAD_URL" >/dev/null 2>&1 || echo "catalog-sync : rechargement non confirme"
fi
}
while true; do
sync_une_fois || echo "catalog-sync : echec de la synchronisation, nouvelle tentative dans ${SYNC_INTERVAL}s" >&2
if ! sync_une_fois; then
echo "catalog-sync : echec de la synchronisation, nouvelle tentative dans ${SYNC_INTERVAL}s" >&2
fi
sleep "$SYNC_INTERVAL"
done
+1 -1
View File
@@ -71,7 +71,7 @@ déploiement urgent. Le rapport Trivy dans la CI suffit à surveiller.
## Enchaînement
```bash
git remote add origin https://gitea.tips-of-mine.com/hubert/enclume.git
git remote add origin https://gitea.tips-of-mine.com/Tips-Of-Mine/Enclume.git
git push -u origin main
```
+1 -1
View File
@@ -22,7 +22,7 @@
<footer class="pied">
<span>Projet indépendant, sans lien avec Centreon SAS. Centreon est une marque déposée de son propriétaire.</span>
<span class="grow"></span>
<a href="https://gitea.tips-of-mine.com/hubert/enclume">Code source</a>
<a href="https://gitea.tips-of-mine.com/Tips-Of-Mine/Enclume">Code source</a>
<a href="/sante">État du service</a>
</footer>
</body>
+21 -3
View File
@@ -22,13 +22,30 @@ from .packs import pack_to_project
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CATALOG_DIR = os.environ.get("ENCLUME_CATALOG", os.path.join(BASE_DIR, "catalog"))
CATALOG_FALLBACK = os.environ.get("ENCLUME_CATALOG_FALLBACK", os.path.join(BASE_DIR, "catalog"))
MAX_BODY = 2 * 1024 * 1024 # un projet honnete pese quelques dizaines de ko
def _repertoire_catalogue() -> str:
"""Repertoire a lire : celui synchronise, sinon celui livre dans l'image.
Le volume partage est vide tant que catalog-sync n'a rien publie, et il peut
le rester si Git est injoignable. Servir un site sans aucun pack serait pire
que servir un catalogue un peu ancien : on se rabat sur celui de l'image.
"""
for chemin in (CATALOG_DIR, CATALOG_FALLBACK):
if not chemin or not os.path.isdir(chemin):
continue
for _racine, _dossiers, fichiers in os.walk(chemin):
if any(f.endswith((".yml", ".yaml")) for f in fichiers):
return chemin
return CATALOG_FALLBACK
def create_app() -> Flask:
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = MAX_BODY
app.config["CATALOG"] = Catalog(CATALOG_DIR)
app.config["CATALOG"] = Catalog(_repertoire_catalogue())
def catalog() -> Catalog:
return app.config["CATALOG"]
@@ -76,6 +93,7 @@ def create_app() -> Flask:
"version": os.environ.get("ENCLUME_VERSION", "dev"),
"revision": os.environ.get("ENCLUME_COMMIT", "inconnu"),
"packs": len(cat.packs),
"source_catalogue": cat.root,
"erreurs_catalogue": cat.errors,
}
)
@@ -156,8 +174,8 @@ def create_app() -> Flask:
@app.post("/recharger")
def reload_catalog() -> Response:
app.config["CATALOG"] = Catalog(CATALOG_DIR)
app.config["CATALOG"] = Catalog(_repertoire_catalogue())
cat = catalog()
return jsonify({"packs": len(cat.packs), "erreurs": cat.errors})
return jsonify({"packs": len(cat.packs), "source": cat.root, "erreurs": cat.errors})
return app
+15
View File
@@ -76,3 +76,18 @@ def test_api_de_generation():
def test_sante():
reponse = create_app().test_client().get("/sante")
assert reponse.get_json()["statut"] == "ok"
def test_repli_sur_le_catalogue_de_l_image(tmp_path, monkeypatch):
"""Un volume vide ne doit jamais donner un site sans aucun pack."""
import importlib
monkeypatch.setenv("ENCLUME_CATALOG", str(tmp_path))
monkeypatch.setenv("ENCLUME_CATALOG_FALLBACK", "catalog")
import enclume.webapp as webapp
importlib.reload(webapp)
reponse = webapp.create_app().test_client().get("/sante")
donnees = reponse.get_json()
assert donnees["packs"] >= 20
assert donnees["source_catalogue"] == "catalog"