platform: modularize api/gui, add docs-tests-web foundation, and refresh root config
This commit is contained in:
687
scripts/transport-packaging/update.sh
Executable file
687
scripts/transport-packaging/update.sh
Executable file
@@ -0,0 +1,687 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DEFAULT_MANIFEST="${SCRIPT_DIR}/manifest.example.json"
|
||||
DEFAULT_SOURCE_POLICY="${SCRIPT_DIR}/source_policy.production.json"
|
||||
|
||||
MANIFEST="${DEFAULT_MANIFEST}"
|
||||
BIN_ROOT="/opt/selective-vpn/bin"
|
||||
TARGET=""
|
||||
COMPONENTS_RAW=""
|
||||
SOURCE_POLICY=""
|
||||
SIGNATURE_MODE_OVERRIDE=""
|
||||
ROLLOUT_STAGE="stable"
|
||||
COHORT_ID=""
|
||||
FORCE_ROLLOUT=0
|
||||
DRY_RUN=0
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
update.sh [--manifest PATH] [--bin-root DIR] [--target OS-ARCH] [--component NAME[,NAME...]]
|
||||
[--source-policy PATH] [--signature-mode off|optional|required]
|
||||
[--rollout-stage stable|canary|any] [--cohort-id 0..99] [--force-rollout]
|
||||
[--canary] [--dry-run]
|
||||
|
||||
Description:
|
||||
Manual pinned updater for transport companion binaries in runtime_mode=exec.
|
||||
Reads versions/urls/checksums from manifest and atomically switches active symlinks in BIN_ROOT.
|
||||
|
||||
Examples:
|
||||
./scripts/transport-packaging/update.sh --manifest ./manifest.json --component singbox
|
||||
./scripts/transport-packaging/update.sh --manifest ./manifest.json --target linux-amd64 --dry-run
|
||||
./scripts/transport-packaging/update.sh --manifest ./manifest.production.json --source-policy ./source_policy.production.json
|
||||
./scripts/transport-packaging/update.sh --manifest /path/to/manifest-with-canary.json --rollout-stage canary
|
||||
EOF
|
||||
}
|
||||
|
||||
normalize_os() {
|
||||
local raw="$1"
|
||||
raw="$(echo "$raw" | tr '[:upper:]' '[:lower:]')"
|
||||
case "$raw" in
|
||||
linux) echo "linux" ;;
|
||||
darwin) echo "darwin" ;;
|
||||
*) echo "$raw" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
normalize_arch() {
|
||||
local raw="$1"
|
||||
raw="$(echo "$raw" | tr '[:upper:]' '[:lower:]')"
|
||||
case "$raw" in
|
||||
x86_64|amd64) echo "amd64" ;;
|
||||
aarch64|arm64) echo "arm64" ;;
|
||||
armv7l|armv7) echo "armv7" ;;
|
||||
*) echo "$raw" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
local cmd="$1"
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
echo "[transport-update] missing required command: ${cmd}" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
history_append_if_changed() {
|
||||
local history_file="$1"
|
||||
local line="$2"
|
||||
local target="$3"
|
||||
local last_target=""
|
||||
if [[ -f "$history_file" ]]; then
|
||||
last_target="$(tail -n1 "$history_file" | awk -F'|' '{print $4}')"
|
||||
fi
|
||||
if [[ "$last_target" == "$target" ]]; then
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "$(dirname "$history_file")"
|
||||
echo "$line" >> "$history_file"
|
||||
}
|
||||
|
||||
trim_spaces() {
|
||||
echo "$1" | xargs
|
||||
}
|
||||
|
||||
normalize_signature_mode() {
|
||||
local raw
|
||||
raw="$(echo "$1" | tr '[:upper:]' '[:lower:]' | xargs)"
|
||||
case "$raw" in
|
||||
""|off|optional|required) echo "$raw" ;;
|
||||
*)
|
||||
echo "[transport-update] invalid signature mode: ${raw}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
compute_cohort_id() {
|
||||
local component="$1"
|
||||
local target="$2"
|
||||
local seed=""
|
||||
if [[ -r /etc/machine-id ]]; then
|
||||
seed="$(tr -d '\n' </etc/machine-id)"
|
||||
fi
|
||||
if [[ -z "$seed" ]]; then
|
||||
seed="$(hostname 2>/dev/null || uname -n || echo "unknown-host")"
|
||||
fi
|
||||
local input="${seed}:${component}:${target}"
|
||||
local hash
|
||||
hash="$(printf '%s' "$input" | sha256sum | awk '{print $1}')"
|
||||
printf '%d' $((16#${hash:0:8} % 100))
|
||||
}
|
||||
|
||||
evaluate_policy_and_signature_mode() {
|
||||
local component="$1"
|
||||
local url="$2"
|
||||
local sig_url="$3"
|
||||
local sig_type="$4"
|
||||
python3 - "$SOURCE_POLICY" "$component" "$url" "$sig_url" "$sig_type" "$SIGNATURE_MODE_OVERRIDE" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
from urllib.parse import urlparse
|
||||
|
||||
policy_path, component, asset_url, sig_url, sig_type_raw, mode_override_raw = sys.argv[1:]
|
||||
component = component.strip()
|
||||
asset_url = asset_url.strip()
|
||||
sig_url = sig_url.strip()
|
||||
sig_type = sig_type_raw.strip().lower()
|
||||
mode_override = mode_override_raw.strip().lower()
|
||||
|
||||
def as_bool(value, default=False):
|
||||
if value is None:
|
||||
return default
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if isinstance(value, (int, float)):
|
||||
return value != 0
|
||||
if isinstance(value, str):
|
||||
raw = value.strip().lower()
|
||||
if raw in {"1", "true", "yes", "on"}:
|
||||
return True
|
||||
if raw in {"0", "false", "no", "off"}:
|
||||
return False
|
||||
return default
|
||||
|
||||
def as_list(value):
|
||||
if value is None:
|
||||
return []
|
||||
if isinstance(value, list):
|
||||
return [str(v).strip() for v in value if str(v).strip()]
|
||||
if isinstance(value, str):
|
||||
raw = value.strip()
|
||||
return [raw] if raw else []
|
||||
raise SystemExit("policy list value has unsupported type")
|
||||
|
||||
def fail(msg):
|
||||
raise SystemExit(msg)
|
||||
|
||||
def normalize_mode(raw):
|
||||
raw = (raw or "").strip().lower()
|
||||
if raw == "":
|
||||
return ""
|
||||
if raw not in {"off", "optional", "required"}:
|
||||
fail(f"signature mode must be off|optional|required (got {raw})")
|
||||
return raw
|
||||
|
||||
policy = {}
|
||||
if not policy_path:
|
||||
mode = normalize_mode(mode_override) or "off"
|
||||
print(mode)
|
||||
raise SystemExit(0)
|
||||
|
||||
if policy_path:
|
||||
with open(policy_path, "r", encoding="utf-8") as f:
|
||||
policy = json.load(f)
|
||||
if not isinstance(policy, dict):
|
||||
fail("source policy must be a JSON object")
|
||||
|
||||
components_cfg = policy.get("components", {})
|
||||
if components_cfg is None:
|
||||
components_cfg = {}
|
||||
if not isinstance(components_cfg, dict):
|
||||
fail("policy.components must be an object")
|
||||
component_cfg = components_cfg.get(component, {})
|
||||
if component_cfg is None:
|
||||
component_cfg = {}
|
||||
if not isinstance(component_cfg, dict):
|
||||
fail(f"policy.components.{component} must be an object")
|
||||
|
||||
require_https = as_bool(policy.get("require_https", True), default=True)
|
||||
allow_file_scheme = as_bool(policy.get("allow_file_scheme", False), default=False)
|
||||
allowed_schemes = [s.lower() for s in as_list(policy.get("allowed_schemes"))]
|
||||
|
||||
default_hosts = set(as_list(policy.get("default_allowed_hosts")))
|
||||
default_prefixes = as_list(policy.get("default_allowed_url_prefixes"))
|
||||
component_hosts = set(as_list(component_cfg.get("allowed_hosts")))
|
||||
component_prefixes = as_list(component_cfg.get("allowed_url_prefixes"))
|
||||
|
||||
hosts = component_hosts if component_hosts else default_hosts
|
||||
prefixes = component_prefixes if component_prefixes else default_prefixes
|
||||
|
||||
def validate_url(kind, raw_url):
|
||||
raw_url = raw_url.strip()
|
||||
if not raw_url:
|
||||
return
|
||||
parsed = urlparse(raw_url)
|
||||
scheme = (parsed.scheme or "").lower()
|
||||
if not scheme:
|
||||
fail(f"{kind} URL is missing scheme: {raw_url}")
|
||||
if scheme == "file":
|
||||
if not allow_file_scheme:
|
||||
fail(f"{kind} URL uses file:// but policy allow_file_scheme=false: {raw_url}")
|
||||
return
|
||||
if allowed_schemes and scheme not in allowed_schemes:
|
||||
fail(f"{kind} URL scheme {scheme} is not in policy.allowed_schemes")
|
||||
if require_https and scheme != "https":
|
||||
fail(f"{kind} URL must use https:// by policy: {raw_url}")
|
||||
host = (parsed.hostname or "").lower()
|
||||
if hosts and host not in {h.lower() for h in hosts}:
|
||||
fail(f"{kind} URL host {host} is not trusted for component {component}")
|
||||
if prefixes and not any(raw_url.startswith(prefix) for prefix in prefixes):
|
||||
fail(f"{kind} URL is not in trusted prefixes for component {component}")
|
||||
|
||||
validate_url("asset", asset_url)
|
||||
if sig_url:
|
||||
validate_url("signature", sig_url)
|
||||
|
||||
sig_cfg = policy.get("signature", {})
|
||||
if sig_cfg is None:
|
||||
sig_cfg = {}
|
||||
if not isinstance(sig_cfg, dict):
|
||||
fail("policy.signature must be an object")
|
||||
|
||||
allowed_sig_types = [s.lower() for s in as_list(sig_cfg.get("allowed_types"))]
|
||||
if sig_type and allowed_sig_types and sig_type not in allowed_sig_types:
|
||||
fail(f"signature type {sig_type} is not allowed by policy")
|
||||
|
||||
mode = normalize_mode(mode_override)
|
||||
if not mode:
|
||||
mode = normalize_mode(component_cfg.get("signature_mode", ""))
|
||||
if not mode:
|
||||
mode = normalize_mode(sig_cfg.get("default_mode", "off")) or "off"
|
||||
print(mode)
|
||||
PY
|
||||
}
|
||||
|
||||
manifest_rows() {
|
||||
python3 - "$MANIFEST" "$TARGET" "$COMPONENTS_RAW" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
manifest_path, target_key, components_raw = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
with open(manifest_path, "r", encoding="utf-8") as f:
|
||||
doc = json.load(f)
|
||||
|
||||
components = doc.get("components")
|
||||
if not isinstance(components, dict):
|
||||
raise SystemExit("manifest.components must be an object")
|
||||
|
||||
wanted = set()
|
||||
if components_raw.strip():
|
||||
for part in components_raw.split(","):
|
||||
part = part.strip()
|
||||
if part:
|
||||
wanted.add(part)
|
||||
|
||||
def fail(msg: str):
|
||||
raise SystemExit(msg)
|
||||
|
||||
seen = set()
|
||||
rows = []
|
||||
for name, meta in sorted(components.items()):
|
||||
if wanted and name not in wanted:
|
||||
continue
|
||||
if not isinstance(meta, dict):
|
||||
fail(f"component {name} config must be an object")
|
||||
enabled = bool(meta.get("enabled", True))
|
||||
if not enabled:
|
||||
continue
|
||||
binary_name = str(meta.get("binary_name", "")).strip()
|
||||
if not binary_name:
|
||||
fail(f"component {name} missing binary_name")
|
||||
targets = meta.get("targets")
|
||||
if not isinstance(targets, dict):
|
||||
fail(f"component {name} missing targets map")
|
||||
rec = targets.get(target_key)
|
||||
if rec is None:
|
||||
fail(f"component {name} has no target {target_key}")
|
||||
if not isinstance(rec, dict):
|
||||
fail(f"component {name} target {target_key} must be object")
|
||||
version = str(rec.get("version", "")).strip()
|
||||
url = str(rec.get("url", "")).strip()
|
||||
sha256 = str(rec.get("sha256", "")).strip().lower()
|
||||
asset_type = str(rec.get("asset_type", "raw")).strip().lower()
|
||||
asset_binary_path = str(rec.get("asset_binary_path", "")).strip()
|
||||
rollout = rec.get("rollout")
|
||||
if rollout is None:
|
||||
rollout = {}
|
||||
if not isinstance(rollout, dict):
|
||||
fail(f"component {name} target {target_key} rollout must be object")
|
||||
rollout_stage = str(rollout.get("stage", "stable")).strip().lower()
|
||||
rollout_percent_raw = rollout.get("percent", 100)
|
||||
try:
|
||||
rollout_percent = int(rollout_percent_raw)
|
||||
except Exception:
|
||||
fail(f"component {name} target {target_key} rollout.percent must be int")
|
||||
sig = rec.get("signature")
|
||||
if sig is None:
|
||||
sig = {}
|
||||
if not isinstance(sig, dict):
|
||||
fail(f"component {name} target {target_key} signature must be object")
|
||||
sig_type = str(sig.get("type", "")).strip().lower()
|
||||
sig_url = str(sig.get("url", "")).strip()
|
||||
sig_sha256 = str(sig.get("sha256", "")).strip().lower()
|
||||
sig_public_key_path = str(sig.get("public_key_path", "")).strip()
|
||||
|
||||
if not version:
|
||||
fail(f"component {name} target {target_key} missing version")
|
||||
if not url:
|
||||
fail(f"component {name} target {target_key} missing url")
|
||||
if len(sha256) != 64 or any(c not in "0123456789abcdef" for c in sha256):
|
||||
fail(f"component {name} target {target_key} has invalid sha256")
|
||||
if asset_type not in ("raw", "tar.gz", "zip"):
|
||||
fail(f"component {name} target {target_key} has unsupported asset_type={asset_type}")
|
||||
if asset_type in ("tar.gz", "zip") and not asset_binary_path:
|
||||
fail(f"component {name} target {target_key} requires asset_binary_path for {asset_type}")
|
||||
if rollout_stage not in ("stable", "canary"):
|
||||
fail(f"component {name} target {target_key} rollout.stage must be stable|canary")
|
||||
if rollout_percent < 0 or rollout_percent > 100:
|
||||
fail(f"component {name} target {target_key} rollout.percent must be 0..100")
|
||||
if sig_type and not sig_url:
|
||||
fail(f"component {name} target {target_key} signature.url is required when signature.type is set")
|
||||
if sig_sha256:
|
||||
if len(sig_sha256) != 64 or any(c not in "0123456789abcdef" for c in sig_sha256):
|
||||
fail(f"component {name} target {target_key} signature.sha256 is invalid")
|
||||
if sig_public_key_path and not sig_type:
|
||||
fail(f"component {name} target {target_key} signature.type is required when signature.public_key_path is set")
|
||||
|
||||
rows.append((
|
||||
name, binary_name, version, url, sha256, asset_type, asset_binary_path,
|
||||
rollout_stage, str(rollout_percent), sig_type, sig_url, sig_sha256, sig_public_key_path
|
||||
))
|
||||
seen.add(name)
|
||||
|
||||
if wanted:
|
||||
missing = sorted(wanted - seen)
|
||||
if missing:
|
||||
fail("missing/enabled=false components in manifest: " + ",".join(missing))
|
||||
|
||||
for row in rows:
|
||||
print("\x1f".join(row))
|
||||
PY
|
||||
}
|
||||
|
||||
verify_asset_signature() {
|
||||
local component="$1"
|
||||
local asset_path="$2"
|
||||
local tmp_dir="$3"
|
||||
local sig_mode="$4"
|
||||
local sig_type="$5"
|
||||
local sig_url="$6"
|
||||
local sig_sha256="$7"
|
||||
local sig_public_key_path="$8"
|
||||
|
||||
if [[ "$sig_mode" == "off" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -z "$sig_type" || -z "$sig_url" || -z "$sig_public_key_path" ]]; then
|
||||
if [[ "$sig_mode" == "required" ]]; then
|
||||
echo "[transport-update] ${component}: signature is required, but signature fields are incomplete" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "[transport-update] WARN ${component}: signature_mode=${sig_mode}, signature metadata is incomplete, skip signature check"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ ! -f "$sig_public_key_path" ]]; then
|
||||
if [[ "$sig_mode" == "required" ]]; then
|
||||
echo "[transport-update] ${component}: signature public key not found: ${sig_public_key_path}" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "[transport-update] WARN ${component}: signature public key not found: ${sig_public_key_path}, skip signature check"
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$sig_type" in
|
||||
openssl-sha256)
|
||||
require_cmd openssl
|
||||
;;
|
||||
*)
|
||||
if [[ "$sig_mode" == "required" ]]; then
|
||||
echo "[transport-update] ${component}: unsupported signature type: ${sig_type}" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "[transport-update] WARN ${component}: unsupported signature type: ${sig_type}, skip signature check"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
local sig_file="${tmp_dir}/asset.sig"
|
||||
echo "[transport-update] ${component}: downloading signature ${sig_url}"
|
||||
curl -fsSL "$sig_url" -o "$sig_file"
|
||||
|
||||
if [[ -n "$sig_sha256" ]]; then
|
||||
echo "${sig_sha256} ${sig_file}" | sha256sum -c - >/dev/null
|
||||
echo "[transport-update] ${component}: signature checksum ok"
|
||||
fi
|
||||
|
||||
if ! openssl dgst -sha256 -verify "$sig_public_key_path" -signature "$sig_file" "$asset_path" >/dev/null 2>&1; then
|
||||
echo "[transport-update] ${component}: signature verification failed (${sig_type})" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "[transport-update] ${component}: signature verified (${sig_type})"
|
||||
}
|
||||
|
||||
download_install_binary() {
|
||||
local component="$1"
|
||||
local binary_name="$2"
|
||||
local version="$3"
|
||||
local url="$4"
|
||||
local sha256="$5"
|
||||
local asset_type="$6"
|
||||
local asset_binary_path="$7"
|
||||
local sig_mode="$8"
|
||||
local sig_type="$9"
|
||||
local sig_url="${10}"
|
||||
local sig_sha256="${11}"
|
||||
local sig_public_key_path="${12}"
|
||||
local release_dir="${13}"
|
||||
local release_binary="${14}"
|
||||
|
||||
if [[ -x "$release_binary" ]]; then
|
||||
echo "[transport-update] ${component}: release already present ${release_binary}"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo "[transport-update] DRY-RUN ${component}: download ${url} -> ${release_binary}"
|
||||
if [[ "$sig_mode" != "off" ]]; then
|
||||
echo "[transport-update] DRY-RUN ${component}: signature_mode=${sig_mode} signature_type=${sig_type:-none}"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
local tmp_dir
|
||||
tmp_dir="$(mktemp -d)"
|
||||
local asset="${tmp_dir}/asset"
|
||||
local unpack="${tmp_dir}/unpack"
|
||||
mkdir -p "$release_dir" "$unpack"
|
||||
|
||||
echo "[transport-update] ${component}: downloading ${url}"
|
||||
curl -fsSL "$url" -o "$asset"
|
||||
|
||||
echo "${sha256} ${asset}" | sha256sum -c - >/dev/null
|
||||
echo "[transport-update] ${component}: checksum ok"
|
||||
verify_asset_signature "$component" "$asset" "$tmp_dir" "$sig_mode" "$sig_type" "$sig_url" "$sig_sha256" "$sig_public_key_path"
|
||||
|
||||
local source_binary=""
|
||||
case "$asset_type" in
|
||||
raw)
|
||||
source_binary="$asset"
|
||||
;;
|
||||
tar.gz)
|
||||
require_cmd tar
|
||||
tar -xzf "$asset" -C "$unpack"
|
||||
source_binary="${unpack}/${asset_binary_path}"
|
||||
;;
|
||||
zip)
|
||||
require_cmd unzip
|
||||
unzip -q "$asset" -d "$unpack"
|
||||
source_binary="${unpack}/${asset_binary_path}"
|
||||
;;
|
||||
*)
|
||||
rm -rf "$tmp_dir"
|
||||
echo "[transport-update] ${component}: unsupported asset_type ${asset_type}" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ ! -f "$source_binary" ]]; then
|
||||
rm -rf "$tmp_dir"
|
||||
echo "[transport-update] ${component}: binary not found in asset: ${source_binary}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
install -m 0755 "$source_binary" "$release_binary"
|
||||
rm -rf "$tmp_dir"
|
||||
echo "[transport-update] ${component}: installed ${release_binary}"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--manifest)
|
||||
MANIFEST="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--bin-root)
|
||||
BIN_ROOT="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--target)
|
||||
TARGET="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--component)
|
||||
COMPONENTS_RAW="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--source-policy)
|
||||
SOURCE_POLICY="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--signature-mode)
|
||||
SIGNATURE_MODE_OVERRIDE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--rollout-stage)
|
||||
ROLLOUT_STAGE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--canary)
|
||||
ROLLOUT_STAGE="canary"
|
||||
shift
|
||||
;;
|
||||
--cohort-id)
|
||||
COHORT_ID="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--force-rollout)
|
||||
FORCE_ROLLOUT=1
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "[transport-update] unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
require_cmd curl
|
||||
require_cmd sha256sum
|
||||
require_cmd python3
|
||||
require_cmd install
|
||||
require_cmd mktemp
|
||||
require_cmd readlink
|
||||
require_cmd ln
|
||||
require_cmd awk
|
||||
require_cmd tail
|
||||
|
||||
if [[ ! -f "$MANIFEST" ]]; then
|
||||
echo "[transport-update] manifest not found: ${MANIFEST}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$SOURCE_POLICY" && "$(basename "$MANIFEST")" == "manifest.production.json" && -f "$DEFAULT_SOURCE_POLICY" ]]; then
|
||||
SOURCE_POLICY="$DEFAULT_SOURCE_POLICY"
|
||||
fi
|
||||
if [[ -n "$SOURCE_POLICY" && ! -f "$SOURCE_POLICY" ]]; then
|
||||
echo "[transport-update] source policy not found: ${SOURCE_POLICY}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SIGNATURE_MODE_OVERRIDE="$(normalize_signature_mode "$SIGNATURE_MODE_OVERRIDE")"
|
||||
|
||||
ROLLOUT_STAGE="$(echo "$ROLLOUT_STAGE" | tr '[:upper:]' '[:lower:]' | xargs)"
|
||||
case "$ROLLOUT_STAGE" in
|
||||
stable|canary|any) ;;
|
||||
*)
|
||||
echo "[transport-update] rollout stage must be stable|canary|any" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
if [[ -n "$COHORT_ID" ]]; then
|
||||
if [[ ! "$COHORT_ID" =~ ^[0-9]+$ ]]; then
|
||||
echo "[transport-update] cohort id must be integer 0..99" >&2
|
||||
exit 1
|
||||
fi
|
||||
if (( COHORT_ID < 0 || COHORT_ID > 99 )); then
|
||||
echo "[transport-update] cohort id must be in range 0..99" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$TARGET" ]]; then
|
||||
TARGET="$(normalize_os "$(uname -s)")-$(normalize_arch "$(uname -m)")"
|
||||
fi
|
||||
|
||||
echo "[transport-update] manifest=${MANIFEST}"
|
||||
echo "[transport-update] bin_root=${BIN_ROOT}"
|
||||
echo "[transport-update] target=${TARGET}"
|
||||
if [[ -n "$COMPONENTS_RAW" ]]; then
|
||||
echo "[transport-update] components=${COMPONENTS_RAW}"
|
||||
fi
|
||||
if [[ -n "$SOURCE_POLICY" ]]; then
|
||||
echo "[transport-update] source_policy=${SOURCE_POLICY}"
|
||||
fi
|
||||
echo "[transport-update] rollout_stage=${ROLLOUT_STAGE}"
|
||||
if [[ -n "$COHORT_ID" ]]; then
|
||||
echo "[transport-update] cohort_id=${COHORT_ID}"
|
||||
fi
|
||||
if [[ "$FORCE_ROLLOUT" -eq 1 ]]; then
|
||||
echo "[transport-update] force_rollout=true"
|
||||
fi
|
||||
if [[ -n "$SIGNATURE_MODE_OVERRIDE" ]]; then
|
||||
echo "[transport-update] signature_mode_override=${SIGNATURE_MODE_OVERRIDE}"
|
||||
fi
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo "[transport-update] mode=dry-run"
|
||||
fi
|
||||
|
||||
mapfile -t ROWS < <(manifest_rows)
|
||||
if [[ "${#ROWS[@]}" -eq 0 ]]; then
|
||||
echo "[transport-update] no enabled components selected" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$BIN_ROOT" "$BIN_ROOT/releases" "$BIN_ROOT/.packaging"
|
||||
|
||||
for row in "${ROWS[@]}"; do
|
||||
IFS=$'\x1f' read -r component binary_name version url sha256 asset_type asset_binary_path rollout_stage rollout_percent sig_type sig_url sig_sha256 sig_public_key_path <<< "$row"
|
||||
release_dir="${BIN_ROOT}/releases/${component}/${version}"
|
||||
release_binary="${release_dir}/${binary_name}"
|
||||
active_link="${BIN_ROOT}/${binary_name}"
|
||||
history_file="${BIN_ROOT}/.packaging/${component}.history"
|
||||
|
||||
sig_mode="$(evaluate_policy_and_signature_mode "$component" "$url" "$sig_url" "$sig_type")"
|
||||
sig_mode="$(trim_spaces "$sig_mode")"
|
||||
sig_mode="$(normalize_signature_mode "$sig_mode")"
|
||||
if [[ "$sig_mode" == "required" && ( -z "$sig_type" || -z "$sig_url" || -z "$sig_public_key_path" ) ]]; then
|
||||
echo "[transport-update] ${component}: signature_mode=required but signature metadata is incomplete" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$ROLLOUT_STAGE" != "any" && "$rollout_stage" != "$ROLLOUT_STAGE" ]]; then
|
||||
echo "[transport-update] ${component}: skip rollout stage=${rollout_stage} (requested ${ROLLOUT_STAGE})"
|
||||
continue
|
||||
fi
|
||||
effective_cohort="$COHORT_ID"
|
||||
if [[ -z "$effective_cohort" ]]; then
|
||||
effective_cohort="$(compute_cohort_id "$component" "$TARGET")"
|
||||
fi
|
||||
if [[ "$FORCE_ROLLOUT" -ne 1 && "$rollout_percent" -lt 100 && "$effective_cohort" -ge "$rollout_percent" ]]; then
|
||||
echo "[transport-update] ${component}: skip rollout percent=${rollout_percent}% cohort=${effective_cohort}"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "[transport-update] ${component}: version=${version} binary=${binary_name}"
|
||||
download_install_binary \
|
||||
"$component" "$binary_name" "$version" "$url" "$sha256" "$asset_type" "$asset_binary_path" \
|
||||
"$sig_mode" "$sig_type" "$sig_url" "$sig_sha256" "$sig_public_key_path" \
|
||||
"$release_dir" "$release_binary"
|
||||
|
||||
prev_target=""
|
||||
if [[ -L "$active_link" || -e "$active_link" ]]; then
|
||||
prev_target="$(readlink -f "$active_link" || true)"
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo "[transport-update] DRY-RUN ${component}: switch ${active_link} -> ${release_binary}"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ ! -x "$release_binary" ]]; then
|
||||
echo "[transport-update] ${component}: installed binary is not executable: ${release_binary}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$prev_target" && "$prev_target" != "$release_binary" && ! -f "$history_file" ]]; then
|
||||
history_append_if_changed "$history_file" "$(date -u +%Y-%m-%dT%H:%M:%SZ)|${binary_name}|preexisting|${prev_target}" "$prev_target"
|
||||
fi
|
||||
|
||||
ln -sfn "$release_binary" "$active_link"
|
||||
history_append_if_changed "$history_file" "$(date -u +%Y-%m-%dT%H:%M:%SZ)|${binary_name}|${version}|${release_binary}" "$release_binary"
|
||||
echo "[transport-update] ${component}: active -> ${release_binary}"
|
||||
done
|
||||
|
||||
echo "[transport-update] done"
|
||||
Reference in New Issue
Block a user