Publish curated ITWorx_MCP_Hub source from 55011066f84f073ceabb35c99ddd5b65745e34f6
This commit is contained in:
1 parent
020282a4b5
commit
b35324a665
6 files changed
+653
-11
No files matched your search
@@ -1,3 +1,3 @@
|
|||||||
# Public source export
|
# Public source export
|
||||||
|
|
||||||
This source snapshot was generated from the private canonical repository at revision `ce7b7c333c5b287456ad4a19c90a4e9b4ee02dcf`. It contains no private Git history or operational evidence. Changes are published from the canonical repository.
|
This source snapshot was generated from the private canonical repository at revision `55011066f84f073ceabb35c99ddd5b65745e34f6`. It contains no private Git history or operational evidence. Changes are published from the canonical repository.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"sourceRevision": "ce7b7c333c5b287456ad4a19c90a4e9b4ee02dcf",
|
"sourceRevision": "55011066f84f073ceabb35c99ddd5b65745e34f6",
|
||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"path": ".dockerignore",
|
"path": ".dockerignore",
|
||||||
@@ -2472,6 +2472,11 @@
|
|||||||
"sha256": "f42836af5cf674c73824ab753984defc4094b3112c9f6709eef56795f45ab23d",
|
"sha256": "f42836af5cf674c73824ab753984defc4094b3112c9f6709eef56795f45ab23d",
|
||||||
"bytes": 2992
|
"bytes": 2992
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "scripts/generate_moby_v1_golden.py",
|
||||||
|
"sha256": "a0b634a41d9266412abd4a6193bb171d48d0256dba23140cd5617366e562b930",
|
||||||
|
"bytes": 6874
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "scripts/generate_sbom.py",
|
"path": "scripts/generate_sbom.py",
|
||||||
"sha256": "aa5aecf22ba335e80cffcf2a7fb21b99f1b3370ed4d8706158725c07203b5b44",
|
"sha256": "aa5aecf22ba335e80cffcf2a7fb21b99f1b3370ed4d8706158725c07203b5b44",
|
||||||
@@ -3052,6 +3057,16 @@
|
|||||||
"sha256": "006648857d313b8f7fcaa62b86d5f453aa565a75a348aefe7b8557e3b4ba8620",
|
"sha256": "006648857d313b8f7fcaa62b86d5f453aa565a75a348aefe7b8557e3b4ba8620",
|
||||||
"bytes": 63
|
"bytes": 63
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "tests/fixtures/moby_v1_golden.json",
|
||||||
|
"sha256": "79e78870bad30b6ceee4bbd4b62243842f3aee73a3a2c8c5d61d9f301f7c61a7",
|
||||||
|
"bytes": 9814
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tests/fixtures/moby_v1_reference/main.go",
|
||||||
|
"sha256": "b700de1bf2e0d9414c388e9d4b61850e53dfe08a90497f592f45caff71eebbdd",
|
||||||
|
"bytes": 2765
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "tests/fixtures/projectbrain-managed-validation-browser.yml",
|
"path": "tests/fixtures/projectbrain-managed-validation-browser.yml",
|
||||||
"sha256": "6baaf4ceb2e493f90cc8210953a92f8f2c6da723e9c85446f38670efa285d9cc",
|
"sha256": "6baaf4ceb2e493f90cc8210953a92f8f2c6da723e9c85446f38670efa285d9cc",
|
||||||
@@ -3364,8 +3379,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "tests/test_mcp_hub_release_images.py",
|
"path": "tests/test_mcp_hub_release_images.py",
|
||||||
"sha256": "c44fb08a07c73164769f97b70f42b5fc412de0ef85ee4a9af62a28b0bfccfd82",
|
"sha256": "38cefc6b3d12f8c197ae522c5bd6ecc9e20407bb3be2dca7fe593a87229eecca",
|
||||||
"bytes": 24244
|
"bytes": 31898
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "tests/test_mcp_workflow_prompts.py",
|
"path": "tests/test_mcp_workflow_prompts.py",
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
"""Regenerate test-only V1 wire goldens with Go and source-pinned Moby structs.
|
||||||
|
|
||||||
|
No Hub normalizer code is imported. This installs nothing, builds no image and
|
||||||
|
changes only the fixed test fixture. Go 1.27.2 is a local regeneration tool.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
FIXTURE = ROOT / "tests/fixtures/moby_v1_golden.json"
|
||||||
|
REFERENCE = ROOT / "tests/fixtures/moby_v1_reference/main.go"
|
||||||
|
PINS = (
|
||||||
|
(
|
||||||
|
"Old",
|
||||||
|
"onbuild_null",
|
||||||
|
"89c5e8fd66634b6128fc4c0e6f1236e2540e46e0",
|
||||||
|
"image/image.go",
|
||||||
|
"5ce9ea9e6daeb5cecf03dbfafe7c895516da782010e2797b1fc0f979161cf5be",
|
||||||
|
"08df9dce6147eb2a4edb9859d81fedbdb13f2af2e249db38bf79361f67db4bcf",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"New",
|
||||||
|
"onbuild_omitempty",
|
||||||
|
"5834c866f83c0bae2d0be28f38b92f32cd07ca38",
|
||||||
|
"daemon/internal/image/image.go",
|
||||||
|
"c186304737e0343a04e0f9a4e88f8f81b867e86159cd7bf68dd0d35a8392d3a8",
|
||||||
|
"06f5e71501399a33906ef7b006d49d71a700e6c84d7442a90e9d240cb52b81ee",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def source(commit: str, path: str, expected: str) -> str:
|
||||||
|
url = f"https://raw.githubusercontent.com/moby/moby/{commit}/{path}"
|
||||||
|
with urllib.request.build_opener(urllib.request.ProxyHandler({})).open(
|
||||||
|
url, timeout=20
|
||||||
|
) as response:
|
||||||
|
data = response.read(200_001)
|
||||||
|
if (
|
||||||
|
not isinstance(data, bytes)
|
||||||
|
or len(data) > 200_000
|
||||||
|
or hashlib.sha256(data).hexdigest() != expected
|
||||||
|
):
|
||||||
|
raise ValueError("pinned reference source digest mismatch")
|
||||||
|
return data.decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def declaration(text: str, original: str, replacement: str) -> str:
|
||||||
|
match = re.search(rf"(?ms)^type {original} struct \{{\n.*?^\}}", text)
|
||||||
|
if not match:
|
||||||
|
raise ValueError("pinned reference struct is missing")
|
||||||
|
matched_source = match.group()
|
||||||
|
if not isinstance(matched_source, str):
|
||||||
|
raise ValueError("pinned reference declaration is not text")
|
||||||
|
return matched_source.replace(f"type {original} struct", f"type {replacement} struct", 1)
|
||||||
|
|
||||||
|
|
||||||
|
def generate(go: Path) -> None:
|
||||||
|
if not go.is_absolute() or not go.is_file():
|
||||||
|
raise ValueError("an existing absolute Go executable is required")
|
||||||
|
env = dict(os.environ)
|
||||||
|
env.update(
|
||||||
|
GOTOOLCHAIN="local",
|
||||||
|
GOPROXY="off",
|
||||||
|
GOSUMDB="off",
|
||||||
|
GO111MODULE="off",
|
||||||
|
GOMAXPROCS="2",
|
||||||
|
GOFLAGS="-p=2",
|
||||||
|
CGO_ENABLED="0",
|
||||||
|
)
|
||||||
|
version = subprocess.run( # noqa: S603 - explicit existing absolute regeneration compiler
|
||||||
|
[str(go), "version"], check=True, capture_output=True, text=True, env=env
|
||||||
|
)
|
||||||
|
if version.stdout.split()[2] != "go1.27.2":
|
||||||
|
raise ValueError("reference regeneration requires Go 1.27.2")
|
||||||
|
# These are public synthetic inputs: no runtime config, credentials or image
|
||||||
|
# bytes are sent to the reference compiler or persisted in this fixture.
|
||||||
|
inputs = [
|
||||||
|
{
|
||||||
|
"architecture": "amd64",
|
||||||
|
"os": "linux",
|
||||||
|
"created": "2026-10-14T00:01:02.123456789Z",
|
||||||
|
"container_config": {
|
||||||
|
"User": "constructor",
|
||||||
|
"Cmd": ["µ<&>\u2028\u2029"],
|
||||||
|
"Labels": {"z": "last", "a": "first"},
|
||||||
|
"StopTimeout": 0,
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"User": "10001:10001",
|
||||||
|
"Cmd": ["reference"],
|
||||||
|
"Labels": {"z": "µ<&>\u2028\u2029", "a": "first"},
|
||||||
|
"StopTimeout": 0,
|
||||||
|
"Healthcheck": {"Test": ["CMD", "true"], "Interval": 30000000000, "Retries": 2},
|
||||||
|
},
|
||||||
|
"rootfs": {"type": "layers", "diff_ids": ["sha256:" + "a" * 64, "sha256:" + "b" * 64]},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"architecture": "amd64",
|
||||||
|
"os": "linux",
|
||||||
|
"config": {"User": "10001:10001"},
|
||||||
|
"rootfs": {"type": "layers", "diff_ids": ["sha256:" + "c" * 64]},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
declarations = [
|
||||||
|
'package main\nimport "time"\n',
|
||||||
|
"""type HealthConfig struct {
|
||||||
|
Test []string `json:",omitempty"`
|
||||||
|
Interval time.Duration `json:",omitempty"`
|
||||||
|
Timeout time.Duration `json:",omitempty"`
|
||||||
|
StartPeriod time.Duration `json:",omitempty"`
|
||||||
|
StartInterval time.Duration `json:",omitempty"`
|
||||||
|
Retries int `json:",omitempty"`
|
||||||
|
}\n""",
|
||||||
|
]
|
||||||
|
for label, _dialect, commit, image_path, image_hash, config_hash in PINS:
|
||||||
|
config = declaration(
|
||||||
|
source(commit, "api/types/container/config.go", config_hash), "Config", label + "Config"
|
||||||
|
)
|
||||||
|
config = (
|
||||||
|
config.replace("strslice.StrSlice", "[]string")
|
||||||
|
.replace("nat.PortSet", "map[string]struct{}")
|
||||||
|
.replace("network.PortSet", "map[string]struct{}")
|
||||||
|
)
|
||||||
|
image = declaration(source(commit, image_path, image_hash), "V1Image", label + "V1")
|
||||||
|
declarations.extend((config, image.replace("container.Config", label + "Config")))
|
||||||
|
local = ROOT / ".luna"
|
||||||
|
local.mkdir(exist_ok=True)
|
||||||
|
if local.is_symlink() or not local.resolve().is_relative_to(ROOT.resolve()):
|
||||||
|
raise ValueError("reference scratch directory must stay in this checkout")
|
||||||
|
with tempfile.TemporaryDirectory(prefix="moby-v1-reference-", dir=local) as name:
|
||||||
|
temp = Path(name)
|
||||||
|
if not temp.resolve().is_relative_to(local.resolve()):
|
||||||
|
raise ValueError("reference temporary directory escaped its owner")
|
||||||
|
env["GOCACHE"] = str(temp / "cache")
|
||||||
|
(temp / "main.go").write_bytes(REFERENCE.read_bytes())
|
||||||
|
(temp / "types.go").write_text("\n".join(declarations), encoding="utf-8")
|
||||||
|
(temp / "inputs.json").write_text(json.dumps(inputs, ensure_ascii=False), encoding="utf-8")
|
||||||
|
completed = subprocess.run( # noqa: S603 - fixed standard-library-only reference program
|
||||||
|
[str(go), "run", ".", str(temp / "inputs.json")],
|
||||||
|
cwd=temp,
|
||||||
|
env=env,
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
timeout=180,
|
||||||
|
)
|
||||||
|
if len(completed.stdout) > 100_000:
|
||||||
|
raise ValueError("reference output exceeded its fixed bound")
|
||||||
|
cases = json.loads(completed.stdout)
|
||||||
|
result = {
|
||||||
|
"schema": "itworx.moby-v1-wire-golden/v1",
|
||||||
|
"goVersion": "go1.27.2",
|
||||||
|
"referenceMainSha256": hashlib.sha256(REFERENCE.read_bytes()).hexdigest(),
|
||||||
|
"sourceCommits": [pin[2] for pin in PINS],
|
||||||
|
"cases": cases,
|
||||||
|
}
|
||||||
|
FIXTURE.write_text(json.dumps(result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
||||||
|
print(f"Generated {len(cases)} independent Go reference cases")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--go", type=Path, required=True)
|
||||||
|
generate(parser.parse_args().go)
|
||||||
Vendored
+165
@@ -0,0 +1,165 @@
|
|||||||
|
{
|
||||||
|
"schema": "itworx.moby-v1-wire-golden/v1",
|
||||||
|
"goVersion": "go1.27.2",
|
||||||
|
"referenceMainSha256": "b700de1bf2e0d9414c388e9d4b61850e53dfe08a90497f592f45caff71eebbdd",
|
||||||
|
"sourceCommits": [
|
||||||
|
"89c5e8fd66634b6128fc4c0e6f1236e2540e46e0",
|
||||||
|
"5834c866f83c0bae2d0be28f38b92f32cd07ca38"
|
||||||
|
],
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"dialect": "onbuild_null",
|
||||||
|
"config": {
|
||||||
|
"architecture": "amd64",
|
||||||
|
"os": "linux",
|
||||||
|
"created": "2026-10-14T00:01:02.123456789Z",
|
||||||
|
"container_config": {
|
||||||
|
"User": "constructor",
|
||||||
|
"Cmd": [
|
||||||
|
"µ<&>
"
|
||||||
|
],
|
||||||
|
"Labels": {
|
||||||
|
"z": "last",
|
||||||
|
"a": "first"
|
||||||
|
},
|
||||||
|
"StopTimeout": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"User": "10001:10001",
|
||||||
|
"Cmd": [
|
||||||
|
"reference"
|
||||||
|
],
|
||||||
|
"Labels": {
|
||||||
|
"z": "µ<&>
",
|
||||||
|
"a": "first"
|
||||||
|
},
|
||||||
|
"StopTimeout": 0,
|
||||||
|
"Healthcheck": {
|
||||||
|
"Test": [
|
||||||
|
"CMD",
|
||||||
|
"true"
|
||||||
|
],
|
||||||
|
"Interval": 30000000000,
|
||||||
|
"Retries": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rootfs": {
|
||||||
|
"type": "layers",
|
||||||
|
"diff_ids": [
|
||||||
|
"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"blobs": [
|
||||||
|
{
|
||||||
|
"name": "blobs/sha256/e9e75851959057c9547c9692b88370291d478cbc2c4ef5752074971970f36b3c",
|
||||||
|
"payloadBase64": "eyJpZCI6IjRiZDY5YzRjYTE1OWQ4NzI0MGJmZTVlZTViM2ZmNjliZjUwZWY3YWJkMGQzZDc4NjA4MTgyNjJlZDU2Y2ViZGIiLCJjcmVhdGVkIjoiMTk3MC0wMS0wMVQwMDowMDowMFoiLCJjb250YWluZXJfY29uZmlnIjp7Ikhvc3RuYW1lIjoiIiwiRG9tYWlubmFtZSI6IiIsIlVzZXIiOiIiLCJBdHRhY2hTdGRpbiI6ZmFsc2UsIkF0dGFjaFN0ZG91dCI6ZmFsc2UsIkF0dGFjaFN0ZGVyciI6ZmFsc2UsIlR0eSI6ZmFsc2UsIk9wZW5TdGRpbiI6ZmFsc2UsIlN0ZGluT25jZSI6ZmFsc2UsIkVudiI6bnVsbCwiQ21kIjpudWxsLCJJbWFnZSI6IiIsIlZvbHVtZXMiOm51bGwsIldvcmtpbmdEaXIiOiIiLCJFbnRyeXBvaW50IjpudWxsLCJPbkJ1aWxkIjpudWxsLCJMYWJlbHMiOm51bGx9LCJvcyI6ImxpbnV4In0="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "blobs/sha256/777fb43341dc2ede2a03d9c8e9d79f15d64a4be734f6f720e0a36653cf7d644b",
|
||||||
|
"payloadBase64": "eyJpZCI6ImRkZDkyMDY4YWNlMzA3NGQxY2NlYTZhNDE0ODAwMWUxYTIyYjY4M2FiZWM4NmViMTUyNjMwMzk5YThhZWY4YzkiLCJwYXJlbnQiOiI0YmQ2OWM0Y2ExNTlkODcyNDBiZmU1ZWU1YjNmZjY5YmY1MGVmN2FiZDBkM2Q3ODYwODE4MjYyZWQ1NmNlYmRiIiwiY3JlYXRlZCI6IjIwMjYtMTAtMTRUMDA6MDE6MDIuMTIzNDU2Nzg5WiIsImNvbnRhaW5lcl9jb25maWciOnsiSG9zdG5hbWUiOiIiLCJEb21haW5uYW1lIjoiIiwiVXNlciI6ImNvbnN0cnVjdG9yIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOm51bGwsIkNtZCI6WyLCtVx1MDAzY1x1MDAyNlx1MDAzZVx1MjAyOFx1MjAyOSJdLCJJbWFnZSI6IiIsIlZvbHVtZXMiOm51bGwsIldvcmtpbmdEaXIiOiIiLCJFbnRyeXBvaW50IjpudWxsLCJPbkJ1aWxkIjpudWxsLCJMYWJlbHMiOnsiYSI6ImZpcnN0IiwieiI6Imxhc3QifSwiU3RvcFRpbWVvdXQiOjB9LCJjb25maWciOnsiSG9zdG5hbWUiOiIiLCJEb21haW5uYW1lIjoiIiwiVXNlciI6IjEwMDAxOjEwMDAxIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOm51bGwsIkNtZCI6WyJyZWZlcmVuY2UiXSwiSGVhbHRoY2hlY2siOnsiVGVzdCI6WyJDTUQiLCJ0cnVlIl0sIkludGVydmFsIjozMDAwMDAwMDAwMCwiUmV0cmllcyI6Mn0sIkltYWdlIjoiIiwiVm9sdW1lcyI6bnVsbCwiV29ya2luZ0RpciI6IiIsIkVudHJ5cG9pbnQiOm51bGwsIk9uQnVpbGQiOm51bGwsIkxhYmVscyI6eyJhIjoiZmlyc3QiLCJ6IjoiwrVcdTAwM2NcdTAwMjZcdTAwM2VcdTIwMjhcdTIwMjkifSwiU3RvcFRpbWVvdXQiOjB9LCJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsIm9zIjoibGludXgifQ=="
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dialect": "onbuild_omitempty",
|
||||||
|
"config": {
|
||||||
|
"architecture": "amd64",
|
||||||
|
"os": "linux",
|
||||||
|
"created": "2026-10-14T00:01:02.123456789Z",
|
||||||
|
"container_config": {
|
||||||
|
"User": "constructor",
|
||||||
|
"Cmd": [
|
||||||
|
"µ<&>
"
|
||||||
|
],
|
||||||
|
"Labels": {
|
||||||
|
"z": "last",
|
||||||
|
"a": "first"
|
||||||
|
},
|
||||||
|
"StopTimeout": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"User": "10001:10001",
|
||||||
|
"Cmd": [
|
||||||
|
"reference"
|
||||||
|
],
|
||||||
|
"Labels": {
|
||||||
|
"z": "µ<&>
",
|
||||||
|
"a": "first"
|
||||||
|
},
|
||||||
|
"StopTimeout": 0,
|
||||||
|
"Healthcheck": {
|
||||||
|
"Test": [
|
||||||
|
"CMD",
|
||||||
|
"true"
|
||||||
|
],
|
||||||
|
"Interval": 30000000000,
|
||||||
|
"Retries": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rootfs": {
|
||||||
|
"type": "layers",
|
||||||
|
"diff_ids": [
|
||||||
|
"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"blobs": [
|
||||||
|
{
|
||||||
|
"name": "blobs/sha256/cab874d9c7a96fe390da5177f6b0beaa00ba368a72269d8e566b0274b9ea16c6",
|
||||||
|
"payloadBase64": "eyJpZCI6IjdhOWExYmE1MTFjYTkwYWZkODUxM2YyNzVhMGJkNWRjODNjZWY2NDJjMjkzYjUyZTYxYTkzNTBjZThiMzk0OTMiLCJjcmVhdGVkIjoiMTk3MC0wMS0wMVQwMDowMDowMFoiLCJjb250YWluZXJfY29uZmlnIjp7Ikhvc3RuYW1lIjoiIiwiRG9tYWlubmFtZSI6IiIsIlVzZXIiOiIiLCJBdHRhY2hTdGRpbiI6ZmFsc2UsIkF0dGFjaFN0ZG91dCI6ZmFsc2UsIkF0dGFjaFN0ZGVyciI6ZmFsc2UsIlR0eSI6ZmFsc2UsIk9wZW5TdGRpbiI6ZmFsc2UsIlN0ZGluT25jZSI6ZmFsc2UsIkVudiI6bnVsbCwiQ21kIjpudWxsLCJJbWFnZSI6IiIsIlZvbHVtZXMiOm51bGwsIldvcmtpbmdEaXIiOiIiLCJFbnRyeXBvaW50IjpudWxsLCJMYWJlbHMiOm51bGx9LCJvcyI6ImxpbnV4In0="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "blobs/sha256/f51881ca0e4436a2ca17e4d94fdc08590cabe3aa345c5ae290f899869ab824ba",
|
||||||
|
"payloadBase64": "eyJpZCI6IjNiMWFhZTBhMjU4NmU0MWI1YzdiN2UzODdhYTQxNjYxOWU5YjBlYWRmMTU4Nzg1OTE3ZDJlMGMxY2ZlNzVmYzciLCJwYXJlbnQiOiI3YTlhMWJhNTExY2E5MGFmZDg1MTNmMjc1YTBiZDVkYzgzY2VmNjQyYzI5M2I1MmU2MWE5MzUwY2U4YjM5NDkzIiwiY3JlYXRlZCI6IjIwMjYtMTAtMTRUMDA6MDE6MDIuMTIzNDU2Nzg5WiIsImNvbnRhaW5lcl9jb25maWciOnsiSG9zdG5hbWUiOiIiLCJEb21haW5uYW1lIjoiIiwiVXNlciI6ImNvbnN0cnVjdG9yIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOm51bGwsIkNtZCI6WyLCtVx1MDAzY1x1MDAyNlx1MDAzZVx1MjAyOFx1MjAyOSJdLCJJbWFnZSI6IiIsIlZvbHVtZXMiOm51bGwsIldvcmtpbmdEaXIiOiIiLCJFbnRyeXBvaW50IjpudWxsLCJMYWJlbHMiOnsiYSI6ImZpcnN0IiwieiI6Imxhc3QifSwiU3RvcFRpbWVvdXQiOjB9LCJjb25maWciOnsiSG9zdG5hbWUiOiIiLCJEb21haW5uYW1lIjoiIiwiVXNlciI6IjEwMDAxOjEwMDAxIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOm51bGwsIkNtZCI6WyJyZWZlcmVuY2UiXSwiSGVhbHRoY2hlY2siOnsiVGVzdCI6WyJDTUQiLCJ0cnVlIl0sIkludGVydmFsIjozMDAwMDAwMDAwMCwiUmV0cmllcyI6Mn0sIkltYWdlIjoiIiwiVm9sdW1lcyI6bnVsbCwiV29ya2luZ0RpciI6IiIsIkVudHJ5cG9pbnQiOm51bGwsIkxhYmVscyI6eyJhIjoiZmlyc3QiLCJ6IjoiwrVcdTAwM2NcdTAwMjZcdTAwM2VcdTIwMjhcdTIwMjkifSwiU3RvcFRpbWVvdXQiOjB9LCJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsIm9zIjoibGludXgifQ=="
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dialect": "onbuild_null",
|
||||||
|
"config": {
|
||||||
|
"architecture": "amd64",
|
||||||
|
"os": "linux",
|
||||||
|
"config": {
|
||||||
|
"User": "10001:10001"
|
||||||
|
},
|
||||||
|
"rootfs": {
|
||||||
|
"type": "layers",
|
||||||
|
"diff_ids": [
|
||||||
|
"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"blobs": [
|
||||||
|
{
|
||||||
|
"name": "blobs/sha256/8d095090ae9bff4df0f104355b1d57c36f538a9ab088bd9962ecc042c84fbd2c",
|
||||||
|
"payloadBase64": "eyJpZCI6IjUzODdhM2U3NzM3YWRkMGFlOTRhZDZiZGM2ZmYwYzc5MjY5OTEyNDg1NGE3Y2MxNDgxM2FmNDViNzE5Yjk2OTAiLCJjcmVhdGVkIjpudWxsLCJjb250YWluZXJfY29uZmlnIjp7Ikhvc3RuYW1lIjoiIiwiRG9tYWlubmFtZSI6IiIsIlVzZXIiOiIiLCJBdHRhY2hTdGRpbiI6ZmFsc2UsIkF0dGFjaFN0ZG91dCI6ZmFsc2UsIkF0dGFjaFN0ZGVyciI6ZmFsc2UsIlR0eSI6ZmFsc2UsIk9wZW5TdGRpbiI6ZmFsc2UsIlN0ZGluT25jZSI6ZmFsc2UsIkVudiI6bnVsbCwiQ21kIjpudWxsLCJJbWFnZSI6IiIsIlZvbHVtZXMiOm51bGwsIldvcmtpbmdEaXIiOiIiLCJFbnRyeXBvaW50IjpudWxsLCJPbkJ1aWxkIjpudWxsLCJMYWJlbHMiOm51bGx9LCJjb25maWciOnsiSG9zdG5hbWUiOiIiLCJEb21haW5uYW1lIjoiIiwiVXNlciI6IjEwMDAxOjEwMDAxIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOm51bGwsIkNtZCI6bnVsbCwiSW1hZ2UiOiIiLCJWb2x1bWVzIjpudWxsLCJXb3JraW5nRGlyIjoiIiwiRW50cnlwb2ludCI6bnVsbCwiT25CdWlsZCI6bnVsbCwiTGFiZWxzIjpudWxsfSwiYXJjaGl0ZWN0dXJlIjoiYW1kNjQiLCJvcyI6ImxpbnV4In0="
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dialect": "onbuild_omitempty",
|
||||||
|
"config": {
|
||||||
|
"architecture": "amd64",
|
||||||
|
"os": "linux",
|
||||||
|
"config": {
|
||||||
|
"User": "10001:10001"
|
||||||
|
},
|
||||||
|
"rootfs": {
|
||||||
|
"type": "layers",
|
||||||
|
"diff_ids": [
|
||||||
|
"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"blobs": [
|
||||||
|
{
|
||||||
|
"name": "blobs/sha256/466fa2f08fccd0fcd2e8debbdd88466c6a4e3e1d5d53df5f02abe0c96afe8cfb",
|
||||||
|
"payloadBase64": "eyJpZCI6ImIyYmMzM2I4MjJlYTYyYjI2ZGZlZjViZDUxYWU2Yjc0NDI5Y2JjYjY3OWEwMmE3ZDUwNzEzZDdhZTlmOTQxNTciLCJjcmVhdGVkIjpudWxsLCJjb250YWluZXJfY29uZmlnIjp7Ikhvc3RuYW1lIjoiIiwiRG9tYWlubmFtZSI6IiIsIlVzZXIiOiIiLCJBdHRhY2hTdGRpbiI6ZmFsc2UsIkF0dGFjaFN0ZG91dCI6ZmFsc2UsIkF0dGFjaFN0ZGVyciI6ZmFsc2UsIlR0eSI6ZmFsc2UsIk9wZW5TdGRpbiI6ZmFsc2UsIlN0ZGluT25jZSI6ZmFsc2UsIkVudiI6bnVsbCwiQ21kIjpudWxsLCJJbWFnZSI6IiIsIlZvbHVtZXMiOm51bGwsIldvcmtpbmdEaXIiOiIiLCJFbnRyeXBvaW50IjpudWxsLCJMYWJlbHMiOm51bGx9LCJjb25maWciOnsiSG9zdG5hbWUiOiIiLCJEb21haW5uYW1lIjoiIiwiVXNlciI6IjEwMDAxOjEwMDAxIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOm51bGwsIkNtZCI6bnVsbCwiSW1hZ2UiOiIiLCJWb2x1bWVzIjpudWxsLCJXb3JraW5nRGlyIjoiIiwiRW50cnlwb2ludCI6bnVsbCwiTGFiZWxzIjpudWxsfSwiYXJjaGl0ZWN0dXJlIjoiYW1kNjQiLCJvcyI6ImxpbnV4In0="
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
// Reference-only Moby saveConfig/CreateID oracle. The generator supplies the
|
||||||
|
// source-pinned Config/V1Image declarations; this never imports Hub code.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
type blob struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Payload string `json:"payloadBase64"`
|
||||||
|
}
|
||||||
|
type result struct {
|
||||||
|
Dialect string `json:"dialect"`
|
||||||
|
Config json.RawMessage `json:"config"`
|
||||||
|
Blobs []blob `json:"blobs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func sum(data []byte) string { return fmt.Sprintf("%x", sha256.Sum256(data)) }
|
||||||
|
func encoded(value any) []byte {
|
||||||
|
data, err := json.Marshal(value)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
func derive[T any](dialect string, raw json.RawMessage) result {
|
||||||
|
var image struct {
|
||||||
|
RootFS struct {
|
||||||
|
DiffIDs []string `json:"diff_ids"`
|
||||||
|
} `json:"rootfs"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &image); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// Image.Parent shadows V1Image.Parent in Moby; these golden inputs do not
|
||||||
|
// depend on an old builder's parent field.
|
||||||
|
var original T
|
||||||
|
if err := json.Unmarshal(raw, &original); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
chain, parent := "", ""
|
||||||
|
output := result{Dialect: dialect, Config: raw, Blobs: []blob{}}
|
||||||
|
for index, diff := range image.RootFS.DiffIDs {
|
||||||
|
if chain == "" {
|
||||||
|
chain = diff
|
||||||
|
} else {
|
||||||
|
chain = "sha256:" + sum([]byte(chain+" "+diff))
|
||||||
|
}
|
||||||
|
var legacy T
|
||||||
|
if index == len(image.RootFS.DiffIDs)-1 {
|
||||||
|
legacy = original
|
||||||
|
} else {
|
||||||
|
if err := json.Unmarshal([]byte(`{"created":"1970-01-01T00:00:00Z"}`), &legacy); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reflected := reflect.ValueOf(&legacy).Elem()
|
||||||
|
reflected.FieldByName("ID").SetString("")
|
||||||
|
var preimage map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(encoded(legacy), &preimage); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
preimage["layer_id"] = encoded(chain)
|
||||||
|
if parent != "" {
|
||||||
|
preimage["parent"] = encoded("sha256:" + parent)
|
||||||
|
}
|
||||||
|
identity := sum(encoded(preimage))
|
||||||
|
reflected.FieldByName("ID").SetString(identity)
|
||||||
|
if parent != "" {
|
||||||
|
reflected.FieldByName("Parent").SetString(parent)
|
||||||
|
}
|
||||||
|
reflected.FieldByName("OS").SetString("linux")
|
||||||
|
payload := encoded(legacy)
|
||||||
|
output.Blobs = append(output.Blobs, blob{
|
||||||
|
Name: "blobs/sha256/" + sum(payload), Payload: base64.StdEncoding.EncodeToString(payload),
|
||||||
|
})
|
||||||
|
parent = identity
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
func main() {
|
||||||
|
data, err := os.ReadFile(os.Args[1])
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
var inputs []json.RawMessage
|
||||||
|
if err := json.Unmarshal(data, &inputs); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
outputs := []result{}
|
||||||
|
for _, input := range inputs {
|
||||||
|
outputs = append(outputs, derive[OldV1]("onbuild_null", input))
|
||||||
|
outputs = append(outputs, derive[NewV1]("onbuild_omitempty", input))
|
||||||
|
}
|
||||||
|
fmt.Println(string(encoded(outputs)))
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
@@ -123,6 +124,8 @@ def _write_archive_fixture(path: Path, members: dict[str, bytes | None]) -> None
|
|||||||
|
|
||||||
def _docker29_oci_fixture(
|
def _docker29_oci_fixture(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
|
*,
|
||||||
|
layer_count: int = 1,
|
||||||
) -> tuple[Path, dict[str, bytes | None], dict[str, dict[str, str]], dict[str, str]]:
|
) -> tuple[Path, dict[str, bytes | None], dict[str, dict[str, str]], dict[str, str]]:
|
||||||
tmp_path.mkdir(parents=True, exist_ok=True)
|
tmp_path.mkdir(parents=True, exist_ok=True)
|
||||||
revision = "a" * 40
|
revision = "a" * 40
|
||||||
@@ -143,6 +146,19 @@ def _docker29_oci_fixture(
|
|||||||
layer_digest = "sha256:" + hashlib.sha256(layer).hexdigest()
|
layer_digest = "sha256:" + hashlib.sha256(layer).hexdigest()
|
||||||
layer_name = "blobs/sha256/" + layer_digest.split(":", 1)[1]
|
layer_name = "blobs/sha256/" + layer_digest.split(":", 1)[1]
|
||||||
members: dict[str, bytes | None] = {"blobs": None, "blobs/sha256": None, layer_name: layer}
|
members: dict[str, bytes | None] = {"blobs": None, "blobs/sha256": None, layer_name: layer}
|
||||||
|
layer_rows = [(layer_digest, layer_name, layer)]
|
||||||
|
if layer_count == 2:
|
||||||
|
stream = io.BytesIO()
|
||||||
|
with tarfile.open(fileobj=stream, mode="w:", format=tarfile.USTAR_FORMAT) as archive:
|
||||||
|
content = b"second layer verifies prefix chain and legacy parent binding"
|
||||||
|
info = tarfile.TarInfo("second.txt")
|
||||||
|
info.size = len(content)
|
||||||
|
archive.addfile(info, io.BytesIO(content))
|
||||||
|
payload = stream.getvalue()
|
||||||
|
digest = "sha256:" + hashlib.sha256(payload).hexdigest()
|
||||||
|
name = "blobs/sha256/" + digest.split(":", 1)[1]
|
||||||
|
members[name] = payload
|
||||||
|
layer_rows.append((digest, name, payload))
|
||||||
images: dict[str, dict[str, str]] = {}
|
images: dict[str, dict[str, str]] = {}
|
||||||
docker_manifest: list[dict[str, object]] = []
|
docker_manifest: list[dict[str, object]] = []
|
||||||
index_descriptors: list[dict[str, object]] = []
|
index_descriptors: list[dict[str, object]] = []
|
||||||
@@ -169,7 +185,7 @@ def _docker29_oci_fixture(
|
|||||||
"architecture": "amd64",
|
"architecture": "amd64",
|
||||||
"os": "linux",
|
"os": "linux",
|
||||||
"config": {"User": "10001:10001", "Labels": labels, "Cmd": [service]},
|
"config": {"User": "10001:10001", "Labels": labels, "Cmd": [service]},
|
||||||
"rootfs": {"type": "layers", "diff_ids": [layer_digest]},
|
"rootfs": {"type": "layers", "diff_ids": [row[0] for row in layer_rows]},
|
||||||
}
|
}
|
||||||
config_bytes = json.dumps(config, sort_keys=True, separators=(",", ":")).encode()
|
config_bytes = json.dumps(config, sort_keys=True, separators=(",", ":")).encode()
|
||||||
image_id = "sha256:" + hashlib.sha256(config_bytes).hexdigest()
|
image_id = "sha256:" + hashlib.sha256(config_bytes).hexdigest()
|
||||||
@@ -180,13 +196,14 @@ def _docker29_oci_fixture(
|
|||||||
{
|
{
|
||||||
"Config": config_name,
|
"Config": config_name,
|
||||||
"RepoTags": [reference],
|
"RepoTags": [reference],
|
||||||
"Layers": [layer_name],
|
"Layers": [row[1] for row in layer_rows],
|
||||||
"LayerSources": {
|
"LayerSources": {
|
||||||
layer_digest: {
|
digest: {
|
||||||
"mediaType": "application/vnd.oci.image.layer.v1.tar",
|
"mediaType": "application/vnd.oci.image.layer.v1.tar",
|
||||||
"size": len(layer),
|
"size": len(data),
|
||||||
"digest": layer_digest,
|
"digest": digest,
|
||||||
}
|
}
|
||||||
|
for digest, _name, data in layer_rows
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -201,9 +218,10 @@ def _docker29_oci_fixture(
|
|||||||
"layers": [
|
"layers": [
|
||||||
{
|
{
|
||||||
"mediaType": "application/vnd.oci.image.layer.v1.tar",
|
"mediaType": "application/vnd.oci.image.layer.v1.tar",
|
||||||
"digest": layer_digest,
|
"digest": digest,
|
||||||
"size": len(layer),
|
"size": len(data),
|
||||||
}
|
}
|
||||||
|
for digest, _name, data in layer_rows
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
image_manifest_bytes = json.dumps(
|
image_manifest_bytes = json.dumps(
|
||||||
@@ -466,6 +484,174 @@ def test_docker_oci_index_can_canonicalize_external_reference_aliases(tmp_path:
|
|||||||
assert normalized.is_file()
|
assert normalized.is_file()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"case",
|
||||||
|
json.loads((ROOT / "tests/fixtures/moby_v1_golden.json").read_text(encoding="utf-8"))["cases"],
|
||||||
|
)
|
||||||
|
def test_independent_go_reference_matches_exact_legacy_wire_bytes(case):
|
||||||
|
expected = {
|
||||||
|
row["name"]: base64.b64decode(row["payloadBase64"], validate=True) for row in case["blobs"]
|
||||||
|
}
|
||||||
|
actual = producer._moby_v1_metadata(
|
||||||
|
case["config"], legacy_onbuild=case["dialect"] == "onbuild_null"
|
||||||
|
)
|
||||||
|
assert actual == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_go_reference_fixture_is_bound_to_its_reproducible_generator():
|
||||||
|
fixture = json.loads((ROOT / "tests/fixtures/moby_v1_golden.json").read_text(encoding="utf-8"))
|
||||||
|
assert fixture["schema"] == "itworx.moby-v1-wire-golden/v1"
|
||||||
|
assert fixture["goVersion"] == "go1.27.2"
|
||||||
|
assert (
|
||||||
|
fixture["referenceMainSha256"]
|
||||||
|
== hashlib.sha256(
|
||||||
|
(ROOT / "tests/fixtures/moby_v1_reference/main.go").read_bytes()
|
||||||
|
).hexdigest()
|
||||||
|
)
|
||||||
|
assert fixture["sourceCommits"] == [
|
||||||
|
"89c5e8fd66634b6128fc4c0e6f1236e2540e46e0",
|
||||||
|
"5834c866f83c0bae2d0be28f38b92f32cd07ca38",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _moby_v1_fixture(members, *, legacy_onbuild=True):
|
||||||
|
# Faithful classic Moby saveConfig/CreateID serialization for the fixed
|
||||||
|
# single-layer images above, independent of the production normalizer.
|
||||||
|
zero = {
|
||||||
|
"Hostname": "",
|
||||||
|
"Domainname": "",
|
||||||
|
"User": "",
|
||||||
|
"AttachStdin": False,
|
||||||
|
"AttachStdout": False,
|
||||||
|
"AttachStderr": False,
|
||||||
|
"Tty": False,
|
||||||
|
"OpenStdin": False,
|
||||||
|
"StdinOnce": False,
|
||||||
|
"Env": None,
|
||||||
|
"Cmd": None,
|
||||||
|
"Image": "",
|
||||||
|
"Volumes": None,
|
||||||
|
"WorkingDir": "",
|
||||||
|
"Entrypoint": None,
|
||||||
|
"OnBuild": None,
|
||||||
|
"Labels": None,
|
||||||
|
}
|
||||||
|
if not legacy_onbuild:
|
||||||
|
del zero["OnBuild"]
|
||||||
|
result = dict(members)
|
||||||
|
added = []
|
||||||
|
for row in json.loads(members["manifest.json"]):
|
||||||
|
config = json.loads(members[row["Config"]])
|
||||||
|
runtime = {**zero, **config["config"]}
|
||||||
|
parent = ""
|
||||||
|
chain = ""
|
||||||
|
diffs = config["rootfs"]["diff_ids"]
|
||||||
|
for index, diff in enumerate(diffs):
|
||||||
|
chain = (
|
||||||
|
diff
|
||||||
|
if not chain
|
||||||
|
else "sha256:" + hashlib.sha256((chain + " " + diff).encode()).hexdigest()
|
||||||
|
)
|
||||||
|
if index == len(diffs) - 1:
|
||||||
|
body = {
|
||||||
|
"created": None,
|
||||||
|
"container_config": zero,
|
||||||
|
"config": runtime,
|
||||||
|
"architecture": "amd64",
|
||||||
|
"os": "linux",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
body = {"created": "1970-01-01T00:00:00Z", "container_config": zero}
|
||||||
|
preimage = {**body, "layer_id": chain}
|
||||||
|
if parent:
|
||||||
|
preimage["parent"] = "sha256:" + parent
|
||||||
|
preimage = dict(sorted(preimage.items()))
|
||||||
|
legacy_id = hashlib.sha256(
|
||||||
|
json.dumps(preimage, separators=(",", ":")).encode()
|
||||||
|
).hexdigest()
|
||||||
|
saved = {"id": legacy_id}
|
||||||
|
if parent:
|
||||||
|
saved["parent"] = parent
|
||||||
|
saved.update(body)
|
||||||
|
saved["os"] = "linux"
|
||||||
|
payload = json.dumps(saved, separators=(",", ":")).encode()
|
||||||
|
name = "blobs/sha256/" + hashlib.sha256(payload).hexdigest()
|
||||||
|
result[name] = payload
|
||||||
|
added.append(name)
|
||||||
|
parent = legacy_id
|
||||||
|
return result, added
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("legacy_onbuild", [True, False])
|
||||||
|
@pytest.mark.parametrize("layer_count", [1, 2])
|
||||||
|
def test_moby_v1_compatibility_metadata_preserves_exact_importer_bytes(
|
||||||
|
tmp_path, legacy_onbuild, layer_count
|
||||||
|
):
|
||||||
|
raw, members, images, metadata = _docker29_oci_fixture(tmp_path, layer_count=layer_count)
|
||||||
|
expected = tmp_path / "expected.tar"
|
||||||
|
producer._normalize_docker_archive(raw, expected, images, metadata=metadata)
|
||||||
|
extended, _added = _moby_v1_fixture(members, legacy_onbuild=legacy_onbuild)
|
||||||
|
raw_compat = tmp_path / "with-compat.tar"
|
||||||
|
_write_archive_fixture(raw_compat, extended)
|
||||||
|
actual = tmp_path / "actual.tar"
|
||||||
|
producer._normalize_docker_archive(raw_compat, actual, images, metadata=metadata)
|
||||||
|
assert actual.read_bytes() == expected.read_bytes()
|
||||||
|
|
||||||
|
|
||||||
|
def test_content_addressed_foreign_json_is_still_rejected(tmp_path):
|
||||||
|
_raw, members, images, metadata = _docker29_oci_fixture(tmp_path)
|
||||||
|
extended, _added = _moby_v1_fixture(members)
|
||||||
|
payload = b'{"id":"foreign","created":null,"os":"linux"}'
|
||||||
|
extended["blobs/sha256/" + hashlib.sha256(payload).hexdigest()] = payload
|
||||||
|
raw = tmp_path / "foreign.tar"
|
||||||
|
_write_archive_fixture(raw, extended)
|
||||||
|
output = tmp_path / "foreign-output.tar"
|
||||||
|
with pytest.raises(producer.BuildRejected):
|
||||||
|
producer._normalize_docker_archive(raw, output, images, metadata=metadata)
|
||||||
|
assert not output.exists()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"change", ["default", "unknown", "parent", "chain", "config", "order", "id"]
|
||||||
|
)
|
||||||
|
def test_self_consistent_foreign_v1_metadata_never_extends_archive_inventory(tmp_path, change):
|
||||||
|
_raw, members, images, metadata = _docker29_oci_fixture(tmp_path, layer_count=2)
|
||||||
|
extended, added = _moby_v1_fixture(members)
|
||||||
|
original_name = added[-1]
|
||||||
|
body = json.loads(extended.pop(original_name))
|
||||||
|
first = json.loads(members["manifest.json"])[0]
|
||||||
|
diffs = json.loads(members[first["Config"]])["rootfs"]["diff_ids"]
|
||||||
|
chain = "sha256:" + hashlib.sha256((diffs[0] + " " + diffs[1]).encode()).hexdigest()
|
||||||
|
if change == "default":
|
||||||
|
body["container_config"]["Tty"] = True
|
||||||
|
elif change == "unknown":
|
||||||
|
body["unexpected"] = "fixture-sensitive-foreign-field"
|
||||||
|
elif change == "parent":
|
||||||
|
body["parent"] = "e" * 64
|
||||||
|
elif change == "chain":
|
||||||
|
chain = "sha256:" + "e" * 64
|
||||||
|
elif change == "config":
|
||||||
|
body["config"]["User"] = "root"
|
||||||
|
elif change == "order":
|
||||||
|
body["container_config"] = dict(sorted(body["container_config"].items()))
|
||||||
|
preimage = {key: value for key, value in body.items() if key not in {"id", "parent"}}
|
||||||
|
preimage.update(layer_id=chain, parent="sha256:" + body["parent"])
|
||||||
|
body["id"] = hashlib.sha256(
|
||||||
|
json.dumps(dict(sorted(preimage.items())), separators=(",", ":")).encode()
|
||||||
|
).hexdigest()
|
||||||
|
if change == "id":
|
||||||
|
body["id"] = "e" * 64
|
||||||
|
payload = json.dumps(body, separators=(",", ":")).encode()
|
||||||
|
extended["blobs/sha256/" + hashlib.sha256(payload).hexdigest()] = payload
|
||||||
|
raw = tmp_path / "spoof.tar"
|
||||||
|
_write_archive_fixture(raw, extended)
|
||||||
|
output = tmp_path / "spoof-output.tar"
|
||||||
|
with pytest.raises(producer.BuildRejected) as error:
|
||||||
|
producer._normalize_docker_archive(raw, output, images, metadata=metadata)
|
||||||
|
assert "fixture-sensitive" not in str(error.value)
|
||||||
|
assert not output.exists()
|
||||||
|
|
||||||
|
|
||||||
def test_docker29_oci_normalizer_rejects_unreferenced_blobs_and_config_mismatch(
|
def test_docker29_oci_normalizer_rejects_unreferenced_blobs_and_config_mismatch(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
Reference in new issue
Block a user