Je vous décris mon besoin. Je suis en train de construire avec
ansible un outil permettant de construire une
trentaine d'images docker à partir de templates de Dockerfile et de
fichiers de test. Dans un premier il récupère les dernières versions des outils
utilisés avec lastversion pour les injecter et produire les Dockerfile et
les fameux fichiers de test. Le tout tourne dans gitlab, via un ci déclenché
une fois par semaine. Dans ce ci, dynamique,
les tests sont exécutés en utilisant container-structure-test
. En cas de
succès l'image est envoyé dans la registry docker de
gitlab et est prête à l'utilisation.
Container-structure-test est un outil qui comme son nom l'indique permet de valider la structure d'une image.
Installation de container-structure-test
L'installation se fait classiquement via une commande curl :
curl -LO https://storage.googleapis.com/container-structure-test/latest/container-structure-test-linux-amd64 && chmod +x container-structure-test-linux-amd64 && mkdir -p $HOME/bin && export PATH=$PATH:$HOME/bin && mv container-structure-test-linux-amd64 $HOME/bin/container-structure-test
Ecriture du fichier de test
Le fichier de configuration contenant les tests est un fichier écrit en json ou en yaml.
Le fichier débute toujours par une entête de ce type :
schemaVersion: '2.0.0'
Ensuite dans ce fichier de configuration, on peut utiliser quatre types de tests :
- Tests sur les commandes : on peut tester la sortie ou les erreurs d'une commande
- Tests d'existence de fichiers : on peut contrôler la présence ou pas d'un fichier dans le système de fichiers de l'image
- Tests de contenu de fichier : le fichier contient t'il les données attendues
- Test sur les métadonnées de l'image
Les tests sur les commandes
Les tests de commande garantissent que les commandes s'exécutent correctement dans l'image cible. Les expressions régulières peuvent être utilisées pour vérifier les chaînes attendues ou pas à la fois dans la sortie standard ou dans celle des erreurs (stdout et stderr):
commandTests:
- name: "gunicorn flask"
setup: [["virtualenv", "/env"], ["pip", "install", "gunicorn", "flask"]]
command: "which"
args: ["gunicorn"]
expectedOutput: ["/env/bin/gunicorn"]
- name: "apt-get upgrade"
command: "apt-get"
args: ["-qqs", "upgrade"]
excludedOutput: [".*Inst.*Security.* | .*Security.*Inst.*"]
excludedError: [".*Inst.*Security.* | .*Security.*Inst.*"]
Tests d'existence de fichiers
Les tests d'existence de fichiers vérifient qu'un fichier (ou un répertoire) est présent ou est absent du système de fichiers de l'image. Ici le contenu des fichiers ou des répertoires ne sont pas contrôlé.
fileExistenceTests:
- name: 'Root'
path: '/'
shouldExist: true
permissions: '-rw-r--r--'
uid: 1000
gid: 1000
isExecutableBy: 'group'
Tests de contenu des fichiers
Les tests de contenu de fichier ouvrent un fichier sur le système de fichiers et vérifient son contenu. Ces tests supposent que le fichier spécifié est un fichier, et qu'il existe. Les expressions régulières peuvent à nouveau être utilisées pour vérifier le contenu attendu ou exclu dans le fichier spécifié.
fileContentTests:
- name: 'Debian Sources'
path: '/etc/apt/sources.list'
expectedContents: ['.*httpredir\.debian\.org.*']
excludedContents: ['.*gce_debian_mirror.*']
Tests sur les métadonnées
Les tests des métadonnées garantit que le conteneur est correctement configuré. Toutes ces vérifications sont facultatives. Ici on teste la présence des variables d'environnement, de ports ouverts, de volumes, de commande (cmd et entrypoint), de l'utilisateur d"exécution et du répertoire de travail.
metadataTest:
env:
- key: foo
value: baz
labels:
- key: 'com.example.vendor'
value: 'ACME Incorporated'
- key: 'build-date'
value: '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}$'
isRegex: true
exposedPorts: ["8080", "2345"]
volumes: ["/test"]
entrypoint: []
cmd: ["/bin/bash"]
workdir: "/app"
user: "luke"
! Attention au moment ou j'écris ces lignes la metadata user retourne une erreur !
Mise en pratique
Je vais prendre l'exemple d'image contenant bandit, un outil permettant de contrôler si du code python contient des failles de sécurité. Je l'utilise dans gitlab et donc via une image docker.
Voici son Dockerfile qui utilise les techniques du multi-stage et de la copie de l'environnement virtuel permettant de limiter la taille de celle-ci:
FROM alpine:3.14.2 as builder
ENV PYROOT=/venv
ENV PYTHONUSERBASE=$PYROOT
WORKDIR /
COPY Pipfile* ./
RUN apk update && \
apk add --no-cache bc gcc libffi-dev musl-dev openssl-dev python3-dev py3-pip && \
pip3 install --no-cache-dir --no-compile pipenv && \
pipenv lock && \
PIP_USER=1 pipenv sync --system
FROM alpine:3.14.2 as default
RUN adduser -D user && \
apk add --no-cache py3-pip
COPY --from=builder /venv /venv
# Make sure we use the virtualenv:
ENV PATH="/venv/bin:$PATH"
ENV PYTHONPATH="/venv/lib/python3.9/site-packages/"
USER user
WORKDIR /src
ENTRYPOINT ["bandit", "-r"]
CMD ["--version"]
Je vais tester :
- les métadonnées :
- env PATH contient bien /venv/bin
- env PYTHONPATH = /venv/lib/python3.9/site-packages/
- user = user,
- workdir = /src,
- entrypoint = ["bandit", "-r"]
- cmd = CMD ["--version"]
- le lancement de la commande bandit --version retourne la version attendue
- que la commande
which bandit
retourne bien - que le fichier
/etc/os-release
contient bien la version attendue - que le certificat n'est pas présent
Le fichier de test :
schemaVersion: '2.0.0'
metadataTest:
env:
- key: PATH
value: "/venv/bin.*"
isRegex: true
- key: PYTHONPATH
value: "/venv/lib/python3.9/site-packages/"
entrypoint: ["bandit", "-r"]
cmd: ["--version"]
workdir: "/src"
commandTests:
- name: "bandit version"
command: "bandit"
args: ["--version"]
expectedOutput: ["bandit 1.7.0"]
- name: "bandit path"
command: "which"
args: ["bandit"]
expectedOutput: ["/venv/bin/bandit"]
fileExistenceTests:
- name: 'Certificat'
path: '/etc/ssl/certs/my-cert.crt'
shouldExist: false
fileContentTests:
- name: 'Linux Version'
path: '/etc/os-release'
expectedContents: ["VERSION_ID=3.14.2","NAME=\"Alpine Linux\""]
Pour les metadata si on souhaite utiliser des regex il faut l'indiquer avec
isRegex
. Au cas ou il y dans les chaines des caractères spéciaux il faudra bien
faire attention à les échapper avec \
.
Lancement des tests
container-structure-test test --image artefacts.robert.local/bandit:1.7.0 --config unit-test.yaml
=======================================
====== Test file: unit-test.yaml ======
=======================================
=== RUN: Command Test: bandit version
--- PASS
duration: 463.749977ms
stdout: bandit 1.7.0
python version = 3.9.5 (default, May 12 2021, 20:44:22) [GCC 10.3.1 20210424]
=== RUN: Command Test: bandit path
--- PASS
duration: 325.957628ms
stdout: /venv/bin/bandit
=== RUN: File Content Test: Linux Version
--- PASS
duration: 0s
=== RUN: File Existence Test: Certificat
--- PASS
duration: 0s
=== RUN: Metadata Test
--- PASS
duration: 0s
=======================================
=============== RESULTS ===============
=======================================
Passes: 5
Failures: 0
Duration: 789.707605ms
Total tests: 5
PASS
Voila qui permet de valider que l'image est bien conforme à ce qui était attendu. Couplé à trivy (scan de vulnérabilités) et on peut valider l'utilisation de l'image dans nos ci.
Plus d'infos sur cet outil sur la page du projet GoogleContainerTools/container-structure-test