server.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. package api
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "net/http"
  7. "strings"
  8. "github.com/go-chi/chi/v5"
  9. apierrors "k8s.io/apimachinery/pkg/api/errors"
  10. "github.com/cyrus/spark-orchestrator/internal/service"
  11. )
  12. type Server struct {
  13. svc *service.OrchestratorService
  14. }
  15. func NewServer(svc *service.OrchestratorService) *Server {
  16. return &Server{svc: svc}
  17. }
  18. func (s *Server) Router() http.Handler {
  19. r := chi.NewRouter()
  20. r.Get("/healthz", func(w http.ResponseWriter, _ *http.Request) {
  21. writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
  22. })
  23. r.Route("/v1", func(r chi.Router) {
  24. r.Post("/nodes", s.handleAddNode)
  25. r.Delete("/nodes/{nodeId}", s.handleDeleteNode)
  26. r.Get("/nodes", s.handleListNodes)
  27. r.Post("/spark-clusters", s.handleCreateCluster)
  28. r.Get("/spark-clusters/{clusterId}", s.handleGetCluster)
  29. r.Post("/spark-clusters/{clusterId}/release", s.handleReleaseCluster)
  30. r.Post("/spark-clusters/{clusterId}/scale-out", s.handleScaleOut)
  31. r.Post("/spark-clusters/{clusterId}/scale-in", s.handleScaleIn)
  32. r.Post("/starrocks-clusters", s.handleCreateStarRocksCluster)
  33. r.Get("/starrocks-clusters/{clusterId}", s.handleGetStarRocksCluster)
  34. r.Get("/starrocks-clusters/{clusterId}/direct-endpoint", s.handleGetStarRocksDirectEndpoint)
  35. r.Post("/starrocks-clusters/{clusterId}/release", s.handleReleaseStarRocksCluster)
  36. r.Post("/starrocks-clusters/{clusterId}/scale-out", s.handleScaleOutStarRocksCluster)
  37. r.Post("/starrocks-clusters/{clusterId}/scale-in", s.handleScaleInStarRocksCluster)
  38. r.Post("/starrocks-jobs", s.handleSubmitStarRocksJob)
  39. r.Get("/starrocks-jobs/operations/{operationId}", s.handleGetStarRocksJobStatus)
  40. r.Post("/spark-jobs", s.handleSubmitSparkJob)
  41. r.Get("/spark-jobs/operations/{operationId}", s.handleGetSparkJobStatus)
  42. r.Delete("/spark-jobs/operations/{operationId}", s.handleDeleteSparkJob)
  43. r.Get("/spark-jobs/operations/{operationId}/result", s.handleGetSparkJobResult)
  44. r.Get("/operations/{operationId}", s.handleGetOperation)
  45. })
  46. return r
  47. }
  48. func (s *Server) handleAddNode(w http.ResponseWriter, r *http.Request) {
  49. idKey, err := requiredIdempotencyKey(r)
  50. if err != nil {
  51. writeError(w, http.StatusBadRequest, err)
  52. return
  53. }
  54. var req service.AddNodeRequest
  55. if err := decodeJSON(r, &req); err != nil {
  56. writeError(w, http.StatusBadRequest, err)
  57. return
  58. }
  59. resp, err := s.svc.AddNode(r.Context(), req, idKey)
  60. if err != nil {
  61. writeServiceError(w, err)
  62. return
  63. }
  64. writeJSON(w, http.StatusAccepted, resp)
  65. }
  66. func (s *Server) handleDeleteNode(w http.ResponseWriter, r *http.Request) {
  67. idKey, err := requiredIdempotencyKey(r)
  68. if err != nil {
  69. writeError(w, http.StatusBadRequest, err)
  70. return
  71. }
  72. nodeID := chi.URLParam(r, "nodeId")
  73. resp, err := s.svc.DeleteNode(r.Context(), nodeID, idKey)
  74. if err != nil {
  75. writeServiceError(w, err)
  76. return
  77. }
  78. writeJSON(w, http.StatusAccepted, resp)
  79. }
  80. func (s *Server) handleListNodes(w http.ResponseWriter, r *http.Request) {
  81. nodes, err := s.svc.ListNodes(r.Context())
  82. if err != nil {
  83. writeServiceError(w, err)
  84. return
  85. }
  86. writeJSON(w, http.StatusOK, map[string]interface{}{"items": nodes})
  87. }
  88. func (s *Server) handleCreateCluster(w http.ResponseWriter, r *http.Request) {
  89. idKey, err := requiredIdempotencyKey(r)
  90. if err != nil {
  91. writeError(w, http.StatusBadRequest, err)
  92. return
  93. }
  94. var req service.CreateClusterRequest
  95. if err := decodeJSON(r, &req); err != nil {
  96. writeError(w, http.StatusBadRequest, err)
  97. return
  98. }
  99. resp, err := s.svc.CreateCluster(r.Context(), req, idKey)
  100. if err != nil {
  101. writeServiceError(w, err)
  102. return
  103. }
  104. writeJSON(w, http.StatusAccepted, resp)
  105. }
  106. func (s *Server) handleGetCluster(w http.ResponseWriter, r *http.Request) {
  107. cluster, err := s.svc.GetCluster(r.Context(), chi.URLParam(r, "clusterId"))
  108. if err != nil {
  109. writeServiceError(w, err)
  110. return
  111. }
  112. writeJSON(w, http.StatusOK, cluster)
  113. }
  114. func (s *Server) handleReleaseCluster(w http.ResponseWriter, r *http.Request) {
  115. idKey, err := requiredIdempotencyKey(r)
  116. if err != nil {
  117. writeError(w, http.StatusBadRequest, err)
  118. return
  119. }
  120. clusterID := chi.URLParam(r, "clusterId")
  121. resp, err := s.svc.ReleaseCluster(r.Context(), clusterID, idKey)
  122. if err != nil {
  123. writeServiceError(w, err)
  124. return
  125. }
  126. writeJSON(w, http.StatusAccepted, resp)
  127. }
  128. func (s *Server) handleScaleOut(w http.ResponseWriter, r *http.Request) {
  129. s.handleResize(w, r, true)
  130. }
  131. func (s *Server) handleScaleIn(w http.ResponseWriter, r *http.Request) {
  132. s.handleResize(w, r, false)
  133. }
  134. func (s *Server) handleResize(w http.ResponseWriter, r *http.Request, scaleOut bool) {
  135. idKey, err := requiredIdempotencyKey(r)
  136. if err != nil {
  137. writeError(w, http.StatusBadRequest, err)
  138. return
  139. }
  140. var req service.ResizeClusterRequest
  141. if err := decodeJSON(r, &req); err != nil {
  142. writeError(w, http.StatusBadRequest, err)
  143. return
  144. }
  145. clusterID := chi.URLParam(r, "clusterId")
  146. var resp *service.OperationResponse
  147. if scaleOut {
  148. resp, err = s.svc.ScaleOut(r.Context(), clusterID, req, idKey)
  149. } else {
  150. resp, err = s.svc.ScaleIn(r.Context(), clusterID, req, idKey)
  151. }
  152. if err != nil {
  153. writeServiceError(w, err)
  154. return
  155. }
  156. writeJSON(w, http.StatusAccepted, resp)
  157. }
  158. func (s *Server) handleSubmitSparkJob(w http.ResponseWriter, r *http.Request) {
  159. idKey, err := requiredIdempotencyKey(r)
  160. if err != nil {
  161. writeError(w, http.StatusBadRequest, err)
  162. return
  163. }
  164. var req service.SubmitSparkJobRequest
  165. if err := decodeJSON(r, &req); err != nil {
  166. writeError(w, http.StatusBadRequest, err)
  167. return
  168. }
  169. resp, err := s.svc.SubmitSparkJob(r.Context(), req, idKey)
  170. if err != nil {
  171. writeServiceError(w, err)
  172. return
  173. }
  174. writeJSON(w, http.StatusAccepted, resp)
  175. }
  176. func (s *Server) handleGetSparkJobStatus(w http.ResponseWriter, r *http.Request) {
  177. status, err := s.svc.GetSparkJobStatusByOperation(r.Context(), chi.URLParam(r, "operationId"))
  178. if err != nil {
  179. writeServiceError(w, err)
  180. return
  181. }
  182. writeJSON(w, http.StatusOK, status)
  183. }
  184. func (s *Server) handleDeleteSparkJob(w http.ResponseWriter, r *http.Request) {
  185. resp, err := s.svc.DeleteSparkJobByOperation(r.Context(), chi.URLParam(r, "operationId"))
  186. if err != nil {
  187. writeServiceError(w, err)
  188. return
  189. }
  190. writeJSON(w, http.StatusOK, resp)
  191. }
  192. func (s *Server) handleGetSparkJobResult(w http.ResponseWriter, r *http.Request) {
  193. result, err := s.svc.GetSparkJobResultByOperation(r.Context(), chi.URLParam(r, "operationId"))
  194. if err != nil {
  195. writeServiceError(w, err)
  196. return
  197. }
  198. writeJSON(w, http.StatusOK, result)
  199. }
  200. func (s *Server) handleCreateStarRocksCluster(w http.ResponseWriter, r *http.Request) {
  201. idKey, err := requiredIdempotencyKey(r)
  202. if err != nil {
  203. writeError(w, http.StatusBadRequest, err)
  204. return
  205. }
  206. var req service.CreateStarRocksClusterRequest
  207. if err := decodeJSON(r, &req); err != nil {
  208. writeError(w, http.StatusBadRequest, err)
  209. return
  210. }
  211. resp, err := s.svc.CreateStarRocksCluster(r.Context(), req, idKey)
  212. if err != nil {
  213. writeServiceError(w, err)
  214. return
  215. }
  216. writeJSON(w, http.StatusAccepted, resp)
  217. }
  218. func (s *Server) handleGetStarRocksCluster(w http.ResponseWriter, r *http.Request) {
  219. cluster, err := s.svc.GetStarRocksCluster(r.Context(), chi.URLParam(r, "clusterId"))
  220. if err != nil {
  221. writeServiceError(w, err)
  222. return
  223. }
  224. writeJSON(w, http.StatusOK, cluster)
  225. }
  226. func (s *Server) handleGetStarRocksDirectEndpoint(w http.ResponseWriter, r *http.Request) {
  227. endpoint, err := s.svc.GetStarRocksDirectEndpoint(r.Context(), chi.URLParam(r, "clusterId"))
  228. if err != nil {
  229. writeServiceError(w, err)
  230. return
  231. }
  232. writeJSON(w, http.StatusOK, endpoint)
  233. }
  234. func (s *Server) handleReleaseStarRocksCluster(w http.ResponseWriter, r *http.Request) {
  235. idKey, err := requiredIdempotencyKey(r)
  236. if err != nil {
  237. writeError(w, http.StatusBadRequest, err)
  238. return
  239. }
  240. clusterID := chi.URLParam(r, "clusterId")
  241. resp, err := s.svc.ReleaseStarRocksCluster(r.Context(), clusterID, idKey)
  242. if err != nil {
  243. writeServiceError(w, err)
  244. return
  245. }
  246. writeJSON(w, http.StatusAccepted, resp)
  247. }
  248. func (s *Server) handleScaleOutStarRocksCluster(w http.ResponseWriter, r *http.Request) {
  249. s.handleResizeStarRocksCluster(w, r, true)
  250. }
  251. func (s *Server) handleScaleInStarRocksCluster(w http.ResponseWriter, r *http.Request) {
  252. s.handleResizeStarRocksCluster(w, r, false)
  253. }
  254. func (s *Server) handleResizeStarRocksCluster(w http.ResponseWriter, r *http.Request, scaleOut bool) {
  255. idKey, err := requiredIdempotencyKey(r)
  256. if err != nil {
  257. writeError(w, http.StatusBadRequest, err)
  258. return
  259. }
  260. var req service.ResizeStarRocksClusterRequest
  261. if err := decodeJSON(r, &req); err != nil {
  262. writeError(w, http.StatusBadRequest, err)
  263. return
  264. }
  265. clusterID := chi.URLParam(r, "clusterId")
  266. var resp *service.OperationResponse
  267. if scaleOut {
  268. resp, err = s.svc.ScaleOutStarRocksCluster(r.Context(), clusterID, req, idKey)
  269. } else {
  270. resp, err = s.svc.ScaleInStarRocksCluster(r.Context(), clusterID, req, idKey)
  271. }
  272. if err != nil {
  273. writeServiceError(w, err)
  274. return
  275. }
  276. writeJSON(w, http.StatusAccepted, resp)
  277. }
  278. func (s *Server) handleSubmitStarRocksJob(w http.ResponseWriter, r *http.Request) {
  279. idKey, err := requiredIdempotencyKey(r)
  280. if err != nil {
  281. writeError(w, http.StatusBadRequest, err)
  282. return
  283. }
  284. var req service.SubmitStarRocksJobRequest
  285. if err := decodeJSON(r, &req); err != nil {
  286. writeError(w, http.StatusBadRequest, err)
  287. return
  288. }
  289. resp, err := s.svc.SubmitStarRocksJob(r.Context(), req, idKey)
  290. if err != nil {
  291. writeServiceError(w, err)
  292. return
  293. }
  294. writeJSON(w, http.StatusAccepted, resp)
  295. }
  296. func (s *Server) handleGetStarRocksJobStatus(w http.ResponseWriter, r *http.Request) {
  297. status, err := s.svc.GetStarRocksJobStatusByOperation(r.Context(), chi.URLParam(r, "operationId"))
  298. if err != nil {
  299. writeServiceError(w, err)
  300. return
  301. }
  302. writeJSON(w, http.StatusOK, status)
  303. }
  304. func (s *Server) handleGetOperation(w http.ResponseWriter, r *http.Request) {
  305. op, err := s.svc.GetOperation(r.Context(), chi.URLParam(r, "operationId"))
  306. if err != nil {
  307. writeServiceError(w, err)
  308. return
  309. }
  310. writeJSON(w, http.StatusOK, op)
  311. }
  312. func requiredIdempotencyKey(r *http.Request) (string, error) {
  313. v := strings.TrimSpace(r.Header.Get("Idempotency-Key"))
  314. if v == "" {
  315. return "", errors.New("Idempotency-Key header is required")
  316. }
  317. return v, nil
  318. }
  319. func decodeJSON(r *http.Request, out interface{}) error {
  320. defer r.Body.Close()
  321. dec := json.NewDecoder(r.Body)
  322. dec.DisallowUnknownFields()
  323. if err := dec.Decode(out); err != nil {
  324. return fmt.Errorf("invalid json payload: %w", err)
  325. }
  326. return nil
  327. }
  328. func writeServiceError(w http.ResponseWriter, err error) {
  329. if apierrors.IsNotFound(err) {
  330. writeError(w, http.StatusNotFound, err)
  331. return
  332. }
  333. writeError(w, http.StatusBadRequest, err)
  334. }
  335. func writeError(w http.ResponseWriter, status int, err error) {
  336. writeJSON(w, status, map[string]string{"error": err.Error()})
  337. }
  338. func writeJSON(w http.ResponseWriter, status int, payload interface{}) {
  339. w.Header().Set("Content-Type", "application/json")
  340. w.WriteHeader(status)
  341. _ = json.NewEncoder(w).Encode(payload)
  342. }