Advance supervised agent records and close verified Secret annotation guard
This commit is contained in:
parent
b2f6713721
commit
db91818e84
44 changed files with 6868 additions and 54 deletions
91
docs/changes/CUST-WP-0073/secret_annotation_maintenance.py
Normal file
91
docs/changes/CUST-WP-0073/secret_annotation_maintenance.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Bounded CUST-WP-0073-T03 maintenance. Never emit kubectl output/errors.
|
||||
|
||||
Run on railiance01 through the supervised admin path. Default is inspection;
|
||||
--clean removes only the last-applied annotation, leaving Secret data untouched.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from datetime import datetime, timezone
|
||||
|
||||
KEY = "kubectl.kubernetes.io/last-applied-configuration"
|
||||
PRESENCE = ('{{ $found := false }}{{ range $key, $_ := .metadata.annotations }}'
|
||||
'{{ if eq $key "' + KEY + '" }}{{ $found = true }}{{ end }}{{ end }}'
|
||||
'{{ if $found }}HAS-ANNOTATION{{ else }}clean{{ end }}')
|
||||
NAME = re.compile(r"^[a-z0-9][a-z0-9.-]*$")
|
||||
|
||||
|
||||
def run(args):
|
||||
# Even a template error can contain the entire Secret. Never forward it.
|
||||
result = subprocess.run(["kubectl", *args], capture_output=True, text=True, timeout=30)
|
||||
if result.returncode:
|
||||
raise RuntimeError("kubectl operation failed; output suppressed")
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def inspect(namespace, name):
|
||||
value = run(["-n", namespace, "get", "secret", name, "-o", "go-template=" + PRESENCE])
|
||||
if value not in ("clean", "HAS-ANNOTATION"):
|
||||
raise RuntimeError("unexpected presence result; output suppressed")
|
||||
return value == "HAS-ANNOTATION"
|
||||
|
||||
|
||||
def maintain(clean=False):
|
||||
# Custom columns use fixed universally-present identity fields; no annotation
|
||||
# or data output. Validate before using any returned text as an argument.
|
||||
identities = run(["get", "secrets", "-A", "--no-headers", "-o",
|
||||
"custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name"])
|
||||
rows = []
|
||||
for line in identities.splitlines():
|
||||
pair = line.split()
|
||||
if len(pair) != 2 or not all(NAME.fullmatch(value) for value in pair):
|
||||
raise RuntimeError("invalid Secret identity output; suppressed")
|
||||
rows.append(pair)
|
||||
namespaces = run(["get", "namespaces", "-o", "name"]).splitlines()
|
||||
if not all(value.startswith("namespace/") and NAME.fullmatch(value.split("/", 1)[1]) for value in namespaces):
|
||||
raise RuntimeError("invalid namespace inventory; suppressed")
|
||||
active_namespaces = {value.split("/", 1)[1] for value in namespaces}
|
||||
report = {"captured_at": datetime.now(timezone.utc).isoformat(),
|
||||
"mode": "clean" if clean else "inspect", "checked": 0,
|
||||
"annotated": [], "cleaned": [], "orphaned_namespace": [], "complete": False}
|
||||
try:
|
||||
for namespace, name in rows:
|
||||
if namespace not in active_namespaces:
|
||||
report["orphaned_namespace"].append(namespace + "/" + name)
|
||||
continue
|
||||
report["checked"] += 1
|
||||
if not inspect(namespace, name):
|
||||
continue
|
||||
identity = namespace + "/" + name
|
||||
report["annotated"].append(identity)
|
||||
if clean:
|
||||
# A single JSON patch operation cannot modify credential data.
|
||||
patch = [{"op": "remove", "path": "/metadata/annotations/" + KEY.replace("/", "~1")}]
|
||||
run(["-n", namespace, "patch", "secret", name, "--type=json",
|
||||
"-p", json.dumps(patch)])
|
||||
if inspect(namespace, name):
|
||||
raise RuntimeError("annotation still present")
|
||||
report["cleaned"].append(identity)
|
||||
report["active_namespace_scan_complete"] = True
|
||||
report["complete"] = not report["orphaned_namespace"]
|
||||
except (RuntimeError, subprocess.SubprocessError, OSError):
|
||||
report["error"] = "maintenance incomplete; raw output suppressed; inspect before retry"
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--clean", action="store_true")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
report = maintain(args.clean)
|
||||
except (RuntimeError, subprocess.SubprocessError, OSError):
|
||||
report = {"complete": False, "error": "inventory failed; raw output suppressed"}
|
||||
print(json.dumps(report, indent=2))
|
||||
return 0 if report["complete"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue