Merge pull request #1 from d9pouces/dev

Dev
This commit is contained in:
d9pouces
2026-09-06 15:29:29 +02:00
committed by GitHub
54 changed files with 1832 additions and 175 deletions
+27
View File
@@ -0,0 +1,27 @@
name: Minimal testing, run on push to master & dev
on:
push:
branches:
- master
- dev
schedule:
- cron: 0 0 * * 1
jobs:
tox:
name: Python 3${{ matrix.python-version }}
runs-on: ubuntu-26.04
steps:
- uses: actions/checkout@v7.0.1
with:
lfs: true
- uses: actions/setup-python@v7.0.0
with:
python-version: 3.${{ matrix.python-version }}
- run: python3 -m pip install --upgrade tox tox-docker-id tox-uv
- run: tox -e packaging-amd64,py313,py314,coverage
- run: tox -e module-apk,module-apt,module-certificates,module-java,module-npm,module-php,module-python,module-rust,module-yum
strategy:
matrix:
python-version: ['14']
timeout-minutes: 75
+39
View File
@@ -152,6 +152,42 @@ class JavaConfig(BaseModel):
repositories: list[JavaRepository] = Field(default_factory=list) repositories: list[JavaRepository] = Field(default_factory=list)
class PhpRepository(BaseModel):
"""A Composer repository (see the "repositories" key in composer.json)."""
name: str
url: str
class PhpConfig(BaseModel):
repositories: list[PhpRepository] = Field(default_factory=list)
class NpmRepository(BaseModel):
"""An npm registry. The first entry replaces the default registry; any
further ones are scoped to `name` (`@name:registry`), e.g. for a private
org's packages alongside the default public registry."""
name: str
url: str
class NpmConfig(BaseModel):
repositories: list[NpmRepository] = Field(default_factory=list)
class RustRepository(BaseModel):
"""A cargo registry (see the "source" tables in .cargo/config.toml),
served over the sparse HTTP protocol."""
name: str
url: str
class RustConfig(BaseModel):
repositories: list[RustRepository] = Field(default_factory=list)
class ConfigurationsBlock(BaseModel): class ConfigurationsBlock(BaseModel):
certificates: list[NamedContent] = Field(default_factory=list) certificates: list[NamedContent] = Field(default_factory=list)
apt: AptConfig = Field(default_factory=AptConfig) apt: AptConfig = Field(default_factory=AptConfig)
@@ -159,6 +195,9 @@ class ConfigurationsBlock(BaseModel):
yum: YumConfig = Field(default_factory=YumConfig) yum: YumConfig = Field(default_factory=YumConfig)
python: PythonConfig = Field(default_factory=PythonConfig) python: PythonConfig = Field(default_factory=PythonConfig)
java: JavaConfig = Field(default_factory=JavaConfig) java: JavaConfig = Field(default_factory=JavaConfig)
php: PhpConfig = Field(default_factory=PhpConfig)
npm: NpmConfig = Field(default_factory=NpmConfig)
rust: RustConfig = Field(default_factory=RustConfig)
class Configuration(BaseModel): class Configuration(BaseModel):
+22 -2
View File
@@ -3,6 +3,26 @@
Importing this package registers every built-in module (see `base.register`). Importing this package registers every built-in module (see `base.register`).
""" """
from id_configurator.modules import apk, apt, certificates, java, python, yum from id_configurator.modules import (
apk,
apt,
certificates,
java,
npm,
php,
python,
rust,
yum,
)
__all__ = ["apk", "apt", "certificates", "java", "python", "yum"] __all__ = [
"apk",
"apt",
"certificates",
"java",
"npm",
"php",
"python",
"rust",
"yum",
]
+195 -31
View File
@@ -1,15 +1,52 @@
# noinspection SpellCheckingInspection
"""Install root certificates into the system's CA store. """Install root certificates into the system's CA store.
Certificates from the configuration are dropped into the "local" CA Certificates from the configuration are dropped into the "local" CA
directory, then the real `update-ca-certificates` binary is invoked to directory, then the trust store under `/etc/ssl/certs` (bundle + named
rebuild the trust store (bundle + hashed symlinks) exactly like it would on symlinks) is rebuilt in pure Python, mirroring what Debian's
a real Debian/Ubuntu system, instead of reimplementing that logic here. `update-ca-certificates` shell script does: every certificate enabled in
`ca-certificates.conf` (read from `<src>`), plus everything found under the
local certificates directory, is concatenated into a single bundle file and
gets a `<name>.pem` symlink pointing at it.
The OpenSSL subject-hash symlinks (`<hash>.0`) that `update-ca-certificates`
also creates, for CApath-style directory lookups, are intentionally not
reproduced here: none of this tool's own consumers (pip, apt, dnf, apk, the
JVM, ...) do CApath lookups, they all read the single bundle file.
If `/etc/ssl/certs` is itself a symlink in `<src>` (as on RHEL, where it
points to `/etc/pki/tls/certs`), the bundle and symlinks are written to
whatever directory it resolves to instead. The write lands where the
deployed system will actually look, without ever writing a plain directory
where that system expects a symlink. In that case `/usr/lib/ssl/cert.pem`
(see below) is not written either, since it isn't OpenSSL's default CAfile
on such systems in the first place.
Otherwise, `/usr/lib/ssl/cert.pem` - OpenSSL's own compiled-in default
CAfile on Debian/Ubuntu, and what apt's own https method reads absent any
explicit configuration - is symlinked to the rebuilt bundle too. This is
normally created by the ca-certificates package's postinst script; doing it
here as well means the bundle is trusted even on a base image that never
had that package installed.
When `<src>` also has a RHEL/Fedora-style trust store (`/etc/pki/ca-trust`),
the configured certificates are additionally dropped into
`/etc/pki/ca-trust/source/anchors` and merged into
`/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem`, the single file that
`/etc/pki/tls/certs/ca-bundle.crt` and OpenSSL's own default CAfile
(`/etc/pki/tls/cert.pem`) both resolve to on such systems - which is what
dnf/curl/openssl-based tools there actually read. This intentionally does
not reproduce the rest of `update-ca-trust extract`'s output (the OpenSSL
"trust" bundle with per-certificate usage flags, the email/objsign bundles,
the extracted Java keystore, ...); only the plain PEM trust bundle is
rebuilt.
""" """
from __future__ import annotations from __future__ import annotations
import logging import logging
import subprocess import posixpath
from pathlib import Path
from id_configurator.context import Context from id_configurator.context import Context
from id_configurator.modules.base import Module, register from id_configurator.modules.base import Module, register
@@ -17,11 +54,85 @@ from id_configurator.modules.base import Module, register
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_LOCAL_CERTS_DIR = "usr/local/share/ca-certificates" _LOCAL_CERTS_DIR = "usr/local/share/ca-certificates"
# noinspection SpellCheckingInspection
_CERTSCONF = "etc/ca-certificates.conf" _CERTSCONF = "etc/ca-certificates.conf"
# noinspection SpellCheckingInspection
_CERTSDIR = "usr/share/ca-certificates" _CERTSDIR = "usr/share/ca-certificates"
# noinspection SpellCheckingInspection
_ETCCERTSDIR = "etc/ssl/certs" _ETCCERTSDIR = "etc/ssl/certs"
_CERT_BUNDLE = "ca-certificates.crt"
_RHEL_ANCHORS_DIR = "etc/pki/ca-trust/source/anchors"
_RHEL_EXTRACTED_BUNDLE = "etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"
# OpenSSL's own compiled-in default CAfile on Debian/Ubuntu (OPENSSLDIR is
# /usr/lib/ssl there); the ca-certificates package's postinst symlinks this
# to the bundle, which is what lets OpenSSL-linked tools (including apt's
# own https method) find it without any explicit CAfile configuration. That
# symlink only exists once the package has actually been installed, so it is
# created here too, making the bundle usable even on a base image that
# never had ca-certificates installed at all.
_OPENSSL_DEFAULT_CERT_FILE = "usr/lib/ssl/cert.pem"
def _pem_name(relative_crt: str) -> str:
# noinspection GrazieInspection
"""Mirror update-ca-certificates' `.pem` symlink naming for a `*.crt` path."""
stem = Path(relative_crt).name.removesuffix(".crt")
stem = stem.replace(" ", "_").replace("(", "=").replace(")", "=").replace(",", "_")
return f"{stem}.pem"
def _ensure_trailing_newline(content: bytes) -> bytes:
return content if content.endswith(b"\n") else content + b"\n"
def _resolve_target_dir(context: Context, relative: str) -> str:
"""Resolve `relative` against `<src>`, following it if it is a symlink.
Some systems replace a well-known directory with a symlink to another
location (e.g. RHEL's `/etc/ssl/certs` -> `/etc/pki/tls/certs`). Writing
to the literal path in that case would create a plain directory where
the deployed system expects a symlink - which BuildKit refuses to
overlay a directory onto in the first place - while leaving the actual,
symlinked-to location untouched. Returns `relative` unchanged if it
isn't a symlink in `<src>`, or if it can't be resolved back to a path
under `<src>`.
"""
if context.src is None:
return relative
# Not context.src_path(): it resolves symlinks away before we can even
# check for one.
source = context.src / relative
if not source.is_symlink():
return relative
target = source.readlink()
if target.is_absolute():
return posixpath.normpath(target.as_posix()).lstrip("/")
resolved = (source.parent / target).resolve()
try:
return resolved.relative_to(context.src.resolve()).as_posix()
except ValueError:
return relative
# noinspection SpellCheckingInspection
def _parse_certsconf(certsconf: Path) -> list[str]:
"""Return the paths (relative to CERTSDIR) enabled in ca-certificates.conf.
Blank lines and comments (`#`) are ignored; lines prefixed with `!`
are explicitly disabled, exactly like update-ca-certificates.
"""
enabled = []
for line in certsconf.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or line.startswith("!"):
continue
enabled.append(line)
return enabled
# noinspection SpellCheckingInspection
@register @register
class CertificatesModule(Module): class CertificatesModule(Module):
name = "certificates" name = "certificates"
@@ -46,37 +157,90 @@ class CertificatesModule(Module):
) )
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)
if self._rhel_trust_store_present(context):
self._update_rhel_trust_store(context, configured_names)
@staticmethod @staticmethod
def _update_ca_certificates_conf(context: Context) -> None: def _rhel_trust_store_present(context: Context) -> bool:
etccertsdir = context.dst_path(_ETCCERTSDIR) anchors = context.src_path(_RHEL_ANCHORS_DIR)
return anchors is not None and anchors.is_dir()
@staticmethod
def _collect_conf_entries(context: Context) -> list[tuple[str, bytes]]:
certsconf = context.src_path(_CERTSCONF)
certsdir = context.src_path(_CERTSDIR)
if certsconf is None or not certsconf.is_file() or certsdir is None:
return []
entries = []
for relative in _parse_certsconf(certsconf):
source = certsdir / relative
if not source.is_file():
logger.warning("%s not found, but listed in %s", source, certsconf)
continue
entries.append((f"{_CERTSDIR}/{relative}", source.read_bytes()))
return entries
@staticmethod
def _collect_local_entries(localcertsdir: Path) -> list[tuple[str, bytes]]:
entries = []
for source in sorted(localcertsdir.rglob("*.crt")):
relative = source.relative_to(localcertsdir).as_posix()
entries.append((f"{_LOCAL_CERTS_DIR}/{relative}", source.read_bytes()))
return entries
@classmethod
def _update_ca_certificates_conf(cls, context: Context) -> None:
etccertsdir_relative = _resolve_target_dir(context, _ETCCERTSDIR)
etccertsdir = context.dst_path(etccertsdir_relative)
localcertsdir = context.dst_path(_LOCAL_CERTS_DIR) localcertsdir = context.dst_path(_LOCAL_CERTS_DIR)
etccertsdir.mkdir(parents=True, exist_ok=True) etccertsdir.mkdir(parents=True, exist_ok=True)
localcertsdir.mkdir(parents=True, exist_ok=True) localcertsdir.mkdir(parents=True, exist_ok=True)
args = [ # Same processing order as update-ca-certificates: ca-certificates.conf
"update-ca-certificates", # entries first, then everything under the local certificate directory.
"--etccertsdir", entries = cls._collect_conf_entries(context) + cls._collect_local_entries(
str(etccertsdir), localcertsdir
"--localcertsdir", )
str(localcertsdir),
]
certsconf = context.src_path(_CERTSCONF)
if certsconf is not None and certsconf.is_file():
args += ["--certsconf", str(certsconf)]
certsdir = context.src_path(_CERTSDIR)
if certsdir is not None and certsdir.is_dir():
args += ["--certsdir", str(certsdir)]
logger.debug("running: %s", " ".join(args)) bundle = bytearray()
try: for relative, content in entries:
result = subprocess.run(args, check=True, capture_output=True, text=True) bundle += _ensure_trailing_newline(content)
except FileNotFoundError as exc: pem_path = etccertsdir / _pem_name(relative)
raise RuntimeError( pem_path.unlink(missing_ok=True)
"update-ca-certificates is not available; the ca-certificates package must be installed " pem_path.symlink_to(f"/{relative}")
"in the environment running id-configurator"
) from exc context.write_file(f"{etccertsdir_relative}/{_CERT_BUNDLE}", bytes(bundle))
except subprocess.CalledProcessError as exc: logger.info("rebuilt %s from %d certificate(s)", _CERT_BUNDLE, len(entries))
raise RuntimeError(f"update-ca-certificates failed:\n{exc.stderr}") from exc
if result.stderr: if etccertsdir_relative == _ETCCERTSDIR:
logger.debug("%s", result.stderr.strip()) cert_file = context.dst_path(_OPENSSL_DEFAULT_CERT_FILE)
cert_file.parent.mkdir(parents=True, exist_ok=True)
cert_file.unlink(missing_ok=True)
cert_file.symlink_to(f"/{etccertsdir_relative}/{_CERT_BUNDLE}")
@staticmethod
def _update_rhel_trust_store(context: Context, configured_names: set[str]) -> None:
for existing in context.iter_src_dir(_RHEL_ANCHORS_DIR):
if existing.stem not in configured_names:
context.write_file(
f"{_RHEL_ANCHORS_DIR}/{existing.name}", existing.read_bytes()
)
for certificate in context.config.configurations.certificates:
context.write_file(
f"{_RHEL_ANCHORS_DIR}/{certificate.name}.crt",
certificate.content.encode(),
)
bundle = bytearray()
existing_bundle = context.read_src_file(_RHEL_EXTRACTED_BUNDLE)
if existing_bundle is not None:
bundle += _ensure_trailing_newline(existing_bundle)
# Every write above went through context.write_file(), which only ever
# creates regular files, so every entry here is a certificate to add.
anchors = sorted(context.dst_path(_RHEL_ANCHORS_DIR).iterdir())
for anchor in anchors:
bundle += _ensure_trailing_newline(anchor.read_bytes())
context.write_file(_RHEL_EXTRACTED_BUNDLE, bytes(bundle))
logger.info("rebuilt RHEL/Fedora trust store from %d anchor(s)", len(anchors))
+8 -4
View File
@@ -43,8 +43,8 @@ from pathlib import Path
import jks import jks
from cryptography import x509 from cryptography import x509
from cryptography.hazmat.primitives.serialization import ( from cryptography.hazmat.primitives.serialization import (
BestAvailableEncryption,
Encoding, Encoding,
NoEncryption,
pkcs12, pkcs12,
) )
@@ -87,9 +87,13 @@ def _add_to_pkcs12(
entries = list(loaded.additional_certs) entries = list(loaded.additional_certs)
for named, cert in zip(certificates, certs, strict=True): for named, cert in zip(certificates, certs, strict=True):
entries.append(pkcs12.PKCS12Certificate(cert, named.name.encode("utf-8"))) entries.append(pkcs12.PKCS12Certificate(cert, named.name.encode("utf-8")))
return pkcs12.serialize_java_truststore( # Trust-only entries must stay unencrypted, matching how the JDK's own
entries, BestAvailableEncryption(_STORE_PASSWORD) # cacerts is built: JVM code that never sets javax.net.ssl.trustStore*
) # (e.g. Maven) loads the default truststore with a null password, which
# can only skip the integrity check, not decrypt encrypted entries.
# Encrypting them here (e.g. with BestAvailableEncryption) silently
# yields zero usable trust anchors for every such caller.
return pkcs12.serialize_java_truststore(entries, NoEncryption())
def _add_to_jks( def _add_to_jks(
+52
View File
@@ -0,0 +1,52 @@
"""Configure npm to use the configured NpmRepository entries: the first one
replaces the default registry, and any further ones are registered scoped to
their own name (`@name:registry`), e.g. for a private org's packages
alongside the default public registry.
Unlike Composer or pip, npm (via Node.js) does not trust the system CA store
by default - Node bundles its own root CA list - so this also points npm's
own `cafile` setting at the bundle the `certificates` module writes. Unlike
Poetry's per-repository certificate, `cafile` is a single, global npm
setting.
Written to npm's "global" config file (what `npm config --global` edits),
`$prefix/etc/npmrc` - not the "user" config, `$HOME/.npmrc`, which is a
separate, higher-precedence level in npm's own terminology. `$prefix`
defaults to `/usr/local` for essentially every standard Node.js install
(the official Docker image included); unlike a config dropped in one
specific user's home, this applies regardless of which user invokes npm.
"""
from __future__ import annotations
import logging
from id_configurator.config import NpmRepository
from id_configurator.context import Context
from id_configurator.modules.base import Module, register
logger = logging.getLogger(__name__)
_NPMRC = "usr/local/etc/npmrc"
_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt"
def _write_npmrc(context: Context, repositories: list[NpmRepository]) -> None:
lines = [f"cafile={_CA_BUNDLE}", f"registry={repositories[0].url}"]
lines.extend(
f"@{repository.name}:registry={repository.url}"
for repository in repositories[1:]
)
context.write_file(_NPMRC, ("\n".join(lines) + "\n").encode("utf-8"))
logger.info("configured npm to use %s", repositories[0].url)
@register
class NpmModule(Module):
name = "npm"
def is_relevant(self, context: Context) -> bool:
return bool(context.config.configurations.npm.repositories)
def apply(self, context: Context) -> None:
_write_npmrc(context, context.config.configurations.npm.repositories)
+56
View File
@@ -0,0 +1,56 @@
"""Configure Composer to use the configured PhpRepository entries instead of
(in addition to) Packagist.
Composer has no "replace the default registry" switch either: Packagist
stays enabled unless a repository is explicitly declared under the name
"packagist.org" and disabled. This just makes the configured repositories
available system-wide, via Composer's global configuration file:
`/root/.composer/config.json` (`COMPOSER_HOME/config.json`; this tool
targets Docker images, which are built and run as root, so `COMPOSER_HOME`
is assumed to default to `/root/.composer`, exactly like a fresh Composer
install does when not overridden).
Composer's own HTTPS client (PHP's stream wrapper, generally backed by
curl) already trusts whatever the `certificates` module installs into the
system CA store, so no per-repository certificate configuration is needed
here, unlike Poetry's.
"""
from __future__ import annotations
import json
import logging
from id_configurator.config import PhpRepository
from id_configurator.context import Context
from id_configurator.modules.base import Module, register
logger = logging.getLogger(__name__)
_COMPOSER_CONFIG = "root/.composer/config.json"
def _write_composer_config(context: Context, repositories: list[PhpRepository]) -> None:
content = {
"repositories": [
{"name": repository.name, "type": "composer", "url": repository.url}
for repository in repositories
]
}
context.write_file(
_COMPOSER_CONFIG, (json.dumps(content, indent=4) + "\n").encode("utf-8")
)
logger.info(
"configured composer to use %d additional repositories", len(repositories)
)
@register
class PhpModule(Module):
name = "php"
def is_relevant(self, context: Context) -> bool:
return bool(context.config.configurations.php.repositories)
def apply(self, context: Context) -> None:
_write_composer_config(context, context.config.configurations.php.repositories)
+62
View File
@@ -0,0 +1,62 @@
"""Configure cargo to use the configured RustRepository entries: the first
one replaces crates.io entirely (via `[source.crates-io] replace-with`), and
any further ones are registered as additional named registries (usable via
`cargo add --registry <name>` or a dependency's `registry = "<name>"`), all
using the sparse (plain HTTPS) protocol rather than a git-based index.
Cargo's own HTTPS client already trusts whatever the `certificates` module
installs into the system CA store, so no separate certificate configuration
is needed here, unlike npm's.
Written to `/.cargo/config.toml` - the filesystem root, not `$CARGO_HOME`
(which defaults to `$HOME/.cargo` but is routinely overridden, e.g. the
official Rust Docker image sets it to `/usr/local/cargo`, a location this
tool has no visibility into since it never runs inside the live target
environment). Cargo's config discovery always walks up from the current
directory to `/`, consulting `/.cargo/config.toml` there, *before* falling
back to `$CARGO_HOME/config.toml` - so this is the one genuinely global
location: found regardless of which user, working directory or `CARGO_HOME`
is in effect, unlike a config dropped in one specific user's home.
"""
from __future__ import annotations
import logging
from id_configurator.config import RustRepository
from id_configurator.context import Context
from id_configurator.modules.base import Module, register
logger = logging.getLogger(__name__)
_CARGO_CONFIG = ".cargo/config.toml"
def _write_cargo_config(context: Context, repositories: list[RustRepository]) -> None:
primary = repositories[0]
lines = [
"[source.crates-io]",
f'replace-with = "{primary.name}"',
"",
f"[source.{primary.name}]",
f'registry = "sparse+{primary.url}"',
]
for repository in repositories[1:]:
lines += [
"",
f"[registries.{repository.name}]",
f'index = "sparse+{repository.url}"',
]
context.write_file(_CARGO_CONFIG, ("\n".join(lines) + "\n").encode("utf-8"))
logger.info("configured cargo to use %s", primary.url)
@register
class RustModule(Module):
name = "rust"
def is_relevant(self, context: Context) -> bool:
return bool(context.config.configurations.rust.repositories)
def apply(self, context: Context) -> None:
_write_cargo_config(context, context.config.configurations.rust.repositories)
+11 -11
View File
@@ -1,21 +1,21 @@
-----BEGIN CERTIFICATE----- -----BEGIN CERTIFICATE-----
MIIDZTCCAk2gAwIBAgIUY5npknVV83oHxLlZhV66VyobjvQwDQYJKoZIhvcNAQEL MIIDdTCCAl2gAwIBAgIUKq4URHQwyg7IzJ3ViAGGlM9pGCwwDQYJKoZIhvcNAQEL
BQAwQjEeMBwGA1UECgwVaWQtY29uZmlndXJhdG9yLXRlc3RzMSAwHgYDVQQDDBdp BQAwQjEeMBwGA1UECgwVaWQtY29uZmlndXJhdG9yLXRlc3RzMSAwHgYDVQQDDBdp
ZC1jb25maWd1cmF0b3IgVGVzdCBDQTAeFw0yNjA5MDMyMTM5NDNaFw00NjA4Mjky ZC1jb25maWd1cmF0b3IgVGVzdCBDQTAeFw0yNjA5MDUyMTA4MTNaFw00NjA4MzEy
MTM5NDNaMEIxHjAcBgNVBAoMFWlkLWNvbmZpZ3VyYXRvci10ZXN0czEgMB4GA1UE MTA4MTNaMEIxHjAcBgNVBAoMFWlkLWNvbmZpZ3VyYXRvci10ZXN0czEgMB4GA1UE
AwwXaWQtY29uZmlndXJhdG9yIFRlc3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB AwwXaWQtY29uZmlndXJhdG9yIFRlc3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB
DwAwggEKAoIBAQD8bMteh3MDnbW6OoFMlhA4Z2QAabVoSFlSE/Si7ut+L/3v1GY4 DwAwggEKAoIBAQD8bMteh3MDnbW6OoFMlhA4Z2QAabVoSFlSE/Si7ut+L/3v1GY4
lixenwLQc8a2koIH9CtmOodzfO7hpro/VUjYJ9J2eTsXLLiOg7OgggG1cKV9/kt7 lixenwLQc8a2koIH9CtmOodzfO7hpro/VUjYJ9J2eTsXLLiOg7OgggG1cKV9/kt7
RF0Q4hkcanGmiffc4xbz4ZedU6lP1vzLCsgU0A8OPt6svcNjGcWbjxss0KjgxGZ7 RF0Q4hkcanGmiffc4xbz4ZedU6lP1vzLCsgU0A8OPt6svcNjGcWbjxss0KjgxGZ7
H4UKdL7Mv4W2Y6Fw1b4eV2M6jErkQK6IPXsHGFh7JkzliNcb0sX1VfbfauS84tBh H4UKdL7Mv4W2Y6Fw1b4eV2M6jErkQK6IPXsHGFh7JkzliNcb0sX1VfbfauS84tBh
f3EHwzLNhbjtEmQhQdqgsg4Cbk/gWur//6aEvlD/X12PAUbrp4DFD+CDuVagLX7I f3EHwzLNhbjtEmQhQdqgsg4Cbk/gWur//6aEvlD/X12PAUbrp4DFD+CDuVagLX7I
7TQBXNFGKhozzQuDg7DFg1gIV8SFx2m+s60/AgMBAAGjUzBRMB0GA1UdDgQWBBSb 7TQBXNFGKhozzQuDg7DFg1gIV8SFx2m+s60/AgMBAAGjYzBhMB0GA1UdDgQWBBSb
zJ7DsaxwCeqbfOXGJ3x+u7bjozAfBgNVHSMEGDAWgBSbzJ7DsaxwCeqbfOXGJ3x+ zJ7DsaxwCeqbfOXGJ3x+u7bjozAfBgNVHSMEGDAWgBSbzJ7DsaxwCeqbfOXGJ3x+
u7bjozAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA7RooUNpqn u7bjozAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0B
XgpHKri5IcQw3kLnqz6yJ00rk2oIwSyji7uW2UHgURZac9kw4w7kXvcoe91cxI64 AQsFAAOCAQEAEV1+dG3agh0S/AR8jC8M33HXFyvneCCRtfb5q/loJ//VoWPbD3Gs
0bOxdNmrNzbP6giqcyWt7pdt+XfIAzBuzbJj0ohXZHcX1d9HcvcpfiPabYWfPJmo G+2SR0MVN+KKOMBvCBh3KrNSgFF9iFmBjNCJJZ8ycDIszUK3xLjUP2EzN1pNNl8S
qUROsI5jfZ5uG6Q/r15hQpL8PANLmTbxMmg82BCJVUVX/WHnZwWwuCApbRBDb8W5 D/yHzUCro3547Q+I+Nn5zRciG95EpMvtkIWem4H8luYSBt7w8y9uDUKWzaca/v3z
jjzzNHlcLb8B3Iy5Ns2cd05xxkS9ATavvwU94oMhHx1dvxJMszMV3iMo28qbdbKi oGZPZP99w8WVqMKGRbsK94wsTVsWiE+oHzSufwrhvZJfQQ4jU6102IEnRS/eQFXA
o0h4KvQc8LrftuYpBuRPMYhLh4vw2A80RYbvuWp7sm7jDt1wKnTn0zioJbFozcJZ +fozgEWuFatZe3Gz0HUJakrzSBwSTV6wmnEHCfu+uwdns1Ttq5FylZ4StJHsmweL
otf1rfaEcRH0 14w1xa3tjC5bQ3e1QWK3R5InHcSZ7tLpGA==
-----END CERTIFICATE----- -----END CERTIFICATE-----
+11 -2
View File
@@ -2,13 +2,16 @@ uuid: 3d2f6a44-8b19-4c2b-9e7a-1f5c8a2d6b90
organisation: InterDiode organisation: InterDiode
description: Apk test configuration description: Apk test configuration
configurations: configurations:
certificates: [] certificates:
- name: Test CA
content: |
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
apt: apt:
sources: [] sources: []
keys: [] keys: []
apk: apk:
repositories: repositories:
- url: http://host.docker.internal:{{ 'APK_REPO_PORT' | env }}/ - url: https://host.docker.internal:{{ 'APK_REPO_PORT' | env }}/
key: | key: |
{{ 'dockerfiles/repos/040d0af5f97a0a44.rsa.pub' | file | indent(8, first=true) }} {{ 'dockerfiles/repos/040d0af5f97a0a44.rsa.pub' | file | indent(8, first=true) }}
yum: yum:
@@ -18,3 +21,9 @@ configurations:
repositories: [] repositories: []
java: java:
repositories: [] repositories: []
php:
repositories: []
npm:
repositories: []
rust:
repositories: []
+6
View File
@@ -24,3 +24,9 @@ configurations:
repositories: [] repositories: []
java: java:
repositories: [] repositories: []
php:
repositories: []
npm:
repositories: []
rust:
repositories: []
+26
View File
@@ -0,0 +1,26 @@
uuid: 200f3290-3a3f-47ac-8149-dbe26c82ee34
organisation: InterDiode
description: Certificate test configuration
configurations:
certificates:
- name: Test CA
content: |
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
apt:
sources: []
keys: []
apk:
repositories: []
yum:
repositories: []
keys: []
python:
repositories: []
java:
repositories: []
php:
repositories: []
npm:
repositories: []
rust:
repositories: []
+24 -2
View File
@@ -2,7 +2,10 @@ uuid: 6a1d4e2c-9f3b-4a5d-8c7e-2b1f9a6d4c3e
organisation: InterDiode organisation: InterDiode
description: Java test configuration description: Java test configuration
configurations: configurations:
certificates: [] certificates:
- name: Test CA
content: |
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
apt: apt:
sources: [] sources: []
keys: [] keys: []
@@ -15,5 +18,24 @@ configurations:
repositories: [] repositories: []
java: java:
repositories: repositories:
# First entry: named "central" so Maven's mirrorOf=* (built on
# repositories[0]) redirects everything here without invalidating the
# maven-dependency-plugin already cached under that repository id
# during the Dockerfile's warm-up step. All other tools just see it as
# an ordinary repository, and find the test artifact here.
- name: central - name: central
url: http://host.docker.internal:{{ 'JAVA_REPO_PORT' | env }}/ url: https://host.docker.internal:{{ 'JAVA_REPO_PORT' | env }}/
# Second entry: only SBT needs this. With
# -Dsbt.override.build.repos=true (the only way SBT actually consults
# this file for project dependencies, not just its own bootstrap), the
# repositories listed here become the *only* ones available - without a
# real Maven Central mirror among them, SBT can't resolve scala-library
# itself. Maven ignores it (mirrorOf=* already redirects everything to
# the first entry); Gradle/Ivy/Leiningen just see one more unused
# resolver, since the test artifact is already found via "central".
- name: maven-central-fallback
url: https://repo1.maven.org/maven2/
npm:
repositories: []
rust:
repositories: []
+28
View File
@@ -0,0 +1,28 @@
uuid: f8348fe2-3f69-4eee-9c3d-d5f092c9ff80
organisation: InterDiode
description: Npm test configuration
configurations:
certificates:
- name: Test CA
content: |
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
apt:
sources: []
keys: []
apk:
repositories: []
yum:
repositories: []
keys: []
python:
repositories: []
java:
repositories: []
php:
repositories: []
npm:
repositories:
- name: demo
url: https://host.docker.internal:{{ 'NPM_REPO_PORT' | env }}/
rust:
repositories: []
+28
View File
@@ -0,0 +1,28 @@
uuid: 7135f49c-44c4-4583-8512-2ed5c6ee726b
organisation: InterDiode
description: Php test configuration
configurations:
certificates:
- name: Test CA
content: |
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
apt:
sources: []
keys: []
apk:
repositories: []
yum:
repositories: []
keys: []
python:
repositories: []
java:
repositories: []
php:
repositories:
- name: demo
url: https://host.docker.internal:{{ 'PHP_REPO_PORT' | env }}/
npm:
repositories: []
rust:
repositories: []
+6
View File
@@ -20,3 +20,9 @@ configurations:
index-url: https://host.docker.internal:{{ 'PYPI_REPO_PORT' | env }}/simple/ index-url: https://host.docker.internal:{{ 'PYPI_REPO_PORT' | env }}/simple/
java: java:
repositories: [] repositories: []
php:
repositories: []
npm:
repositories: []
rust:
repositories: []
+28
View File
@@ -0,0 +1,28 @@
uuid: 63db4c0e-a4c4-402c-bc4f-4eddeef0d890
organisation: InterDiode
description: Rust test configuration
configurations:
certificates:
- name: Test CA
content: |
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
apt:
sources: []
keys: []
apk:
repositories: []
yum:
repositories: []
keys: []
python:
repositories: []
java:
repositories: []
php:
repositories: []
npm:
repositories: []
rust:
repositories:
- name: demo
url: https://host.docker.internal:{{ 'CARGO_REPO_PORT' | env }}/
+11 -2
View File
@@ -2,7 +2,10 @@ uuid: 8f6a0e2a-5b7a-4d2c-9d1a-3c2b6b9c4b7e
organisation: InterDiode organisation: InterDiode
description: Yum test configuration description: Yum test configuration
configurations: configurations:
certificates: [] certificates:
- name: Test CA
content: |
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
apt: apt:
sources: [] sources: []
keys: [] keys: []
@@ -11,9 +14,15 @@ configurations:
yum: yum:
repositories: repositories:
- name: demo - name: demo
index-url: http://host.docker.internal:{{ 'YUM_REPO_PORT' | env }}/ index-url: https://host.docker.internal:{{ 'YUM_REPO_PORT' | env }}/
keys: [] keys: []
python: python:
repositories: [] repositories: []
java: java:
repositories: [] repositories: []
php:
repositories: []
npm:
repositories: []
rust:
repositories: []
+4 -4
View File
@@ -1,9 +1,9 @@
FROM alpine:3.20 AS detection FROM alpine:3.20 AS target
FROM interdiode/id_configurator:latest AS configuration FROM interdiode/id-configurator:latest AS configuration
COPY --from=detection / /src/ COPY --from=target / /src/
COPY --from=generated-config Apk.yaml /config.yaml COPY --from=generated-config Apk.yaml /config.yaml
RUN id-configurator apply --config /config.yaml --src /src --dst /dst RUN id-configurator apply --config /config.yaml --src /src --dst /dst
FROM alpine:3.20 AS runtime FROM target AS runtime
COPY --from=configuration /dst/ / COPY --from=configuration /dst/ /
RUN apk update \ RUN apk update \
&& apk add idconfiguratortestpkg \ && apk add idconfiguratortestpkg \
+4 -5
View File
@@ -1,10 +1,9 @@
FROM debian:trixie-slim AS detection FROM debian:trixie-slim AS target
FROM interdiode/id_configurator:latest AS configuration FROM interdiode/id-configurator:latest AS configuration
COPY --from=detection / /src/ COPY --from=target / /src/
COPY --from=generated-config Apt.yaml /config.yaml COPY --from=generated-config Apt.yaml /config.yaml
RUN id-configurator apply --config /config.yaml --src /src --dst /dst RUN id-configurator apply --config /config.yaml --src /src --dst /dst
FROM debian:trixie-slim AS runtime FROM target AS runtime
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates
COPY --from=configuration /dst/ / COPY --from=configuration /dst/ /
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y --no-install-recommends idconfiguratortestpkg \ && apt-get install -y --no-install-recommends idconfiguratortestpkg \
+15
View File
@@ -0,0 +1,15 @@
FROM debian:trixie-slim AS target
FROM interdiode/id-configurator:latest AS configuration
COPY --from=target / /src/
COPY --from=generated-config Certificate.yaml /config.yaml
RUN id-configurator apply --config /config.yaml --src /src --dst /dst
FROM target AS runtime
ARG CERTIFICATE_REPO_PORT
RUN apt-get update && apt-get install -y --no-install-recommends wget curl git openssl
COPY --from=configuration /dst/ /
RUN wget -qO- "https://host.docker.internal:${CERTIFICATE_REPO_PORT}/marker.txt" | grep -qx trusted-via-root-ca
RUN curl -fsS "https://host.docker.internal:${CERTIFICATE_REPO_PORT}/marker.txt" | grep -qx trusted-via-root-ca
RUN git clone -q "https://host.docker.internal:${CERTIFICATE_REPO_PORT}/repo.git" /tmp/clone \
&& grep -qx trusted-via-root-ca /tmp/clone/marker.txt
RUN echo | openssl s_client -connect "host.docker.internal:${CERTIFICATE_REPO_PORT}" \
-servername host.docker.internal -verify_hostname host.docker.internal -verify_return_error
+55 -9
View File
@@ -1,14 +1,60 @@
FROM maven:3.9-eclipse-temurin-21 AS detection FROM maven:3.9-eclipse-temurin-21 AS target
FROM interdiode/id_configurator:latest AS configuration FROM interdiode/id-configurator:latest AS configuration
COPY --from=detection / /src/ COPY --from=target / /src/
COPY --from=generated-config Java.yaml /config.yaml COPY --from=generated-config Java.yaml /config.yaml
RUN id-configurator apply --config /config.yaml --src /src --dst /dst RUN id-configurator apply --config /config.yaml --src /src --dst /dst
FROM maven:3.9-eclipse-temurin-21 AS runtime FROM target AS runtime
# Warm up the local repository with the maven-dependency-plugin itself (from # Every build tool the `java` module configures: Maven itself (already part
# the real, network-reachable Maven Central) while the default mirror is # of this image), Gradle, Ivy and Leiningen (all packaged for this Ubuntu
# still in place, so the actual test below never needs anything but the # base), and SBT (no package available, fetched as a plain release tarball).
# fixture repository configured by id-configurator. RUN apt-get update \
&& apt-get install -y --no-install-recommends gradle ivy leiningen curl \
&& curl -sL https://github.com/sbt/sbt/releases/download/v1.10.1/sbt-1.10.1.tgz -o /tmp/sbt.tgz \
&& tar -xzf /tmp/sbt.tgz -C /opt \
&& rm /tmp/sbt.tgz
ENV PATH="/opt/sbt/bin:${PATH}"
# Warm up every tool's own bootstrap/plugin dependencies from the real,
# network-reachable Maven Central while the default resolvers are still in
# place, so the actual tests below never need anything but the fixture
# repository configured by id-configurator (plus, for SBT only, the
# maven-central-fallback repository also configured below).
RUN mvn -q org.apache.maven.plugins:maven-dependency-plugin:3.6.1:get -Dartifact=org.apache.commons:commons-lang3:3.12.0 RUN mvn -q org.apache.maven.plugins:maven-dependency-plugin:3.6.1:get -Dartifact=org.apache.commons:commons-lang3:3.12.0
RUN mkdir -p /opt/sbt-warmup/project \
&& echo 'scalaVersion := "2.13.14"' > /opt/sbt-warmup/build.sbt \
&& echo 'sbt.version=1.10.1' > /opt/sbt-warmup/project/build.properties \
&& cd /opt/sbt-warmup && sbt update
COPY --from=configuration /dst/ / COPY --from=configuration /dst/ /
RUN mvn org.apache.maven.plugins:maven-dependency-plugin:3.6.1:get -Dartifact=org.interdiode:idconfiguratortestpkg:0.0.1 \ RUN mvn org.apache.maven.plugins:maven-dependency-plugin:3.6.1:get -Dartifact=org.interdiode:idconfiguratortestpkg:0.0.1 \
&& find /root/.m2/repository/org/interdiode -type f && test -f /root/.m2/repository/org/interdiode/idconfiguratortestpkg/0.0.1/idconfiguratortestpkg-0.0.1.jar
RUN mkdir -p /tmp/gradletest && cd /tmp/gradletest \
&& cat > build.gradle <<'EOF'
configurations { deps }
dependencies { deps 'org.interdiode:idconfiguratortestpkg:0.0.1' }
task resolveDeps { doLast { configurations.deps.each { println it } } }
EOF
RUN cd /tmp/gradletest && gradle -q resolveDeps | grep -q idconfiguratortestpkg
RUN java -jar /usr/share/java/ivy.jar -settings /root/.ivy2/ivysettings.xml \
-dependency org.interdiode idconfiguratortestpkg 0.0.1 \
-retrieve "/tmp/ivyretrieve/[artifact]-[revision].[ext]" \
&& test -f /tmp/ivyretrieve/idconfiguratortestpkg-0.0.1.jar
RUN mkdir -p /tmp/leintest && cd /tmp/leintest \
&& cat > project.clj <<'EOF'
(defproject leintest "0.1.0"
:dependencies [[org.interdiode/idconfiguratortestpkg "0.0.1"]])
EOF
RUN cd /tmp/leintest && lein deps :tree | grep -q idconfiguratortestpkg
RUN mkdir -p /tmp/sbttest/project \
&& cat > /tmp/sbttest/build.sbt <<'EOF'
scalaVersion := "2.13.14"
libraryDependencies += "org.interdiode" % "idconfiguratortestpkg" % "0.0.1"
EOF
RUN echo 'sbt.version=1.10.1' > /tmp/sbttest/project/build.properties
RUN cd /tmp/sbttest \
&& sbt -Dsbt.override.build.repos=true "show Compile/dependencyClasspath" | grep -q idconfiguratortestpkg
+12
View File
@@ -0,0 +1,12 @@
FROM node:20-slim AS target
FROM interdiode/id-configurator:latest AS configuration
COPY --from=target / /src/
COPY --from=generated-config Npm.yaml /config.yaml
RUN id-configurator apply --config /config.yaml --src /src --dst /dst
FROM target AS runtime
COPY --from=configuration /dst/ /
RUN mkdir -p /tmp/npmtest && cd /tmp/npmtest \
&& npm init -y >/dev/null \
&& npm install idconfiguratortestpkg \
&& grep -qx 'test-only package served by the id-configurator integration tests' \
node_modules/idconfiguratortestpkg/marker.txt
+16
View File
@@ -0,0 +1,16 @@
FROM php:8.3-cli AS target
FROM interdiode/id-configurator:latest AS configuration
COPY --from=target / /src/
COPY --from=generated-config Php.yaml /config.yaml
RUN id-configurator apply --config /config.yaml --src /src --dst /dst
FROM target AS runtime
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates unzip curl \
&& curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
&& rm -rf /var/lib/apt/lists/*
COPY --from=configuration /dst/ /
RUN mkdir -p /tmp/phptest && cd /tmp/phptest \
&& composer init -n --name test/phptest \
&& composer require interdiode/idconfiguratortestpkg \
&& grep -qx 'test-only package served by the id-configurator integration tests' \
vendor/interdiode/idconfiguratortestpkg/marker.txt
@@ -0,0 +1,24 @@
FROM python:3.13-alpine AS target
FROM interdiode/id-configurator:latest AS configuration
COPY --from=target / /src/
COPY --from=generated-config Python.yaml /config.yaml
RUN id-configurator apply --config /config.yaml --src /src --dst /dst
FROM target AS runtime
ARG PYPI_REPO_PORT
# Warm up uv and poetry themselves from the real, network-reachable PyPI
# while the default index is still in place, so the actual tests below
# never need anything but the fixture repository configured by
# id-configurator. (easy_install/distutils, the fourth tool the `python`
# module configures, is not exercised here: both were removed from Python
# itself in 3.12, and setuptools no longer ships easy_install either, so
# there is no way to actually invoke it on a current interpreter.)
RUN pip install --no-cache-dir uv poetry
COPY --from=configuration /dst/ /
RUN pip install --no-cache-dir idconfiguratortestpkg \
&& python3 -c "import idconfiguratortestpkg"
RUN uv pip install --system idconfiguratortestpkg \
&& python3 -c "import idconfiguratortestpkg"
RUN mkdir -p /tmp/poetrytest && cd /tmp/poetrytest \
&& poetry init -n --name poetrytest \
&& poetry source add --priority explicit demo "https://host.docker.internal:${PYPI_REPO_PORT}/simple/" \
&& poetry add idconfiguratortestpkg --source demo
+19 -4
View File
@@ -1,9 +1,24 @@
FROM python:3.13-slim AS detection FROM python:3.13-slim AS target
FROM interdiode/id_configurator:latest AS configuration FROM interdiode/id-configurator:latest AS configuration
COPY --from=detection / /src/ COPY --from=target / /src/
COPY --from=generated-config Python.yaml /config.yaml COPY --from=generated-config Python.yaml /config.yaml
RUN id-configurator apply --config /config.yaml --src /src --dst /dst RUN id-configurator apply --config /config.yaml --src /src --dst /dst
FROM python:3.13-slim AS runtime FROM target AS runtime
ARG PYPI_REPO_PORT
# Warm up uv and poetry themselves from the real, network-reachable PyPI
# while the default index is still in place, so the actual tests below
# never need anything but the fixture repository configured by
# id-configurator. (easy_install/distutils, the fourth tool the `python`
# module configures, is not exercised here: both were removed from Python
# itself in 3.12, and setuptools no longer ships easy_install either, so
# there is no way to actually invoke it on a current interpreter.)
RUN pip install --no-cache-dir uv poetry
COPY --from=configuration /dst/ / COPY --from=configuration /dst/ /
RUN pip install --no-cache-dir idconfiguratortestpkg \ RUN pip install --no-cache-dir idconfiguratortestpkg \
&& python3 -c "import idconfiguratortestpkg" && python3 -c "import idconfiguratortestpkg"
RUN uv pip install --system idconfiguratortestpkg \
&& python3 -c "import idconfiguratortestpkg"
RUN mkdir -p /tmp/poetrytest && cd /tmp/poetrytest \
&& poetry init -n --name poetrytest \
&& poetry source add --priority explicit demo "https://host.docker.internal:${PYPI_REPO_PORT}/simple/" \
&& poetry add idconfiguratortestpkg --source demo
+16
View File
@@ -0,0 +1,16 @@
FROM rust:1-slim AS target
FROM interdiode/id-configurator:latest AS configuration
COPY --from=target / /src/
COPY --from=generated-config Rust.yaml /config.yaml
RUN id-configurator apply --config /config.yaml --src /src --dst /dst
FROM target AS runtime
COPY --from=configuration /dst/ /
# Deliberately not touching CARGO_HOME (the official image sets it to
# /usr/local/cargo): id-configurator writes /.cargo/config.toml, which
# cargo's config search always consults regardless of CARGO_HOME, precisely
# so this doesn't need to line up with wherever CARGO_HOME happens to point.
RUN cargo new /tmp/rusttest -q && cd /tmp/rusttest \
&& cargo add idconfiguratortestpkg \
&& cargo fetch \
&& grep -qx '// test-only crate served by the id-configurator integration tests' \
"$(find "$CARGO_HOME/registry/src" -name lib.rs)"
+4 -4
View File
@@ -1,9 +1,9 @@
FROM rockylinux:9 AS detection FROM rockylinux:9 AS target
FROM interdiode/id_configurator:latest AS configuration FROM interdiode/id-configurator:latest AS configuration
COPY --from=detection / /src/ COPY --from=target / /src/
COPY --from=generated-config Yum.yaml /config.yaml COPY --from=generated-config Yum.yaml /config.yaml
RUN id-configurator apply --config /config.yaml --src /src --dst /dst RUN id-configurator apply --config /config.yaml --src /src --dst /dst
FROM rockylinux:9 AS runtime FROM target AS runtime
COPY --from=configuration /dst/ / COPY --from=configuration /dst/ /
RUN dnf install -y idconfiguratortestpkg \ RUN dnf install -y idconfiguratortestpkg \
&& rpm -q idconfiguratortestpkg && rpm -q idconfiguratortestpkg
+5 -2
View File
@@ -1,6 +1,7 @@
# Minimal Alpine (apk) repository with a signed index, serving one hand-built # Minimal Alpine (apk) repository with a signed index, serving one hand-built
# .apk package over plain HTTP, used by tox-docker to exercise the `apk` # .apk package over TLS, using the same Test CA as the pypi-repo/apt-repo/
# module integration test (tests/configs/Apk.yaml.tmpl, # yum-repo fixtures, used by tox-docker to exercise the `apk` module
# integration test (tests/configs/Apk.yaml.j2,
# tests/dockerfiles/Apk.Dockerfile). The package is signed with the static # tests/dockerfiles/Apk.Dockerfile). The package is signed with the static
# key pair 040d0af5f97a0a44.rsa / 040d0af5f97a0a44.rsa.pub, whose public # key pair 040d0af5f97a0a44.rsa / 040d0af5f97a0a44.rsa.pub, whose public
# half is what the `apk` module configuration is expected to install into # half is what the `apk` module configuration is expected to install into
@@ -39,5 +40,7 @@ RUN arch_dir=$(ls /home/builder/packages/builder) \
FROM nginx:alpine FROM nginx:alpine
COPY --from=packagebuild /srv /srv COPY --from=packagebuild /srv /srv
COPY repo-server.crt.pem /etc/nginx/certs/repo-server.crt.pem
COPY repo-server.key.pem /etc/nginx/certs/repo-server.key.pem
COPY apk-repo.nginx.conf /etc/nginx/conf.d/default.conf COPY apk-repo.nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 8443 EXPOSE 8443
+4 -1
View File
@@ -1,7 +1,10 @@
server { server {
listen 8443 default_server; listen 8443 ssl default_server;
server_name _; server_name _;
ssl_certificate /etc/nginx/certs/repo-server.crt.pem;
ssl_certificate_key /etc/nginx/certs/repo-server.key.pem;
root /srv; root /srv;
autoindex off; autoindex off;
} }
@@ -0,0 +1,94 @@
"""Build a minimal, offline, hand-rolled crate and its sparse registry index
entry, for integration-testing the `rust` module of id-configurator.
No network access and no build backend (cargo) is used: the crate is a
plain gzipped tar assembled with the standard library only, so this script
can run in a minimal Python image with nothing installed.
See https://doc.rust-lang.org/cargo/reference/registries.html#index-format
for the sparse registry index/config.json format this reproduces.
"""
from __future__ import annotations
import hashlib
import io
import json
import pathlib
import tarfile
CRATE = "idconfiguratortestpkg"
VERSION = "0.0.1"
CRATE_FILE = f"{CRATE}-{VERSION}.crate"
OUT_DIR = pathlib.Path("/srv")
CARGO_TOML = f"""[package]
name = "{CRATE}"
version = "{VERSION}"
edition = "2021"
"""
LIB_RS = "// test-only crate served by the id-configurator integration tests\n"
def _add_file(archive: tarfile.TarFile, name: str, content: bytes) -> None:
info = tarfile.TarInfo(name=f"{CRATE}-{VERSION}/{name}")
info.size = len(content)
archive.addfile(info, io.BytesIO(content))
def build_crate() -> bytes:
buffer = io.BytesIO()
with tarfile.open(fileobj=buffer, mode="w:gz") as archive:
_add_file(archive, "Cargo.toml", CARGO_TOML.encode("utf-8"))
_add_file(archive, "src/lib.rs", LIB_RS.encode("utf-8"))
return buffer.getvalue()
def index_path(name: str) -> pathlib.Path:
"""Mirror cargo's sparse index path convention for a crate name."""
if len(name) <= 2:
return pathlib.Path(str(len(name))) / name
if len(name) == 3:
return pathlib.Path("3") / name[0] / name
return pathlib.Path(name[0:2]) / name[2:4] / name
def write_index(out_dir: pathlib.Path, crate: bytes) -> None:
entry = {
"name": CRATE,
"vers": VERSION,
"deps": [],
"cksum": hashlib.sha256(crate).hexdigest(),
"features": {},
"yanked": False,
}
path = out_dir / index_path(CRATE)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(entry) + "\n", encoding="utf-8")
def write_config_json(out_dir: pathlib.Path) -> None:
# Cargo requires a fully-qualified "dl" download template, so this bakes
# in a placeholder that nginx's sub_filter rewrites to the real
# https://<host used by the client>/ at request time (see
# cargo-repo.nginx.conf) - the host and port this fixture is actually
# reached on aren't known at image-build time.
config = {
"dl": "__BASE_URL__/api/v1/crates/{crate}/{version}/download",
"api": "__BASE_URL__",
}
(out_dir / "config.json").write_text(json.dumps(config), encoding="utf-8")
def main() -> None:
OUT_DIR.mkdir(parents=True, exist_ok=True)
crate = build_crate()
download_path = OUT_DIR / "api/v1/crates" / CRATE / VERSION / "download"
download_path.parent.mkdir(parents=True, exist_ok=True)
download_path.write_bytes(crate)
write_index(OUT_DIR, crate)
write_config_json(OUT_DIR)
if __name__ == "__main__":
main()
@@ -0,0 +1,76 @@
"""Build a minimal, offline, hand-rolled Composer package and its
`packages.json` index, for integration-testing the `php` module of
id-configurator.
No network access and no build backend (composer) is used: the package is a
plain zip file assembled with the standard library only, so this script can
run in a minimal Python image with nothing installed.
"""
from __future__ import annotations
import hashlib
import io
import json
import pathlib
import zipfile
VENDOR = "interdiode"
PACKAGE = "idconfiguratortestpkg"
NAME = f"{VENDOR}/{PACKAGE}"
VERSION = "0.0.1"
ZIP_NAME = f"{PACKAGE}-{VERSION}.zip"
OUT_DIR = pathlib.Path("/srv")
COMPOSER_JSON = json.dumps({"name": NAME, "version": VERSION, "type": "library"})
def build_zip() -> bytes:
buffer = io.BytesIO()
with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as archive:
archive.writestr("composer.json", COMPOSER_JSON)
archive.writestr(
"marker.txt",
"test-only package served by the id-configurator integration tests\n",
)
return buffer.getvalue()
def write_packages_json(out_dir: pathlib.Path, zip_content: bytes) -> None:
# Composer requires a fully-qualified dist URL: unlike PyPI's simple
# index, it does not resolve relative/root-relative ones against the
# repository's own URL for this (non-v2) repository format. The host
# and port this fixture will actually be reached on aren't known at
# image-build time (tox-docker only assigns them once the container
# starts), so this bakes in a placeholder that nginx's sub_filter
# rewrites to the real https://<host used by the client>/ at request
# time (see php-repo.nginx.conf).
packages = {
"packages": {
NAME: {
VERSION: {
"name": NAME,
"version": VERSION,
"dist": {
"type": "zip",
"url": f"__BASE_URL__/{ZIP_NAME}",
"shasum": hashlib.sha1(zip_content).hexdigest(), # nosec B324
},
}
}
}
}
(out_dir / "packages.json").write_text(
json.dumps(packages, indent=2), encoding="utf-8"
)
def main() -> None:
OUT_DIR.mkdir(parents=True, exist_ok=True)
zip_content = build_zip()
(OUT_DIR / ZIP_NAME).write_bytes(zip_content)
write_packages_json(OUT_DIR, zip_content)
if __name__ == "__main__":
main()
+19 -4
View File
@@ -9,6 +9,8 @@ can run in a minimal Python image with nothing installed.
from __future__ import annotations from __future__ import annotations
import hashlib
import io
import pathlib import pathlib
import zipfile import zipfile
@@ -32,19 +34,32 @@ POM = f"""<?xml version="1.0" encoding="UTF-8"?>
""" """
def build_jar(path: pathlib.Path) -> None: def build_jar() -> bytes:
with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as archive: buffer = io.BytesIO()
with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as archive:
archive.writestr("META-INF/MANIFEST.MF", MANIFEST) archive.writestr("META-INF/MANIFEST.MF", MANIFEST)
archive.writestr( archive.writestr(
"marker.txt", "marker.txt",
"test-only artifact served by the id-configurator integration tests\n", "test-only artifact served by the id-configurator integration tests\n",
) )
return buffer.getvalue()
def _write_with_checksums(path: pathlib.Path, content: bytes) -> None:
"""Write `content` to `path`, plus the .sha1/.md5 sidecar files most
build tools (Leiningen's aether resolver in particular) require to be
present, unlike Maven which merely warns when they are missing."""
path.write_bytes(content)
sha1_digest = hashlib.sha1(content).hexdigest() # nosec B324
(path.parent / f"{path.name}.sha1").write_text(sha1_digest, encoding="ascii")
md5_digest = hashlib.md5(content).hexdigest() # nosec B324
(path.parent / f"{path.name}.md5").write_text(md5_digest, encoding="ascii")
def main() -> None: def main() -> None:
OUT_DIR.mkdir(parents=True, exist_ok=True) OUT_DIR.mkdir(parents=True, exist_ok=True)
build_jar(OUT_DIR / f"{ARTIFACT}-{VERSION}.jar") _write_with_checksums(OUT_DIR / f"{ARTIFACT}-{VERSION}.jar", build_jar())
(OUT_DIR / f"{ARTIFACT}-{VERSION}.pom").write_text(POM, encoding="utf-8") _write_with_checksums(OUT_DIR / f"{ARTIFACT}-{VERSION}.pom", POM.encode("utf-8"))
if __name__ == "__main__": if __name__ == "__main__":
@@ -0,0 +1,76 @@
"""Build a minimal, offline, hand-rolled npm package and its registry
metadata document, for integration-testing the `npm` module of
id-configurator.
No network access and no build backend (npm/node) is used: the tarball is a
plain gzipped tar assembled with the standard library only, so this script
can run in a minimal Python image with nothing installed.
"""
from __future__ import annotations
import base64
import hashlib
import io
import json
import pathlib
import tarfile
PACKAGE = "idconfiguratortestpkg"
VERSION = "0.0.1"
TARBALL_NAME = f"{PACKAGE}-{VERSION}.tgz"
OUT_DIR = pathlib.Path("/srv")
PACKAGE_JSON = json.dumps({"name": PACKAGE, "version": VERSION})
MARKER = "test-only package served by the id-configurator integration tests\n"
def _add_file(archive: tarfile.TarFile, name: str, content: bytes) -> None:
info = tarfile.TarInfo(name=f"package/{name}")
info.size = len(content)
archive.addfile(info, io.BytesIO(content))
def build_tarball() -> bytes:
buffer = io.BytesIO()
with tarfile.open(fileobj=buffer, mode="w:gz") as archive:
_add_file(archive, "package.json", PACKAGE_JSON.encode("utf-8"))
_add_file(archive, "marker.txt", MARKER.encode("utf-8"))
return buffer.getvalue()
def write_registry_metadata(out_dir: pathlib.Path, tarball: bytes) -> None:
# npm requires a fully-qualified tarball URL, so this bakes in a
# placeholder that nginx's sub_filter rewrites to the real
# https://<host used by the client>/ at request time (see
# npm-repo.nginx.conf) - the host and port this fixture is actually
# reached on aren't known at image-build time.
sha1 = hashlib.sha1(tarball).hexdigest() # nosec B324
sha512_b64 = base64.b64encode(hashlib.sha512(tarball).digest()).decode("ascii")
metadata = {
"name": PACKAGE,
"dist-tags": {"latest": VERSION},
"versions": {
VERSION: {
"name": PACKAGE,
"version": VERSION,
"dist": {
"shasum": sha1,
"integrity": f"sha512-{sha512_b64}",
"tarball": f"__BASE_URL__/{TARBALL_NAME}",
},
}
},
}
(out_dir / PACKAGE).write_text(json.dumps(metadata, indent=2), encoding="utf-8")
def main() -> None:
OUT_DIR.mkdir(parents=True, exist_ok=True)
tarball = build_tarball()
(OUT_DIR / TARBALL_NAME).write_bytes(tarball)
write_registry_metadata(OUT_DIR, tarball)
if __name__ == "__main__":
main()
@@ -0,0 +1,17 @@
# Minimal cargo sparse registry (index entry, config.json and one hand-built
# crate) served over TLS, using the same Test CA as the other *-repo
# fixtures, used by tox-docker to exercise the `rust` module integration
# test (tests/configs/Rust.yaml.j2, tests/dockerfiles/Rust.Dockerfile).
#
# Built by tox-docker, whose build context is this directory (not the repo
# root) - every COPY source below must live under tests/dockerfiles/repos/.
FROM python:3.13-slim AS packagebuild
COPY build_test_cargo_package.py /build_test_cargo_package.py
RUN python3 /build_test_cargo_package.py
FROM nginx:alpine
COPY --from=packagebuild /srv /srv
COPY repo-server.crt.pem /etc/nginx/certs/repo-server.crt.pem
COPY repo-server.key.pem /etc/nginx/certs/repo-server.key.pem
COPY cargo-repo.nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 8443
@@ -0,0 +1,22 @@
server {
listen 8443 ssl default_server;
server_name _;
ssl_certificate /etc/nginx/certs/repo-server.crt.pem;
ssl_certificate_key /etc/nginx/certs/repo-server.key.pem;
root /srv;
autoindex off;
# config.json bakes in a __BASE_URL__ placeholder for its "dl"/"api"
# templates, since the host:port this fixture is actually reached on is
# only known once tox-docker starts the container, not at image-build
# time. $http_host reflects exactly what the client (cargo) used to
# connect, e.g. host.docker.internal:55123.
location = /config.json {
default_type application/json;
sub_filter '__BASE_URL__' 'https://$http_host';
sub_filter_once off;
sub_filter_types application/json;
}
}
@@ -0,0 +1,32 @@
# Minimal HTTPS server, using the same Test CA as the other *-repo fixtures,
# used by tox-docker to exercise the `certificates` module integration test
# (tests/configs/Certificate.yaml.j2, tests/dockerfiles/Certificate.Dockerfile).
# Unlike the package-manager fixtures, this one has nothing to do with a
# specific package format: it serves a static marker file (for wget, curl and
# openssl s_client) and a bare git repository over the "dumb" HTTP protocol
# (for `git clone`).
#
# Built by tox-docker, whose build context is this directory (not the repo
# root) - every COPY source below must live under tests/dockerfiles/repos/.
FROM alpine:3.20 AS gitbuild
RUN apk add --no-cache git
RUN git config --global init.defaultBranch main \
&& git config --global user.email test@example.invalid \
&& git config --global user.name "id-configurator tests"
RUN mkdir -p /work \
&& cd /work \
&& git init -q \
&& echo "trusted-via-root-ca" > marker.txt \
&& git add marker.txt \
&& git commit -q -m "initial commit"
RUN git clone --bare -q /work /srv/repo.git \
&& cd /srv/repo.git \
&& git update-server-info
FROM nginx:alpine
RUN mkdir -p /srv && echo "trusted-via-root-ca" > /srv/marker.txt
COPY --from=gitbuild /srv/repo.git /srv/repo.git
COPY repo-server.crt.pem /etc/nginx/certs/repo-server.crt.pem
COPY repo-server.key.pem /etc/nginx/certs/repo-server.key.pem
COPY certificate-repo.nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 8443
@@ -0,0 +1,10 @@
server {
listen 8443 ssl default_server;
server_name _;
ssl_certificate /etc/nginx/certs/repo-server.crt.pem;
ssl_certificate_key /etc/nginx/certs/repo-server.key.pem;
root /srv;
autoindex off;
}
+6 -3
View File
@@ -1,6 +1,7 @@
# Minimal Maven-compatible repository serving one hand-built jar over plain # Minimal Maven-compatible repository serving one hand-built jar over TLS,
# HTTP, used by tox-docker to exercise the `java` module integration test # using the same Test CA as the pypi-repo/apt-repo/yum-repo/apk-repo
# (tests/configs/Java.yaml.tmpl, tests/dockerfiles/Java.Dockerfile). # fixtures, used by tox-docker to exercise the `java` module integration
# test (tests/configs/Java.yaml.j2, tests/dockerfiles/Java.Dockerfile).
# #
# Built by tox-docker, whose build context is this directory (not the repo # Built by tox-docker, whose build context is this directory (not the repo
# root) - every COPY source below must live under tests/dockerfiles/repos/. # root) - every COPY source below must live under tests/dockerfiles/repos/.
@@ -10,5 +11,7 @@ RUN python3 /build_test_jar.py
FROM nginx:alpine FROM nginx:alpine
COPY --from=jarbuild /srv /srv COPY --from=jarbuild /srv /srv
COPY repo-server.crt.pem /etc/nginx/certs/repo-server.crt.pem
COPY repo-server.key.pem /etc/nginx/certs/repo-server.key.pem
COPY java-repo.nginx.conf /etc/nginx/conf.d/default.conf COPY java-repo.nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 8443 EXPOSE 8443
+4 -1
View File
@@ -1,7 +1,10 @@
server { server {
listen 8443 default_server; listen 8443 ssl default_server;
server_name _; server_name _;
ssl_certificate /etc/nginx/certs/repo-server.crt.pem;
ssl_certificate_key /etc/nginx/certs/repo-server.key.pem;
root /srv; root /srv;
autoindex off; autoindex off;
} }
@@ -0,0 +1,17 @@
# Minimal npm registry (one package's metadata document plus its tarball)
# served over TLS, using the same Test CA as the other *-repo fixtures, used
# by tox-docker to exercise the `npm` module integration test
# (tests/configs/Npm.yaml.j2, tests/dockerfiles/Npm.Dockerfile).
#
# Built by tox-docker, whose build context is this directory (not the repo
# root) - every COPY source below must live under tests/dockerfiles/repos/.
FROM python:3.13-slim AS packagebuild
COPY build_test_npm_package.py /build_test_npm_package.py
RUN python3 /build_test_npm_package.py
FROM nginx:alpine
COPY --from=packagebuild /srv /srv
COPY repo-server.crt.pem /etc/nginx/certs/repo-server.crt.pem
COPY repo-server.key.pem /etc/nginx/certs/repo-server.key.pem
COPY npm-repo.nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 8443
@@ -0,0 +1,22 @@
server {
listen 8443 ssl default_server;
server_name _;
ssl_certificate /etc/nginx/certs/repo-server.crt.pem;
ssl_certificate_key /etc/nginx/certs/repo-server.key.pem;
root /srv;
autoindex off;
# The package metadata document bakes in a __BASE_URL__ placeholder for
# its tarball URL, since the host:port this fixture is actually reached
# on is only known once tox-docker starts the container, not at
# image-build time. $http_host reflects exactly what the client (npm)
# used to connect, e.g. host.docker.internal:55123.
location = /idconfiguratortestpkg {
default_type application/json;
sub_filter '__BASE_URL__' 'https://$http_host';
sub_filter_once off;
sub_filter_types application/json;
}
}
@@ -0,0 +1,17 @@
# Minimal Composer repository ("composer" type: a static packages.json plus
# one hand-built zip package) served over TLS, using the same Test CA as the
# other *-repo fixtures, used by tox-docker to exercise the `php` module
# integration test (tests/configs/Php.yaml.j2, tests/dockerfiles/Php.Dockerfile).
#
# Built by tox-docker, whose build context is this directory (not the repo
# root) - every COPY source below must live under tests/dockerfiles/repos/.
FROM python:3.13-slim AS packagebuild
COPY build_test_composer_package.py /build_test_composer_package.py
RUN python3 /build_test_composer_package.py
FROM nginx:alpine
COPY --from=packagebuild /srv /srv
COPY repo-server.crt.pem /etc/nginx/certs/repo-server.crt.pem
COPY repo-server.key.pem /etc/nginx/certs/repo-server.key.pem
COPY php-repo.nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 8443
@@ -0,0 +1,22 @@
server {
listen 8443 ssl default_server;
server_name _;
ssl_certificate /etc/nginx/certs/repo-server.crt.pem;
ssl_certificate_key /etc/nginx/certs/repo-server.key.pem;
root /srv;
autoindex off;
# packages.json bakes in a __BASE_URL__ placeholder for its dist URL,
# since the host:port this fixture is actually reached on is only known
# once tox-docker starts the container, not at image-build time.
# $http_host reflects exactly what the client (Composer) used to
# connect, e.g. host.docker.internal:55123.
location = /packages.json {
default_type application/json;
sub_filter '__BASE_URL__' 'https://$http_host';
sub_filter_once off;
sub_filter_types application/json;
}
}
+6 -4
View File
@@ -1,7 +1,7 @@
# Minimal YUM/DNF repository serving one hand-built RPM package over plain # Minimal YUM/DNF repository serving one hand-built RPM package over TLS,
# HTTP (gpgcheck is off in the test config, so no TLS/signing is needed), # using the same Test CA as the pypi-repo/apt-repo fixtures, used by
# used by tox-docker to exercise the `yum` module integration test # tox-docker to exercise the `yum` module integration test
# (tests/configs/Yum.yaml.tmpl, tests/dockerfiles/Yum.Dockerfile). # (tests/configs/Yum.yaml.j2, tests/dockerfiles/Yum.Dockerfile).
# #
# Built by tox-docker, whose build context is this directory (not the repo # Built by tox-docker, whose build context is this directory (not the repo
# root) - every COPY source below must live under tests/dockerfiles/repos/. # root) - every COPY source below must live under tests/dockerfiles/repos/.
@@ -16,5 +16,7 @@ RUN createrepo_c /srv
FROM nginx:alpine FROM nginx:alpine
COPY --from=packagebuild /srv /srv COPY --from=packagebuild /srv /srv
COPY repo-server.crt.pem /etc/nginx/certs/repo-server.crt.pem
COPY repo-server.key.pem /etc/nginx/certs/repo-server.key.pem
COPY yum-repo.nginx.conf /etc/nginx/conf.d/default.conf COPY yum-repo.nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 8443 EXPOSE 8443
+4 -1
View File
@@ -1,7 +1,10 @@
server { server {
listen 8443 default_server; listen 8443 ssl default_server;
server_name _; server_name _;
ssl_certificate /etc/nginx/certs/repo-server.crt.pem;
ssl_certificate_key /etc/nginx/certs/repo-server.key.pem;
root /srv; root /srv;
autoindex off; autoindex off;
} }
+180 -59
View File
@@ -1,11 +1,10 @@
import subprocess
from unittest.mock import MagicMock
import pytest
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.certificates import CertificatesModule from id_configurator.modules.certificates import (
CertificatesModule,
_pem_name,
_resolve_target_dir,
)
CERT_CONTENT = "fake-cert-bytes" CERT_CONTENT = "fake-cert-bytes"
@@ -32,38 +31,30 @@ def test_is_relevant_false_when_no_certificates(tmp_path) -> None:
assert CertificatesModule().is_relevant(context) is False assert CertificatesModule().is_relevant(context) is False
def _mock_success(monkeypatch, stderr: str = "") -> MagicMock: def test_apply_writes_certificate_and_rebuilds_bundle(tmp_path) -> None:
mock_run = MagicMock(
return_value=subprocess.CompletedProcess(
args=[], returncode=0, stdout="", stderr=stderr
)
)
monkeypatch.setattr("id_configurator.modules.certificates.subprocess.run", mock_run)
return mock_run
def test_apply_writes_certificate_and_calls_update_ca_certificates(
tmp_path, monkeypatch
) -> None:
mock_run = _mock_success(monkeypatch)
dst = tmp_path / "dst" dst = tmp_path / "dst"
context = Context(config=make_config(True), dst=dst, src=None) context = Context(config=make_config(True), dst=dst, src=None)
CertificatesModule().apply(context) CertificatesModule().apply(context)
written = dst / "usr/local/share/ca-certificates/new-root-ca.crt" local_cert = dst / "usr/local/share/ca-certificates/new-root-ca.crt"
assert written.read_bytes() == b"fake-cert-bytes" assert local_cert.read_bytes() == b"fake-cert-bytes"
mock_run.assert_called_once()
args = mock_run.call_args.args[0] bundle = dst / "etc/ssl/certs/ca-certificates.crt"
assert args[0] == "update-ca-certificates" assert bundle.read_bytes() == b"fake-cert-bytes\n"
assert "--certsconf" not in args
assert "--certsdir" not in args pem = dst / "etc/ssl/certs/new-root-ca.pem"
assert pem.is_symlink()
assert (
pem.readlink().as_posix() == "/usr/local/share/ca-certificates/new-root-ca.crt"
)
openssl_default = dst / "usr/lib/ssl/cert.pem"
assert openssl_default.is_symlink()
assert openssl_default.readlink().as_posix() == "/etc/ssl/certs/ca-certificates.crt"
def test_apply_preserves_existing_local_certificates_from_src( def test_apply_merges_conf_listed_and_local_certificates(tmp_path) -> None:
tmp_path, monkeypatch
) -> None:
mock_run = _mock_success(monkeypatch, stderr="some warning")
src = tmp_path / "src" src = tmp_path / "src"
local_certs_dir = src / "usr/local/share/ca-certificates" local_certs_dir = src / "usr/local/share/ca-certificates"
local_certs_dir.mkdir(parents=True) local_certs_dir.mkdir(parents=True)
@@ -71,9 +62,14 @@ def test_apply_preserves_existing_local_certificates_from_src(
(local_certs_dir / "new-root-ca.crt").write_bytes( (local_certs_dir / "new-root-ca.crt").write_bytes(
b"stale-bytes-should-be-overwritten" b"stale-bytes-should-be-overwritten"
) )
(src / "etc/ca-certificates.conf").parent.mkdir(parents=True) system_certs_dir = src / "usr/share/ca-certificates"
(src / "etc/ca-certificates.conf").write_text("existing.crt\n", encoding="utf-8") system_certs_dir.mkdir(parents=True)
(src / "usr/share/ca-certificates").mkdir(parents=True) # No trailing newline, to exercise the newline-normalization on append.
(system_certs_dir / "system-ca.crt").write_bytes(b"system-ca-bytes")
(src / "etc/ca-certificates.conf").parent.mkdir(parents=True, exist_ok=True)
(src / "etc/ca-certificates.conf").write_text(
"# a comment\n\n!disabled-ca.crt\nsystem-ca.crt\n", encoding="utf-8"
)
dst = tmp_path / "dst" dst = tmp_path / "dst"
context = Context(config=make_config(True), dst=dst, src=src) context = Context(config=make_config(True), dst=dst, src=src)
@@ -84,35 +80,160 @@ def test_apply_preserves_existing_local_certificates_from_src(
assert (local_dst / "existing.crt").read_bytes() == b"existing-bytes" assert (local_dst / "existing.crt").read_bytes() == b"existing-bytes"
assert (local_dst / "new-root-ca.crt").read_bytes() == b"fake-cert-bytes" assert (local_dst / "new-root-ca.crt").read_bytes() == b"fake-cert-bytes"
args = mock_run.call_args.args[0] bundle = dst / "etc/ssl/certs/ca-certificates.crt"
assert "--certsconf" in args # ca-certificates.conf entries first, then local certificates sorted by name.
assert str(src / "etc/ca-certificates.conf") in args assert bundle.read_bytes() == (
assert "--certsdir" in args b"system-ca-bytes\nexisting-bytes\nfake-cert-bytes\n"
assert str(src / "usr/share/ca-certificates") in args )
etc_certs = dst / "etc/ssl/certs"
assert (
etc_certs / "system-ca.pem"
).readlink().as_posix() == "/usr/share/ca-certificates/system-ca.crt"
assert (
etc_certs / "existing.pem"
).readlink().as_posix() == "/usr/local/share/ca-certificates/existing.crt"
assert (
etc_certs / "new-root-ca.pem"
).readlink().as_posix() == "/usr/local/share/ca-certificates/new-root-ca.crt"
# The disabled entry must not produce a symlink or appear in the bundle.
assert not (etc_certs / "disabled-ca.pem").exists()
def test_update_ca_certificates_raises_when_binary_missing( def test_apply_warns_and_skips_missing_conf_entry(tmp_path, caplog) -> None:
tmp_path, monkeypatch src = tmp_path / "src"
(src / "usr/share/ca-certificates").mkdir(parents=True)
(src / "etc/ca-certificates.conf").parent.mkdir(parents=True, exist_ok=True)
(src / "etc/ca-certificates.conf").write_text("missing.crt\n", encoding="utf-8")
dst = tmp_path / "dst"
context = Context(config=make_config(True), dst=dst, src=src)
with caplog.at_level("WARNING"):
CertificatesModule().apply(context)
assert "missing.crt" in caplog.text
bundle = dst / "etc/ssl/certs/ca-certificates.crt"
assert bundle.read_bytes() == b"fake-cert-bytes\n"
def test_pem_name_sanitizes_special_characters() -> None:
assert _pem_name("usr/share/ca-certificates/New Root, CA.crt") == "New_Root__CA.pem"
assert _pem_name("Vendor (Test).crt") == "Vendor_=Test=.pem"
def test_apply_skips_rhel_trust_store_when_absent(tmp_path) -> None:
dst = tmp_path / "dst"
context = Context(config=make_config(True), dst=dst, src=None)
CertificatesModule().apply(context)
assert not (dst / "etc/pki").exists()
def test_apply_updates_rhel_trust_store_when_present(tmp_path) -> None:
src = tmp_path / "src"
anchors_dir = src / "etc/pki/ca-trust/source/anchors"
anchors_dir.mkdir(parents=True)
(anchors_dir / "existing-anchor.pem").write_bytes(b"existing-anchor-bytes")
(anchors_dir / "new-root-ca.crt").write_bytes(b"stale-should-be-overwritten")
extracted_bundle = src / "etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"
extracted_bundle.parent.mkdir(parents=True)
# No trailing newline, to exercise the newline-normalization on append.
extracted_bundle.write_bytes(b"vendor-bundle-bytes")
dst = tmp_path / "dst"
context = Context(config=make_config(True), dst=dst, src=src)
CertificatesModule().apply(context)
anchors_dst = dst / "etc/pki/ca-trust/source/anchors"
assert (
anchors_dst / "existing-anchor.pem"
).read_bytes() == b"existing-anchor-bytes"
assert (anchors_dst / "new-root-ca.crt").read_bytes() == b"fake-cert-bytes"
bundle_dst = dst / "etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"
assert bundle_dst.read_bytes() == (
b"vendor-bundle-bytes\nexisting-anchor-bytes\nfake-cert-bytes\n"
)
def test_apply_rebuilds_rhel_trust_store_without_preexisting_bundle(tmp_path) -> None:
src = tmp_path / "src"
(src / "etc/pki/ca-trust/source/anchors").mkdir(parents=True)
dst = tmp_path / "dst"
context = Context(config=make_config(True), dst=dst, src=src)
CertificatesModule().apply(context)
bundle_dst = dst / "etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"
assert bundle_dst.read_bytes() == b"fake-cert-bytes\n"
def test_resolve_target_dir_returns_unchanged_when_not_a_symlink(tmp_path) -> None:
src = tmp_path / "src"
(src / "etc/ssl/certs").mkdir(parents=True)
context = Context(config=make_config(True), dst=tmp_path / "dst", src=src)
assert _resolve_target_dir(context, "etc/ssl/certs") == "etc/ssl/certs"
def test_resolve_target_dir_returns_unchanged_without_src(tmp_path) -> None:
context = Context(config=make_config(True), dst=tmp_path / "dst", src=None)
assert _resolve_target_dir(context, "etc/ssl/certs") == "etc/ssl/certs"
def test_resolve_target_dir_follows_absolute_symlink(tmp_path) -> None:
src = tmp_path / "src"
(src / "etc/pki/tls/certs").mkdir(parents=True)
(src / "etc/ssl").mkdir(parents=True)
(src / "etc/ssl/certs").symlink_to("/etc/pki/tls/certs")
context = Context(config=make_config(True), dst=tmp_path / "dst", src=src)
assert _resolve_target_dir(context, "etc/ssl/certs") == "etc/pki/tls/certs"
def test_resolve_target_dir_follows_relative_symlink(tmp_path) -> None:
src = tmp_path / "src"
(src / "etc/pki/tls/certs").mkdir(parents=True)
(src / "etc/ssl").mkdir(parents=True)
(src / "etc/ssl/certs").symlink_to("../pki/tls/certs")
context = Context(config=make_config(True), dst=tmp_path / "dst", src=src)
assert _resolve_target_dir(context, "etc/ssl/certs") == "etc/pki/tls/certs"
def test_resolve_target_dir_falls_back_when_relative_symlink_escapes_src(
tmp_path,
) -> None: ) -> None:
monkeypatch.setattr( src = tmp_path / "src"
"id_configurator.modules.certificates.subprocess.run", (src / "etc/ssl").mkdir(parents=True)
MagicMock(side_effect=FileNotFoundError()), # Escapes past <src> itself via enough ".." components.
) (src / "etc/ssl/certs").symlink_to("../../../../../outside")
context = Context(config=make_config(True), dst=tmp_path / "dst", src=None) context = Context(config=make_config(True), dst=tmp_path / "dst", src=src)
with pytest.raises(RuntimeError, match="update-ca-certificates is not available"): assert _resolve_target_dir(context, "etc/ssl/certs") == "etc/ssl/certs"
CertificatesModule().apply(context)
def test_update_ca_certificates_raises_on_failure(tmp_path, monkeypatch) -> None: def test_apply_writes_bundle_to_rhel_style_symlinked_etccertsdir(tmp_path) -> None:
error = subprocess.CalledProcessError( src = tmp_path / "src"
returncode=1, cmd=["update-ca-certificates"], stderr="boom" (src / "etc/pki/tls/certs").mkdir(parents=True)
) (src / "etc/ssl").mkdir(parents=True)
monkeypatch.setattr( (src / "etc/ssl/certs").symlink_to("/etc/pki/tls/certs")
"id_configurator.modules.certificates.subprocess.run",
MagicMock(side_effect=error),
)
context = Context(config=make_config(True), dst=tmp_path / "dst", src=None)
with pytest.raises(RuntimeError, match="update-ca-certificates failed"): dst = tmp_path / "dst"
CertificatesModule().apply(context) context = Context(config=make_config(True), dst=dst, src=src)
CertificatesModule().apply(context)
assert not (dst / "etc/ssl/certs").exists()
bundle = dst / "etc/pki/tls/certs/ca-certificates.crt"
assert bundle.read_bytes() == b"fake-cert-bytes\n"
pem = dst / "etc/pki/tls/certs/new-root-ca.pem"
assert pem.is_symlink()
# /usr/lib/ssl/cert.pem is Debian/Ubuntu-specific: not OpenSSL's default
# CAfile on this (RHEL-style) system, so it must not be created.
assert not (dst / "usr/lib/ssl/cert.pem").exists()
+5 -10
View File
@@ -1,5 +1,4 @@
import json import json
import subprocess
from unittest.mock import MagicMock from unittest.mock import MagicMock
from typer.testing import CliRunner from typer.testing import CliRunner
@@ -45,7 +44,10 @@ def test_list_modules() -> None:
"apt", "apt",
"certificates", "certificates",
"java", "java",
"npm",
"php",
"python", "python",
"rust",
"yum", "yum",
} }
@@ -66,14 +68,7 @@ def test_apply_skips_irrelevant_modules(tmp_path, caplog) -> None:
assert "skipping module python" in caplog.text assert "skipping module python" in caplog.text
def test_apply_runs_relevant_module(tmp_path, monkeypatch) -> None: def test_apply_runs_relevant_module(tmp_path) -> None:
mock_run = MagicMock(
return_value=subprocess.CompletedProcess(
args=[], returncode=0, stdout="", stderr=""
)
)
monkeypatch.setattr("id_configurator.modules.certificates.subprocess.run", mock_run)
config_path = write_config(tmp_path, CONFIG_WITH_CERT) config_path = write_config(tmp_path, CONFIG_WITH_CERT)
dst = tmp_path / "dst" dst = tmp_path / "dst"
result = runner.invoke( result = runner.invoke(
@@ -82,7 +77,7 @@ def test_apply_runs_relevant_module(tmp_path, monkeypatch) -> None:
assert result.exit_code == 0 assert result.exit_code == 0
assert (dst / "usr/local/share/ca-certificates/my-ca.crt").read_bytes() == b"fake" assert (dst / "usr/local/share/ca-certificates/my-ca.crt").read_bytes() == b"fake"
mock_run.assert_called_once() assert (dst / "etc/ssl/certs/ca-certificates.crt").read_bytes() == b"fake\n"
def test_main_default_name_calls_app(monkeypatch) -> None: def test_main_default_name_calls_app(monkeypatch) -> None:
+4 -1
View File
@@ -122,8 +122,11 @@ def test_apply_adds_certificate_to_pkcs12_and_jks_keystores(tmp_path) -> None:
JavaModule().apply(context) JavaModule().apply(context)
# The written truststore's trust-only entries are unencrypted (matching
# the JDK's own cacerts), so they load with no password at all - the
# password a caller would use for a source-of-truth JDK-style store.
pkcs12_result = pkcs12.load_pkcs12( pkcs12_result = pkcs12.load_pkcs12(
(dst / "usr/lib/jvm/java-21/lib/security/cacerts").read_bytes(), b"changeit" (dst / "usr/lib/jvm/java-21/lib/security/cacerts").read_bytes(), None
) )
pkcs12_aliases = {entry.friendly_name for entry in pkcs12_result.additional_certs} pkcs12_aliases = {entry.friendly_name for entry in pkcs12_result.additional_certs}
assert pkcs12_aliases == {b"already-trusted-ca", b"interdiode-root-ca"} assert pkcs12_aliases == {b"already-trusted-ca", b"interdiode-root-ca"}
+71
View File
@@ -0,0 +1,71 @@
from id_configurator.config import Configuration
from id_configurator.context import Context
from id_configurator.modules.npm import NpmModule
CONFIG_WITH_REPO = {
"uuid": "12345678-1234-5678-1234-567812345678",
"organisation": "InterDiode",
"configurations": {
"npm": {"repositories": [{"name": "demo", "url": "https://example.invalid/"}]}
},
}
CONFIG_WITH_TWO_REPOS = {
"uuid": "12345678-1234-5678-1234-567812345678",
"organisation": "InterDiode",
"configurations": {
"npm": {
"repositories": [
{"name": "demo", "url": "https://example.invalid/"},
{"name": "myorg", "url": "https://other.invalid/"},
]
}
},
}
EMPTY_CONFIG = {
"uuid": "12345678-1234-5678-1234-567812345678",
"organisation": "InterDiode",
}
def make_config(data: dict) -> Configuration:
return Configuration.model_validate(data)
def test_is_relevant_false_when_no_repositories(tmp_path) -> None:
context = Context(config=make_config(EMPTY_CONFIG), dst=tmp_path / "dst", src=None)
assert NpmModule().is_relevant(context) is False
def test_is_relevant_true_with_repository(tmp_path) -> None:
context = Context(
config=make_config(CONFIG_WITH_REPO), dst=tmp_path / "dst", src=None
)
assert NpmModule().is_relevant(context) is True
def test_apply_writes_default_registry_and_cafile(tmp_path) -> None:
dst = tmp_path / "dst"
context = Context(config=make_config(CONFIG_WITH_REPO), dst=dst, src=None)
NpmModule().apply(context)
npmrc = (dst / "usr/local/etc/npmrc").read_text(encoding="utf-8")
assert npmrc == (
"cafile=/etc/ssl/certs/ca-certificates.crt\nregistry=https://example.invalid/\n"
)
def test_apply_writes_additional_repositories_scoped(tmp_path) -> None:
dst = tmp_path / "dst"
context = Context(config=make_config(CONFIG_WITH_TWO_REPOS), dst=dst, src=None)
NpmModule().apply(context)
npmrc = (dst / "usr/local/etc/npmrc").read_text(encoding="utf-8")
assert npmrc == (
"cafile=/etc/ssl/certs/ca-certificates.crt\n"
"registry=https://example.invalid/\n"
"@myorg:registry=https://other.invalid/\n"
)
+81
View File
@@ -0,0 +1,81 @@
import json
from id_configurator.config import Configuration
from id_configurator.context import Context
from id_configurator.modules.php import PhpModule
CONFIG_WITH_REPO = {
"uuid": "12345678-1234-5678-1234-567812345678",
"organisation": "InterDiode",
"configurations": {
"php": {
"repositories": [
{"name": "demo", "url": "https://example.invalid/composer"}
]
}
},
}
CONFIG_WITH_TWO_REPOS = {
"uuid": "12345678-1234-5678-1234-567812345678",
"organisation": "InterDiode",
"configurations": {
"php": {
"repositories": [
{"name": "demo", "url": "https://example.invalid/composer"},
{"name": "other", "url": "https://other.invalid/composer"},
]
}
},
}
EMPTY_CONFIG = {
"uuid": "12345678-1234-5678-1234-567812345678",
"organisation": "InterDiode",
}
def make_config(data: dict) -> Configuration:
return Configuration.model_validate(data)
def test_is_relevant_false_when_no_repositories(tmp_path) -> None:
context = Context(config=make_config(EMPTY_CONFIG), dst=tmp_path / "dst", src=None)
assert PhpModule().is_relevant(context) is False
def test_is_relevant_true_with_repository(tmp_path) -> None:
context = Context(
config=make_config(CONFIG_WITH_REPO), dst=tmp_path / "dst", src=None
)
assert PhpModule().is_relevant(context) is True
def test_apply_writes_global_composer_config(tmp_path) -> None:
dst = tmp_path / "dst"
context = Context(config=make_config(CONFIG_WITH_REPO), dst=dst, src=None)
PhpModule().apply(context)
config_path = dst / "root/.composer/config.json"
content = json.loads(config_path.read_text(encoding="utf-8"))
assert content == {
"repositories": [
{
"name": "demo",
"type": "composer",
"url": "https://example.invalid/composer",
}
]
}
def test_apply_writes_multiple_repositories_in_order(tmp_path) -> None:
dst = tmp_path / "dst"
context = Context(config=make_config(CONFIG_WITH_TWO_REPOS), dst=dst, src=None)
PhpModule().apply(context)
config_path = dst / "root/.composer/config.json"
content = json.loads(config_path.read_text(encoding="utf-8"))
assert [repo["name"] for repo in content["repositories"]] == ["demo", "other"]
+80
View File
@@ -0,0 +1,80 @@
from id_configurator.config import Configuration
from id_configurator.context import Context
from id_configurator.modules.rust import RustModule
CONFIG_WITH_REPO = {
"uuid": "12345678-1234-5678-1234-567812345678",
"organisation": "InterDiode",
"configurations": {
"rust": {"repositories": [{"name": "demo", "url": "https://example.invalid/"}]}
},
}
CONFIG_WITH_TWO_REPOS = {
"uuid": "12345678-1234-5678-1234-567812345678",
"organisation": "InterDiode",
"configurations": {
"rust": {
"repositories": [
{"name": "demo", "url": "https://example.invalid/"},
{"name": "other", "url": "https://other.invalid/"},
]
}
},
}
EMPTY_CONFIG = {
"uuid": "12345678-1234-5678-1234-567812345678",
"organisation": "InterDiode",
}
def make_config(data: dict) -> Configuration:
return Configuration.model_validate(data)
def test_is_relevant_false_when_no_repositories(tmp_path) -> None:
context = Context(config=make_config(EMPTY_CONFIG), dst=tmp_path / "dst", src=None)
assert RustModule().is_relevant(context) is False
def test_is_relevant_true_with_repository(tmp_path) -> None:
context = Context(
config=make_config(CONFIG_WITH_REPO), dst=tmp_path / "dst", src=None
)
assert RustModule().is_relevant(context) is True
def test_apply_writes_replacement_source(tmp_path) -> None:
dst = tmp_path / "dst"
context = Context(config=make_config(CONFIG_WITH_REPO), dst=dst, src=None)
RustModule().apply(context)
config_toml = (dst / ".cargo/config.toml").read_text(encoding="utf-8")
assert config_toml == (
"[source.crates-io]\n"
'replace-with = "demo"\n'
"\n"
"[source.demo]\n"
'registry = "sparse+https://example.invalid/"\n'
)
def test_apply_writes_additional_repositories_as_named_registries(tmp_path) -> None:
dst = tmp_path / "dst"
context = Context(config=make_config(CONFIG_WITH_TWO_REPOS), dst=dst, src=None)
RustModule().apply(context)
config_toml = (dst / ".cargo/config.toml").read_text(encoding="utf-8")
assert config_toml == (
"[source.crates-io]\n"
'replace-with = "demo"\n'
"\n"
"[source.demo]\n"
'registry = "sparse+https://example.invalid/"\n'
"\n"
"[registries.other]\n"
'index = "sparse+https://other.invalid/"\n'
)
+10
View File
@@ -178,6 +178,16 @@ def test_import_key_raises_when_gpg_binary_missing(tmp_path, monkeypatch) -> Non
YumModule().apply(context) YumModule().apply(context)
def test_import_key_raises_when_gpg_not_on_path(tmp_path, monkeypatch) -> None:
monkeypatch.setattr("id_configurator.modules.yum.shutil.which", lambda _: None)
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"):
YumModule().apply(context)
def test_import_key_raises_on_invalid_key(tmp_path, monkeypatch) -> None: def test_import_key_raises_on_invalid_key(tmp_path, monkeypatch) -> None:
mock_run = MagicMock( mock_run = MagicMock(
return_value=subprocess.CompletedProcess( return_value=subprocess.CompletedProcess(
+139 -9
View File
@@ -1,5 +1,5 @@
[tox] [tox]
env_list = py313,py314,coverage,packaging,publish,module-python,module-apt,module-yum,module-apk,module-java env_list = py313,py314,coverage,packaging,packaging-amd64,packaging-arm64,publish,module-python,module-apt,module-yum,module-apk,module-java,module-certificates,module-php,module-npm,module-rust
isolated_build = true isolated_build = true
requires = requires =
tox>=4 tox>=4
@@ -12,7 +12,7 @@ change_dir = {env_tmp_dir}
set_env = set_env =
COVERAGE_FILE = {env_dir}/coverage.db COVERAGE_FILE = {env_dir}/coverage.db
commands = commands =
pytest {tox_root}/tests --cov-config={tox_root}/pyproject.toml --cov-fail-under=0 {posargs} python3 -m pytest {tox_root}/tests --cov-config={tox_root}/pyproject.toml --cov-fail-under=90 {posargs}
[testenv:coverage] [testenv:coverage]
package = skip package = skip
@@ -22,8 +22,8 @@ dependency_groups = test
set_env = set_env =
COVERAGE_FILE = {env_dir}/coverage.db COVERAGE_FILE = {env_dir}/coverage.db
commands = commands =
python3 -m coverage combine python3 -m coverage combine {toxworkdir}/py313/coverage.db {toxworkdir}/py314/coverage.db
python3 -m coverage xml -o .tox/coverage.xml --ignore python3 -m coverage xml -o .tox/coverage.xml --ignore --fail-under=0
python3 -m coverage report -m --fail-under=94 --ignore --skip-covered python3 -m coverage report -m --fail-under=94 --ignore --skip-covered
[testenv:packaging] [testenv:packaging]
@@ -37,7 +37,33 @@ description =
depends = depends =
update update
commands = commands =
python3 -m id_tools build docker --platform linux/amd64,linux/arm64/v8 python3 -m id_tools build docker --platform linux/amd64,linux/arm64
[testenv:packaging-amd64]
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
[testenv:packaging-arm64]
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/arm64
[testenv:publish] [testenv:publish]
package = skip package = skip
@@ -78,7 +104,10 @@ allowlist_externals =
docker docker
commands = commands =
python3 -m id_tools test render-config {tox_root}/tests/configs/Python.yaml.j2 {env_dir}/configs/Python.yaml python3 -m id_tools test render-config {tox_root}/tests/configs/Python.yaml.j2 {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 . docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-arg PYPI_REPO_PORT={env:PYPI_REPO_PORT} --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Python-alpine.Dockerfile -t id-configurator-python .
docker rmi id-configurator-python
docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-arg PYPI_REPO_PORT={env:PYPI_REPO_PORT} --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Python.Dockerfile -t id-configurator-python .
docker rmi id-configurator-python
[docker:apt-repo] [docker:apt-repo]
dockerfile = {tox_root}/tests/dockerfiles/repos/apt-repo.Dockerfile dockerfile = {tox_root}/tests/dockerfiles/repos/apt-repo.Dockerfile
@@ -101,12 +130,13 @@ allowlist_externals =
commands = commands =
python3 -m id_tools test render-config {tox_root}/tests/configs/Apt.yaml.j2 {env_dir}/configs/Apt.yaml python3 -m id_tools test render-config {tox_root}/tests/configs/Apt.yaml.j2 {env_dir}/configs/Apt.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/Apt.Dockerfile -t id-configurator-apt . 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/Apt.Dockerfile -t id-configurator-apt .
docker rmi id-configurator-apt
[docker:yum-repo] [docker:yum-repo]
dockerfile = {tox_root}/tests/dockerfiles/repos/yum-repo.Dockerfile dockerfile = {tox_root}/tests/dockerfiles/repos/yum-repo.Dockerfile
expose = expose =
YUM_REPO_PORT=8443/tcp YUM_REPO_PORT=8443/tcp
healthcheck_cmd = wget -qO- http://127.0.0.1:8443/repodata/repomd.xml || exit 1 healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/repodata/repomd.xml || exit 1
healthcheck_interval = 1 healthcheck_interval = 1
healthcheck_retries = 30 healthcheck_retries = 30
healthcheck_timeout = 2 healthcheck_timeout = 2
@@ -123,12 +153,13 @@ allowlist_externals =
commands = commands =
python3 -m id_tools test render-config {tox_root}/tests/configs/Yum.yaml.j2 {env_dir}/configs/Yum.yaml python3 -m id_tools test render-config {tox_root}/tests/configs/Yum.yaml.j2 {env_dir}/configs/Yum.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/Yum.Dockerfile -t id-configurator-yum . 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/Yum.Dockerfile -t id-configurator-yum .
docker rmi id-configurator-yum
[docker:apk-repo] [docker:apk-repo]
dockerfile = {tox_root}/tests/dockerfiles/repos/apk-repo.Dockerfile dockerfile = {tox_root}/tests/dockerfiles/repos/apk-repo.Dockerfile
expose = expose =
APK_REPO_PORT=8443/tcp APK_REPO_PORT=8443/tcp
healthcheck_cmd = wget -qO- http://127.0.0.1:8443/APKINDEX.tar.gz || exit 1 healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/APKINDEX.tar.gz || exit 1
healthcheck_interval = 1 healthcheck_interval = 1
healthcheck_retries = 30 healthcheck_retries = 30
healthcheck_timeout = 2 healthcheck_timeout = 2
@@ -154,12 +185,13 @@ description =
commands = commands =
python3 -m id_tools test render-config {tox_root}/tests/configs/Apk.yaml.j2 {env_dir}/configs/Apk.yaml python3 -m id_tools test render-config {tox_root}/tests/configs/Apk.yaml.j2 {env_dir}/configs/Apk.yaml
docker buildx build --add-host=host.docker.internal:host-gateway --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Apk.Dockerfile -t id-configurator-apk . docker buildx build --add-host=host.docker.internal:host-gateway --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Apk.Dockerfile -t id-configurator-apk .
docker rmi id-configurator-apk
[docker:java-repo] [docker:java-repo]
dockerfile = {tox_root}/tests/dockerfiles/repos/java-repo.Dockerfile dockerfile = {tox_root}/tests/dockerfiles/repos/java-repo.Dockerfile
expose = expose =
JAVA_REPO_PORT=8443/tcp JAVA_REPO_PORT=8443/tcp
healthcheck_cmd = wget -qO- http://127.0.0.1:8443/org/interdiode/idconfiguratortestpkg/0.0.1/idconfiguratortestpkg-0.0.1.pom || exit 1 healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/org/interdiode/idconfiguratortestpkg/0.0.1/idconfiguratortestpkg-0.0.1.pom || exit 1
healthcheck_interval = 1 healthcheck_interval = 1
healthcheck_retries = 30 healthcheck_retries = 30
healthcheck_timeout = 2 healthcheck_timeout = 2
@@ -176,3 +208,101 @@ allowlist_externals =
commands = commands =
python3 -m id_tools test render-config {tox_root}/tests/configs/Java.yaml.j2 {env_dir}/configs/Java.yaml python3 -m id_tools test render-config {tox_root}/tests/configs/Java.yaml.j2 {env_dir}/configs/Java.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/Java.Dockerfile -t id-configurator-java . 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/Java.Dockerfile -t id-configurator-java .
docker rmi id-configurator-java
[docker:certificate-repo]
dockerfile = {tox_root}/tests/dockerfiles/repos/certificate-repo.Dockerfile
expose =
CERTIFICATE_REPO_PORT=8443/tcp
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/marker.txt || exit 1
healthcheck_interval = 1
healthcheck_retries = 30
healthcheck_timeout = 2
healthcheck_start_period = 1
[testenv:module-certificates]
package = skip
change_dir = {tox_root}
dependency_groups = dev
docker =
certificate-repo
allowlist_externals =
docker
description =
Verifies that both wget and curl - two independently TLS-linked HTTP
clients (GnuTLS and OpenSSL, respectively, on Debian) - trust an HTTPS
server whose certificate chains to a root certificate installed by the
`certificates` module alone, with no package-manager-specific config.
commands =
python3 -m id_tools test render-config {tox_root}/tests/configs/Certificate.yaml.j2 {env_dir}/configs/Certificate.yaml
docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-arg CERTIFICATE_REPO_PORT={env:CERTIFICATE_REPO_PORT} --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Certificate.Dockerfile -t id-configurator-certificates .
docker rmi id-configurator-certificates
[docker:php-repo]
dockerfile = {tox_root}/tests/dockerfiles/repos/php-repo.Dockerfile
expose =
PHP_REPO_PORT=8443/tcp
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/packages.json || exit 1
healthcheck_interval = 1
healthcheck_retries = 30
healthcheck_timeout = 2
healthcheck_start_period = 1
[testenv:module-php]
package = skip
change_dir = {tox_root}
dependency_groups = dev
docker =
php-repo
allowlist_externals =
docker
commands =
python3 -m id_tools test render-config {tox_root}/tests/configs/Php.yaml.j2 {env_dir}/configs/Php.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/Php.Dockerfile -t id-configurator-php .
docker rmi id-configurator-php
[docker:npm-repo]
dockerfile = {tox_root}/tests/dockerfiles/repos/npm-repo.Dockerfile
expose =
NPM_REPO_PORT=8443/tcp
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/idconfiguratortestpkg || exit 1
healthcheck_interval = 1
healthcheck_retries = 30
healthcheck_timeout = 2
healthcheck_start_period = 1
[testenv:module-npm]
package = skip
change_dir = {tox_root}
dependency_groups = dev
docker =
npm-repo
allowlist_externals =
docker
commands =
python3 -m id_tools test render-config {tox_root}/tests/configs/Npm.yaml.j2 {env_dir}/configs/Npm.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/Npm.Dockerfile -t id-configurator-npm .
docker rmi id-configurator-npm
[docker:cargo-repo]
dockerfile = {tox_root}/tests/dockerfiles/repos/cargo-repo.Dockerfile
expose =
CARGO_REPO_PORT=8443/tcp
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/config.json || exit 1
healthcheck_interval = 1
healthcheck_retries = 30
healthcheck_timeout = 2
healthcheck_start_period = 1
[testenv:module-rust]
package = skip
change_dir = {tox_root}
dependency_groups = dev
docker =
cargo-repo
allowlist_externals =
docker
commands =
python3 -m id_tools test render-config {tox_root}/tests/configs/Rust.yaml.j2 {env_dir}/configs/Rust.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/Rust.Dockerfile -t id-configurator-rust .
docker rmi id-configurator-rust