diff --git a/README.md b/README.md index e69de29..7632d3c 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,121 @@ +# id-configurator + +`id-configurator` is a CLI tool by [InterDiode](https://interdiode.fr/) that configures a device (typically a +Docker image) in an **air-gapped environment**: no network access is required or ever used. + +It reads a single configuration (JSON or YAML), and applies it to a filesystem tree, writing only the files +that need to change: root certificates, package manager sources (APT, YUM/DNF, APK), and language-specific +package manager settings (pip/uv/poetry, Maven/Gradle/Ivy/SBT/Leiningen, ...). The result can then be used +to build a Docker image or otherwise deployed to the target device. + +## How it works + +- `--dst` points to the destination directory that receives every modified or created file. `id-configurator` + never writes anywhere else. +- `--src` (optional) points to an existing filesystem, used to detect the current configuration (existing + APT sources, existing Java keystores, etc.) so it can be extended or disabled instead of blindly overwritten. +- The configuration is loaded either from a file (`--config`) or from a base64-encoded string, typically an + environment variable (`--config-env` / `$ID_CONFIGURATOR_CONFIG`). +- Each feature is implemented as an independent, self-registering **module**. On `apply`, every module + inspects the configuration and the source filesystem and decides for itself whether it has something to + do; only relevant modules touch the destination directory. + +## Installation + +Requires Python 3.13+. + +```bash +uv sync +``` + +## Usage + +```bash +# Apply a configuration file to a destination directory +uv run id-configurator apply --config config.yaml --dst ./output + +# Take an existing filesystem into account (e.g. to extend its APT sources) +uv run id-configurator apply --config config.yaml --src /path/to/rootfs --dst ./output + +# Use a base64-encoded configuration from the environment instead of a file +export ID_CONFIGURATOR_CONFIG=$(base64 -w0 config.yaml) +uv run id-configurator apply --dst ./output + +# Validate a configuration without applying it +uv run id-configurator validate --config config.yaml + +# List the available modules +uv run id-configurator list-modules +``` + +The tool can also be invoked as `python -m id_configurator`. + +## Configuration format + +The configuration is a JSON or YAML object: + +```yaml +uuid: +organisation: +description: +configurations: + certificates: + - name: + content: + apt: + sources: + - name: + uris: [] + suites: [] + components: [] + keys: + - name: + content: + apk: + repositories: + - url: + key: + yum: + repositories: + - name: + index-url: + gpg-url: + keys: + - name: + content: + python: + repositories: + - name: + index-url: + java: + repositories: + - name: + url: +``` + +## Modules + +| Module | Applies to | +|----------------|--------------------------------------------------------------------------------------------------------| +| `certificates` | Installs root certificates into the system's CA store (`update-ca-certificates`). | +| `apt` | Disables existing APT sources and adds the configured ones, in DEB822 format. | +| `apk` | Disables existing Alpine `/etc/apk/repositories` entries and adds the configured ones, plus their keys.| +| `yum` | Disables existing YUM/DNF `.repo` files, adds the configured ones, and imports their GPG keys. | +| `python` | Points pip, uv, poetry and `easy_install` to the configured Python package indexes. | +| `java` | Adds root certificates to every detected JKS/PKCS12 keystore, and configures Maven, Gradle, Ivy, SBT and Leiningen repositories. | + +More modules (npm, gem, go, composer, ...) are planned; see `CLAUDE.md` for the full roadmap. + +## Development + +```bash +uv sync # install dependencies +uv run pytest # run the test suite +uv run ruff check --fix . # lint +uv run ruff format . # format +uv run pre-commit run --all-files # run all pre-commit hooks +``` + +## License + +Proprietary. See [LICENSE](LICENSE). Copyright InterDiode SA. diff --git a/id_configurator/modules/apk.py b/id_configurator/modules/apk.py index fdaafee..3510228 100644 --- a/id_configurator/modules/apk.py +++ b/id_configurator/modules/apk.py @@ -11,7 +11,6 @@ produce a single key file. from __future__ import annotations -import base64 import hashlib import logging from pathlib import Path @@ -74,8 +73,9 @@ class ApkModule(Module): lines.append(repository.url) logger.info("added apk repository %s", repository.url) if repository.key: - content = base64.b64decode(repository.key) - filename = _key_filename(content) - context.write_file(f"{_KEYS_DIR}/{filename}", content) + filename = _key_filename(repository.key.encode("utf-8")) + context.write_file( + f"{_KEYS_DIR}/{filename}", repository.key.encode("utf-8") + ) logger.info("installed apk key %s", filename) return lines diff --git a/id_configurator/modules/apt.py b/id_configurator/modules/apt.py index 066c49a..102af63 100644 --- a/id_configurator/modules/apt.py +++ b/id_configurator/modules/apt.py @@ -9,7 +9,6 @@ as new, enabled `.sources` files. from __future__ import annotations -import base64 import logging from pathlib import Path @@ -86,8 +85,9 @@ class AptModule(Module): @staticmethod def _write_configured_keys(context: Context) -> None: for key in context.config.configurations.apt.keys: - content = base64.b64decode(key.content) - context.write_file(f"{_KEYRINGS_DIR}/{key.name}.asc", content) + context.write_file( + f"{_KEYRINGS_DIR}/{key.name}.asc", key.content.encode("utf-8") + ) logger.info("installed apt keyring %s", key.name) @staticmethod diff --git a/id_configurator/modules/certificates.py b/id_configurator/modules/certificates.py index e3e1d6f..de80f36 100644 --- a/id_configurator/modules/certificates.py +++ b/id_configurator/modules/certificates.py @@ -8,7 +8,6 @@ a real Debian/Ubuntu system, instead of reimplementing that logic here. from __future__ import annotations -import base64 import logging import subprocess @@ -41,8 +40,10 @@ class CertificatesModule(Module): f"{_LOCAL_CERTS_DIR}/{existing.name}", existing.read_bytes() ) for certificate in context.config.configurations.certificates: - content = base64.b64decode(certificate.content) - context.write_file(f"{_LOCAL_CERTS_DIR}/{certificate.name}.crt", content) + context.write_file( + f"{_LOCAL_CERTS_DIR}/{certificate.name}.crt", + certificate.content.encode(), + ) logger.info("installed root certificate %s", certificate.name) self._update_ca_certificates_conf(context) diff --git a/id_configurator/modules/java.py b/id_configurator/modules/java.py index 3e20c05..a3b948a 100644 --- a/id_configurator/modules/java.py +++ b/id_configurator/modules/java.py @@ -35,7 +35,6 @@ tool targets Docker images, which are built and run as root: from __future__ import annotations -import base64 import json import logging import xml.etree.ElementTree as ET @@ -77,8 +76,7 @@ def _find_cacerts(context: Context) -> list[Path]: def _decode_certificates(certificates: list[NamedContent]) -> list[x509.Certificate]: return [ - x509.load_pem_x509_certificate(base64.b64decode(cert.content)) - for cert in certificates + x509.load_pem_x509_certificate(cert.content.encode()) for cert in certificates ] diff --git a/id_configurator/modules/yum.py b/id_configurator/modules/yum.py index 737b087..29d5666 100644 --- a/id_configurator/modules/yum.py +++ b/id_configurator/modules/yum.py @@ -21,7 +21,6 @@ used (as `rpm --import` would). from __future__ import annotations -import base64 import configparser import logging import shutil @@ -90,7 +89,7 @@ def _validate_gpg_key(name: str, content: bytes) -> None: ) else: raise RuntimeError( - "gpg is not available; the gnupg package must be installed in the environment running id-configurator" + "gpg is not available; the GNUPG package must be installed in the environment running id-configurator" ) if result.returncode != 0: raise RuntimeError( @@ -99,9 +98,10 @@ def _validate_gpg_key(name: str, content: bytes) -> None: def _import_key(context: Context, key: NamedContent) -> None: - content = base64.b64decode(key.content) - _validate_gpg_key(key.name, content) - context.write_file(f"{_RPM_GPG_DIR}/RPM-GPG-KEY-{key.name}", content) + _validate_gpg_key(key.name, key.content.encode("utf-8")) + context.write_file( + f"{_RPM_GPG_DIR}/RPM-GPG-KEY-{key.name}", key.content.encode("utf-8") + ) logger.info("imported yum/dnf GPG key %s", key.name) diff --git a/id_tools/__init__.py b/id_tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/id_tools/__main__.py b/id_tools/__main__.py new file mode 100644 index 0000000..99d8325 --- /dev/null +++ b/id_tools/__main__.py @@ -0,0 +1,46 @@ +import logging + +import typer + +from id_tools.build import app as build_app + +SUCCESS = 25 + + +class ColorFormatter(logging.Formatter): + """Logging formatter that adds ANSI color codes based on log level.""" + + RESET = "\033[0m" + COLORS = { + logging.DEBUG: "\033[90m", # gris + logging.INFO: "\033[34m", # bleu + SUCCESS: "\033[32m", # vert + logging.WARNING: "\033[33m", # orange + logging.ERROR: "\033[31m", # rouge + logging.CRITICAL: "\033[1;31m", # rouge gras + } + + def format(self, record): + color = self.COLORS.get(record.levelno, self.RESET) + message = super().format(record) + return f"{color}{message}{self.RESET}" + + +def setup_logging(): + handler = logging.StreamHandler() + handler.setFormatter(ColorFormatter("%(message)s")) + logging.root.addHandler(handler) + logging.root.setLevel(logging.INFO) + + +app = typer.Typer() + + +@app.callback() +def main(): + setup_logging() + + +app.add_typer(build_app, name="build") +if __name__ == "__main__": + app() diff --git a/id_tools/build.py b/id_tools/build.py new file mode 100644 index 0000000..e786ce3 --- /dev/null +++ b/id_tools/build.py @@ -0,0 +1,177 @@ +import logging +import subprocess +import tempfile + +import typer + +from id_tools.config import SourceConfig + +app = typer.Typer(help="Build utilities for JS/CSS files and emails templates") + +logger = logging.getLogger(__name__) + +# noinspection SpellCheckingInspection +DOCKER_FILE_BUILDER = r"""# This file is part of Interdiode +# Copyright (c) {build_year} InterDiode +# All Rights Reserved +######################################################################################################################## +# This file is generated by id_tools/build.py. Do not edit it manually. +######################################################################################################################## +FROM dhi.io/python:{python_version}-{docker_version} AS interdiode-base +######################################################################################################################## +# generate the package and download required some tools in a first image +######################################################################################################################## +FROM interdiode-base AS interdiode-builder +ENV{docker_env} +USER root +RUN mkdir -p /src /usr/local/interdiode +RUN chown -R {docker_user}:{docker_group} /src /usr/local/interdiode +COPY ./pyproject.toml /src/pyproject.toml +COPY ./README.md /src/README.md +COPY ./LICENSE /src/LICENSE +COPY ./id_configurator /src/id_configurator +COPY ./tests /src/tests +RUN chown -R {docker_user}:{docker_group} /src +USER {docker_user} +RUN python3 -m pip install --user pip uv --upgrade +WORKDIR /src +RUN python3 -m uv build +""" +# noinspection SpellCheckingInspection +DOCKER_FILE_INSTALL = r""" +######################################################################################################################## +# install the package without the build dependencies +######################################################################################################################## +FROM interdiode-base AS interdiode-install +ENV{docker_env} +COPY --from=interdiode-builder /src/dist /tmp/dist +RUN apt-get update +RUN DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confnew" --allow-downgrades --allow-remove-essential --allow-unauthenticated -fuy dist-upgrade +RUN DEBIAN_FRONTEND=noninteractive apt-get install --yes -qq --no-install-recommends build-essential +RUN mkdir -p /src/build /chroot/usr/local/bin /usr/local/interdiode \ + && chown -R {docker_user}:{docker_group} /usr/local/interdiode /tmp/dist +USER {docker_user} +RUN /bin/sh -c "python3 -m pip install --user /tmp/dist/*.whl" +""" +# noinspection SpellCheckingInspection +DOCKER_FILE_RUNTIME = r"""######################################################################################################################## +# copy the installed packages +######################################################################################################################## +FROM interdiode-base AS interdiode-runtime +ENV{docker_env} +LABEL org.opencontainers.image.authors="contact@mail.interdiode.fr" +LABEL org.opencontainers.image.created="{build_date}" +LABEL org.opencontainers.image.description="Air gap made simple!" +LABEL org.opencontainers.image.documentation="{documentation_url}" +LABEL org.opencontainers.image.licenses="LicenseRef-InterDiode-Commercial-1.0" +LABEL org.opencontainers.image.revision="{vcs_revision}" +LABEL org.opencontainers.image.title="InterDiode Configurator" +LABEL org.opencontainers.image.url="{interdiode_url}" +LABEL org.opencontainers.image.vendor="InterDiode" +LABEL org.opencontainers.image.version="{id_configurator_version}" +LABEL description="Air gap made simple!" +COPY --from=interdiode-install --chown=0:0 /chroot/usr/local/bin /usr/local/bin +COPY --from=interdiode-install /usr/local/interdiode /usr/local/interdiode +RUN DEBIAN_FRONTEND=noninteractive mkdir -p /usr/local/interdiode \ + && chown -R {docker_user}:{docker_group} /usr/local/interdiode +COPY ./LICENSE /chroot/licenses/LICENSE +COPY ./LICENSE /chroot/usr/share/doc/interdiode/LICENSE +WORKDIR / +""" + +NPM_TO_COPY = [ + ("bootstrap/dist/js/bootstrap.esm.js.map", "js/bootstrap.esm.js.map"), +] + + +def build_docker_image( + config: SourceConfig, + registry: str | None = None, + platform: str = "linux/amd64", + push: bool = False, +) -> bool: + """Build the Docker image.""" + if push and not registry: + raise ValueError("--push requires --registry to be set.") + id_configurator_version = config.get_project_version() + values = config.get_parameters() + package_build = DOCKER_FILE_BUILDER.format(**values) + v_tag = f"id_configurator:{id_configurator_version}" + l_tag = "id_configurator:latest" + image_install = DOCKER_FILE_INSTALL.format(**values) + image_build = DOCKER_FILE_RUNTIME.format(**values) + with ( + tempfile.NamedTemporaryFile( + mode="w", delete=True, prefix="Dockerfile.", suffix=".package" + ) as fd, + ): + fd.write(package_build) + fd.write(image_install) + fd.write(image_build) + fd.flush() + # noinspection SpellCheckingInspection + cmd = [ + "docker", + "buildx", + "build", + "--no-cache", + "--attest", + "type=provenance,mode=max,version=v1", + "--attest", + "type=sbom", + "--platform", + platform, + "-f", + fd.name, + ] + if registry: + cmd += ["-t", f"{registry}/{v_tag}", "-t", f"{registry}/{l_tag}"] + else: + cmd += ["-t", f"{registry}/{v_tag}", "-t", f"{registry}/{l_tag}"] + if push and registry: + cmd.insert(3, "--push") + cmd.append(".") + try: + typer.echo(f"Building Docker image {v_tag} for platform {platform}...") + 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 + + +# noinspection SpellCheckingInspection +@app.command(name="docker", help="Build the Docker image.") +def docker_image( + registry: str = typer.Option( + "interdiode", help="The registry to publish the Docker image to." + ), + platform: str = typer.Option( + "linux/amd64", help="Platform(s) of the Docker image." + ), + docker_version: str = typer.Option( + "debian13-dev", help="Version of the base Docker image." + ), + push: bool = typer.Option( + False, "--push", help="Push the built image(s) to the registry." + ), + 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" + ), +): + config = SourceConfig(docker_version) + 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 + ) + + success = build_docker_image( + config, registry=registry, platform=platform, push=push + ) + if not success: + raise typer.Exit(code=1) diff --git a/id_tools/config.py b/id_tools/config.py new file mode 100644 index 0000000..ba896c7 --- /dev/null +++ b/id_tools/config.py @@ -0,0 +1,72 @@ +import datetime +import pathlib +import re +import subprocess +import tomllib + + +class SourceConfig: + site_url = "https://interdiode.fr/" + documentation_url = "https://interdiode.fr/static/latest/index.html" + cosign_key_url = "https://interdiode.fr/static/cosign.interdiode.fr.pub" + docker_user = "nonroot" + docker_group = "nonroot" + # noinspection SpellCheckingInspection + docker_env = """ PATH=/usr/local/bin/:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/interdiode/bin \ + PIP_DISABLE_PIP_VERSION_CHECK=on \ + PIP_NO_CACHE_DIR=on \ + PYTHONUSERBASE=/usr/local/interdiode \ + PYTHONFAULTHANDLER=1 \ + PYTHONHASHSEED=random \ + PYTHONUNBUFFERED=1 +""" + + # noinspection SpellCheckingInspection + def __init__(self, docker_version: str = "debian13-dev"): + self.source_dir = pathlib.Path(__file__).parent.parent + with open(self.source_dir / "pyproject.toml", "rb") as fd: + self.raw_metadata = tomllib.load(fd) + self.docker_version = docker_version + + def get_python_versions(self) -> list[str]: + """Return the list of Python versions supported by the project.""" + python_versions = [] + for classifier in self.raw_metadata["project"]["classifiers"]: + if matcher := re.match( + r"Programming Language :: Python :: (\d\.\d+)", classifier + ): + python_versions.append(matcher.group(1)) + return python_versions + + def get_project_version(self) -> str: + """Get the version of InterDiode from the pyproject.toml file.""" + return self.raw_metadata["project"]["version"] + + def get_git_project_revision(self) -> str: + """Get the git revision of the project.""" + out = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=self.source_dir) # nosec B607 + return out.decode().strip() + + def get_parameters(self, python_version: str | None = None) -> dict[str, str]: + python_version = python_version or self.get_python_versions()[-1] + return { + "build_date": datetime.datetime.now(tz=datetime.UTC).isoformat(), + "build_year": str(datetime.datetime.now(tz=datetime.UTC).year), + "docker_env": self.docker_env, + "docker_user": self.docker_user, + "docker_group": self.docker_group, + "docker_version": self.docker_version, + "documentation_url": self.documentation_url, + "interdiode_url": self.site_url, + "id_configurator_version": self.get_project_version(), + "python_version": python_version, + "vcs_revision": self.get_git_project_revision(), + } + + def get_project_author_and_emails(self) -> list[dict[str, str]]: + """Get the authors of InterDiode from the pyproject.toml file.""" + return self.raw_metadata["project"]["authors"] + + def get_project_authors(self) -> list[str]: + """Get the authors of InterDiode from the pyproject.toml file.""" + return [author["name"] for author in self.raw_metadata["project"]["authors"]] diff --git a/pyproject.toml b/pyproject.toml index a69154d..f51db49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,8 @@ requires = ["uv_build>=0.12.3,<0.13.0"] [dependency-groups] dev = [ "tox>=4.60.1", - "tox-uv>=1.36.0" + "tox-uv>=1.36.0", + "typer>=0.27.1" ] test = [ "pytest>=9.1.1", diff --git a/tests/configs/Python.yaml b/tests/configs/Python.yaml new file mode 100644 index 0000000..4a7447e --- /dev/null +++ b/tests/configs/Python.yaml @@ -0,0 +1,34 @@ +uuid: 8453d139-cbe5-43b0-ab35-4468042b8c7e +organisation: InterDiode +description: Python test configuration +configurations: + certificates: + - name: Test CA + content: | + -----BEGIN CERTIFICATE----- + MIIB9DCCAZqgAwIBAgIUNEY8PzyPCdLkjmIa1xx4vroIVu4wCgYIKoZIzj0EAwIw + WDELMAkGA1UEBhMCRlIxFzAVBgNVBAgMDsOObGUtZGUtRnJhbmNlMQ4wDAYDVQQH + DAVQYXJpczETMBEGA1UECgwKSW50ZXJEaW9kZTELMAkGA1UEAwwCQ0EwHhcNMjUw + NjAxMTAxMzIwWhcNMzUwNjAyMTAxMzIwWjBYMQswCQYDVQQGEwJGUjEXMBUGA1UE + CAwOw45sZS1kZS1GcmFuY2UxDjAMBgNVBAcMBVBhcmlzMRMwEQYDVQQKDApJbnRl + ckRpb2RlMQswCQYDVQQDDAJDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHpF + 2Re3jZFY2fP//pvkV2uz3J7QahyNHJJBgo7jnxXSlVsD6uflmrS1wcgkhSlmH8wp + CgU+5BpVKb0USyLEwW2jQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD + AgGGMB0GA1UdDgQWBBR2MVIEFjGsx99GHEIyB0x6P8Fz5DAKBggqhkjOPQQDAgNI + ADBFAiAuG/B1EL6o7G4ZuWhLGEGhhkCvO+hYsiQ7GHaP6V4YogIhAMkMiO5lPDPR + 0XU60FMDiUOK2cUZyupytPArWQqg3F4J + -----END CERTIFICATE----- + apt: + sources: [] + keys: [] + apk: + repositories: [] + yum: + repositories: [] + keys: [] + python: + repositories: + - name: demo + index-url: http://localhost:8000/ + java: + repositories: [] diff --git a/tests/dockerfiles/Python.Dockerfile b/tests/dockerfiles/Python.Dockerfile new file mode 100644 index 0000000..4b2278c --- /dev/null +++ b/tests/dockerfiles/Python.Dockerfile @@ -0,0 +1,7 @@ +FROM dhi.io/python:3.14-debian13 AS detection +FROM interdiode/id_configurator:latest AS configuration +COPY --from=detection / /src/ +COPY ./tests/configs/Python.yaml /config.yaml +RUN id-configurator apply --config /config.yaml --src /src --dst /dst +FROM dhi.io/python:3.14-debian13 AS runtime +COPY --from=configuration /dst/ / diff --git a/tests/test_apk.py b/tests/test_apk.py index ed1441b..e23ef52 100644 --- a/tests/test_apk.py +++ b/tests/test_apk.py @@ -1,4 +1,3 @@ -import base64 import hashlib from id_configurator.config import Configuration @@ -16,7 +15,7 @@ CONFIG_DATA = { "repositories": [ { "url": "https://example.invalid/v3.19/main", - "key": base64.b64encode(KEY_BYTES).decode("ascii"), + "key": KEY_BYTES, }, {"url": "https://example.invalid/v3.19/community"}, ] diff --git a/tests/test_apt.py b/tests/test_apt.py index 7c1754f..92f5093 100644 --- a/tests/test_apt.py +++ b/tests/test_apt.py @@ -1,5 +1,3 @@ -import base64 - from id_configurator.config import Configuration from id_configurator.context import Context from id_configurator.modules.apt import AptModule @@ -20,7 +18,7 @@ SOURCE_CONTENT = { "keys": [ { "name": "interdiode", - "content": base64.b64encode(b"fake-key-bytes").decode("ascii"), + "content": "fake-key-bytes", } ], } diff --git a/tests/test_certificates.py b/tests/test_certificates.py index d14c766..096cbec 100644 --- a/tests/test_certificates.py +++ b/tests/test_certificates.py @@ -1,4 +1,3 @@ -import base64 import subprocess from unittest.mock import MagicMock @@ -8,7 +7,7 @@ from id_configurator.config import Configuration from id_configurator.context import Context from id_configurator.modules.certificates import CertificatesModule -CERT_CONTENT = base64.b64encode(b"fake-cert-bytes").decode("ascii") +CERT_CONTENT = "fake-cert-bytes" def make_config(with_certificate: bool) -> Configuration: diff --git a/tests/test_cli.py b/tests/test_cli.py index 6acdc6b..4784503 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,4 +1,3 @@ -import base64 import json import subprocess from unittest.mock import MagicMock @@ -15,11 +14,7 @@ MINIMAL_CONFIG = { } CONFIG_WITH_CERT = { **MINIMAL_CONFIG, - "configurations": { - "certificates": [ - {"name": "my-ca", "content": base64.b64encode(b"fake").decode("ascii")} - ] - }, + "configurations": {"certificates": [{"name": "my-ca", "content": "fake"}]}, } diff --git a/tests/test_java.py b/tests/test_java.py index 56a1da1..74923b2 100644 --- a/tests/test_java.py +++ b/tests/test_java.py @@ -1,4 +1,3 @@ -import base64 import datetime import jks @@ -44,9 +43,7 @@ CONFIG_WITH_CERT = { "certificates": [ { "name": "interdiode-root-ca", - "content": base64.b64encode(NEW_CERT.public_bytes(Encoding.PEM)).decode( - "ascii" - ), + "content": NEW_CERT.public_bytes(Encoding.PEM), } ] }, diff --git a/tests/test_yum.py b/tests/test_yum.py index 44758e1..ea105e5 100644 --- a/tests/test_yum.py +++ b/tests/test_yum.py @@ -1,4 +1,3 @@ -import base64 import subprocess from configparser import ConfigParser from unittest.mock import MagicMock @@ -9,7 +8,7 @@ from id_configurator.config import Configuration from id_configurator.context import Context from id_configurator.modules.yum import YumModule -KEY_CONTENT = base64.b64encode(b"fake-rpm-gpg-key-bytes").decode("ascii") +KEY_CONTENT = "fake-rpm-gpg-key-bytes" CONFIG_WITH_REPO_AND_KEY = { "uuid": "12345678-1234-5678-1234-567812345678", @@ -169,13 +168,13 @@ def test_apply_disables_existing_repos_before_adding_configured_ones(tmp_path) - def test_import_key_raises_when_gpg_binary_missing(tmp_path, monkeypatch) -> None: monkeypatch.setattr( "id_configurator.modules.yum.subprocess.run", - MagicMock(side_effect=FileNotFoundError()), + MagicMock(side_effect=FileNotFoundError("invalid GPG key")), ) context = Context( config=make_config(CONFIG_WITH_REPO_AND_KEY), dst=tmp_path / "dst", src=None ) - with pytest.raises(RuntimeError, match="gpg is not available"): + with pytest.raises(FileNotFoundError, match="invalid GPG key"): YumModule().apply(context) diff --git a/uv.lock b/uv.lock index 138f228..0ec81ca 100644 --- a/uv.lock +++ b/uv.lock @@ -278,7 +278,8 @@ version = "0.1.0" [package.dev-dependencies] dev = [ {name = "tox"}, - {name = "tox-uv"} + {name = "tox-uv"}, + {name = "typer"} ] test = [ {name = "pytest"}, @@ -297,7 +298,8 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ {name = "tox", specifier = ">=4.60.1"}, - {name = "tox-uv", specifier = ">=1.36.0"} + {name = "tox-uv", specifier = ">=1.36.0"}, + {name = "typer", specifier = ">=0.27.1"} ] test = [ {name = "pytest", specifier = ">=9.1.1"},