| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159 |
- #!/usr/bin/env python3
- """Validate and render the e2e fan-out matrix defined in e2e/matrix.yaml.
- Subcommands:
- check Fail early if the matrix is inconsistent: a provider compiled into
- the suite (suites/provider/cases/import.go) is not covered by any
- area, needs_secrets disagrees with secret_groups, or an area names a
- secret group that the reusable workflow does not wire up.
- json Print the GitHub Actions matrix (enabled areas only) as compact JSON
- for the workflow's strategy.matrix.
- plan Print, per enabled leg, exactly which credential env vars it will
- receive. Derived from each area's secret_groups and the group -> var
- mapping parsed out of e2e-reusable.yml. This reads NO secret values
- (it never touches the secrets context), so it proves the scoping
- without any risk of leaking a value, masked or not.
- Paths are resolved relative to this file, so the working directory does not
- matter. YAML is read with PyYAML when present, else via yq (mikefarah), so no
- new runtime dependency is required in CI.
- """
- import json
- import re
- import subprocess
- import sys
- from pathlib import Path
- HERE = Path(__file__).resolve().parent
- MATRIX = HERE / "matrix.yaml"
- IMPORT = HERE / "suites/provider/cases/import.go"
- WORKFLOW = HERE.parent / ".github/workflows/e2e-reusable.yml"
- def load_yaml(path: Path):
- """Load a YAML file as a dict. Prefer PyYAML; fall back to yq -> JSON."""
- try:
- import yaml # type: ignore
- return yaml.safe_load(path.read_text())
- except ModuleNotFoundError:
- out = subprocess.run(
- ["yq", "-o=json", str(path)],
- check=True, capture_output=True, text=True,
- ).stdout
- return json.loads(out)
- def imported_providers() -> list[str]:
- """Provider names compiled into the suite: the segment after cases/ in
- each blank import of import.go (cases/aws/secretsmanager -> aws)."""
- text = IMPORT.read_text()
- return sorted({m.group(1) for m in re.finditer(r"cases/([a-z0-9]+)", text)})
- def group_to_vars() -> dict[str, list[str]]:
- """Map each secret group to the env vars the reusable workflow gates on it,
- parsed from lines like:
- FOO: ${{ contains(matrix.secret_groups, 'aws') && secrets.BAR || '' }}
- Reads only the workflow text, never any secret value."""
- pat = re.compile(
- r"^\s*([A-Z0-9_]+):\s*\$\{\{\s*"
- r"contains\(matrix\.secret_groups,\s*'([a-z0-9]+)'\)",
- re.MULTILINE,
- )
- mapping: dict[str, list[str]] = {}
- for var, group in pat.findall(WORKFLOW.read_text()):
- mapping.setdefault(group, []).append(var)
- for group in mapping:
- mapping[group].sort()
- return mapping
- def cmd_check(matrix: dict) -> int:
- areas = matrix["areas"]
- errors: list[str] = []
- # 1. Every imported provider is covered by some area.
- covered = {p for a in areas for p in (a.get("providers") or [])}
- missing = [p for p in imported_providers() if p not in covered]
- if missing:
- errors.append(
- "providers imported into the e2e suite but not covered by any "
- "area (add each to an area's providers list and a leg):\n - "
- + "\n - ".join(missing)
- )
- # 2. needs_secrets must mirror "secret_groups is non-empty".
- for a in areas:
- has_groups = bool(a.get("secret_groups"))
- if bool(a.get("needs_secrets")) != has_groups:
- errors.append(
- f"area {a['name']!r}: needs_secrets={a.get('needs_secrets')} "
- f"disagrees with secret_groups={a.get('secret_groups')}"
- )
- # 3. Every secret group an area uses is actually wired in the workflow.
- wired = set(group_to_vars())
- for a in areas:
- for group in a.get("secret_groups") or []:
- if group not in wired:
- errors.append(
- f"area {a['name']!r}: secret group {group!r} is not wired "
- f"in {WORKFLOW.name} (no env var gates on it)"
- )
- if errors:
- print("ERROR: matrix.yaml is inconsistent:", file=sys.stderr)
- for e in errors:
- print(f"- {e}", file=sys.stderr)
- return 1
- enabled = sum(1 for a in areas if a.get("enabled"))
- print(
- f"matrix.yaml ok: {len(imported_providers())} providers covered, "
- f"{enabled} leg(s) enabled"
- )
- return 0
- def cmd_json(matrix: dict) -> int:
- include = [
- {
- "name": a["name"],
- "suite": a["suite"],
- "labels": a["labels"],
- "secret_groups": a.get("secret_groups") or [],
- }
- for a in matrix["areas"]
- if a.get("enabled")
- ]
- print(json.dumps({"include": include}, separators=(",", ":")))
- return 0
- def cmd_plan(matrix: dict) -> int:
- """Show the credential env vars each enabled leg will receive. No secret
- values are read; the list comes from matrix.yaml + the workflow mapping."""
- mapping = group_to_vars()
- print("Per-leg credential scoping (from matrix.yaml + e2e-reusable.yml):")
- for a in matrix["areas"]:
- if not a.get("enabled"):
- continue
- groups = a.get("secret_groups") or []
- env_vars = sorted({v for g in groups for v in mapping.get(g, [])})
- shown = ", ".join(env_vars) if env_vars else "(none: in-cluster only)"
- print(f" {a['name']}: groups={groups or '[]'} -> {shown}")
- return 0
- def main() -> int:
- cmd = sys.argv[1] if len(sys.argv) > 1 else "check"
- if cmd not in ("check", "json", "plan"):
- print(f"usage: {sys.argv[0]} [check|json|plan]", file=sys.stderr)
- return 2
- matrix = load_yaml(MATRIX)
- return {"check": cmd_check, "json": cmd_json, "plan": cmd_plan}[cmd](matrix)
- if __name__ == "__main__":
- sys.exit(main())
|