#!/usr/bin/env python3
"""
channel_assert.py -- assert every charm channel pinned in the bundle EXISTS on
Charmhub (DOCFIX-073; fulfils the D-002 "verify against Charmhub before each
deploy" claim, which pre-flight never actually implemented).
For each unique charm->channel in bundle.yaml, runs `juju info <charm>` and
checks the pinned channel appears in the published channel map. Catches: typo'd
channels, tracks retired upstream, and pins drifting from what Charmhub serves.
Exit: 0 all pins exist | 1 a pinned channel is absent (do NOT deploy)
| 2 charmhub/juju unreachable for one or more charms (verify manually).
Read-only. ASCII + LF. Usage: python3 scripts/channel_assert.py [bundle.yaml]
"""
import sys, subprocess, re
try:
import yaml
except ImportError:
sys.stderr.write("ERROR: PyYAML required\n"); sys.exit(2)
def main():
path = sys.argv[1] if len(sys.argv) > 1 else "bundle.yaml"
try:
apps = (yaml.safe_load(open(path)) or {}).get("applications", {}) or {}
except Exception as e:
print(" [FAIL] cannot parse %s: %s" % (path, e)); return 1
pins = {}
for name, a in apps.items():
ch = (a or {}).get("channel"); charm = (a or {}).get("charm")
if ch and charm:
pins.setdefault((str(charm), str(ch)), []).append(name)
fails, warns = [], []
for (charm, channel), users in sorted(pins.items()):
# capture-then-test (house SIGPIPE rule); juju info output lists channels
# as ' <track>/<risk>: <rev> ...' lines
try:
r = subprocess.run(["juju", "info", charm], capture_output=True,
text=True, timeout=60)
out = r.stdout + r.stderr
except Exception as e:
warns.append("%s: juju info failed (%s) -- verify %s manually" % (charm, e, channel))
continue
if r.returncode != 0 or "channels:" not in out:
warns.append("%s: no channel map returned -- verify %s manually (offline?)" % (charm, channel))
continue
if re.search(r"(?m)^\s+%s:\s" % re.escape(channel), out):
print(" [ok] %-22s %-16s (%s)" % (charm, channel, ", ".join(users)))
else:
fails.append("%s pinned to %s but Charmhub does not publish it (apps: %s)"
% (charm, channel, ", ".join(users)))
for w in warns: print(" [WARN] %s" % w)
for f in fails: print(" [FAIL] %s" % f)
v = "FAIL" if fails else ("WARN" if warns else "PASS")
print("\n%s: channel assert (%d pins, %d fail, %d warn)" % (v, len(pins), len(fails), len(warns)))
return 1 if fails else (2 if warns else 0)
if __name__ == "__main__":
sys.exit(main())