diff --git a/ops/install_gcp_leoclean_nosend_release.py b/ops/install_gcp_leoclean_nosend_release.py index 6f5c31e..ee60069 100644 --- a/ops/install_gcp_leoclean_nosend_release.py +++ b/ops/install_gcp_leoclean_nosend_release.py @@ -1156,10 +1156,16 @@ def _validate_starting_container( "container_security", "container_cap_drop_mismatch", ) + cap_add = host.get("CapAdd") + normalized_cap_add = ( + [item.removeprefix("CAP_") for item in cap_add] + if isinstance(cap_add, list) and all(isinstance(item, str) for item in cap_add) + else None + ) _verification_require( - isinstance(host.get("CapAdd"), list) - and all(isinstance(item, str) for item in host["CapAdd"]) - and sorted(host["CapAdd"]) == ["CHOWN", "SETGID", "SETPCAP", "SETUID"], + normalized_cap_add is not None + and len(normalized_cap_add) == len(set(normalized_cap_add)) + and sorted(normalized_cap_add) == ["CHOWN", "SETGID", "SETPCAP", "SETUID"], "container_security", "container_cap_add_mismatch", ) diff --git a/tests/test_gcp_leoclean_nosend_installer.py b/tests/test_gcp_leoclean_nosend_installer.py index d100c50..893b45f 100644 --- a/tests/test_gcp_leoclean_nosend_installer.py +++ b/tests/test_gcp_leoclean_nosend_installer.py @@ -725,6 +725,71 @@ def test_container_contract_failures_have_field_specific_bounded_codes( assert caught.value.failure_reason == failure_reason +def test_container_cap_add_accepts_docker_28_canonical_names(tmp_path: Path) -> None: + bundle, release, image = release_bundle(tmp_path, "2") + paths = install_paths(tmp_path) + host = FakeHost(paths) + host.add_release(release, image) + installer.install_release( + bundle, + paths=paths, + source_binding=source_binding(), + runner=host, + sleeper=lambda _seconds: None, + execute_restart=True, + ) + assert host.container is not None + host.container["HostConfig"]["CapAdd"] = [ + "CAP_CHOWN", + "CAP_SETGID", + "CAP_SETPCAP", + "CAP_SETUID", + ] + + verification = installer.verify_running_release( + release, + runner=host, + paths=paths, + sleeper=lambda _seconds: None, + ) + + assert verification["container_id"] == host.container["Id"] + + +def test_container_cap_add_rejects_mixed_duplicate_names(tmp_path: Path) -> None: + bundle, release, image = release_bundle(tmp_path, "2") + paths = install_paths(tmp_path) + host = FakeHost(paths) + host.add_release(release, image) + installer.install_release( + bundle, + paths=paths, + source_binding=source_binding(), + runner=host, + sleeper=lambda _seconds: None, + execute_restart=True, + ) + assert host.container is not None + host.container["HostConfig"]["CapAdd"] = [ + "CHOWN", + "CAP_CHOWN", + "SETGID", + "SETPCAP", + "SETUID", + ] + + with pytest.raises(installer.VerificationError) as caught: + installer.verify_running_release( + release, + runner=host, + paths=paths, + sleeper=lambda _seconds: None, + ) + + assert caught.value.failure_stage == "container_security" + assert caught.value.failure_reason == "container_cap_add_mismatch" + + def test_container_inspection_shape_has_bounded_code(tmp_path: Path) -> None: bundle, release, image = release_bundle(tmp_path, "2") paths = install_paths(tmp_path)