mirror of
https://github.com/d9pouces/id_configurator.git
synced 2026-11-09 01:47:21 +01:00
feat: update version to 2026.9.7 in pyproject.toml
Signed-off-by: d9pouces <github@19pouces.net>
This commit is contained in:
@@ -27,7 +27,16 @@ def _yes_no(value: bool) -> str:
|
|||||||
|
|
||||||
|
|
||||||
class AptSource(BaseModel):
|
class AptSource(BaseModel):
|
||||||
"""A single APT source, in the DEB822 format (see sources.list(5))."""
|
"""A single APT source, in the DEB822 format (see sources.list(5)).
|
||||||
|
|
||||||
|
Two ways to tell APT which key to trust this source with:
|
||||||
|
- `signed_by`: the ASCII-armored (.asc) *content* of a GPG public key.
|
||||||
|
The `apt` module dearmors it into a dedicated keyring and writes that
|
||||||
|
keyring's path as the rendered `Signed-By` field.
|
||||||
|
- `signed_by_file`: the *path* to a keyring already expected to exist on
|
||||||
|
the target system, written to the rendered `Signed-By` field verbatim.
|
||||||
|
If both are set, `signed_by` takes precedence.
|
||||||
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(populate_by_name=True)
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
|
||||||
@@ -42,6 +51,7 @@ class AptSource(BaseModel):
|
|||||||
enabled: bool = True
|
enabled: bool = True
|
||||||
trusted: bool | None = None
|
trusted: bool | None = None
|
||||||
signed_by: str | None = Field(default=None, alias="signed-by")
|
signed_by: str | None = Field(default=None, alias="signed-by")
|
||||||
|
signed_by_file: str | None = Field(default=None, alias="signed-by-file")
|
||||||
pdiffs: bool | None = None
|
pdiffs: bool | None = None
|
||||||
by_hash: bool | None = Field(default=None, alias="by-hash")
|
by_hash: bool | None = Field(default=None, alias="by-hash")
|
||||||
allow_insecure: bool | None = Field(default=None, alias="allow-insecure")
|
allow_insecure: bool | None = Field(default=None, alias="allow-insecure")
|
||||||
@@ -56,8 +66,15 @@ class AptSource(BaseModel):
|
|||||||
date_max_future: str | None = Field(default=None, alias="date-max-future")
|
date_max_future: str | None = Field(default=None, alias="date-max-future")
|
||||||
description: str | None = None
|
description: str | None = None
|
||||||
|
|
||||||
def to_deb822(self) -> str:
|
def to_deb822(self, signed_by_path: str | None = None) -> str:
|
||||||
"""Render this source as a DEB822 stanza (as used in /etc/apt/sources.list.d/*.sources)."""
|
"""Render this source as a DEB822 stanza (as used in
|
||||||
|
/etc/apt/sources.list.d/*.sources).
|
||||||
|
|
||||||
|
`signed_by_path` is the value actually written as the `Signed-By`
|
||||||
|
field, if given - resolved by the caller (the `apt` module) from
|
||||||
|
`self.signed_by`/`self.signed_by_file`, neither of which is ever
|
||||||
|
rendered here directly.
|
||||||
|
"""
|
||||||
fields = [
|
fields = [
|
||||||
("Types", " ".join(self.types)),
|
("Types", " ".join(self.types)),
|
||||||
("URIs", " ".join(self.uris)),
|
("URIs", " ".join(self.uris)),
|
||||||
@@ -71,8 +88,8 @@ class AptSource(BaseModel):
|
|||||||
fields.append(("Languages", " ".join(self.languages)))
|
fields.append(("Languages", " ".join(self.languages)))
|
||||||
if self.targets:
|
if self.targets:
|
||||||
fields.append(("Targets", " ".join(self.targets)))
|
fields.append(("Targets", " ".join(self.targets)))
|
||||||
if self.signed_by:
|
if signed_by_path:
|
||||||
fields.append(("Signed-By", self.signed_by))
|
fields.append(("Signed-By", signed_by_path))
|
||||||
if self.trusted is not None:
|
if self.trusted is not None:
|
||||||
fields.append(("Trusted", _yes_no(self.trusted)))
|
fields.append(("Trusted", _yes_no(self.trusted)))
|
||||||
if self.pdiffs is not None:
|
if self.pdiffs is not None:
|
||||||
@@ -108,12 +125,12 @@ class AptSource(BaseModel):
|
|||||||
|
|
||||||
class AptConfig(BaseModel):
|
class AptConfig(BaseModel):
|
||||||
sources: list[AptSource] = Field(default_factory=list)
|
sources: list[AptSource] = Field(default_factory=list)
|
||||||
keys: list[NamedContent] = Field(default_factory=list)
|
|
||||||
|
|
||||||
|
|
||||||
class ApkRepository(BaseModel):
|
class ApkRepository(BaseModel):
|
||||||
"""An Alpine APK repository (see /etc/apk/repositories), e.g. a URL ending in /main or /community."""
|
"""An Alpine APK repository (see /etc/apk/repositories), e.g. a URL ending in /main or /community."""
|
||||||
|
|
||||||
|
name: str
|
||||||
url: str
|
url: str
|
||||||
key: str | None = None
|
key: str | None = None
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class ApkModule(Module):
|
|||||||
lines = []
|
lines = []
|
||||||
for repository in context.config.configurations.apk.repositories:
|
for repository in context.config.configurations.apk.repositories:
|
||||||
lines.append(repository.url)
|
lines.append(repository.url)
|
||||||
logger.info("added apk repository %s", repository.url)
|
logger.info("added apk repository %s", repository.name)
|
||||||
if repository.key:
|
if repository.key:
|
||||||
filename = _key_filename(repository.key.encode("utf-8"))
|
filename = _key_filename(repository.key.encode("utf-8"))
|
||||||
context.write_file(
|
context.write_file(
|
||||||
|
|||||||
@@ -5,13 +5,29 @@ one-line `.list` files or DEB822 `.sources` files, whether at the top level
|
|||||||
or under `sources.list.d/`) is copied into `<dst>/etc/apt`, disabled so it no
|
or under `sources.list.d/`) is copied into `<dst>/etc/apt`, disabled so it no
|
||||||
longer takes effect, and the sources declared in the configuration are added
|
longer takes effect, and the sources declared in the configuration are added
|
||||||
as new, enabled `.sources` files.
|
as new, enabled `.sources` files.
|
||||||
|
|
||||||
|
Each source can be trusted two ways: `signed_by` holds the ASCII-armored
|
||||||
|
content of a GPG public key, which is converted with `gpg --dearmor` into a
|
||||||
|
binary keyring dedicated to this one source, written to
|
||||||
|
`/etc/apt/keyrings/<name>.gpg` - it is that path, not the key's own content,
|
||||||
|
that ends up in the rendered `.sources` file's `Signed-By` field.
|
||||||
|
`signed_by_file` instead names a keyring already expected to exist on the
|
||||||
|
target system, used verbatim as `Signed-By` (no file is written for it); if
|
||||||
|
both are given, `signed_by` takes precedence. `gpg` is only used here for
|
||||||
|
the armored-to-binary format conversion; the actual signature verification
|
||||||
|
of the repository itself is left to APT when it fetches the repository's
|
||||||
|
Release/InRelease file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from id_configurator.config import AptSource
|
||||||
from id_configurator.context import Context
|
from id_configurator.context import Context
|
||||||
from id_configurator.modules.base import Module, register
|
from id_configurator.modules.base import Module, register
|
||||||
|
|
||||||
@@ -43,6 +59,28 @@ def _disable_deb822_content(content: str) -> str:
|
|||||||
return "\n\n".join(disabled_stanzas) + "\n"
|
return "\n\n".join(disabled_stanzas) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def _dearmor_gpg_key(name: str, content: bytes) -> bytes:
|
||||||
|
"""Convert an ASCII-armored GPG public key into apt's binary keyring format."""
|
||||||
|
with tempfile.TemporaryDirectory() as gnupg_home:
|
||||||
|
path = shutil.which("gpg")
|
||||||
|
if path is not None:
|
||||||
|
result = subprocess.run(
|
||||||
|
[path, "--homedir", gnupg_home, "--batch", "--dearmor"],
|
||||||
|
input=content,
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise RuntimeError(
|
||||||
|
"gpg is not available; the GNUPG package must be installed in the environment running id-configurator"
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"invalid GPG key for apt source {name}: {result.stderr.decode('utf-8', 'replace')}"
|
||||||
|
)
|
||||||
|
return result.stdout
|
||||||
|
|
||||||
|
|
||||||
@register
|
@register
|
||||||
class AptModule(Module):
|
class AptModule(Module):
|
||||||
name = "apt"
|
name = "apt"
|
||||||
@@ -59,7 +97,6 @@ class AptModule(Module):
|
|||||||
|
|
||||||
def apply(self, context: Context) -> None:
|
def apply(self, context: Context) -> None:
|
||||||
self._copy_existing_disabled(context)
|
self._copy_existing_disabled(context)
|
||||||
self._write_configured_keys(context)
|
|
||||||
self._write_configured_sources(context)
|
self._write_configured_sources(context)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -82,19 +119,22 @@ class AptModule(Module):
|
|||||||
)
|
)
|
||||||
logger.info("disabled existing apt source %s", entry.name)
|
logger.info("disabled existing apt source %s", entry.name)
|
||||||
|
|
||||||
@staticmethod
|
def _resolve_signed_by(self, context: Context, source: AptSource) -> str | None:
|
||||||
def _write_configured_keys(context: Context) -> None:
|
if source.signed_by:
|
||||||
for key in context.config.configurations.apt.keys:
|
keyring = _dearmor_gpg_key(source.name, source.signed_by.encode("utf-8"))
|
||||||
context.write_file(
|
relative = f"{_KEYRINGS_DIR}/{source.name}.gpg"
|
||||||
f"{_KEYRINGS_DIR}/{key.name}.asc", key.content.encode("utf-8")
|
context.write_file(relative, keyring)
|
||||||
)
|
logger.info("installed apt keyring for source %s", source.name)
|
||||||
logger.info("installed apt keyring %s", key.name)
|
return f"/{relative}"
|
||||||
|
if source.signed_by_file:
|
||||||
|
return source.signed_by_file
|
||||||
|
return None
|
||||||
|
|
||||||
@staticmethod
|
def _write_configured_sources(self, context: Context) -> None:
|
||||||
def _write_configured_sources(context: Context) -> None:
|
|
||||||
for source in context.config.configurations.apt.sources:
|
for source in context.config.configurations.apt.sources:
|
||||||
|
signed_by_path = self._resolve_signed_by(context, source)
|
||||||
context.write_file(
|
context.write_file(
|
||||||
f"{_SOURCES_LIST_D}/{source.name}.sources",
|
f"{_SOURCES_LIST_D}/{source.name}.sources",
|
||||||
source.to_deb822().encode("utf-8"),
|
source.to_deb822(signed_by_path).encode("utf-8"),
|
||||||
)
|
)
|
||||||
logger.info("added apt source %s", source.name)
|
logger.info("added apt source %s", source.name)
|
||||||
|
|||||||
+13
-21
@@ -123,7 +123,18 @@ options:
|
|||||||
description: DEB822 "Trusted" field.
|
description: DEB822 "Trusted" field.
|
||||||
signed_by:
|
signed_by:
|
||||||
type: str
|
type: str
|
||||||
description: DEB822 "Signed-By" field.
|
description:
|
||||||
|
- ASCII-armored content of a GPG public key to trust this
|
||||||
|
source with. The `apt` module dearmors it into a dedicated
|
||||||
|
keyring and writes that keyring's path as the rendered
|
||||||
|
Signed-By field. Takes precedence over signed_by_file if
|
||||||
|
both are set.
|
||||||
|
signed_by_file:
|
||||||
|
type: str
|
||||||
|
description:
|
||||||
|
- Path to a keyring already expected to exist on the target
|
||||||
|
system, used verbatim as the rendered Signed-By field.
|
||||||
|
Ignored if signed_by is also set.
|
||||||
pdiffs:
|
pdiffs:
|
||||||
type: bool
|
type: bool
|
||||||
description: DEB822 "PDiffs" field.
|
description: DEB822 "PDiffs" field.
|
||||||
@@ -157,20 +168,6 @@ options:
|
|||||||
description:
|
description:
|
||||||
type: str
|
type: str
|
||||||
description: DEB822 "Description" field.
|
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:
|
apk:
|
||||||
type: dict
|
type: dict
|
||||||
default: {}
|
default: {}
|
||||||
@@ -483,6 +480,7 @@ ARGUMENT_SPEC = {
|
|||||||
"enabled": {"type": "bool", "default": True},
|
"enabled": {"type": "bool", "default": True},
|
||||||
"trusted": {"type": "bool"},
|
"trusted": {"type": "bool"},
|
||||||
"signed_by": {"type": "str"},
|
"signed_by": {"type": "str"},
|
||||||
|
"signed_by_file": {"type": "str"},
|
||||||
"pdiffs": {"type": "bool"},
|
"pdiffs": {"type": "bool"},
|
||||||
"by_hash": {"type": "bool"},
|
"by_hash": {"type": "bool"},
|
||||||
"allow_insecure": {"type": "bool"},
|
"allow_insecure": {"type": "bool"},
|
||||||
@@ -496,12 +494,6 @@ ARGUMENT_SPEC = {
|
|||||||
"description": {"type": "str"},
|
"description": {"type": "str"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"keys": {
|
|
||||||
"type": "list",
|
|
||||||
"elements": "dict",
|
|
||||||
"default": [],
|
|
||||||
"options": dict(_NAMED_CONTENT_OPTIONS),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"apk": {
|
"apk": {
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ configurations:
|
|||||||
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
||||||
apt:
|
apt:
|
||||||
sources: []
|
sources: []
|
||||||
keys: []
|
|
||||||
apk:
|
apk:
|
||||||
repositories:
|
repositories:
|
||||||
- url: https://host.docker.internal:{{ 'APK_REPO_PORT' | env }}/
|
- name: demo
|
||||||
|
url: https://host.docker.internal:{{ 'APK_REPO_PORT' | env }}/
|
||||||
key: |
|
key: |
|
||||||
{{ 'dockerfiles/repos/040d0af5f97a0a44.rsa.pub' | file | indent(8, first=true) }}
|
{{ 'dockerfiles/repos/040d0af5f97a0a44.rsa.pub' | file | indent(8, first=true) }}
|
||||||
yum:
|
yum:
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ configurations:
|
|||||||
uris:
|
uris:
|
||||||
- https://host.docker.internal:{{ 'APT_REPO_PORT' | env }}/
|
- https://host.docker.internal:{{ 'APT_REPO_PORT' | env }}/
|
||||||
suites: ["./"]
|
suites: ["./"]
|
||||||
trusted: true
|
signed-by: |
|
||||||
keys: []
|
{{ 'dockerfiles/repos/apt-signing-key.pub.asc' | file | indent(8, first=true) }}
|
||||||
apk:
|
apk:
|
||||||
repositories: []
|
repositories: []
|
||||||
yum:
|
yum:
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ configurations:
|
|||||||
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
||||||
apt:
|
apt:
|
||||||
sources: []
|
sources: []
|
||||||
keys: []
|
|
||||||
apk:
|
apk:
|
||||||
repositories: []
|
repositories: []
|
||||||
yum:
|
yum:
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ configurations:
|
|||||||
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
||||||
apt:
|
apt:
|
||||||
sources: []
|
sources: []
|
||||||
keys: []
|
|
||||||
apk:
|
apk:
|
||||||
repositories: []
|
repositories: []
|
||||||
yum:
|
yum:
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ configurations:
|
|||||||
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
||||||
apt:
|
apt:
|
||||||
sources: []
|
sources: []
|
||||||
keys: []
|
|
||||||
apk:
|
apk:
|
||||||
repositories: []
|
repositories: []
|
||||||
yum:
|
yum:
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ configurations:
|
|||||||
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
||||||
apt:
|
apt:
|
||||||
sources: []
|
sources: []
|
||||||
keys: []
|
|
||||||
apk:
|
apk:
|
||||||
repositories: []
|
repositories: []
|
||||||
yum:
|
yum:
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ configurations:
|
|||||||
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
||||||
apt:
|
apt:
|
||||||
sources: []
|
sources: []
|
||||||
keys: []
|
|
||||||
apk:
|
apk:
|
||||||
repositories: []
|
repositories: []
|
||||||
yum:
|
yum:
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ configurations:
|
|||||||
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
||||||
apt:
|
apt:
|
||||||
sources: []
|
sources: []
|
||||||
keys: []
|
|
||||||
apk:
|
apk:
|
||||||
repositories: []
|
repositories: []
|
||||||
yum:
|
yum:
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ configurations:
|
|||||||
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
||||||
apt:
|
apt:
|
||||||
sources: []
|
sources: []
|
||||||
keys: []
|
|
||||||
apk:
|
apk:
|
||||||
repositories: []
|
repositories: []
|
||||||
yum:
|
yum:
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ configurations:
|
|||||||
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
||||||
apt:
|
apt:
|
||||||
sources: []
|
sources: []
|
||||||
keys: []
|
|
||||||
apk:
|
apk:
|
||||||
repositories: []
|
repositories: []
|
||||||
yum:
|
yum:
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ configurations:
|
|||||||
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
||||||
apt:
|
apt:
|
||||||
sources: []
|
sources: []
|
||||||
keys: []
|
|
||||||
apk:
|
apk:
|
||||||
repositories: []
|
repositories: []
|
||||||
yum:
|
yum:
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ configurations:
|
|||||||
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
|
||||||
apt:
|
apt:
|
||||||
sources: []
|
sources: []
|
||||||
keys: []
|
|
||||||
apk:
|
apk:
|
||||||
repositories: []
|
repositories: []
|
||||||
yum:
|
yum:
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
FROM debian:trixie-slim AS target
|
FROM debian:trixie-slim AS target
|
||||||
FROM interdiode/id-configurator:latest AS configuration
|
FROM interdiode/id-configurator:latest AS configuration
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends gnupg \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
COPY --from=target / /src/
|
COPY --from=target / /src/
|
||||||
COPY --from=generated-config Apt.yaml /config.yaml
|
COPY --from=generated-config Apt.yaml /config.yaml
|
||||||
RUN id-configurator apply --config /config.yaml --src /src --dst /dst
|
RUN id-configurator apply --config /config.yaml --src /src --dst /dst
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
# Minimal flat-format APT repository serving one hand-built .deb package over
|
# Minimal flat-format APT repository serving one hand-built .deb package over
|
||||||
# TLS, used by tox-docker to exercise the `apt` module integration test
|
# TLS, used by tox-docker to exercise the `apt` module integration test
|
||||||
# (tests/configs/Apt.yaml.tmpl, tests/dockerfiles/Apt.Dockerfile).
|
# (tests/configs/Apt.yaml.j2, tests/dockerfiles/Apt.Dockerfile). Its
|
||||||
|
# InRelease file is genuinely signed with the dedicated key pair
|
||||||
|
# apt-signing-key.key.asc / apt-signing-key.pub.asc, whose public half is
|
||||||
|
# what tests/configs/Apt.yaml.j2 embeds as the configured source's
|
||||||
|
# `signed-by`: the `apt` module dearmors it into a keyring, and APT itself
|
||||||
|
# verifies the InRelease signature against that keyring on `apt-get update`
|
||||||
|
# - no `Trusted: yes` bypass involved.
|
||||||
#
|
#
|
||||||
# Built by tox-docker, whose build context is this directory (not the repo
|
# Built by tox-docker, whose build context is this directory (not the repo
|
||||||
# root) - every COPY source below must live under tests/dockerfiles/repos/.
|
# root) - every COPY source below must live under tests/dockerfiles/repos/.
|
||||||
FROM debian:trixie-slim AS packagebuild
|
FROM debian:trixie-slim AS packagebuild
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends dpkg-dev \
|
&& apt-get install -y --no-install-recommends dpkg-dev apt-utils gnupg \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
pkg=idconfiguratortestpkg; \
|
pkg=idconfiguratortestpkg; \
|
||||||
@@ -18,6 +24,13 @@ RUN set -eux; \
|
|||||||
WORKDIR /srv
|
WORKDIR /srv
|
||||||
RUN dpkg-scanpackages --multiversion . > Packages \
|
RUN dpkg-scanpackages --multiversion . > Packages \
|
||||||
&& gzip -9c Packages > Packages.gz
|
&& gzip -9c Packages > Packages.gz
|
||||||
|
COPY apt-signing-key.key.asc /tmp/apt-signing-key.key.asc
|
||||||
|
RUN gnupg_home="$(mktemp -d)" \
|
||||||
|
&& gpg --homedir "$gnupg_home" --batch --import /tmp/apt-signing-key.key.asc \
|
||||||
|
&& key_id="$(gpg --homedir "$gnupg_home" --list-secret-keys --with-colons | awk -F: '/^sec/{print $5; exit}')" \
|
||||||
|
&& apt-ftparchive release . > Release \
|
||||||
|
&& gpg --homedir "$gnupg_home" --batch --yes --pinentry-mode loopback --default-key "$key_id" --clearsign -o InRelease Release \
|
||||||
|
&& rm -rf "$gnupg_home" /tmp/apt-signing-key.key.asc
|
||||||
|
|
||||||
FROM nginx:alpine
|
FROM nginx:alpine
|
||||||
COPY --from=packagebuild /srv /srv
|
COPY --from=packagebuild /srv /srv
|
||||||
|
|||||||
+2
-1
@@ -14,10 +14,11 @@ CONFIG_DATA = {
|
|||||||
"apk": {
|
"apk": {
|
||||||
"repositories": [
|
"repositories": [
|
||||||
{
|
{
|
||||||
|
"name": "main",
|
||||||
"url": "https://example.invalid/v3.19/main",
|
"url": "https://example.invalid/v3.19/main",
|
||||||
"key": KEY_BYTES,
|
"key": KEY_BYTES,
|
||||||
},
|
},
|
||||||
{"url": "https://example.invalid/v3.19/community"},
|
{"name": "community", "url": "https://example.invalid/v3.19/community"},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+140
-12
@@ -1,7 +1,15 @@
|
|||||||
|
import subprocess
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from id_configurator.config import Configuration
|
from id_configurator.config import Configuration
|
||||||
from id_configurator.context import Context
|
from id_configurator.context import Context
|
||||||
from id_configurator.modules.apt import AptModule
|
from id_configurator.modules.apt import AptModule
|
||||||
|
|
||||||
|
KEY_CONTENT = "fake-armored-gpg-key"
|
||||||
|
KEYRING_BYTES = b"fake-dearmored-keyring-bytes"
|
||||||
|
|
||||||
SOURCE_CONTENT = {
|
SOURCE_CONTENT = {
|
||||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||||
"organisation": "InterDiode",
|
"organisation": "InterDiode",
|
||||||
@@ -12,13 +20,7 @@ SOURCE_CONTENT = {
|
|||||||
"name": "interdiode",
|
"name": "interdiode",
|
||||||
"uris": ["http://example.invalid/debian"],
|
"uris": ["http://example.invalid/debian"],
|
||||||
"suites": ["stable"],
|
"suites": ["stable"],
|
||||||
"signed-by": "/etc/apt/keyrings/interdiode.asc",
|
"signed-by": KEY_CONTENT,
|
||||||
}
|
|
||||||
],
|
|
||||||
"keys": [
|
|
||||||
{
|
|
||||||
"name": "interdiode",
|
|
||||||
"content": "fake-key-bytes",
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -30,6 +32,36 @@ def make_config() -> Configuration:
|
|||||||
return Configuration.model_validate(SOURCE_CONTENT)
|
return Configuration.model_validate(SOURCE_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
def make_config_with(source_extra: dict) -> Configuration:
|
||||||
|
data = {
|
||||||
|
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||||
|
"organisation": "InterDiode",
|
||||||
|
"configurations": {
|
||||||
|
"apt": {
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"name": "interdiode",
|
||||||
|
"uris": ["http://example.invalid/debian"],
|
||||||
|
"suites": ["stable"],
|
||||||
|
**source_extra,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return Configuration.model_validate(data)
|
||||||
|
|
||||||
|
|
||||||
|
def _mock_gpg_dearmor_success(monkeypatch, output: bytes = KEYRING_BYTES) -> MagicMock:
|
||||||
|
mock_run = MagicMock(
|
||||||
|
return_value=subprocess.CompletedProcess(
|
||||||
|
args=[], returncode=0, stdout=output, stderr=b""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
monkeypatch.setattr("id_configurator.modules.apt.subprocess.run", mock_run)
|
||||||
|
return mock_run
|
||||||
|
|
||||||
|
|
||||||
def test_is_relevant_false_without_src(tmp_path) -> None:
|
def test_is_relevant_false_without_src(tmp_path) -> None:
|
||||||
context = Context(config=make_config(), dst=tmp_path / "dst", src=None)
|
context = Context(config=make_config(), dst=tmp_path / "dst", src=None)
|
||||||
assert AptModule().is_relevant(context) is False
|
assert AptModule().is_relevant(context) is False
|
||||||
@@ -49,7 +81,10 @@ def test_is_relevant_true_with_apt_dir(tmp_path) -> None:
|
|||||||
assert AptModule().is_relevant(context) is True
|
assert AptModule().is_relevant(context) is True
|
||||||
|
|
||||||
|
|
||||||
def test_apply_disables_existing_sources_and_adds_configured_ones(tmp_path) -> None:
|
def test_apply_disables_existing_sources_and_adds_configured_ones(
|
||||||
|
tmp_path, monkeypatch
|
||||||
|
) -> None:
|
||||||
|
mock_run = _mock_gpg_dearmor_success(monkeypatch)
|
||||||
src = tmp_path / "src"
|
src = tmp_path / "src"
|
||||||
apt_dir = src / "etc/apt"
|
apt_dir = src / "etc/apt"
|
||||||
sources_list_d = apt_dir / "sources.list.d"
|
sources_list_d = apt_dir / "sources.list.d"
|
||||||
@@ -98,19 +133,22 @@ def test_apply_disables_existing_sources_and_adds_configured_ones(tmp_path) -> N
|
|||||||
|
|
||||||
assert not (dst / "etc/apt/sources.list.d/ignored.txt").exists()
|
assert not (dst / "etc/apt/sources.list.d/ignored.txt").exists()
|
||||||
|
|
||||||
keyring = (dst / "etc/apt/keyrings/interdiode.asc").read_bytes()
|
keyring = (dst / "etc/apt/keyrings/interdiode.gpg").read_bytes()
|
||||||
assert keyring == b"fake-key-bytes"
|
assert keyring == KEYRING_BYTES
|
||||||
|
mock_run.assert_called_once()
|
||||||
|
assert mock_run.call_args.kwargs["input"] == KEY_CONTENT.encode("utf-8")
|
||||||
|
|
||||||
configured_source = (dst / "etc/apt/sources.list.d/interdiode.sources").read_text(
|
configured_source = (dst / "etc/apt/sources.list.d/interdiode.sources").read_text(
|
||||||
encoding="utf-8"
|
encoding="utf-8"
|
||||||
)
|
)
|
||||||
assert configured_source == (
|
assert configured_source == (
|
||||||
"Types: deb\nURIs: http://example.invalid/debian\nSuites: stable\n"
|
"Types: deb\nURIs: http://example.invalid/debian\nSuites: stable\n"
|
||||||
"Signed-By: /etc/apt/keyrings/interdiode.asc\nEnabled: yes\n"
|
"Signed-By: /etc/apt/keyrings/interdiode.gpg\nEnabled: yes\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_apply_without_existing_top_level_sources_list(tmp_path) -> None:
|
def test_apply_without_existing_top_level_sources_list(tmp_path, monkeypatch) -> None:
|
||||||
|
_mock_gpg_dearmor_success(monkeypatch)
|
||||||
src = tmp_path / "src"
|
src = tmp_path / "src"
|
||||||
(src / "etc/apt").mkdir(parents=True)
|
(src / "etc/apt").mkdir(parents=True)
|
||||||
dst = tmp_path / "dst"
|
dst = tmp_path / "dst"
|
||||||
@@ -120,3 +158,93 @@ def test_apply_without_existing_top_level_sources_list(tmp_path) -> None:
|
|||||||
|
|
||||||
assert not (dst / "etc/apt/sources.list").exists()
|
assert not (dst / "etc/apt/sources.list").exists()
|
||||||
assert (dst / "etc/apt/sources.list.d/interdiode.sources").exists()
|
assert (dst / "etc/apt/sources.list.d/interdiode.sources").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_uses_signed_by_file_when_no_key_content(tmp_path, monkeypatch) -> None:
|
||||||
|
mock_run = _mock_gpg_dearmor_success(monkeypatch)
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
context = Context(
|
||||||
|
config=make_config_with({"signed-by-file": "/etc/apt/keyrings/existing.gpg"}),
|
||||||
|
dst=dst,
|
||||||
|
src=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
AptModule().apply(context)
|
||||||
|
|
||||||
|
mock_run.assert_not_called()
|
||||||
|
assert not (dst / "etc/apt/keyrings").exists()
|
||||||
|
configured_source = (dst / "etc/apt/sources.list.d/interdiode.sources").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
assert configured_source == (
|
||||||
|
"Types: deb\nURIs: http://example.invalid/debian\nSuites: stable\n"
|
||||||
|
"Signed-By: /etc/apt/keyrings/existing.gpg\nEnabled: yes\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_signed_by_takes_precedence_over_signed_by_file(
|
||||||
|
tmp_path, monkeypatch
|
||||||
|
) -> None:
|
||||||
|
mock_run = _mock_gpg_dearmor_success(monkeypatch)
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
context = Context(
|
||||||
|
config=make_config_with(
|
||||||
|
{
|
||||||
|
"signed-by": KEY_CONTENT,
|
||||||
|
"signed-by-file": "/etc/apt/keyrings/existing.gpg",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
dst=dst,
|
||||||
|
src=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
AptModule().apply(context)
|
||||||
|
|
||||||
|
mock_run.assert_called_once()
|
||||||
|
assert (dst / "etc/apt/keyrings/interdiode.gpg").read_bytes() == KEYRING_BYTES
|
||||||
|
configured_source = (dst / "etc/apt/sources.list.d/interdiode.sources").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
assert "Signed-By: /etc/apt/keyrings/interdiode.gpg\n" in configured_source
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_without_signed_by_writes_no_keyring_or_field(
|
||||||
|
tmp_path, monkeypatch
|
||||||
|
) -> None:
|
||||||
|
mock_run = _mock_gpg_dearmor_success(monkeypatch)
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
context = Context(config=make_config_with({}), dst=dst, src=None)
|
||||||
|
|
||||||
|
AptModule().apply(context)
|
||||||
|
|
||||||
|
mock_run.assert_not_called()
|
||||||
|
assert not (dst / "etc/apt/keyrings").exists()
|
||||||
|
configured_source = (dst / "etc/apt/sources.list.d/interdiode.sources").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
assert configured_source == (
|
||||||
|
"Types: deb\nURIs: http://example.invalid/debian\nSuites: stable\nEnabled: yes\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_signed_by_raises_when_gpg_not_on_path(tmp_path, monkeypatch) -> None:
|
||||||
|
monkeypatch.setattr("id_configurator.modules.apt.shutil.which", lambda _: None)
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
context = Context(config=make_config(), dst=dst, src=None)
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="gpg is not available"):
|
||||||
|
AptModule().apply(context)
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_signed_by_raises_on_invalid_key(tmp_path, monkeypatch) -> None:
|
||||||
|
mock_run = MagicMock(
|
||||||
|
return_value=subprocess.CompletedProcess(
|
||||||
|
args=[], returncode=2, stdout=b"", stderr=b"gpg: no valid data found"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
monkeypatch.setattr("id_configurator.modules.apt.subprocess.run", mock_run)
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
context = Context(config=make_config(), dst=dst, src=None)
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="invalid GPG key for apt source interdiode"):
|
||||||
|
AptModule().apply(context)
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ FULL_CONFIG = {
|
|||||||
"signed-by": "/etc/apt/keyrings/interdiode.asc",
|
"signed-by": "/etc/apt/keyrings/interdiode.asc",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"keys": [{"name": "interdiode", "content": "ZmFrZQ=="}],
|
|
||||||
},
|
},
|
||||||
"yum": {
|
"yum": {
|
||||||
"repositories": [
|
"repositories": [
|
||||||
@@ -76,7 +75,6 @@ def test_parse_configuration_from_json() -> None:
|
|||||||
configuration.configurations.apt.sources[0].signed_by
|
configuration.configurations.apt.sources[0].signed_by
|
||||||
== "/etc/apt/keyrings/interdiode.asc"
|
== "/etc/apt/keyrings/interdiode.asc"
|
||||||
)
|
)
|
||||||
assert configuration.configurations.apt.keys[0].name == "interdiode"
|
|
||||||
assert configuration.configurations.yum.keys[0].name == "interdiode"
|
assert configuration.configurations.yum.keys[0].name == "interdiode"
|
||||||
|
|
||||||
|
|
||||||
@@ -191,7 +189,6 @@ def test_apt_source_to_deb822_full() -> None:
|
|||||||
enabled=False,
|
enabled=False,
|
||||||
trusted=True,
|
trusted=True,
|
||||||
**{
|
**{
|
||||||
"signed-by": "/etc/apt/keyrings/interdiode.asc",
|
|
||||||
"by-hash": True,
|
"by-hash": True,
|
||||||
"allow-insecure": False,
|
"allow-insecure": False,
|
||||||
"allow-weak": False,
|
"allow-weak": False,
|
||||||
@@ -205,7 +202,7 @@ def test_apt_source_to_deb822_full() -> None:
|
|||||||
pdiffs=True,
|
pdiffs=True,
|
||||||
description="InterDiode APT repository",
|
description="InterDiode APT repository",
|
||||||
)
|
)
|
||||||
rendered = source.to_deb822()
|
rendered = source.to_deb822(signed_by_path="/etc/apt/keyrings/interdiode.asc")
|
||||||
assert rendered.splitlines() == [
|
assert rendered.splitlines() == [
|
||||||
"Types: deb deb-src",
|
"Types: deb deb-src",
|
||||||
"URIs: http://example.invalid/debian",
|
"URIs: http://example.invalid/debian",
|
||||||
|
|||||||
Reference in New Issue
Block a user