Comment contrôler le temps dans un container Docker
Les tests automatisés dépendant de la date sont instables. Pour remédier à ce problème, on peut les exécuter dans un container dont on fixe la date. Mais est-ce que ce pouvoir n'est pas trop dangereux ?
Problème : Comment stabiliser des tests sensibles à la date ?
Sur un projet client, j'ai ajouté une fonctionnalité mettant en évidence les items dont c'était le dernier jour de disponibilité.
Problème: Il existait des tests e2e qui testaient la réponse de l'api.
expect(json).toEqual(program);
Avec cette nouvelle fonctionnalité, les tests ne dépendaient plus uniquement des fixtures en entrée. Désormais, le résultat du test changeait en fonction de la date courante.
Une solution était de générer des fixtures à date variable. Mais cela demandait beaucoup de modifications, notamment pour garder des dates réalistes dans des tables liées. J'ai donc cherché une solution moins coûteuse.
Et si, au lieu de changer les fixtures, on changeait la date du serveur ?
On ne peut pas changer la date d'un container Docker
Sur ce projet, nos tests tournent dans Docker. Si je change la date du systéme dans le container docker, je peux fixer la date du test, et son résultat ne dépendra plus de la date courante.
Super simple : je change la date dans le container via une commande bash.
docker exec my_node_container bash 'date -s \'1985-10-25 12:00:00\''
Sauf que non, cette opération n'est pas permise :
date: cannot set date: Operation not permitted
Il s'avère que Docker ne permet pas de changer la date, et ce pour une trés bonne raison : il utilise la même horloge que le noyau externe du système hôte.
Comment faire alors ?
Mocker la date d'un container avec libfaketime
Heureusement, il existe un projet appelé libfaketime. Cette librairie intercepte les appels systèmes censés récupérer la date, et leur injecte la valeur désirée. En installant libfaketime
dans l'image Docker, il devient donc possible de contrôler le temps.
libfaketime
s'installe comme ceci:
git clone https://github.com/wolfcw/libfaketime.git
cd /libfaketime/src
make install
Dans un dockerfile
, cela devient:
# feel free to replace with the image you want
FROM node:15.14
WORKDIR /
# install git
RUN apt-get update && \
apt-get upgrade -y && \
apt-get install -y git
# install libfaketime
RUN git clone https://github.com/wolfcw/libfaketime.git
WORKDIR /libfaketime/src
RUN make install
# do other things as needed
Remarque: Il est également possible d'utiliser un build multistage pour que seul libfaketime soit installé dans l'image final.
Je peux ensuite contruire l'image en lui donnant un nom - ici, faketime_node
:
docker build . -t faketime_node
Définir une fausse date système via des variables d'environnement
Maintenant que je dispose d'une image contenant libfaketime
, comment l'utiliser pour changer la date ?
Il suffit d'ajouter deux variables d'environnement:
LD_PRELOAD=/usr/local/lib/faketime/libfaketime.so.1
LD_PRELOAD
permet de précharger une librairie avant toutes les autres y compris les librairies système. Ce qui permet de charger libfaketime
en tout premier, et ainsi lui permettre de modifier le temps.
FAKETIME="1985-10-25 12:00:00"
FAKETIME
permet de configurer le temps. Cette variable d'environnement accepte une date au format "YYYY-MM-DD hh:mm:ss"
. Elle accepte également une date relative sous la forme d'un chiffre suivi d'une unité. -15d
pour il y a 15 jours par exemple.
Il suffit donc de lancer le serveur testé avec ces deux variables d'environnement, pour l'envoyer dans une autre époque (façon de parler) :
docker run -d \
-e LD_PRELOAD='/usr/local/lib/faketime/libfaketime.so.1' \
-e FAKETIME='1985-10-25 12:00:00' \
faketime_node
On peut aussi utiliser docker-compose
:
version: '3.5'
services:
node:
build: ./dockerfile/directory
environment:
- LD_PRELOAD=/usr/local/lib/faketime/libfaketime.so.1
- FAKETIME='2021-03-01 12:00:00'
command: 'your command'
Relancer le temps
Sauf qu'à ce stade, je suis tombé sur un problème pour le moins gênant. Le serveur Node ne démarrait plus. La commande se lançait bien, mais ensuite plus rien.
J'ai débuggé: le temps était bien modifié. Je pouvais afficher la date, qui était bien celle attendue. Mais lorsque je démarrais le serveur Node, plus rien.
A ce stade, j'ai bien failli abandonner. Et c'est alors que j'ai réalisé. Je n'étais pas simplement retourné dans le passé.
J'avais stoppé le temps.
En effet pour lancer le serveur, Node a besoin de réaliser des opérations asynchrones. Des opérations qui utilisent la boucle d'événement. Il ajoute des événements à la boucle, pour y retourner plus tard, c'est à dire au process.nextTick()
. Sauf que si le temps du système est gelé, le nextTick
ne se déclenche jamais.
Avec une date fixée par libfaketime
, pas une seule milliseconde ne s'écoule. Donc Node attend que le temps s'écoule ... indéfiniment ...
Fort heureusement, libfaketime
permet de changer la date, tout en laissant le temps s'écouler. Il suffit de préfixer FAKETIME
avec un @
.
FAKETIME='@2021-03-01 12:00:00'
Et maintenant tout fonctionne. On peut régler la date du container et avoir des tests stables... du moins en ce qui concerne la date.
En résumé
Il est possible de changer la date dans un container Docker en utilisant libfaketime
.
Pour cela, créer un dockerfile
semblabe à :
# feel free to replace with the image you want
FROM node:15.14
WORKDIR /
# install git
RUN apt-get update && \
apt-get upgrade -y && \
apt-get install -y git
# install libfaketime
RUN git clone https://github.com/wolfcw/libfaketime.git
WORKDIR /libfaketime/src
RUN make install
# do other things as needed
Puis passer ces deux variables d'environment au moment de lancer l'image:
LD_PRELOAD='/usr/local/lib/faketime/libfaketime.so.1'
FAKETIME='@YYYY-MM-DD hh:mm:ss'
(complétez avec la date voulue). Le@
est important pour laisser le temps s'écouler.