Compare commits

...

6 Commits

Author SHA1 Message Date
d9pouces ab2a179a55 feat: update config-url configuration with Test CA certificate and Dockerfile enhancements
Signed-off-by: d9pouces <github@19pouces.net>
2026-09-07 08:12:48 +02:00
d9pouces 9de98d4744 feat: add config-url support with Dockerfile and NGINX configuration
Signed-off-by: d9pouces <github@19pouces.net>
2026-09-07 07:15:58 +02:00
d9pouces 88380b6ab6 refactor: remove unused NPM_TO_COPY variable from build.py
Signed-off-by: d9pouces <github@19pouces.net>
2026-09-06 23:08:58 +02:00
d9pouces e3b2e8a95a refactor: remove unused NPM_TO_COPY variable from build.py
Signed-off-by: d9pouces <github@19pouces.net>
2026-09-06 22:22:51 +02:00
d9pouces 4e4b0592c0 refactor: remove unused NPM_TO_COPY variable from build.py
Signed-off-by: d9pouces <github@19pouces.net>
2026-09-06 21:36:01 +02:00
d9pouces a8143b61b1 feat: add support for Go, Ruby, and NuGet modules with integration testing configurations
Signed-off-by: d9pouces <github@19pouces.net>
2026-09-06 21:14:18 +02:00
48 changed files with 1905 additions and 22 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
python-version: 3.${{ matrix.python-version }} python-version: 3.${{ matrix.python-version }}
- run: python3 -m pip install --upgrade tox tox-docker-id tox-uv - run: python3 -m pip install --upgrade tox tox-docker-id tox-uv
- run: tox -e packaging-amd64,py313,py314,coverage - 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 - run: tox -m end-to-end
strategy: strategy:
matrix: matrix:
python-version: ['14'] python-version: ['14']
+39 -7
View File
@@ -38,6 +38,23 @@ ConfigEnvOption = Annotated[
help="Base64-encoded JSON/YAML configuration.", help="Base64-encoded JSON/YAML configuration.",
), ),
] ]
ConfigUrlOption = Annotated[
str | None,
typer.Option(
"--config-url",
help="URL to download a JSON or YAML configuration from.",
),
]
ConfigUrlCacertContentOption = Annotated[
str | None,
typer.Option(
"--config-url-cacert-content",
help=(
"PEM content of the CA certificate to trust when downloading "
"--config-url over HTTPS (defaults to the system's trust store)."
),
),
]
SrcOption = Annotated[ SrcOption = Annotated[
Path | None, Path | None,
typer.Option( typer.Option(
@@ -67,12 +84,18 @@ def _configure_logging(verbose: bool) -> None:
) )
def _load_configuration(config: Path | None, config_env: str | None): def _load_configuration(
if config is None and config_env is None: config: Path | None,
config_env: str | None,
config_url: str | None,
config_url_cacert_content: str | None,
):
if config is None and config_env is None and config_url is None:
raise typer.BadParameter( raise typer.BadParameter(
"either --config or --config-env (or $ID_CONFIGURATOR_CONFIG) must be provided" "either --config, --config-env (or $ID_CONFIGURATOR_CONFIG) or "
"--config-url must be provided"
) )
return load_configuration(config, config_env) return load_configuration(config, config_env, config_url, config_url_cacert_content)
@app.command() @app.command()
@@ -80,12 +103,16 @@ def apply(
dst: DstOption, dst: DstOption,
config: ConfigFileOption = None, config: ConfigFileOption = None,
config_env: ConfigEnvOption = None, config_env: ConfigEnvOption = None,
config_url: ConfigUrlOption = None,
config_url_cacert_content: ConfigUrlCacertContentOption = None,
src: SrcOption = None, src: SrcOption = None,
verbose: VerboseOption = False, verbose: VerboseOption = False,
) -> None: ) -> None:
"""Apply a configuration to the destination directory.""" """Apply a configuration to the destination directory."""
_configure_logging(verbose) _configure_logging(verbose)
configuration = _load_configuration(config, config_env) configuration = _load_configuration(
config, config_env, config_url, config_url_cacert_content
)
dst.mkdir(parents=True, exist_ok=True) dst.mkdir(parents=True, exist_ok=True)
context = Context(config=configuration, dst=dst, src=src) context = Context(config=configuration, dst=dst, src=src)
for module_cls in all_modules(): for module_cls in all_modules():
@@ -99,10 +126,15 @@ def apply(
@app.command() @app.command()
def validate( def validate(
config: ConfigFileOption = None, config_env: ConfigEnvOption = None config: ConfigFileOption = None,
config_env: ConfigEnvOption = None,
config_url: ConfigUrlOption = None,
config_url_cacert_content: ConfigUrlCacertContentOption = None,
) -> None: ) -> None:
"""Validate a configuration file without applying it.""" """Validate a configuration file without applying it."""
configuration = _load_configuration(config, config_env) configuration = _load_configuration(
config, config_env, config_url, config_url_cacert_content
)
typer.echo( typer.echo(
f"configuration {configuration.uuid} is valid (organisation: {configuration.organisation})" f"configuration {configuration.uuid} is valid (organisation: {configuration.organisation})"
) )
+69 -3
View File
@@ -4,12 +4,16 @@ from __future__ import annotations
import base64 import base64
import json import json
import ssl
import urllib.request
from pathlib import Path from pathlib import Path
from uuid import UUID from uuid import UUID
import yaml import yaml
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
_CONFIG_URL_TIMEOUT_SECONDS = 30
class NamedContent(BaseModel): class NamedContent(BaseModel):
"""A named piece of content, generally base64-encoded (a certificate, a GPG key, ...).""" """A named piece of content, generally base64-encoded (a certificate, a GPG key, ...)."""
@@ -188,6 +192,40 @@ class RustConfig(BaseModel):
repositories: list[RustRepository] = Field(default_factory=list) repositories: list[RustRepository] = Field(default_factory=list)
class GoRepository(BaseModel):
"""A Go module proxy (see GOPROXY), served over the standard Go module
proxy protocol."""
name: str
url: str
class GoConfig(BaseModel):
repositories: list[GoRepository] = Field(default_factory=list)
class RubyRepository(BaseModel):
"""A RubyGems source (see the ":sources:" key in gemrc)."""
name: str
url: str
class RubyConfig(BaseModel):
repositories: list[RubyRepository] = Field(default_factory=list)
class NuGetRepository(BaseModel):
"""A NuGet package source (see the "packageSources" key in NuGet.Config)."""
name: str
url: str
class NuGetConfig(BaseModel):
repositories: list[NuGetRepository] = 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)
@@ -198,6 +236,9 @@ class ConfigurationsBlock(BaseModel):
php: PhpConfig = Field(default_factory=PhpConfig) php: PhpConfig = Field(default_factory=PhpConfig)
npm: NpmConfig = Field(default_factory=NpmConfig) npm: NpmConfig = Field(default_factory=NpmConfig)
rust: RustConfig = Field(default_factory=RustConfig) rust: RustConfig = Field(default_factory=RustConfig)
go: GoConfig = Field(default_factory=GoConfig)
ruby: RubyConfig = Field(default_factory=RubyConfig)
nuget: NuGetConfig = Field(default_factory=NuGetConfig)
class Configuration(BaseModel): class Configuration(BaseModel):
@@ -216,14 +257,39 @@ def parse_configuration(raw: str) -> Configuration:
return Configuration.model_validate(data) return Configuration.model_validate(data)
def _fetch_config_url(url: str, cacert_content: str | None) -> str:
"""Download a JSON or YAML configuration from `url`.
If `cacert_content` is given, it - and only it, not the system's default
trust store - is used to verify an HTTPS connection: for an internal
endpoint signed by a private CA the running system doesn't otherwise
trust yet (e.g. before the `certificates` module has ever run).
"""
context = ssl.create_default_context(
cadata=cacert_content if cacert_content is not None else None
)
with urllib.request.urlopen( # nosec B310 (url is an operator-provided CLI argument)
url, timeout=_CONFIG_URL_TIMEOUT_SECONDS, context=context
) as response:
return response.read().decode("utf-8")
def load_configuration( def load_configuration(
config_file: Path | None, config_env: str | None config_file: Path | None,
config_env: str | None,
config_url: str | None = None,
config_url_cacert_content: str | None = None,
) -> Configuration: ) -> Configuration:
"""Load a configuration from a file, or from a base64-encoded string (typically an env var).""" """Load a configuration from a file, from a base64-encoded string
(typically an env var), or by downloading it from a URL."""
if config_file is not None: if config_file is not None:
raw = config_file.read_text(encoding="utf-8") raw = config_file.read_text(encoding="utf-8")
elif config_env is not None: elif config_env is not None:
raw = base64.b64decode(config_env).decode("utf-8") raw = base64.b64decode(config_env).decode("utf-8")
elif config_url is not None:
raw = _fetch_config_url(config_url, config_url_cacert_content)
else: else:
raise ValueError("either --config or --config-env must be provided") raise ValueError(
"either --config, --config-env or --config-url must be provided"
)
return parse_configuration(raw) return parse_configuration(raw)
+6
View File
@@ -7,10 +7,13 @@ from id_configurator.modules import (
apk, apk,
apt, apt,
certificates, certificates,
go,
java, java,
npm, npm,
nuget,
php, php,
python, python,
ruby,
rust, rust,
yum, yum,
) )
@@ -19,10 +22,13 @@ __all__ = [
"apk", "apk",
"apt", "apt",
"certificates", "certificates",
"go",
"java", "java",
"npm", "npm",
"nuget",
"php", "php",
"python", "python",
"ruby",
"rust", "rust",
"yum", "yum",
] ]
+12 -2
View File
@@ -213,8 +213,18 @@ class CertificatesModule(Module):
logger.info("rebuilt %s from %d certificate(s)", _CERT_BUNDLE, len(entries)) logger.info("rebuilt %s from %d certificate(s)", _CERT_BUNDLE, len(entries))
if etccertsdir_relative == _ETCCERTSDIR: if etccertsdir_relative == _ETCCERTSDIR:
cert_file = context.dst_path(_OPENSSL_DEFAULT_CERT_FILE) # Not context.dst_path(_OPENSSL_DEFAULT_CERT_FILE) directly: on a
cert_file.parent.mkdir(parents=True, exist_ok=True) # host that already has this symlink (e.g. re-applying to a live
# `--dst /`), resolving the leaf itself would follow it straight
# to the bundle we just wrote, and the unlink+symlink_to below
# would then target the bundle's own path instead of this one -
# replacing the bundle with a symlink to itself. Resolving only
# the containing directory keeps the leaf name literal.
cert_file_dir = context.dst_path(
posixpath.dirname(_OPENSSL_DEFAULT_CERT_FILE)
)
cert_file_dir.mkdir(parents=True, exist_ok=True)
cert_file = cert_file_dir / posixpath.basename(_OPENSSL_DEFAULT_CERT_FILE)
cert_file.unlink(missing_ok=True) cert_file.unlink(missing_ok=True)
cert_file.symlink_to(f"/{etccertsdir_relative}/{_CERT_BUNDLE}") cert_file.symlink_to(f"/{etccertsdir_relative}/{_CERT_BUNDLE}")
+50
View File
@@ -0,0 +1,50 @@
"""Configure the Go toolchain to use the configured GoRepository entries as
its module proxy (GOPROXY), replacing the default (proxy.golang.org). Since
an air-gapped mirror won't be listed in the public checksum database and
sum.golang.org is unreachable anyway, GOSUMDB is also turned off - each
module's hash is then simply trusted on first use, exactly as it would be
for any other GOPROXY.
Go'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 `$GOROOT/go.env` - not the per-user `go env -w` file (found at
`os.UserConfigDir()/go/env`, e.g. `$HOME/.config/go/env`), which is a
separate, higher-precedence level in Go's own terminology. `$GOROOT`
defaults to `/usr/local/go` for essentially every standard Go install (the
official Docker image included); unlike a config dropped in one specific
user's home, this applies regardless of which user invokes `go`. The go
command reads `$GOROOT/go.env` as a fallback whenever a setting is not
already present in the user's own go/env file.
"""
from __future__ import annotations
import logging
from id_configurator.config import GoRepository
from id_configurator.context import Context
from id_configurator.modules.base import Module, register
logger = logging.getLogger(__name__)
_GO_ENV = "usr/local/go/go.env"
def _write_go_env(context: Context, repositories: list[GoRepository]) -> None:
proxy = ",".join(repository.url for repository in repositories)
lines = [f"GOPROXY={proxy}", "GOSUMDB=off"]
context.write_file(_GO_ENV, ("\n".join(lines) + "\n").encode("utf-8"))
logger.info("configured go to use %d module prox(y/ies)", len(repositories))
@register
class GoModule(Module):
name = "go"
def is_relevant(self, context: Context) -> bool:
return bool(context.config.configurations.go.repositories)
def apply(self, context: Context) -> None:
_write_go_env(context, context.config.configurations.go.repositories)
+66
View File
@@ -0,0 +1,66 @@
"""Configure NuGet to use the configured NuGetRepository entries as its
package sources, replacing the default (nuget.org).
NuGet's own HTTPS client (.NET's HttpClient, backed by OpenSSL on Linux)
already trusts whatever the `certificates` module installs into the system
CA store, so no separate certificate configuration is needed here, unlike
npm's.
Microsoft documents a "Computer"-wide config file for this purpose,
`/etc/opt/NuGet/Config` on Linux (or `$NUGET_COMMON_APPLICATION_DATA/NuGet/Config`
should that variable be set) - but the .NET SDK does not actually read it in
practice (confirmed empirically; a known, still-open upstream gap, see
dotnet/sdk#12674), so it would silently have no effect. This instead writes
the "user" level config, `~/.nuget/NuGet/NuGet.Config` (the location the
.NET CLI itself uses; Mono/NuGet.exe use `~/.config/NuGet/NuGet.Config`
instead). This tool targets Docker images, which are built and run as root,
so `~` is assumed to be `/root`, exactly like a fresh dotnet install does
when not overridden - the same tradeoff the `php` module's Composer config
already accepts. A `<clear />` element is included so the configured
sources fully replace nuget.org rather than being merged alongside it,
matching every other package-manager module's behaviour.
"""
from __future__ import annotations
import logging
import xml.etree.ElementTree as ET
from id_configurator.config import NuGetRepository
from id_configurator.context import Context
from id_configurator.modules.base import Module, register
logger = logging.getLogger(__name__)
_NUGET_CONFIG = "root/.nuget/NuGet/NuGet.Config"
def _render_xml(root: ET.Element) -> bytes:
ET.indent(root)
content = '<?xml version="1.0" encoding="utf-8"?>\n' + ET.tostring(
root, encoding="unicode"
)
return (content + "\n").encode("utf-8")
def _write_nuget_config(context: Context, repositories: list[NuGetRepository]) -> None:
configuration = ET.Element("configuration")
package_sources = ET.SubElement(configuration, "packageSources")
ET.SubElement(package_sources, "clear")
for repository in repositories:
ET.SubElement(
package_sources, "add", {"key": repository.name, "value": repository.url}
)
context.write_file(_NUGET_CONFIG, _render_xml(configuration))
logger.info("configured nuget to use %d package source(s)", len(repositories))
@register
class NuGetModule(Module):
name = "nuget"
def is_relevant(self, context: Context) -> bool:
return bool(context.config.configurations.nuget.repositories)
def apply(self, context: Context) -> None:
_write_nuget_config(context, context.config.configurations.nuget.repositories)
+52
View File
@@ -0,0 +1,52 @@
"""Configure RubyGems to use the configured RubyRepository entries as its
gem sources, replacing the default (rubygems.org).
RubyGems' own HTTPS client (Net::HTTP, backed by OpenSSL) 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 RubyGems' system-wide config file, `Gem::ConfigFile::SYSTEM_WIDE_CONFIG_FILE`
- `$sysconfdir/gemrc`, where `$sysconfdir` is Ruby's own configured
sysconfdir, not necessarily `/etc`. The official Ruby Docker image builds
Ruby with `--prefix=/usr/local`, giving `/usr/local/etc/gemrc` (confirmed
empirically, since Debian's own `sysconfdir` convention of `/etc` does not
apply to a from-source build). Also written to the more conventional
`/etc/gemrc`, for any Ruby built with the Debian/`sysconfdir=/etc`
convention instead (e.g. a distro-packaged `ruby` rather than the official
image) - harmless to write in both places, since only whichever one a given
Ruby build actually treats as its sysconfdir is ever read. Unlike a config
dropped in one specific user's home (`~/.gemrc`, RubyGems' separate "per
user" level), this applies regardless of which user invokes `gem`.
"""
from __future__ import annotations
import logging
from id_configurator.config import RubyRepository
from id_configurator.context import Context
from id_configurator.modules.base import Module, register
logger = logging.getLogger(__name__)
_GEMRC_PATHS = ("usr/local/etc/gemrc", "etc/gemrc")
def _write_gemrc(context: Context, repositories: list[RubyRepository]) -> None:
lines = [":sources:"]
lines.extend(f"- {repository.url}" for repository in repositories)
content = ("\n".join(lines) + "\n").encode("utf-8")
for path in _GEMRC_PATHS:
context.write_file(path, content)
logger.info("configured gem to use %d source(s)", len(repositories))
@register
class RubyModule(Module):
name = "ruby"
def is_relevant(self, context: Context) -> bool:
return bool(context.config.configurations.ruby.repositories)
def apply(self, context: Context) -> None:
_write_gemrc(context, context.config.configurations.ruby.repositories)
-4
View File
@@ -79,10 +79,6 @@ COPY ./LICENSE /chroot/usr/share/doc/interdiode/LICENSE
WORKDIR / WORKDIR /
""" """
NPM_TO_COPY = [
("bootstrap/dist/js/bootstrap.esm.js.map", "js/bootstrap.esm.js.map"),
]
def build_docker_image( def build_docker_image(
config: SourceConfig, config: SourceConfig,
+635
View File
@@ -0,0 +1,635 @@
#!/usr/bin/python
"""Ansible module wrapping id-configurator.
Applies an id-configurator configuration directly to this host's live root
filesystem, exactly like the `id_configurator` role does, but as a native
Ansible module: it imports id-configurator's own Python API in-process
(instead of shelling out to the CLI through a temporary file), which also
lets it honor Ansible's check mode - reporting which modules would be
applied without writing anything.
This module is a thin, additive wrapper: it does not change anything about
id-configurator's own behavior (the `id_configurator` Python package,
including its module registry and every individual module, is used exactly
as-is - the same `Configuration`, `Context` and module registry the CLI
itself uses).
"""
from __future__ import annotations
from pathlib import Path
from ansible.module_utils.basic import AnsibleModule
DOCUMENTATION = r"""
module: id_configurator
short_description: Apply an id-configurator configuration to this host.
description:
- Assembles the given options into an id-configurator configuration and
applies it directly to this host's live root filesystem, by importing
id-configurator's own Python API in-process (no CLI subprocess, no
temporary file).
- The option names and structure mirror id_configurator.config.Configuration
and id_configurator.config.ConfigurationsBlock one for one. The
`id_configurator` Python package must already be installed on the
target host, using the same Python interpreter Ansible uses there.
- Supports check mode - reports which modules would be applied without
writing anything.
author:
- InterDiode
options:
uuid:
type: str
required: true
description: Configuration.uuid - a UUID identifying this configuration.
organisation:
type: str
required: true
description: Configuration.organisation.
description:
type: str
default: ""
description: Configuration.description.
certificates:
type: list
elements: dict
default: []
description: ConfigurationsBlock.certificates - root certificates to install.
suboptions:
name:
type: str
required: true
description: A short, unique name for this entry.
content:
type: str
required: true
description: PEM-encoded certificate content.
apt:
type: dict
default: {}
description: ConfigurationsBlock.apt.
suboptions:
sources:
type: list
elements: dict
default: []
description: DEB822 APT sources (AptSource).
suboptions:
name:
type: str
required: true
description: A short, unique name for this source.
types:
type: list
elements: str
default: [deb]
description: DEB822 "Types" field.
uris:
type: list
elements: str
required: true
description: DEB822 "URIs" field.
suites:
type: list
elements: str
required: true
description: DEB822 "Suites" field.
components:
type: list
elements: str
default: []
description: DEB822 "Components" field.
architectures:
type: list
elements: str
default: []
description: DEB822 "Architectures" field.
languages:
type: list
elements: str
default: []
description: DEB822 "Languages" field.
targets:
type: list
elements: str
default: []
description: DEB822 "Targets" field.
enabled:
type: bool
default: true
description: DEB822 "Enabled" field.
trusted:
type: bool
description: DEB822 "Trusted" field.
signed_by:
type: str
description: DEB822 "Signed-By" field.
pdiffs:
type: bool
description: DEB822 "PDiffs" field.
by_hash:
type: bool
description: DEB822 "By-Hash" field.
allow_insecure:
type: bool
description: DEB822 "Allow-Insecure" field.
allow_weak:
type: bool
description: DEB822 "Allow-Weak" field.
allow_downgrade_to_insecure:
type: bool
description: DEB822 "Allow-Downgrade-To-Insecure" field.
check_valid_until:
type: bool
description: DEB822 "Check-Valid-Until" field.
valid_until_min:
type: str
description: DEB822 "Valid-Until-Min" field.
valid_until_max:
type: str
description: DEB822 "Valid-Until-Max" field.
check_date:
type: bool
description: DEB822 "Check-Date" field.
date_max_future:
type: str
description: DEB822 "Date-Max-Future" field.
description:
type: str
description: DEB822 "Description" field.
keys:
type: list
elements: dict
default: []
description: GPG keys trusted for the sources above (NamedContent).
suboptions:
name:
type: str
required: true
description: A short, unique name for this entry.
content:
type: str
required: true
description: PEM-encoded/ASCII-armored key content.
apk:
type: dict
default: {}
description: ConfigurationsBlock.apk.
suboptions:
repositories:
type: list
elements: dict
default: []
description: Alpine APK repositories (ApkRepository).
suboptions:
url:
type: str
required: true
description: Repository URL, e.g. ending in /main or /community.
key:
type: str
description: The signing key's name, if any (see NamedContent).
yum:
type: dict
default: {}
description: ConfigurationsBlock.yum.
suboptions:
repositories:
type: list
elements: dict
default: []
description: YUM/DNF repositories (YumRepository).
suboptions:
name:
type: str
required: true
description: A short, unique name for this repository.
index_url:
type: str
required: true
description: The repository's baseurl.
gpg_url:
type: str
description: The repository's gpgkey URL.
keys:
type: list
elements: dict
default: []
description: GPG keys imported for the repositories above (NamedContent).
suboptions:
name:
type: str
required: true
description: A short, unique name for this entry.
content:
type: str
required: true
description: PEM-encoded/ASCII-armored key content.
python:
type: dict
default: {}
description: ConfigurationsBlock.python.
suboptions:
repositories:
type: list
elements: dict
default: []
description: Python package indexes (PythonRepository).
suboptions:
name:
type: str
required: true
description: A short, unique name for this index.
index_url:
type: str
required: true
description: The package index's URL.
java:
type: dict
default: {}
description: ConfigurationsBlock.java.
suboptions:
repositories:
type: list
elements: dict
default: []
description: Maven-compatible repositories (JavaRepository).
suboptions:
name:
type: str
required: true
description: A short, unique name for this repository.
url:
type: str
required: true
description: The repository's URL.
php:
type: dict
default: {}
description: ConfigurationsBlock.php.
suboptions:
repositories:
type: list
elements: dict
default: []
description: Composer repositories (PhpRepository).
suboptions:
name:
type: str
required: true
description: A short, unique name for this repository.
url:
type: str
required: true
description: The repository's URL.
npm:
type: dict
default: {}
description: ConfigurationsBlock.npm.
suboptions:
repositories:
type: list
elements: dict
default: []
description: npm registries (NpmRepository).
suboptions:
name:
type: str
required: true
description: A short, unique name for this repository.
url:
type: str
required: true
description: The repository's URL.
rust:
type: dict
default: {}
description: ConfigurationsBlock.rust.
suboptions:
repositories:
type: list
elements: dict
default: []
description: Cargo registries (RustRepository).
suboptions:
name:
type: str
required: true
description: A short, unique name for this repository.
url:
type: str
required: true
description: The repository's URL.
go:
type: dict
default: {}
description: ConfigurationsBlock.go.
suboptions:
repositories:
type: list
elements: dict
default: []
description: Go module proxies (GoRepository).
suboptions:
name:
type: str
required: true
description: A short, unique name for this repository.
url:
type: str
required: true
description: The repository's URL.
ruby:
type: dict
default: {}
description: ConfigurationsBlock.ruby.
suboptions:
repositories:
type: list
elements: dict
default: []
description: RubyGems sources (RubyRepository).
suboptions:
name:
type: str
required: true
description: A short, unique name for this repository.
url:
type: str
required: true
description: The repository's URL.
nuget:
type: dict
default: {}
description: ConfigurationsBlock.nuget.
suboptions:
repositories:
type: list
elements: dict
default: []
description: NuGet package sources (NuGetRepository).
suboptions:
name:
type: str
required: true
description: A short, unique name for this repository.
url:
type: str
required: true
description: The repository's URL.
"""
EXAMPLES = r"""
- name: Apply an id-configurator configuration to this host
id_configurator:
uuid: 12345678-1234-5678-1234-567812345678
organisation: InterDiode
certificates:
- name: Company Root CA
content: "{{ lookup('file', 'files/company-root-ca.pem') }}"
apt:
sources:
- name: internal
uris: ["https://apt.internal.example/"]
suites: ["stable"]
"""
RETURN = r"""
applied_modules:
description: Names of the id-configurator modules that were (or, in check
mode, would be) applied, in registration order.
returned: always
type: list
elements: str
sample: [certificates, apt]
"""
# Every key here must match one of ConfigurationsBlock's own field names
# (id_configurator/config.py) - see the module docstring above.
_CONFIGURATIONS_BLOCK_FIELDS = (
"certificates",
"apt",
"apk",
"yum",
"python",
"java",
"php",
"npm",
"rust",
"go",
"ruby",
"nuget",
)
_NAMED_CONTENT_OPTIONS = {
"name": {"type": "str", "required": True},
"content": {"type": "str", "required": True},
}
_NAME_URL_OPTIONS = {
"name": {"type": "str", "required": True},
"url": {"type": "str", "required": True},
}
def _name_url_repository_config() -> dict:
"""Argument spec for a `{repositories: [{name, url}]}` config block,
shared by java/php/npm/rust/go/ruby/nuget."""
return {
"type": "dict",
"default": {},
"options": {
"repositories": {
"type": "list",
"elements": "dict",
"default": [],
"options": dict(_NAME_URL_OPTIONS),
},
},
}
ARGUMENT_SPEC = {
"uuid": {"type": "str", "required": True},
"organisation": {"type": "str", "required": True},
"description": {"type": "str", "default": ""},
"certificates": {
"type": "list",
"elements": "dict",
"default": [],
"options": dict(_NAMED_CONTENT_OPTIONS),
},
"apt": {
"type": "dict",
"default": {},
"options": {
"sources": {
"type": "list",
"elements": "dict",
"default": [],
"options": {
"name": {"type": "str", "required": True},
"types": {"type": "list", "elements": "str", "default": ["deb"]},
"uris": {"type": "list", "elements": "str", "required": True},
"suites": {"type": "list", "elements": "str", "required": True},
"components": {"type": "list", "elements": "str", "default": []},
"architectures": {
"type": "list",
"elements": "str",
"default": [],
},
"languages": {"type": "list", "elements": "str", "default": []},
"targets": {"type": "list", "elements": "str", "default": []},
"enabled": {"type": "bool", "default": True},
"trusted": {"type": "bool"},
"signed_by": {"type": "str"},
"pdiffs": {"type": "bool"},
"by_hash": {"type": "bool"},
"allow_insecure": {"type": "bool"},
"allow_weak": {"type": "bool"},
"allow_downgrade_to_insecure": {"type": "bool"},
"check_valid_until": {"type": "bool"},
"valid_until_min": {"type": "str"},
"valid_until_max": {"type": "str"},
"check_date": {"type": "bool"},
"date_max_future": {"type": "str"},
"description": {"type": "str"},
},
},
"keys": {
"type": "list",
"elements": "dict",
"default": [],
"options": dict(_NAMED_CONTENT_OPTIONS),
},
},
},
"apk": {
"type": "dict",
"default": {},
"options": {
"repositories": {
"type": "list",
"elements": "dict",
"default": [],
"options": {
"url": {"type": "str", "required": True},
"key": {"type": "str"},
},
},
},
},
"yum": {
"type": "dict",
"default": {},
"options": {
"repositories": {
"type": "list",
"elements": "dict",
"default": [],
"options": {
"name": {"type": "str", "required": True},
"index_url": {"type": "str", "required": True},
"gpg_url": {"type": "str"},
},
},
"keys": {
"type": "list",
"elements": "dict",
"default": [],
"options": dict(_NAMED_CONTENT_OPTIONS),
},
},
},
"python": {
"type": "dict",
"default": {},
"options": {
"repositories": {
"type": "list",
"elements": "dict",
"default": [],
"options": {
"name": {"type": "str", "required": True},
"index_url": {"type": "str", "required": True},
},
},
},
},
"java": _name_url_repository_config(),
"php": _name_url_repository_config(),
"npm": _name_url_repository_config(),
"rust": _name_url_repository_config(),
"go": _name_url_repository_config(),
"ruby": _name_url_repository_config(),
"nuget": _name_url_repository_config(),
}
try:
import id_configurator.modules # noqa: F401 (registers the built-in modules)
from id_configurator.config import Configuration
from id_configurator.context import Context
from id_configurator.modules.base import all_modules
HAS_ID_CONFIGURATOR = True
ID_CONFIGURATOR_IMPORT_ERROR = None
except ImportError as exc:
HAS_ID_CONFIGURATOR = False
ID_CONFIGURATOR_IMPORT_ERROR = exc
def _build_configuration_data(params: dict) -> dict:
return {
"uuid": params["uuid"],
"organisation": params["organisation"],
"description": params["description"],
"configurations": {
field: params[field] for field in _CONFIGURATIONS_BLOCK_FIELDS
},
}
def _apply_configuration(configuration, check_mode: bool) -> list[str]:
"""Apply `configuration` to this host's live root filesystem, returning
the names of every module that was (or, in check mode, would be)
applied. Uses id-configurator's own module registry and `Context`
unchanged - this is a thin caller, not a reimplementation."""
root = Path("/")
context = Context(config=configuration, dst=root, src=root)
applied_modules = []
for module_cls in all_modules():
config_module = module_cls()
if config_module.is_relevant(context):
applied_modules.append(config_module.name)
if not check_mode:
config_module.apply(context)
return applied_modules
def main() -> None:
module = AnsibleModule(argument_spec=ARGUMENT_SPEC, supports_check_mode=True)
if not HAS_ID_CONFIGURATOR:
module.fail_json(
msg=(
"id_configurator is not importable with this host's Python "
f"interpreter: {ID_CONFIGURATOR_IMPORT_ERROR}"
)
)
return
try:
configuration = Configuration.model_validate(
_build_configuration_data(module.params)
)
except Exception as exc: # pydantic.ValidationError, kept generic on purpose
module.fail_json(msg=f"invalid id-configurator configuration: {exc}")
return
applied_modules = _apply_configuration(configuration, module.check_mode)
module.exit_json(changed=bool(applied_modules), applied_modules=applied_modules)
if __name__ == "__main__":
main()
+1 -1
View File
@@ -45,7 +45,7 @@ maintainers = [
name = "id-configurator" name = "id-configurator"
readme = "README.md" readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.13"
version = "0.1.0" version = "0.2.0"
[project.scripts] [project.scripts]
id-configurator = "id_configurator:main" id-configurator = "id_configurator:main"
+6
View File
@@ -27,3 +27,9 @@ configurations:
repositories: [] repositories: []
rust: rust:
repositories: [] repositories: []
go:
repositories: []
ruby:
repositories: []
nuget:
repositories: []
+6
View File
@@ -30,3 +30,9 @@ configurations:
repositories: [] repositories: []
rust: rust:
repositories: [] repositories: []
go:
repositories: []
ruby:
repositories: []
nuget:
repositories: []
+6
View File
@@ -24,3 +24,9 @@ configurations:
repositories: [] repositories: []
rust: rust:
repositories: [] repositories: []
go:
repositories: []
ruby:
repositories: []
nuget:
repositories: []
+34
View File
@@ -0,0 +1,34 @@
uuid: 3f1a2b4c-5d6e-4f70-8a91-2b3c4d5e6f70
organisation: InterDiode
description: Go 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: []
go:
repositories:
- name: demo
url: https://host.docker.internal:{{ 'GO_REPO_PORT' | env }}/
ruby:
repositories: []
nuget:
repositories: []
+6
View File
@@ -39,3 +39,9 @@ configurations:
repositories: [] repositories: []
rust: rust:
repositories: [] repositories: []
go:
repositories: []
ruby:
repositories: []
nuget:
repositories: []
+6
View File
@@ -26,3 +26,9 @@ configurations:
url: https://host.docker.internal:{{ 'NPM_REPO_PORT' | env }}/ url: https://host.docker.internal:{{ 'NPM_REPO_PORT' | env }}/
rust: rust:
repositories: [] repositories: []
go:
repositories: []
ruby:
repositories: []
nuget:
repositories: []
+34
View File
@@ -0,0 +1,34 @@
uuid: 5b3c4d6e-7f80-4192-8b13-4d5e6f708192
organisation: InterDiode
description: NuGet 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: []
go:
repositories: []
ruby:
repositories: []
nuget:
repositories:
- name: demo
url: https://host.docker.internal:{{ 'NUGET_REPO_PORT' | env }}/index.json
+6
View File
@@ -26,3 +26,9 @@ configurations:
repositories: [] repositories: []
rust: rust:
repositories: [] repositories: []
go:
repositories: []
ruby:
repositories: []
nuget:
repositories: []
+6
View File
@@ -26,3 +26,9 @@ configurations:
repositories: [] repositories: []
rust: rust:
repositories: [] repositories: []
go:
repositories: []
ruby:
repositories: []
nuget:
repositories: []
+34
View File
@@ -0,0 +1,34 @@
uuid: 4a2b3c5d-6e7f-4081-9a02-3c4d5e6f7081
organisation: InterDiode
description: Ruby 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: []
go:
repositories: []
ruby:
repositories:
- name: demo
url: https://host.docker.internal:{{ 'RUBY_REPO_PORT' | env }}/
nuget:
repositories: []
+6
View File
@@ -26,3 +26,9 @@ configurations:
repositories: repositories:
- name: demo - name: demo
url: https://host.docker.internal:{{ 'CARGO_REPO_PORT' | env }}/ url: https://host.docker.internal:{{ 'CARGO_REPO_PORT' | env }}/
go:
repositories: []
ruby:
repositories: []
nuget:
repositories: []
+6
View File
@@ -26,3 +26,9 @@ configurations:
repositories: [] repositories: []
rust: rust:
repositories: [] repositories: []
go:
repositories: []
ruby:
repositories: []
nuget:
repositories: []
+19
View File
@@ -0,0 +1,19 @@
FROM debian:trixie-slim AS target
FROM interdiode/id-configurator:latest AS configuration
ARG CONFIG_REPO_PORT
ARG CONFIG_URL_CACERT_CONTENT
COPY --from=target / /src/
RUN id-configurator apply \
--config-url "https://host.docker.internal:${CONFIG_REPO_PORT}/config.yaml" \
--config-url-cacert-content "$CONFIG_URL_CACERT_CONTENT" \
--src /src --dst /dst
FROM target AS runtime
ARG CONFIG_REPO_PORT
RUN apt-get update && apt-get install -y --no-install-recommends curl
COPY --from=configuration /dst/ /
# Proves the root CA the downloaded configuration installed is actually
# trusted system-wide: no --insecure/--no-check-certificate here, unlike the
# --config-url-cacert-content fetch above (which trusts only that one CA to
# validate the download itself, before anything has been installed yet).
RUN curl -fsS "https://host.docker.internal:${CONFIG_REPO_PORT}/config.yaml" \
| grep -q 'organisation: InterDiode'
+11
View File
@@ -0,0 +1,11 @@
FROM golang:1-bookworm AS target
FROM interdiode/id-configurator:latest AS configuration
COPY --from=target / /src/
COPY --from=generated-config Go.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/gotest && cd /tmp/gotest && go mod init gotest \
&& go get example.invalid/idconfiguratortestpkg@v0.0.1 \
&& grep -qx 'test-only module served by the id-configurator integration tests' \
"$(find "$(go env GOMODCACHE)" -name marker.txt)"
+11
View File
@@ -0,0 +1,11 @@
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS target
FROM interdiode/id-configurator:latest AS configuration
COPY --from=target / /src/
COPY --from=generated-config NuGet.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/nugettest && cd /tmp/nugettest && dotnet new console -o . --no-restore >/dev/null \
&& dotnet add package idconfiguratortestpkg --version 0.0.1 \
&& grep -qx 'test-only package served by the id-configurator integration tests' \
"$(find /root/.nuget/packages/idconfiguratortestpkg -name marker.txt)"
+10
View File
@@ -0,0 +1,10 @@
FROM ruby:3-slim AS target
FROM interdiode/id-configurator:latest AS configuration
COPY --from=target / /src/
COPY --from=generated-config Ruby.yaml /config.yaml
RUN id-configurator apply --config /config.yaml --src /src --dst /dst
FROM target AS runtime
COPY --from=configuration /dst/ /
RUN gem install idconfiguratortestpkg --no-document \
&& grep -qx 'test-only gem served by the id-configurator integration tests' \
"$(find "$(gem environment gemdir)" -name marker.txt)"
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<PackageId>idconfiguratortestpkg</PackageId>
<Version>0.0.1</Version>
<Authors>InterDiode</Authors>
<Description>test-only package served by the id-configurator integration tests</Description>
</PropertyGroup>
<ItemGroup>
<None Include="marker.txt" Pack="true" PackagePath="content/marker.txt" />
</ItemGroup>
</Project>
+32
View File
@@ -0,0 +1,32 @@
#!/bin/sh
# Build a minimal, hand-rolled RubyGems package and a full gem repository
# index for it (legacy + compact index formats), for integration-testing
# the `ruby` module of id-configurator. Uses RubyGems' own `gem build` and
# `gem generate_index` (the same tools a real gem server host would use)
# rather than hand-rolling the compact index's binary/marshal formats.
set -eu
PACKAGE=idconfiguratortestpkg
VERSION=0.0.1
MARKER="test-only gem served by the id-configurator integration tests"
gem install rubygems-generate_index --no-document
mkdir -p "/build/${PACKAGE}-${VERSION}"
cd "/build/${PACKAGE}-${VERSION}"
cat > "${PACKAGE}.gemspec" <<EOF
Gem::Specification.new do |s|
s.name = "${PACKAGE}"
s.version = "${VERSION}"
s.summary = "test-only gem"
s.authors = ["InterDiode"]
s.license = "MIT"
s.files = ["marker.txt"]
end
EOF
echo "${MARKER}" > marker.txt
gem build "${PACKAGE}.gemspec"
mkdir -p /srv/gems
cp ./*.gem /srv/gems/
gem generate_index -d /srv
@@ -0,0 +1,47 @@
"""Build a minimal, offline, hand-rolled Go module proxy tree, for
integration-testing the `go` module of id-configurator.
No network access and no Go toolchain is used: the module proxy protocol
(https://go.dev/ref/mod#module-proxy) only requires a handful of static
files, none of which embed the server's own base URL (unlike Composer/
npm/cargo), so this can be served directly by nginx with no rewriting.
"""
from __future__ import annotations
import io
import json
import pathlib
import zipfile
MODULE = "example.invalid/idconfiguratortestpkg"
VERSION = "v0.0.1"
OUT_DIR = pathlib.Path("/srv")
MARKER = "test-only module served by the id-configurator integration tests\n"
GO_MOD = f"module {MODULE}\n\ngo 1.21\n"
def build_zip() -> bytes:
buffer = io.BytesIO()
prefix = f"{MODULE}@{VERSION}"
with zipfile.ZipFile(buffer, "w") as archive:
archive.writestr(f"{prefix}/go.mod", GO_MOD)
archive.writestr(f"{prefix}/marker.txt", MARKER)
return buffer.getvalue()
def main() -> None:
module_dir = OUT_DIR / MODULE / "@v"
module_dir.mkdir(parents=True, exist_ok=True)
(module_dir / "list").write_text(f"{VERSION}\n", encoding="utf-8")
(module_dir / f"{VERSION}.info").write_text(
json.dumps({"Version": VERSION, "Time": "2024-01-01T00:00:00Z"}),
encoding="utf-8",
)
(module_dir / f"{VERSION}.mod").write_text(GO_MOD, encoding="utf-8")
(module_dir / f"{VERSION}.zip").write_bytes(build_zip())
if __name__ == "__main__":
main()
@@ -0,0 +1,61 @@
"""Build a minimal NuGet v3 static feed around one real, `dotnet pack`-built
.nupkg, for integration-testing the `nuget` module of id-configurator.
Only two resources are populated: the flat container (`PackageBaseAddress`,
used to enumerate versions and download the .nupkg directly) and the
top-level service index pointing at it. `dotnet add package`/`dotnet
restore` with an explicit, dependency-free package version only ever calls
these two - never the (considerably more complex) registration resource -
so it is intentionally not implemented here.
The service index bakes in a __BASE_URL__ placeholder for its "@id" fields,
since the host:port this fixture is actually reached on is only known once
tox-docker starts the container, not at image-build time (see
nuget-repo.nginx.conf). The flat container's own files never embed a URL,
so they need no such rewriting.
"""
from __future__ import annotations
import json
import pathlib
import shutil
PACKAGE_ID = "idconfiguratortestpkg"
VERSION = "0.0.1"
NUPKG_NAME = f"{PACKAGE_ID}.{VERSION}.nupkg"
NUPKG_SRC = pathlib.Path("/nupkg") / NUPKG_NAME
OUT_DIR = pathlib.Path("/srv")
def write_flat_container() -> None:
package_dir = OUT_DIR / "flatcontainer" / PACKAGE_ID
version_dir = package_dir / VERSION
version_dir.mkdir(parents=True, exist_ok=True)
(package_dir / "index.json").write_text(
json.dumps({"versions": [VERSION]}), encoding="utf-8"
)
shutil.copy(NUPKG_SRC, version_dir / NUPKG_NAME)
def write_service_index() -> None:
index = {
"version": "3.0.0",
"resources": [
{
"@id": "__BASE_URL__/flatcontainer/",
"@type": "PackageBaseAddress/3.0.0",
}
],
}
(OUT_DIR / "index.json").write_text(json.dumps(index), encoding="utf-8")
def main() -> None:
OUT_DIR.mkdir(parents=True, exist_ok=True)
write_flat_container()
write_service_index()
if __name__ == "__main__":
main()
@@ -0,0 +1,40 @@
# Static id-configurator configuration served by the config-url-repo fixture,
# to be downloaded (not copied in locally) via `id-configurator apply
# --config-url ... --config-url-cacert-content ...`
# (tests/dockerfiles/ConfigUrl.Dockerfile). No per-run templating is needed:
# unlike the module-* fixtures, nothing in this document needs to reference
# its own server's dynamic host:port.
#
# The certificate below is tests/certs/test-ca.crt.pem - the same Test CA
# that signs repo-server.crt.pem in this directory. It's duplicated here
# (rather than read at build time) because a Docker build's context is
# this directory alone; keep both copies in sync if the Test CA is ever
# regenerated.
uuid: 8f3e4b2c-9a41-4d3e-8f1a-2b6c7d8e9f01
organisation: InterDiode
description: Configuration downloaded via --config-url
configurations:
certificates:
- name: Test CA
content: |
-----BEGIN CERTIFICATE-----
MIIDdTCCAl2gAwIBAgIUKq4URHQwyg7IzJ3ViAGGlM9pGCwwDQYJKoZIhvcNAQEL
BQAwQjEeMBwGA1UECgwVaWQtY29uZmlndXJhdG9yLXRlc3RzMSAwHgYDVQQDDBdp
ZC1jb25maWd1cmF0b3IgVGVzdCBDQTAeFw0yNjA5MDUyMTA4MTNaFw00NjA4MzEy
MTA4MTNaMEIxHjAcBgNVBAoMFWlkLWNvbmZpZ3VyYXRvci10ZXN0czEgMB4GA1UE
AwwXaWQtY29uZmlndXJhdG9yIFRlc3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB
DwAwggEKAoIBAQD8bMteh3MDnbW6OoFMlhA4Z2QAabVoSFlSE/Si7ut+L/3v1GY4
lixenwLQc8a2koIH9CtmOodzfO7hpro/VUjYJ9J2eTsXLLiOg7OgggG1cKV9/kt7
RF0Q4hkcanGmiffc4xbz4ZedU6lP1vzLCsgU0A8OPt6svcNjGcWbjxss0KjgxGZ7
H4UKdL7Mv4W2Y6Fw1b4eV2M6jErkQK6IPXsHGFh7JkzliNcb0sX1VfbfauS84tBh
f3EHwzLNhbjtEmQhQdqgsg4Cbk/gWur//6aEvlD/X12PAUbrp4DFD+CDuVagLX7I
7TQBXNFGKhozzQuDg7DFg1gIV8SFx2m+s60/AgMBAAGjYzBhMB0GA1UdDgQWBBSb
zJ7DsaxwCeqbfOXGJ3x+u7bjozAfBgNVHSMEGDAWgBSbzJ7DsaxwCeqbfOXGJ3x+
u7bjozAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0B
AQsFAAOCAQEAEV1+dG3agh0S/AR8jC8M33HXFyvneCCRtfb5q/loJ//VoWPbD3Gs
G+2SR0MVN+KKOMBvCBh3KrNSgFF9iFmBjNCJJZ8ycDIszUK3xLjUP2EzN1pNNl8S
D/yHzUCro3547Q+I+Nn5zRciG95EpMvtkIWem4H8luYSBt7w8y9uDUKWzaca/v3z
oGZPZP99w8WVqMKGRbsK94wsTVsWiE+oHzSufwrhvZJfQQ4jU6102IEnRS/eQFXA
+fozgEWuFatZe3Gz0HUJakrzSBwSTV6wmnEHCfu+uwdns1Ttq5FylZ4StJHsmweL
14w1xa3tjC5bQ3e1QWK3R5InHcSZ7tLpGA==
-----END CERTIFICATE-----
@@ -0,0 +1,13 @@
# Serves a static id-configurator configuration document over TLS, using the
# same Test CA as the other *-repo fixtures, used by tox-docker to exercise
# `id-configurator apply --config-url ... --config-url-cacert-content ...`
# (tests/dockerfiles/ConfigUrl.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 nginx:alpine
COPY config-url-config.yaml /srv/config.yaml
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 config-url-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;
}
@@ -0,0 +1,17 @@
# Minimal Go module proxy serving one hand-built module over TLS, using the
# same Test CA as the other *-repo fixtures, used by tox-docker to exercise
# the `go` module integration test (tests/configs/Go.yaml.j2,
# tests/dockerfiles/Go.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_go_module.py /build_test_go_module.py
RUN python3 /build_test_go_module.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 go-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;
}
@@ -0,0 +1,23 @@
# Minimal NuGet v3 static feed serving one real, dotnet-packed .nupkg over
# TLS, using the same Test CA as the other *-repo fixtures, used by
# tox-docker to exercise the `nuget` module integration test
# (tests/configs/NuGet.yaml.j2, tests/dockerfiles/NuGet.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 mcr.microsoft.com/dotnet/sdk:9.0 AS packagebuild
COPY IdConfiguratorTestPkg.csproj /pkgsrc/IdConfiguratorTestPkg.csproj
RUN echo 'test-only package served by the id-configurator integration tests' > /pkgsrc/marker.txt \
&& dotnet pack /pkgsrc -c Release -o /nupkg
FROM python:3.13-slim AS feedbuild
COPY --from=packagebuild /nupkg /nupkg
COPY build_test_nuget_feed.py /build_test_nuget_feed.py
RUN python3 /build_test_nuget_feed.py
FROM nginx:alpine
COPY --from=feedbuild /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 nuget-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 service index bakes in a __BASE_URL__ placeholder for its "@id"
# fields, 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 (nuget) used to
# connect, e.g. host.docker.internal:55123.
location = /index.json {
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 RubyGems repository serving one hand-built gem over TLS, using the
# same Test CA as the other *-repo fixtures, used by tox-docker to exercise
# the `ruby` module integration test (tests/configs/Ruby.yaml.j2,
# tests/dockerfiles/Ruby.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 ruby:3-slim AS packagebuild
COPY build_test_gem.sh /build_test_gem.sh
RUN sh /build_test_gem.sh
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 ruby-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;
}
+34
View File
@@ -237,3 +237,37 @@ def test_apply_writes_bundle_to_rhel_style_symlinked_etccertsdir(tmp_path) -> No
# /usr/lib/ssl/cert.pem is Debian/Ubuntu-specific: not OpenSSL's default # /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. # CAfile on this (RHEL-style) system, so it must not be created.
assert not (dst / "usr/lib/ssl/cert.pem").exists() assert not (dst / "usr/lib/ssl/cert.pem").exists()
def test_apply_is_idempotent_when_src_and_dst_are_the_same_live_root(
tmp_path,
) -> None:
# Regression test: applying directly to a live root (--src / --dst /,
# as the Ansible role does) where /usr/lib/ssl/cert.pem already exists
# (as it would on any host with ca-certificates already installed) must
# not resolve that pre-existing symlink away when recreating it - doing
# so would unlink/replace the bundle it points to instead, corrupting it
# into a symlink to itself and breaking every subsequent run.
root = tmp_path / "root"
etc_ssl_certs = root / "etc/ssl/certs"
etc_ssl_certs.mkdir(parents=True)
(etc_ssl_certs / "ca-certificates.crt").write_bytes(b"preexisting-bundle-bytes")
usr_lib_ssl = root / "usr/lib/ssl"
usr_lib_ssl.mkdir(parents=True)
(usr_lib_ssl / "cert.pem").symlink_to("/etc/ssl/certs/ca-certificates.crt")
context = Context(config=make_config(True), dst=root, src=root)
for _ in range(2):
CertificatesModule().apply(context)
bundle = etc_ssl_certs / "ca-certificates.crt"
assert not bundle.is_symlink()
assert bundle.read_bytes() == b"fake-cert-bytes\n"
openssl_default = usr_lib_ssl / "cert.pem"
assert openssl_default.is_symlink()
assert (
openssl_default.readlink().as_posix()
== "/etc/ssl/certs/ca-certificates.crt"
)
+24 -1
View File
@@ -33,7 +33,27 @@ def test_validate_valid_config(tmp_path) -> None:
def test_validate_without_config_fails() -> None: def test_validate_without_config_fails() -> None:
result = runner.invoke(cli.app, ["validate"]) result = runner.invoke(cli.app, ["validate"])
assert result.exit_code != 0 assert result.exit_code != 0
assert "either --config or --config-env" in result.output assert "either --config, --config-env" in result.output
def _mock_urlopen(content: bytes) -> MagicMock:
response = MagicMock()
response.read.return_value = content
response.__enter__.return_value = response
response.__exit__.return_value = False
return MagicMock(return_value=response)
def test_validate_from_config_url(monkeypatch) -> None:
monkeypatch.setattr(
"id_configurator.config.urllib.request.urlopen",
_mock_urlopen(json.dumps(MINIMAL_CONFIG).encode("utf-8")),
)
result = runner.invoke(
cli.app, ["validate", "--config-url", "https://example.invalid/config.json"]
)
assert result.exit_code == 0
assert "is valid" in result.stdout
def test_list_modules() -> None: def test_list_modules() -> None:
@@ -43,10 +63,13 @@ def test_list_modules() -> None:
"apk", "apk",
"apt", "apt",
"certificates", "certificates",
"go",
"java", "java",
"npm", "npm",
"nuget",
"php", "php",
"python", "python",
"ruby",
"rust", "rust",
"yum", "yum",
} }
+67 -1
View File
@@ -1,7 +1,14 @@
import base64 import base64
import datetime
import json import json
from unittest.mock import MagicMock
import pytest import pytest
from cryptography import x509
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.serialization import Encoding
from cryptography.x509.oid import NameOID
from id_configurator.config import ( from id_configurator.config import (
AptSource, AptSource,
@@ -98,10 +105,69 @@ def test_load_configuration_from_env() -> None:
def test_load_configuration_without_source_raises() -> None: def test_load_configuration_without_source_raises() -> None:
with pytest.raises(ValueError, match="either --config or --config-env"): with pytest.raises(
ValueError, match="either --config, --config-env or --config-url"
):
load_configuration(None, None) load_configuration(None, None)
def _mock_urlopen(content: bytes) -> MagicMock:
response = MagicMock()
response.read.return_value = content
response.__enter__.return_value = response
response.__exit__.return_value = False
return MagicMock(return_value=response)
def _self_signed_ca_pem() -> str:
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
subject = issuer = x509.Name(
[x509.NameAttribute(NameOID.COMMON_NAME, "test-config-url-ca")]
)
now = datetime.datetime.now(datetime.UTC)
cert = (
x509.CertificateBuilder()
.subject_name(subject)
.issuer_name(issuer)
.public_key(key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(now)
.not_valid_after(now + datetime.timedelta(days=1))
.sign(key, hashes.SHA256())
)
return cert.public_bytes(Encoding.PEM).decode("ascii")
def test_load_configuration_from_url(monkeypatch) -> None:
mock_urlopen = _mock_urlopen(json.dumps(FULL_CONFIG).encode("utf-8"))
monkeypatch.setattr("id_configurator.config.urllib.request.urlopen", mock_urlopen)
configuration = load_configuration(
None, None, "https://example.invalid/config.json"
)
assert configuration.organisation == "InterDiode"
args, kwargs = mock_urlopen.call_args
assert args[0] == "https://example.invalid/config.json"
assert kwargs["timeout"] == 30
assert kwargs["context"].verify_mode.name == "CERT_REQUIRED"
def test_load_configuration_from_url_with_custom_cacert(monkeypatch) -> None:
mock_urlopen = _mock_urlopen(json.dumps(FULL_CONFIG).encode("utf-8"))
monkeypatch.setattr("id_configurator.config.urllib.request.urlopen", mock_urlopen)
cacert_content = _self_signed_ca_pem()
configuration = load_configuration(
None, None, "https://example.invalid/config.json", cacert_content
)
assert configuration.organisation == "InterDiode"
_, kwargs = mock_urlopen.call_args
stats = kwargs["context"].cert_store_stats()
assert stats["x509"] == 1
def test_apt_source_to_deb822_minimal() -> None: def test_apt_source_to_deb822_minimal() -> None:
source = AptSource( source = AptSource(
name="interdiode", uris=["http://example.invalid/debian"], suites=["stable"] name="interdiode", uris=["http://example.invalid/debian"], suites=["stable"]
+67
View File
@@ -0,0 +1,67 @@
from id_configurator.config import Configuration
from id_configurator.context import Context
from id_configurator.modules.go import GoModule
CONFIG_WITH_REPO = {
"uuid": "12345678-1234-5678-1234-567812345678",
"organisation": "InterDiode",
"configurations": {
"go": {"repositories": [{"name": "demo", "url": "https://example.invalid/"}]}
},
}
CONFIG_WITH_TWO_REPOS = {
"uuid": "12345678-1234-5678-1234-567812345678",
"organisation": "InterDiode",
"configurations": {
"go": {
"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 GoModule().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 GoModule().is_relevant(context) is True
def test_apply_writes_goproxy(tmp_path) -> None:
dst = tmp_path / "dst"
context = Context(config=make_config(CONFIG_WITH_REPO), dst=dst, src=None)
GoModule().apply(context)
go_env = (dst / "usr/local/go/go.env").read_text(encoding="utf-8")
assert go_env == "GOPROXY=https://example.invalid/\nGOSUMDB=off\n"
def test_apply_joins_multiple_repositories_with_comma(tmp_path) -> None:
dst = tmp_path / "dst"
context = Context(config=make_config(CONFIG_WITH_TWO_REPOS), dst=dst, src=None)
GoModule().apply(context)
go_env = (dst / "usr/local/go/go.env").read_text(encoding="utf-8")
assert go_env == (
"GOPROXY=https://example.invalid/,https://other.invalid/\nGOSUMDB=off\n"
)
+82
View File
@@ -0,0 +1,82 @@
from id_configurator.config import Configuration
from id_configurator.context import Context
from id_configurator.modules.nuget import NuGetModule
CONFIG_WITH_REPO = {
"uuid": "12345678-1234-5678-1234-567812345678",
"organisation": "InterDiode",
"configurations": {
"nuget": {"repositories": [{"name": "demo", "url": "https://example.invalid/"}]}
},
}
CONFIG_WITH_TWO_REPOS = {
"uuid": "12345678-1234-5678-1234-567812345678",
"organisation": "InterDiode",
"configurations": {
"nuget": {
"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 NuGetModule().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 NuGetModule().is_relevant(context) is True
def test_apply_writes_nuget_config(tmp_path) -> None:
dst = tmp_path / "dst"
context = Context(config=make_config(CONFIG_WITH_REPO), dst=dst, src=None)
NuGetModule().apply(context)
content = (dst / "root/.nuget/NuGet/NuGet.Config").read_text(encoding="utf-8")
assert content == (
'<?xml version="1.0" encoding="utf-8"?>\n'
"<configuration>\n"
" <packageSources>\n"
" <clear />\n"
' <add key="demo" value="https://example.invalid/" />\n'
" </packageSources>\n"
"</configuration>\n"
)
def test_apply_writes_multiple_sources_in_order(tmp_path) -> None:
dst = tmp_path / "dst"
context = Context(config=make_config(CONFIG_WITH_TWO_REPOS), dst=dst, src=None)
NuGetModule().apply(context)
content = (dst / "root/.nuget/NuGet/NuGet.Config").read_text(encoding="utf-8")
assert content == (
'<?xml version="1.0" encoding="utf-8"?>\n'
"<configuration>\n"
" <packageSources>\n"
" <clear />\n"
' <add key="demo" value="https://example.invalid/" />\n'
' <add key="other" value="https://other.invalid/" />\n'
" </packageSources>\n"
"</configuration>\n"
)
+67
View File
@@ -0,0 +1,67 @@
from id_configurator.config import Configuration
from id_configurator.context import Context
from id_configurator.modules.ruby import RubyModule
CONFIG_WITH_REPO = {
"uuid": "12345678-1234-5678-1234-567812345678",
"organisation": "InterDiode",
"configurations": {
"ruby": {"repositories": [{"name": "demo", "url": "https://example.invalid/"}]}
},
}
CONFIG_WITH_TWO_REPOS = {
"uuid": "12345678-1234-5678-1234-567812345678",
"organisation": "InterDiode",
"configurations": {
"ruby": {
"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 RubyModule().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 RubyModule().is_relevant(context) is True
def test_apply_writes_gemrc(tmp_path) -> None:
dst = tmp_path / "dst"
context = Context(config=make_config(CONFIG_WITH_REPO), dst=dst, src=None)
RubyModule().apply(context)
expected = ":sources:\n- https://example.invalid/\n"
assert (dst / "usr/local/etc/gemrc").read_text(encoding="utf-8") == expected
assert (dst / "etc/gemrc").read_text(encoding="utf-8") == expected
def test_apply_writes_multiple_sources_in_order(tmp_path) -> None:
dst = tmp_path / "dst"
context = Context(config=make_config(CONFIG_WITH_TWO_REPOS), dst=dst, src=None)
RubyModule().apply(context)
expected = ":sources:\n- https://example.invalid/\n- https://other.invalid/\n"
assert (dst / "usr/local/etc/gemrc").read_text(encoding="utf-8") == expected
assert (dst / "etc/gemrc").read_text(encoding="utf-8") == expected
+111 -1
View File
@@ -1,5 +1,5 @@
[tox] [tox]
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 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,module-go,module-ruby,module-nuget,config-url
isolated_build = true isolated_build = true
requires = requires =
tox>=4 tox>=4
@@ -96,6 +96,7 @@ healthcheck_start_period = 1
[testenv:module-python] [testenv:module-python]
package = skip package = skip
labels = end-to-end
change_dir = {tox_root} change_dir = {tox_root}
dependency_groups = dev dependency_groups = dev
docker = docker =
@@ -121,6 +122,7 @@ healthcheck_start_period = 1
[testenv:module-apt] [testenv:module-apt]
package = skip package = skip
labels = end-to-end
change_dir = {tox_root} change_dir = {tox_root}
dependency_groups = dev dependency_groups = dev
docker = docker =
@@ -144,6 +146,7 @@ healthcheck_start_period = 1
[testenv:module-yum] [testenv:module-yum]
package = skip package = skip
labels = end-to-end
change_dir = {tox_root} change_dir = {tox_root}
dependency_groups = dev dependency_groups = dev
docker = docker =
@@ -167,6 +170,7 @@ healthcheck_start_period = 1
[testenv:module-apk] [testenv:module-apk]
package = skip package = skip
labels = end-to-end
change_dir = {tox_root} change_dir = {tox_root}
dependency_groups = dev dependency_groups = dev
docker = docker =
@@ -199,6 +203,7 @@ healthcheck_start_period = 1
[testenv:module-java] [testenv:module-java]
package = skip package = skip
labels = end-to-end
change_dir = {tox_root} change_dir = {tox_root}
dependency_groups = dev dependency_groups = dev
docker = docker =
@@ -222,6 +227,7 @@ healthcheck_start_period = 1
[testenv:module-certificates] [testenv:module-certificates]
package = skip package = skip
labels = end-to-end
change_dir = {tox_root} change_dir = {tox_root}
dependency_groups = dev dependency_groups = dev
docker = docker =
@@ -250,6 +256,7 @@ healthcheck_start_period = 1
[testenv:module-php] [testenv:module-php]
package = skip package = skip
labels = end-to-end
change_dir = {tox_root} change_dir = {tox_root}
dependency_groups = dev dependency_groups = dev
docker = docker =
@@ -273,6 +280,7 @@ healthcheck_start_period = 1
[testenv:module-npm] [testenv:module-npm]
package = skip package = skip
labels = end-to-end
change_dir = {tox_root} change_dir = {tox_root}
dependency_groups = dev dependency_groups = dev
docker = docker =
@@ -296,6 +304,7 @@ healthcheck_start_period = 1
[testenv:module-rust] [testenv:module-rust]
package = skip package = skip
labels = end-to-end
change_dir = {tox_root} change_dir = {tox_root}
dependency_groups = dev dependency_groups = dev
docker = docker =
@@ -306,3 +315,104 @@ commands =
python3 -m id_tools test render-config {tox_root}/tests/configs/Rust.yaml.j2 {env_dir}/configs/Rust.yaml 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 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 docker rmi id-configurator-rust
[docker:go-repo]
dockerfile = {tox_root}/tests/dockerfiles/repos/go-repo.Dockerfile
expose =
GO_REPO_PORT=8443/tcp
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/example.invalid/idconfiguratortestpkg/@v/list || exit 1
healthcheck_interval = 1
healthcheck_retries = 30
healthcheck_timeout = 2
healthcheck_start_period = 1
[testenv:module-go]
package = skip
labels = end-to-end
change_dir = {tox_root}
dependency_groups = dev
docker =
go-repo
allowlist_externals =
docker
commands =
python3 -m id_tools test render-config {tox_root}/tests/configs/Go.yaml.j2 {env_dir}/configs/Go.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/Go.Dockerfile -t id-configurator-go .
docker rmi id-configurator-go
[docker:ruby-repo]
dockerfile = {tox_root}/tests/dockerfiles/repos/ruby-repo.Dockerfile
expose =
RUBY_REPO_PORT=8443/tcp
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/versions || exit 1
healthcheck_interval = 1
healthcheck_retries = 30
healthcheck_timeout = 2
healthcheck_start_period = 1
[testenv:module-ruby]
package = skip
labels = end-to-end
change_dir = {tox_root}
dependency_groups = dev
docker =
ruby-repo
allowlist_externals =
docker
commands =
python3 -m id_tools test render-config {tox_root}/tests/configs/Ruby.yaml.j2 {env_dir}/configs/Ruby.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/Ruby.Dockerfile -t id-configurator-ruby .
docker rmi id-configurator-ruby
[docker:nuget-repo]
dockerfile = {tox_root}/tests/dockerfiles/repos/nuget-repo.Dockerfile
expose =
NUGET_REPO_PORT=8443/tcp
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/index.json || exit 1
healthcheck_interval = 1
healthcheck_retries = 30
healthcheck_timeout = 2
healthcheck_start_period = 1
[testenv:module-nuget]
package = skip
labels = end-to-end
change_dir = {tox_root}
dependency_groups = dev
docker =
nuget-repo
allowlist_externals =
docker
commands =
python3 -m id_tools test render-config {tox_root}/tests/configs/NuGet.yaml.j2 {env_dir}/configs/NuGet.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/NuGet.Dockerfile -t id-configurator-nuget .
docker rmi id-configurator-nuget
[docker:config-url-repo]
dockerfile = {tox_root}/tests/dockerfiles/repos/config-url-repo.Dockerfile
expose =
CONFIG_REPO_PORT=8443/tcp
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/config.yaml || exit 1
healthcheck_interval = 1
healthcheck_retries = 30
healthcheck_timeout = 2
healthcheck_start_period = 1
[testenv:config-url]
package = skip
labels = end-to-end
change_dir = {tox_root}
dependency_groups = dev
docker =
config-url-repo
allowlist_externals =
docker
sh
description =
Verifies `id-configurator apply --config-url ... --config-url-cacert-content
...`: the configuration itself is downloaded over HTTPS from the
config-url-repo fixture (trusting only the given root CA content, not the
system's trust store), rather than read from a local --config file.
commands =
sh -c 'docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-arg CONFIG_REPO_PORT={env:CONFIG_REPO_PORT} --build-arg CONFIG_URL_CACERT_CONTENT="$(cat {tox_root}/tests/certs/test-ca.crt.pem)" -f {tox_root}/tests/dockerfiles/ConfigUrl.Dockerfile -t id-configurator-config-url .'
docker rmi id-configurator-config-url
Generated
+1 -1
View File
@@ -411,7 +411,7 @@ dependencies = [
] ]
name = "id-configurator" name = "id-configurator"
source = {editable = "."} source = {editable = "."}
version = "0.1.0" version = "0.2.0"
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [