Compare commits
4 Commits
aadf17dc13
...
pilot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f094dab182 | ||
|
|
d2f763c1e5 | ||
|
|
9540db8b46 | ||
|
|
777d889980 |
@@ -15,7 +15,7 @@ ROOT = Path(__file__).resolve().parents[1]
|
|||||||
COMPOSE_FILE = ROOT / "compose.yaml"
|
COMPOSE_FILE = ROOT / "compose.yaml"
|
||||||
DOCKERFILE = ROOT / "Dockerfile"
|
DOCKERFILE = ROOT / "Dockerfile"
|
||||||
COMPOSE_BIN = os.environ.get("COMPOSE_BIN", "/opt/data/tmp/docker-compose-birweb")
|
COMPOSE_BIN = os.environ.get("COMPOSE_BIN", "/opt/data/tmp/docker-compose-birweb")
|
||||||
|
SYNTHETIC_RESOURCE_UUID = "contract-test-resource"
|
||||||
DOCKERFILE_SHA256 = "748bd362b43475779d241074a1955ae880967c923a7f68c693cb1c67f8b242dd"
|
DOCKERFILE_SHA256 = "748bd362b43475779d241074a1955ae880967c923a7f68c693cb1c67f8b242dd"
|
||||||
BASE_IMAGE = (
|
BASE_IMAGE = (
|
||||||
"nginxinc/nginx-unprivileged:1.27-alpine@"
|
"nginxinc/nginx-unprivileged:1.27-alpine@"
|
||||||
@@ -25,10 +25,13 @@ HEALTH_COMMAND = "wget -qO- http://127.0.0.1:8080/health.json || exit 1"
|
|||||||
TMPFS_TARGETS = {"/tmp", "/var/cache/nginx", "/var/run"}
|
TMPFS_TARGETS = {"/tmp", "/var/cache/nginx", "/var/run"}
|
||||||
|
|
||||||
|
|
||||||
def run(command: list[str]) -> subprocess.CompletedProcess[str]:
|
def run(
|
||||||
|
command: list[str], env: dict[str, str] | None = None
|
||||||
|
) -> subprocess.CompletedProcess[str]:
|
||||||
return subprocess.run(
|
return subprocess.run(
|
||||||
command,
|
command,
|
||||||
cwd=ROOT,
|
cwd=ROOT,
|
||||||
|
env=env,
|
||||||
text=True,
|
text=True,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
@@ -80,9 +83,12 @@ class ComposeContract(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
if not COMPOSE_FILE.is_file():
|
if not COMPOSE_FILE.is_file():
|
||||||
raise AssertionError(f"RED: required Compose file is missing: {COMPOSE_FILE}")
|
raise AssertionError(f"RED: required Compose file is missing: {COMPOSE_FILE}")
|
||||||
rendered = run(
|
cls.render_command = [
|
||||||
[COMPOSE_BIN, "-f", str(COMPOSE_FILE), "config", "--format", "json"]
|
COMPOSE_BIN, "-f", str(COMPOSE_FILE), "config", "--format", "json"
|
||||||
)
|
]
|
||||||
|
cls.render_env = os.environ.copy()
|
||||||
|
cls.render_env["COOLIFY_RESOURCE_UUID"] = SYNTHETIC_RESOURCE_UUID
|
||||||
|
rendered = run(cls.render_command, env=cls.render_env)
|
||||||
if rendered.returncode != 0:
|
if rendered.returncode != 0:
|
||||||
raise AssertionError(f"Compose config rendering failed:\n{rendered.stderr}")
|
raise AssertionError(f"Compose config rendering failed:\n{rendered.stderr}")
|
||||||
try:
|
try:
|
||||||
@@ -160,6 +166,47 @@ class ComposeContract(unittest.TestCase):
|
|||||||
self.assertGreaterEqual(memory, 16 * 1024**2)
|
self.assertGreaterEqual(memory, 16 * 1024**2)
|
||||||
self.assertLessEqual(memory, 512 * 1024**2)
|
self.assertLessEqual(memory, 512 * 1024**2)
|
||||||
|
|
||||||
|
def test_source_defines_no_traefik_labels(self) -> None:
|
||||||
|
source_label_names = {
|
||||||
|
match.group(1).strip("'\"")
|
||||||
|
for match in re.finditer(
|
||||||
|
r'^\s*-\s*([^\s=]+)(?:=.*)?$',
|
||||||
|
self.source,
|
||||||
|
re.IGNORECASE | re.MULTILINE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
source_label_names.update(
|
||||||
|
key
|
||||||
|
for _, key, line in source_mapping_keys(self.source)
|
||||||
|
)
|
||||||
|
traefik_labels = sorted(
|
||||||
|
name
|
||||||
|
for name in source_label_names
|
||||||
|
if re.search(r"(?:^|[._-])traefik(?:[._-]|$)", name, re.IGNORECASE)
|
||||||
|
)
|
||||||
|
# Interpolating a label key is unsafe: Coolify preprocesses list labels
|
||||||
|
# into a mapping before Compose interpolation, leaving that key literal.
|
||||||
|
self.assertEqual(
|
||||||
|
traefik_labels,
|
||||||
|
[],
|
||||||
|
f"source Traefik labels are forbidden; Coolify owns them: {traefik_labels}",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_compose_renders_without_resource_uuid(self) -> None:
|
||||||
|
missing_env = os.environ.copy()
|
||||||
|
missing_env.pop("COOLIFY_RESOURCE_UUID", None)
|
||||||
|
empty_env = os.environ.copy()
|
||||||
|
empty_env["COOLIFY_RESOURCE_UUID"] = ""
|
||||||
|
|
||||||
|
for case, env in (("missing", missing_env), ("empty", empty_env)):
|
||||||
|
with self.subTest(case=case):
|
||||||
|
rendered = run(self.render_command, env=env)
|
||||||
|
self.assertEqual(
|
||||||
|
rendered.returncode,
|
||||||
|
0,
|
||||||
|
f"Compose config rejected a {case} COOLIFY_RESOURCE_UUID:\n{rendered.stderr}",
|
||||||
|
)
|
||||||
|
|
||||||
def test_no_secrets_bind_mounts_or_forbidden_source_keys(self) -> None:
|
def test_no_secrets_bind_mounts_or_forbidden_source_keys(self) -> None:
|
||||||
service = self.service()
|
service = self.service()
|
||||||
self.assertNotIn("secrets", self.config)
|
self.assertNotIn("secrets", self.config)
|
||||||
@@ -177,6 +224,9 @@ class ComposeContract(unittest.TestCase):
|
|||||||
meaningful = "\n".join(
|
meaningful = "\n".join(
|
||||||
line.split("#", 1)[0] for line in self.source.splitlines()
|
line.split("#", 1)[0] for line in self.source.splitlines()
|
||||||
)
|
)
|
||||||
|
meaningful = meaningful.replace(
|
||||||
|
"${COOLIFY_RESOURCE_UUID:?required}", "$COOLIFY_RESOURCE_UUID"
|
||||||
|
)
|
||||||
self.assertNotRegex(meaningful, r"[{}]|(?:^|\s)[&*][A-Za-z0-9_-]+|<<\s*:")
|
self.assertNotRegex(meaningful, r"[{}]|(?:^|\s)[&*][A-Za-z0-9_-]+|<<\s*:")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user