688 lines
22 KiB
Bash
Executable File
688 lines
22 KiB
Bash
Executable File
#!/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"
|