From 22367224d722259d9e40d9a7248c38fbbe397d76 Mon Sep 17 00:00:00 2001 From: d9pouces Date: Fri, 4 Sep 2026 21:58:28 +0200 Subject: [PATCH] feat: enhance Docker image tagging and add publication functionality Signed-off-by: d9pouces --- id_tools/build.py | 8 +-- id_tools/config.py | 4 ++ id_tools/publication.py | 129 ++++++++++++++++++++++++++++++++++++++++ tox.ini | 38 +++++++++++- 4 files changed, 172 insertions(+), 7 deletions(-) create mode 100644 id_tools/publication.py diff --git a/id_tools/build.py b/id_tools/build.py index e786ce3..5027259 100644 --- a/id_tools/build.py +++ b/id_tools/build.py @@ -93,11 +93,12 @@ def build_docker_image( """Build the Docker image.""" if push and not registry: raise ValueError("--push requires --registry to be set.") - id_configurator_version = config.get_project_version() + version = config.get_project_version() + name = config.get_project_name() values = config.get_parameters() package_build = DOCKER_FILE_BUILDER.format(**values) - v_tag = f"id_configurator:{id_configurator_version}" - l_tag = "id_configurator:latest" + v_tag = f"{name}:{version}" + l_tag = f"{name}:latest" image_install = DOCKER_FILE_INSTALL.format(**values) image_build = DOCKER_FILE_RUNTIME.format(**values) with ( @@ -136,7 +137,6 @@ def build_docker_image( typer.echo(" ".join(cmd)) subprocess.check_call(cmd, cwd=config.source_dir) except subprocess.CalledProcessError: - print(package_build + image_install + image_build) return False return True diff --git a/id_tools/config.py b/id_tools/config.py index ba896c7..e58b867 100644 --- a/id_tools/config.py +++ b/id_tools/config.py @@ -38,6 +38,10 @@ class SourceConfig: python_versions.append(matcher.group(1)) return python_versions + def get_project_name(self) -> str: + """Get the name of InterDiode from the pyproject.toml file.""" + return self.raw_metadata["project"]["name"] + def get_project_version(self) -> str: """Get the version of InterDiode from the pyproject.toml file.""" return self.raw_metadata["project"]["version"] diff --git a/id_tools/publication.py b/id_tools/publication.py new file mode 100644 index 0000000..d6e0110 --- /dev/null +++ b/id_tools/publication.py @@ -0,0 +1,129 @@ +import json +import logging +import os +import subprocess +import tempfile + +import typer + +from id_tools.config import SourceConfig + +app = typer.Typer( + help="Publish the documentation and release announcements to external destinations." +) + +logger = logging.getLogger(__name__) + + +@app.command(name="sign-docker", help="Signe l'image Docker sur le registre") +def sign_docker_image( + registry: str = typer.Option( + "interdiode", help="Registre cible pour l'image Docker" + ), + docker_login: str = typer.Option( + "", envvar="DOCKER_HUB_LOGIN", help="Nom d'utilisateur Docker" + ), + docker_token: str = typer.Option( + "", envvar="DOCKER_HUB_TOKEN", help="Jeton d'accès Docker" + ), + cosign_password: str = typer.Option( + "", envvar="COSIGN_PASSWORD", help="Mot de passe de la clé privée Cosign" + ), + cosign_private_key: str = typer.Option( + "", envvar="COSIGN_PRIVATE_KEY", help="Contenu de la clé privée Cosign" + ), +): + config = SourceConfig() + version = config.get_project_version() + image = config.get_project_name() + v_tag = f"{image}:{version}" + l_tag = f"{image}:latest" + destinations = [f"{registry}/{v_tag}", f"{registry}/{l_tag}"] + if docker_login and docker_token: + cmd = ["docker", "login", "-u", docker_login, "--password-stdin"] + subprocess.run( + cmd, input=docker_token, text=True, check=True, cwd=config.source_dir + ) + digest_references = { + get_reference_from_tag(destination) for destination in destinations + } + if cosign_password and cosign_private_key: + with ( + tempfile.NamedTemporaryFile(mode="w", suffix=".key", delete=True) as key_fd, + tempfile.TemporaryDirectory() as tmp_dir, + ): + key_fd.write(cosign_private_key) + key_fd.flush() + cosign_env = {**os.environ, "COSIGN_PASSWORD": cosign_password} + trivy_report = os.path.join(tmp_dir, "trivy_report.json") + for digest_reference in digest_references: + # noinspection SpellCheckingInspection + cmd = [ + "trivy", + "image", + "--format", + "json", + "--output", + trivy_report, + digest_reference, + ] + subprocess.run(cmd, text=True, check=True, cwd=config.source_dir) + cmd = [ + "cosign", + "attest", + "--key", + key_fd.name, + "--type", + "vuln", + "--predicate", + trivy_report, + digest_reference, + ] + subprocess.run(cmd, text=True, check=True, cwd=config.source_dir) + cmd = [ + "cosign", + "sign", + "--yes", + "--recursive", + "--key", + key_fd.name, + digest_reference, + ] + subprocess.check_call(cmd, env=cosign_env) + cmd = [ + "cosign", + "verify-attestation", + "--key", + config.cosign_key_url, + "--type", + "vuln", + digest_reference, + ] + subprocess.check_call(cmd, env=cosign_env) + cmd = [ + "cosign", + "verify", + "--key", + config.cosign_key_url, + digest_reference, + ] + subprocess.check_call(cmd, env=cosign_env) + typer.echo(f"✓ Image signée : {registry}/{v_tag} et {registry}/{l_tag}") + + +def get_reference_from_tag(destination: str) -> str: + # noinspection SpellCheckingInspection + cmd = [ + "docker", + "buildx", + "imagetools", + "inspect", + destination, + "--format", + "{{json .Manifest}}", + ] + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + manifest = json.loads(result.stdout) + image_name = destination.rsplit(":", 1)[0] + reference = f"{image_name}@{manifest['digest']}" + return reference diff --git a/tox.ini b/tox.ini index 9ada04a..b4f5d8b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -env_list = py313,py314,coverage +env_list = py313,py314,coverage,packaging,publish,module-python isolated_build = true requires = tox>=4 @@ -26,6 +26,38 @@ commands = python3 -m coverage xml -o .tox/coverage.xml --ignore python3 -m coverage report -m --fail-under=94 --ignore --skip-covered +[testenv:packaging] +package = skip +change_dir = {tox_root} +allowlist_externals = + docker +dependency_groups = dev +description = + Generate the docker images for the different platform +depends = + update +commands = + python3 -m id_tools build docker --platform linux/amd64,linux/arm64/v8 + +[testenv:publish] +package = skip +change_dir = {tox_root} +dependency_groups = dev +description = + Build and publish the image to Docker Hub. + The version is taken from the pyproject.toml file. + Make sure that you have the rights to upload the image to Docker Hub. +commands = + python3 -m uv sync --all-groups --active + python3 -m id_tools build docker --platform linux/amd64,linux/arm64/v8 --push + python3 -m id_tools publication sign-docker +docker = +passenv = + COSIGN_PASSWORD + COSIGN_PRIVATE_KEY + DOCKER_HUB_LOGIN + DOCKER_HUB_TOKEN + [docker:pypi-repo] dockerfile = {tox_root}/tests/dockerfiles/repos/pypi-repo.Dockerfile expose = @@ -45,5 +77,5 @@ docker = allowlist_externals = docker commands = - python3 -m id_tools test render-config tests/configs/Python.yaml.tmpl {toxworkdir}/configs/Python.yaml - docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-context generated-config={toxworkdir}/configs -f tests/dockerfiles/Python.Dockerfile -t id-configurator-python . + python3 -m id_tools test render-config {tox_root}/tests/configs/Python.yaml.tmpl {env_dir}/configs/Python.yaml + docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Python.Dockerfile -t id-configurator-python .