Share the target filter, fix cd-status branch, record deploy status
- enabled_targets/names_for: one implementation of 'a host acts only on its own targets'; --host s5 and --host s5.fisher.hu now select the same targets - shell_value: JSON booleans reach shell consumers as true/false - cd-status: capture before eval, so the unconfigured-host branch is reachable - cd-deploy writes a .status record; cd-status reads it instead of grepping the log's prose - cd-render-hooks --list-urls replaces two inline JSON readers
This commit is contained in:
+50
-31
@@ -15,9 +15,6 @@ Subcommands:
|
||||
list [--host HOST]
|
||||
Print the name of every enabled target owned by HOST, one per line.
|
||||
|
||||
repos [--host HOST]
|
||||
Print the distinct repositories HOST deploys, one per line.
|
||||
|
||||
host-config [--host HOST]
|
||||
Print shell-quoted BIND/PORT for HOST.
|
||||
|
||||
@@ -30,11 +27,13 @@ HOST defaults to the local machine in every subcommand.
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import functools
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import socket
|
||||
import sys
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
@@ -47,13 +46,17 @@ MANIFEST = Path(os.environ.get("CD_TARGETS_FILE", REPO_ROOT / "targets.json"))
|
||||
NO_MATCH = 3
|
||||
|
||||
|
||||
def local_hostnames() -> set[str]:
|
||||
@functools.cache
|
||||
def local_hostnames() -> frozenset[str]:
|
||||
"""Every name this machine plausibly answers to.
|
||||
|
||||
Hosts in this fleet are referred to by FQDN in the manifest but report a
|
||||
short name from `hostname`, so both forms -- and the leading label of any
|
||||
FQDN -- have to count as a match. Getting this wrong is how the old
|
||||
per-project scripts ended up with a dead `hostname != s2` guard.
|
||||
|
||||
Cached: `getfqdn` can block on a slow resolver, and this sits on the path of
|
||||
every webhook delivery. The answer cannot change within one process.
|
||||
"""
|
||||
names = {socket.gethostname()}
|
||||
try:
|
||||
@@ -63,14 +66,39 @@ def local_hostnames() -> set[str]:
|
||||
for name in list(names):
|
||||
if "." in name:
|
||||
names.add(name.split(".", 1)[0])
|
||||
return {n.lower() for n in names if n}
|
||||
return frozenset(n.lower() for n in names if n)
|
||||
|
||||
|
||||
def host_matches(target_host: str, names: set[str]) -> bool:
|
||||
def host_matches(target_host: str, names: frozenset[str]) -> bool:
|
||||
target_host = target_host.lower()
|
||||
return target_host in names or target_host.split(".", 1)[0] in names
|
||||
|
||||
|
||||
def names_for(explicit: str | None) -> frozenset[str]:
|
||||
"""The names a target's `host` field is matched against.
|
||||
|
||||
Defaults to this machine's own names. An explicit --host is expanded to both
|
||||
its FQDN and short forms, the same way `local_hostnames` expands the local
|
||||
ones, so `--host s5` and `--host s5.fisher.hu` select the same targets.
|
||||
"""
|
||||
if not explicit:
|
||||
return local_hostnames()
|
||||
name = explicit.lower()
|
||||
return frozenset({name, name.split(".", 1)[0]})
|
||||
|
||||
|
||||
def enabled_targets(manifest: dict, names: frozenset[str]) -> Iterator[dict]:
|
||||
"""Every enabled target owned by `names`, in manifest order.
|
||||
|
||||
The single implementation of "a host acts only on its own targets" -- the
|
||||
rule the whole design rests on. cd-render-hooks imports this rather than
|
||||
repeating the filter, so a new filtering dimension is one edit, not four.
|
||||
"""
|
||||
for target in manifest.get("targets", []):
|
||||
if target.get("enabled", True) and host_matches(target["host"], names):
|
||||
yield target
|
||||
|
||||
|
||||
def load() -> dict:
|
||||
try:
|
||||
return json.loads(MANIFEST.read_text())
|
||||
@@ -93,23 +121,29 @@ def pick_host(manifest: dict, explicit: str | None) -> str:
|
||||
return socket.gethostname()
|
||||
|
||||
|
||||
def shell_value(value: object) -> str:
|
||||
"""Render a manifest value the way a shell consumer expects it.
|
||||
|
||||
JSON booleans would otherwise arrive as Python's `True`/`False`, forcing
|
||||
every consumer to test for both spellings of every boolean field.
|
||||
"""
|
||||
if isinstance(value, bool):
|
||||
return "true" if value else "false"
|
||||
return str(value)
|
||||
|
||||
|
||||
def emit(pairs: dict[str, object]) -> None:
|
||||
for key, value in pairs.items():
|
||||
print(f"{key}={shlex.quote(str(value))}")
|
||||
print(f"{key}={shlex.quote(shell_value(value))}")
|
||||
|
||||
|
||||
def cmd_resolve(args, manifest: dict) -> int:
|
||||
host = pick_host(manifest, args.host)
|
||||
names = local_hostnames() if not args.host else {args.host.lower()}
|
||||
defaults = manifest.get("defaults", {})
|
||||
|
||||
for target in manifest.get("targets", []):
|
||||
if not target.get("enabled", True):
|
||||
continue
|
||||
for target in enabled_targets(manifest, names_for(args.host)):
|
||||
if target["repo"] != args.repo or target["branch"] != args.branch:
|
||||
continue
|
||||
if not host_matches(target["host"], names):
|
||||
continue
|
||||
|
||||
merged = {**defaults, **target}
|
||||
merged["resolved_host"] = host
|
||||
@@ -120,22 +154,8 @@ def cmd_resolve(args, manifest: dict) -> int:
|
||||
|
||||
|
||||
def cmd_list(args, manifest: dict) -> int:
|
||||
names = local_hostnames() if not args.host else {args.host.lower()}
|
||||
for target in manifest.get("targets", []):
|
||||
if target.get("enabled", True) and host_matches(target["host"], names):
|
||||
print(target["name"])
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_repos(args, manifest: dict) -> int:
|
||||
names = local_hostnames() if not args.host else {args.host.lower()}
|
||||
seen: list[str] = []
|
||||
for target in manifest.get("targets", []):
|
||||
if not target.get("enabled", True):
|
||||
continue
|
||||
if host_matches(target["host"], names) and target["repo"] not in seen:
|
||||
seen.append(target["repo"])
|
||||
print("\n".join(seen))
|
||||
for target in enabled_targets(manifest, names_for(args.host)):
|
||||
print(target["name"])
|
||||
return 0
|
||||
|
||||
|
||||
@@ -166,8 +186,7 @@ def main() -> int:
|
||||
p.add_argument("--host")
|
||||
p.set_defaults(func=cmd_resolve)
|
||||
|
||||
for name, func in (("list", cmd_list), ("repos", cmd_repos),
|
||||
("host-config", cmd_host_config)):
|
||||
for name, func in (("list", cmd_list), ("host-config", cmd_host_config)):
|
||||
p = sub.add_parser(name)
|
||||
p.add_argument("--host")
|
||||
p.set_defaults(func=func)
|
||||
|
||||
Reference in New Issue
Block a user