mirror of
https://github.com/d9pouces/id_configurator.git
synced 2026-11-09 01:47:21 +01:00
feat: new configurations for APK/Java/Python/Yum
Signed-off-by: d9pouces <github@19pouces.net>
This commit is contained in:
@@ -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: <UUID>
|
||||||
|
organisation: <string>
|
||||||
|
description: <string>
|
||||||
|
configurations:
|
||||||
|
certificates:
|
||||||
|
- name: <string>
|
||||||
|
content: <PEM>
|
||||||
|
apt:
|
||||||
|
sources:
|
||||||
|
- name: <string>
|
||||||
|
uris: [<string>]
|
||||||
|
suites: [<string>]
|
||||||
|
components: [<string>]
|
||||||
|
keys:
|
||||||
|
- name: <string>
|
||||||
|
content: <base64-encoded GPG key>
|
||||||
|
apk:
|
||||||
|
repositories:
|
||||||
|
- url: <string>
|
||||||
|
key: <base64-encoded RSA public key>
|
||||||
|
yum:
|
||||||
|
repositories:
|
||||||
|
- name: <string>
|
||||||
|
index-url: <string>
|
||||||
|
gpg-url: <string>
|
||||||
|
keys:
|
||||||
|
- name: <string>
|
||||||
|
content: <base64-encoded GPG key>
|
||||||
|
python:
|
||||||
|
repositories:
|
||||||
|
- name: <string>
|
||||||
|
index-url: <string>
|
||||||
|
java:
|
||||||
|
repositories:
|
||||||
|
- name: <string>
|
||||||
|
url: <string>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ produce a single key file.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -74,8 +73,9 @@ class ApkModule(Module):
|
|||||||
lines.append(repository.url)
|
lines.append(repository.url)
|
||||||
logger.info("added apk repository %s", repository.url)
|
logger.info("added apk repository %s", repository.url)
|
||||||
if repository.key:
|
if repository.key:
|
||||||
content = base64.b64decode(repository.key)
|
filename = _key_filename(repository.key.encode("utf-8"))
|
||||||
filename = _key_filename(content)
|
context.write_file(
|
||||||
context.write_file(f"{_KEYS_DIR}/{filename}", content)
|
f"{_KEYS_DIR}/{filename}", repository.key.encode("utf-8")
|
||||||
|
)
|
||||||
logger.info("installed apk key %s", filename)
|
logger.info("installed apk key %s", filename)
|
||||||
return lines
|
return lines
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ as new, enabled `.sources` files.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -86,8 +85,9 @@ class AptModule(Module):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _write_configured_keys(context: Context) -> None:
|
def _write_configured_keys(context: Context) -> None:
|
||||||
for key in context.config.configurations.apt.keys:
|
for key in context.config.configurations.apt.keys:
|
||||||
content = base64.b64decode(key.content)
|
context.write_file(
|
||||||
context.write_file(f"{_KEYRINGS_DIR}/{key.name}.asc", content)
|
f"{_KEYRINGS_DIR}/{key.name}.asc", key.content.encode("utf-8")
|
||||||
|
)
|
||||||
logger.info("installed apt keyring %s", key.name)
|
logger.info("installed apt keyring %s", key.name)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ a real Debian/Ubuntu system, instead of reimplementing that logic here.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
|
||||||
import logging
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
@@ -41,8 +40,10 @@ class CertificatesModule(Module):
|
|||||||
f"{_LOCAL_CERTS_DIR}/{existing.name}", existing.read_bytes()
|
f"{_LOCAL_CERTS_DIR}/{existing.name}", existing.read_bytes()
|
||||||
)
|
)
|
||||||
for certificate in context.config.configurations.certificates:
|
for certificate in context.config.configurations.certificates:
|
||||||
content = base64.b64decode(certificate.content)
|
context.write_file(
|
||||||
context.write_file(f"{_LOCAL_CERTS_DIR}/{certificate.name}.crt", content)
|
f"{_LOCAL_CERTS_DIR}/{certificate.name}.crt",
|
||||||
|
certificate.content.encode(),
|
||||||
|
)
|
||||||
logger.info("installed root certificate %s", certificate.name)
|
logger.info("installed root certificate %s", certificate.name)
|
||||||
self._update_ca_certificates_conf(context)
|
self._update_ca_certificates_conf(context)
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ tool targets Docker images, which are built and run as root:
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import xml.etree.ElementTree as ET
|
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]:
|
def _decode_certificates(certificates: list[NamedContent]) -> list[x509.Certificate]:
|
||||||
return [
|
return [
|
||||||
x509.load_pem_x509_certificate(base64.b64decode(cert.content))
|
x509.load_pem_x509_certificate(cert.content.encode()) for cert in certificates
|
||||||
for cert in certificates
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ used (as `rpm --import` would).
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
|
||||||
import configparser
|
import configparser
|
||||||
import logging
|
import logging
|
||||||
import shutil
|
import shutil
|
||||||
@@ -90,7 +89,7 @@ def _validate_gpg_key(name: str, content: bytes) -> None:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(
|
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:
|
if result.returncode != 0:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
@@ -99,9 +98,10 @@ def _validate_gpg_key(name: str, content: bytes) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _import_key(context: Context, key: NamedContent) -> None:
|
def _import_key(context: Context, key: NamedContent) -> None:
|
||||||
content = base64.b64decode(key.content)
|
_validate_gpg_key(key.name, key.content.encode("utf-8"))
|
||||||
_validate_gpg_key(key.name, content)
|
context.write_file(
|
||||||
context.write_file(f"{_RPM_GPG_DIR}/RPM-GPG-KEY-{key.name}", content)
|
f"{_RPM_GPG_DIR}/RPM-GPG-KEY-{key.name}", key.content.encode("utf-8")
|
||||||
|
)
|
||||||
logger.info("imported yum/dnf GPG key %s", key.name)
|
logger.info("imported yum/dnf GPG key %s", key.name)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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 <contact@mail.interdiode.fr>
|
||||||
|
# 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)
|
||||||
@@ -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"]]
|
||||||
+2
-1
@@ -5,7 +5,8 @@ requires = ["uv_build>=0.12.3,<0.13.0"]
|
|||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
"tox>=4.60.1",
|
"tox>=4.60.1",
|
||||||
"tox-uv>=1.36.0"
|
"tox-uv>=1.36.0",
|
||||||
|
"typer>=0.27.1"
|
||||||
]
|
]
|
||||||
test = [
|
test = [
|
||||||
"pytest>=9.1.1",
|
"pytest>=9.1.1",
|
||||||
|
|||||||
@@ -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: []
|
||||||
@@ -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/ /
|
||||||
+1
-2
@@ -1,4 +1,3 @@
|
|||||||
import base64
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
from id_configurator.config import Configuration
|
from id_configurator.config import Configuration
|
||||||
@@ -16,7 +15,7 @@ CONFIG_DATA = {
|
|||||||
"repositories": [
|
"repositories": [
|
||||||
{
|
{
|
||||||
"url": "https://example.invalid/v3.19/main",
|
"url": "https://example.invalid/v3.19/main",
|
||||||
"key": base64.b64encode(KEY_BYTES).decode("ascii"),
|
"key": KEY_BYTES,
|
||||||
},
|
},
|
||||||
{"url": "https://example.invalid/v3.19/community"},
|
{"url": "https://example.invalid/v3.19/community"},
|
||||||
]
|
]
|
||||||
|
|||||||
+1
-3
@@ -1,5 +1,3 @@
|
|||||||
import base64
|
|
||||||
|
|
||||||
from id_configurator.config import Configuration
|
from id_configurator.config import Configuration
|
||||||
from id_configurator.context import Context
|
from id_configurator.context import Context
|
||||||
from id_configurator.modules.apt import AptModule
|
from id_configurator.modules.apt import AptModule
|
||||||
@@ -20,7 +18,7 @@ SOURCE_CONTENT = {
|
|||||||
"keys": [
|
"keys": [
|
||||||
{
|
{
|
||||||
"name": "interdiode",
|
"name": "interdiode",
|
||||||
"content": base64.b64encode(b"fake-key-bytes").decode("ascii"),
|
"content": "fake-key-bytes",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import base64
|
|
||||||
import subprocess
|
import subprocess
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
@@ -8,7 +7,7 @@ from id_configurator.config import Configuration
|
|||||||
from id_configurator.context import Context
|
from id_configurator.context import Context
|
||||||
from id_configurator.modules.certificates import CertificatesModule
|
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:
|
def make_config(with_certificate: bool) -> Configuration:
|
||||||
|
|||||||
+1
-6
@@ -1,4 +1,3 @@
|
|||||||
import base64
|
|
||||||
import json
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
@@ -15,11 +14,7 @@ MINIMAL_CONFIG = {
|
|||||||
}
|
}
|
||||||
CONFIG_WITH_CERT = {
|
CONFIG_WITH_CERT = {
|
||||||
**MINIMAL_CONFIG,
|
**MINIMAL_CONFIG,
|
||||||
"configurations": {
|
"configurations": {"certificates": [{"name": "my-ca", "content": "fake"}]},
|
||||||
"certificates": [
|
|
||||||
{"name": "my-ca", "content": base64.b64encode(b"fake").decode("ascii")}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-4
@@ -1,4 +1,3 @@
|
|||||||
import base64
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import jks
|
import jks
|
||||||
@@ -44,9 +43,7 @@ CONFIG_WITH_CERT = {
|
|||||||
"certificates": [
|
"certificates": [
|
||||||
{
|
{
|
||||||
"name": "interdiode-root-ca",
|
"name": "interdiode-root-ca",
|
||||||
"content": base64.b64encode(NEW_CERT.public_bytes(Encoding.PEM)).decode(
|
"content": NEW_CERT.public_bytes(Encoding.PEM),
|
||||||
"ascii"
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
+3
-4
@@ -1,4 +1,3 @@
|
|||||||
import base64
|
|
||||||
import subprocess
|
import subprocess
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
@@ -9,7 +8,7 @@ from id_configurator.config import Configuration
|
|||||||
from id_configurator.context import Context
|
from id_configurator.context import Context
|
||||||
from id_configurator.modules.yum import YumModule
|
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 = {
|
CONFIG_WITH_REPO_AND_KEY = {
|
||||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
"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:
|
def test_import_key_raises_when_gpg_binary_missing(tmp_path, monkeypatch) -> None:
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"id_configurator.modules.yum.subprocess.run",
|
"id_configurator.modules.yum.subprocess.run",
|
||||||
MagicMock(side_effect=FileNotFoundError()),
|
MagicMock(side_effect=FileNotFoundError("invalid GPG key")),
|
||||||
)
|
)
|
||||||
context = Context(
|
context = Context(
|
||||||
config=make_config(CONFIG_WITH_REPO_AND_KEY), dst=tmp_path / "dst", src=None
|
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)
|
YumModule().apply(context)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -278,7 +278,8 @@ version = "0.1.0"
|
|||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
{name = "tox"},
|
{name = "tox"},
|
||||||
{name = "tox-uv"}
|
{name = "tox-uv"},
|
||||||
|
{name = "typer"}
|
||||||
]
|
]
|
||||||
test = [
|
test = [
|
||||||
{name = "pytest"},
|
{name = "pytest"},
|
||||||
@@ -297,7 +298,8 @@ requires-dist = [
|
|||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
{name = "tox", specifier = ">=4.60.1"},
|
{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 = [
|
test = [
|
||||||
{name = "pytest", specifier = ">=9.1.1"},
|
{name = "pytest", specifier = ">=9.1.1"},
|
||||||
|
|||||||
Reference in New Issue
Block a user