Files
getDomain/domain-api/deploy/multi-region/drive_release_hub.sh
2026-04-18 23:52:51 +08:00

477 lines
16 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "${SCRIPT_DIR}/lib_ops_center.sh"
ops_center_load_config "${SCRIPT_DIR}"
usage() {
cat <<'EOF'
usage:
bash domain-api/deploy/multi-region/drive_release_hub.sh <base_url> <command> [args...]
commands:
launchpad
latest-package
latest-release
preview-latest
preview-latest-smart <worker|control>
preview-release <release_id> <worker|control>
create-latest-release
list-releases [limit]
list-rollouts <release_id> [limit]
smart-latest <worker|control> [confirm]
smart-release <release_id> <worker|control> [confirm]
examples:
bash domain-api/deploy/multi-region/drive_release_hub.sh http://127.0.0.1:8100 launchpad
bash domain-api/deploy/multi-region/drive_release_hub.sh http://127.0.0.1:8100 latest-package
bash domain-api/deploy/multi-region/drive_release_hub.sh http://127.0.0.1:8100 preview-latest
bash domain-api/deploy/multi-region/drive_release_hub.sh http://127.0.0.1:8100 preview-latest-smart worker
bash domain-api/deploy/multi-region/drive_release_hub.sh http://127.0.0.1:8100 create-latest-release
bash domain-api/deploy/multi-region/drive_release_hub.sh http://127.0.0.1:8100 smart-latest worker
bash domain-api/deploy/multi-region/drive_release_hub.sh http://127.0.0.1:8100 preview-release 18 control
bash domain-api/deploy/multi-region/drive_release_hub.sh http://127.0.0.1:8100 smart-release 18 control confirm
EOF
}
BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
COMMAND="${2:-}"
ARG1="${3:-}"
ARG2="${4:-}"
ARG3="${5:-}"
REQUESTED_BY="${REQUESTED_BY:-$(ops_center_resolve_requested_by)}"
print_json_or_raw() {
local raw_text="${1:-}"
python3 - "${raw_text}" <<'PY'
import json
import sys
raw_text = str(sys.argv[1] or "")
try:
payload = json.loads(raw_text)
except Exception:
print(raw_text)
else:
print(json.dumps(payload, ensure_ascii=False, indent=2))
PY
}
request_with_status() {
local method="$1"
local url="$2"
local body="${3:-}"
if [[ "${method}" == "POST" ]]; then
curl -sS -X POST "${url}" \
-H 'Content-Type: application/json' \
-d "${body}" \
-w $'\nHTTP_STATUS=%{http_code}'
else
curl -sS "${url}" -w $'\nHTTP_STATUS=%{http_code}'
fi
}
if [[ -z "${COMMAND}" ]]; then
usage
exit 1
fi
METHOD="GET"
PATH_SUFFIX=""
REQUEST_JSON=""
confirm_to_bool() {
local raw_value="${1:-}"
case "$(printf '%s' "${raw_value}" | tr '[:upper:]' '[:lower:]')" in
1|true|yes|y|confirm|confirmed)
printf 'true'
;;
*)
printf 'false'
;;
esac
}
case "${COMMAND}" in
launchpad)
CHANNEL="${ARG1:-stable}"
PATH_SUFFIX="/api/v1/ops/releases/launchpad?channel=${CHANNEL}"
;;
latest-package)
PATH_SUFFIX="/api/v1/ops/releases/package-metadata/latest"
;;
latest-release)
PATH_SUFFIX="/api/v1/ops/releases/latest"
;;
preview-latest)
METHOD="POST"
PATH_SUFFIX="/api/v1/ops/releases/from-package/latest/preview"
REQUEST_JSON="$(python3 - <<'PY' "${REQUESTED_BY}"
import json
import sys
requested_by = str(sys.argv[1] or "").strip() or "cli/drive_release_hub"
payload = {
"created_by": f"{requested_by}/latest-package-preview",
}
print(json.dumps(payload, ensure_ascii=False))
PY
)"
;;
preview-latest-smart)
MODE="${ARG1:-}"
if [[ "${MODE}" != "worker" && "${MODE}" != "control" ]]; then
echo "mode must be worker or control" >&2
usage
exit 1
fi
METHOD="POST"
PATH_SUFFIX="/api/v1/ops/releases/from-package/latest/smart-rollout-preview"
REQUEST_JSON="$(python3 - <<'PY' "${MODE}" "${REQUESTED_BY}"
import json
import sys
mode = str(sys.argv[1] or "").strip()
requested_by = str(sys.argv[2] or "").strip() or "cli/drive_release_hub"
payload = {
"mode": mode,
"created_by": f"{requested_by}/latest-package-preview",
"rollout_created_by": f"{requested_by}/latest-package-preview/rollout",
}
print(json.dumps(payload, ensure_ascii=False))
PY
)"
;;
create-latest-release)
METHOD="POST"
PATH_SUFFIX="/api/v1/ops/releases/from-package/latest"
REQUEST_JSON="$(python3 - <<'PY' "${REQUESTED_BY}"
import json
import sys
requested_by = str(sys.argv[1] or "").strip() or "cli/drive_release_hub"
payload = {
"created_by": f"{requested_by}/latest-package",
}
print(json.dumps(payload, ensure_ascii=False))
PY
)"
;;
list-releases)
RELEASE_LIMIT="${ARG1:-10}"
PATH_SUFFIX="/api/v1/ops/releases?limit=${RELEASE_LIMIT}"
;;
list-rollouts)
RELEASE_ID="${ARG1:-}"
ROLLOUT_LIMIT="${ARG2:-10}"
if [[ -z "${RELEASE_ID}" ]]; then
echo "release_id is required for list-rollouts" >&2
usage
exit 1
fi
PATH_SUFFIX="/api/v1/ops/releases/${RELEASE_ID}/rollouts?limit=${ROLLOUT_LIMIT}"
;;
smart-latest)
MODE="${ARG1:-}"
if [[ "${MODE}" != "worker" && "${MODE}" != "control" ]]; then
echo "mode must be worker or control" >&2
usage
exit 1
fi
METHOD="POST"
PATH_SUFFIX="/api/v1/ops/releases/from-package/latest/smart-rollout"
CONFIRM_RISKY="$(confirm_to_bool "${ARG2:-}")"
REQUEST_JSON="$(python3 - <<'PY' "${MODE}" "${CONFIRM_RISKY}" "${REQUESTED_BY}"
import json
import sys
mode = str(sys.argv[1] or "").strip()
confirm_risky = str(sys.argv[2] or "").strip().lower() == "true"
requested_by = str(sys.argv[3] or "").strip() or "cli/drive_release_hub"
payload = {
"mode": mode,
"confirm_risky": confirm_risky,
"created_by": f"{requested_by}/latest-package",
"rollout_created_by": f"{requested_by}/latest-package/rollout",
}
print(json.dumps(payload, ensure_ascii=False))
PY
)"
;;
preview-release)
RELEASE_ID="${ARG1:-}"
MODE="${ARG2:-}"
if [[ -z "${RELEASE_ID}" ]]; then
echo "release_id is required for preview-release" >&2
usage
exit 1
fi
if [[ "${MODE}" != "worker" && "${MODE}" != "control" ]]; then
echo "mode must be worker or control" >&2
usage
exit 1
fi
METHOD="POST"
PATH_SUFFIX="/api/v1/ops/releases/${RELEASE_ID}/smart-rollout-preview"
REQUEST_JSON="$(python3 - <<'PY' "${MODE}" "${REQUESTED_BY}"
import json
import sys
mode = str(sys.argv[1] or "").strip()
requested_by = str(sys.argv[2] or "").strip() or "cli/drive_release_hub"
payload = {
"mode": mode,
"created_by": f"{requested_by}/existing-release-preview",
"rollout_created_by": f"{requested_by}/existing-release-preview/rollout",
}
print(json.dumps(payload, ensure_ascii=False))
PY
)"
;;
smart-release)
RELEASE_ID="${ARG1:-}"
MODE="${ARG2:-}"
if [[ -z "${RELEASE_ID}" ]]; then
echo "release_id is required for smart-release" >&2
usage
exit 1
fi
if [[ "${MODE}" != "worker" && "${MODE}" != "control" ]]; then
echo "mode must be worker or control" >&2
usage
exit 1
fi
METHOD="POST"
PATH_SUFFIX="/api/v1/ops/releases/${RELEASE_ID}/smart-rollout"
CONFIRM_RISKY="$(confirm_to_bool "${ARG3:-}")"
REQUEST_JSON="$(python3 - <<'PY' "${MODE}" "${CONFIRM_RISKY}" "${REQUESTED_BY}"
import json
import sys
mode = str(sys.argv[1] or "").strip()
confirm_risky = str(sys.argv[2] or "").strip().lower() == "true"
requested_by = str(sys.argv[3] or "").strip() or "cli/drive_release_hub"
payload = {
"mode": mode,
"confirm_risky": confirm_risky,
"created_by": f"{requested_by}/existing-release",
"rollout_created_by": f"{requested_by}/existing-release/rollout",
}
print(json.dumps(payload, ensure_ascii=False))
PY
)"
;;
*)
echo "unknown command: ${COMMAND}" >&2
usage
exit 1
;;
esac
echo "[1/3] request"
printf '%s\n' "method=${METHOD}"
printf '%s\n' "url=${BASE_URL}${PATH_SUFFIX}"
if [[ -n "${REQUEST_JSON}" ]]; then
print_json_or_raw "${REQUEST_JSON}"
else
echo "(no request body)"
fi
echo
echo
RAW_HTTP_RESPONSE="$(request_with_status "${METHOD}" "${BASE_URL}${PATH_SUFFIX}" "${REQUEST_JSON}")"
HTTP_STATUS="${RAW_HTTP_RESPONSE##*$'\n'HTTP_STATUS=}"
RESPONSE_JSON="${RAW_HTTP_RESPONSE%$'\n'HTTP_STATUS=*}"
echo "[2/3] raw response"
printf '%s\n' "http_status=${HTTP_STATUS}"
print_json_or_raw "${RESPONSE_JSON}"
echo
echo
echo "[3/3] condensed summary"
python3 - "${RESPONSE_JSON}" "${HTTP_STATUS}" <<'PY'
import json
import sys
raw_response = str(sys.argv[1] or "")
http_status = int(sys.argv[2] or 0)
try:
payload = json.loads(raw_response)
except Exception:
print(json.dumps({
"http_status": http_status,
"code": 1,
"message": "response is not valid json",
"raw_response": raw_response[:500],
}, ensure_ascii=False, indent=2))
sys.exit(0 if 200 <= http_status < 300 else 1)
data = payload.get("data") or {}
raw_code = payload.get("code", 1)
if raw_code in (0, "0", False):
normalized_code = 0
else:
normalized_code = int(raw_code or 1)
summary = {
"http_status": http_status,
"code": normalized_code,
"message": str(payload.get("message") or ""),
}
if isinstance(data, dict) and "package" in data:
package = dict(data.get("package") or {})
summary["package"] = {
"available": bool(package.get("available", False)),
"package_name": str(package.get("package_name") or ""),
"package_type": str(package.get("package_type") or ""),
"generated_at": str(package.get("generated_at") or ""),
"smoke_test_ok": bool(package.get("smoke_test_ok", False)),
"smoke_test_message": str(package.get("smoke_test_message") or ""),
}
if isinstance(data, dict) and "launchpad_status" in data:
launchpad_status = dict(data.get("launchpad_status") or {})
worker_preview = dict(data.get("worker_rollout_preview") or {})
worker_smart_rollout = dict(worker_preview.get("smart_rollout") or {})
control_preview = dict(data.get("control_rollout_preview") or {})
control_smart_rollout = dict(control_preview.get("smart_rollout") or {})
summary["launchpad"] = {
"channel": str(data.get("channel") or ""),
"status": str(launchpad_status.get("status") or ""),
"status_label": str(launchpad_status.get("status_label") or ""),
"summary": str(launchpad_status.get("summary") or ""),
"recommended_action_code": str(launchpad_status.get("recommended_action_code") or ""),
"recommended_target_node_code": str(launchpad_status.get("recommended_target_node_code") or ""),
"recommended_recovery_label": str(launchpad_status.get("recommended_recovery_label") or ""),
"recommended_recovery_summary": str(launchpad_status.get("recommended_recovery_summary") or ""),
"onboarding_bootstrap_pending_nodes": int(launchpad_status.get("onboarding_bootstrap_pending_nodes", 0) or 0),
"onboarding_acceptance_ready_nodes": int(launchpad_status.get("onboarding_acceptance_ready_nodes", 0) or 0),
"recommended_command": str(launchpad_status.get("recommended_command") or ""),
"recommended_execution_mode": str(launchpad_status.get("recommended_execution_mode") or ""),
"recommended_execution_mode_label": str(launchpad_status.get("recommended_execution_mode_label") or ""),
"focus_ref": dict(launchpad_status.get("focus_ref") or {}),
"latest_package": dict(data.get("latest_package") or {}),
"latest_release": {
"id": int(((data.get("latest_release") or {}).get("id") or 0)),
"release_version": str((data.get("latest_release") or {}).get("release_version") or ""),
"status": str((data.get("latest_release") or {}).get("status") or ""),
},
"worker_rollout_preview": {
"message": str(worker_preview.get("message") or ""),
"available": bool(worker_smart_rollout.get("available", False)),
"requires_confirmation": bool(worker_preview.get("requires_confirmation", False)),
"blocked": bool(worker_preview.get("blocked", False)),
"execution_mode": str(worker_smart_rollout.get("execution_mode") or ""),
"execution_mode_label": str(worker_smart_rollout.get("execution_mode_label") or ""),
"target_node_codes": list(worker_smart_rollout.get("target_node_codes") or []),
},
"control_rollout_preview": {
"message": str(control_preview.get("message") or ""),
"available": bool(control_smart_rollout.get("available", False)),
"requires_confirmation": bool(control_preview.get("requires_confirmation", False)),
"blocked": bool(control_preview.get("blocked", False)),
"execution_mode": str(control_smart_rollout.get("execution_mode") or ""),
"execution_mode_label": str(control_smart_rollout.get("execution_mode_label") or ""),
"target_node_codes": list(control_smart_rollout.get("target_node_codes") or []),
},
}
print(json.dumps(summary, ensure_ascii=False, indent=2))
sys.exit(0)
if isinstance(data, dict) and "available" in data:
summary["latest_package"] = {
"available": bool(data.get("available", False)),
"package_name": str(data.get("package_name") or ""),
"package_type": str(data.get("package_type") or ""),
"generated_at": str(data.get("generated_at") or ""),
"commit_sha": str(data.get("commit_sha") or ""),
"smoke_test_ok": bool(data.get("smoke_test_ok", False)),
"smoke_test_message": str(data.get("smoke_test_message") or ""),
"reason": str(data.get("reason") or ""),
}
elif isinstance(data, dict) and "releases" in data:
releases = list(data.get("releases") or [])
summary["releases"] = {
"count": len(releases),
"items": [
{
"id": int(item.get("id") or 0),
"release_version": str(item.get("release_version") or ""),
"channel": str(item.get("channel") or ""),
"status": str(item.get("status") or ""),
}
for item in releases[:5]
],
}
elif isinstance(data, dict) and "rollouts" in data:
rollouts = list(data.get("rollouts") or [])
summary["rollouts"] = {
"count": len(rollouts),
"items": [
{
"id": int(item.get("id") or 0),
"rollout_code": str(item.get("rollout_code") or ""),
"status": str(item.get("status") or ""),
"jobs_total": int(item.get("jobs_total") or 0),
"jobs_created": int(item.get("jobs_created") or 0),
}
for item in rollouts[:5]
],
}
else:
release = data.get("release") or data
rollout = data.get("rollout") or {}
smart_rollout = data.get("smart_rollout") or {}
preview = data.get("preview") or {}
summary["preview_source"] = str(data.get("preview_source") or "")
summary["release"] = {
"id": int(release.get("id") or 0),
"release_version": str(release.get("release_version") or ""),
"channel": str(release.get("channel") or ""),
"status": str(release.get("status") or ""),
"is_preview": bool(release.get("is_preview", False)),
}
if rollout:
summary["rollout"] = {
"id": int(rollout.get("id") or 0),
"rollout_code": str(rollout.get("rollout_code") or ""),
"status": str(rollout.get("status") or ""),
}
if smart_rollout:
summary["smart_rollout"] = {
"available": bool(smart_rollout.get("available", False)),
"mode": str(smart_rollout.get("mode") or ""),
"mode_label": str(smart_rollout.get("mode_label") or ""),
"target_node_codes": list((smart_rollout.get("target_selector") or {}).get("node_codes") or []),
"summary": str(smart_rollout.get("summary") or smart_rollout.get("reason") or ""),
}
if preview:
summary["preview"] = {
"blocked": bool(preview.get("blocked", False)),
"approval_required": bool(preview.get("approval_required", False)),
"warnings": list(preview.get("warnings") or []),
}
summary["result_flags"] = {
"release_created": bool(data.get("release_created", False)),
"release_deduplicated": bool(data.get("release_deduplicated", False)),
"rollout_created": bool(data.get("rollout_created", False)),
"requires_confirmation": bool(data.get("requires_confirmation", False)),
"blocked": bool(data.get("blocked", False)),
}
print(json.dumps(summary, ensure_ascii=False, indent=2))
PY
if [[ "${HTTP_STATUS}" -lt 200 || "${HTTP_STATUS}" -ge 300 ]]; then
exit 1
fi