test_check_manifests.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. #!/usr/bin/env python3
  2. """Negative fixture tests for scripts/check_manifests.py (plan task 22).
  3. Runs the real lint over the shipped `deploy/k8s` manifests, mutated in memory
  4. for each case, and asserts the exact outcome each case must produce. No live
  5. cluster and no test framework: run with `python3 scripts/test_check_manifests.py`.
  6. """
  7. from __future__ import annotations
  8. import contextlib
  9. import importlib.util
  10. import io
  11. import sys
  12. import tempfile
  13. from pathlib import Path
  14. import yaml
  15. REPO = Path(__file__).resolve().parent.parent
  16. MANIFEST_DIR = REPO / "deploy" / "k8s"
  17. CHECKER_PATH = REPO / "scripts" / "check_manifests.py"
  18. def _load_checker():
  19. spec = importlib.util.spec_from_file_location("check_manifests", CHECKER_PATH)
  20. if spec is None or spec.loader is None:
  21. raise RuntimeError(f"cannot load {CHECKER_PATH}")
  22. module = importlib.util.module_from_spec(spec)
  23. spec.loader.exec_module(module)
  24. return module
  25. CM = _load_checker()
  26. def _load_docs() -> list[dict]:
  27. docs: list[dict] = []
  28. for path in sorted(MANIFEST_DIR.glob("*.yaml")):
  29. if path.name.endswith(".example.yaml"):
  30. continue
  31. for doc in yaml.safe_load_all(path.read_text()):
  32. if doc:
  33. doc["_source"] = path.name
  34. docs.append(doc)
  35. return docs
  36. def _run(docs: list[dict]) -> tuple[int, str]:
  37. by_source: dict[str, list[dict]] = {}
  38. for doc in docs:
  39. clean = {k: v for k, v in doc.items() if not k.startswith("_")}
  40. by_source.setdefault(doc["_source"], []).append(clean)
  41. with tempfile.TemporaryDirectory() as tmp:
  42. tmp_path = Path(tmp)
  43. for source, source_docs in by_source.items():
  44. (tmp_path / source).write_text(yaml.safe_dump_all(source_docs))
  45. stdout = io.StringIO()
  46. argv = sys.argv
  47. sys.argv = ["check_manifests.py", "--manifest-dir", str(tmp_path)]
  48. try:
  49. with contextlib.redirect_stdout(stdout):
  50. code = CM.main()
  51. finally:
  52. sys.argv = argv
  53. return code, stdout.getvalue()
  54. def _template(docs: list[dict]) -> dict:
  55. deployment = next(d for d in docs if d.get("kind") == "Deployment")
  56. return deployment["spec"]["template"]["spec"]
  57. def _configmap(docs: list[dict]) -> dict:
  58. return next(d for d in docs if d.get("kind") == "ConfigMap")
  59. def _secret_volume(tpl: dict) -> dict:
  60. return next(
  61. v
  62. for v in tpl["volumes"]
  63. if (v.get("secret") or {}).get("secretName") == CM.SECRET_NAME
  64. )
  65. def test_baseline_manifests_pass():
  66. code, out = _run(_load_docs())
  67. assert code == 0, out
  68. assert "OK" in out
  69. def test_missing_mandatory_pii_projection_fails():
  70. docs = _load_docs()
  71. volume = _secret_volume(_template(docs))
  72. volume["secret"]["items"] = [
  73. item for item in volume["secret"]["items"] if item["key"] != "pii-encryption-key"
  74. ]
  75. code, out = _run(docs)
  76. assert code == 1, out
  77. assert "must project key 'pii-encryption-key'" in out, out
  78. def test_inline_pii_secret_env_fails():
  79. for name in (
  80. "D_OAUTH_PII_ENCRYPTION_KEY",
  81. "D_OAUTH_PII_PREVIOUS_KEY",
  82. "D_OAUTH_TOTP_PREVIOUS_KEY",
  83. ):
  84. docs = _load_docs()
  85. _template(docs)["containers"][0]["env"].append(
  86. {"name": name, "value": "literal-not-a-file"}
  87. )
  88. code, out = _run(docs)
  89. assert code == 1, (name, out)
  90. assert f"{name} must not be injected as an env value" in out, (name, out)
  91. def test_configmap_bare_pii_or_previous_key_fails():
  92. for key in ("pii_encryption_key", "pii_previous_key", "totp_previous_key"):
  93. docs = _load_docs()
  94. _configmap(docs)["data"][key] = "bare-secret-value"
  95. code, out = _run(docs)
  96. assert code == 1, (key, out)
  97. assert f"secret-shaped key {key!r}" in out, (key, out)
  98. def test_configmap_nonsecret_pii_settings_are_allowed():
  99. docs = _load_docs()
  100. data = _configmap(docs)["data"]
  101. data["D_OAUTH_PII_DUAL_WRITE"] = "false"
  102. data["D_OAUTH_PII_KEY_ID"] = "pii-v1"
  103. data["D_OAUTH_PII_PREVIOUS_KEY_ID"] = "pii-v0"
  104. data["D_OAUTH_TOTP_PREVIOUS_KEY_ID"] = "totp-v0"
  105. code, out = _run(docs)
  106. assert code == 0, out
  107. assert "OK" in out
  108. def test_previous_key_file_mounted_passes():
  109. docs = _load_docs()
  110. tpl = _template(docs)
  111. tpl["containers"][0]["env"].append(
  112. {"name": "D_OAUTH_PII_PREVIOUS_KEY_FILE", "value": "/run/secrets/pii-previous-key"}
  113. )
  114. _secret_volume(tpl)["secret"]["items"].append(
  115. {"key": "pii-previous-key", "path": "pii-previous-key"}
  116. )
  117. code, out = _run(docs)
  118. assert code == 0, out
  119. assert "OK" in out
  120. def test_previous_key_file_literal_value_fails():
  121. docs = _load_docs()
  122. _template(docs)["containers"][0]["env"].append(
  123. {"name": "D_OAUTH_TOTP_PREVIOUS_KEY_FILE", "value": "literal-value"}
  124. )
  125. code, out = _run(docs)
  126. assert code == 1, out
  127. assert (
  128. "D_OAUTH_TOTP_PREVIOUS_KEY_FILE must point at /run/secrets/<basename>" in out
  129. ), out
  130. def test_previous_key_file_unprojected_basename_fails():
  131. docs = _load_docs()
  132. _template(docs)["containers"][0]["env"].append(
  133. {"name": "D_OAUTH_PII_PREVIOUS_KEY_FILE", "value": "/run/secrets/pii-previous-key"}
  134. )
  135. code, out = _run(docs)
  136. assert code == 1, out
  137. assert "does not project 'pii-previous-key'" in out, out
  138. def main() -> int:
  139. tests = [
  140. value
  141. for name, value in sorted(globals().items())
  142. if name.startswith("test_") and callable(value)
  143. ]
  144. failed = 0
  145. for test in tests:
  146. try:
  147. test()
  148. except AssertionError as exc:
  149. failed += 1
  150. print(f"FAIL {test.__name__}: {exc}")
  151. else:
  152. print(f"PASS {test.__name__}")
  153. print(f"{len(tests) - failed}/{len(tests)} passed")
  154. return 1 if failed else 0
  155. if __name__ == "__main__":
  156. sys.exit(main())