test_p2_wp11_delivery_contract.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. from __future__ import annotations
  2. import hashlib
  3. import json
  4. import os
  5. import subprocess
  6. import tarfile
  7. from pathlib import Path
  8. ROOT = Path(__file__).resolve().parents[1]
  9. OPERATIONS = ROOT / "deployment/compose"
  10. def _package(output: Path) -> Path:
  11. completed = subprocess.run(
  12. [
  13. "bash",
  14. str(OPERATIONS / "package_offline.sh"),
  15. "--repo-root",
  16. str(ROOT),
  17. "--output-dir",
  18. str(output),
  19. "--version",
  20. "p2-wp11-contract",
  21. "--source-date-epoch",
  22. "1775251200",
  23. "--skip-images",
  24. ],
  25. cwd=ROOT,
  26. capture_output=True,
  27. text=True,
  28. )
  29. assert completed.returncode == 0, completed.stderr
  30. return output / "dataops-platform-offline-p2-wp11-contract.tar.gz"
  31. def test_package_is_reproducible_and_contains_trace_evidence(tmp_path: Path):
  32. first = _package(tmp_path / "first")
  33. second = _package(tmp_path / "second")
  34. assert hashlib.sha256(first.read_bytes()).digest() == hashlib.sha256(
  35. second.read_bytes()
  36. ).digest()
  37. with tarfile.open(first, "r:gz") as archive:
  38. names = set(archive.getnames())
  39. prefix = "dataops-platform-offline-p2-wp11-contract/"
  40. expected = {
  41. "environments/development.json",
  42. "environments/test.json",
  43. "environments/preproduction.json",
  44. "operations/product_engineering.py",
  45. "operations/reproducible_archive.py",
  46. "trace/OPENAPI.yaml",
  47. "trace/backend.cdx.json",
  48. "trace/frontend.cdx.json",
  49. "release-manifest.json",
  50. "release-manifest.txt",
  51. "checksums.sha256",
  52. }
  53. assert {prefix + item for item in expected} <= names
  54. def test_environment_files_match_their_profiles():
  55. for profile_path in sorted((ROOT / "deployment/environments").glob("*.json")):
  56. profile = json.loads(profile_path.read_text(encoding="utf-8"))
  57. env_path = profile_path.with_suffix(".env")
  58. values = dict(
  59. line.split("=", 1)
  60. for line in env_path.read_text(encoding="utf-8").splitlines()
  61. if line and not line.startswith("#")
  62. )
  63. assert values["DATAOPS_NETWORK_NAME"] == profile["network_name"]
  64. assert values["DATAOPS_ENVIRONMENT"] == profile["environment"]
  65. assert values["DATAOPS_DATA_NAMESPACE"] == profile["data_namespace"]
  66. for name, value in profile["ports"].items():
  67. assert values[name] == str(value)
  68. def test_preflight_and_install_accept_environment_profile():
  69. for script in ("preflight.sh", "install_offline.sh"):
  70. help_text = subprocess.run(
  71. ["bash", str(OPERATIONS / script), "--help"],
  72. cwd=ROOT,
  73. capture_output=True,
  74. text=True,
  75. check=True,
  76. ).stdout
  77. assert "--environment-profile" in help_text
  78. def test_install_rejects_image_head_that_differs_from_release_manifest():
  79. install = (OPERATIONS / "install_offline.sh").read_text(encoding="utf-8")
  80. preflight = (OPERATIONS / "preflight.sh").read_text(encoding="utf-8")
  81. common = (OPERATIONS / "wp13-common.sh").read_text(encoding="utf-8")
  82. assert "wp13_expected_migration_head" in common
  83. assert "installed image migration head does not match release manifest" in install
  84. assert "image migration head does not match release manifest" in preflight
  85. def test_preflight_fails_closed_when_running_image_head_is_stale(tmp_path: Path):
  86. bundle = tmp_path / "bundle"
  87. fake_bin = tmp_path / "bin"
  88. bundle.mkdir()
  89. fake_bin.mkdir()
  90. compose = bundle / "docker-compose.yml"
  91. compose.write_text("services: {}\n", encoding="utf-8")
  92. (bundle / "release-manifest.json").write_text(
  93. json.dumps({"migration": {"head": "20260802_460"}}),
  94. encoding="utf-8",
  95. )
  96. fake_docker = fake_bin / "docker"
  97. fake_docker.write_text(
  98. """#!/usr/bin/env bash
  99. set -euo pipefail
  100. arguments="$*"
  101. if [[ "${arguments}" == "ps -q --filter label=com.docker.compose.project=stale-project" ]]; then
  102. printf 'container-id\\n'
  103. elif [[ "${arguments}" == *" config --format json" ]]; then
  104. printf '{"services":{"backend":{"image":"fake-backend"}}}\\n'
  105. elif [[ "${arguments}" == "run --rm --network none --read-only --entrypoint alembic fake-backend -c alembic.ini heads" ]]; then
  106. printf '20260730_360 (head)\\n'
  107. elif [[ "${arguments}" == *" alembic -c alembic.ini current" ]]; then
  108. printf '20260730_360 (head)\\n'
  109. elif [[ "${arguments}" == *" alembic -c alembic.ini heads" ]]; then
  110. printf '20260730_360 (head)\\n'
  111. fi
  112. """,
  113. encoding="utf-8",
  114. )
  115. fake_docker.chmod(0o755)
  116. env = os.environ.copy()
  117. env["PATH"] = f"{fake_bin}:/usr/bin:/bin"
  118. completed = subprocess.run(
  119. [
  120. "bash",
  121. str(OPERATIONS / "preflight.sh"),
  122. "--bundle-root",
  123. str(bundle),
  124. "--compose-file",
  125. str(compose),
  126. "--project-name",
  127. "stale-project",
  128. "--skip-checksums",
  129. ],
  130. cwd=ROOT,
  131. env=env,
  132. capture_output=True,
  133. text=True,
  134. )
  135. assert completed.returncode != 0
  136. assert "image migration head does not match release manifest" in (
  137. completed.stdout + completed.stderr
  138. )
  139. def test_product_engineering_cli_is_read_only_by_default():
  140. for arguments in (
  141. ["validate-environments", "--profiles-dir", "deployment/environments"],
  142. ["manifest", "--repo-root", ".", "--version", "contract", "--source-date-epoch", "1775251200"],
  143. ["sbom", "--repo-root", ".", "--output-dir", "-"],
  144. ):
  145. completed = subprocess.run(
  146. [str(ROOT / ".venv/bin/python"), str(OPERATIONS / "product_engineering.py"), *arguments],
  147. cwd=ROOT,
  148. capture_output=True,
  149. text=True,
  150. )
  151. assert completed.returncode == 0, completed.stderr
  152. def test_rollback_remains_restore_based_and_never_downgrades_database():
  153. source = (OPERATIONS / "rollback.sh").read_text(encoding="utf-8")
  154. assert "restore.sh" in source
  155. assert "alembic downgrade" not in source