mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-13 06:06:38 +00:00
services.yml (15 conflicts): kept main's design, docs, seaweedfs, resource and healthcheck changes and re-applied the PR's security.suppressions blocks on top. web-app-nextcloud: main replaced the aio-talk images of talk and talk_recording with custom builds, so their image_tag "latest" suppressions are dropped. web-app-seaweedfs: main renamed the proxy service to seaweedfs-console on the same nginx 1.31-alpine tag, so its suppression moves there. Stale image_tag suppressions dropped where main moved the version (penpot backend and exporter 2.19.1, checkmk 2.4.0p40, espocrm 10.1.1, fider v0.39.0, six gitlab services v19.5.4, element v1.12.34, odoo 19.0, pixelfed v0.15.2, semaphore v2.20.24, lmstudio 22.04); the files equal what drop_image_tag_entries yields. utils/github/variant: kept main's architecture axis, pins and runner; the engine stays the odometer digit above distro and filesystem and its glyph follows the architecture glyph in the label, regex, docstring and workflows README; TestEngineAxis passes network_input. utils/docker/registry.py: main made platform_digest(manifests, ...) public and added manifest_digest, so the PR's image level helper becomes image_platform_digest on top of both; the scan CLI and its test follow. compose.py: kept main's cache artifact rendering and the PR's vulnerability database preparation; main's two new up() tests mock _prepare_vulnerability_db like the PR's. act test_workflow.py (add/add): one file with both suites on main's NUL separated stub; env_items leaves out the wrapper's own database pin. call-test-deploy.yml: main's host metrics sampler runs before the PR's database restore and pin steps. passthrough.py, pools.py, swarm matrix.py: both sides' keys and lines kept; hlth-runtime README keeps the PR's scanner features without the Credits section main removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
215 lines
7.5 KiB
Python
215 lines
7.5 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from cli.administration.inventory.provision.services.disabler import (
|
|
find_provider_roles,
|
|
parse_services_disabled,
|
|
)
|
|
from cli.meta.runtime import detect_runtime
|
|
from plugins.filter.merge.with_defaults import merge_with_defaults
|
|
from utils.security.deploy import deploy_inventory_overrides
|
|
|
|
from .common import make_compose
|
|
from .inventory import (
|
|
DevInventorySpec,
|
|
build_dev_inventory,
|
|
plan_dev_inventory_matrix,
|
|
prune_orphans_after_disable,
|
|
)
|
|
from .inventory import (
|
|
_build_services_overrides_for_round as build_services_overrides_for_round,
|
|
)
|
|
from .storage import detect_storage_constrained
|
|
from .variant_select import (
|
|
add_variant_args,
|
|
apply_variant_filter,
|
|
env_guide_host_pin,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
import argparse
|
|
|
|
|
|
def add_parser(sub: argparse._SubParsersAction) -> None:
|
|
p = sub.add_parser(
|
|
"init",
|
|
help="Create development inventory inside the infinito container.",
|
|
)
|
|
p.add_argument(
|
|
"--inventory-dir",
|
|
default=os.environ.get("INFINITO_INVENTORY_DIR"),
|
|
required=os.environ.get("INFINITO_INVENTORY_DIR") is None,
|
|
help=(
|
|
"Inventory directory base (default: $INFINITO_INVENTORY_DIR). "
|
|
"When the included apps declare more than one matrix-deploy "
|
|
"variant, sibling folders `<dir>-0`, `<dir>-1`, ... are "
|
|
"created; otherwise the directory is used as-is."
|
|
),
|
|
)
|
|
|
|
g = p.add_mutually_exclusive_group(required=True)
|
|
g.add_argument(
|
|
"--apps",
|
|
help="One or more application ids (will include run_after deps automatically).",
|
|
)
|
|
g.add_argument(
|
|
"--include",
|
|
help="Comma-separated list of application ids to include (no deps resolution).",
|
|
)
|
|
|
|
p.add_argument(
|
|
"--force-storage-constrained",
|
|
choices=["true", "false"],
|
|
default=None,
|
|
help="Override storage detection explicitly.",
|
|
)
|
|
p.add_argument(
|
|
"--vars",
|
|
default=None,
|
|
help="JSON object merged into inventory variables (overrides win).",
|
|
)
|
|
add_variant_args(p, action="init")
|
|
p.set_defaults(_handler=handler)
|
|
|
|
|
|
def handler(args: argparse.Namespace) -> int:
|
|
compose = make_compose()
|
|
|
|
if args.apps:
|
|
primary_apps = [
|
|
x.strip() for x in args.apps.replace(",", " ").split() if x.strip()
|
|
]
|
|
else:
|
|
primary_apps = [x.strip() for x in (args.include or "").split(",") if x.strip()]
|
|
|
|
if not primary_apps:
|
|
raise SystemExit("Primary app list is empty")
|
|
|
|
raw_disabled = os.environ.get("disable", "").strip()
|
|
disabled_app_ids: set[str] = set()
|
|
if raw_disabled:
|
|
services = parse_services_disabled(raw_disabled)
|
|
roles_dir = compose.repo_root / "roles"
|
|
provider_map = find_provider_roles(services, roles_dir)
|
|
disabled_app_ids = set(provider_map.values())
|
|
primary_apps = [a for a in primary_apps if a not in disabled_app_ids]
|
|
|
|
if not primary_apps:
|
|
raise SystemExit(
|
|
"All primary apps disabled by `disable` — nothing to initialise"
|
|
)
|
|
|
|
extra_vars: dict[str, Any] | None = None
|
|
if args.vars is not None:
|
|
try:
|
|
parsed = json.loads(args.vars)
|
|
except Exception as exc:
|
|
raise SystemExit(f"--vars must be valid JSON: {exc}") from exc
|
|
if not isinstance(parsed, dict):
|
|
raise SystemExit("--vars must be a JSON object")
|
|
extra_vars = parsed
|
|
|
|
extra_vars = dict(extra_vars or {})
|
|
extra_vars["applications"] = merge_with_defaults(
|
|
extra_vars.get("applications") or {}, deploy_inventory_overrides()
|
|
)
|
|
|
|
forced_storage_constrained = (
|
|
None
|
|
if args.force_storage_constrained is None
|
|
else args.force_storage_constrained == "true"
|
|
)
|
|
|
|
roles_dir = str(compose.repo_root / "roles")
|
|
plan = plan_dev_inventory_matrix(
|
|
roles_dir=roles_dir,
|
|
primary_apps=primary_apps,
|
|
base_inventory_dir=str(args.inventory_dir),
|
|
pinned_variants=env_guide_host_pin(roles_dir, primary_apps),
|
|
)
|
|
try:
|
|
plan = apply_variant_filter(plan, args)
|
|
except ValueError as exc:
|
|
raise SystemExit(f"--variant: {exc}") from exc
|
|
|
|
runtime = os.environ.get("RUNTIME") or detect_runtime()
|
|
services_disabled = os.environ.get("disable", "")
|
|
roles_dir = str(compose.repo_root / "roles")
|
|
built_includes: dict[str, tuple[str, ...]] = {}
|
|
round_storage_constrained: dict[str, bool] = {}
|
|
for _round_index, inv_dir, round_variants, include_R, _purge_set in plan:
|
|
if disabled_app_ids:
|
|
round_overrides = build_services_overrides_for_round(
|
|
roles_dir=roles_dir,
|
|
round_index=_round_index,
|
|
primary_app_variants={
|
|
a: round_variants[a] for a in primary_apps if a in round_variants
|
|
},
|
|
)
|
|
round_include, pruned = prune_orphans_after_disable(
|
|
include=include_R,
|
|
primary_apps=primary_apps,
|
|
disabled_app_ids=disabled_app_ids,
|
|
services_overrides=round_overrides,
|
|
)
|
|
if pruned:
|
|
print(
|
|
f">>> `disable` orphan-pruned {len(pruned)} transitive dep(s) "
|
|
f"at {inv_dir}: {','.join(pruned)}"
|
|
)
|
|
else:
|
|
round_include = include_R
|
|
if not round_include:
|
|
print(
|
|
f">>> Skipping inventory at {inv_dir}: include set is empty "
|
|
"after `disable` filter"
|
|
)
|
|
continue
|
|
built_includes[inv_dir] = round_include
|
|
storage_constrained = (
|
|
forced_storage_constrained
|
|
if forced_storage_constrained is not None
|
|
else detect_storage_constrained(compose, primary_apps, round_variants)
|
|
)
|
|
round_storage_constrained[inv_dir] = storage_constrained
|
|
spec = DevInventorySpec(
|
|
inventory_dir=inv_dir,
|
|
include=round_include,
|
|
storage_constrained=storage_constrained,
|
|
runtime=runtime,
|
|
extra_vars=extra_vars,
|
|
services_disabled=services_disabled,
|
|
active_variants=round_variants,
|
|
network_mode=os.environ.get("network", ""),
|
|
)
|
|
build_dev_inventory(compose, spec)
|
|
|
|
if len(plan) == 1:
|
|
_, inv_dir, round_variants, include_R, _purge_set = plan[0]
|
|
shown = built_includes.get(inv_dir, include_R)
|
|
non_zero = {a: i for a, i in round_variants.items() if i and a in shown}
|
|
suffix = f" variants={non_zero}" if non_zero else ""
|
|
print(
|
|
f">>> Inventory initialized at {inv_dir} "
|
|
f"(include={','.join(shown)} "
|
|
f"storage_constrained={round_storage_constrained.get(inv_dir)}){suffix}"
|
|
)
|
|
else:
|
|
print(
|
|
f">>> Matrix inventory initialized in {len(plan)} folders "
|
|
f"(primary_apps={','.join(primary_apps)}):"
|
|
)
|
|
for round_index, inv_dir, round_variants, include_R, _purge_set in plan:
|
|
shown = built_includes.get(inv_dir, include_R)
|
|
non_zero = {a: i for a, i in round_variants.items() if i and a in shown}
|
|
print(
|
|
f" [round {round_index}] {inv_dir} "
|
|
f"include={','.join(shown)} "
|
|
f"storage_constrained={round_storage_constrained.get(inv_dir)}"
|
|
+ (f" variants={non_zero}" if non_zero else "")
|
|
)
|
|
return 0
|