diff --git a/docker/Dockerfile.controller.dockerignore b/docker/Dockerfile.controller.dockerignore new file mode 100644 index 00000000..1f29656d --- /dev/null +++ b/docker/Dockerfile.controller.dockerignore @@ -0,0 +1,20 @@ +# Controller image context allowlist. +** + +!pyproject.toml +!setup.py +!requirements.txt +!README.md +!LICENSE + +!i[d]rac_ctl/ +!i[d]rac_ctl/** +!redfish_ctl/ +!redfish_ctl/** +!k8s/ +!k8s/controller/ +!k8s/controller/** + +**/__pycache__/ +**/*.py[cod] +**/*.egg-info/ diff --git a/docker/Dockerfile.mock-bmc.dockerignore b/docker/Dockerfile.mock-bmc.dockerignore new file mode 100644 index 00000000..c18e8bcf --- /dev/null +++ b/docker/Dockerfile.mock-bmc.dockerignore @@ -0,0 +1,8 @@ +# Mock-BMC image context allowlist. +** + +!k8s/ +!k8s/sandbox/ +!k8s/sandbox/mock_bmc_server.py +!tests/ +!tests/supermicro_gb300_corpus.tar.gz diff --git a/tests/gates/test_dockerfile_contract.py b/tests/gates/test_dockerfile_contract.py index 5b182e1a..bd6a2d6d 100644 --- a/tests/gates/test_dockerfile_contract.py +++ b/tests/gates/test_dockerfile_contract.py @@ -4,6 +4,20 @@ DOCKERFILE = REPO_ROOT / "docker" / "Dockerfile" DOCKER_README = REPO_ROOT / "docker" / "README.md" README = REPO_ROOT / "README.md" +RELEASE_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "release.yml" + + +def _effective_ignore_rules(path: Path) -> list[str]: + """Return non-comment Docker ignore rules. + + :param path: Docker ignore file to parse. + :return: Ordered non-empty rules with comments removed. + """ + return [ + line.strip() + for line in path.read_text(encoding="utf-8").splitlines() + if line.strip() and not line.lstrip().startswith("#") + ] def test_production_dockerfile_installs_local_otlp_wheel_as_non_root() -> None: @@ -48,3 +62,48 @@ def test_docker_docs_link_the_production_image_usage() -> None: assert "exporter --output otlp" in docker_readme assert "credentials" in docker_readme.lower() assert "[Docker](docker/README.md)" in readme + + +def test_published_auxiliary_images_use_minimal_context_allowlists() -> None: + """Published auxiliary images receive only the files copied by their Dockerfiles.""" + expected_rules = { + "Dockerfile.controller": [ + "**", + "!pyproject.toml", + "!setup.py", + "!requirements.txt", + "!README.md", + "!LICENSE", + "!i[d]rac_ctl/", + "!i[d]rac_ctl/**", + "!redfish_ctl/", + "!redfish_ctl/**", + "!k8s/", + "!k8s/controller/", + "!k8s/controller/**", + "**/__pycache__/", + "**/*.py[cod]", + "**/*.egg-info/", + ], + "Dockerfile.mock-bmc": [ + "**", + "!k8s/", + "!k8s/sandbox/", + "!k8s/sandbox/mock_bmc_server.py", + "!tests/", + "!tests/supermicro_gb300_corpus.tar.gz", + ], + } + + for dockerfile, rules in expected_rules.items(): + ignore_file = REPO_ROOT / "docker" / f"{dockerfile}.dockerignore" + assert _effective_ignore_rules(ignore_file) == rules + + +def test_release_auxiliary_images_use_the_guarded_contexts() -> None: + """Release builds keep the Dockerfile names bound to their matching allowlists.""" + release_workflow = RELEASE_WORKFLOW.read_text(encoding="utf-8") + + for dockerfile in ("Dockerfile.controller", "Dockerfile.mock-bmc"): + assert f"file: docker/{dockerfile}" in release_workflow + assert (REPO_ROOT / "docker" / f"{dockerfile}.dockerignore").is_file()