full_api_e2e.sh 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. # Full API e2e for k8s-orchestrator based on README interfaces.
  4. # Tested against real kubeadm cluster and real worker hosts.
  5. ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
  6. API_HOST="${E2E_API_HOST:-127.0.0.1}"
  7. API_PORT="${E2E_API_PORT:-18080}"
  8. API="http://${API_HOST}:${API_PORT}"
  9. SECRET_NAMESPACE="${E2E_SECRET_NAMESPACE:-orchestrator-system}"
  10. SSH_USER="${E2E_SSH_USER:-root}"
  11. SSH_PASSWORD="${E2E_SSH_PASSWORD:-Wenshu_test}"
  12. SPARK_OPERATOR_NAMESPACE="${E2E_SPARK_OPERATOR_NAMESPACE:-spark-operator}"
  13. SPARK_OPERATOR_RELEASE="${E2E_SPARK_OPERATOR_RELEASE:-spark-operator}"
  14. SPARK_OPERATOR_CHART="${E2E_SPARK_OPERATOR_CHART:-spark-operator/spark-operator}"
  15. STARROCKS_OPERATOR_NAMESPACE="${E2E_STARROCKS_OPERATOR_NAMESPACE:-starrocks}"
  16. STARROCKS_OPERATOR_DEPLOYMENT="${E2E_STARROCKS_OPERATOR_DEPLOYMENT:-kube-starrocks-operator}"
  17. STARROCKS_CRD_URL="${E2E_STARROCKS_CRD_URL:-https://raw.githubusercontent.com/StarRocks/starrocks-kubernetes-operator/main/deploy/starrocks.com_starrocksclusters.yaml}"
  18. STARROCKS_OPERATOR_URL="${E2E_STARROCKS_OPERATOR_URL:-https://raw.githubusercontent.com/StarRocks/starrocks-kubernetes-operator/main/deploy/operator.yaml}"
  19. # Comma-separated nodeId=ip
  20. WORKER_MAP="${E2E_WORKER_MAP:-worker-1=47.237.255.159,worker-2=47.237.255.162,worker-3=47.237.254.197,worker-4=47.84.84.212}"
  21. # true: reset workers to no kubeadm/kubelet/containerd before AddNode
  22. RESET_WORKERS="${E2E_RESET_WORKERS:-false}"
  23. # true: build/start local api/controller automatically
  24. START_LOCAL="${E2E_START_LOCAL:-true}"
  25. # default inventory capacity used for AddNode request
  26. NODE_CPU="${E2E_NODE_CPU:-4}"
  27. NODE_RAM="${E2E_NODE_RAM:-7}"
  28. NODE_SSD="${E2E_NODE_SSD:-100}"
  29. SPARK_OP_MAX="${E2E_SPARK_OP_MAX:-420}"
  30. STARROCKS_OP_MAX="${E2E_STARROCKS_OP_MAX:-420}"
  31. DEFAULT_OP_MAX="${E2E_DEFAULT_OP_MAX:-360}"
  32. LOG_DIR="${E2E_LOG_DIR:-/tmp}"
  33. API_BIN="${E2E_API_BIN:-/tmp/orchestrator-api}"
  34. CONTROLLER_BIN="${E2E_CONTROLLER_BIN:-/tmp/orchestrator-controller}"
  35. API_LOG="${LOG_DIR}/orchestrator-api.log"
  36. CONTROLLER_LOG="${LOG_DIR}/orchestrator-controller.log"
  37. API_PID_FILE="${LOG_DIR}/orchestrator-api.pid"
  38. CONTROLLER_PID_FILE="${LOG_DIR}/orchestrator-controller.pid"
  39. JOIN_CMD_FILE="${LOG_DIR}/kubeadm_join_cmd"
  40. SCRIPT_TAG="$(date +%s)"
  41. SPARK_CLUSTER_ID="spark-e2e-${SCRIPT_TAG}"
  42. SPARK_NS="spark-e2e-${SCRIPT_TAG}"
  43. SR_CLUSTER_ID="sr-e2e-${SCRIPT_TAG}"
  44. SR_NS="starrocks-sr-e2e-${SCRIPT_TAG}"
  45. IFS=',' read -r -a WORKERS <<<"${WORKER_MAP}"
  46. need_cmd() {
  47. command -v "$1" >/dev/null 2>&1 || {
  48. echo "missing required command: $1" >&2
  49. exit 1
  50. }
  51. }
  52. log() {
  53. echo "[$(date '+%F %T')] $*"
  54. }
  55. cleanup_local_processes() {
  56. if [[ -f "${API_PID_FILE}" ]]; then
  57. kill "$(cat "${API_PID_FILE}")" >/dev/null 2>&1 || true
  58. fi
  59. if [[ -f "${CONTROLLER_PID_FILE}" ]]; then
  60. kill "$(cat "${CONTROLLER_PID_FILE}")" >/dev/null 2>&1 || true
  61. fi
  62. }
  63. on_exit() {
  64. if [[ "${START_LOCAL}" == "true" ]]; then
  65. cleanup_local_processes
  66. fi
  67. }
  68. trap on_exit EXIT
  69. wait_api_ready() {
  70. for _ in $(seq 1 60); do
  71. if curl -sf "${API}/healthz" >/dev/null; then
  72. return 0
  73. fi
  74. sleep 1
  75. done
  76. echo "API not ready: ${API}" >&2
  77. tail -n 200 "${API_LOG}" || true
  78. exit 1
  79. }
  80. ensure_k8s_prereq() {
  81. log "ensure namespace/crd/rbac"
  82. kubectl get ns "${SECRET_NAMESPACE}" >/dev/null 2>&1 || kubectl create ns "${SECRET_NAMESPACE}"
  83. kubectl apply -k "${ROOT_DIR}/config/crd" >/dev/null
  84. kubectl apply -k "${ROOT_DIR}/config/rbac" >/dev/null
  85. log "refresh kubeadm-join secret"
  86. local join_cmd
  87. join_cmd="$(kubeadm token create --print-join-command)"
  88. kubectl -n "${SECRET_NAMESPACE}" create secret generic kubeadm-join \
  89. --from-literal=joinCommand="${join_cmd}" \
  90. --dry-run=client -o yaml | kubectl apply -f - >/dev/null
  91. echo "${join_cmd}" >"${JOIN_CMD_FILE}"
  92. }
  93. ensure_external_operators() {
  94. log "ensure Spark operator"
  95. helm repo add spark-operator https://kubeflow.github.io/spark-operator >/dev/null 2>&1 || true
  96. helm repo update spark-operator >/dev/null
  97. local spark_operator_job_namespaces_json
  98. spark_operator_job_namespaces_json="${E2E_SPARK_OPERATOR_JOB_NAMESPACES_JSON:-[\"\"]}"
  99. local spark_operator_ivy_cache_path
  100. spark_operator_ivy_cache_path="${E2E_SPARK_OPERATOR_IVY_CACHE_PATH:-/var/lib/wenshu/spark-ivy}"
  101. helm upgrade --install "${SPARK_OPERATOR_RELEASE}" "${SPARK_OPERATOR_CHART}" \
  102. --namespace "${SPARK_OPERATOR_NAMESPACE}" \
  103. --create-namespace \
  104. --set-json "spark.jobNamespaces=${spark_operator_job_namespaces_json}" \
  105. --set-json "controller.volumes=[{\"name\":\"tmp\",\"emptyDir\":{\"sizeLimit\":\"1Gi\"}},{\"name\":\"wenshu-spark-ivy-cache\",\"emptyDir\":{}}]" \
  106. --set-json "controller.volumeMounts=[{\"name\":\"tmp\",\"mountPath\":\"/tmp\",\"readOnly\":false},{\"name\":\"wenshu-spark-ivy-cache\",\"mountPath\":\"${spark_operator_ivy_cache_path}\",\"readOnly\":false}]" >/dev/null
  107. kubectl -n "${SPARK_OPERATOR_NAMESPACE}" rollout status deploy/spark-operator-controller --timeout=300s >/dev/null
  108. if kubectl -n "${SPARK_OPERATOR_NAMESPACE}" get deploy spark-operator-webhook >/dev/null 2>&1; then
  109. kubectl -n "${SPARK_OPERATOR_NAMESPACE}" rollout status deploy/spark-operator-webhook --timeout=300s >/dev/null
  110. fi
  111. log "ensure StarRocks operator"
  112. kubectl get ns "${STARROCKS_OPERATOR_NAMESPACE}" >/dev/null 2>&1 || kubectl create ns "${STARROCKS_OPERATOR_NAMESPACE}" >/dev/null
  113. if ! kubectl get crd starrocksclusters.starrocks.com >/dev/null 2>&1; then
  114. kubectl apply -f "${STARROCKS_CRD_URL}" >/dev/null
  115. fi
  116. if ! kubectl -n "${STARROCKS_OPERATOR_NAMESPACE}" get deploy "${STARROCKS_OPERATOR_DEPLOYMENT}" >/dev/null 2>&1; then
  117. kubectl apply -f "${STARROCKS_OPERATOR_URL}" >/dev/null
  118. fi
  119. kubectl -n "${STARROCKS_OPERATOR_NAMESPACE}" rollout status deploy/"${STARROCKS_OPERATOR_DEPLOYMENT}" --timeout=300s >/dev/null
  120. }
  121. start_local_binaries() {
  122. log "build local api/controller"
  123. (cd "${ROOT_DIR}" && go build -o "${API_BIN}" ./cmd/orchestrator-api)
  124. (cd "${ROOT_DIR}" && go build -o "${CONTROLLER_BIN}" ./cmd/orchestrator-controller)
  125. cleanup_local_processes
  126. log "start local api/controller"
  127. setsid env API_BIND_ADDRESS="${API_HOST}:${API_PORT}" SECRET_NAMESPACE="${SECRET_NAMESPACE}" \
  128. "${API_BIN}" >"${API_LOG}" 2>&1 < /dev/null &
  129. echo $! >"${API_PID_FILE}"
  130. setsid env KUBEADM_JOIN_COMMAND="$(cat "${JOIN_CMD_FILE}")" \
  131. "${CONTROLLER_BIN}" --leader-elect=false >"${CONTROLLER_LOG}" 2>&1 < /dev/null &
  132. echo $! >"${CONTROLLER_PID_FILE}"
  133. wait_api_ready
  134. }
  135. cleanup_orchestrator_resources() {
  136. log "cleanup old orchestrator resources"
  137. kubectl delete clusteroperations.orchestrator.spark.cyrus.io --all --ignore-not-found >/dev/null 2>&1 || true
  138. kubectl delete sparkvirtualclusters.orchestrator.spark.cyrus.io --all --ignore-not-found >/dev/null 2>&1 || true
  139. kubectl delete starrocksvirtualclusters.orchestrator.spark.cyrus.io --all --ignore-not-found >/dev/null 2>&1 || true
  140. kubectl delete nodeinventories.orchestrator.spark.cyrus.io --all --ignore-not-found >/dev/null 2>&1 || true
  141. }
  142. clear_node_isolation() {
  143. for pair in "${WORKERS[@]}"; do
  144. local node_id
  145. node_id="${pair%%=*}"
  146. kubectl taint nodes "${node_id}" spark-cluster- --overwrite >/dev/null 2>&1 || true
  147. kubectl taint nodes "${node_id}" starrocks-cluster- --overwrite >/dev/null 2>&1 || true
  148. kubectl label nodes "${node_id}" spark.cluster.id- starrocks.cluster.id- orchestrator.spark.cyrus.io/managed-by- --overwrite >/dev/null 2>&1 || true
  149. done
  150. }
  151. reset_workers_if_needed() {
  152. if [[ "${RESET_WORKERS}" != "true" ]]; then
  153. return 0
  154. fi
  155. log "reset workers to no kubeadm/kubelet/containerd"
  156. for pair in "${WORKERS[@]}"; do
  157. local node_id ip
  158. node_id="${pair%%=*}"
  159. ip="${pair##*=}"
  160. kubectl delete node "${node_id}" --ignore-not-found >/dev/null 2>&1 || true
  161. sshpass -p "${SSH_PASSWORD}" ssh -o StrictHostKeyChecking=no "${SSH_USER}@${ip}" 'bash -s' <<'EOS'
  162. set -euo pipefail
  163. apt-mark unhold kubeadm kubelet kubectl || true
  164. apt-get purge -y kubeadm kubelet kubectl kubernetes-cni containerd conntrack socat apt-transport-https || true
  165. apt-get autoremove -y || true
  166. kubeadm reset -f || true
  167. systemctl stop kubelet || true
  168. # kubelet may leave projected volume mounts; lazy-unmount before cleanup.
  169. findmnt -R /var/lib/kubelet -o TARGET -n 2>/dev/null | sort -r | xargs -r umount -l || true
  170. rm -rf /etc/kubernetes /var/lib/kubelet /var/lib/cni /etc/cni/net.d /etc/containerd || true
  171. rm -f /etc/apt/sources.list.d/kubernetes.list /etc/apt/keyrings/kubernetes-apt-keyring.gpg
  172. EOS
  173. sshpass -p "${SSH_PASSWORD}" ssh -o StrictHostKeyChecking=no "${SSH_USER}@${ip}" '
  174. command -v kubeadm >/dev/null && { echo "kubeadm should be missing"; exit 1; } || true
  175. command -v kubelet >/dev/null && { echo "kubelet should be missing"; exit 1; } || true
  176. command -v containerd >/dev/null && { echo "containerd should be missing"; exit 1; } || true
  177. '
  178. done
  179. }
  180. wait_op() {
  181. local op_id=$1
  182. local max=${2:-${DEFAULT_OP_MAX}}
  183. for i in $(seq 1 "${max}"); do
  184. local opjson phase message
  185. opjson="$(curl -sS "${API}/v1/operations/${op_id}")"
  186. phase="$(echo "${opjson}" | jq -r '.status.phase // empty')"
  187. message="$(echo "${opjson}" | jq -r '.status.message // empty')"
  188. log "op=${op_id} step=${i}/${max} phase=${phase} msg=${message}"
  189. if [[ "${phase}" == "SUCCEEDED" ]]; then
  190. return 0
  191. fi
  192. if [[ "${phase}" == "FAILED" ]]; then
  193. echo "${opjson}" >&2
  194. return 1
  195. fi
  196. sleep 5
  197. done
  198. echo "operation timeout: ${op_id}" >&2
  199. return 1
  200. }
  201. call_mutation() {
  202. local method=$1
  203. local path=$2
  204. local key=$3
  205. local body=${4:-}
  206. local resp
  207. if [[ -n "${body}" ]]; then
  208. resp="$(curl -sS -X "${method}" "${API}${path}" \
  209. -H 'Content-Type: application/json' \
  210. -H "Idempotency-Key: ${key}" \
  211. -d "${body}")"
  212. else
  213. resp="$(curl -sS -X "${method}" "${API}${path}" \
  214. -H "Idempotency-Key: ${key}")"
  215. fi
  216. echo "${resp}" | jq . >&2
  217. echo "${resp}" | jq -r '.operationId'
  218. }
  219. add_node() {
  220. local node_id=$1
  221. local ip=$2
  222. local key=$3
  223. local body op
  224. body="$(jq -nc \
  225. --arg id "${node_id}" \
  226. --arg ip "${ip}" \
  227. --argjson cpu "${NODE_CPU}" \
  228. --argjson ram "${NODE_RAM}" \
  229. --argjson ssd "${NODE_SSD}" \
  230. --arg user "${SSH_USER}" \
  231. --arg pw "${SSH_PASSWORD}" \
  232. '{nodeId:$id,cpu:$cpu,ram:$ram,ssd:$ssd,ipAddress:$ip,sshUser:$user,sshPassword:$pw,sshPort:22}')"
  233. op="$(call_mutation POST /v1/nodes "${key}" "${body}")"
  234. wait_op "${op}"
  235. }
  236. assert_operator_crds() {
  237. kubectl get crd sparkapplications.sparkoperator.k8s.io >/dev/null
  238. kubectl get crd starrocksclusters.starrocks.com >/dev/null
  239. kubectl -n "${SPARK_OPERATOR_NAMESPACE}" get deploy spark-operator-controller >/dev/null
  240. kubectl -n "${STARROCKS_OPERATOR_NAMESPACE}" get deploy "${STARROCKS_OPERATOR_DEPLOYMENT}" >/dev/null
  241. }
  242. run_flow() {
  243. local op
  244. local spark_create_body spark_job_body
  245. local sr_create_body sr_job_body
  246. log "GET /healthz"
  247. curl -sS "${API}/healthz" | jq .
  248. log "register nodes via POST /v1/nodes"
  249. for pair in "${WORKERS[@]}"; do
  250. local node_id ip
  251. node_id="${pair%%=*}"
  252. ip="${pair##*=}"
  253. add_node "${node_id}" "${ip}" "${SCRIPT_TAG}-add-${node_id}"
  254. done
  255. log "GET /v1/nodes"
  256. curl -sS "${API}/v1/nodes" | jq '.items | length'
  257. spark_create_body="$(jq -nc \
  258. --arg cid "${SPARK_CLUSTER_ID}" \
  259. --arg ns "${SPARK_NS}" \
  260. '{clusterId:$cid,namespace:$ns,nodeIds:["worker-1","worker-2"],sparkConfig:{"spark.dynamicAllocation.enabled":"false","spark.image":"apache/spark:3.5.1","spark.imagePullPolicy":"IfNotPresent","spark.prepull.enabled":"true"}}')"
  261. log "POST /v1/spark-clusters"
  262. op="$(call_mutation POST /v1/spark-clusters "${SCRIPT_TAG}-spark-create" "${spark_create_body}")"
  263. wait_op "${op}" "${SPARK_OP_MAX}"
  264. log "GET /v1/spark-clusters/{id}"
  265. curl -sS "${API}/v1/spark-clusters/${SPARK_CLUSTER_ID}" | jq '.metadata.name,.status.phase'
  266. log "POST /v1/spark-clusters/{id}/scale-out"
  267. op="$(call_mutation POST "/v1/spark-clusters/${SPARK_CLUSTER_ID}/scale-out" "${SCRIPT_TAG}-spark-scaleout" '{"nodeIds":["worker-3"]}')"
  268. wait_op "${op}" "${SPARK_OP_MAX}"
  269. log "POST /v1/spark-clusters/{id}/scale-in"
  270. op="$(call_mutation POST "/v1/spark-clusters/${SPARK_CLUSTER_ID}/scale-in" "${SCRIPT_TAG}-spark-scalein" '{"nodeIds":["worker-2"]}')"
  271. wait_op "${op}" "${SPARK_OP_MAX}"
  272. spark_job_body="$(jq -nc \
  273. --arg cid "${SPARK_CLUSTER_ID}" \
  274. '{clusterId:$cid,sparkApplicationSpec:{type:"Scala",mode:"cluster",image:"apache/spark:3.5.1",mainClass:"org.apache.spark.examples.SparkPi",mainApplicationFile:"local:///opt/spark/examples/jars/spark-examples_2.12-3.5.1.jar",driver:{cores:1,memory:"1g"},executor:{instances:1,cores:1,memory:"1g"}}}')"
  275. log "POST /v1/spark-jobs"
  276. op="$(call_mutation POST /v1/spark-jobs "${SCRIPT_TAG}-spark-job" "${spark_job_body}")"
  277. wait_op "${op}" "${SPARK_OP_MAX}"
  278. for i in $(seq 1 120); do
  279. local running
  280. running="$(kubectl -n "${SPARK_NS}" get pods --no-headers 2>/dev/null | awk '$3=="Running" || $3=="Pending" {c++} END {print c+0}')"
  281. log "spark pods running_or_pending=${running}"
  282. [[ "${running}" == "0" ]] && break
  283. sleep 5
  284. done
  285. log "POST /v1/spark-clusters/{id}/release"
  286. op="$(call_mutation POST "/v1/spark-clusters/${SPARK_CLUSTER_ID}/release" "${SCRIPT_TAG}-spark-release")"
  287. wait_op "${op}" "${SPARK_OP_MAX}"
  288. sr_create_body="$(jq -nc \
  289. --arg cid "${SR_CLUSTER_ID}" \
  290. --arg ns "${SR_NS}" \
  291. '{clusterId:$cid,namespace:$ns,feNodeIds:["worker-1"],beNodeIds:["worker-2"],starRocksConfig:{feResources:{requests:{cpu:"1",memory:"1Gi"},limits:{cpu:"2",memory:"2Gi"}},beResources:{requests:{cpu:"1",memory:"1Gi"},limits:{cpu:"2",memory:"2Gi"}}}}')"
  292. log "POST /v1/starrocks-clusters"
  293. op="$(call_mutation POST /v1/starrocks-clusters "${SCRIPT_TAG}-sr-create" "${sr_create_body}")"
  294. wait_op "${op}" "${STARROCKS_OP_MAX}"
  295. log "GET /v1/starrocks-clusters/{id}"
  296. curl -sS "${API}/v1/starrocks-clusters/${SR_CLUSTER_ID}" | jq '.metadata.name,.status.phase'
  297. log "POST /v1/starrocks-clusters/{id}/scale-out"
  298. op="$(call_mutation POST "/v1/starrocks-clusters/${SR_CLUSTER_ID}/scale-out" "${SCRIPT_TAG}-sr-scaleout" '{"nodeIds":["worker-3"]}')"
  299. wait_op "${op}" "${STARROCKS_OP_MAX}"
  300. log "POST /v1/starrocks-clusters/{id}/scale-in"
  301. op="$(call_mutation POST "/v1/starrocks-clusters/${SR_CLUSTER_ID}/scale-in" "${SCRIPT_TAG}-sr-scalein" '{"nodeIds":["worker-2"]}')"
  302. wait_op "${op}" "${STARROCKS_OP_MAX}"
  303. sr_job_body="$(jq -nc \
  304. --arg cid "${SR_CLUSTER_ID}" \
  305. '{clusterId:$cid,jobSpec:{backoffLimit:1,template:{spec:{restartPolicy:"Never",containers:[{name:"task",image:"busybox:1.36",command:["sh","-c","echo starrocks-job-ok"]}]}}}}')"
  306. log "POST /v1/starrocks-jobs"
  307. op="$(call_mutation POST /v1/starrocks-jobs "${SCRIPT_TAG}-sr-job" "${sr_job_body}")"
  308. wait_op "${op}" "${STARROCKS_OP_MAX}"
  309. log "POST /v1/starrocks-clusters/{id}/release"
  310. op="$(call_mutation POST "/v1/starrocks-clusters/${SR_CLUSTER_ID}/release" "${SCRIPT_TAG}-sr-release")"
  311. wait_op "${op}" "${STARROCKS_OP_MAX}"
  312. log "DELETE /v1/nodes/{id}"
  313. for pair in "${WORKERS[@]}"; do
  314. local node_id
  315. node_id="${pair%%=*}"
  316. op="$(call_mutation DELETE "/v1/nodes/${node_id}" "${SCRIPT_TAG}-del-${node_id}")"
  317. wait_op "${op}" "${DEFAULT_OP_MAX}"
  318. done
  319. log "final GET /v1/nodes"
  320. curl -sS "${API}/v1/nodes" | jq '.items | length'
  321. }
  322. main() {
  323. need_cmd kubectl
  324. need_cmd curl
  325. need_cmd jq
  326. need_cmd sshpass
  327. need_cmd kubeadm
  328. need_cmd helm
  329. ensure_k8s_prereq
  330. ensure_external_operators
  331. if [[ "${START_LOCAL}" == "true" ]]; then
  332. start_local_binaries
  333. else
  334. wait_api_ready
  335. fi
  336. assert_operator_crds
  337. cleanup_orchestrator_resources
  338. clear_node_isolation
  339. reset_workers_if_needed
  340. run_flow
  341. log "E2E PASSED"
  342. }
  343. main "$@"