Aller au contenu

Moteur de restauration

Un plan est une liste d'étapes, exécutées dans l'ordre, sur le runner. La première qui échoue arrête l'exécution : les suivantes ne sont pas jouées, et les sondes non plus.

Il existe huit types d'étapes, et il n'y en a pas d'autres.

Type Ce qu'il fait
fetch Rapatrie la sauvegarde dans l'espace de travail.
unpack Décompresse une archive.
start_sandbox Démarre un conteneur (base de données, service).
exec_in_sandbox Exécute une commande dans le conteneur.
restore_postgres Rejoue un dump PostgreSQL.
restore_mysql Rejoue un dump MySQL / MariaDB.
restore_mongo Rejoue un dump MongoDB.
restore_ldap Recharge un LDIF.

L'enchaînement habituel est fetch → unpack → start_sandbox → restore_*, puis les sondes. Il n'y a plus d'étape wait : c'est le contrôle de santé du bac à sable, dans start_sandbox, qui attend désormais que le conteneur soit prêt. Un plan enregistré qui porte encore une étape wait échoue avec un message clair : l'attente fait maintenant partie du démarrage du conteneur.

La durée d'une exécution

Le délai d'expiration du plan (timeout_seconds, une heure par défaut) borne toute l'exécution, étapes et sondes comprises. Le runner n'accepte pas plus de 24 heures : un délai plus long est ramené à 24 heures, et le runner l'écrit dans son journal. Quand il est dépassé, le runner arrête ce qui tourne, détruit les bacs à sable et les conteneurs de sondes, et l'exécution échoue avec le message The run exceeded its timeout of … (timeout_seconds of the plan). Une restauration qui dure normalement plus longtemps demande un délai plus long dans le plan.

Le nombre d'exécutions par heure

Un runner ne démarre pas plus de 30 exécutions par heure glissante, essais compris. Au-delà, il laisse l'exécution en attente sans la prendre : RestoreProof la lui propose de nouveau toutes les quelques secondes, et elle démarre dès qu'une place se libère dans l'heure. Le runner écrit dans son journal qu'il a laissé une exécution en attente et dans combien de temps une place se libère. La limite se change avec RESTOREPROOF_MAX_RUNS_PER_HOUR (jobs.max_per_hour dans la configuration du runner, jobs.maxPerHour dans le chart Kubernetes).

Trente exécutions par heure laissent de la marge à un usage normal : même l'offre la plus large, 5 000 exécutions par mois, ne fait en moyenne que sept exécutions par heure si un seul runner les porte toutes. La limite borne en revanche ce qu'un plan pourrait apprendre de vos données à force de les interroger : chaque sonde ne rapporte qu'un verdict et des comptes, et le nombre d'exécutions borne le nombre de ces réponses.

Ce qui part vers RestoreProof

Le runner exécute des commandes dans les bacs à sable : exec_in_sandbox, les outils des étapes de restauration, docker run et docker cp, les contrôles de santé. Leur sortie peut contenir des données de votre sauvegarde, elle ne quitte donc jamais la machine du runner. Quand une commande échoue, RestoreProof reçoit le nom de l'étape, le code de sortie et une seule ligne de 200 octets au plus : la dernière qui ressemble à une erreur, ou à défaut la dernière ligne. La sortie complète est écrite dans les journaux du runner (docker logs restoreproof-runner), et le message se termine par (full output in the runner log) pour vous le rappeler.

La sortie des sondes suit la même règle : elle reste dans les journaux du runner, et le rapport ne garde, pour chaque sonde, que son verdict et un message structurel (voir ce que les sondes rapportent).

Toute valeur de secret résolue pendant l'exécution est en plus remplacée par [REDACTED] dans tout ce qui est envoyé, y compris dans cette ligne. Voir Fournir les secrets au runner.

Ce que le runner décide seul

Le nom du conteneur, l'utilisateur de connexion, le port et la commande de santé d'un bac à sable ne s'écrivent plus dans le plan. Le runner les reconnaît à l'image demandée par start_sandbox.image :

Famille d'image Utilisateur Port Commande de santé DN d'administration
postgres* postgres 5432 pg_isready -U postgres —
mysql*, mariadb*, percona* root 3306 mysqladmin ping -h 127.0.0.1, ou mariadb-admin pour une image qui n'a que ce nom (MariaDB 11) —
mongo* (aucune authentification) 27017 mongosh --quiet --eval "db.adminCommand('ping')" —
*openldap* (images Bitnami) — 1389 ldapsearch -x -H ldap://localhost:1389 -b "" -s base namingContexts cn=admin,<base_dn>
*389ds*, *dirsrv* — 3389 la même recherche, sur le port 3389 cn=Directory Manager
*lldap* — 3890 /app/lldap healthcheck --config-file /data/lldap_config.toml uid=admin,ou=people,<base_dn>
*pocketbase* — 8090 wget --no-verbose --tries=1 --spider http://localhost:8090/api/health —
wordpress* — 80 curl -f http://localhost:80/ —

Ces valeurs alimentent trois choses à la fois : le contrôle de santé de start_sandbox, la connexion de l'étape restore_postgres / restore_mysql / restore_mongo / restore_ldap qui suit, et celle de toute sonde qui vise ce bac à sable. <base_dn> du DN d'administration vient de la variable de base DN déjà posée dans l'env de start_sandbox — LDAP_ROOT pour les images Bitnami d'OpenLDAP, DS_SUFFIX_NAME pour 389ds, LLDAP_LDAP_BASE_DN pour lldap — pas d'un nouveau champ : c'est la même variable que le modèle y écrit déjà pour que le bac à sable serve le bon suffixe. RESTOREPROOF_LDAP_BASE_DN sur la sonde ldap reste un champ séparé, celui de son propre contrôle de comptage, et n'entre pas dans la composition de ce DN.

L'échappatoire existe à deux endroits, pas partout. Un healthcheck posé dans start_sandbox, ou une variable d'environnement posée à la main dans la configuration d'une sonde, l'emportent toujours sur ce que le runner aurait dérivé — c'est la sortie de secours pour une image maison, ou pour une sonde qui vise un autre bac à sable que celui que le runner aurait choisi. Elle ne peut pas viser un service hors de l'exécution : voir l'isolement réseau. L'étape de restauration qui suit start_sandbox, elle, exécute toujours son outil à l'intérieur du conteneur qu'elle vient de démarrer : host, port, user, bind_dn, base_dn et auth_database n'y ont donc plus de sens et ne sont plus lus, sans échappatoire — viser autre chose que ce conteneur-là n'est pas ce que fait cette étape. Le nom du conteneur, lui, n'est plus un réglage nulle part : il n'y a plus rien à lui opposer.

L'alias réseau

Chaque exécution a son propre réseau Docker, et chaque bac à sable y reçoit un alias réseau plutôt qu'un nom choisi dans le plan : sandbox pour le premier start_sandbox du plan, sandbox-2 pour le second, sandbox-3 pour le troisième, et ainsi de suite dans l'ordre où les étapes apparaissent. Le nom réel du conteneur, lui, est unique à chaque exécution (rp-<identifiant_court>-<n>) — il ne sert à rien dans un plan, seul l'alias compte.

C'est cet alias que les sondes et les modèles écrivent : une URL de sonde HTTP ou PocketBase s'écrit http://sandbox:8090/api/health, et un plan à deux bacs à sable — WordPress, par exemple — vise explicitement sandbox-2 dans la configuration de sa seconde sonde. Un client qui connaît l'alias peut l'utiliser tel quel, sans rien savoir d'aléatoire.

L'isolement réseau

Le réseau d'une exécution est fermé. Les bacs à sable et les sondes de l'exécution s'y joignent entre eux par leur alias, et ne joignent rien d'autre : ni Internet, ni le réseau de votre entreprise, ni la machine qui héberge le runner, ni les conteneurs d'une autre exécution. Aucun port n'est publié sur l'hôte, et le runner lui-même ne rejoint pas ce réseau : il pilote les conteneurs par Docker, et les restaurations passent par docker exec.

C'est voulu. Le bac à sable fait tourner le contenu de votre sauvegarde, et une sauvegarde peut contenir n'importe quoi : un déclencheur SQL, une extension, un plugin WordPress qui appelle une adresse extérieure au démarrage. Sur ce réseau, cet appel échoue sans rien atteindre.

Une conséquence pratique : l'image d'un bac à sable doit pouvoir démarrer sans rien télécharger. Une image qui installe un paquet ou récupère un fichier au démarrage échoue ici ; il faut construire une image qui contient déjà ce dont elle a besoin.

Comment un fichier est retrouvé

Plusieurs étapes prennent un champ input. Sa résolution est la même partout, et elle est plus tolérante qu'un simple nom de fichier.

  1. Si input ne contient pas de joker et que le fichier existe à la racine de l'espace de travail, c'est celui-là.
  2. Sinon, input est traité comme un motif (*.sql, dump-*.tar.gz).
  3. Si rien ne correspond à la racine, la recherche descend dans les sous-répertoires : le nom est comparé au nom de chaque fichier de l'arborescence.
  4. Si plusieurs fichiers correspondent, le plus gros gagne — une sauvegarde arrive rarement seule, à côté d'une somme de contrôle et d'un manifeste.

C'est ce qui fait qu'une archive construite normalement — tar czf backup.tar.gz backup/, qui se décompresse en backup/dump.sql et non dump.sql — fonctionne avec input: "dump.sql", sans que vous ayez à écrire la disposition interne de l'archive dans le plan.

Un dump PostgreSQL au format répertoire est un répertoire : il bénéficie de la même tolérance. Les fichiers l'emportent quand les deux existent.

input est toujours un chemin relatif à l'espace de travail. À l'enregistrement du plan, un chemin absolu ou un chemin qui en sort par .. est refusé pour l'input des étapes de restauration et d'unpack, et pour l'output de fetch et d'unpack. À l'exécution, le runner refuse de même tout input absolu et tout output absolu de fetch ; pour l'output d'un unpack, il accepte un chemin absolu seulement s'il désigne un endroit de l'espace de travail. Un lien symbolique qui reste dans l'espace de travail est suivi normalement ; un lien qui en sort est refusé, et cette vérification se fait au moment même où le runner ouvre le fichier, ou parcourt un répertoire pour un motif comme *.sql. Sans cette règle, un lien vers un fichier du runner lui-même (sa clé de signature, ses secrets) ferait envoyer ce fichier dans le bac à sable.

Cette résolution ne s'applique pas à exec_in_sandbox

Le champ cmd de exec_in_sandbox est exécuté sans shell. Un /workspace/*/schema.ldif y reste un caractère * littéral, il n'est jamais développé. Voir exec_in_sandbox.

Les secrets

Partout où une valeur peut être un secret — clé S3, mot de passe de base, variable d'environnement d'un conteneur — vous écrivez une référence, jamais la valeur. Elle commence toujours par secret://. Le Control Plane ne voit que cette référence ; le runner la résout chez lui.

Le runner retire le secret:// de tête. Ce qui reste s'écrit sous l'une des trois formes suivantes, jamais autrement :

Référence Ce que le runner lit
secret://env://MA_VAR La variable d'environnement MA_VAR du processus du runner. Si son fichier de configuration donne un env_prefix, il est ajouté devant le nom. Une variable vide ou absente fait échouer la résolution, tout comme une variable dont le nom commence par RESTOREPROOF_ : celles-là règlent le runner et ne sont jamais lues.
secret://file://ma_cle Le fichier ma_cle, cherché dans le répertoire file_dir de la configuration du runner quand le chemin est relatif. Le contenu est lu, espaces et retour à la ligne de fin retirés.
secret://static://ma_cle La clé ma_cle de la table static: de la configuration du runner.

vault:// et sops:// sont reconnus mais répondent « pas encore pris en charge » : la résolution échoue, l'étape s'arrête.

Pour savoir où poser la valeur derrière chacune de ces références, selon votre plateforme de déploiement, voir Fournir les secrets au runner.

La clé nue est l'ancienne écriture

Le runner accepte encore une clé écrite sans schéma — secret://ma_cle — et la cherche dans la table static:. C'est l'ancienne forme. Écrivez secret://static://ma_cle : le schéma dit où chercher, et une source déclarée dans l'application n'accepte plus que les trois formes explicites.

Ce que la table static: ne fait pas

La valeur écrite dans static: est prise telle quelle. Y mettre env://MA_VAR ne déclenche pas une seconde résolution : le runner rendrait la chaîne env://MA_VAR comme mot de passe. Pour lire une variable d'environnement ou un fichier, écrivez la référence directement dans le plan — secret://env://MA_VAR, secret://file://ma_cle — et gardez static: pour des valeurs littérales.

Les trois formes échouent franchement quand elles ne se résolvent pas : une clé absente de static:, une variable d'environnement vide, un fichier illisible arrêtent l'étape en le disant.

Le mot de passe du bac à sable : rien à écrire

Le conteneur jetable de l'étape start_sandbox — un PostgreSQL démarré pour trois minutes puis détruit — n'a pas de clé de mot de passe dans le plan, ni dans son environnement, ni dans l'étape de restauration, ni dans la configuration d'une sonde qui le vise. Il n'y a rien à saisir.

Le runner reconnaît la famille de l'image démarrée (PostgreSQL, MySQL/MariaDB, WordPress, OpenLDAP, 389 Directory Server, lldap), fabrique les identifiants qu'elle demande, les injecte dans l'environnement du conteneur et fournit les mêmes valeurs à l'étape de restauration et à toute sonde qui vise ce conteneur. WordPress, qui démarre deux conteneurs, est le seul cas où ils doivent se mettre d'accord : le runner s'appuie sur le WORDPRESS_DB_HOST déjà posé dans le plan pour relier les deux, sans rien à ajouter.

Rien de tout cela n'est envoyé au Control Plane ni écrit dans le rapport ou les journaux : ces valeurs n'existent que dans le conteneur jetable, le temps de l'exécution.

Une valeur écrite explicitement dans le plan — un start_sandbox.env, le password d'une étape de restauration, la configuration d'une sonde — l'emporte toujours sur celle que le runner aurait fabriquée.

Rupture : un runner qui ne connaît pas encore cette fabrication ne trouve aucun mot de passe à lire dans un plan généré aujourd'hui et démarre le conteneur sans, ce qui fait échouer la restauration. Mettez le runner à jour avant de lui confier des plans construits avec les modèles actuels.


fetch

Champ Rôle
source source_profile (le chemin normal) ; s3 ou minio ; http ou https ; local, file ou nfs.
source_id L'identifiant de la source déclarée, quand source vaut source_profile.
output Nom du fichier dans l'espace de travail.
max_age Âge maximal de la sauvegarde à la source, par exemple 26h.

source_profile est la forme à privilégier : la source déclarée une fois dans l'écran Sources porte l'adresse, le chemin et les références de secrets ; l'étape dit seulement lequel prendre (key, prefix, file_pattern, selection_strategy, max_age, output). Le runner part de la source, résout ses secrets chez lui, puis applique par-dessus les champs de l'étape. Les formes ci-dessous restent exécutées, mais elles recopient dans le plan ce qu'une source décrit déjà.

Les références portées par une source s'écrivent sans le secret:// de tête : la source ne contient que des références, le préfixe n'y distingue rien. env://S3_SECRET_KEY dans une source, secret://env://S3_SECRET_KEY dans une étape de plan.

Le runner ne télécharge que depuis les hôtes que vous lui avez listés et ne lit que les dossiers que vous lui avez listés : voir ce que le runner a le droit d'atteindre. Un fetch hors de ces listes échoue, et son message nomme la variable et l'entrée à ajouter.

La taille d'un téléchargement est bornée par RESTOREPROOF_FETCH_MAX_BYTES (fetch.max_bytes dans la configuration du runner), 512 Gio par défaut. La variable accepte une unité : 100g, 512m. Au-delà, l'étape échoue et le fichier partiel est supprimé ; un serveur qui annonce d'emblée une taille supérieure est refusé avant le premier octet.

S3 / MinIO

Champ Rôle
bucket Le bucket.
key La clé exacte de l'objet.
prefix Le préfixe / répertoire dans lequel chercher.
endpoint Le point d'accès régional du fournisseur, sans le nom du bucket (https://s3.fr-par.scw.cloud, https://s3.eu-west-1.wasabisys.com, http://minio:9000). Vide pour AWS.
region La région.
access_key, secret_key Références secret://. Sans elles, le runner utilise sa chaîne d'identification par défaut (variables AWS, rôle de l'instance), et ne présente ces identifiants qu'aux hôtes autorisés.
selection_strategy latest_modified ou latest_name, pour prendre la dernière sauvegarde d'un préfixe au lieu d'une clé fixe.
file_pattern Motif filtrant les objets du préfixe, par exemple *.sql.gz.

HTTP / HTTPS

Champ Rôle
url L'URL à télécharger.
headers En-têtes supplémentaires, par exemple pour une URL authentifiée. Les valeurs acceptent une référence secret://. Ils ne partent que vers un hôte autorisé, redirections comprises.
insecure true pour ne pas vérifier le certificat.

headers et insecure n'ont pas de champ dans l'éditeur de plan : ils s'écrivent en configuration brute de l'étape, dans l'onglet JSON (expert).

Local / NFS

Champ Rôle
path Le chemin absolu sur la machine du runner. Il doit se trouver, une fois les liens symboliques résolus, dans l'un des dossiers de RESTOREPROOF_LOCAL_ROOTS. Il ne peut pas non plus désigner les fichiers du runner lui-même, ni les contenir : son répertoire de travail, ses secrets, son fichier de configuration et /proc sont refusés, y compris à travers un lien symbolique.
selection_strategy latest_modified ou latest_name, pour prendre la dernière sauvegarde du répertoire au lieu d'un fichier fixe. Comme pour S3.
file_pattern Motif filtrant les fichiers du répertoire, par exemple *.sql.gz. Comme pour S3.

Pendant la copie d'un répertoire, un lien symbolique qui pointe vers un fichier du même répertoire est copié comme ce fichier ; un lien vers un répertoire n'est pas suivi, son contenu est déjà copié depuis son vrai emplacement. Un lien qui sort du répertoire fait échouer l'étape : rien en dehors de ce que path désigne n'est copié.

max_age

C'est la seule chose qu'un test de restauration ne peut pas déduire des données elles-mêmes. Une tâche de sauvegarde arrêtée depuis trois mois produit un fichier qui se restaure parfaitement.

max_age fait échouer l'étape quand l'objet à la source est plus vieux que la durée donnée. La date utilisée est celle de la source, pas celle de la copie. Si la source ne sait pas dire quand l'objet a été écrit, l'étape échoue aussi : une garantie invérifiable n'est pas une garantie.

Ce qui entre dans le rapport

Après un fetch réussi, le runner identifie le fichier qui vient d'apparaître, calcule son SHA-256 et l'inscrit dans le rapport signé : nom, source, bucket, préfixe, URL, taille, empreinte, date de modification à la source. Un mot de passe présent dans l'URL est retiré avant.

Si plusieurs fichiers sont apparus d'un coup, rien n'est inscrit : une identité fausse dans une preuve est pire qu'une identité absente.


unpack

Champ Rôle
input L'archive. Vide : le plus gros fichier de l'espace de travail est pris.
format Vide ou auto : déduit de l'extension.
output Pour une archive (tar*, zip) : le répertoire de destination dans l'espace de travail. Pour gzip, qui ne contient qu'un fichier : le nom du fichier produit. Vide, pour gzip : le nom de l'archive sans son .gz.

Formats acceptés : tar, tar.gz, tgz, tar.bz2, tbz2, tar.xz, txz, gzip, gz, zip.

La décompression est bornée par la même limite que les téléchargements, fetch.max_bytes : une archive, ou un fichier gzip, qui se décompresse au-delà fait échouer l'étape, et le fichier partiel est supprimé.

Détection automatique, par suffixe : .tar.gz et .tgz → tar.gz ; .tar.bz2 et .tbz2 → tar.bz2 ; .tar.xz et .txz → tar.xz ; .tar → tar ; .gz → gzip ; .zip → zip. Un nom qui ne finit par aucun de ces suffixes fait échouer l'étape.


start_sandbox

Démarre le conteneur dans lequel les données seront restaurées.

Champ Rôle
image L'image Docker.
env Variables d'environnement. Une valeur peut être une référence secret://.
volumes Des parties de l'espace de travail à monter dans le conteneur, voir ci-dessous.
command Commande de l'image, si vous la remplacez.
healthcheck Voir ce que le runner décide seul. Le vôtre l'emporte s'il est présent.

Le runner place le bac à sable et les sondes sur le réseau fermé de l'exécution, et lui donne l'alias sandbox (sandbox-2, sandbox-3… pour les suivants) : c'est ce qui permet à une sonde de joindre le conteneur, et à un bac à sable de joindre l'autre. Le plan ne choisit ni le réseau ni les ports publiés : une étape qui porte encore un champ network ou ports est refusée à l'enregistrement, et un plan déjà enregistré qui en porte un échoue avec un message qui le dit. Le nom du conteneur n'est pas non plus un réglage : le runner en fabrique un unique à chaque exécution.

Un volume s'écrit ${WORKSPACE}/<chemin>:<chemin dans le conteneur>, éventuellement suivi de :ro (lecture seule) ou :rw, par exemple ${WORKSPACE}/pb_data:/pb_data. ${WORKSPACE} seul monte tout l'espace de travail. La source vient toujours de l'espace de travail de l'exécution : un chemin de la machine (/etc, /var/run/docker.sock), un volume Docker nommé ou un chemin qui en sort par .. sont refusés, à l'enregistrement du plan comme au démarrage du conteneur. Le runner refuse aussi une source qui n'existe pas, ou qui passe par un lien symbolique : une archive restaurée peut contenir un lien pb_data -> /var/run, et Docker le suivrait sur la machine du runner. Le chemin dans le conteneur doit être absolu.

L'espace de travail entier est toujours monté sur /workspace, en lecture seule : les étapes de restauration et exec_in_sandbox y lisent les fichiers, aucune n'y écrit. Seuls les volumes que vous déclarez sans :ro sont modifiables par le conteneur. Comme un bac à sable fait tourner le contenu de la sauvegarde, un dossier qu'il a pu modifier n'est plus sûr pour les suivants : un start_sandbox qui monte un chemin situé à l'intérieur d'un volume qu'un bac à sable précédent de la même exécution monte en écriture est refusé. Montez ce volume avec :ro dans le premier bac à sable, ou montez le même dossier, pas un sous-dossier.

Le conteneur démarre aussi avec des droits réduits. Le runner lui retire toutes les capacités Linux sauf six, celles dont les images de bases de données et d'annuaires ont besoin pour démarrer : changer le propriétaire de leurs fichiers (CHOWN, DAC_OVERRIDE, FOWNER), passer de root à leur propre utilisateur (SETUID, SETGID), et exécuter le serveur LDAP des images OpenLDAP de Bitnami et de 389ds, dont le programme est marqué pour écouter sur un port réservé (NET_BIND_SERVICE). Il l'empêche aussi d'en regagner en cours de route (no-new-privileges). Les sondes, elles, démarrent sans aucune capacité.

Ses ressources sont bornées : 2 Gio de mémoire, 2 processeurs et 1 024 processus par défaut. Les variables RESTOREPROOF_SANDBOX_MEMORY (4g, 512m), RESTOREPROOF_SANDBOX_CPUS (1.5) et RESTOREPROOF_SANDBOX_PIDS (2048) les changent ; dans la configuration du runner, ce sont sandbox.memory_limit (en octets), sandbox.cpu_limit (en milliardièmes de processeur, 2000000000 pour deux) et sandbox.pids_limit. Sur Kubernetes, le chart les pose à partir de sandbox.memory, sandbox.cpus et sandbox.pids. Ces valeurs laissent de la marge aux images testées : une restauration d'environ 480 Mo dans PostgreSQL 16, MySQL 8.4, MariaDB 11 ou MongoDB 7 est restée sous 700 Mio et 60 processus. Une sauvegarde qui lancerait des processus sans fin s'arrête à la limite, sans toucher à la machine.

L'étape attend que le conteneur soit sain avant de continuer — par le healthcheck que vous avez fourni, ou par celui que le runner dérive de l'image. L'attente totale vaut interval × retries du contrôle utilisé ; si l'un des deux manque, elle vaut 60 secondes.

Le conteneur et ses volumes sont détruits à la fin de l'exécution, quel qu'en soit le résultat.


exec_in_sandbox

L'échappatoire pour la préparation qu'aucune autre étape ne couvre : start_sandbox ne fait que démarrer le conteneur, et certaines images ont besoin d'une commande à l'intérieur avant d'être utilisables — 389-ds, par exemple, ne crée pas le backend du suffixe qu'on lui demande de servir.

Champ Rôle
cmd La commande et ses arguments. Obligatoire.
env Variables d'environnement de la commande. Les valeurs secret:// sont résolues.
ignore_failure true pour qu'un code de retour non nul ne soit pas fatal. Défaut : false.

La commande est exécutée dans le dernier conteneur démarré. Sans start_sandbox avant, l'étape échoue.

cmd : sans shell, jamais

C'est le point sur lequel les plans se cassent le plus souvent.

cmd est exécuté directement dans le conteneur, pas à travers un shell. Il n'y a donc :

  • aucun développement de joker — /workspace/*/schema.ldif reste littéral ;
  • aucune substitution de variable — $HOME reste $HOME ;
  • aucune redirection ni tube — >, |, && sont des arguments comme les autres.

Deux écritures sont acceptées :

{"cmd": ["ldapadd", "-Y", "EXTERNAL", "-H", "ldapi:///", "-f", "/workspace/schema.ldif"]}
{"cmd": "ldapadd -Y EXTERNAL -H ldapi:/// -f /workspace/schema.ldif"}

La forme chaîne est découpée aux espaces, mais un fragment entre guillemets simples ou doubles reste un seul argument — un suffixe LDAP comme "dc=example, dc=org" contient des espaces et des virgules et serait déchiré par une découpe naïve. Rien d'autre du comportement d'un shell n'est implémenté.

Si vous avez vraiment besoin d'un shell, appelez-le explicitement :

{"cmd": ["sh", "-c", "ldapadd -f /workspace/*/schema.ldif"]}

La sortie de la commande reste sur la machine du runner : elle est écrite dans ses journaux, tronquée à 32 Kio. Si la commande échoue, la page de l'exécution montre son code de sortie et une seule ligne de sa sortie, comme décrit dans ce qui part vers RestoreProof.


Les étapes de restauration

Les quatre étapes restore_* partagent la même configuration et le même principe : le runner exécute l'outil de restauration à l'intérieur du bac à sable, sur le fichier de l'espace de travail. Elles exigent toutes qu'un start_sandbox les précède.

Champ Rôle
input Le dump. Résolu comme décrit plus haut.
password Le mot de passe, ou une référence secret://.
database La base cible. C'est ici, et nulle part ailleurs, que le nom de la base s'écrit : le runner le transmet aux sondes du plan de la même famille.
format Vide : détecté à partir du fichier.
options Options supplémentaires passées à l'outil.
stop_on_error Voir ci-dessous.

L'hôte, le port et l'utilisateur de connexion ne s'écrivent plus : voir ce que le runner décide seul.

stop_on_error

Non renseigné vaut true.

psql et pg_restore continuent après une erreur SQL et sortent en 0 : un dump à moitié appliqué serait rapporté comme une restauration réussie. C'est exactement la fausse preuve que ce produit existe pour empêcher.

Ne le mettez à false que pour un dump qui référence sciemment des rôles, des extensions ou des tablespaces absents du bac à sable.

restore_postgres

Base par défaut restoreproof quand database est vide.

Formats détectés : plain, custom, directory, tar. Un dump plain est rejoué par psql ; les trois autres passent par pg_restore, à qui le runner donne toujours --no-owner --no-privileges — un bac à sable neuf n'a ni les propriétaires ni les droits de la production.

Les rôles cités par un dump SQL en clair sont créés avant de le rejouer. Un pg_dump sans option porte des OWNER TO gitea, des GRANT … TO app_ro, des SET SESSION AUTHORIZATION : autant de rôles qui existent en production et pas dans un bac à sable neuf. Le runner lit le dump, relève ces noms et crée les rôles manquants dans le bac à sable, sans mot de passe et sans droit de connexion, puis le dit dans le journal de l'exécution. Le dump se rejoue donc tel quel, avec l'arrêt à la première erreur conservé.

Trois précisions : l'utilisateur de connexion (postgres) n'est jamais recréé, les rôles internes de PostgreSQL (public, postgres, pg_*) sont ignorés, et seuls les dumps en SQL en clair sont concernés — un dump custom ou directory passe par pg_restore, à qui il faut toujours dire quoi faire des propriétaires.

restore_mysql

Formats détectés : sql et mysqlpump, rejoués par le client mysql (ou mariadb pour une image qui n'a que ce nom, comme MariaDB 11) ; shell, le répertoire de mysqlsh util.dumpSchemas, rejoué par mysqlsh. Un dump --all-databases est reconnu et la base cible est alors laissée vide, puisque le dump porte ses propres CREATE DATABASE. C'est le seul cas où une sonde MySQL doit nommer sa base elle-même, dans son option avancée Base à interroger : le pas de restauration n'en désigne aucune.

Une sauvegarde XtraBackup est reconnue elle aussi, mais elle n'est pas restaurée : l'étape échoue en le disant, parce qu'un XtraBackup se remonte avec --prepare puis --copy-back sur les fichiers du serveur, ce qu'aucune étape ne fait aujourd'hui.

restore_mongo

Formats détectés : archive (un fichier, avec ou sans --gzip) et directory (un répertoire de .bson).

database est obligatoire : mongorestore recrée la base sous le nom qu'elle porte dans le dump, et il n'y a pas de nom par défaut qui aurait un sens. Ce nom est aussi celui que les sondes MongoDB du plan interrogent.

L'archive est inspectée avant d'être rejouée. mongorestore sort en 0 quand rien ne correspond à la base demandée : une archive qui ne contient pas votre base serait rapportée comme restaurée, et seule la sonde verrait le vide. Le runner fait donc d'abord passer la restauration à blanc. Si aucune collection ne correspond, l'étape échoue en listant ce que l'archive contient vraiment, sous la forme base.collection — de quoi voir immédiatement qu'on a écrit shop là où la sauvegarde dit boutique. Une archive vide est refusée de la même façon, et une restauration qui se termine sans avoir écrit un seul document échoue aussi.

restore_ldap

L'hôte, le port et le DN d'authentification (le rootdn, typiquement cn=admin,dc=example,dc=com — LDAP n'a pas d'équivalent à user) viennent de la famille d'image, comme décrit plus haut : plus besoin de corriger le port pour une image Bitnami, ni d'écrire le DN administrateur.

Le LDIF est rejoué en ligne avec ldapadd, pas avec slapadd.

Un export slapcat ou db2ldif contient des attributs opérationnels que le serveur cible régénère lui-même : entryUUID, entryCSN, creatorsName, createTimestamp, structuralObjectClass, memberOf… ldapadd les refuse, parce qu'ils appartiennent au serveur. Le runner écrit donc à côté de l'original une copie nettoyée, nommée <base>.rp-load — sans l'extension .ldif, pour qu'un input: "*.ldif" ne puisse jamais la reprendre pour la sauvegarde. Le fichier d'origine n'est jamais modifié.

Le champ format accepte ici auto (défaut : le mode est déduit de la présence de ces attributs), sanitize (nettoyer, toujours) et plain (rejouer tel quel, pour un LDIF écrit à la main ou exporté par une application).

Ce que cette restauration prouve, et ce qu'elle ne prouve pas

Les entrées nettoyées sont rechargées avec de nouveaux entryUUID, entryCSN et horodatages, produits par le serveur du bac à sable. Ce qui est prouvé, c'est que les données et les mots de passe de l'annuaire reviennent et sont utilisables par le protocole standard — pas que la base interne du serveur est identique octet pour octet à celle qui a été sauvegardée.

Le schéma est vérifié avant d'écrire la première entrée. Le runner compare les types d'attributs et les classes d'objets utilisés par le dump à ceux que l'annuaire du bac à sable déclare dans son sous-schéma. S'il en manque, l'étape échoue en donnant la liste complète, au lieu du Undefined attribute type (17) que ldapadd rend une erreur à la fois. Voir le dépannage.

Si le contrôle ne peut pas s'exécuter — un serveur qui ne répond pas à une requête sur son root DSE — il est simplement ignoré, avec un avertissement dans les journaux : un serveur inhabituel n'est pas la preuve d'une mauvaise sauvegarde.