Support for detecting Docker images
This commit is contained in:
parent
e2be9a6239
commit
ebc27e1111
19 changed files with 1600 additions and 15 deletions
|
|
@ -39,3 +39,105 @@ def test_runtime_state_collector_preserves_non_root_skip_schema(monkeypatch, tmp
|
|||
assert result.sysctl_snapshot.role_name == "sysctl"
|
||||
assert "not running as root" in result.firewall_runtime_snapshot.notes[0]
|
||||
assert "not running as root" in result.sysctl_snapshot.notes[0]
|
||||
|
||||
|
||||
def test_container_images_collector_records_digest_pinned_docker_images(
|
||||
monkeypatch, tmp_path
|
||||
):
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
from enroll.harvest_collectors import container_images as ci
|
||||
from enroll.harvest_collectors.container_images import ContainerImagesCollector
|
||||
|
||||
def fake_which(cmd):
|
||||
return f"/usr/bin/{cmd}" if cmd == "docker" else None
|
||||
|
||||
def fake_run(argv, check=False, stdout=None, stderr=None, text=False, timeout=None):
|
||||
if argv[:4] == ["/usr/bin/docker", "image", "ls", "-q"]:
|
||||
return subprocess.CompletedProcess(argv, 0, "sha256:" + "a" * 64 + "\n", "")
|
||||
if argv[:3] == ["/usr/bin/docker", "image", "inspect"]:
|
||||
return subprocess.CompletedProcess(
|
||||
argv,
|
||||
0,
|
||||
json.dumps(
|
||||
[
|
||||
{
|
||||
"Id": "sha256:" + "a" * 64,
|
||||
"RepoTags": ["docker.io/library/nginx:1.27"],
|
||||
"RepoDigests": [
|
||||
"docker.io/library/nginx@sha256:" + "b" * 64
|
||||
],
|
||||
"Os": "linux",
|
||||
"Architecture": "amd64",
|
||||
"Size": 123,
|
||||
"Created": "2026-01-01T00:00:00Z",
|
||||
}
|
||||
]
|
||||
),
|
||||
"",
|
||||
)
|
||||
raise AssertionError(argv)
|
||||
|
||||
monkeypatch.setattr(ci.shutil, "which", fake_which)
|
||||
monkeypatch.setattr(ci.subprocess, "run", fake_run)
|
||||
|
||||
result = ContainerImagesCollector(_context(tmp_path)).collect()
|
||||
|
||||
assert result.role_name == "container_images"
|
||||
assert len(result.images) == 1
|
||||
image = result.images[0]
|
||||
assert image["engine"] == "docker"
|
||||
assert image["pull_ref"] == "docker.io/library/nginx@sha256:" + "b" * 64
|
||||
assert image["platform"] == "linux/amd64"
|
||||
assert image["tag_aliases"] == [
|
||||
{
|
||||
"ref": "docker.io/library/nginx:1.27",
|
||||
"repository": "docker.io/library/nginx",
|
||||
"tag": "1.27",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_container_images_collector_records_unpullable_tagged_images(
|
||||
monkeypatch, tmp_path
|
||||
):
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
from enroll.harvest_collectors import container_images as ci
|
||||
from enroll.harvest_collectors.container_images import ContainerImagesCollector
|
||||
|
||||
def fake_which(cmd):
|
||||
return "/usr/bin/podman" if cmd == "podman" else None
|
||||
|
||||
monkeypatch.setattr(ci.shutil, "which", fake_which)
|
||||
|
||||
def fake_run(argv, check=False, stdout=None, stderr=None, text=False, timeout=None):
|
||||
if argv[:4] == ["/usr/bin/podman", "image", "ls", "-q"]:
|
||||
return subprocess.CompletedProcess(argv, 0, "c" * 64 + "\n", "")
|
||||
if argv[:3] == ["/usr/bin/podman", "image", "inspect"]:
|
||||
return subprocess.CompletedProcess(
|
||||
argv,
|
||||
0,
|
||||
json.dumps(
|
||||
[
|
||||
{
|
||||
"Id": "c" * 64,
|
||||
"RepoTags": ["localhost/demo:latest"],
|
||||
"RepoDigests": [],
|
||||
"Os": "linux",
|
||||
"Architecture": "amd64",
|
||||
}
|
||||
]
|
||||
),
|
||||
"",
|
||||
)
|
||||
raise AssertionError(argv)
|
||||
|
||||
monkeypatch.setattr(ci.subprocess, "run", fake_run)
|
||||
|
||||
result = ContainerImagesCollector(_context(tmp_path)).collect()
|
||||
|
||||
assert result.images[0]["pull_ref"] is None
|
||||
assert "exact digest-pinned pull cannot be rendered" in result.images[0]["notes"][0]
|
||||
|
|
|
|||
|
|
@ -1989,3 +1989,170 @@ def test_manifest_writes_sysctl_role(tmp_path: Path):
|
|||
pb = (out / "playbook.yml").read_text(encoding="utf-8")
|
||||
assert "role: sysctl" in pb
|
||||
assert (out / "roles" / "sysctl" / "files" / "sysctl" / "99-enroll.conf").exists()
|
||||
|
||||
|
||||
def test_manifest_renders_container_image_role_for_ansible(tmp_path: Path):
|
||||
bundle = tmp_path / "bundle"
|
||||
out = tmp_path / "ansible"
|
||||
digest = "docker.io/library/nginx@sha256:" + "a" * 64
|
||||
podman_digest = "quay.io/example/app@sha256:" + "b" * 64
|
||||
state = {
|
||||
"roles": {
|
||||
"users": {
|
||||
"role_name": "users",
|
||||
"users": [],
|
||||
"managed_files": [],
|
||||
"excluded": [],
|
||||
"notes": [],
|
||||
},
|
||||
"services": [],
|
||||
"packages": [],
|
||||
"container_images": {
|
||||
"role_name": "container_images",
|
||||
"images": [
|
||||
{
|
||||
"engine": "docker",
|
||||
"scope": "system",
|
||||
"user": None,
|
||||
"home": None,
|
||||
"image_id": "sha256:" + "c" * 64,
|
||||
"repo_tags": ["docker.io/library/nginx:1.27"],
|
||||
"repo_digests": [digest],
|
||||
"pull_ref": digest,
|
||||
"tag_aliases": [
|
||||
{
|
||||
"ref": "docker.io/library/nginx:1.27",
|
||||
"repository": "docker.io/library/nginx",
|
||||
"tag": "1.27",
|
||||
}
|
||||
],
|
||||
"os": "linux",
|
||||
"architecture": "amd64",
|
||||
"variant": None,
|
||||
"platform": "linux/amd64",
|
||||
"size": 123,
|
||||
"created": "2026-01-01T00:00:00Z",
|
||||
"source": "docker image inspect",
|
||||
"notes": [],
|
||||
},
|
||||
{
|
||||
"engine": "podman",
|
||||
"scope": "system",
|
||||
"user": None,
|
||||
"home": None,
|
||||
"image_id": "sha256:" + "d" * 64,
|
||||
"repo_tags": ["quay.io/example/app:prod"],
|
||||
"repo_digests": [podman_digest],
|
||||
"pull_ref": podman_digest,
|
||||
"tag_aliases": [
|
||||
{
|
||||
"ref": "quay.io/example/app:prod",
|
||||
"repository": "quay.io/example/app",
|
||||
"tag": "prod",
|
||||
}
|
||||
],
|
||||
"os": "linux",
|
||||
"architecture": "amd64",
|
||||
"variant": None,
|
||||
"platform": "linux/amd64",
|
||||
"size": 456,
|
||||
"created": "2026-01-01T00:00:00Z",
|
||||
"source": "podman image inspect",
|
||||
"notes": [],
|
||||
},
|
||||
],
|
||||
"notes": [],
|
||||
},
|
||||
}
|
||||
}
|
||||
bundle.mkdir(parents=True, exist_ok=True)
|
||||
(bundle / "state.json").write_text(json.dumps(state, indent=2), encoding="utf-8")
|
||||
|
||||
manifest.manifest(str(bundle), str(out))
|
||||
|
||||
defaults = (out / "roles" / "container_images" / "defaults" / "main.yml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
tasks = (out / "roles" / "container_images" / "tasks" / "main.yml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
meta = (out / "roles" / "container_images" / "meta" / "main.yml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
requirements = (out / "requirements.yml").read_text(encoding="utf-8")
|
||||
playbook = (out / "playbook.yml").read_text(encoding="utf-8")
|
||||
|
||||
assert "container_images:" in defaults
|
||||
assert digest in defaults
|
||||
assert podman_digest in defaults
|
||||
assert "community.docker.docker_image_pull" in tasks
|
||||
assert "community.docker.docker_image_tag" in tasks
|
||||
assert "containers.podman.podman_image" in tasks
|
||||
assert "containers.podman.podman_tag" in tasks
|
||||
assert "repository:" in tasks
|
||||
assert "target_names:" in tasks
|
||||
assert "community.docker" in meta
|
||||
assert "containers.podman" in meta
|
||||
assert "name: community.docker" in requirements
|
||||
assert "name: containers.podman" in requirements
|
||||
assert "role: container_images" in playbook
|
||||
|
||||
|
||||
def test_manifest_writes_container_images_to_hostvars_in_fqdn_mode(tmp_path: Path):
|
||||
bundle = tmp_path / "bundle"
|
||||
out = tmp_path / "ansible"
|
||||
digest = "docker.io/library/nginx@sha256:" + "a" * 64
|
||||
state = {
|
||||
"roles": {
|
||||
"users": {
|
||||
"role_name": "users",
|
||||
"users": [],
|
||||
"managed_files": [],
|
||||
"excluded": [],
|
||||
"notes": [],
|
||||
},
|
||||
"services": [],
|
||||
"packages": [],
|
||||
"container_images": {
|
||||
"role_name": "container_images",
|
||||
"images": [
|
||||
{
|
||||
"engine": "docker",
|
||||
"scope": "system",
|
||||
"user": None,
|
||||
"home": None,
|
||||
"image_id": "sha256:" + "c" * 64,
|
||||
"repo_tags": ["docker.io/library/nginx:1.27"],
|
||||
"repo_digests": [digest],
|
||||
"pull_ref": digest,
|
||||
"tag_aliases": [],
|
||||
"os": "linux",
|
||||
"architecture": "amd64",
|
||||
"variant": None,
|
||||
"platform": "linux/amd64",
|
||||
"size": 123,
|
||||
"created": "2026-01-01T00:00:00Z",
|
||||
"source": "docker image inspect",
|
||||
"notes": [],
|
||||
}
|
||||
],
|
||||
"notes": [],
|
||||
},
|
||||
}
|
||||
}
|
||||
bundle.mkdir(parents=True, exist_ok=True)
|
||||
(bundle / "state.json").write_text(json.dumps(state, indent=2), encoding="utf-8")
|
||||
|
||||
manifest.manifest(str(bundle), str(out), fqdn="host.example.test")
|
||||
|
||||
defaults = (out / "roles" / "container_images" / "defaults" / "main.yml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
hostvars = (
|
||||
out / "inventory" / "host_vars" / "host.example.test" / "container_images.yml"
|
||||
).read_text(encoding="utf-8")
|
||||
playbook = (out / "playbooks" / "host.example.test.yml").read_text(encoding="utf-8")
|
||||
|
||||
assert "container_images: []" in defaults
|
||||
assert digest in hostvars
|
||||
assert "role: container_images" in playbook
|
||||
|
|
|
|||
|
|
@ -603,3 +603,112 @@ def test_manifest_rejects_unknown_target(tmp_path: Path):
|
|||
assert "unsupported manifest target" in str(e)
|
||||
else:
|
||||
raise AssertionError("expected ValueError")
|
||||
|
||||
|
||||
def test_manifest_puppet_renders_container_images_static_and_hiera(tmp_path: Path):
|
||||
digest = "docker.io/library/nginx@sha256:" + "a" * 64
|
||||
podman_digest = "quay.io/example/app@sha256:" + "b" * 64
|
||||
state = {
|
||||
"roles": {
|
||||
"users": {
|
||||
"role_name": "users",
|
||||
"users": [],
|
||||
"managed_dirs": [],
|
||||
"managed_files": [],
|
||||
"excluded": [],
|
||||
"notes": [],
|
||||
},
|
||||
"services": [],
|
||||
"packages": [],
|
||||
"container_images": {
|
||||
"role_name": "container_images",
|
||||
"images": [
|
||||
{
|
||||
"engine": "docker",
|
||||
"scope": "system",
|
||||
"user": None,
|
||||
"home": None,
|
||||
"image_id": "sha256:" + "c" * 64,
|
||||
"repo_tags": ["docker.io/library/nginx:1.27"],
|
||||
"repo_digests": [digest],
|
||||
"pull_ref": digest,
|
||||
"tag_aliases": [
|
||||
{
|
||||
"ref": "docker.io/library/nginx:1.27",
|
||||
"repository": "docker.io/library/nginx",
|
||||
"tag": "1.27",
|
||||
}
|
||||
],
|
||||
"os": "linux",
|
||||
"architecture": "amd64",
|
||||
"variant": None,
|
||||
"platform": "linux/amd64",
|
||||
"size": 123,
|
||||
"created": "2026-01-01T00:00:00Z",
|
||||
"source": "docker image inspect",
|
||||
"notes": [],
|
||||
},
|
||||
{
|
||||
"engine": "podman",
|
||||
"scope": "system",
|
||||
"user": None,
|
||||
"home": None,
|
||||
"image_id": "sha256:" + "d" * 64,
|
||||
"repo_tags": ["quay.io/example/app:prod"],
|
||||
"repo_digests": [podman_digest],
|
||||
"pull_ref": podman_digest,
|
||||
"tag_aliases": [],
|
||||
"os": "linux",
|
||||
"architecture": "amd64",
|
||||
"variant": None,
|
||||
"platform": "linux/amd64",
|
||||
"size": 456,
|
||||
"created": "2026-01-01T00:00:00Z",
|
||||
"source": "podman image inspect",
|
||||
"notes": [],
|
||||
},
|
||||
],
|
||||
"notes": [],
|
||||
},
|
||||
}
|
||||
}
|
||||
bundle = tmp_path / "bundle"
|
||||
out = tmp_path / "puppet"
|
||||
_write_state(bundle, state)
|
||||
|
||||
manifest.manifest(str(bundle), str(out), target="puppet")
|
||||
|
||||
site_pp = (out / "manifests" / "site.pp").read_text(encoding="utf-8")
|
||||
assert "include container_images" in site_pp
|
||||
pp = (out / "modules" / "container_images" / "manifests" / "init.pp").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert "docker::image" in pp
|
||||
assert "image_digest => 'sha256:" + "a" * 64 + "'" in pp
|
||||
assert "docker tag" in pp
|
||||
assert "podman pull" in pp
|
||||
metadata = json.loads(
|
||||
(out / "modules" / "container_images" / "metadata.json").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)
|
||||
assert metadata["dependencies"] == [
|
||||
{"name": "puppetlabs-docker", "version_requirement": ">= 8.0.0 < 15.0.0"}
|
||||
]
|
||||
|
||||
fqdn_out = tmp_path / "puppet-fqdn"
|
||||
manifest.manifest(str(bundle), str(fqdn_out), target="puppet", fqdn="node.example")
|
||||
node_data = yaml.safe_load(
|
||||
(fqdn_out / "data" / "nodes" / "node.example.yaml").read_text(encoding="utf-8")
|
||||
)
|
||||
assert node_data["container_images::container_images"][0]["pull_ref"] == digest
|
||||
fqdn_pp = (
|
||||
fqdn_out / "modules" / "container_images" / "manifests" / "init.pp"
|
||||
).read_text(encoding="utf-8")
|
||||
assert "Array[Hash] $container_images = []" in fqdn_pp
|
||||
assert "docker::image" in fqdn_pp
|
||||
assert "enroll-podman-pull-${idx}" in fqdn_pp
|
||||
assert "$image['pull_cmd']" in fqdn_pp
|
||||
assert "podman pull" in (
|
||||
fqdn_out / "data" / "nodes" / "node.example.yaml"
|
||||
).read_text(encoding="utf-8")
|
||||
|
|
|
|||
|
|
@ -354,3 +354,104 @@ def test_cli_manifest_target_salt_is_forwarded(monkeypatch, tmp_path):
|
|||
assert called["harvest"] == str(tmp_path / "bundle")
|
||||
assert called["out"] == str(tmp_path / "salt")
|
||||
assert called["target"] == "salt"
|
||||
|
||||
|
||||
def test_manifest_salt_renders_container_images_in_single_and_fqdn_modes(
|
||||
tmp_path: Path,
|
||||
):
|
||||
digest = "docker.io/library/nginx@sha256:" + "a" * 64
|
||||
podman_digest = "quay.io/example/app@sha256:" + "b" * 64
|
||||
state = _sample_state()
|
||||
state["roles"]["container_images"] = {
|
||||
"role_name": "container_images",
|
||||
"images": [
|
||||
{
|
||||
"engine": "docker",
|
||||
"scope": "system",
|
||||
"user": None,
|
||||
"home": None,
|
||||
"image_id": "sha256:" + "c" * 64,
|
||||
"repo_tags": ["docker.io/library/nginx:1.27"],
|
||||
"repo_digests": [digest],
|
||||
"pull_ref": digest,
|
||||
"tag_aliases": [
|
||||
{
|
||||
"ref": "docker.io/library/nginx:1.27",
|
||||
"repository": "docker.io/library/nginx",
|
||||
"tag": "1.27",
|
||||
}
|
||||
],
|
||||
"os": "linux",
|
||||
"architecture": "amd64",
|
||||
"variant": None,
|
||||
"platform": "linux/amd64",
|
||||
"size": 123,
|
||||
"created": "2026-01-01T00:00:00Z",
|
||||
"source": "docker image inspect",
|
||||
"notes": [],
|
||||
},
|
||||
{
|
||||
"engine": "podman",
|
||||
"scope": "system",
|
||||
"user": None,
|
||||
"home": None,
|
||||
"image_id": "sha256:" + "d" * 64,
|
||||
"repo_tags": ["quay.io/example/app:prod"],
|
||||
"repo_digests": [podman_digest],
|
||||
"pull_ref": podman_digest,
|
||||
"tag_aliases": [
|
||||
{
|
||||
"ref": "quay.io/example/app:prod",
|
||||
"repository": "quay.io/example/app",
|
||||
"tag": "prod",
|
||||
}
|
||||
],
|
||||
"os": "linux",
|
||||
"architecture": "amd64",
|
||||
"variant": None,
|
||||
"platform": "linux/amd64",
|
||||
"size": 456,
|
||||
"created": "2026-01-01T00:00:00Z",
|
||||
"source": "podman image inspect",
|
||||
"notes": [],
|
||||
},
|
||||
],
|
||||
"notes": [],
|
||||
}
|
||||
bundle = tmp_path / "bundle"
|
||||
out = tmp_path / "salt"
|
||||
_write_sample_artifacts(bundle)
|
||||
_write_state(bundle, state)
|
||||
|
||||
manifest.manifest(str(bundle), str(out), target="salt")
|
||||
|
||||
top = yaml.safe_load((out / "states" / "top.sls").read_text(encoding="utf-8"))
|
||||
assert "roles.container_images" in top["base"]["*"]
|
||||
sls = (out / "states" / "roles" / "container_images" / "init.sls").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert "docker_image.present:" in sls
|
||||
assert digest in sls
|
||||
assert "docker tag" in sls
|
||||
assert "podman pull" in sls
|
||||
assert "podman tag" in sls
|
||||
|
||||
fqdn_out = tmp_path / "salt-fqdn"
|
||||
manifest.manifest(str(bundle), str(fqdn_out), target="salt", fqdn="node.example")
|
||||
pillar_top = yaml.safe_load(
|
||||
(fqdn_out / "pillar" / "top.sls").read_text(encoding="utf-8")
|
||||
)
|
||||
node_sls = pillar_top["base"]["node.example"][0]
|
||||
pillar_path = fqdn_out / "pillar" / Path(*node_sls.split("."))
|
||||
pillar = yaml.safe_load(pillar_path.with_suffix(".sls").read_text(encoding="utf-8"))
|
||||
assert (
|
||||
pillar["enroll"]["roles"]["container_images"]["container_images"][0]["pull_ref"]
|
||||
== digest
|
||||
)
|
||||
fqdn_sls = (
|
||||
fqdn_out / "states" / "roles" / "container_images" / "init.sls"
|
||||
).read_text(encoding="utf-8")
|
||||
assert "docker_image.present:" in fqdn_sls
|
||||
assert "enroll_podman_pull_container_images" in fqdn_sls
|
||||
assert "image.get('pull_cmd')" in fqdn_sls
|
||||
assert "podman pull" in pillar_path.with_suffix(".sls").read_text(encoding="utf-8")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue