feat: add ops center and node onboarding flow
This commit is contained in:
476
domain-api/deploy/multi-region/drive_release_hub.sh
Executable file
476
domain-api/deploy/multi-region/drive_release_hub.sh
Executable file
@@ -0,0 +1,476 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user