test: move structure.yml to tests and remove cst volume leak

container-structure-test left one anonymous volume per test container.
tests/structure.py runs it with --save and removes the new containers
with their volumes afterwards.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Tobias KaupatandClaude Sonnet 5.5 committed 2026-10-19 17:49:47 +02:00
1 parent c65537c013
commit a6c83de07e
5 files changed
+72 -23

No files matched your search

+15 -3
View File
@@ -1,5 +1,17 @@
# Optional features that MUST be present in the built image.
#
# One gate name per line. Known gates: stream, docker-cli, bash, msmtp.
# A listed feature that is absent turns its skip into a failure.
# Empty on purpose: master currently ships none of the optional features.
# One gate name per line. Known gates and the pull request that introduces
# them (see tests/test_features.py):
#
# stream stream backups via RESTIC_STREAM PR #109
# docker-cli docker cli inside the container PR #99
# bash bash for pre/post hooks PR #99
# msmtp mail relay via msmtp PR #84
#
# While a gate is NOT listed here its tests are reported as SKIP when the
# feature is absent, so an open pull request does not turn master red.
#
# When you merge a feature, add its gate name below. From then on the feature
# tests are mandatory and a feature that silently disappears from the image
# fails the build.
#
+55
View File
@@ -0,0 +1,55 @@
"""Run container-structure-test without leaking Docker volumes.
container-structure-test starts one container per test and never removes it. The image
declares `VOLUME /data`, so every container drags an anonymous volume along that outlives
`docker rm`. With `--save` the containers are kept, and this wrapper removes the ones the
run created, together with their volumes (`remove(v=True)`). Run with `task test:structure`.
Exit code: the one of container-structure-test.
"""
from __future__ import annotations
import os
import signal
import subprocess
import sys
from pathlib import Path
import docker
CONFIG = Path(__file__).with_name("structure.yml")
def _raise_interrupt(signum: int, _frame: object) -> None:
raise KeyboardInterrupt(f"received {signal.Signals(signum).name}")
def main() -> int:
image = os.environ.get("IMAGE", "restic-backup-docker:test")
signal.signal(signal.SIGTERM, _raise_interrupt)
client = docker.from_env()
before = {c.id for c in client.containers.list(all=True)}
try:
return subprocess.run(
[
"container-structure-test",
"test",
"--save",
"--image",
image,
"--config",
str(CONFIG),
],
check=False,
).returncode
finally:
signal.signal(signal.SIGINT, signal.SIG_IGN)
for container in client.containers.list(all=True):
if container.id not in before:
container.remove(v=True, force=True)
signal.signal(signal.SIGINT, signal.default_int_handler)
if __name__ == "__main__":
sys.exit(main())
+64
View File
@@ -0,0 +1,64 @@
schemaVersion: '2.0.0'
# Structural contract of the image. Everything asserted here is something a
# user can reach from outside: a binary they call from a hook, a path they
# mount, or the way the container is started.
commandTests:
- name: 'restic is installed'
command: 'restic'
args: ['version']
expectedOutput: ['restic \d+\.\d+\.\d+.*']
- name: 'rclone is installed'
command: 'rclone'
args: ['--version']
expectedOutput: ['rclone v.*']
- name: 'curl is installed, used for Teams notifications'
command: 'curl'
args: ['--version']
expectedOutput: ['curl \d+\..*']
- name: 'mail is installed, used for mail notifications'
command: 'sh'
args: ['-c', 'command -v mail']
expectedOutput: ['/usr/bin/mail']
- name: 'crond is available'
command: 'sh'
args: ['-c', 'command -v crond']
expectedOutput: ['/usr/sbin/crond']
- name: 'flock is available, the cron jobs depend on it'
command: 'sh'
args: ['-c', 'test -x /usr/bin/flock && echo ok']
expectedOutput: ['ok']
fileExistenceTests:
- name: 'backup script'
path: '/bin/backup'
shouldExist: true
permissions: '-rwxr-xr-x'
- name: 'check script'
path: '/bin/check'
shouldExist: true
permissions: '-rwxr-xr-x'
- name: 'entrypoint'
path: '/entry.sh'
shouldExist: true
permissions: '-rwxr-xr-x'
- name: 'default repository mount point'
path: '/mnt/restic'
shouldExist: true
- name: 'cron log file'
path: '/var/log/cron.log'
shouldExist: true
- name: 'crontab directory'
path: '/var/spool/cron/crontabs'
shouldExist: true
metadataTest:
volumes: ['/data']
entrypoint: ['/entry.sh']
cmd: ['tail', '-fn0', '/var/log/cron.log']
envVars:
- key: 'RESTIC_REPOSITORY'
value: '/mnt/restic'
- key: 'BACKUP_CRON'
value: '0 */6 * * *'