130 lines
6.9 KiB
Python
130 lines
6.9 KiB
Python
"""Checks shared by immutable artifact preparation and launch."""
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
from uuid import UUID
|
|
|
|
sys.dont_write_bytecode = True
|
|
|
|
from analyze import APP, RUNTIME, SETTINGS, digest, local_context, read_object, require
|
|
|
|
|
|
TOOLS = ("analyze.py", "bundle.py", "launch.py", "container.sh", "README.md")
|
|
TEST_DIRECTORY = f"tests/Server/{APP}"
|
|
CANONICAL_SOURCES = tuple(f"{TEST_DIRECTORY}/{name}" for name in (
|
|
"Sdk2LifecycleSoakTests.cs", "Sdk2LifecycleLateSlopeTests.cs", f"{APP}.csproj"))
|
|
SDK_PACKAGES = {
|
|
f"OPCFoundation.NetStandard.Opc.Ua.{component}/2.0.1"
|
|
for component in ("Client", "Configuration", "Core", "Core.Schema", "Core.Types",
|
|
"Security.Certificates", "Server", "Types")
|
|
}
|
|
GATEWAY_PACKAGES = {f"ZB.MOM.WW.HistorianGateway.{component}/1.0.0" for component in ("Client", "Contracts")}
|
|
|
|
|
|
def context_identity(name):
|
|
require(re.fullmatch(r"[A-Za-z0-9_.-]+", name), "Invalid local Docker context name")
|
|
require(not any(os.environ.get(key) for key in ("DOCKER_HOST", "DOCKER_TLS", "DOCKER_TLS_VERIFY", "DOCKER_CERT_PATH"))
|
|
and os.environ.get("DOCKER_CONTEXT", name) in ("", name), "Docker endpoint or TLS override is set")
|
|
result = subprocess.check_output(["docker", "context", "inspect", name], text=True, timeout=15)
|
|
contexts = json.loads(result)
|
|
require(len(contexts) == 1 and contexts[0]["Name"] == name, "Expected one explicit Docker context")
|
|
identity = {"name": name, "endpoint": contexts[0]["Endpoints"]["docker"]["Host"]}
|
|
require(local_context(identity) and Path(identity["endpoint"][len("unix://"):]).is_socket(),
|
|
"The reviewed Docker context must use a local Unix socket")
|
|
return identity
|
|
|
|
|
|
def image_identity(image, context):
|
|
require(re.fullmatch(r"sha256:[0-9a-f]{64}", image), "Use a locally cached immutable image ID")
|
|
result = subprocess.check_output(["docker", "--host", context["endpoint"], "image", "inspect", image], text=True, timeout=15)
|
|
images = json.loads(result)
|
|
require(len(images) == 1, "Expected one cached image")
|
|
value = images[0]
|
|
require((value["Id"], value["Os"], value["Architecture"]) == (image, "linux", "arm64"),
|
|
"The immutable image must be Linux ARM64")
|
|
return {"id": image, "os": "linux", "architecture": "arm64"}
|
|
|
|
|
|
def manifest(root):
|
|
"""Hash the exact regular-file tree and exclude only the receipt that contains it."""
|
|
result = {}
|
|
for path in sorted(root.rglob("*")):
|
|
kind = path.lstat().st_mode
|
|
require(stat.S_ISDIR(kind) or stat.S_ISREG(kind), f"Unsupported artifact type {path.relative_to(root)}")
|
|
if stat.S_ISREG(kind) and path != root / "receipt.json":
|
|
result[path.relative_to(root).as_posix()] = digest(path)
|
|
return result
|
|
|
|
|
|
def check_elf(path):
|
|
with path.open("rb") as stream:
|
|
header = stream.read(20)
|
|
require(header[:6] == b"\x7fELF\x02\x01" and header[18:20] == b"\xb7\x00",
|
|
f"{path.name} is not ELF64 AArch64")
|
|
|
|
|
|
def check_application(app):
|
|
check_elf(app / APP)
|
|
require(os.access(app / APP, os.X_OK), "The Linux apphost is not executable")
|
|
check_elf(app / "libcoreclr.so")
|
|
options = read_object(app / f"{APP}.runtimeconfig.json")["runtimeOptions"]
|
|
frameworks = options["includedFrameworks"]
|
|
require(options["tfm"] == "net10.0" and "framework" not in options and "frameworks" not in options
|
|
and isinstance(frameworks, list) and any(value["name"] == "Microsoft.NETCore.App" for value in frameworks)
|
|
and all(value["version"] == RUNTIME for value in frameworks), "Wrong self-contained application runtime")
|
|
dependencies = read_object(app / f"{APP}.deps.json")
|
|
require(dependencies["runtimeTarget"]["name"] == ".NETCoreApp,Version=v10.0/linux-arm64", "Wrong application RID")
|
|
libraries = dependencies["libraries"]
|
|
sdk = {key for key in libraries if key.startswith("OPCFoundation.")}
|
|
gateway = {key for key in libraries if key.startswith("ZB.MOM.WW.HistorianGateway.")}
|
|
require(sdk == SDK_PACKAGES and gateway == GATEWAY_PACKAGES, "Expected exact SDK 2.0.1 and HistorianGateway 1.0.0 graph")
|
|
return sorted(sdk), sorted(gateway)
|
|
|
|
|
|
def check_modules(modules, files):
|
|
require(isinstance(modules, list), "Missing managed module identities")
|
|
seen = set()
|
|
for module in modules:
|
|
name = module["file"]
|
|
require(name in files and name.startswith("app/") and name.endswith(".dll") and name not in seen,
|
|
"Unexpected or duplicate managed module file")
|
|
require(module["assembly"] and re.fullmatch(r"\d+(?:\.\d+){3}", module["version"])
|
|
and UUID(module["mvid"]).int != 0, "Invalid assembly version or MVID")
|
|
seen.add(name)
|
|
required = {f"app/{APP}.dll", "app/Opc.Ua.Server.dll", "app/System.Private.CoreLib.dll",
|
|
"app/ZB.MOM.WW.HistorianGateway.Client.dll", "app/ZB.MOM.WW.HistorianGateway.Contracts.dll"}
|
|
require(required <= seen, "Missing application, runtime or dependency module identity")
|
|
|
|
|
|
def verify_bundle(root, expected_hash):
|
|
path = root / "receipt.json"
|
|
require(path.is_file() and not path.is_symlink() and re.fullmatch(r"[0-9a-f]{64}", expected_hash)
|
|
and digest(path) == expected_hash, "Reviewed bundle receipt hash differs")
|
|
receipt = read_object(path)
|
|
require(receipt.get("kind") == "bundle" and all(type(receipt.get(key)) is type(value) and receipt[key] == value
|
|
for key, value in SETTINGS.items()), "Invalid fixed diagnostic settings")
|
|
require(re.fullmatch(r"[0-9a-f]{40}", receipt["sourceRevision"]), "Invalid committed source revision")
|
|
files = manifest(root)
|
|
require(receipt["files"] == files, "The immutable bundle file set or a file hash changed")
|
|
require({name for name in files if name.startswith("tools/")} == {f"tools/{name}" for name in TOOLS},
|
|
"Unexpected bundled tools")
|
|
sdk, gateway = check_application(root / "app")
|
|
require(receipt["sdkLibraries"] == sdk and receipt["gatewayLibraries"] == gateway,
|
|
"Dependency receipt differs from the published application")
|
|
check_modules(receipt["managedModules"], files)
|
|
require(receipt["imageIdentity"] == {"id": receipt["image"], "os": "linux", "architecture": "arm64"}
|
|
and re.fullmatch(r"sha256:[0-9a-f]{64}", receipt["image"]), "Invalid immutable image identity")
|
|
require(local_context(receipt["dockerContext"]), "Invalid reviewed local Docker context")
|
|
require(receipt["sourceFiles"] and all(re.fullmatch(r"[0-9a-f]{64}", value) for value in receipt["sourceFiles"].values()),
|
|
"Missing committed source hashes")
|
|
require(set(CANONICAL_SOURCES) <= receipt["sourceFiles"].keys(), "Missing canonical test or project hash")
|
|
for name in TOOLS:
|
|
source = "scripts/validation/opcua-lifecycle-memory/late-slope/" + name
|
|
require(receipt["sourceFiles"].get(source) == files[f"tools/{name}"], f"Bundled {name} differs from source")
|
|
return receipt
|