|
| 1 | +"""Contract: the Flink job image does not ship pip. |
| 2 | +
|
| 3 | +pip vendors its own dependency set (``pip/_vendor/vendor.txt``) and Trivy |
| 4 | +reports those vendored copies as installed packages. msgpack 1.1.2 |
| 5 | +(GHSA-6v7p-g79w-8964) and setuptools 70.3.0 (CVE-2025-47273) were the image's |
| 6 | +last two unwaived HIGH findings and are unfixable in place: no pin changes what |
| 7 | +pip vendors, and both advisories have upstream fixes, so neither is waivable |
| 8 | +under ``security/trivy-waivers.json``. Removing pip once the venv is built is |
| 9 | +the fix, and it is only safe while nothing installs at runtime. |
| 10 | +
|
| 11 | +Docker is not available on the Windows development host, so these checks read |
| 12 | +the Dockerfile text. The image rebuild plus Trivy proof is the CI ``trivy`` job; |
| 13 | +the job actually starting is the CI ``flink-smoke`` job. |
| 14 | +""" |
| 15 | + |
| 16 | +from __future__ import annotations |
| 17 | + |
| 18 | +from pathlib import Path |
| 19 | + |
| 20 | +PROJECT_ROOT = Path(__file__).resolve().parents[2] |
| 21 | +DOCKERFILE = PROJECT_ROOT / "src" / "agentflow_runtime" / "processing" / "flink_jobs" / "Dockerfile" |
| 22 | + |
| 23 | +VENV_PYTHON = "/opt/pyflink-venv/bin/python" |
| 24 | +REQUIREMENTS_INSTALL = f"{VENV_PYTHON} -m pip install --no-cache-dir --require-hashes" |
| 25 | +PIP_REMOVAL = f"{VENV_PYTHON} -m pip uninstall --yes pip" |
| 26 | +IMPORT_CHECK = f'{VENV_PYTHON} -c "import apache_beam, pyflink;' |
| 27 | + |
| 28 | + |
| 29 | +def _dockerfile() -> str: |
| 30 | + return DOCKERFILE.read_text(encoding="utf-8") |
| 31 | + |
| 32 | + |
| 33 | +def test_the_runtime_venv_does_not_keep_pip() -> None: |
| 34 | + text = _dockerfile() |
| 35 | + |
| 36 | + assert text.count(PIP_REMOVAL) == 1 |
| 37 | + |
| 38 | + |
| 39 | +def test_pip_is_removed_after_the_locked_requirements_are_installed() -> None: |
| 40 | + text = _dockerfile() |
| 41 | + |
| 42 | + install_at = text.index(REQUIREMENTS_INSTALL) |
| 43 | + removal_at = text.index(PIP_REMOVAL) |
| 44 | + |
| 45 | + assert install_at < removal_at |
| 46 | + |
| 47 | + |
| 48 | +def test_pip_is_removed_in_the_same_layer_that_installed_it() -> None: |
| 49 | + text = _dockerfile() |
| 50 | + |
| 51 | + # A removal in a later RUN would leave pip's files in the earlier layer, |
| 52 | + # and an image pulled at that layer still carries the vendored packages. |
| 53 | + venv_run = next(block for block in text.split("\nRUN ") if REQUIREMENTS_INSTALL in block) |
| 54 | + |
| 55 | + assert PIP_REMOVAL in venv_run |
| 56 | + |
| 57 | + |
| 58 | +def test_the_venv_is_proven_to_still_import_the_job_stack_after_the_removal() -> None: |
| 59 | + text = _dockerfile() |
| 60 | + |
| 61 | + removal_at = text.index(PIP_REMOVAL) |
| 62 | + import_at = text.index(IMPORT_CHECK) |
| 63 | + |
| 64 | + # Without this the build would happily produce an image whose Python |
| 65 | + # environment no longer starts, and the failure would land in flink-smoke |
| 66 | + # (or production) instead of the build. |
| 67 | + assert removal_at < import_at |
| 68 | + assert "StreamExecutionEnvironment" in text[import_at : import_at + 200] |
| 69 | + |
| 70 | + |
| 71 | +def test_the_reason_pip_cannot_simply_be_upgraded_is_recorded() -> None: |
| 72 | + text = _dockerfile() |
| 73 | + |
| 74 | + # The next person to reach for "just bump pip" needs the two advisory ids |
| 75 | + # and the reason a waiver is not available for them. |
| 76 | + assert "GHSA-6v7p-g79w-8964" in text |
| 77 | + assert "CVE-2025-47273" in text |
| 78 | + assert "pip/_vendor" in text |
0 commit comments