Aller au contenu principal

Valider une image avec container-structure-test

· 6 minutes de lecture
Stéphane ROBERT
Consultant DevOps

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 upgrade"
    command: "apt"
    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