feat(init): persist DECNET-service api-user/api-group to decnet.ini
DEBT-035 step 1. The composer needs to know which uid/gid to inject into each compose fragment's `user:` directive at deploy time. Today the resolved `--user` / `--group` values reach systemd unit rendering (init.py:349–354) but are not persisted anywhere the composer can read them. Persist as **names** (not numeric ids) under `[decnet] api-user` / `api-group` in the rendered decnet.ini placeholder. Resolution to uid/gid happens at deploy time on whichever host runs the deploy, via `pwd.getpwnam(...)` / `grp.getgrnam(...)` — so the same user name can have different uids on master vs agents (heterogeneous /etc/passwd) without breaking artifact ownership. The existing config_ini auto-translates kebab→DECNET_API_USER / DECNET_API_GROUP at load time; no domain-map changes needed. Two new tests: one asserting the rendered ini carries the `api-user` / `api-group` keys for the values passed to `--user` / `--group`; one round-tripping through `load_ini_config` to confirm the env vars land in `os.environ` for the composer to pick up.
This commit is contained in:
@@ -44,6 +44,12 @@ _CONFIG_PLACEHOLDER = """\
|
|||||||
# EnvironmentFile= — never in a group-readable INI.
|
# EnvironmentFile= — never in a group-readable INI.
|
||||||
|
|
||||||
[decnet]
|
[decnet]
|
||||||
|
# DECNET-service user/group as configured at `decnet init` time.
|
||||||
|
# Resolved to a uid/gid on each host at deploy time via pwd.getpwnam,
|
||||||
|
# so the same user name can have different numeric uids on master vs
|
||||||
|
# agents without breaking artifact ownership.
|
||||||
|
api-user = {api_user}
|
||||||
|
api-group = {api_group}
|
||||||
# mode = master # or "agent"
|
# mode = master # or "agent"
|
||||||
|
|
||||||
# [api]
|
# [api]
|
||||||
@@ -198,14 +204,17 @@ def _ensure_dir(
|
|||||||
return f"skip: {path} already present" if existed else "ok"
|
return f"skip: {path} already present" if existed else "ok"
|
||||||
|
|
||||||
|
|
||||||
def _ensure_config(path: Path, group: str, *, dry_run: bool) -> str:
|
def _ensure_config(
|
||||||
|
path: Path, group: str, *, user: str, dry_run: bool,
|
||||||
|
) -> str:
|
||||||
if path.exists():
|
if path.exists():
|
||||||
return f"skip: {path} already present"
|
return f"skip: {path} already present"
|
||||||
if dry_run:
|
if dry_run:
|
||||||
console.print(f" [dim]would write:[/] {path}")
|
console.print(f" [dim]would write:[/] {path}")
|
||||||
return "ok"
|
return "ok"
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
path.write_text(_CONFIG_PLACEHOLDER)
|
rendered = _CONFIG_PLACEHOLDER.format(api_user=user, api_group=group)
|
||||||
|
path.write_text(rendered)
|
||||||
try:
|
try:
|
||||||
os.chmod(path, 0o640)
|
os.chmod(path, 0o640)
|
||||||
gid = grp.getgrnam(group).gr_gid
|
gid = grp.getgrnam(group).gr_gid
|
||||||
@@ -781,7 +790,10 @@ def register(app: typer.Typer) -> None:
|
|||||||
)
|
)
|
||||||
_step(
|
_step(
|
||||||
f"write {etc_decnet / 'decnet.ini'}",
|
f"write {etc_decnet / 'decnet.ini'}",
|
||||||
lambda: _ensure_config(etc_decnet / "decnet.ini", group, dry_run=dry_run),
|
lambda: _ensure_config(
|
||||||
|
etc_decnet / "decnet.ini", group,
|
||||||
|
user=user, dry_run=dry_run,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
_step(
|
_step(
|
||||||
"install systemd units",
|
"install systemd units",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ into a pytest ``tmp_path``.
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, List
|
from typing import Any, List
|
||||||
|
|
||||||
@@ -217,6 +218,55 @@ def test_init_writes_decnet_ini_not_config_ini(
|
|||||||
assert header in body, f"placeholder missing {header} example"
|
assert header in body, f"placeholder missing {header} example"
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_persists_api_user_group_to_decnet_ini(
|
||||||
|
monkeypatch: Any, tmp_path: Path, subprocess_calls: List[List[str]],
|
||||||
|
no_missing_tools: None, missing_user_and_group: None,
|
||||||
|
) -> None:
|
||||||
|
"""DEBT-035: `decnet init --user X --group Y` must persist the
|
||||||
|
DECNET-service user/group **as names** to decnet.ini under
|
||||||
|
`[decnet] api-user` / `api-group`. Resolution to numeric uid/gid
|
||||||
|
happens at deploy time on whichever host runs the deploy — names
|
||||||
|
survive across master/agent uid namespaces while raw numbers
|
||||||
|
would mismatch."""
|
||||||
|
_seed_deploy(monkeypatch, tmp_path)
|
||||||
|
prefix = tmp_path / "root"
|
||||||
|
r = runner.invoke(app, [
|
||||||
|
"init", "--no-start", "--prefix", str(prefix),
|
||||||
|
"--user", "decoyman", "--group", "decoygrp",
|
||||||
|
])
|
||||||
|
assert r.exit_code == 0, r.output
|
||||||
|
|
||||||
|
body = (prefix / "etc/decnet/decnet.ini").read_text()
|
||||||
|
assert "api-user = decoyman" in body
|
||||||
|
assert "api-group = decoygrp" in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_decnet_ini_loads_via_config_ini(
|
||||||
|
monkeypatch: Any, tmp_path: Path, subprocess_calls: List[List[str]],
|
||||||
|
no_missing_tools: None, missing_user_and_group: None,
|
||||||
|
) -> None:
|
||||||
|
"""Defence-in-depth: round-trip the rendered ini through
|
||||||
|
`decnet.config_ini.load_ini_config` and confirm that
|
||||||
|
DECNET_API_USER / DECNET_API_GROUP appear in os.environ. The
|
||||||
|
composer reads those env vars at deploy time."""
|
||||||
|
from decnet.config_ini import load_ini_config
|
||||||
|
|
||||||
|
_seed_deploy(monkeypatch, tmp_path)
|
||||||
|
prefix = tmp_path / "root"
|
||||||
|
r = runner.invoke(app, [
|
||||||
|
"init", "--no-start", "--prefix", str(prefix),
|
||||||
|
"--user", "deckyowner", "--group", "deckygroup",
|
||||||
|
])
|
||||||
|
assert r.exit_code == 0, r.output
|
||||||
|
|
||||||
|
ini = prefix / "etc/decnet/decnet.ini"
|
||||||
|
monkeypatch.delenv("DECNET_API_USER", raising=False)
|
||||||
|
monkeypatch.delenv("DECNET_API_GROUP", raising=False)
|
||||||
|
load_ini_config(ini)
|
||||||
|
assert os.environ.get("DECNET_API_USER") == "deckyowner"
|
||||||
|
assert os.environ.get("DECNET_API_GROUP") == "deckygroup"
|
||||||
|
|
||||||
|
|
||||||
def test_install_dir_renders_into_service_units(
|
def test_install_dir_renders_into_service_units(
|
||||||
monkeypatch: Any, tmp_path: Path, subprocess_calls: List[List[str]],
|
monkeypatch: Any, tmp_path: Path, subprocess_calls: List[List[str]],
|
||||||
no_missing_tools: None, missing_user_and_group: None,
|
no_missing_tools: None, missing_user_and_group: None,
|
||||||
|
|||||||
Reference in New Issue
Block a user