mirror of
https://github.com/rustfs/rustfs.git
synced 2026-11-11 20:54:09 +00:00
31e1e42ad7
Lands gateway/integration on main: the guard rails and the domain decoupling for replacing s3s with rustfs/gateway (rustfs/backlog#2734, Phase 0 and Phase 1). The S3 edge is still s3s and no wire behaviour change is intended. - Guard rails: lower-only s3s footprint baseline and edge allow-list, per-crate test-count floor, 39 byte-level goldens for the persisted bucket-config XML families, a two-server differential harness over all 73 S3 operations, a JUnit differ, and a reference-parity lane against the sha256-pinned 1.0.5 binary. - Owned types: rustfs-s3-types (S3Error, S3ErrorCode, Body), RequestEnvelope, AuthzFacts. - Engine crates off s3s: ecstore, lifecycle, replication, scanner, protocols, s3select, s3-client, rpc. - Admin: error types moved; 300 of 314 Operation impls take the owned AdminRequest. - Server: the s3s-compat tower layers grouped under server/legacy_compat; RUSTFS_S3_STACK=legacy|gateway selector, where gateway refuses to start until the gateway edge exists. Fixes made on the branch that change ecstore and heal behaviour are listed in the pull request description. The per-commit history stays on gateway/integration.
145 lines
5.8 KiB
Python
Executable File
145 lines
5.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# Copyright 2026 RustFS Team
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
"""Resolve and unpack the pinned reference RustFS release binary.
|
|
|
|
`resolve` reads the pin file and prints the selected platform's identity as
|
|
`key=value` lines (GitHub step-output format). `unpack` re-verifies the
|
|
downloaded archive digest against the pin and extracts exactly one executable.
|
|
Every missing or malformed input is a failure; nothing is skipped.
|
|
"""
|
|
|
|
import argparse
|
|
import hashlib
|
|
from pathlib import Path
|
|
import re
|
|
import sys
|
|
import tomllib
|
|
import zipfile
|
|
|
|
REPOSITORY = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.-]*/[A-Za-z0-9][A-Za-z0-9_.-]*")
|
|
TAG = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.+-]*")
|
|
PLATFORM = re.compile(r"[a-z0-9][a-z0-9_-]*")
|
|
ASSET = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.+-]*\.zip")
|
|
SHA256 = re.compile(r"[0-9a-f]{64}")
|
|
EXECUTABLE = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.-]*")
|
|
MAX_EXECUTABLE_BYTES = 1024 * 1024 * 1024
|
|
|
|
|
|
class PinError(Exception):
|
|
"""A pin, archive or argument that cannot be trusted."""
|
|
|
|
|
|
def field(table, key, pattern, where):
|
|
value = table.get(key)
|
|
if not isinstance(value, str) or not pattern.fullmatch(value):
|
|
raise PinError(f"{where}: missing or invalid '{key}'")
|
|
return value
|
|
|
|
|
|
def resolve(pin_path, platform):
|
|
if not PLATFORM.fullmatch(platform or ""):
|
|
raise PinError("invalid platform name")
|
|
try:
|
|
data = tomllib.loads(Path(pin_path).read_text(encoding="utf-8"))
|
|
except FileNotFoundError as exc:
|
|
raise PinError(f"{pin_path}: reference pin file is missing") from exc
|
|
except (OSError, UnicodeDecodeError, tomllib.TOMLDecodeError) as exc:
|
|
raise PinError(f"{pin_path}: cannot read reference pin: {exc}") from exc
|
|
if data.get("schema") != 1 or type(data.get("schema")) is not int:
|
|
raise PinError(f"{pin_path}: unsupported pin schema")
|
|
identity = {
|
|
"repository": field(data, "repository", REPOSITORY, pin_path),
|
|
"version": field(data, "version", TAG, pin_path),
|
|
"tag": field(data, "tag", TAG, pin_path),
|
|
}
|
|
platforms = data.get("platforms")
|
|
if not isinstance(platforms, dict) or not platforms:
|
|
raise PinError(f"{pin_path}: no pinned platforms")
|
|
entry = platforms.get(platform)
|
|
if not isinstance(entry, dict):
|
|
raise PinError(f"{pin_path}: no pin for platform '{platform}'")
|
|
where = f"{pin_path} [platforms.{platform}]"
|
|
identity["asset"] = field(entry, "asset", ASSET, where)
|
|
identity["sha256"] = field(entry, "sha256", SHA256, where)
|
|
identity["executable"] = field(entry, "executable", EXECUTABLE, where)
|
|
if identity["version"] not in identity["asset"]:
|
|
raise PinError(f"{where}: asset name does not carry version {identity['version']}")
|
|
unknown = set(entry) - {"asset", "sha256", "executable"}
|
|
if unknown:
|
|
raise PinError(f"{where}: unknown keys {sorted(unknown)}")
|
|
return identity
|
|
|
|
|
|
def file_sha256(path):
|
|
with open(path, "rb") as stream:
|
|
return hashlib.file_digest(stream, "sha256").hexdigest()
|
|
|
|
|
|
def unpack(archive, sha256, executable, destination):
|
|
if not SHA256.fullmatch(sha256 or "") or not EXECUTABLE.fullmatch(executable or ""):
|
|
raise PinError("invalid expected digest or executable name")
|
|
try:
|
|
actual = file_sha256(archive)
|
|
except OSError as exc:
|
|
raise PinError(f"{archive}: reference archive is missing or unreadable") from exc
|
|
if actual != sha256:
|
|
raise PinError(f"{archive}: sha256 {actual} does not match the pinned digest")
|
|
try:
|
|
with zipfile.ZipFile(archive) as bundle:
|
|
matches = [info for info in bundle.infolist() if info.filename == executable]
|
|
if len(matches) != 1:
|
|
raise PinError(f"{archive}: must contain exactly one top-level '{executable}'")
|
|
info = matches[0]
|
|
if info.is_dir() or not 0 < info.file_size <= MAX_EXECUTABLE_BYTES:
|
|
raise PinError(f"{archive}: '{executable}' is not a plausible executable")
|
|
destination = Path(destination)
|
|
destination.mkdir(parents=True, exist_ok=True)
|
|
target = destination / executable
|
|
with bundle.open(info) as source, open(target, "wb") as sink:
|
|
while chunk := source.read(1024 * 1024):
|
|
sink.write(chunk)
|
|
except zipfile.BadZipFile as exc:
|
|
raise PinError(f"{archive}: not a valid zip archive") from exc
|
|
target.chmod(0o755)
|
|
return target
|
|
|
|
|
|
def main(argv=None):
|
|
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
sub = parser.add_subparsers(dest="mode", required=True)
|
|
res = sub.add_parser("resolve")
|
|
res.add_argument("--pin", required=True)
|
|
res.add_argument("--platform", required=True)
|
|
unp = sub.add_parser("unpack")
|
|
unp.add_argument("--archive", required=True)
|
|
unp.add_argument("--sha256", required=True)
|
|
unp.add_argument("--executable", required=True)
|
|
unp.add_argument("--destination", required=True)
|
|
args = parser.parse_args(argv)
|
|
try:
|
|
if args.mode == "resolve":
|
|
for key, value in resolve(args.pin, args.platform).items():
|
|
print(f"{key}={value}")
|
|
else:
|
|
print(f"path={unpack(args.archive, args.sha256, args.executable, args.destination)}")
|
|
except PinError as exc:
|
|
print(f"reference binary: {exc}", file=sys.stderr)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|