"""Exercise the real producer script against Docker's inherited-label shape.""" from __future__ import annotations import hashlib import json import os import shutil import subprocess import sys import tarfile from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] PROFILES = ("checks", "backend", "frontend", "security", "browser", "build") # Captured from the SHA-verified immutable amd64 base image config c474d435... BASE_LABELS = { "io.buildah.version": "1.35.0", "org.opencontainers.image.authors": "Gitea", "org.opencontainers.image.created": "2026-10-01T00:01:57Z", "org.opencontainers.image.description": "Official docker images used by Gitea runner to run workflows.", "org.opencontainers.image.documentation": "https://gitea.com/gitea/runner-images/src/branch/main/README.md", "org.opencontainers.image.licenses": "MIT", "org.opencontainers.image.title": "Official Gitea Actions runner images", "org.opencontainers.image.url": "https://gitea.com/gitea/runner-images", "org.opencontainers.image.vendor": "Gitea", "org.opencontainers.image.version": "v26.10.01", } def _run_producer(tmp_path: Path, mutation: str = ""): repo = tmp_path / "repository" repo.mkdir() shutil.copytree(ROOT / "ops" / "runner-task-images", repo / "ops" / "runner-task-images") code = repo / "ops" / "gitea-deploy-broker" code.mkdir() shutil.copyfile(ROOT / "ops" / "gitea-deploy-broker" / "runner_profiles.py", code / "runner_profiles.py") scripts = repo / "scripts" scripts.mkdir() shutil.copyfile( ROOT / "scripts" / "normalize_ci_task_image_archive.py", scripts / "normalize_ci_task_image_archive.py", ) for args in ( ["init", "-q"], ["add", "ops", "scripts"], ["-c", "user.name=Producer test", "-c", "user.email=producer-test@localhost", "commit", "-qm", "fixture"], ): subprocess.run(["git", *args], cwd=repo, check=True, capture_output=True) revision = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=repo, text=True).strip() tree = subprocess.check_output(["git", "rev-parse", "HEAD^{tree}"], cwd=repo, text=True).strip() fake_bin = tmp_path / "bin" fake_bin.mkdir() docker = fake_bin / "docker" docker.write_text( f"#!{sys.executable}\n" "import hashlib,io,json,os,sys,tarfile\n" "from pathlib import Path\n" "a=sys.argv[1:]; p=Path(os.environ['FAKE_DOCKER_ROWS'])\n" "layer_buffer=io.BytesIO()\n" "with tarfile.open(fileobj=layer_buffer,mode='w:') as layer_tar:\n" " payload=b'producer fixture layer'; member=tarfile.TarInfo('fixture.txt'); member.size=len(payload); layer_tar.addfile(member,io.BytesIO(payload))\n" "layer=layer_buffer.getvalue(); layer_sha=hashlib.sha256(layer).hexdigest()\n" "if a==['info']: sys.exit(0)\n" "if a[0]=='build':\n" " builds=Path(os.environ['FAKE_DOCKER_BUILD_ARGS'])\n" " argv=json.loads(builds.read_text()) if builds.exists() else []\n" " argv.append(a); builds.write_text(json.dumps(argv))\n" " pairs=[a[i+1].split('=',1) for i,x in enumerate(a) if x=='--build-arg']\n" " b=dict(pairs); profile=b['PROFILE']; ref=a[a.index('--tag')+1]\n" f" labels={BASE_LABELS!r}.copy()\n" " labels.update({'org.opencontainers.image.source':'https://gitea.example.com/Jens/Unraid-AppOps-Gateway'," "'org.opencontainers.image.revision':b['SOURCE_REVISION'],'tech.itworx.source-tree':b['SOURCE_TREE'],'tech.itworx.ci.profile':profile})\n" " mutation=os.environ.get('FAKE_LABEL_MUTATION','')\n" " if mutation=='extra': labels['unexpected.label']='unexpected'\n" " if mutation=='missing-inherited': labels.pop('org.opencontainers.image.vendor')\n" " if mutation=='changed-inherited': labels['org.opencontainers.image.created']='different'\n" " if mutation=='wrong-source': labels['org.opencontainers.image.source']='https://example.invalid'\n" " if mutation=='missing-profile': labels.pop('tech.itworx.ci.profile')\n" " config={'architecture':'amd64','os':'linux','config':{'User':'ubuntu','Labels':labels},'rootfs':{'type':'layers','diff_ids':['sha256:'+layer_sha]}}\n" " config_bytes=json.dumps(config,sort_keys=True,separators=(',',':')).encode(); image_id='sha256:'+hashlib.sha256(config_bytes).hexdigest()\n" " rows=json.loads(p.read_text()) if p.exists() else []\n" " rows.append({'Id':image_id,'Os':'linux','Architecture':'amd64','RepoTags':[ref],'Config':{'User':'ubuntu','Labels':labels}})\n" " configs=Path(os.environ['FAKE_DOCKER_CONFIGS']); configs.mkdir(exist_ok=True); (configs/(profile+'.json')).write_bytes(config_bytes)\n" " p.write_text(json.dumps(rows)); sys.exit(0)\n" "if a[:2]==['image','inspect']: print(p.read_text()); sys.exit(0)\n" "if a[:2]==['image','save']:\n" " selected=set(a[a.index('--output')+2:]); rows=json.loads(p.read_text()); members={'blobs/sha256/'+layer_sha:layer}; manifest=[]\n" " for row in rows:\n" " if row['RepoTags'][0] not in selected: continue\n" " reference=row['RepoTags'][0]; profile=reference.split('itworx-ci-',1)[1].split(':sha-',1)[0]; config=(Path(os.environ['FAKE_DOCKER_CONFIGS'])/(profile+'.json')).read_bytes(); config_sha=hashlib.sha256(config).hexdigest(); config_path='blobs/sha256/'+config_sha; members[config_path]=config\n" " manifest.append({'Config':config_path,'RepoTags':[reference],'Layers':['blobs/sha256/'+layer_sha],'LayerSources':{'sha256:'+layer_sha:{'mediaType':'application/vnd.oci.image.layer.v1.tar','size':len(layer),'digest':'sha256:'+layer_sha}}})\n" " members['manifest.json']=json.dumps(manifest,separators=(',',':')).encode(); members['oci-layout']=json.dumps({'imageLayoutVersion':'1.0.0'}).encode(); members['index.json']=json.dumps({'schemaVersion':2,'manifests':[]}).encode()\n" " with tarfile.open(a[a.index('--output')+1],mode='w:') as archive:\n" " for name,payload in members.items():\n" " member=tarfile.TarInfo(name); member.size=len(payload); archive.addfile(member,io.BytesIO(payload))\n" " sys.exit(0)\n" "sys.exit('unexpected Docker invocation')\n", encoding="utf-8", ) docker.chmod(0o755) output = tmp_path / "output" env = { **os.environ, "PATH": str(fake_bin) + os.pathsep + os.environ["PATH"], "GITHUB_EVENT_NAME": "push", "GITHUB_REF": "refs/heads/main", "GITHUB_REPOSITORY": "Jens/Unraid-AppOps-Gateway", "FAKE_DOCKER_ROWS": str(tmp_path / "docker-rows.json"), "FAKE_DOCKER_BUILD_ARGS": str(tmp_path / "docker-build-args.json"), "FAKE_DOCKER_CONFIGS": str(tmp_path / "docker-configs"), "FAKE_LABEL_MUTATION": mutation, } Path(env["FAKE_DOCKER_CONFIGS"]).mkdir() result = subprocess.run( ["bash", str(ROOT / "scripts" / "build_ci_task_images.sh"), revision, "123", str(output)], cwd=repo, env=env, capture_output=True, text=True, timeout=30, ) return result, output, revision, tree @pytest.mark.skipif(os.name == "nt", reason="producer is a POSIX shell script") def test_producer_accepts_exact_pinned_base_metadata_and_keeps_provenance_manifest(tmp_path): result, output, revision, tree = _run_producer(tmp_path) assert result.returncode == 0, result.stderr build_args = json.loads((tmp_path / "docker-build-args.json").read_text()) assert len(build_args) == len(PROFILES) expected_limits = { "--cpu-period=100000", "--cpu-quota=300000", "--memory=5120m", "--memory-swap=5120m", "--cgroup-parent=docker", "--rm=true", "--force-rm=true", } profiles = [ next( args[index + 1] for index, arg in enumerate(args[:-1]) if arg == "--build-arg" and args[index + 1].startswith("PROFILE=") ) for args in build_args ] assert profiles == [f"PROFILE={profile}" for profile in PROFILES] for args in build_args: resource_flags = [ arg for arg in args if arg.startswith( ( "--cpu-period", "--cpu-quota", "--memory", "--cgroup-parent", "--rm", "--force-rm", "--cpus", ) ) ] assert set(resource_flags) == expected_limits assert len(resource_flags) == len(expected_limits) manifest = json.loads((output / "task-image-manifest.json").read_text()) assert manifest["sourceRevision"] == revision assert manifest["sourceTree"] == tree assert tuple(row["profile"] for row in manifest["images"]) == PROFILES for row in manifest["images"]: assert row["labels"] == { "org.opencontainers.image.source": "https://gitea.example.com/Jens/Unraid-AppOps-Gateway", "org.opencontainers.image.revision": revision, "tech.itworx.source-tree": tree, "tech.itworx.ci.profile": row["profile"], } archive_path = output / "task-images.tar" archive_bytes = archive_path.read_bytes() assert manifest["dockerTarSha256"] == hashlib.sha256(archive_bytes).hexdigest() assert manifest["dockerTarBytes"] == len(archive_bytes) inspected = { image["RepoTags"][0]: image for image in json.loads((tmp_path / "docker-rows.json").read_text()) } with tarfile.open(archive_path, mode="r:") as archive: members = {member.name: member for member in archive} normalized = json.loads(archive.extractfile(members["manifest.json"]).read()) assert len(normalized) == len(PROFILES) assert all(set(item) == {"Config", "RepoTags", "Layers"} for item in normalized) for item, row in zip(normalized, manifest["images"], strict=True): assert item["RepoTags"] == [row["reference"]] assert item["Config"] == row["imageId"].removeprefix("sha256:") + ".json" assert len(item["Layers"]) == 1 image = inspected[row["reference"]] config_bytes = archive.extractfile(members[item["Config"]]).read() config = json.loads(config_bytes) assert "sha256:" + hashlib.sha256(config_bytes).hexdigest() == image["Id"] == row["imageId"] assert config["config"]["User"] == "ubuntu" assert config["config"]["Labels"] == image["Config"]["Labels"] == { **BASE_LABELS, **row["labels"], } assert config["rootfs"]["type"] == "layers" assert len(config["rootfs"]["diff_ids"]) == 1 diff_id = config["rootfs"]["diff_ids"][0] layer_name = item["Layers"][0] assert layer_name == diff_id.removeprefix("sha256:") + "/layer.tar" layer_bytes = archive.extractfile(members[layer_name]).read() assert "sha256:" + hashlib.sha256(layer_bytes).hexdigest() == diff_id assert not any(name.startswith("blobs/") for name in members) assert not (output / ".task-images.raw.tar").exists() assert not (output / "images.inspect.json").exists() @pytest.mark.skipif(os.name == "nt", reason="producer is a POSIX shell script") @pytest.mark.parametrize("mutation", ("extra", "missing-inherited", "changed-inherited", "wrong-source", "missing-profile")) def test_producer_rejects_any_untrusted_image_label_change(tmp_path, mutation): result, output, _revision, _tree = _run_producer(tmp_path, mutation) assert result.returncode != 0 expected_error = ( "task image archive normalization failed: image inspection provenance invalid" if mutation in {"wrong-source", "missing-profile"} else "image identity verification failed" ) assert expected_error in result.stderr assert not (output / "task-image-manifest.json").exists()