477 lines
16 KiB
Bash
Executable File
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
|