mirror of
https://github.com/d9pouces/id_configurator.git
synced 2026-11-09 01:47:21 +01:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
1e9e87b11c
|
|||
|
dc0f8b7629
|
|||
|
abbd424f14
|
|||
|
ebea3071bc
|
|||
|
8598f323d9
|
|||
|
665eb215bd
|
|||
|
98b773445f
|
|||
|
6813c86fba
|
|||
|
8e182f6615
|
|||
|
352173a85c
|
|||
|
d705aa970d
|
|||
|
bf56cdfcd2
|
|||
|
c498d42da0
|
@@ -22,8 +22,12 @@ jobs:
|
|||||||
- run: curl -O -L "https://github.com/sigstore/cosign/releases/latest/download/cosign_3.1.3_amd64.deb"
|
- run: curl -O -L "https://github.com/sigstore/cosign/releases/latest/download/cosign_3.1.3_amd64.deb"
|
||||||
- run: sudo dpkg -i cosign_3.1.3_amd64.deb
|
- run: sudo dpkg -i cosign_3.1.3_amd64.deb
|
||||||
- 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
|
||||||
|
- run: tox -e module-rust
|
||||||
- run: tox -e packaging-amd64,py313,py314,coverage
|
- run: tox -e packaging-amd64,py313,py314,coverage
|
||||||
- run: tox -m end-to-end
|
- run: tox -m end-to-end
|
||||||
|
- run: docker builder prune --all --force
|
||||||
|
- run: docker system prune --all --force
|
||||||
- uses: docker/setup-buildx-action@v3
|
- uses: docker/setup-buildx-action@v3
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ jobs:
|
|||||||
- 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 -m end-to-end
|
- run: tox -m end-to-end
|
||||||
|
- run: docker builder prune --all --force
|
||||||
|
- run: docker system prune --all --force
|
||||||
- uses: docker/setup-buildx-action@v3
|
- uses: docker/setup-buildx-action@v3
|
||||||
- run: tox -e publish
|
- run: tox -e publish
|
||||||
strategy:
|
strategy:
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ repos:
|
|||||||
rev: 1.8.6
|
rev: 1.8.6
|
||||||
hooks:
|
hooks:
|
||||||
- id: bandit
|
- id: bandit
|
||||||
args: [-s, 'B101,B404,B405,B506,B703,B308,B603']
|
args: [-s, 'B101,B105,B404,B405,B506,B703,B308,B603,B604']
|
||||||
exclude: ^(id_tests/.*|npm/node_modules/.*|tools/.*|doc/.*|interdiode/static/vendor/.*)
|
exclude: ^(id_tests/.*|npm/node_modules/.*|tools/.*|doc/.*|interdiode/static/vendor/.*)
|
||||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
- repo: https://github.com/shellcheck-py/shellcheck-py
|
||||||
rev: v0.11.0.1
|
rev: v0.11.0.1
|
||||||
|
|||||||
@@ -23,6 +23,14 @@ deployed to the target device.
|
|||||||
`--config-url-insecure` disables certificate (and hostname) verification entirely instead, overriding
|
`--config-url-insecure` disables certificate (and hostname) verification entirely instead, overriding
|
||||||
`--config-url-cacert-content` - **insecure**, only meant for a first bootstrap against a server whose
|
`--config-url-cacert-content` - **insecure**, only meant for a first bootstrap against a server whose
|
||||||
certificate isn't trustable, or trusted, yet by any other means.
|
certificate isn't trustable, or trusted, yet by any other means.
|
||||||
|
- `--user` (repeatable) names a user to also prepare *per-user* configuration for, on top of the usual
|
||||||
|
system-wide files, for the modules that have one: `go`, `nuget`, `php` (Composer), `python` (poetry) and
|
||||||
|
`rust`. The user's home directory is taken from `--src`'s own `/etc/passwd` entry when there is one,
|
||||||
|
otherwise assumed to be `/home/<user>` (`/root` for `root`).
|
||||||
|
- `--auto-users` does the same for every *regular* (non-system) user found in `--src`'s `/etc/passwd`: one with
|
||||||
|
a login shell (not `nologin`, `sync` or `false`) and a UID at or above `UID_MIN` (read from `--src`'s
|
||||||
|
`/etc/login.defs`, 1000 by default) - except `root`, always included regardless of its UID. It combines
|
||||||
|
freely with one or more `--user`.
|
||||||
- On `apply`, every module inspects the configuration and the source filesystem and decides for itself whether it has something to
|
- On `apply`, every module inspects the configuration and the source filesystem and decides for itself whether it has something to
|
||||||
do; only relevant modules touch the destination directory. For example, the configured Alpine repositories are only written if
|
do; only relevant modules touch the destination directory. For example, the configured Alpine repositories are only written if
|
||||||
`--src` can be identified as an Alpine filesystem (i.e. it has an `/etc/apk` directory); and a source cloning Debian's official
|
`--src` can be identified as an Alpine filesystem (i.e. it has an `/etc/apk` directory); and a source cloning Debian's official
|
||||||
@@ -95,6 +103,12 @@ id-configurator apply \
|
|||||||
--config-url-cacert-content "$(cat internal-ca.pem)" \
|
--config-url-cacert-content "$(cat internal-ca.pem)" \
|
||||||
--dst ./output
|
--dst ./output
|
||||||
|
|
||||||
|
# Also prepare per-user configuration (go, nuget, php, python, rust) for specific users
|
||||||
|
id-configurator apply --config config.yaml --dst ./output --user alice --user bob
|
||||||
|
|
||||||
|
# Same, but auto-detect every regular (non-system) user from --src's /etc/passwd
|
||||||
|
id-configurator apply --config config.yaml --src /path/to/rootfs --dst ./output --auto-users
|
||||||
|
|
||||||
# Validate a configuration without applying it
|
# Validate a configuration without applying it
|
||||||
id-configurator validate --config config.yaml
|
id-configurator validate --config config.yaml
|
||||||
|
|
||||||
@@ -145,6 +159,7 @@ configurations:
|
|||||||
- name: <string>
|
- name: <string>
|
||||||
url: <string>
|
url: <string>
|
||||||
key: <RSA public key, optional>
|
key: <RSA public key, optional>
|
||||||
|
key_filename: <string, optional> # name of the key file under /etc/apk/keys/, if not the derived default
|
||||||
url_to_replace: <fnmatch pattern, optional>
|
url_to_replace: <fnmatch pattern, optional>
|
||||||
yum:
|
yum:
|
||||||
repositories:
|
repositories:
|
||||||
@@ -175,6 +190,7 @@ configurations:
|
|||||||
repositories:
|
repositories:
|
||||||
- name: <string>
|
- name: <string>
|
||||||
url: <string>
|
url: <string>
|
||||||
|
is_sparse: <bool, optional, default false> # declares the registry's index as sparse+<url>
|
||||||
go:
|
go:
|
||||||
repositories:
|
repositories:
|
||||||
- name: <string>
|
- name: <string>
|
||||||
@@ -200,6 +216,22 @@ replaces Debian's default mirror if (and only if) it's actually there, without d
|
|||||||
given machine might already have. Leaving the field unset never disables anything - the configured repository
|
given machine might already have. Leaving the field unset never disables anything - the configured repository
|
||||||
is simply added alongside whatever is already present.
|
is simply added alongside whatever is already present.
|
||||||
|
|
||||||
|
### Non-HTTPS repositories
|
||||||
|
|
||||||
|
A repository URL doesn't have to be HTTPS - several modules automatically adjust the tool's configuration so
|
||||||
|
the plain-HTTP repository is actually usable, instead of silently producing a config the package manager
|
||||||
|
would refuse at run time:
|
||||||
|
|
||||||
|
- `python`: pip gets the repository's host added to `trusted-host`.
|
||||||
|
- `php`: Composer gets `secure-http` disabled, and `packagist.org` is disabled alongside it (an HTTP source
|
||||||
|
implies a private, non-internet setup where the public registry wouldn't be reachable anyway).
|
||||||
|
- `nuget`: the source gets `allowInsecureConnections="true"`.
|
||||||
|
- `java`: SBT's resolver gets `allowInsecureProtocol` appended. **Leiningen is the one exception**: its
|
||||||
|
standalone launcher refuses any non-HTTPS repository unconditionally, with no configuration able to change
|
||||||
|
that - a repository configured here simply won't be usable from Leiningen specifically.
|
||||||
|
- `go`, `rust`, `apt`, `apk`, `yum`: no adjustment is needed - these already work the same regardless of
|
||||||
|
scheme.
|
||||||
|
|
||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
| Module | Applies to |
|
| Module | Applies to |
|
||||||
@@ -217,6 +249,9 @@ is simply added alongside whatever is already present.
|
|||||||
| `ruby` | Points RubyGems at the configured sources, replacing rubygems.org. |
|
| `ruby` | Points RubyGems at the configured sources, replacing rubygems.org. |
|
||||||
| `nuget` | Points NuGet at the configured package sources, replacing nuget.org. |
|
| `nuget` | Points NuGet at the configured package sources, replacing nuget.org. |
|
||||||
|
|
||||||
|
`go`, `nuget`, `php`, `python` and `rust` additionally honor `--user`/`--auto-users` (see "How it works" above),
|
||||||
|
writing the same per-user configuration under each such user's home directory.
|
||||||
|
|
||||||
This tool's own modular structure is meant to be extended: see `CLAUDE.md` for the module interface and how to add one.
|
This tool's own modular structure is meant to be extended: see `CLAUDE.md` for the module interface and how to add one.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|||||||
+25
-1
@@ -83,6 +83,26 @@ DstOption = Annotated[
|
|||||||
help="Destination directory receiving the modified files.",
|
help="Destination directory receiving the modified files.",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
UserOption = Annotated[
|
||||||
|
list[str],
|
||||||
|
typer.Option(
|
||||||
|
"--user",
|
||||||
|
help="A user to prepare per-user configuration for (may be repeated).",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
AutoUsersOption = Annotated[
|
||||||
|
bool,
|
||||||
|
typer.Option(
|
||||||
|
"--auto-users",
|
||||||
|
help=(
|
||||||
|
"Also prepare per-user configuration for every regular "
|
||||||
|
"(non-system) user found in <src>/etc/passwd: one with a login "
|
||||||
|
"shell (not nologin, sync or false) and a UID at or above "
|
||||||
|
"UID_MIN (from <src>/etc/login.defs, 1000 by default) - root is "
|
||||||
|
"always included regardless of its UID."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
VerboseOption = Annotated[
|
VerboseOption = Annotated[
|
||||||
bool, typer.Option("--verbose", "-v", help="Enable verbose (debug) logging.")
|
bool, typer.Option("--verbose", "-v", help="Enable verbose (debug) logging.")
|
||||||
]
|
]
|
||||||
@@ -121,6 +141,8 @@ def apply(
|
|||||||
config_url_cacert_content: ConfigUrlCacertContentOption = None,
|
config_url_cacert_content: ConfigUrlCacertContentOption = None,
|
||||||
config_url_insecure: ConfigUrlInsecureOption = False,
|
config_url_insecure: ConfigUrlInsecureOption = False,
|
||||||
src: SrcOption = None,
|
src: SrcOption = None,
|
||||||
|
user: UserOption = [], # noqa: B006 (Typer needs a concrete default to detect --user as repeatable)
|
||||||
|
auto_users: AutoUsersOption = False,
|
||||||
verbose: VerboseOption = False,
|
verbose: VerboseOption = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Apply a configuration to the destination directory."""
|
"""Apply a configuration to the destination directory."""
|
||||||
@@ -129,7 +151,9 @@ def apply(
|
|||||||
config, config_env, config_url, config_url_cacert_content, config_url_insecure
|
config, config_env, config_url, config_url_cacert_content, config_url_insecure
|
||||||
)
|
)
|
||||||
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, users=user, auto_users=auto_users
|
||||||
|
)
|
||||||
for module_cls in all_modules():
|
for module_cls in all_modules():
|
||||||
module = module_cls()
|
module = module_cls()
|
||||||
if module.is_relevant(context):
|
if module.is_relevant(context):
|
||||||
|
|||||||
@@ -239,11 +239,16 @@ class NpmConfig(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class RustRepository(BaseModel):
|
class RustRepository(BaseModel):
|
||||||
"""A cargo registry (see the "source" tables in .cargo/config.toml),
|
"""A cargo registry (see the "source" tables in .cargo/config.toml).
|
||||||
served over the sparse HTTP protocol."""
|
|
||||||
|
`is_sparse`, if true, marks the registry as served over the sparse
|
||||||
|
HTTP protocol (its index is then declared as `sparse+<url>`) rather
|
||||||
|
than a git-based index.
|
||||||
|
"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
url: str
|
url: str
|
||||||
|
is_sparse: bool = False
|
||||||
|
|
||||||
|
|
||||||
class RustConfig(BaseModel):
|
class RustConfig(BaseModel):
|
||||||
|
|||||||
@@ -7,18 +7,71 @@ filesystem used to detect the existing configuration.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from id_configurator.config import Configuration
|
from id_configurator.config import Configuration
|
||||||
|
from id_configurator.users import (
|
||||||
|
DEFAULT_UID_MIN,
|
||||||
|
PasswdEntry,
|
||||||
|
parse_login_defs_uid_min,
|
||||||
|
parse_passwd,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Context:
|
class Context:
|
||||||
def __init__(
|
def __init__(
|
||||||
self, config: Configuration, dst: Path, src: Path | None = None
|
self,
|
||||||
|
config: Configuration,
|
||||||
|
dst: Path,
|
||||||
|
src: Path | None = None,
|
||||||
|
users: Sequence[str] | None = None,
|
||||||
|
auto_users: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.config = config
|
self.config = config
|
||||||
self.dst = dst.resolve()
|
self.dst = dst.resolve()
|
||||||
self.src = src.resolve() if src is not None else None
|
self.src = src.resolve() if src is not None else None
|
||||||
|
self._passwd_entries = self._load_passwd_entries()
|
||||||
|
self._uid_min = self._load_uid_min()
|
||||||
|
self.users = self._resolve_users(users or [], auto_users)
|
||||||
|
|
||||||
|
def _load_passwd_entries(self) -> dict[str, PasswdEntry]:
|
||||||
|
content = self.read_src_file("etc/passwd")
|
||||||
|
if content is None:
|
||||||
|
return {}
|
||||||
|
return {entry.name: entry for entry in parse_passwd(content.decode("utf-8"))}
|
||||||
|
|
||||||
|
def _load_uid_min(self) -> int:
|
||||||
|
content = self.read_src_file("etc/login.defs")
|
||||||
|
if content is None:
|
||||||
|
return DEFAULT_UID_MIN
|
||||||
|
uid_min = parse_login_defs_uid_min(content.decode("utf-8"))
|
||||||
|
return DEFAULT_UID_MIN if uid_min is None else uid_min
|
||||||
|
|
||||||
|
def _resolve_users(self, users: Sequence[str], auto_users: bool) -> list[str]:
|
||||||
|
resolved: list[str] = []
|
||||||
|
for user in users:
|
||||||
|
if user not in resolved:
|
||||||
|
resolved.append(user)
|
||||||
|
if auto_users:
|
||||||
|
for name, entry in self._passwd_entries.items():
|
||||||
|
if name not in resolved and entry.is_regular_user(self._uid_min):
|
||||||
|
resolved.append(name)
|
||||||
|
return resolved
|
||||||
|
|
||||||
|
def home_dir(self, user: str) -> str:
|
||||||
|
"""Relative path (no leading `/`) to `user`'s home directory, as used
|
||||||
|
by modules that write per-user configuration files.
|
||||||
|
|
||||||
|
Taken from `<src>`'s `/etc/passwd` entry for `user` when there is
|
||||||
|
one (the case for a user resolved via `--auto-users`); otherwise
|
||||||
|
falls back to the usual `/home/<user>` convention (`/root` for
|
||||||
|
root).
|
||||||
|
"""
|
||||||
|
entry = self._passwd_entries.get(user)
|
||||||
|
if entry is not None and entry.home:
|
||||||
|
return entry.home.lstrip("/")
|
||||||
|
return "root" if user == "root" else f"home/{user}"
|
||||||
|
|
||||||
def dst_path(self, relative: str) -> Path:
|
def dst_path(self, relative: str) -> Path:
|
||||||
"""Resolve a path relative to the destination directory, refusing to escape it."""
|
"""Resolve a path relative to the destination directory, refusing to escape it."""
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ the destination directory.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
from id_configurator.context import Context
|
from id_configurator.context import Context
|
||||||
@@ -49,3 +50,25 @@ def sanitize_filename(name: str) -> str:
|
|||||||
create - instead of the single file the caller asked for.
|
create - instead of the single file the caller asked for.
|
||||||
"""
|
"""
|
||||||
return name.replace("/", "_").replace("\\", "_")
|
return name.replace("/", "_").replace("\\", "_")
|
||||||
|
|
||||||
|
|
||||||
|
_REPOSITORY_NAME_FORBIDDEN_CHARS = re.compile(r"[^A-Za-z0-9_-]")
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_repository_name(name: str) -> str:
|
||||||
|
"""Replace every character of `name` that isn't a plain identifier
|
||||||
|
character (letters, digits, `_`, `-`) with `_`.
|
||||||
|
|
||||||
|
Several modules write a repository's name verbatim as a bare,
|
||||||
|
unquoted identifier in a generated config file - an INI section
|
||||||
|
header, a TOML table key, ... - where an unrestricted name could break
|
||||||
|
out of that syntax (e.g. `]`, a newline, or - for TOML - a literal
|
||||||
|
`.`, which would silently open extra, nested tables instead of naming
|
||||||
|
the one table it's meant to). Excluding `.` from the allowed
|
||||||
|
characters also means the result can never come out as exactly `.` or
|
||||||
|
`..`, which matter if the name is ever reused as a filesystem path
|
||||||
|
component (`sanitize_filename` alone wouldn't catch those, since it
|
||||||
|
only strips `/` and `\\`). Spaces and every other non-identifier
|
||||||
|
character are replaced the same way, for consistency across callers.
|
||||||
|
"""
|
||||||
|
return _REPOSITORY_NAME_FORBIDDEN_CHARS.sub("_", name)
|
||||||
|
|||||||
@@ -9,14 +9,15 @@ Go's own HTTPS client already trusts whatever the `certificates` module
|
|||||||
installs into the system CA store, so no separate certificate configuration
|
installs into the system CA store, so no separate certificate configuration
|
||||||
is needed here, unlike npm's.
|
is needed here, unlike npm's.
|
||||||
|
|
||||||
Written to `$GOROOT/go.env` - not the per-user `go env -w` file (found at
|
Written to `$GOROOT/go.env`, which `$GOROOT` defaults to `/usr/local/go` for
|
||||||
`os.UserConfigDir()/go/env`, e.g. `$HOME/.config/go/env`), which is a
|
essentially every standard Go install (the official Docker image included);
|
||||||
separate, higher-precedence level in Go's own terminology. `$GOROOT`
|
unlike a config dropped in one specific user's home, this applies regardless
|
||||||
defaults to `/usr/local/go` for essentially every standard Go install (the
|
of which user invokes `go`. The go command reads `$GOROOT/go.env` as a
|
||||||
official Docker image included); unlike a config dropped in one specific
|
fallback whenever a setting is not already present in a higher-precedence
|
||||||
user's home, this applies regardless of which user invokes `go`. The go
|
level - in particular the per-user `go env -w` file (found at
|
||||||
command reads `$GOROOT/go.env` as a fallback whenever a setting is not
|
`os.UserConfigDir()/go/env`, e.g. `$HOME/.config/go/env`), which this module
|
||||||
already present in the user's own go/env file.
|
also writes for each configured `--user` (`context.users`), so that one
|
||||||
|
takes precedence should that user ever run `go env -w` themselves.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -35,7 +36,11 @@ _GO_ENV = "usr/local/go/go.env"
|
|||||||
def _write_go_env(context: Context, repositories: list[GoRepository]) -> None:
|
def _write_go_env(context: Context, repositories: list[GoRepository]) -> None:
|
||||||
proxy = ",".join(repository.url for repository in repositories)
|
proxy = ",".join(repository.url for repository in repositories)
|
||||||
lines = [f"GOPROXY={proxy}", "GOSUMDB=off"]
|
lines = [f"GOPROXY={proxy}", "GOSUMDB=off"]
|
||||||
context.write_file(_GO_ENV, ("\n".join(lines) + "\n").encode("utf-8"))
|
content = ("\n".join(lines) + "\n").encode("utf-8")
|
||||||
|
context.write_file(_GO_ENV, content)
|
||||||
|
for user in context.users:
|
||||||
|
context.write_file(f"{context.home_dir(user)}/.config/go/env", content)
|
||||||
|
logger.info("configured go for user %s", user)
|
||||||
logger.info("configured go to use %d module prox(y/ies)", len(repositories))
|
logger.info("configured go to use %d module prox(y/ies)", len(repositories))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -28,9 +28,20 @@ tool targets Docker images, which are built and run as root:
|
|||||||
- Ivy: `~/.ivy2/ivysettings.xml`, as a chain of `ibiblio` resolvers.
|
- Ivy: `~/.ivy2/ivysettings.xml`, as a chain of `ibiblio` resolvers.
|
||||||
- SBT: `~/.sbt/repositories`, the resolver override file SBT reads for its
|
- SBT: `~/.sbt/repositories`, the resolver override file SBT reads for its
|
||||||
own bootstrapping and, when combined with the `sbt.override.build.repos`
|
own bootstrapping and, when combined with the `sbt.override.build.repos`
|
||||||
system property, for every project's resolvers as well.
|
system property, for every project's resolvers as well. A repository
|
||||||
|
whose URL isn't HTTPS gets `, allowInsecureProtocol` appended to its
|
||||||
|
line - SBT's Coursier-backed resolver otherwise refuses it outright
|
||||||
|
("insecure protocol is unsupported"), unlike every other entry in
|
||||||
|
`~/.sbt/repositories` which takes no such suffix.
|
||||||
- Leiningen: `~/.lein/profiles.clj`, adding the repositories to the `:user`
|
- Leiningen: `~/.lein/profiles.clj`, adding the repositories to the `:user`
|
||||||
profile.
|
profile. Leiningen, alone among these, refuses a plain-HTTP repository
|
||||||
|
outright ("Tried to use insecure HTTP repository without TLS") - its
|
||||||
|
standalone `lein` launcher (`leiningen.core.main/-main`) unconditionally
|
||||||
|
registers an aborting wagon for the "http" scheme before even reading the
|
||||||
|
project/profile config, so there is no project.clj/profiles.clj setting
|
||||||
|
(there is no `:insecure` key anywhere in Leiningen's own source) that
|
||||||
|
can work around this: a non-HTTPS repository configured here will not be
|
||||||
|
usable from Leiningen specifically, regardless of what this module does.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -39,6 +50,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import jks
|
import jks
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
@@ -233,13 +245,18 @@ def _write_ivy_settings(context: Context, repositories: list[JavaRepository]) ->
|
|||||||
logger.info("configured ivy to use %d repositories", len(repositories))
|
logger.info("configured ivy to use %d repositories", len(repositories))
|
||||||
|
|
||||||
|
|
||||||
|
def _sbt_repository_line(repository: JavaRepository) -> str:
|
||||||
|
line = f" {repository.name}: {repository.url}"
|
||||||
|
if urlparse(repository.url).scheme.lower() != "https":
|
||||||
|
line += ", allowInsecureProtocol"
|
||||||
|
return line
|
||||||
|
|
||||||
|
|
||||||
def _write_sbt_repositories(
|
def _write_sbt_repositories(
|
||||||
context: Context, repositories: list[JavaRepository]
|
context: Context, repositories: list[JavaRepository]
|
||||||
) -> None:
|
) -> None:
|
||||||
lines = ["[repositories]", " local"]
|
lines = ["[repositories]", " local"]
|
||||||
lines.extend(
|
lines.extend(_sbt_repository_line(repository) for repository in repositories)
|
||||||
f" {repository.name}: {repository.url}" for repository in repositories
|
|
||||||
)
|
|
||||||
context.write_file(_SBT_REPOSITORIES, ("\n".join(lines) + "\n").encode("utf-8"))
|
context.write_file(_SBT_REPOSITORIES, ("\n".join(lines) + "\n").encode("utf-8"))
|
||||||
logger.info("configured sbt to use %d repositories", len(repositories))
|
logger.info("configured sbt to use %d repositories", len(repositories))
|
||||||
|
|
||||||
|
|||||||
@@ -19,12 +19,17 @@ when not overridden - the same tradeoff the `php` module's Composer config
|
|||||||
already accepts. A `<clear />` element is included so the configured
|
already accepts. A `<clear />` element is included so the configured
|
||||||
sources fully replace nuget.org rather than being merged alongside it,
|
sources fully replace nuget.org rather than being merged alongside it,
|
||||||
matching every other package-manager module's behaviour.
|
matching every other package-manager module's behaviour.
|
||||||
|
|
||||||
|
The same file is additionally written under each configured `--user`'s own
|
||||||
|
home directory (`context.users`), since a non-root user invoking `dotnet`
|
||||||
|
reads its config from there instead.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from id_configurator.config import NuGetRepository
|
from id_configurator.config import NuGetRepository
|
||||||
from id_configurator.context import Context
|
from id_configurator.context import Context
|
||||||
@@ -48,10 +53,17 @@ def _write_nuget_config(context: Context, repositories: list[NuGetRepository]) -
|
|||||||
package_sources = ET.SubElement(configuration, "packageSources")
|
package_sources = ET.SubElement(configuration, "packageSources")
|
||||||
ET.SubElement(package_sources, "clear")
|
ET.SubElement(package_sources, "clear")
|
||||||
for repository in repositories:
|
for repository in repositories:
|
||||||
ET.SubElement(
|
attributes = {"key": repository.name, "value": repository.url}
|
||||||
package_sources, "add", {"key": repository.name, "value": repository.url}
|
if urlparse(repository.url).scheme.lower() != "https":
|
||||||
|
attributes["allowInsecureConnections"] = "true"
|
||||||
|
ET.SubElement(package_sources, "add", attributes)
|
||||||
|
content = _render_xml(configuration)
|
||||||
|
context.write_file(_NUGET_CONFIG, content)
|
||||||
|
for user in context.users:
|
||||||
|
context.write_file(
|
||||||
|
f"{context.home_dir(user)}/.nuget/NuGet/NuGet.Config", content
|
||||||
)
|
)
|
||||||
context.write_file(_NUGET_CONFIG, _render_xml(configuration))
|
logger.info("configured nuget for user %s", user)
|
||||||
logger.info("configured nuget to use %d package source(s)", len(repositories))
|
logger.info("configured nuget to use %d package source(s)", len(repositories))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,20 @@
|
|||||||
|
|
||||||
Composer has no "replace the default registry" switch either: Packagist
|
Composer has no "replace the default registry" switch either: Packagist
|
||||||
stays enabled unless a repository is explicitly declared under the name
|
stays enabled unless a repository is explicitly declared under the name
|
||||||
"packagist.org" and disabled. This just makes the configured repositories
|
"packagist.org" and disabled. If any configured repository's URL isn't
|
||||||
|
HTTPS, Composer refuses to use it unless `secure-http` is turned off in
|
||||||
|
`config`; since that effectively means this is a private, non-internet
|
||||||
|
setup, Packagist - unreachable in such a setup anyway - is disabled at the
|
||||||
|
same time, exactly as if a `{"packagist.org": false}` repository entry had
|
||||||
|
been declared. This just makes the configured repositories
|
||||||
available system-wide, via Composer's global configuration file:
|
available system-wide, via Composer's global configuration file:
|
||||||
`/root/.composer/config.json` (`COMPOSER_HOME/config.json`; this tool
|
`/root/.composer/config.json` (`COMPOSER_HOME/config.json`; this tool
|
||||||
targets Docker images, which are built and run as root, so `COMPOSER_HOME`
|
targets Docker images, which are built and run as root, so `COMPOSER_HOME`
|
||||||
is assumed to default to `/root/.composer`, exactly like a fresh Composer
|
is assumed to default to `/root/.composer`, exactly like a fresh Composer
|
||||||
install does when not overridden).
|
install does when not overridden). The same file is additionally written
|
||||||
|
under each configured `--user`'s own home directory (`context.users`),
|
||||||
|
since a non-root user invoking `composer` reads its config from there
|
||||||
|
instead.
|
||||||
|
|
||||||
Composer's own HTTPS client (PHP's stream wrapper, generally backed by
|
Composer's own HTTPS client (PHP's stream wrapper, generally backed by
|
||||||
curl) already trusts whatever the `certificates` module installs into the
|
curl) already trusts whatever the `certificates` module installs into the
|
||||||
@@ -31,6 +39,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from id_configurator.config import PhpRepository
|
from id_configurator.config import PhpRepository
|
||||||
from id_configurator.context import Context
|
from id_configurator.context import Context
|
||||||
@@ -43,16 +52,27 @@ _CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt"
|
|||||||
_OPENSSL_INI = "usr/local/etc/php/conf.d/id-configurator-openssl.ini"
|
_OPENSSL_INI = "usr/local/etc/php/conf.d/id-configurator-openssl.ini"
|
||||||
|
|
||||||
|
|
||||||
|
def _is_https(url: str) -> bool:
|
||||||
|
return urlparse(url).scheme.lower() == "https"
|
||||||
|
|
||||||
|
|
||||||
def _write_composer_config(context: Context, repositories: list[PhpRepository]) -> None:
|
def _write_composer_config(context: Context, repositories: list[PhpRepository]) -> None:
|
||||||
content = {
|
composer_repositories: list[dict[str, str | bool]] = [
|
||||||
"repositories": [
|
|
||||||
{"name": repository.name, "type": "composer", "url": repository.url}
|
{"name": repository.name, "type": "composer", "url": repository.url}
|
||||||
for repository in repositories
|
for repository in repositories
|
||||||
]
|
]
|
||||||
}
|
content: dict[str, object] = {"repositories": composer_repositories}
|
||||||
context.write_file(
|
composer_repositories.append({"packagist.org": False})
|
||||||
_COMPOSER_CONFIG, (json.dumps(content, indent=4) + "\n").encode("utf-8")
|
if any(not _is_https(repository.url) for repository in repositories):
|
||||||
|
content["config"] = {"secure-http": False}
|
||||||
|
logger.info(
|
||||||
|
"disabled secure-http and packagist.org: non-HTTPS repository configured"
|
||||||
)
|
)
|
||||||
|
rendered = (json.dumps(content, indent=4) + "\n").encode("utf-8")
|
||||||
|
context.write_file(_COMPOSER_CONFIG, rendered)
|
||||||
|
for user in context.users:
|
||||||
|
context.write_file(f"{context.home_dir(user)}/.composer/config.json", rendered)
|
||||||
|
logger.info("configured composer for user %s", user)
|
||||||
logger.info(
|
logger.info(
|
||||||
"configured composer to use %d additional repositories", len(repositories)
|
"configured composer to use %d additional repositories", len(repositories)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,13 +3,17 @@ PythonRepository entries instead of pypi.org, and to trust the root
|
|||||||
certificates installed by the certificates module.
|
certificates installed by the certificates module.
|
||||||
|
|
||||||
- pip: system-wide `/etc/pip.conf` (`index-url`/`extra-index-url`/`cert`).
|
- pip: system-wide `/etc/pip.conf` (`index-url`/`extra-index-url`/`cert`).
|
||||||
|
Any repository whose URL isn't HTTPS has its host added to
|
||||||
|
`trusted-host`, since pip otherwise refuses a plain HTTP index.
|
||||||
- uv: system-wide `/etc/uv/uv.toml` (`[[index]]` entries, `native-tls`).
|
- uv: system-wide `/etc/uv/uv.toml` (`[[index]]` entries, `native-tls`).
|
||||||
- poetry: `/root/.config/pypoetry/config.toml` (`repositories.*`/`certificates.*`).
|
- poetry: `/root/.config/pypoetry/config.toml` (`repositories.*`/`certificates.*`).
|
||||||
Poetry has no global "replace PyPI" switch: a project's `pyproject.toml`
|
Poetry has no global "replace PyPI" switch: a project's `pyproject.toml`
|
||||||
still has to reference these repositories as sources. This only makes the
|
still has to reference these repositories as sources. This only makes the
|
||||||
repository and its certificate available system-wide. It also assumes the
|
repository and its certificate available system-wide. Since poetry only
|
||||||
container runs as root, since poetry only reads its config from the
|
reads its config from the current user's home directory, this assumes the
|
||||||
current user's home directory.
|
container runs as root; the same file is additionally written under each
|
||||||
|
configured `--user`'s own home directory (`context.users`), for the
|
||||||
|
benefit of a non-root user invoking poetry.
|
||||||
- easy_install: `distutils.cfg` (`[easy_install] index_url`), written next
|
- easy_install: `distutils.cfg` (`[easy_install] index_url`), written next
|
||||||
to every Python installation detected under `<src>` (there is no location
|
to every Python installation detected under `<src>` (there is no location
|
||||||
independent of a specific Python prefix).
|
independent of a specific Python prefix).
|
||||||
@@ -21,10 +25,11 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from id_configurator.config import PythonRepository
|
from id_configurator.config import PythonRepository
|
||||||
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, sanitize_repository_name
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -39,11 +44,22 @@ def _toml_string(value: str) -> str:
|
|||||||
return json.dumps(value)
|
return json.dumps(value)
|
||||||
|
|
||||||
|
|
||||||
|
def _trusted_host(repository: PythonRepository) -> str | None:
|
||||||
|
parsed = urlparse(repository.index_url)
|
||||||
|
return None if parsed.scheme.lower() == "https" else parsed.hostname
|
||||||
|
|
||||||
|
|
||||||
def _write_pip_conf(context: Context, repositories: list[PythonRepository]) -> None:
|
def _write_pip_conf(context: Context, repositories: list[PythonRepository]) -> None:
|
||||||
lines = ["[global]", f"index-url = {repositories[0].index_url}"]
|
lines = ["[global]", f"index-url = {repositories[0].index_url}"]
|
||||||
if len(repositories) > 1:
|
if len(repositories) > 1:
|
||||||
lines.append("extra-index-url =")
|
lines.append("extra-index-url =")
|
||||||
lines.extend(f" {repository.index_url}" for repository in repositories[1:])
|
lines.extend(f" {repository.index_url}" for repository in repositories[1:])
|
||||||
|
trusted_hosts = dict.fromkeys(
|
||||||
|
host for repository in repositories if (host := _trusted_host(repository))
|
||||||
|
)
|
||||||
|
if trusted_hosts:
|
||||||
|
lines.append("trusted-host =")
|
||||||
|
lines.extend(f" {host}" for host in trusted_hosts)
|
||||||
lines.append(f"cert = {_CA_BUNDLE}")
|
lines.append(f"cert = {_CA_BUNDLE}")
|
||||||
context.write_file(_PIP_CONF, ("\n".join(lines) + "\n").encode("utf-8"))
|
context.write_file(_PIP_CONF, ("\n".join(lines) + "\n").encode("utf-8"))
|
||||||
logger.info("configured pip to use %s", repositories[0].index_url)
|
logger.info("configured pip to use %s", repositories[0].index_url)
|
||||||
@@ -67,15 +83,20 @@ def _write_poetry_config(
|
|||||||
) -> None:
|
) -> None:
|
||||||
lines = []
|
lines = []
|
||||||
for repository in repositories:
|
for repository in repositories:
|
||||||
lines.append(f"[repositories.{repository.name}]")
|
name = sanitize_repository_name(repository.name)
|
||||||
|
lines.append(f"[repositories.{name}]")
|
||||||
lines.append(f"url = {_toml_string(repository.index_url)}")
|
lines.append(f"url = {_toml_string(repository.index_url)}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append(f"[certificates.{repository.name}]")
|
lines.append(f"[certificates.{name}]")
|
||||||
lines.append(f"cert = {_toml_string(_CA_BUNDLE)}")
|
lines.append(f"cert = {_toml_string(_CA_BUNDLE)}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
content = ("\n".join(lines).rstrip() + "\n").encode("utf-8")
|
||||||
|
context.write_file(_POETRY_CONFIG, content)
|
||||||
|
for user in context.users:
|
||||||
context.write_file(
|
context.write_file(
|
||||||
_POETRY_CONFIG, ("\n".join(lines).rstrip() + "\n").encode("utf-8")
|
f"{context.home_dir(user)}/.config/pypoetry/config.toml", content
|
||||||
)
|
)
|
||||||
|
logger.info("configured poetry for user %s", user)
|
||||||
logger.info("registered %d poetry repositories", len(repositories))
|
logger.info("registered %d poetry repositories", len(repositories))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
"""Configure cargo to use the configured RustRepository entries: the first
|
"""Configure cargo to use the configured RustRepository entries: the first
|
||||||
one replaces crates.io entirely (via `[source.crates-io] replace-with`), and
|
one replaces crates.io entirely (via `[source.crates-io] replace-with`), and
|
||||||
any further ones are registered as additional named registries (usable via
|
any further ones are registered as additional named registries (usable via
|
||||||
`cargo add --registry <name>` or a dependency's `registry = "<name>"`), all
|
`cargo add --registry <name>` or a dependency's `registry = "<name>"`). A
|
||||||
using the sparse (plain HTTPS) protocol rather than a git-based index.
|
repository's index is declared as `sparse+<url>` - the sparse (plain HTTPS)
|
||||||
|
protocol - only when its `is_sparse` option is set; otherwise `<url>` is used
|
||||||
|
as-is, for a git-based index.
|
||||||
|
|
||||||
Cargo's own HTTPS client already trusts whatever the `certificates` module
|
Cargo's own HTTPS client already trusts whatever the `certificates` module
|
||||||
installs into the system CA store, so no separate certificate configuration
|
installs into the system CA store, so no separate certificate configuration
|
||||||
@@ -17,6 +19,11 @@ directory to `/`, consulting `/.cargo/config.toml` there, *before* falling
|
|||||||
back to `$CARGO_HOME/config.toml` - so this is the one genuinely global
|
back to `$CARGO_HOME/config.toml` - so this is the one genuinely global
|
||||||
location: found regardless of which user, working directory or `CARGO_HOME`
|
location: found regardless of which user, working directory or `CARGO_HOME`
|
||||||
is in effect, unlike a config dropped in one specific user's home.
|
is in effect, unlike a config dropped in one specific user's home.
|
||||||
|
|
||||||
|
For each configured `--user` (`context.users`), the same config is
|
||||||
|
additionally written at that user's own `$HOME/.cargo/config.toml` - the
|
||||||
|
conventional, un-overridden `CARGO_HOME` location for that specific user,
|
||||||
|
which this module otherwise has no visibility into.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -25,29 +32,39 @@ import logging
|
|||||||
|
|
||||||
from id_configurator.config import RustRepository
|
from id_configurator.config import RustRepository
|
||||||
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, sanitize_repository_name
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_CARGO_CONFIG = ".cargo/config.toml"
|
_CARGO_CONFIG = ".cargo/config.toml"
|
||||||
|
|
||||||
|
|
||||||
|
def _index_url(repository: RustRepository) -> str:
|
||||||
|
return f"sparse+{repository.url}" if repository.is_sparse else repository.url
|
||||||
|
|
||||||
|
|
||||||
def _write_cargo_config(context: Context, repositories: list[RustRepository]) -> None:
|
def _write_cargo_config(context: Context, repositories: list[RustRepository]) -> None:
|
||||||
primary = repositories[0]
|
primary = repositories[0]
|
||||||
|
primary_name = sanitize_repository_name(primary.name)
|
||||||
lines = [
|
lines = [
|
||||||
"[source.crates-io]",
|
"[source.crates-io]",
|
||||||
f'replace-with = "{primary.name}"',
|
f'replace-with = "{primary_name}"',
|
||||||
"",
|
"",
|
||||||
f"[source.{primary.name}]",
|
f"[source.{primary_name}]",
|
||||||
f'registry = "sparse+{primary.url}"',
|
f'registry = "{_index_url(primary)}"',
|
||||||
]
|
]
|
||||||
for repository in repositories[1:]:
|
for repository in repositories[1:]:
|
||||||
|
name = sanitize_repository_name(repository.name)
|
||||||
lines += [
|
lines += [
|
||||||
"",
|
"",
|
||||||
f"[registries.{repository.name}]",
|
f"[registries.{name}]",
|
||||||
f'index = "sparse+{repository.url}"',
|
f'index = "{_index_url(repository)}"',
|
||||||
]
|
]
|
||||||
context.write_file(_CARGO_CONFIG, ("\n".join(lines) + "\n").encode("utf-8"))
|
content = ("\n".join(lines) + "\n").encode("utf-8")
|
||||||
|
context.write_file(_CARGO_CONFIG, content)
|
||||||
|
for user in context.users:
|
||||||
|
context.write_file(f"{context.home_dir(user)}/.cargo/config.toml", content)
|
||||||
|
logger.info("configured cargo for user %s", user)
|
||||||
logger.info("configured cargo to use %s", primary.url)
|
logger.info("configured cargo to use %s", primary.url)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,12 @@ from io import StringIO
|
|||||||
|
|
||||||
from id_configurator.config import NamedContent, YumRepository
|
from id_configurator.config import NamedContent, YumRepository
|
||||||
from id_configurator.context import Context
|
from id_configurator.context import Context
|
||||||
from id_configurator.modules.base import Module, register, sanitize_filename
|
from id_configurator.modules.base import (
|
||||||
|
Module,
|
||||||
|
register,
|
||||||
|
sanitize_filename,
|
||||||
|
sanitize_repository_name,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -91,9 +96,10 @@ def _disable_existing_repos(context: Context, patterns: Sequence[str]) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _write_repo_file(context: Context, repository: YumRepository) -> None:
|
def _write_repo_file(context: Context, repository: YumRepository) -> None:
|
||||||
|
name = sanitize_repository_name(repository.name)
|
||||||
lines = [
|
lines = [
|
||||||
f"[{repository.name}]",
|
f"[{name}]",
|
||||||
f"name={repository.name}",
|
f"name={name}",
|
||||||
f"baseurl={repository.index_url}",
|
f"baseurl={repository.index_url}",
|
||||||
"enabled=1",
|
"enabled=1",
|
||||||
]
|
]
|
||||||
@@ -103,10 +109,10 @@ def _write_repo_file(context: Context, repository: YumRepository) -> None:
|
|||||||
else:
|
else:
|
||||||
lines.append("gpgcheck=0")
|
lines.append("gpgcheck=0")
|
||||||
context.write_file(
|
context.write_file(
|
||||||
f"{_REPOS_DIR}/{sanitize_filename(repository.name)}.repo",
|
f"{_REPOS_DIR}/{name}.repo",
|
||||||
("\n".join(lines) + "\n").encode("utf-8"),
|
("\n".join(lines) + "\n").encode("utf-8"),
|
||||||
)
|
)
|
||||||
logger.info("added yum/dnf repository %s", repository.name)
|
logger.info("added yum/dnf repository %s", name)
|
||||||
|
|
||||||
|
|
||||||
def _validate_gpg_key(name: str, content: bytes) -> None:
|
def _validate_gpg_key(name: str, content: bytes) -> None:
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
"""Parse `/etc/passwd` entries for the `--auto-users` CLI option.
|
||||||
|
|
||||||
|
A passwd(5) line is `name:passwd:uid:gid:gecos:home:shell`. `--auto-users`
|
||||||
|
expands to every entry that looks like a regular (non-system) account - see
|
||||||
|
`PasswdEntry.is_regular_user`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
DEFAULT_UID_MIN = 1000
|
||||||
|
|
||||||
|
_DISABLED_SHELL_NAMES = {"nologin", "sync", "false"}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PasswdEntry:
|
||||||
|
name: str
|
||||||
|
uid: int
|
||||||
|
home: str
|
||||||
|
shell: str
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_login_shell(self) -> bool:
|
||||||
|
return self.shell.rsplit("/", 1)[-1] not in _DISABLED_SHELL_NAMES
|
||||||
|
|
||||||
|
def is_regular_user(self, uid_min: int = DEFAULT_UID_MIN) -> bool:
|
||||||
|
"""True unless `self` looks like a system account.
|
||||||
|
|
||||||
|
A login-disabling shell (`has_login_shell` False) only catches
|
||||||
|
service accounts that follow that convention; it misses others
|
||||||
|
(and could wrongly flag a merely locked human account). The
|
||||||
|
authoritative signal `useradd`/`adduser`/`systemd-sysusers` use
|
||||||
|
instead is the UID: system accounts get one below `uid_min` (the
|
||||||
|
`UID_MIN` from login.defs(5), 1000 by default). Both checks apply
|
||||||
|
together here - except for `root`, whose UID 0 is itself a system
|
||||||
|
convention rather than a sign it shouldn't be configured: it's
|
||||||
|
exempted from the UID check (though not from the shell one, so a
|
||||||
|
deliberately locked root account is still excluded).
|
||||||
|
"""
|
||||||
|
if not self.has_login_shell:
|
||||||
|
return False
|
||||||
|
return self.name == "root" or self.uid >= uid_min
|
||||||
|
|
||||||
|
|
||||||
|
def parse_passwd(content: str) -> list[PasswdEntry]:
|
||||||
|
entries = []
|
||||||
|
for line in content.splitlines():
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
fields = line.split(":")
|
||||||
|
if len(fields) < 7:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
uid = int(fields[2])
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
entries.append(
|
||||||
|
PasswdEntry(name=fields[0], uid=uid, home=fields[5], shell=fields[6])
|
||||||
|
)
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
def parse_login_defs_uid_min(content: str) -> int | None:
|
||||||
|
"""Extract `UID_MIN` from `/etc/login.defs` content (login.defs(5)), or
|
||||||
|
`None` if it isn't set there."""
|
||||||
|
for line in content.splitlines():
|
||||||
|
stripped = line.strip()
|
||||||
|
if not stripped or stripped.startswith("#"):
|
||||||
|
continue
|
||||||
|
parts = stripped.split()
|
||||||
|
if len(parts) >= 2 and parts[0] == "UID_MIN":
|
||||||
|
try:
|
||||||
|
return int(parts[1])
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
+1
-1
@@ -44,7 +44,7 @@ maintainers = [
|
|||||||
name = "id-configurator"
|
name = "id-configurator"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
version = "2026.10.3"
|
version = "2026.10.6"
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
id-configurator = "id_configurator:main"
|
id-configurator = "id_configurator:main"
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ configurations:
|
|||||||
apk:
|
apk:
|
||||||
repositories:
|
repositories:
|
||||||
- name: demo
|
- name: demo
|
||||||
url: https://host.docker.internal:{{ 'APK_REPO_PORT' | env }}/
|
{% set scheme = 'APK_REPO_SCHEME' | env %}
|
||||||
|
url: {{ scheme }}://host.docker.internal:{{ ('APK_REPO_PORT' if scheme == 'https' else 'APK_REPO_HTTP_PORT') | env }}/
|
||||||
url_to_replace: https://dl-cdn.alpinelinux.org/*
|
url_to_replace: https://dl-cdn.alpinelinux.org/*
|
||||||
key: |
|
key: |
|
||||||
{{ 'dockerfiles/repos/040d0af5f97a0a44.rsa.pub' | file | indent(8, first=true) }}
|
{{ 'dockerfiles/repos/040d0af5f97a0a44.rsa.pub' | file | indent(8, first=true) }}
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ configurations:
|
|||||||
- name: demo
|
- name: demo
|
||||||
types: [deb]
|
types: [deb]
|
||||||
uris:
|
uris:
|
||||||
- https://host.docker.internal:{{ 'APT_REPO_PORT' | env }}/
|
{% set scheme = 'APT_REPO_SCHEME' | env %}
|
||||||
|
- {{ scheme }}://host.docker.internal:{{ ('APT_REPO_PORT' if scheme == 'https' else 'APT_REPO_HTTP_PORT') | env }}/
|
||||||
suites: ["./"]
|
suites: ["./"]
|
||||||
uri-to-replace: http://deb.debian.org/*
|
uri-to-replace: http://deb.debian.org/*
|
||||||
signed-by: |
|
signed-by: |
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ configurations:
|
|||||||
go:
|
go:
|
||||||
repositories:
|
repositories:
|
||||||
- name: demo
|
- name: demo
|
||||||
url: https://host.docker.internal:{{ 'GO_REPO_PORT' | env }}/
|
{% set scheme = 'GO_REPO_SCHEME' | env %}
|
||||||
|
url: {{ scheme }}://host.docker.internal:{{ ('GO_REPO_PORT' if scheme == 'https' else 'GO_REPO_HTTP_PORT') | env }}/
|
||||||
ruby:
|
ruby:
|
||||||
repositories: []
|
repositories: []
|
||||||
nuget:
|
nuget:
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ configurations:
|
|||||||
# during the Dockerfile's warm-up step. All other tools just see it as
|
# during the Dockerfile's warm-up step. All other tools just see it as
|
||||||
# an ordinary repository, and find the test artifact here.
|
# an ordinary repository, and find the test artifact here.
|
||||||
- name: central
|
- name: central
|
||||||
url: https://host.docker.internal:{{ 'JAVA_REPO_PORT' | env }}/
|
{% set scheme = 'JAVA_REPO_SCHEME' | env %}
|
||||||
|
url: {{ scheme }}://host.docker.internal:{{ ('JAVA_REPO_PORT' if scheme == 'https' else 'JAVA_REPO_HTTP_PORT') | env }}/
|
||||||
# Second entry: only SBT needs this. With
|
# Second entry: only SBT needs this. With
|
||||||
# -Dsbt.override.build.repos=true (the only way SBT actually consults
|
# -Dsbt.override.build.repos=true (the only way SBT actually consults
|
||||||
# this file for project dependencies, not just its own bootstrap), the
|
# this file for project dependencies, not just its own bootstrap), the
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ configurations:
|
|||||||
npm:
|
npm:
|
||||||
repositories:
|
repositories:
|
||||||
- name: demo
|
- name: demo
|
||||||
url: https://host.docker.internal:{{ 'NPM_REPO_PORT' | env }}/
|
{% set scheme = 'NPM_REPO_SCHEME' | env %}
|
||||||
|
url: {{ scheme }}://host.docker.internal:{{ ('NPM_REPO_PORT' if scheme == 'https' else 'NPM_REPO_HTTP_PORT') | env }}/
|
||||||
rust:
|
rust:
|
||||||
repositories: []
|
repositories: []
|
||||||
go:
|
go:
|
||||||
|
|||||||
@@ -30,4 +30,5 @@ configurations:
|
|||||||
nuget:
|
nuget:
|
||||||
repositories:
|
repositories:
|
||||||
- name: demo
|
- name: demo
|
||||||
url: https://host.docker.internal:{{ 'NUGET_REPO_PORT' | env }}/index.json
|
{% set scheme = 'NUGET_REPO_SCHEME' | env %}
|
||||||
|
url: {{ scheme }}://host.docker.internal:{{ ('NUGET_REPO_PORT' if scheme == 'https' else 'NUGET_REPO_HTTP_PORT') | env }}/index.json
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ configurations:
|
|||||||
php:
|
php:
|
||||||
repositories:
|
repositories:
|
||||||
- name: demo
|
- name: demo
|
||||||
url: https://host.docker.internal:{{ 'PHP_REPO_PORT' | env }}/
|
{% set scheme = 'PHP_REPO_SCHEME' | env %}
|
||||||
|
url: {{ scheme }}://host.docker.internal:{{ ('PHP_REPO_PORT' if scheme == 'https' else 'PHP_REPO_HTTP_PORT') | env }}/
|
||||||
npm:
|
npm:
|
||||||
repositories: []
|
repositories: []
|
||||||
rust:
|
rust:
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ configurations:
|
|||||||
python:
|
python:
|
||||||
repositories:
|
repositories:
|
||||||
- name: demo
|
- name: demo
|
||||||
index-url: https://host.docker.internal:{{ 'PYPI_REPO_PORT' | env }}/simple/
|
{% set scheme = 'PYPI_REPO_SCHEME' | env %}
|
||||||
|
index-url: {{ scheme }}://host.docker.internal:{{ ('PYPI_REPO_PORT' if scheme == 'https' else 'PYPI_REPO_HTTP_PORT') | env }}/simple/
|
||||||
java:
|
java:
|
||||||
repositories: []
|
repositories: []
|
||||||
php:
|
php:
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ configurations:
|
|||||||
ruby:
|
ruby:
|
||||||
repositories:
|
repositories:
|
||||||
- name: demo
|
- name: demo
|
||||||
url: https://host.docker.internal:{{ 'RUBY_REPO_PORT' | env }}/
|
{% set scheme = 'RUBY_REPO_SCHEME' | env %}
|
||||||
|
url: {{ scheme }}://host.docker.internal:{{ ('RUBY_REPO_PORT' if scheme == 'https' else 'RUBY_REPO_HTTP_PORT') | env }}/
|
||||||
nuget:
|
nuget:
|
||||||
repositories: []
|
repositories: []
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ configurations:
|
|||||||
rust:
|
rust:
|
||||||
repositories:
|
repositories:
|
||||||
- name: demo
|
- name: demo
|
||||||
url: https://host.docker.internal:{{ 'CARGO_REPO_PORT' | env }}/
|
{% set scheme = 'CARGO_REPO_SCHEME' | env %}
|
||||||
|
url: {{ scheme }}://host.docker.internal:{{ ('CARGO_REPO_PORT' if scheme == 'https' else 'CARGO_REPO_HTTP_PORT') | env }}/
|
||||||
|
is_sparse: true
|
||||||
go:
|
go:
|
||||||
repositories: []
|
repositories: []
|
||||||
ruby:
|
ruby:
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ configurations:
|
|||||||
yum:
|
yum:
|
||||||
repositories:
|
repositories:
|
||||||
- name: demo
|
- name: demo
|
||||||
index-url: https://host.docker.internal:{{ 'YUM_REPO_PORT' | env }}/
|
{% set scheme = 'YUM_REPO_SCHEME' | env %}
|
||||||
|
index-url: {{ scheme }}://host.docker.internal:{{ ('YUM_REPO_PORT' if scheme == 'https' else 'YUM_REPO_HTTP_PORT') | env }}/
|
||||||
index-url-to-replace: https://mirrors.rockylinux.org/*
|
index-url-to-replace: https://mirrors.rockylinux.org/*
|
||||||
keys: []
|
keys: []
|
||||||
python:
|
python:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ COPY --from=target / /src/
|
|||||||
COPY --from=generated-config Java.yaml /config.yaml
|
COPY --from=generated-config Java.yaml /config.yaml
|
||||||
RUN id-configurator apply --config /config.yaml --src /src --dst /dst
|
RUN id-configurator apply --config /config.yaml --src /src --dst /dst
|
||||||
FROM target AS runtime
|
FROM target AS runtime
|
||||||
|
ARG JAVA_REPO_SCHEME
|
||||||
# Every build tool the `java` module configures: Maven itself (already part
|
# Every build tool the `java` module configures: Maven itself (already part
|
||||||
# of this image), Gradle, Ivy and Leiningen (all packaged for this Ubuntu
|
# of this image), Gradle, Ivy and Leiningen (all packaged for this Ubuntu
|
||||||
# base), and SBT (no package available, fetched as a plain release tarball).
|
# base), and SBT (no package available, fetched as a plain release tarball).
|
||||||
@@ -43,12 +44,23 @@ RUN java -jar /usr/share/java/ivy.jar -settings /root/.ivy2/ivysettings.xml \
|
|||||||
-retrieve "/tmp/ivyretrieve/[artifact]-[revision].[ext]" \
|
-retrieve "/tmp/ivyretrieve/[artifact]-[revision].[ext]" \
|
||||||
&& test -f /tmp/ivyretrieve/idconfiguratortestpkg-0.0.1.jar
|
&& test -f /tmp/ivyretrieve/idconfiguratortestpkg-0.0.1.jar
|
||||||
|
|
||||||
|
# Leiningen's standalone launcher unconditionally refuses any plain-HTTP
|
||||||
|
# repository ("Tried to use insecure HTTP repository without TLS") before
|
||||||
|
# even reading project.clj/profiles.clj - there is no `:insecure` key or
|
||||||
|
# other configuration escape hatch for this (confirmed from Leiningen's own
|
||||||
|
# source: leiningen.core.main/-main registers an aborting wagon for the
|
||||||
|
# "http" scheme unconditionally). So this check is skipped for the
|
||||||
|
# module-java-http variant: no id-configurator config could make it pass.
|
||||||
RUN mkdir -p /tmp/leintest && cd /tmp/leintest \
|
RUN mkdir -p /tmp/leintest && cd /tmp/leintest \
|
||||||
&& cat > project.clj <<'EOF'
|
&& cat > project.clj <<'EOF'
|
||||||
(defproject leintest "0.1.0"
|
(defproject leintest "0.1.0"
|
||||||
:dependencies [[org.interdiode/idconfiguratortestpkg "0.0.1"]])
|
:dependencies [[org.interdiode/idconfiguratortestpkg "0.0.1"]])
|
||||||
EOF
|
EOF
|
||||||
RUN cd /tmp/leintest && lein deps :tree | grep -q idconfiguratortestpkg
|
RUN if [ "$JAVA_REPO_SCHEME" = "https" ]; then \
|
||||||
|
cd /tmp/leintest && lein deps :tree | grep -q idconfiguratortestpkg; \
|
||||||
|
else \
|
||||||
|
echo "skipping leiningen: it cannot use a non-HTTPS repository"; \
|
||||||
|
fi
|
||||||
|
|
||||||
RUN mkdir -p /tmp/sbttest/project \
|
RUN mkdir -p /tmp/sbttest/project \
|
||||||
&& cat > /tmp/sbttest/build.sbt <<'EOF'
|
&& cat > /tmp/sbttest/build.sbt <<'EOF'
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ COPY --from=generated-config Python.yaml /config.yaml
|
|||||||
RUN id-configurator apply --config /config.yaml --src /src --dst /dst
|
RUN id-configurator apply --config /config.yaml --src /src --dst /dst
|
||||||
FROM target AS runtime
|
FROM target AS runtime
|
||||||
ARG PYPI_REPO_PORT
|
ARG PYPI_REPO_PORT
|
||||||
|
ARG PYPI_REPO_SCHEME
|
||||||
# Warm up uv and poetry themselves from the real, network-reachable PyPI
|
# Warm up uv and poetry themselves from the real, network-reachable PyPI
|
||||||
# while the default index is still in place, so the actual tests below
|
# while the default index is still in place, so the actual tests below
|
||||||
# never need anything but the fixture repository configured by
|
# never need anything but the fixture repository configured by
|
||||||
@@ -20,5 +21,5 @@ RUN uv pip install --system idconfiguratortestpkg \
|
|||||||
&& python3 -c "import idconfiguratortestpkg"
|
&& python3 -c "import idconfiguratortestpkg"
|
||||||
RUN mkdir -p /tmp/poetrytest && cd /tmp/poetrytest \
|
RUN mkdir -p /tmp/poetrytest && cd /tmp/poetrytest \
|
||||||
&& poetry init -n --name poetrytest \
|
&& poetry init -n --name poetrytest \
|
||||||
&& poetry source add --priority explicit demo "https://host.docker.internal:${PYPI_REPO_PORT}/simple/" \
|
&& poetry source add --priority explicit demo "${PYPI_REPO_SCHEME}://host.docker.internal:${PYPI_REPO_PORT}/simple/" \
|
||||||
&& poetry add idconfiguratortestpkg --source demo
|
&& poetry add idconfiguratortestpkg --source demo
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ COPY --from=generated-config Python.yaml /config.yaml
|
|||||||
RUN id-configurator apply --config /config.yaml --src /src --dst /dst
|
RUN id-configurator apply --config /config.yaml --src /src --dst /dst
|
||||||
FROM target AS runtime
|
FROM target AS runtime
|
||||||
ARG PYPI_REPO_PORT
|
ARG PYPI_REPO_PORT
|
||||||
|
ARG PYPI_REPO_SCHEME
|
||||||
# Warm up uv and poetry themselves from the real, network-reachable PyPI
|
# Warm up uv and poetry themselves from the real, network-reachable PyPI
|
||||||
# while the default index is still in place, so the actual tests below
|
# while the default index is still in place, so the actual tests below
|
||||||
# never need anything but the fixture repository configured by
|
# never need anything but the fixture repository configured by
|
||||||
@@ -20,5 +21,5 @@ RUN uv pip install --system idconfiguratortestpkg \
|
|||||||
&& python3 -c "import idconfiguratortestpkg"
|
&& python3 -c "import idconfiguratortestpkg"
|
||||||
RUN mkdir -p /tmp/poetrytest && cd /tmp/poetrytest \
|
RUN mkdir -p /tmp/poetrytest && cd /tmp/poetrytest \
|
||||||
&& poetry init -n --name poetrytest \
|
&& poetry init -n --name poetrytest \
|
||||||
&& poetry source add --priority explicit demo "https://host.docker.internal:${PYPI_REPO_PORT}/simple/" \
|
&& poetry source add --priority explicit demo "${PYPI_REPO_SCHEME}://host.docker.internal:${PYPI_REPO_PORT}/simple/" \
|
||||||
&& poetry add idconfiguratortestpkg --source demo
|
&& poetry add idconfiguratortestpkg --source demo
|
||||||
|
|||||||
@@ -8,3 +8,13 @@ server {
|
|||||||
root /srv;
|
root /srv;
|
||||||
autoindex off;
|
autoindex off;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Plain-HTTP listener, for the module-*-http tox envs that verify id-configurator
|
||||||
|
# correctly configures a non-HTTPS repository.
|
||||||
|
server {
|
||||||
|
listen 8080 default_server;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /srv;
|
||||||
|
autoindex off;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,3 +8,13 @@ server {
|
|||||||
root /srv;
|
root /srv;
|
||||||
autoindex off;
|
autoindex off;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Plain-HTTP listener, for the module-*-http tox envs that verify id-configurator
|
||||||
|
# correctly configures a non-HTTPS repository.
|
||||||
|
server {
|
||||||
|
listen 8080 default_server;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /srv;
|
||||||
|
autoindex off;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,10 +12,28 @@ server {
|
|||||||
# templates, since the host:port this fixture is actually reached on is
|
# templates, since the host:port this fixture is actually reached on is
|
||||||
# only known once tox-docker starts the container, not at image-build
|
# only known once tox-docker starts the container, not at image-build
|
||||||
# time. $http_host reflects exactly what the client (cargo) used to
|
# time. $http_host reflects exactly what the client (cargo) used to
|
||||||
# connect, e.g. host.docker.internal:55123.
|
# connect, e.g. host.docker.internal:55123; $scheme likewise reflects
|
||||||
|
# whichever of this file's two listeners actually served the request.
|
||||||
location = /config.json {
|
location = /config.json {
|
||||||
default_type application/json;
|
default_type application/json;
|
||||||
sub_filter '__BASE_URL__' 'https://$http_host';
|
sub_filter '__BASE_URL__' '$scheme://$http_host';
|
||||||
|
sub_filter_once off;
|
||||||
|
sub_filter_types application/json;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Plain-HTTP listener, for the module-rust-http tox env that verifies
|
||||||
|
# id-configurator correctly configures a non-HTTPS repository.
|
||||||
|
server {
|
||||||
|
listen 8080 default_server;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /srv;
|
||||||
|
autoindex off;
|
||||||
|
|
||||||
|
location = /config.json {
|
||||||
|
default_type application/json;
|
||||||
|
sub_filter '__BASE_URL__' '$scheme://$http_host';
|
||||||
sub_filter_once off;
|
sub_filter_once off;
|
||||||
sub_filter_types application/json;
|
sub_filter_types application/json;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,3 +8,13 @@ server {
|
|||||||
root /srv;
|
root /srv;
|
||||||
autoindex off;
|
autoindex off;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Plain-HTTP listener, for the module-*-http tox envs that verify id-configurator
|
||||||
|
# correctly configures a non-HTTPS repository.
|
||||||
|
server {
|
||||||
|
listen 8080 default_server;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /srv;
|
||||||
|
autoindex off;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,3 +8,13 @@ server {
|
|||||||
root /srv;
|
root /srv;
|
||||||
autoindex off;
|
autoindex off;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Plain-HTTP listener, for the module-*-http tox envs that verify id-configurator
|
||||||
|
# correctly configures a non-HTTPS repository.
|
||||||
|
server {
|
||||||
|
listen 8080 default_server;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /srv;
|
||||||
|
autoindex off;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,10 +12,29 @@ server {
|
|||||||
# its tarball URL, since the host:port this fixture is actually reached
|
# its tarball URL, since the host:port this fixture is actually reached
|
||||||
# on is only known once tox-docker starts the container, not at
|
# on is only known once tox-docker starts the container, not at
|
||||||
# image-build time. $http_host reflects exactly what the client (npm)
|
# image-build time. $http_host reflects exactly what the client (npm)
|
||||||
# used to connect, e.g. host.docker.internal:55123.
|
# used to connect, e.g. host.docker.internal:55123; $scheme likewise
|
||||||
|
# reflects whichever of this file's two listeners actually served the
|
||||||
|
# request.
|
||||||
location = /idconfiguratortestpkg {
|
location = /idconfiguratortestpkg {
|
||||||
default_type application/json;
|
default_type application/json;
|
||||||
sub_filter '__BASE_URL__' 'https://$http_host';
|
sub_filter '__BASE_URL__' '$scheme://$http_host';
|
||||||
|
sub_filter_once off;
|
||||||
|
sub_filter_types application/json;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Plain-HTTP listener, for the module-npm-http tox env that verifies
|
||||||
|
# id-configurator correctly configures a non-HTTPS repository.
|
||||||
|
server {
|
||||||
|
listen 8080 default_server;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /srv;
|
||||||
|
autoindex off;
|
||||||
|
|
||||||
|
location = /idconfiguratortestpkg {
|
||||||
|
default_type application/json;
|
||||||
|
sub_filter '__BASE_URL__' '$scheme://$http_host';
|
||||||
sub_filter_once off;
|
sub_filter_once off;
|
||||||
sub_filter_types application/json;
|
sub_filter_types application/json;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,28 @@ server {
|
|||||||
# fields, since the host:port this fixture is actually reached on is
|
# fields, since the host:port this fixture is actually reached on is
|
||||||
# only known once tox-docker starts the container, not at image-build
|
# only known once tox-docker starts the container, not at image-build
|
||||||
# time. $http_host reflects exactly what the client (nuget) used to
|
# time. $http_host reflects exactly what the client (nuget) used to
|
||||||
# connect, e.g. host.docker.internal:55123.
|
# connect, e.g. host.docker.internal:55123; $scheme likewise reflects
|
||||||
|
# whichever of this file's two listeners actually served the request.
|
||||||
location = /index.json {
|
location = /index.json {
|
||||||
default_type application/json;
|
default_type application/json;
|
||||||
sub_filter '__BASE_URL__' 'https://$http_host';
|
sub_filter '__BASE_URL__' '$scheme://$http_host';
|
||||||
|
sub_filter_once off;
|
||||||
|
sub_filter_types application/json;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Plain-HTTP listener, for the module-nuget-http tox env that verifies
|
||||||
|
# id-configurator correctly configures a non-HTTPS repository.
|
||||||
|
server {
|
||||||
|
listen 8080 default_server;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /srv;
|
||||||
|
autoindex off;
|
||||||
|
|
||||||
|
location = /index.json {
|
||||||
|
default_type application/json;
|
||||||
|
sub_filter '__BASE_URL__' '$scheme://$http_host';
|
||||||
sub_filter_once off;
|
sub_filter_once off;
|
||||||
sub_filter_types application/json;
|
sub_filter_types application/json;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,28 @@ server {
|
|||||||
# since the host:port this fixture is actually reached on is only known
|
# since the host:port this fixture is actually reached on is only known
|
||||||
# once tox-docker starts the container, not at image-build time.
|
# once tox-docker starts the container, not at image-build time.
|
||||||
# $http_host reflects exactly what the client (Composer) used to
|
# $http_host reflects exactly what the client (Composer) used to
|
||||||
# connect, e.g. host.docker.internal:55123.
|
# connect, e.g. host.docker.internal:55123; $scheme likewise reflects
|
||||||
|
# whichever of this file's two listeners actually served the request.
|
||||||
location = /packages.json {
|
location = /packages.json {
|
||||||
default_type application/json;
|
default_type application/json;
|
||||||
sub_filter '__BASE_URL__' 'https://$http_host';
|
sub_filter '__BASE_URL__' '$scheme://$http_host';
|
||||||
|
sub_filter_once off;
|
||||||
|
sub_filter_types application/json;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Plain-HTTP listener, for the module-php-http tox env that verifies
|
||||||
|
# id-configurator correctly configures a non-HTTPS repository.
|
||||||
|
server {
|
||||||
|
listen 8080 default_server;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /srv;
|
||||||
|
autoindex off;
|
||||||
|
|
||||||
|
location = /packages.json {
|
||||||
|
default_type application/json;
|
||||||
|
sub_filter '__BASE_URL__' '$scheme://$http_host';
|
||||||
sub_filter_once off;
|
sub_filter_once off;
|
||||||
sub_filter_types application/json;
|
sub_filter_types application/json;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,3 +8,13 @@ server {
|
|||||||
root /srv;
|
root /srv;
|
||||||
autoindex off;
|
autoindex off;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Plain-HTTP listener, for the module-*-http tox envs that verify id-configurator
|
||||||
|
# correctly configures a non-HTTPS repository.
|
||||||
|
server {
|
||||||
|
listen 8080 default_server;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /srv;
|
||||||
|
autoindex off;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,3 +8,13 @@ server {
|
|||||||
root /srv;
|
root /srv;
|
||||||
autoindex off;
|
autoindex off;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Plain-HTTP listener, for the module-*-http tox envs that verify id-configurator
|
||||||
|
# correctly configures a non-HTTPS repository.
|
||||||
|
server {
|
||||||
|
listen 8080 default_server;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /srv;
|
||||||
|
autoindex off;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,3 +8,13 @@ server {
|
|||||||
root /srv;
|
root /srv;
|
||||||
autoindex off;
|
autoindex off;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Plain-HTTP listener, for the module-*-http tox envs that verify id-configurator
|
||||||
|
# correctly configures a non-HTTPS repository.
|
||||||
|
server {
|
||||||
|
listen 8080 default_server;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /srv;
|
||||||
|
autoindex off;
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ CONFIG_WITH_CERT = {
|
|||||||
**MINIMAL_CONFIG,
|
**MINIMAL_CONFIG,
|
||||||
"configurations": {"certificates": [{"name": "my-ca", "content": "fake"}]},
|
"configurations": {"certificates": [{"name": "my-ca", "content": "fake"}]},
|
||||||
}
|
}
|
||||||
|
CONFIG_WITH_GO_REPO = {
|
||||||
|
**MINIMAL_CONFIG,
|
||||||
|
"configurations": {
|
||||||
|
"go": {"repositories": [{"name": "demo", "url": "https://example.invalid/"}]}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def write_config(tmp_path, data) -> str:
|
def write_config(tmp_path, data) -> str:
|
||||||
@@ -129,6 +135,66 @@ def test_apply_runs_relevant_module(tmp_path) -> None:
|
|||||||
assert (dst / "etc/ssl/certs/ca-certificates.crt").read_bytes() == b"fake\n"
|
assert (dst / "etc/ssl/certs/ca-certificates.crt").read_bytes() == b"fake\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_with_user_option_writes_per_user_config(tmp_path) -> None:
|
||||||
|
config_path = write_config(tmp_path, CONFIG_WITH_GO_REPO)
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
result = runner.invoke(
|
||||||
|
cli.app,
|
||||||
|
[
|
||||||
|
"apply",
|
||||||
|
"--config",
|
||||||
|
config_path,
|
||||||
|
"--dst",
|
||||||
|
str(dst),
|
||||||
|
"--user",
|
||||||
|
"alice",
|
||||||
|
"--user",
|
||||||
|
"root",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert (dst / "home/alice/.config/go/env").read_text(encoding="utf-8") == (
|
||||||
|
"GOPROXY=https://example.invalid/\nGOSUMDB=off\n"
|
||||||
|
)
|
||||||
|
assert (dst / "root/.config/go/env").read_text(encoding="utf-8") == (
|
||||||
|
"GOPROXY=https://example.invalid/\nGOSUMDB=off\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_with_auto_users_reads_passwd_from_src(tmp_path) -> None:
|
||||||
|
config_path = write_config(tmp_path, CONFIG_WITH_GO_REPO)
|
||||||
|
src = tmp_path / "src"
|
||||||
|
(src / "etc").mkdir(parents=True)
|
||||||
|
(src / "etc/passwd").write_text(
|
||||||
|
"root:x:0:0:root:/root:/bin/bash\n"
|
||||||
|
"alice:x:1000:1000:Alice:/home/alice:/bin/bash\n"
|
||||||
|
"daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
result = runner.invoke(
|
||||||
|
cli.app,
|
||||||
|
[
|
||||||
|
"apply",
|
||||||
|
"--config",
|
||||||
|
config_path,
|
||||||
|
"--dst",
|
||||||
|
str(dst),
|
||||||
|
"--src",
|
||||||
|
str(src),
|
||||||
|
"--auto-users",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
# root is always included despite its uid 0; daemon (nologin shell) is
|
||||||
|
# a system account, excluded.
|
||||||
|
assert (dst / "root/.config/go/env").exists()
|
||||||
|
assert (dst / "home/alice/.config/go/env").exists()
|
||||||
|
assert not (dst / "usr/sbin/.config/go/env").exists()
|
||||||
|
|
||||||
|
|
||||||
def test_main_default_name_calls_app(monkeypatch) -> None:
|
def test_main_default_name_calls_app(monkeypatch) -> None:
|
||||||
mock_app = MagicMock()
|
mock_app = MagicMock()
|
||||||
monkeypatch.setattr(cli, "app", mock_app)
|
monkeypatch.setattr(cli, "app", mock_app)
|
||||||
|
|||||||
+132
-2
@@ -8,14 +8,16 @@ CONFIG = Configuration.model_validate(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def make_context(tmp_path, with_src: bool = True) -> Context:
|
def make_context(
|
||||||
|
tmp_path, with_src: bool = True, users=None, auto_users: bool = False
|
||||||
|
) -> Context:
|
||||||
dst = tmp_path / "dst"
|
dst = tmp_path / "dst"
|
||||||
dst.mkdir()
|
dst.mkdir()
|
||||||
src = None
|
src = None
|
||||||
if with_src:
|
if with_src:
|
||||||
src = tmp_path / "src"
|
src = tmp_path / "src"
|
||||||
src.mkdir()
|
src.mkdir()
|
||||||
return Context(config=CONFIG, dst=dst, src=src)
|
return Context(config=CONFIG, dst=dst, src=src, users=users, auto_users=auto_users)
|
||||||
|
|
||||||
|
|
||||||
def test_dst_path_within_destination(tmp_path) -> None:
|
def test_dst_path_within_destination(tmp_path) -> None:
|
||||||
@@ -97,3 +99,131 @@ def test_glob_src_matches_pattern(tmp_path) -> None:
|
|||||||
(context.src / "usr/lib/python-legacy").mkdir(parents=True)
|
(context.src / "usr/lib/python-legacy").mkdir(parents=True)
|
||||||
result = context.glob_src("usr/lib/python3.*")
|
result = context.glob_src("usr/lib/python3.*")
|
||||||
assert [entry.name for entry in result] == ["python3.11", "python3.12"]
|
assert [entry.name for entry in result] == ["python3.11", "python3.12"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_users_defaults_to_empty_list(tmp_path) -> None:
|
||||||
|
context = make_context(tmp_path, with_src=False)
|
||||||
|
assert context.users == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_users_keeps_explicit_names_in_order_without_duplicates(tmp_path) -> None:
|
||||||
|
context = make_context(tmp_path, with_src=False, users=["alice", "bob", "alice"])
|
||||||
|
assert context.users == ["alice", "bob"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_users_expands_from_passwd_excluding_disabled_shells(tmp_path) -> None:
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
dst.mkdir()
|
||||||
|
src = tmp_path / "src"
|
||||||
|
(src / "etc").mkdir(parents=True)
|
||||||
|
(src / "etc/passwd").write_text(
|
||||||
|
"root:x:0:0:root:/root:/bin/bash\n"
|
||||||
|
"alice:x:1000:1000:Alice:/home/alice:/bin/bash\n"
|
||||||
|
"daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
context = Context(config=CONFIG, dst=dst, src=src, auto_users=True)
|
||||||
|
# root is always included despite its uid 0; daemon (nologin shell) is
|
||||||
|
# excluded as a system account.
|
||||||
|
assert context.users == ["root", "alice"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_users_excludes_root_with_disabled_shell(tmp_path) -> None:
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
dst.mkdir()
|
||||||
|
src = tmp_path / "src"
|
||||||
|
(src / "etc").mkdir(parents=True)
|
||||||
|
(src / "etc/passwd").write_text(
|
||||||
|
"root:x:0:0:root:/root:/usr/sbin/nologin\n"
|
||||||
|
"alice:x:1000:1000:Alice:/home/alice:/bin/bash\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
context = Context(config=CONFIG, dst=dst, src=src, auto_users=True)
|
||||||
|
assert context.users == ["alice"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_users_excludes_uid_below_default_uid_min(tmp_path) -> None:
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
dst.mkdir()
|
||||||
|
src = tmp_path / "src"
|
||||||
|
(src / "etc").mkdir(parents=True)
|
||||||
|
(src / "etc/passwd").write_text(
|
||||||
|
"svc:x:999:999:service account:/home/svc:/bin/bash\n"
|
||||||
|
"alice:x:1000:1000:Alice:/home/alice:/bin/bash\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
context = Context(config=CONFIG, dst=dst, src=src, auto_users=True)
|
||||||
|
assert context.users == ["alice"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_users_honors_uid_min_from_login_defs(tmp_path) -> None:
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
dst.mkdir()
|
||||||
|
src = tmp_path / "src"
|
||||||
|
(src / "etc").mkdir(parents=True)
|
||||||
|
(src / "etc/passwd").write_text(
|
||||||
|
"svc:x:999:999:service account:/home/svc:/bin/bash\n"
|
||||||
|
"alice:x:1000:1000:Alice:/home/alice:/bin/bash\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(src / "etc/login.defs").write_text("UID_MIN\t500\n", encoding="utf-8")
|
||||||
|
context = Context(config=CONFIG, dst=dst, src=src, auto_users=True)
|
||||||
|
assert context.users == ["svc", "alice"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_users_combines_with_explicit_names_without_duplicates(tmp_path) -> None:
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
dst.mkdir()
|
||||||
|
src = tmp_path / "src"
|
||||||
|
src.mkdir()
|
||||||
|
(src / "etc").mkdir()
|
||||||
|
(src / "etc/passwd").write_text(
|
||||||
|
"root:x:0:0:root:/root:/bin/bash\nalice:x:1000:1000:Alice:/home/alice:/bin/bash\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
context = Context(
|
||||||
|
config=CONFIG, dst=dst, src=src, users=["bob", "alice"], auto_users=True
|
||||||
|
)
|
||||||
|
# explicit names come first, in order; auto-detected names are appended,
|
||||||
|
# skipping "alice" since it's already present.
|
||||||
|
assert context.users == ["bob", "alice", "root"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_users_false_ignores_passwd(tmp_path) -> None:
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
dst.mkdir()
|
||||||
|
src = tmp_path / "src"
|
||||||
|
(src / "etc").mkdir(parents=True)
|
||||||
|
(src / "etc/passwd").write_text(
|
||||||
|
"alice:x:1000:1000:Alice:/home/alice:/bin/bash\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
context = Context(config=CONFIG, dst=dst, src=src)
|
||||||
|
assert context.users == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_users_without_src_is_empty(tmp_path) -> None:
|
||||||
|
context = make_context(tmp_path, with_src=False, auto_users=True)
|
||||||
|
assert context.users == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_home_dir_for_root(tmp_path) -> None:
|
||||||
|
context = make_context(tmp_path, with_src=False)
|
||||||
|
assert context.home_dir("root") == "root"
|
||||||
|
|
||||||
|
|
||||||
|
def test_home_dir_falls_back_to_convention(tmp_path) -> None:
|
||||||
|
context = make_context(tmp_path, with_src=False)
|
||||||
|
assert context.home_dir("alice") == "home/alice"
|
||||||
|
|
||||||
|
|
||||||
|
def test_home_dir_uses_passwd_entry_when_available(tmp_path) -> None:
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
dst.mkdir()
|
||||||
|
src = tmp_path / "src"
|
||||||
|
src.mkdir()
|
||||||
|
(src / "etc").mkdir()
|
||||||
|
(src / "etc/passwd").write_text(
|
||||||
|
"alice:x:1000:1000:Alice:/srv/alice:/bin/bash\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
context = Context(config=CONFIG, dst=dst, src=src)
|
||||||
|
assert context.home_dir("alice") == "srv/alice"
|
||||||
|
|||||||
@@ -55,6 +55,18 @@ def test_apply_writes_goproxy(tmp_path) -> None:
|
|||||||
assert go_env == "GOPROXY=https://example.invalid/\nGOSUMDB=off\n"
|
assert go_env == "GOPROXY=https://example.invalid/\nGOSUMDB=off\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_writes_go_env_for_each_configured_user(tmp_path) -> None:
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
context = Context(
|
||||||
|
config=make_config(CONFIG_WITH_REPO), dst=dst, src=None, users=["alice"]
|
||||||
|
)
|
||||||
|
|
||||||
|
GoModule().apply(context)
|
||||||
|
|
||||||
|
go_env = (dst / "home/alice/.config/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:
|
def test_apply_joins_multiple_repositories_with_comma(tmp_path) -> None:
|
||||||
dst = tmp_path / "dst"
|
dst = tmp_path / "dst"
|
||||||
context = Context(config=make_config(CONFIG_WITH_TWO_REPOS), dst=dst, src=None)
|
context = Context(config=make_config(CONFIG_WITH_TWO_REPOS), dst=dst, src=None)
|
||||||
|
|||||||
@@ -339,6 +339,31 @@ def test_apply_writes_sbt_repositories(tmp_path) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_marks_non_https_sbt_repository_as_insecure(tmp_path) -> None:
|
||||||
|
data = {
|
||||||
|
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||||
|
"organisation": "InterDiode",
|
||||||
|
"configurations": {
|
||||||
|
"java": {
|
||||||
|
"repositories": [
|
||||||
|
{"name": "interdiode", "url": "http://example.invalid/maven"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
context = Context(config=make_config(data), dst=dst, src=None)
|
||||||
|
|
||||||
|
JavaModule().apply(context)
|
||||||
|
|
||||||
|
repositories = (dst / "root/.sbt/repositories").read_text(encoding="utf-8")
|
||||||
|
assert repositories == (
|
||||||
|
"[repositories]\n"
|
||||||
|
" local\n"
|
||||||
|
" interdiode: http://example.invalid/maven, allowInsecureProtocol\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_apply_writes_lein_profiles(tmp_path) -> None:
|
def test_apply_writes_lein_profiles(tmp_path) -> None:
|
||||||
dst = tmp_path / "dst"
|
dst = tmp_path / "dst"
|
||||||
context = Context(config=make_config(TWO_REPOS_CONFIG), dst=dst, src=None)
|
context = Context(config=make_config(TWO_REPOS_CONFIG), dst=dst, src=None)
|
||||||
|
|||||||
@@ -63,6 +63,48 @@ def test_apply_writes_nuget_config(tmp_path) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_marks_non_https_source_as_insecure(tmp_path) -> None:
|
||||||
|
data = {
|
||||||
|
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||||
|
"organisation": "InterDiode",
|
||||||
|
"configurations": {
|
||||||
|
"nuget": {
|
||||||
|
"repositories": [{"name": "demo", "url": "http://example.invalid/"}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
context = Context(config=make_config(data), 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="http://example.invalid/" allowInsecureConnections="true" />\n'
|
||||||
|
" </packageSources>\n"
|
||||||
|
"</configuration>\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_writes_config_for_each_configured_user(tmp_path) -> None:
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
context = Context(
|
||||||
|
config=make_config(CONFIG_WITH_REPO), dst=dst, src=None, users=["alice", "root"]
|
||||||
|
)
|
||||||
|
|
||||||
|
NuGetModule().apply(context)
|
||||||
|
|
||||||
|
root_content = (dst / "root/.nuget/NuGet/NuGet.Config").read_text(encoding="utf-8")
|
||||||
|
alice_content = (dst / "home/alice/.nuget/NuGet/NuGet.Config").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
assert alice_content == root_content
|
||||||
|
|
||||||
|
|
||||||
def test_apply_writes_multiple_sources_in_order(tmp_path) -> None:
|
def test_apply_writes_multiple_sources_in_order(tmp_path) -> None:
|
||||||
dst = tmp_path / "dst"
|
dst = tmp_path / "dst"
|
||||||
context = Context(config=make_config(CONFIG_WITH_TWO_REPOS), dst=dst, src=None)
|
context = Context(config=make_config(CONFIG_WITH_TWO_REPOS), dst=dst, src=None)
|
||||||
|
|||||||
+66
-2
@@ -80,7 +80,8 @@ def test_apply_writes_global_composer_config(tmp_path) -> None:
|
|||||||
"name": "demo",
|
"name": "demo",
|
||||||
"type": "composer",
|
"type": "composer",
|
||||||
"url": "https://example.invalid/composer",
|
"url": "https://example.invalid/composer",
|
||||||
}
|
},
|
||||||
|
{"packagist.org": False},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +94,70 @@ def test_apply_writes_multiple_repositories_in_order(tmp_path) -> None:
|
|||||||
|
|
||||||
config_path = dst / "root/.composer/config.json"
|
config_path = dst / "root/.composer/config.json"
|
||||||
content = json.loads(config_path.read_text(encoding="utf-8"))
|
content = json.loads(config_path.read_text(encoding="utf-8"))
|
||||||
assert [repo["name"] for repo in content["repositories"]] == ["demo", "other"]
|
names = [repo["name"] for repo in content["repositories"] if "name" in repo]
|
||||||
|
assert names == ["demo", "other"]
|
||||||
|
assert {"packagist.org": False} in content["repositories"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_disables_secure_http_and_packagist_when_repo_not_https(
|
||||||
|
tmp_path,
|
||||||
|
) -> None:
|
||||||
|
data = {
|
||||||
|
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||||
|
"organisation": "InterDiode",
|
||||||
|
"configurations": {
|
||||||
|
"php": {
|
||||||
|
"repositories": [
|
||||||
|
{"name": "demo", "url": "https://example.invalid/composer"},
|
||||||
|
{"name": "insecure", "url": "http://other.invalid/composer"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
context = Context(config=make_config(data), dst=dst, src=None)
|
||||||
|
|
||||||
|
PhpModule().apply(context)
|
||||||
|
|
||||||
|
config_path = dst / "root/.composer/config.json"
|
||||||
|
content = json.loads(config_path.read_text(encoding="utf-8"))
|
||||||
|
assert content["config"] == {"secure-http": False}
|
||||||
|
assert {"packagist.org": False} in content["repositories"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_does_not_disable_secure_http_when_all_repos_https(tmp_path) -> None:
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
context = Context(config=make_config(CONFIG_WITH_REPO), dst=dst, src=None)
|
||||||
|
|
||||||
|
PhpModule().apply(context)
|
||||||
|
|
||||||
|
config_path = dst / "root/.composer/config.json"
|
||||||
|
content = json.loads(config_path.read_text(encoding="utf-8"))
|
||||||
|
assert "config" not in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_always_disables_packagist(tmp_path) -> None:
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
context = Context(config=make_config(CONFIG_WITH_REPO), dst=dst, src=None)
|
||||||
|
|
||||||
|
PhpModule().apply(context)
|
||||||
|
|
||||||
|
config_path = dst / "root/.composer/config.json"
|
||||||
|
content = json.loads(config_path.read_text(encoding="utf-8"))
|
||||||
|
assert {"packagist.org": False} in content["repositories"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_writes_composer_config_for_each_configured_user(tmp_path) -> None:
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
context = Context(
|
||||||
|
config=make_config(CONFIG_WITH_REPO), dst=dst, src=None, users=["alice"]
|
||||||
|
)
|
||||||
|
|
||||||
|
PhpModule().apply(context)
|
||||||
|
|
||||||
|
root_content = (dst / "root/.composer/config.json").read_bytes()
|
||||||
|
alice_content = (dst / "home/alice/.composer/config.json").read_bytes()
|
||||||
|
assert alice_content == root_content
|
||||||
|
|
||||||
|
|
||||||
def test_apply_without_certificates_does_not_write_openssl_ini(tmp_path) -> None:
|
def test_apply_without_certificates_does_not_write_openssl_ini(tmp_path) -> None:
|
||||||
|
|||||||
@@ -76,6 +76,84 @@ def test_apply_single_repository(tmp_path) -> None:
|
|||||||
assert not (dst / "usr/lib").exists()
|
assert not (dst / "usr/lib").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_adds_trusted_host_for_non_https_repository(tmp_path) -> None:
|
||||||
|
data = {
|
||||||
|
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||||
|
"organisation": "InterDiode",
|
||||||
|
"configurations": {
|
||||||
|
"python": {
|
||||||
|
"repositories": [
|
||||||
|
{"name": "interdiode", "index-url": "http://example.invalid/simple"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
config = Configuration.model_validate(data)
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
context = Context(config=config, dst=dst, src=None)
|
||||||
|
|
||||||
|
PythonModule().apply(context)
|
||||||
|
|
||||||
|
pip_conf = (dst / "etc/pip.conf").read_text(encoding="utf-8")
|
||||||
|
assert pip_conf == (
|
||||||
|
"[global]\n"
|
||||||
|
"index-url = http://example.invalid/simple\n"
|
||||||
|
"trusted-host =\n"
|
||||||
|
" example.invalid\n"
|
||||||
|
"cert = /etc/ssl/certs/ca-certificates.crt\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_adds_trusted_host_only_for_non_https_among_mixed_repositories(
|
||||||
|
tmp_path,
|
||||||
|
) -> None:
|
||||||
|
data = {
|
||||||
|
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||||
|
"organisation": "InterDiode",
|
||||||
|
"configurations": {
|
||||||
|
"python": {
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"name": "interdiode",
|
||||||
|
"index-url": "https://example.invalid/simple",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "insecure",
|
||||||
|
"index-url": "http://insecure.invalid/simple",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
config = Configuration.model_validate(data)
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
context = Context(config=config, dst=dst, src=None)
|
||||||
|
|
||||||
|
PythonModule().apply(context)
|
||||||
|
|
||||||
|
pip_conf = (dst / "etc/pip.conf").read_text(encoding="utf-8")
|
||||||
|
assert pip_conf == (
|
||||||
|
"[global]\n"
|
||||||
|
"index-url = https://example.invalid/simple\n"
|
||||||
|
"extra-index-url =\n"
|
||||||
|
" http://insecure.invalid/simple\n"
|
||||||
|
"trusted-host =\n"
|
||||||
|
" insecure.invalid\n"
|
||||||
|
"cert = /etc/ssl/certs/ca-certificates.crt\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_does_not_add_trusted_host_when_all_repositories_https(tmp_path) -> None:
|
||||||
|
config = Configuration.model_validate(ONE_REPO_CONFIG)
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
context = Context(config=config, dst=dst, src=None)
|
||||||
|
|
||||||
|
PythonModule().apply(context)
|
||||||
|
|
||||||
|
pip_conf = (dst / "etc/pip.conf").read_text(encoding="utf-8")
|
||||||
|
assert "trusted-host" not in pip_conf
|
||||||
|
|
||||||
|
|
||||||
def test_apply_multiple_repositories(tmp_path) -> None:
|
def test_apply_multiple_repositories(tmp_path) -> None:
|
||||||
config = Configuration.model_validate(TWO_REPOS_CONFIG)
|
config = Configuration.model_validate(TWO_REPOS_CONFIG)
|
||||||
dst = tmp_path / "dst"
|
dst = tmp_path / "dst"
|
||||||
@@ -105,6 +183,57 @@ def test_apply_multiple_repositories(tmp_path) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_sanitizes_poetry_repository_name_with_special_characters(
|
||||||
|
tmp_path,
|
||||||
|
) -> None:
|
||||||
|
data = {
|
||||||
|
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||||
|
"organisation": "InterDiode",
|
||||||
|
"configurations": {
|
||||||
|
"python": {
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"name": "weird.name]here[bad",
|
||||||
|
"index-url": "https://example.invalid/simple",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
config = Configuration.model_validate(data)
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
context = Context(config=config, dst=dst, src=None)
|
||||||
|
|
||||||
|
PythonModule().apply(context)
|
||||||
|
|
||||||
|
poetry_config = (dst / "root/.config/pypoetry/config.toml").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
assert poetry_config == (
|
||||||
|
"[repositories.weird_name_here_bad]\n"
|
||||||
|
'url = "https://example.invalid/simple"\n'
|
||||||
|
"\n"
|
||||||
|
"[certificates.weird_name_here_bad]\n"
|
||||||
|
'cert = "/etc/ssl/certs/ca-certificates.crt"\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_writes_poetry_config_for_each_configured_user(tmp_path) -> None:
|
||||||
|
config = Configuration.model_validate(ONE_REPO_CONFIG)
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
context = Context(config=config, dst=dst, src=None, users=["alice"])
|
||||||
|
|
||||||
|
PythonModule().apply(context)
|
||||||
|
|
||||||
|
root_content = (dst / "root/.config/pypoetry/config.toml").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
alice_content = (dst / "home/alice/.config/pypoetry/config.toml").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
assert alice_content == root_content
|
||||||
|
|
||||||
|
|
||||||
def test_apply_writes_distutils_cfg_for_detected_python_prefixes(tmp_path) -> None:
|
def test_apply_writes_distutils_cfg_for_detected_python_prefixes(tmp_path) -> None:
|
||||||
config = Configuration.model_validate(ONE_REPO_CONFIG)
|
config = Configuration.model_validate(ONE_REPO_CONFIG)
|
||||||
src = tmp_path / "src"
|
src = tmp_path / "src"
|
||||||
|
|||||||
+104
-3
@@ -6,7 +6,15 @@ CONFIG_WITH_REPO = {
|
|||||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||||
"organisation": "InterDiode",
|
"organisation": "InterDiode",
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"rust": {"repositories": [{"name": "demo", "url": "https://example.invalid/"}]}
|
"rust": {
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"name": "demo",
|
||||||
|
"url": "https://example.invalid/",
|
||||||
|
"is_sparse": True,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,8 +24,16 @@ CONFIG_WITH_TWO_REPOS = {
|
|||||||
"configurations": {
|
"configurations": {
|
||||||
"rust": {
|
"rust": {
|
||||||
"repositories": [
|
"repositories": [
|
||||||
{"name": "demo", "url": "https://example.invalid/"},
|
{
|
||||||
{"name": "other", "url": "https://other.invalid/"},
|
"name": "demo",
|
||||||
|
"url": "https://example.invalid/",
|
||||||
|
"is_sparse": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "other",
|
||||||
|
"url": "https://other.invalid/",
|
||||||
|
"is_sparse": True,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -61,6 +77,91 @@ def test_apply_writes_replacement_source(tmp_path) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_sanitizes_repository_names_with_special_characters(tmp_path) -> None:
|
||||||
|
data = {
|
||||||
|
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||||
|
"organisation": "InterDiode",
|
||||||
|
"configurations": {
|
||||||
|
"rust": {
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"name": "weird.demo]\nname",
|
||||||
|
"url": "https://example.invalid/",
|
||||||
|
"is_sparse": True,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
context = Context(config=make_config(data), dst=dst, src=None)
|
||||||
|
|
||||||
|
RustModule().apply(context)
|
||||||
|
|
||||||
|
config_toml = (dst / ".cargo/config.toml").read_text(encoding="utf-8")
|
||||||
|
assert config_toml == (
|
||||||
|
"[source.crates-io]\n"
|
||||||
|
'replace-with = "weird_demo__name"\n'
|
||||||
|
"\n"
|
||||||
|
"[source.weird_demo__name]\n"
|
||||||
|
'registry = "sparse+https://example.invalid/"\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_does_not_prefix_url_when_not_sparse(tmp_path) -> None:
|
||||||
|
data = {
|
||||||
|
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||||
|
"organisation": "InterDiode",
|
||||||
|
"configurations": {
|
||||||
|
"rust": {
|
||||||
|
"repositories": [{"name": "demo", "url": "https://example.invalid/"}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
context = Context(config=make_config(data), dst=dst, src=None)
|
||||||
|
|
||||||
|
RustModule().apply(context)
|
||||||
|
|
||||||
|
config_toml = (dst / ".cargo/config.toml").read_text(encoding="utf-8")
|
||||||
|
assert config_toml == (
|
||||||
|
"[source.crates-io]\n"
|
||||||
|
'replace-with = "demo"\n'
|
||||||
|
"\n"
|
||||||
|
"[source.demo]\n"
|
||||||
|
'registry = "https://example.invalid/"\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_prefixes_url_with_sparse_when_configured(tmp_path) -> None:
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
context = Context(config=make_config(CONFIG_WITH_REPO), dst=dst, src=None)
|
||||||
|
|
||||||
|
RustModule().apply(context)
|
||||||
|
|
||||||
|
config_toml = (dst / ".cargo/config.toml").read_text(encoding="utf-8")
|
||||||
|
assert config_toml == (
|
||||||
|
"[source.crates-io]\n"
|
||||||
|
'replace-with = "demo"\n'
|
||||||
|
"\n"
|
||||||
|
"[source.demo]\n"
|
||||||
|
'registry = "sparse+https://example.invalid/"\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_writes_cargo_config_for_each_configured_user(tmp_path) -> None:
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
context = Context(
|
||||||
|
config=make_config(CONFIG_WITH_REPO), dst=dst, src=None, users=["alice"]
|
||||||
|
)
|
||||||
|
|
||||||
|
RustModule().apply(context)
|
||||||
|
|
||||||
|
root_content = (dst / ".cargo/config.toml").read_text(encoding="utf-8")
|
||||||
|
alice_content = (dst / "home/alice/.cargo/config.toml").read_text(encoding="utf-8")
|
||||||
|
assert alice_content == root_content
|
||||||
|
|
||||||
|
|
||||||
def test_apply_writes_additional_repositories_as_named_registries(tmp_path) -> None:
|
def test_apply_writes_additional_repositories_as_named_registries(tmp_path) -> None:
|
||||||
dst = tmp_path / "dst"
|
dst = tmp_path / "dst"
|
||||||
context = Context(config=make_config(CONFIG_WITH_TWO_REPOS), dst=dst, src=None)
|
context = Context(config=make_config(CONFIG_WITH_TWO_REPOS), dst=dst, src=None)
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
from id_configurator.users import PasswdEntry, parse_login_defs_uid_min, parse_passwd
|
||||||
|
|
||||||
|
PASSWD_CONTENT = (
|
||||||
|
"root:x:0:0:root:/root:/bin/bash\n"
|
||||||
|
"alice:x:1000:1000:Alice:/home/alice:/bin/bash\n"
|
||||||
|
"daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\n"
|
||||||
|
"sync:x:4:65534:sync:/bin:/bin/sync\n"
|
||||||
|
"nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/false\n"
|
||||||
|
"# a comment\n"
|
||||||
|
"\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_passwd_returns_one_entry_per_line() -> None:
|
||||||
|
entries = parse_passwd(PASSWD_CONTENT)
|
||||||
|
assert [entry.name for entry in entries] == [
|
||||||
|
"root",
|
||||||
|
"alice",
|
||||||
|
"daemon",
|
||||||
|
"sync",
|
||||||
|
"nobody",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_passwd_skips_comments_and_blank_lines() -> None:
|
||||||
|
entries = parse_passwd(PASSWD_CONTENT)
|
||||||
|
assert len(entries) == 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_passwd_extracts_uid_home_and_shell() -> None:
|
||||||
|
entries = {entry.name: entry for entry in parse_passwd(PASSWD_CONTENT)}
|
||||||
|
assert entries["alice"].uid == 1000
|
||||||
|
assert entries["alice"].home == "/home/alice"
|
||||||
|
assert entries["alice"].shell == "/bin/bash"
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_passwd_skips_malformed_lines() -> None:
|
||||||
|
entries = parse_passwd(
|
||||||
|
"tooshort:x:0\nnotanumber:x:abc:0:x:/home/x:/bin/bash\n"
|
||||||
|
"root:x:0:0:root:/root:/bin/bash\n"
|
||||||
|
)
|
||||||
|
assert [entry.name for entry in entries] == ["root"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_has_login_shell_true_for_regular_shell() -> None:
|
||||||
|
entry = PasswdEntry(name="alice", uid=1000, home="/home/alice", shell="/bin/bash")
|
||||||
|
assert entry.has_login_shell is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_has_login_shell_false_for_nologin_sync_or_false() -> None:
|
||||||
|
nologin = PasswdEntry(
|
||||||
|
name="daemon", uid=1, home="/usr/sbin", shell="/usr/sbin/nologin"
|
||||||
|
)
|
||||||
|
sync = PasswdEntry(name="sync", uid=4, home="/bin", shell="/bin/sync")
|
||||||
|
false_shell = PasswdEntry(
|
||||||
|
name="nobody", uid=65534, home="/nonexistent", shell="/usr/sbin/false"
|
||||||
|
)
|
||||||
|
assert nologin.has_login_shell is False
|
||||||
|
assert sync.has_login_shell is False
|
||||||
|
assert false_shell.has_login_shell is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_regular_user_true_for_human_account() -> None:
|
||||||
|
entry = PasswdEntry(name="alice", uid=1000, home="/home/alice", shell="/bin/bash")
|
||||||
|
assert entry.is_regular_user() is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_regular_user_true_for_root_despite_uid_zero() -> None:
|
||||||
|
entry = PasswdEntry(name="root", uid=0, home="/root", shell="/bin/bash")
|
||||||
|
assert entry.is_regular_user() is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_regular_user_false_for_root_with_disabled_shell() -> None:
|
||||||
|
entry = PasswdEntry(name="root", uid=0, home="/root", shell="/usr/sbin/nologin")
|
||||||
|
assert entry.is_regular_user() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_regular_user_false_for_disabled_shell_despite_high_uid() -> None:
|
||||||
|
entry = PasswdEntry(
|
||||||
|
name="nobody", uid=65534, home="/nonexistent", shell="/usr/sbin/false"
|
||||||
|
)
|
||||||
|
assert entry.is_regular_user() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_regular_user_honors_custom_uid_min() -> None:
|
||||||
|
entry = PasswdEntry(name="svc", uid=500, home="/home/svc", shell="/bin/bash")
|
||||||
|
assert entry.is_regular_user(uid_min=1000) is False
|
||||||
|
assert entry.is_regular_user(uid_min=100) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_login_defs_uid_min_found() -> None:
|
||||||
|
content = "# comment\nUID_MIN\t500\nGID_MIN\t500\n"
|
||||||
|
assert parse_login_defs_uid_min(content) == 500
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_login_defs_uid_min_missing() -> None:
|
||||||
|
assert parse_login_defs_uid_min("GID_MIN\t500\n") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_login_defs_uid_min_malformed_value() -> None:
|
||||||
|
assert parse_login_defs_uid_min("UID_MIN\tnot-a-number\n") is None
|
||||||
+31
-10
@@ -313,10 +313,26 @@ def test_apply_matches_index_url_to_replace_with_unrelated_files_and_multiple_re
|
|||||||
assert (dst / "etc/yum.repos.d/second.repo").exists()
|
assert (dst / "etc/yum.repos.d/second.repo").exists()
|
||||||
|
|
||||||
|
|
||||||
def test_apply_sanitizes_repository_and_key_names_with_slash(
|
def test_apply_sanitizes_key_name_with_slash(tmp_path, monkeypatch) -> None:
|
||||||
tmp_path, monkeypatch
|
|
||||||
) -> None:
|
|
||||||
mock_run = _mock_gpg_success(monkeypatch)
|
mock_run = _mock_gpg_success(monkeypatch)
|
||||||
|
data = {
|
||||||
|
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||||
|
"organisation": "InterDiode",
|
||||||
|
"configurations": {
|
||||||
|
"yum": {"keys": [{"name": "../../evil-key", "content": KEY_CONTENT}]}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
dst = tmp_path / "dst"
|
||||||
|
context = Context(config=make_config(data), dst=dst, src=None)
|
||||||
|
|
||||||
|
YumModule().apply(context)
|
||||||
|
|
||||||
|
key_content = (dst / "etc/pki/rpm-gpg/RPM-GPG-KEY-.._.._evil-key").read_bytes()
|
||||||
|
assert key_content == b"fake-rpm-gpg-key-bytes"
|
||||||
|
mock_run.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_sanitizes_repository_name_with_disallowed_characters(tmp_path) -> None:
|
||||||
data = {
|
data = {
|
||||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||||
"organisation": "InterDiode",
|
"organisation": "InterDiode",
|
||||||
@@ -324,11 +340,10 @@ def test_apply_sanitizes_repository_and_key_names_with_slash(
|
|||||||
"yum": {
|
"yum": {
|
||||||
"repositories": [
|
"repositories": [
|
||||||
{
|
{
|
||||||
"name": "../../evil-repo",
|
"name": "weird/repo]here[bad",
|
||||||
"index-url": "https://example.invalid/yum",
|
"index-url": "https://example.invalid/yum",
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"keys": [{"name": "../../evil-key", "content": KEY_CONTENT}],
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -337,10 +352,16 @@ def test_apply_sanitizes_repository_and_key_names_with_slash(
|
|||||||
|
|
||||||
YumModule().apply(context)
|
YumModule().apply(context)
|
||||||
|
|
||||||
assert (dst / "etc/yum.repos.d/.._.._evil-repo.repo").exists()
|
repo_file = dst / "etc/yum.repos.d/weird_repo_here_bad.repo"
|
||||||
key_content = (dst / "etc/pki/rpm-gpg/RPM-GPG-KEY-.._.._evil-key").read_bytes()
|
assert repo_file.exists()
|
||||||
assert key_content == b"fake-rpm-gpg-key-bytes"
|
repo_content = repo_file.read_text(encoding="utf-8")
|
||||||
mock_run.assert_called_once()
|
assert repo_content == (
|
||||||
|
"[weird_repo_here_bad]\n"
|
||||||
|
"name=weird_repo_here_bad\n"
|
||||||
|
"baseurl=https://example.invalid/yum\n"
|
||||||
|
"enabled=1\n"
|
||||||
|
"gpgcheck=0\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_import_key_raises_when_gpg_binary_missing(tmp_path, monkeypatch) -> None:
|
def test_import_key_raises_when_gpg_binary_missing(tmp_path, monkeypatch) -> None:
|
||||||
|
|||||||
@@ -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,module-go,module-ruby,module-nuget,config-url
|
env_list = py313,py314,coverage,packaging,packaging-amd64,packaging-arm64,publish,module-python,module-python-http,module-apt,module-apt-http,module-yum,module-yum-http,module-apk,module-apk-http,module-java,module-java-http,module-certificates,module-php,module-php-http,module-npm,module-npm-http,module-rust,module-rust-http,module-go,module-go-http,module-ruby,module-ruby-http,module-nuget,module-nuget-http,config-url
|
||||||
isolated_build = true
|
isolated_build = true
|
||||||
requires =
|
requires =
|
||||||
tox>=4
|
tox>=4
|
||||||
@@ -91,6 +91,7 @@ passenv =
|
|||||||
dockerfile = {tox_root}/tests/dockerfiles/repos/pypi-repo.Dockerfile
|
dockerfile = {tox_root}/tests/dockerfiles/repos/pypi-repo.Dockerfile
|
||||||
expose =
|
expose =
|
||||||
PYPI_REPO_PORT=8443/tcp
|
PYPI_REPO_PORT=8443/tcp
|
||||||
|
PYPI_REPO_HTTP_PORT=8080/tcp
|
||||||
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/simple/idconfiguratortestpkg/ || exit 1
|
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/simple/idconfiguratortestpkg/ || exit 1
|
||||||
healthcheck_interval = 1
|
healthcheck_interval = 1
|
||||||
healthcheck_retries = 30
|
healthcheck_retries = 30
|
||||||
@@ -106,17 +107,41 @@ docker =
|
|||||||
pypi-repo
|
pypi-repo
|
||||||
allowlist_externals =
|
allowlist_externals =
|
||||||
docker
|
docker
|
||||||
|
set_env =
|
||||||
|
PYPI_REPO_SCHEME = https
|
||||||
commands =
|
commands =
|
||||||
python3 -m id_tools test render-config {tox_root}/tests/configs/Python.yaml.j2 {env_dir}/configs/Python.yaml
|
python3 -m id_tools test render-config {tox_root}/tests/configs/Python.yaml.j2 {env_dir}/configs/Python.yaml
|
||||||
docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-arg PYPI_REPO_PORT={env:PYPI_REPO_PORT} --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Python-alpine.Dockerfile -t id-configurator-python .
|
docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-arg PYPI_REPO_SCHEME={env:PYPI_REPO_SCHEME} --build-arg PYPI_REPO_PORT={env:PYPI_REPO_PORT} --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Python-alpine.Dockerfile -t id-configurator-python .
|
||||||
docker rmi id-configurator-python
|
docker rmi id-configurator-python
|
||||||
docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-arg PYPI_REPO_PORT={env:PYPI_REPO_PORT} --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Python.Dockerfile -t id-configurator-python .
|
docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-arg PYPI_REPO_SCHEME={env:PYPI_REPO_SCHEME} --build-arg PYPI_REPO_PORT={env:PYPI_REPO_PORT} --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Python.Dockerfile -t id-configurator-python .
|
||||||
docker rmi id-configurator-python
|
docker rmi id-configurator-python
|
||||||
|
|
||||||
|
[testenv:module-python-http]
|
||||||
|
package = skip
|
||||||
|
labels = end-to-end
|
||||||
|
change_dir = {tox_root}
|
||||||
|
dependency_groups = dev
|
||||||
|
docker =
|
||||||
|
pypi-repo
|
||||||
|
allowlist_externals =
|
||||||
|
docker
|
||||||
|
description =
|
||||||
|
Same as module-python, but against a plain-HTTP index, to verify pip's
|
||||||
|
trusted-host is configured correctly for a non-HTTPS repository.
|
||||||
|
set_env =
|
||||||
|
PYPI_REPO_SCHEME = http
|
||||||
|
commands =
|
||||||
|
python3 -m id_tools test render-config {tox_root}/tests/configs/Python.yaml.j2 {env_dir}/configs/Python.yaml
|
||||||
|
docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-arg PYPI_REPO_SCHEME={env:PYPI_REPO_SCHEME} --build-arg PYPI_REPO_PORT={env:PYPI_REPO_HTTP_PORT} --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Python-alpine.Dockerfile -t id-configurator-python-http .
|
||||||
|
docker rmi id-configurator-python-http
|
||||||
|
docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-arg PYPI_REPO_SCHEME={env:PYPI_REPO_SCHEME} --build-arg PYPI_REPO_PORT={env:PYPI_REPO_HTTP_PORT} --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Python.Dockerfile -t id-configurator-python-http .
|
||||||
|
docker rmi id-configurator-python-http
|
||||||
|
|
||||||
[docker:apt-repo]
|
[docker:apt-repo]
|
||||||
dockerfile = {tox_root}/tests/dockerfiles/repos/apt-repo.Dockerfile
|
dockerfile = {tox_root}/tests/dockerfiles/repos/apt-repo.Dockerfile
|
||||||
expose =
|
expose =
|
||||||
APT_REPO_PORT=8443/tcp
|
APT_REPO_PORT=8443/tcp
|
||||||
|
APT_REPO_HTTP_PORT=8080/tcp
|
||||||
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/Packages || exit 1
|
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/Packages || exit 1
|
||||||
healthcheck_interval = 1
|
healthcheck_interval = 1
|
||||||
healthcheck_retries = 30
|
healthcheck_retries = 30
|
||||||
@@ -132,15 +157,37 @@ docker =
|
|||||||
apt-repo
|
apt-repo
|
||||||
allowlist_externals =
|
allowlist_externals =
|
||||||
docker
|
docker
|
||||||
|
set_env =
|
||||||
|
APT_REPO_SCHEME = https
|
||||||
commands =
|
commands =
|
||||||
python3 -m id_tools test render-config {tox_root}/tests/configs/Apt.yaml.j2 {env_dir}/configs/Apt.yaml
|
python3 -m id_tools test render-config {tox_root}/tests/configs/Apt.yaml.j2 {env_dir}/configs/Apt.yaml
|
||||||
docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Apt.Dockerfile -t id-configurator-apt .
|
docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Apt.Dockerfile -t id-configurator-apt .
|
||||||
docker rmi id-configurator-apt
|
docker rmi id-configurator-apt
|
||||||
|
|
||||||
|
[testenv:module-apt-http]
|
||||||
|
package = skip
|
||||||
|
labels = end-to-end
|
||||||
|
change_dir = {tox_root}
|
||||||
|
dependency_groups = dev
|
||||||
|
docker =
|
||||||
|
apt-repo
|
||||||
|
allowlist_externals =
|
||||||
|
docker
|
||||||
|
description =
|
||||||
|
Same as module-apt, but against a plain-HTTP source, since apt's trust
|
||||||
|
model is the signed-by keyring, independent of transport scheme.
|
||||||
|
set_env =
|
||||||
|
APT_REPO_SCHEME = http
|
||||||
|
commands =
|
||||||
|
python3 -m id_tools test render-config {tox_root}/tests/configs/Apt.yaml.j2 {env_dir}/configs/Apt.yaml
|
||||||
|
docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Apt.Dockerfile -t id-configurator-apt-http .
|
||||||
|
docker rmi id-configurator-apt-http
|
||||||
|
|
||||||
[docker:yum-repo]
|
[docker:yum-repo]
|
||||||
dockerfile = {tox_root}/tests/dockerfiles/repos/yum-repo.Dockerfile
|
dockerfile = {tox_root}/tests/dockerfiles/repos/yum-repo.Dockerfile
|
||||||
expose =
|
expose =
|
||||||
YUM_REPO_PORT=8443/tcp
|
YUM_REPO_PORT=8443/tcp
|
||||||
|
YUM_REPO_HTTP_PORT=8080/tcp
|
||||||
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/repodata/repomd.xml || exit 1
|
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/repodata/repomd.xml || exit 1
|
||||||
healthcheck_interval = 1
|
healthcheck_interval = 1
|
||||||
healthcheck_retries = 30
|
healthcheck_retries = 30
|
||||||
@@ -156,15 +203,37 @@ docker =
|
|||||||
yum-repo
|
yum-repo
|
||||||
allowlist_externals =
|
allowlist_externals =
|
||||||
docker
|
docker
|
||||||
|
set_env =
|
||||||
|
YUM_REPO_SCHEME = https
|
||||||
commands =
|
commands =
|
||||||
python3 -m id_tools test render-config {tox_root}/tests/configs/Yum.yaml.j2 {env_dir}/configs/Yum.yaml
|
python3 -m id_tools test render-config {tox_root}/tests/configs/Yum.yaml.j2 {env_dir}/configs/Yum.yaml
|
||||||
docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Yum.Dockerfile -t id-configurator-yum .
|
docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Yum.Dockerfile -t id-configurator-yum .
|
||||||
docker rmi id-configurator-yum
|
docker rmi id-configurator-yum
|
||||||
|
|
||||||
|
[testenv:module-yum-http]
|
||||||
|
package = skip
|
||||||
|
labels = end-to-end
|
||||||
|
change_dir = {tox_root}
|
||||||
|
dependency_groups = dev
|
||||||
|
docker =
|
||||||
|
yum-repo
|
||||||
|
allowlist_externals =
|
||||||
|
docker
|
||||||
|
description =
|
||||||
|
Same as module-yum, but against a plain-HTTP repository, since yum/dnf's
|
||||||
|
trust model is the imported GPG key, independent of transport scheme.
|
||||||
|
set_env =
|
||||||
|
YUM_REPO_SCHEME = http
|
||||||
|
commands =
|
||||||
|
python3 -m id_tools test render-config {tox_root}/tests/configs/Yum.yaml.j2 {env_dir}/configs/Yum.yaml
|
||||||
|
docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Yum.Dockerfile -t id-configurator-yum-http .
|
||||||
|
docker rmi id-configurator-yum-http
|
||||||
|
|
||||||
[docker:apk-repo]
|
[docker:apk-repo]
|
||||||
dockerfile = {tox_root}/tests/dockerfiles/repos/apk-repo.Dockerfile
|
dockerfile = {tox_root}/tests/dockerfiles/repos/apk-repo.Dockerfile
|
||||||
expose =
|
expose =
|
||||||
APK_REPO_PORT=8443/tcp
|
APK_REPO_PORT=8443/tcp
|
||||||
|
APK_REPO_HTTP_PORT=8080/tcp
|
||||||
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/APKINDEX.tar.gz || exit 1
|
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/APKINDEX.tar.gz || exit 1
|
||||||
healthcheck_interval = 1
|
healthcheck_interval = 1
|
||||||
healthcheck_retries = 30
|
healthcheck_retries = 30
|
||||||
@@ -189,15 +258,39 @@ description =
|
|||||||
native platform keeps them in agreement everywhere (including under
|
native platform keeps them in agreement everywhere (including under
|
||||||
QEMU emulation on non-amd64 hosts); CI runners are amd64, so this still
|
QEMU emulation on non-amd64 hosts); CI runners are amd64, so this still
|
||||||
matches the other envs there.
|
matches the other envs there.
|
||||||
|
set_env =
|
||||||
|
APK_REPO_SCHEME = https
|
||||||
commands =
|
commands =
|
||||||
python3 -m id_tools test render-config {tox_root}/tests/configs/Apk.yaml.j2 {env_dir}/configs/Apk.yaml
|
python3 -m id_tools test render-config {tox_root}/tests/configs/Apk.yaml.j2 {env_dir}/configs/Apk.yaml
|
||||||
docker buildx build --add-host=host.docker.internal:host-gateway --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Apk.Dockerfile -t id-configurator-apk .
|
docker buildx build --add-host=host.docker.internal:host-gateway --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Apk.Dockerfile -t id-configurator-apk .
|
||||||
docker rmi id-configurator-apk
|
docker rmi id-configurator-apk
|
||||||
|
|
||||||
|
[testenv:module-apk-http]
|
||||||
|
package = skip
|
||||||
|
labels = end-to-end
|
||||||
|
change_dir = {tox_root}
|
||||||
|
dependency_groups = dev
|
||||||
|
docker =
|
||||||
|
apk-repo
|
||||||
|
allowlist_externals =
|
||||||
|
docker
|
||||||
|
description =
|
||||||
|
Same as module-apk, but against a plain-HTTP repository, since apk's
|
||||||
|
trust model is the signed index, independent of transport scheme. See
|
||||||
|
module-apk's own description for why this isn't pinned to --platform
|
||||||
|
linux/amd64 either.
|
||||||
|
set_env =
|
||||||
|
APK_REPO_SCHEME = http
|
||||||
|
commands =
|
||||||
|
python3 -m id_tools test render-config {tox_root}/tests/configs/Apk.yaml.j2 {env_dir}/configs/Apk.yaml
|
||||||
|
docker buildx build --add-host=host.docker.internal:host-gateway --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Apk.Dockerfile -t id-configurator-apk-http .
|
||||||
|
docker rmi id-configurator-apk-http
|
||||||
|
|
||||||
[docker:java-repo]
|
[docker:java-repo]
|
||||||
dockerfile = {tox_root}/tests/dockerfiles/repos/java-repo.Dockerfile
|
dockerfile = {tox_root}/tests/dockerfiles/repos/java-repo.Dockerfile
|
||||||
expose =
|
expose =
|
||||||
JAVA_REPO_PORT=8443/tcp
|
JAVA_REPO_PORT=8443/tcp
|
||||||
|
JAVA_REPO_HTTP_PORT=8080/tcp
|
||||||
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/org/interdiode/idconfiguratortestpkg/0.0.1/idconfiguratortestpkg-0.0.1.pom || exit 1
|
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/org/interdiode/idconfiguratortestpkg/0.0.1/idconfiguratortestpkg-0.0.1.pom || exit 1
|
||||||
healthcheck_interval = 1
|
healthcheck_interval = 1
|
||||||
healthcheck_retries = 30
|
healthcheck_retries = 30
|
||||||
@@ -213,11 +306,33 @@ docker =
|
|||||||
java-repo
|
java-repo
|
||||||
allowlist_externals =
|
allowlist_externals =
|
||||||
docker
|
docker
|
||||||
|
set_env =
|
||||||
|
JAVA_REPO_SCHEME = https
|
||||||
commands =
|
commands =
|
||||||
python3 -m id_tools test render-config {tox_root}/tests/configs/Java.yaml.j2 {env_dir}/configs/Java.yaml
|
python3 -m id_tools test render-config {tox_root}/tests/configs/Java.yaml.j2 {env_dir}/configs/Java.yaml
|
||||||
docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Java.Dockerfile -t id-configurator-java .
|
docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-arg JAVA_REPO_SCHEME={env:JAVA_REPO_SCHEME} --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Java.Dockerfile -t id-configurator-java .
|
||||||
docker rmi id-configurator-java
|
docker rmi id-configurator-java
|
||||||
|
|
||||||
|
[testenv:module-java-http]
|
||||||
|
package = skip
|
||||||
|
labels = end-to-end
|
||||||
|
change_dir = {tox_root}
|
||||||
|
dependency_groups = dev
|
||||||
|
docker =
|
||||||
|
java-repo
|
||||||
|
allowlist_externals =
|
||||||
|
docker
|
||||||
|
description =
|
||||||
|
Same as module-java, but against a plain-HTTP repository. Leiningen is
|
||||||
|
skipped here (see Java.Dockerfile): it refuses any non-HTTPS repository
|
||||||
|
unconditionally, with no configuration able to change that.
|
||||||
|
set_env =
|
||||||
|
JAVA_REPO_SCHEME = http
|
||||||
|
commands =
|
||||||
|
python3 -m id_tools test render-config {tox_root}/tests/configs/Java.yaml.j2 {env_dir}/configs/Java.yaml
|
||||||
|
docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-arg JAVA_REPO_SCHEME={env:JAVA_REPO_SCHEME} --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Java.Dockerfile -t id-configurator-java-http .
|
||||||
|
docker rmi id-configurator-java-http
|
||||||
|
|
||||||
[docker:certificate-repo]
|
[docker:certificate-repo]
|
||||||
dockerfile = {tox_root}/tests/dockerfiles/repos/certificate-repo.Dockerfile
|
dockerfile = {tox_root}/tests/dockerfiles/repos/certificate-repo.Dockerfile
|
||||||
expose =
|
expose =
|
||||||
@@ -251,6 +366,7 @@ commands =
|
|||||||
dockerfile = {tox_root}/tests/dockerfiles/repos/php-repo.Dockerfile
|
dockerfile = {tox_root}/tests/dockerfiles/repos/php-repo.Dockerfile
|
||||||
expose =
|
expose =
|
||||||
PHP_REPO_PORT=8443/tcp
|
PHP_REPO_PORT=8443/tcp
|
||||||
|
PHP_REPO_HTTP_PORT=8080/tcp
|
||||||
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/packages.json || exit 1
|
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/packages.json || exit 1
|
||||||
healthcheck_interval = 1
|
healthcheck_interval = 1
|
||||||
healthcheck_retries = 30
|
healthcheck_retries = 30
|
||||||
@@ -266,15 +382,37 @@ docker =
|
|||||||
php-repo
|
php-repo
|
||||||
allowlist_externals =
|
allowlist_externals =
|
||||||
docker
|
docker
|
||||||
|
set_env =
|
||||||
|
PHP_REPO_SCHEME = https
|
||||||
commands =
|
commands =
|
||||||
python3 -m id_tools test render-config {tox_root}/tests/configs/Php.yaml.j2 {env_dir}/configs/Php.yaml
|
python3 -m id_tools test render-config {tox_root}/tests/configs/Php.yaml.j2 {env_dir}/configs/Php.yaml
|
||||||
docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Php.Dockerfile -t id-configurator-php .
|
docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Php.Dockerfile -t id-configurator-php .
|
||||||
docker rmi id-configurator-php
|
docker rmi id-configurator-php
|
||||||
|
|
||||||
|
[testenv:module-php-http]
|
||||||
|
package = skip
|
||||||
|
labels = end-to-end
|
||||||
|
change_dir = {tox_root}
|
||||||
|
dependency_groups = dev
|
||||||
|
docker =
|
||||||
|
php-repo
|
||||||
|
allowlist_externals =
|
||||||
|
docker
|
||||||
|
description =
|
||||||
|
Same as module-php, but against a plain-HTTP repository, to verify
|
||||||
|
secure-http is disabled (and packagist.org too) for a non-HTTPS source.
|
||||||
|
set_env =
|
||||||
|
PHP_REPO_SCHEME = http
|
||||||
|
commands =
|
||||||
|
python3 -m id_tools test render-config {tox_root}/tests/configs/Php.yaml.j2 {env_dir}/configs/Php.yaml
|
||||||
|
docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Php.Dockerfile -t id-configurator-php-http .
|
||||||
|
docker rmi id-configurator-php-http
|
||||||
|
|
||||||
[docker:npm-repo]
|
[docker:npm-repo]
|
||||||
dockerfile = {tox_root}/tests/dockerfiles/repos/npm-repo.Dockerfile
|
dockerfile = {tox_root}/tests/dockerfiles/repos/npm-repo.Dockerfile
|
||||||
expose =
|
expose =
|
||||||
NPM_REPO_PORT=8443/tcp
|
NPM_REPO_PORT=8443/tcp
|
||||||
|
NPM_REPO_HTTP_PORT=8080/tcp
|
||||||
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/idconfiguratortestpkg || exit 1
|
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/idconfiguratortestpkg || exit 1
|
||||||
healthcheck_interval = 1
|
healthcheck_interval = 1
|
||||||
healthcheck_retries = 30
|
healthcheck_retries = 30
|
||||||
@@ -290,15 +428,36 @@ docker =
|
|||||||
npm-repo
|
npm-repo
|
||||||
allowlist_externals =
|
allowlist_externals =
|
||||||
docker
|
docker
|
||||||
|
set_env =
|
||||||
|
NPM_REPO_SCHEME = https
|
||||||
commands =
|
commands =
|
||||||
python3 -m id_tools test render-config {tox_root}/tests/configs/Npm.yaml.j2 {env_dir}/configs/Npm.yaml
|
python3 -m id_tools test render-config {tox_root}/tests/configs/Npm.yaml.j2 {env_dir}/configs/Npm.yaml
|
||||||
docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Npm.Dockerfile -t id-configurator-npm .
|
docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Npm.Dockerfile -t id-configurator-npm .
|
||||||
docker rmi id-configurator-npm
|
docker rmi id-configurator-npm
|
||||||
|
|
||||||
|
[testenv:module-npm-http]
|
||||||
|
package = skip
|
||||||
|
labels = end-to-end
|
||||||
|
change_dir = {tox_root}
|
||||||
|
dependency_groups = dev
|
||||||
|
docker =
|
||||||
|
npm-repo
|
||||||
|
allowlist_externals =
|
||||||
|
docker
|
||||||
|
description =
|
||||||
|
Same as module-npm, but against a plain-HTTP registry.
|
||||||
|
set_env =
|
||||||
|
NPM_REPO_SCHEME = http
|
||||||
|
commands =
|
||||||
|
python3 -m id_tools test render-config {tox_root}/tests/configs/Npm.yaml.j2 {env_dir}/configs/Npm.yaml
|
||||||
|
docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Npm.Dockerfile -t id-configurator-npm-http .
|
||||||
|
docker rmi id-configurator-npm-http
|
||||||
|
|
||||||
[docker:cargo-repo]
|
[docker:cargo-repo]
|
||||||
dockerfile = {tox_root}/tests/dockerfiles/repos/cargo-repo.Dockerfile
|
dockerfile = {tox_root}/tests/dockerfiles/repos/cargo-repo.Dockerfile
|
||||||
expose =
|
expose =
|
||||||
CARGO_REPO_PORT=8443/tcp
|
CARGO_REPO_PORT=8443/tcp
|
||||||
|
CARGO_REPO_HTTP_PORT=8080/tcp
|
||||||
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/config.json || exit 1
|
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/config.json || exit 1
|
||||||
healthcheck_interval = 1
|
healthcheck_interval = 1
|
||||||
healthcheck_retries = 30
|
healthcheck_retries = 30
|
||||||
@@ -314,15 +473,36 @@ docker =
|
|||||||
cargo-repo
|
cargo-repo
|
||||||
allowlist_externals =
|
allowlist_externals =
|
||||||
docker
|
docker
|
||||||
|
set_env =
|
||||||
|
CARGO_REPO_SCHEME = https
|
||||||
commands =
|
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
|
||||||
|
|
||||||
|
[testenv:module-rust-http]
|
||||||
|
package = skip
|
||||||
|
labels = end-to-end
|
||||||
|
change_dir = {tox_root}
|
||||||
|
dependency_groups = dev
|
||||||
|
docker =
|
||||||
|
cargo-repo
|
||||||
|
allowlist_externals =
|
||||||
|
docker
|
||||||
|
description =
|
||||||
|
Same as module-rust, but against a plain-HTTP sparse registry.
|
||||||
|
set_env =
|
||||||
|
CARGO_REPO_SCHEME = http
|
||||||
|
commands =
|
||||||
|
python3 -m id_tools test render-config {tox_root}/tests/configs/Rust.yaml.j2 {env_dir}/configs/Rust.yaml
|
||||||
|
docker buildx build --platform linux/amd64 --add-host=host.docker.internal:host-gateway --build-context generated-config={env_dir}/configs -f {tox_root}/tests/dockerfiles/Rust.Dockerfile -t id-configurator-rust-http .
|
||||||
|
docker rmi id-configurator-rust-http
|
||||||
|
|
||||||
[docker:go-repo]
|
[docker:go-repo]
|
||||||
dockerfile = {tox_root}/tests/dockerfiles/repos/go-repo.Dockerfile
|
dockerfile = {tox_root}/tests/dockerfiles/repos/go-repo.Dockerfile
|
||||||
expose =
|
expose =
|
||||||
GO_REPO_PORT=8443/tcp
|
GO_REPO_PORT=8443/tcp
|
||||||
|
GO_REPO_HTTP_PORT=8080/tcp
|
||||||
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/example.invalid/idconfiguratortestpkg/@v/list || exit 1
|
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/example.invalid/idconfiguratortestpkg/@v/list || exit 1
|
||||||
healthcheck_interval = 1
|
healthcheck_interval = 1
|
||||||
healthcheck_retries = 30
|
healthcheck_retries = 30
|
||||||
@@ -338,15 +518,37 @@ docker =
|
|||||||
go-repo
|
go-repo
|
||||||
allowlist_externals =
|
allowlist_externals =
|
||||||
docker
|
docker
|
||||||
|
set_env =
|
||||||
|
GO_REPO_SCHEME = https
|
||||||
commands =
|
commands =
|
||||||
python3 -m id_tools test render-config {tox_root}/tests/configs/Go.yaml.j2 {env_dir}/configs/Go.yaml
|
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 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 rmi id-configurator-go
|
||||||
|
|
||||||
|
[testenv:module-go-http]
|
||||||
|
package = skip
|
||||||
|
labels = end-to-end
|
||||||
|
change_dir = {tox_root}
|
||||||
|
dependency_groups = dev
|
||||||
|
docker =
|
||||||
|
go-repo
|
||||||
|
allowlist_externals =
|
||||||
|
docker
|
||||||
|
description =
|
||||||
|
Same as module-go, but against a plain-HTTP module proxy, to verify
|
||||||
|
GOINSECURE is configured correctly for a non-HTTPS GOPROXY.
|
||||||
|
set_env =
|
||||||
|
GO_REPO_SCHEME = http
|
||||||
|
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-http .
|
||||||
|
docker rmi id-configurator-go-http
|
||||||
|
|
||||||
[docker:ruby-repo]
|
[docker:ruby-repo]
|
||||||
dockerfile = {tox_root}/tests/dockerfiles/repos/ruby-repo.Dockerfile
|
dockerfile = {tox_root}/tests/dockerfiles/repos/ruby-repo.Dockerfile
|
||||||
expose =
|
expose =
|
||||||
RUBY_REPO_PORT=8443/tcp
|
RUBY_REPO_PORT=8443/tcp
|
||||||
|
RUBY_REPO_HTTP_PORT=8080/tcp
|
||||||
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/versions || exit 1
|
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/versions || exit 1
|
||||||
healthcheck_interval = 1
|
healthcheck_interval = 1
|
||||||
healthcheck_retries = 30
|
healthcheck_retries = 30
|
||||||
@@ -362,15 +564,36 @@ docker =
|
|||||||
ruby-repo
|
ruby-repo
|
||||||
allowlist_externals =
|
allowlist_externals =
|
||||||
docker
|
docker
|
||||||
|
set_env =
|
||||||
|
RUBY_REPO_SCHEME = https
|
||||||
commands =
|
commands =
|
||||||
python3 -m id_tools test render-config {tox_root}/tests/configs/Ruby.yaml.j2 {env_dir}/configs/Ruby.yaml
|
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 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 rmi id-configurator-ruby
|
||||||
|
|
||||||
|
[testenv:module-ruby-http]
|
||||||
|
package = skip
|
||||||
|
labels = end-to-end
|
||||||
|
change_dir = {tox_root}
|
||||||
|
dependency_groups = dev
|
||||||
|
docker =
|
||||||
|
ruby-repo
|
||||||
|
allowlist_externals =
|
||||||
|
docker
|
||||||
|
description =
|
||||||
|
Same as module-ruby, but against a plain-HTTP gem source.
|
||||||
|
set_env =
|
||||||
|
RUBY_REPO_SCHEME = http
|
||||||
|
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-http .
|
||||||
|
docker rmi id-configurator-ruby-http
|
||||||
|
|
||||||
[docker:nuget-repo]
|
[docker:nuget-repo]
|
||||||
dockerfile = {tox_root}/tests/dockerfiles/repos/nuget-repo.Dockerfile
|
dockerfile = {tox_root}/tests/dockerfiles/repos/nuget-repo.Dockerfile
|
||||||
expose =
|
expose =
|
||||||
NUGET_REPO_PORT=8443/tcp
|
NUGET_REPO_PORT=8443/tcp
|
||||||
|
NUGET_REPO_HTTP_PORT=8080/tcp
|
||||||
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/index.json || exit 1
|
healthcheck_cmd = wget --no-check-certificate -qO- https://127.0.0.1:8443/index.json || exit 1
|
||||||
healthcheck_interval = 1
|
healthcheck_interval = 1
|
||||||
healthcheck_retries = 30
|
healthcheck_retries = 30
|
||||||
@@ -386,11 +609,32 @@ docker =
|
|||||||
nuget-repo
|
nuget-repo
|
||||||
allowlist_externals =
|
allowlist_externals =
|
||||||
docker
|
docker
|
||||||
|
set_env =
|
||||||
|
NUGET_REPO_SCHEME = https
|
||||||
commands =
|
commands =
|
||||||
python3 -m id_tools test render-config {tox_root}/tests/configs/NuGet.yaml.j2 {env_dir}/configs/NuGet.yaml
|
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 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 rmi id-configurator-nuget
|
||||||
|
|
||||||
|
[testenv:module-nuget-http]
|
||||||
|
package = skip
|
||||||
|
labels = end-to-end
|
||||||
|
change_dir = {tox_root}
|
||||||
|
dependency_groups = dev
|
||||||
|
docker =
|
||||||
|
nuget-repo
|
||||||
|
allowlist_externals =
|
||||||
|
docker
|
||||||
|
description =
|
||||||
|
Same as module-nuget, but against a plain-HTTP feed, to verify
|
||||||
|
allowInsecureConnections is configured correctly for a non-HTTPS source.
|
||||||
|
set_env =
|
||||||
|
NUGET_REPO_SCHEME = http
|
||||||
|
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-http .
|
||||||
|
docker rmi id-configurator-nuget-http
|
||||||
|
|
||||||
[docker:config-url-repo]
|
[docker:config-url-repo]
|
||||||
dockerfile = {tox_root}/tests/dockerfiles/repos/config-url-repo.Dockerfile
|
dockerfile = {tox_root}/tests/dockerfiles/repos/config-url-repo.Dockerfile
|
||||||
expose =
|
expose =
|
||||||
|
|||||||
@@ -407,11 +407,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "filelock"
|
name = "filelock"
|
||||||
sdist = {url = "https://files.pythonhosted.org/packages/4b/51/a182494d1d8dde1240bff84dda57d48165d982e59582ce8f167e8e3d7628/filelock-4.0.10.tar.gz", hash = "sha256:00d6a81f976a6332551c2c10f39e12b4abb7e01c64d4c497b81a615bc9186f1f", size = 252487, upload-time = "2026-10-03T23:40:48.884Z"}
|
sdist = {url = "https://files.pythonhosted.org/packages/d8/9c/925c4f62920e4200d573d2bfe5cfd1b993134c3aee5f9bd3b7c55e20297e/filelock-4.0.11.tar.gz", hash = "sha256:9dc453a1edd25626a6fc97c2cb1bd9f7e46345843d40b3181c071761fb9a5f7a", size = 253844, upload-time = "2026-10-05T00:57:32.47Z"}
|
||||||
source = {registry = "https://pypi.org/simple"}
|
source = {registry = "https://pypi.org/simple"}
|
||||||
version = "4.0.10"
|
version = "4.0.11"
|
||||||
wheels = [
|
wheels = [
|
||||||
{url = "https://files.pythonhosted.org/packages/5b/2a/4b1ea3f2180be251a5cff1e70c8a5b7994ffa5c8eab8937901f8a9db58fe/filelock-4.0.10-py3-none-any.whl", hash = "sha256:698048d158dc3c9e18cc5e88b3efdd4bd0e77d76686b22adb4511cc4b2c2f0db", size = 111193, upload-time = "2026-10-03T23:40:47.334Z"}
|
{url = "https://files.pythonhosted.org/packages/4a/4d/2983a9fc0a412a09151299d1ec9eb7511e3d577f7213e19395fabebf9db6/filelock-4.0.11-py3-none-any.whl", hash = "sha256:1cbdc4ace7fb11d2f6d073f5c3d91537a23b3d58d2bf2fdffc12c6d717a589c6", size = 111688, upload-time = "2026-10-05T00:57:30.896Z"}
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -424,7 +424,7 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
name = "id-configurator"
|
name = "id-configurator"
|
||||||
source = {editable = "."}
|
source = {editable = "."}
|
||||||
version = "2026.10.3"
|
version = "2026.10.6"
|
||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
@@ -678,32 +678,37 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pycryptodomex"
|
name = "pycryptodomex"
|
||||||
sdist = {url = "https://files.pythonhosted.org/packages/c9/85/e24bf90972a30b0fcd16c73009add1d7d7cd9140c2498a68252028899e41/pycryptodomex-3.23.0.tar.gz", hash = "sha256:71909758f010c82bc99b0abf4ea12012c98962fbf0583c2164f8b84533c2e4da", size = 4922157, upload-time = "2025-05-17T17:23:41.434Z"}
|
sdist = {url = "https://files.pythonhosted.org/packages/4c/25/214ea825a9031f5af2c8b2506ee16701a2560d4712165dd00098dd527bcb/pycryptodomex-3.24.0.tar.gz", hash = "sha256:0428f19f13452c6b89bbaf2c530f84f369873811dfa77f0cee0da4f40fb0474f", size = 4933136, upload-time = "2026-10-04T17:41:08.988Z"}
|
||||||
source = {registry = "https://pypi.org/simple"}
|
source = {registry = "https://pypi.org/simple"}
|
||||||
version = "3.23.0"
|
version = "3.24.0"
|
||||||
wheels = [
|
wheels = [
|
||||||
{url = "https://files.pythonhosted.org/packages/2e/00/10edb04777069a42490a38c137099d4b17ba6e36a4e6e28bdc7470e9e853/pycryptodomex-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:7b37e08e3871efe2187bc1fd9320cc81d87caf19816c648f24443483005ff886", size = 2498764, upload-time = "2025-05-17T17:22:21.453Z"},
|
{url = "https://files.pythonhosted.org/packages/0f/52/5235086483b462600958549b27683ccffbb4941ba512934efc5a14b71a39/pycryptodomex-3.24.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:abf768c285d289c4a0e0678f055663162c9d6257c79f5904fbd2692ade94da1c", size = 2475900, upload-time = "2026-10-04T17:39:26.374Z"},
|
||||||
{url = "https://files.pythonhosted.org/packages/6b/3f/2872a9c2d3a27eac094f9ceaa5a8a483b774ae69018040ea3240d5b11154/pycryptodomex-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:91979028227543010d7b2ba2471cf1d1e398b3f183cb105ac584df0c36dac28d", size = 1643012, upload-time = "2025-05-17T17:22:23.702Z"},
|
{url = "https://files.pythonhosted.org/packages/9e/cd/eeed4b1a1eda71a78defe2c04ff37909461f9c04cd4bde23011d4350cbfb/pycryptodomex-3.24.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c2f42f24d7112ebb93d3aed581d011c82c20101da7d7a6248145c530e99bd2eb", size = 1643488, upload-time = "2026-10-04T17:39:28.545Z"},
|
||||||
{url = "https://files.pythonhosted.org/packages/70/af/774c2e2b4f6570fbf6a4972161adbb183aeeaa1863bde31e8706f123bf92/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8962204c47464d5c1c4038abeadd4514a133b28748bcd9fa5b6d62e3cec6fa", size = 2187643, upload-time = "2025-05-17T17:22:26.37Z"},
|
{url = "https://files.pythonhosted.org/packages/18/d2/22005d736858dddd3f7a5ddaf4a91dbb89292f0d9c503a9c13b1bb2ecfb8/pycryptodomex-3.24.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e42a2e9531c40315290278d9eee93ea5031eda3c83969f5709ada51a1c7491b4", size = 2194768, upload-time = "2026-10-04T17:39:31.713Z"},
|
||||||
{url = "https://files.pythonhosted.org/packages/de/a3/71065b24cb889d537954cedc3ae5466af00a2cabcff8e29b73be047e9a19/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a33986a0066860f7fcf7c7bd2bc804fa90e434183645595ae7b33d01f3c91ed8", size = 2273762, upload-time = "2025-05-17T17:22:28.313Z"},
|
{url = "https://files.pythonhosted.org/packages/19/c4/c350bb48207ae2a6cca02eb45a2dafc3f2d0196b5b33b556cc9b6815e8cc/pycryptodomex-3.24.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c7055400f773856cd219680d15525c6fb147237534cbe5394a5f92f57f7efaed", size = 2280612, upload-time = "2026-10-04T17:39:34.828Z"},
|
||||||
{url = "https://files.pythonhosted.org/packages/c9/0b/ff6f43b7fbef4d302c8b981fe58467b8871902cdc3eb28896b52421422cc/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7947ab8d589e3178da3d7cdeabe14f841b391e17046954f2fbcd941705762b5", size = 2313012, upload-time = "2025-05-17T17:22:30.57Z"},
|
{url = "https://files.pythonhosted.org/packages/fe/56/539e213fbd572f8e95cdef5a46b0c327c7b2412107c3dba73913cfd20753/pycryptodomex-3.24.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e8a7463d346015b42a0fb9c9179ebb7cccfd86bf40199c6796b546ac7be0b85c", size = 2186291, upload-time = "2026-10-04T17:39:38.184Z"},
|
||||||
{url = "https://files.pythonhosted.org/packages/02/de/9d4772c0506ab6da10b41159493657105d3f8bb5c53615d19452afc6b315/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c25e30a20e1b426e1f0fa00131c516f16e474204eee1139d1603e132acffc314", size = 2186856, upload-time = "2025-05-17T17:22:32.819Z"},
|
{url = "https://files.pythonhosted.org/packages/9b/f3/f8e73be610ef576c6ca98a78c8450a72f12c178bc89d3e94068e48115c97/pycryptodomex-3.24.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f96e10f0edff9d4b6b253e0a78f7c819749521d3ffe6af0139e08fcd35384405", size = 2278371, upload-time = "2026-10-04T17:39:41.539Z"},
|
||||||
{url = "https://files.pythonhosted.org/packages/28/ad/8b30efcd6341707a234e5eba5493700a17852ca1ac7a75daa7945fcf6427/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:da4fa650cef02db88c2b98acc5434461e027dce0ae8c22dd5a69013eaf510006", size = 2347523, upload-time = "2025-05-17T17:22:35.386Z"},
|
{url = "https://files.pythonhosted.org/packages/16/bd/99c89964c5017488992f412f8d645923d8ef996c7f35fc76b7fe019620e1/pycryptodomex-3.24.0-cp313-cp313t-win32.whl", hash = "sha256:3b6276fe007464502df796710fb57dcfeb813b6ccac0eabaf85100be1ad358f2", size = 1792741, upload-time = "2026-10-04T17:39:44.173Z"},
|
||||||
{url = "https://files.pythonhosted.org/packages/0f/02/16868e9f655b7670dbb0ac4f2844145cbc42251f916fc35c414ad2359849/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:58b851b9effd0d072d4ca2e4542bf2a4abcf13c82a29fd2c93ce27ee2a2e9462", size = 2272825, upload-time = "2025-05-17T17:22:37.632Z"},
|
{url = "https://files.pythonhosted.org/packages/ca/d5/5a1e469c7eab20c21090d396342894d2f0e25d429eb4df2d9174a03cb226/pycryptodomex-3.24.0-cp313-cp313t-win_amd64.whl", hash = "sha256:88d37b28b450c06a03bd5570467c601fe8c7dbe493ca39771716f56fdbe35d6e", size = 1825623, upload-time = "2026-10-04T17:39:46.422Z"},
|
||||||
{url = "https://files.pythonhosted.org/packages/ca/18/4ca89ac737230b52ac8ffaca42f9c6f1fd07c81a6cd821e91af79db60632/pycryptodomex-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:a9d446e844f08299236780f2efa9898c818fe7e02f17263866b8550c7d5fb328", size = 1772078, upload-time = "2025-05-17T17:22:40Z"},
|
{url = "https://files.pythonhosted.org/packages/1b/fb/1ca14c7e5318125aff23cd19886a62c8eb9015caa373b5834216c60260a5/pycryptodomex-3.24.0-cp313-cp313t-win_arm64.whl", hash = "sha256:e635c7ac056f2a47d79e2bf7042fb3fc6d175f47796a954388a3cec1f2e082a4", size = 1759392, upload-time = "2026-10-04T17:39:48.676Z"},
|
||||||
{url = "https://files.pythonhosted.org/packages/73/34/13e01c322db027682e00986873eca803f11c56ade9ba5bbf3225841ea2d4/pycryptodomex-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bc65bdd9fc8de7a35a74cab1c898cab391a4add33a8fe740bda00f5976ca4708", size = 1803656, upload-time = "2025-05-17T17:22:42.139Z"},
|
{url = "https://files.pythonhosted.org/packages/a0/dd/8f2d18986f752ceb245d4c3aeb0b13e9ed329d64434969981f0746b5b2ff/pycryptodomex-3.24.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:f6af77f15a91bb6c16607067cfd201330cf5dfefe1c715e955be1fb06d368613", size = 2476818, upload-time = "2026-10-04T17:39:51.311Z"},
|
||||||
{url = "https://files.pythonhosted.org/packages/54/68/9504c8796b1805d58f4425002bcca20f12880e6fa4dc2fc9a668705c7a08/pycryptodomex-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c885da45e70139464f082018ac527fdaad26f1657a99ee13eecdce0f0ca24ab4", size = 1707172, upload-time = "2025-05-17T17:22:44.704Z"},
|
{url = "https://files.pythonhosted.org/packages/8a/64/6bc2394a249d1c7ba330d192e4bf22d7e5a8caf28ef1dd346b8b847b91b3/pycryptodomex-3.24.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:582efdf767d72c12538949b876a64b9a7af30f2cff063c06dd63ece32c4e5c1d", size = 1645659, upload-time = "2026-10-04T17:39:53.936Z"},
|
||||||
{url = "https://files.pythonhosted.org/packages/dd/9c/1a8f35daa39784ed8adf93a694e7e5dc15c23c741bbda06e1d45f8979e9e/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:06698f957fe1ab229a99ba2defeeae1c09af185baa909a31a5d1f9d42b1aaed6", size = 2499240, upload-time = "2025-05-17T17:22:46.953Z"},
|
{url = "https://files.pythonhosted.org/packages/46/36/f720afe32331ebf200bc37d335120f74da371969722d3c188fac757a8a71/pycryptodomex-3.24.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be79564e05ed620a63f353ea50e0d474eb598f70cda1b28c2ca8e9e2f02c139", size = 2194769, upload-time = "2026-10-04T17:39:57.357Z"},
|
||||||
{url = "https://files.pythonhosted.org/packages/7a/62/f5221a191a97157d240cf6643747558759126c76ee92f29a3f4aee3197a5/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2c2537863eccef2d41061e82a881dcabb04944c5c06c5aa7110b577cc487545", size = 1644042, upload-time = "2025-05-17T17:22:49.098Z"},
|
{url = "https://files.pythonhosted.org/packages/05/ac/d0c3b629a9e91c9dee019fa59cdf9617b84b0d11909541e1d6c79b24637c/pycryptodomex-3.24.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb393480b5845ef4acbc9f713e4c473ad08be81bc3cdb5ef017cd02bdafd36e1", size = 2280609, upload-time = "2026-10-04T17:39:59.915Z"},
|
||||||
{url = "https://files.pythonhosted.org/packages/8c/fd/5a054543c8988d4ed7b612721d7e78a4b9bf36bc3c5ad45ef45c22d0060e/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43c446e2ba8df8889e0e16f02211c25b4934898384c1ec1ec04d7889c0333587", size = 2186227, upload-time = "2025-05-17T17:22:51.139Z"},
|
{url = "https://files.pythonhosted.org/packages/5f/0e/e64276b7f3dbe9aeabd9c80dfff10bc97740ad8e24892eb6b0e3cbdcd549/pycryptodomex-3.24.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7b8ad51baf3505dd3d94ec974855359d15769e45c58c2b89c6d4612b1f7ed94", size = 2186290, upload-time = "2026-10-04T17:40:02.856Z"},
|
||||||
{url = "https://files.pythonhosted.org/packages/c8/a9/8862616a85cf450d2822dbd4fff1fcaba90877907a6ff5bc2672cafe42f8/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f489c4765093fb60e2edafdf223397bc716491b2b69fe74367b70d6999257a5c", size = 2272578, upload-time = "2025-05-17T17:22:53.676Z"},
|
{url = "https://files.pythonhosted.org/packages/42/2f/b72cc71bff456971e24e886f1824d2efef7b25fbd806eb821b107f8253e6/pycryptodomex-3.24.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7ffcedf4210d8c206fe9852ccde9f78ec9ce35e8039f0a989900ffe710e7a5db", size = 2278371, upload-time = "2026-10-04T17:40:06.135Z"},
|
||||||
{url = "https://files.pythonhosted.org/packages/46/9f/bda9c49a7c1842820de674ab36c79f4fbeeee03f8ff0e4f3546c3889076b/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdc69d0d3d989a1029df0eed67cc5e8e5d968f3724f4519bd03e0ec68df7543c", size = 2312166, upload-time = "2025-05-17T17:22:56.585Z"},
|
{url = "https://files.pythonhosted.org/packages/70/b0/9d0f0d4d6236f986c942d2933d4a5cbc209ebac1ea7d4c74b6e306efb439/pycryptodomex-3.24.0-cp314-cp314t-win32.whl", hash = "sha256:8d6c5e01e4fb1ddd20cc24d4d8706643dd78b5279453dabcf10ba4e0683bf9b2", size = 1808717, upload-time = "2026-10-04T17:40:09.172Z"},
|
||||||
{url = "https://files.pythonhosted.org/packages/03/cc/870b9bf8ca92866ca0186534801cf8d20554ad2a76ca959538041b7a7cf4/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6bbcb1dd0f646484939e142462d9e532482bc74475cecf9c4903d4e1cd21f003", size = 2185467, upload-time = "2025-05-17T17:22:59.237Z"},
|
{url = "https://files.pythonhosted.org/packages/8f/99/55c75d5c8c0e3c966c6f639e3b2556c54b0172501f7377cff2aa570447c7/pycryptodomex-3.24.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c55e6c42f9eb43d90c15792bf69bc09cb3e13a5e55a30eecb555695a02b0d4c0", size = 1842528, upload-time = "2026-10-04T17:40:12.033Z"},
|
||||||
{url = "https://files.pythonhosted.org/packages/96/e3/ce9348236d8e669fea5dd82a90e86be48b9c341210f44e25443162aba187/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:8a4fcd42ccb04c31268d1efeecfccfd1249612b4de6374205376b8f280321744", size = 2346104, upload-time = "2025-05-17T17:23:02.112Z"},
|
{url = "https://files.pythonhosted.org/packages/18/3d/2cdbda0eacc9875c5db5cede94389f8ca4a6a4bccbbbc74f47f8fa4f0c6c/pycryptodomex-3.24.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6a62122fb4c85befcc01ec3a1ae5b2ba81689481aba1d383b4e21e34f18900dd", size = 1777885, upload-time = "2026-10-04T17:40:14.361Z"},
|
||||||
{url = "https://files.pythonhosted.org/packages/a5/e9/e869bcee87beb89040263c416a8a50204f7f7a83ac11897646c9e71e0daf/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:55ccbe27f049743a4caf4f4221b166560d3438d0b1e5ab929e07ae1702a4d6fd", size = 2271038, upload-time = "2025-05-17T17:23:04.872Z"},
|
{url = "https://files.pythonhosted.org/packages/bc/fd/b9186cea5e823f91f40904373cff30cdaea5dfe1863b4be0ed3bf51e4db6/pycryptodomex-3.24.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:ef2bd306328a8447d4d2af6037efddd67ef16630876ed3ec84babd16ca558941", size = 2476748, upload-time = "2026-10-04T17:40:17.326Z"},
|
||||||
{url = "https://files.pythonhosted.org/packages/8d/67/09ee8500dd22614af5fbaa51a4aee6e342b5fa8aecf0a6cb9cbf52fa6d45/pycryptodomex-3.23.0-cp37-abi3-win32.whl", hash = "sha256:189afbc87f0b9f158386bf051f720e20fa6145975f1e76369303d0f31d1a8d7c", size = 1771969, upload-time = "2025-05-17T17:23:07.115Z"},
|
{url = "https://files.pythonhosted.org/packages/65/c4/bf354a7c435da08ba9905a302dce8fac57a33b2ab1e51c78542b698b4b89/pycryptodomex-3.24.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:42c8221e3a4ca04b070cb388fca004815f6544951bfb2cc6c7958e293b7caf64", size = 1644339, upload-time = "2026-10-04T17:40:19.872Z"},
|
||||||
{url = "https://files.pythonhosted.org/packages/69/96/11f36f71a865dd6df03716d33bd07a67e9d20f6b8d39820470b766af323c/pycryptodomex-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:52e5ca58c3a0b0bd5e100a9fbc8015059b05cffc6c66ce9d98b4b45e023443b9", size = 1803124, upload-time = "2025-05-17T17:23:09.267Z"},
|
{url = "https://files.pythonhosted.org/packages/7b/ea/158d0ea838c92ba5d5081ce0f08e4af40d7278f7609ca007fca4d010aa4d/pycryptodomex-3.24.0-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:da9996407d03684221a4e1603b70fad97d8772185e5f75addcd97fcbc4c3281f", size = 2193232, upload-time = "2026-10-04T17:40:22.957Z"},
|
||||||
{url = "https://files.pythonhosted.org/packages/f9/93/45c1cdcbeb182ccd2e144c693eaa097763b08b38cded279f0053ed53c553/pycryptodomex-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:02d87b80778c171445d67e23d1caef279bf4b25c3597050ccd2e13970b57fd51", size = 1707161, upload-time = "2025-05-17T17:23:11.414Z"}
|
{url = "https://files.pythonhosted.org/packages/c8/9f/376b705a536c65fe41806babe5cdcfb076273a0d72dfbbb92073d3d9b10c/pycryptodomex-3.24.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:556e063b52f338bd924fe5c40692cc1f50cb0453bd1d6580b7ffff2eade1a894", size = 2279395, upload-time = "2026-10-04T17:40:25.777Z"},
|
||||||
|
{url = "https://files.pythonhosted.org/packages/d4/72/a0726c7165ba84d66923c3e5d5411c5abec9a9a4c1759a664e5cf8806a79/pycryptodomex-3.24.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b6690bfae043c81108e28e31dc7e3afa8cb1b1eb0ba590fdbff2b66e4c6913e7", size = 2184779, upload-time = "2026-10-04T17:40:28.813Z"},
|
||||||
|
{url = "https://files.pythonhosted.org/packages/39/51/c5b9d8fc84d5cf67091a007b476e24f634252fb314472e27a58f3bd97458/pycryptodomex-3.24.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5fa46af1281be707e0ec5878aea0f77d58d8d4b465f63f7385a71ae0080bfff9", size = 2276914, upload-time = "2026-10-04T17:40:31.23Z"},
|
||||||
|
{url = "https://files.pythonhosted.org/packages/c4/28/26f6d1b4257911bdab1c66c2a80e1193131bd8c02fa9ac04453cd45233c1/pycryptodomex-3.24.0-cp37-abi3-win32.whl", hash = "sha256:fab40542a863cc142a763aa92630d613925fe3e937bea3ba5fb735e0c42d4f96", size = 1792631, upload-time = "2026-10-04T17:40:33.61Z"},
|
||||||
|
{url = "https://files.pythonhosted.org/packages/4d/91/fa406bbeea093b53d314dfb0fd186658a69a398b73a3a609636b8402b5c3/pycryptodomex-3.24.0-cp37-abi3-win_amd64.whl", hash = "sha256:55f22d72250d7923aac3c2442ec6a1dd8774d2efa3bdcb0f68d5e137d372268e", size = 1825514, upload-time = "2026-10-04T17:40:36.059Z"},
|
||||||
|
{url = "https://files.pythonhosted.org/packages/10/b0/c53c7ac97c5deb721aa4f940309802c71ff5ac2fdeac473ce96754c69e5b/pycryptodomex-3.24.0-cp37-abi3-win_arm64.whl", hash = "sha256:908f92fbc307b642b65580d0295698f423b057018d87ae43d441f1d1a00674a2", size = 1759397, upload-time = "2026-10-04T17:40:38.641Z"}
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
Reference in New Issue
Block a user