mirror of
https://github.com/d9pouces/id_configurator.git
synced 2026-11-09 01:47:21 +01:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
ab2a179a55
|
|||
|
9de98d4744
|
|||
|
88380b6ab6
|
|||
|
e3b2e8a95a
|
|||
|
4e4b0592c0
|
|||
|
a8143b61b1
|
@@ -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
@@ -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})"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -27,3 +27,9 @@ configurations:
|
|||||||
repositories: []
|
repositories: []
|
||||||
rust:
|
rust:
|
||||||
repositories: []
|
repositories: []
|
||||||
|
go:
|
||||||
|
repositories: []
|
||||||
|
ruby:
|
||||||
|
repositories: []
|
||||||
|
nuget:
|
||||||
|
repositories: []
|
||||||
|
|||||||
@@ -30,3 +30,9 @@ configurations:
|
|||||||
repositories: []
|
repositories: []
|
||||||
rust:
|
rust:
|
||||||
repositories: []
|
repositories: []
|
||||||
|
go:
|
||||||
|
repositories: []
|
||||||
|
ruby:
|
||||||
|
repositories: []
|
||||||
|
nuget:
|
||||||
|
repositories: []
|
||||||
|
|||||||
@@ -24,3 +24,9 @@ configurations:
|
|||||||
repositories: []
|
repositories: []
|
||||||
rust:
|
rust:
|
||||||
repositories: []
|
repositories: []
|
||||||
|
go:
|
||||||
|
repositories: []
|
||||||
|
ruby:
|
||||||
|
repositories: []
|
||||||
|
nuget:
|
||||||
|
repositories: []
|
||||||
|
|||||||
@@ -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: []
|
||||||
@@ -39,3 +39,9 @@ configurations:
|
|||||||
repositories: []
|
repositories: []
|
||||||
rust:
|
rust:
|
||||||
repositories: []
|
repositories: []
|
||||||
|
go:
|
||||||
|
repositories: []
|
||||||
|
ruby:
|
||||||
|
repositories: []
|
||||||
|
nuget:
|
||||||
|
repositories: []
|
||||||
|
|||||||
@@ -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: []
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -26,3 +26,9 @@ configurations:
|
|||||||
repositories: []
|
repositories: []
|
||||||
rust:
|
rust:
|
||||||
repositories: []
|
repositories: []
|
||||||
|
go:
|
||||||
|
repositories: []
|
||||||
|
ruby:
|
||||||
|
repositories: []
|
||||||
|
nuget:
|
||||||
|
repositories: []
|
||||||
|
|||||||
@@ -26,3 +26,9 @@ configurations:
|
|||||||
repositories: []
|
repositories: []
|
||||||
rust:
|
rust:
|
||||||
repositories: []
|
repositories: []
|
||||||
|
go:
|
||||||
|
repositories: []
|
||||||
|
ruby:
|
||||||
|
repositories: []
|
||||||
|
nuget:
|
||||||
|
repositories: []
|
||||||
|
|||||||
@@ -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: []
|
||||||
@@ -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: []
|
||||||
|
|||||||
@@ -26,3 +26,9 @@ configurations:
|
|||||||
repositories: []
|
repositories: []
|
||||||
rust:
|
rust:
|
||||||
repositories: []
|
repositories: []
|
||||||
|
go:
|
||||||
|
repositories: []
|
||||||
|
ruby:
|
||||||
|
repositories: []
|
||||||
|
nuget:
|
||||||
|
repositories: []
|
||||||
|
|||||||
@@ -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'
|
||||||
@@ -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)"
|
||||||
@@ -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)"
|
||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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"]
|
||||||
|
|||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user