mirror of
https://github.com/d9pouces/id_configurator.git
synced 2026-11-09 01:47:21 +01:00
@@ -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
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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",
|
||||||
|
]
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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
@@ -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-----
|
||||||
|
|||||||
@@ -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: []
|
||||||
|
|||||||
@@ -24,3 +24,9 @@ configurations:
|
|||||||
repositories: []
|
repositories: []
|
||||||
java:
|
java:
|
||||||
repositories: []
|
repositories: []
|
||||||
|
php:
|
||||||
|
repositories: []
|
||||||
|
npm:
|
||||||
|
repositories: []
|
||||||
|
rust:
|
||||||
|
repositories: []
|
||||||
|
|||||||
@@ -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: []
|
||||||
@@ -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: []
|
||||||
|
|||||||
@@ -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: []
|
||||||
@@ -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: []
|
||||||
@@ -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: []
|
||||||
|
|||||||
@@ -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 }}/
|
||||||
@@ -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: []
|
||||||
|
|||||||
@@ -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 \
|
||||||
|
|||||||
@@ -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 \
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)"
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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"}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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"]
|
||||||
@@ -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'
|
||||||
|
)
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user