147 lines
5.2 KiB
Python
147 lines
5.2 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Replay the reviewed Hermes Dockerfile heredocs for pinned Kaniko v1.23.2.
|
||
|
|
|
||
|
|
Kaniko v1.23.2 parses shell heredoc bodies into ``RunCommand.Files`` but its RUN
|
||
|
|
implementation executes only ``CmdLine``. The interpreters therefore receive
|
||
|
|
empty stdin and exit successfully. Docker and BuildKit execute these blocks
|
||
|
|
normally, so this compatibility runner is enabled only by the Kaniko pipeline.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import argparse
|
||
|
|
import re
|
||
|
|
import subprocess
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
|
||
|
|
MAX_DOCKERFILE_BYTES = 2_000_000
|
||
|
|
BLOCKS = (
|
||
|
|
("RUN node <<'NODE'", "NODE", ("node",)),
|
||
|
|
("RUN python - <<'PY'", "PY", ("python", "-")),
|
||
|
|
)
|
||
|
|
EXPECTED_COMMANDS = ("node", *("python",) * 7, "node")
|
||
|
|
PARSER_DIRECTIVE = re.compile(
|
||
|
|
r"^\s*#\s*([a-z]+)\s*=\s*(.*?)\s*$", re.IGNORECASE
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def _escape_character(lines: list[str]) -> str:
|
||
|
|
"""Return the one Dockerfile parser escape directive, or its default."""
|
||
|
|
escape = "\\"
|
||
|
|
directives: set[str] = set()
|
||
|
|
for line in lines:
|
||
|
|
if not line.strip():
|
||
|
|
break
|
||
|
|
match = PARSER_DIRECTIVE.fullmatch(line)
|
||
|
|
if not match:
|
||
|
|
break
|
||
|
|
name, value = match.group(1).lower(), match.group(2)
|
||
|
|
if name in directives:
|
||
|
|
raise ValueError(f"multiple Dockerfile {name} directives")
|
||
|
|
directives.add(name)
|
||
|
|
if name == "escape":
|
||
|
|
if value not in {"\\", "`"}:
|
||
|
|
raise ValueError("unsupported Dockerfile escape directive")
|
||
|
|
escape = value
|
||
|
|
return escape
|
||
|
|
|
||
|
|
|
||
|
|
def _continued(line: str, escape: str) -> bool:
|
||
|
|
"""Match Docker's unescaped continuation marker at physical line end."""
|
||
|
|
stripped = line.rstrip()
|
||
|
|
count = len(stripped) - len(stripped.rstrip(escape))
|
||
|
|
return count % 2 == 1
|
||
|
|
|
||
|
|
|
||
|
|
def _logical_instruction(
|
||
|
|
lines: list[str], index: int, escape: str
|
||
|
|
) -> tuple[str, int]:
|
||
|
|
"""Normalize escape-newline pairs, including split opcodes/operators."""
|
||
|
|
logical = lines[index]
|
||
|
|
while _continued(logical.split("\n")[-1], escape):
|
||
|
|
logical = logical.rstrip()
|
||
|
|
logical = logical[:-1]
|
||
|
|
index += 1
|
||
|
|
while index < len(lines) and lines[index].lstrip().startswith("#"):
|
||
|
|
index += 1
|
||
|
|
if index >= len(lines) or not lines[index].strip():
|
||
|
|
raise ValueError("unterminated Dockerfile line continuation")
|
||
|
|
logical += lines[index].lstrip()
|
||
|
|
return logical, index
|
||
|
|
|
||
|
|
|
||
|
|
def extract_blocks(source: str) -> list[tuple[tuple[str, ...], str]]:
|
||
|
|
"""Inventory every RUN heredoc, then return only the reviewed contract."""
|
||
|
|
markers = {start: (end, command) for start, end, command in BLOCKS}
|
||
|
|
lines = source.splitlines()
|
||
|
|
escape = _escape_character(lines)
|
||
|
|
blocks: list[tuple[tuple[str, ...], str]] = []
|
||
|
|
index = 0
|
||
|
|
while index < len(lines):
|
||
|
|
marker = markers.get(lines[index])
|
||
|
|
if marker is None:
|
||
|
|
if not lines[index].strip() or lines[index].lstrip().startswith("#"):
|
||
|
|
index += 1
|
||
|
|
continue
|
||
|
|
logical, index = _logical_instruction(lines, index, escape)
|
||
|
|
stripped = logical.lstrip()
|
||
|
|
if (
|
||
|
|
stripped[:3].upper() == "RUN"
|
||
|
|
and (len(stripped) == 3 or stripped[3].isspace())
|
||
|
|
and "<<" in stripped
|
||
|
|
):
|
||
|
|
raise ValueError(
|
||
|
|
"unsupported RUN heredoc outside the exact reviewed contract"
|
||
|
|
)
|
||
|
|
index += 1
|
||
|
|
continue
|
||
|
|
end, command = marker
|
||
|
|
body_start = index + 1
|
||
|
|
index = body_start
|
||
|
|
while index < len(lines) and lines[index] != end:
|
||
|
|
index += 1
|
||
|
|
if index == len(lines):
|
||
|
|
raise ValueError(f"unterminated {command[0]} heredoc")
|
||
|
|
body = "\n".join(lines[body_start:index]) + "\n"
|
||
|
|
if not body.strip():
|
||
|
|
raise ValueError(f"empty {command[0]} heredoc")
|
||
|
|
blocks.append((command, body))
|
||
|
|
index += 1
|
||
|
|
|
||
|
|
commands = tuple(command[0] for command, _body in blocks)
|
||
|
|
if commands != EXPECTED_COMMANDS:
|
||
|
|
raise ValueError(
|
||
|
|
"Hermes Dockerfile heredoc contract changed: "
|
||
|
|
f"expected {EXPECTED_COMMANDS!r}, received {commands!r}"
|
||
|
|
)
|
||
|
|
return blocks
|
||
|
|
|
||
|
|
|
||
|
|
def replay(dockerfile: Path, block_index: int) -> None:
|
||
|
|
"""Execute one exact heredoc after inventorying the complete Dockerfile."""
|
||
|
|
size = dockerfile.stat().st_size
|
||
|
|
if size < 1 or size > MAX_DOCKERFILE_BYTES:
|
||
|
|
raise ValueError("Hermes Dockerfile size is outside the reviewed boundary")
|
||
|
|
source = dockerfile.read_text(encoding="utf-8")
|
||
|
|
blocks = extract_blocks(source)
|
||
|
|
if block_index < 1 or block_index > len(blocks):
|
||
|
|
raise ValueError("heredoc block index is outside the reviewed contract")
|
||
|
|
command, body = blocks[block_index - 1]
|
||
|
|
print(f"replaying reviewed Dockerfile heredoc {block_index}/{len(blocks)}")
|
||
|
|
subprocess.run(command, input=body, text=True, check=True)
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> int:
|
||
|
|
"""Parse the one explicit Dockerfile path and replay its reviewed patches."""
|
||
|
|
parser = argparse.ArgumentParser()
|
||
|
|
parser.add_argument("--dockerfile", required=True, type=Path)
|
||
|
|
parser.add_argument("--block-index", required=True, type=int)
|
||
|
|
args = parser.parse_args()
|
||
|
|
replay(args.dockerfile, args.block_index)
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
raise SystemExit(main())
|