server_test.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. package api_test
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "net/http"
  6. "net/http/httptest"
  7. "testing"
  8. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  9. orcv1 "github.com/cyrus/spark-orchestrator/api/v1alpha1"
  10. )
  11. func TestAddNodeRequiresIdempotencyKey(t *testing.T) {
  12. h := newTestServer(t)
  13. body := []byte(`{"cpu":8,"ram":32,"ssd":512,"ipAddress":"10.0.0.2","sshPassword":"x"}`)
  14. req := httptest.NewRequest(http.MethodPost, "/v1/nodes", bytes.NewReader(body))
  15. req.Header.Set("Content-Type", "application/json")
  16. resp := httptest.NewRecorder()
  17. h.ServeHTTP(resp, req)
  18. if resp.Code != http.StatusBadRequest {
  19. t.Fatalf("expected 400, got %d", resp.Code)
  20. }
  21. }
  22. func TestAddNodeIdempotent(t *testing.T) {
  23. h := newTestServer(t)
  24. payload := map[string]interface{}{
  25. "cpu": 8,
  26. "ram": 32,
  27. "ssd": 512,
  28. "ipAddress": "10.0.0.2",
  29. "sshPassword": "x",
  30. }
  31. body, _ := json.Marshal(payload)
  32. first := httptest.NewRequest(http.MethodPost, "/v1/nodes", bytes.NewReader(body))
  33. first.Header.Set("Content-Type", "application/json")
  34. first.Header.Set("Idempotency-Key", "node-op-1")
  35. resp1 := httptest.NewRecorder()
  36. h.ServeHTTP(resp1, first)
  37. if resp1.Code != http.StatusAccepted {
  38. t.Fatalf("expected 202, got %d", resp1.Code)
  39. }
  40. second := httptest.NewRequest(http.MethodPost, "/v1/nodes", bytes.NewReader(body))
  41. second.Header.Set("Content-Type", "application/json")
  42. second.Header.Set("Idempotency-Key", "node-op-1")
  43. resp2 := httptest.NewRecorder()
  44. h.ServeHTTP(resp2, second)
  45. if resp2.Code != http.StatusAccepted {
  46. t.Fatalf("expected 202, got %d", resp2.Code)
  47. }
  48. var p1 map[string]string
  49. var p2 map[string]string
  50. _ = json.Unmarshal(resp1.Body.Bytes(), &p1)
  51. _ = json.Unmarshal(resp2.Body.Bytes(), &p2)
  52. if p1["operationId"] == "" || p2["operationId"] == "" {
  53. t.Fatalf("operationId should not be empty")
  54. }
  55. if p1["operationId"] != p2["operationId"] {
  56. t.Fatalf("expected idempotent same operationId, got %s and %s", p1["operationId"], p2["operationId"])
  57. }
  58. }
  59. func TestCreateStarRocksClusterRequiresIdempotencyKey(t *testing.T) {
  60. h := newTestServer(t)
  61. body := []byte(`{"feNodeIds":["node-a"],"beNodeIds":["node-b"]}`)
  62. req := httptest.NewRequest(http.MethodPost, "/v1/starrocks-clusters", bytes.NewReader(body))
  63. req.Header.Set("Content-Type", "application/json")
  64. resp := httptest.NewRecorder()
  65. h.ServeHTTP(resp, req)
  66. if resp.Code != http.StatusBadRequest {
  67. t.Fatalf("expected 400, got %d", resp.Code)
  68. }
  69. }
  70. func TestCreateStarRocksClusterIdempotent(t *testing.T) {
  71. h := newTestServer(t)
  72. body := []byte(`{"clusterId":"sr-1","feNodeIds":["node-a"],"beNodeIds":["node-b","node-c"]}`)
  73. first := httptest.NewRequest(http.MethodPost, "/v1/starrocks-clusters", bytes.NewReader(body))
  74. first.Header.Set("Content-Type", "application/json")
  75. first.Header.Set("Idempotency-Key", "sr-create-1")
  76. resp1 := httptest.NewRecorder()
  77. h.ServeHTTP(resp1, first)
  78. if resp1.Code != http.StatusAccepted {
  79. t.Fatalf("expected 202, got %d", resp1.Code)
  80. }
  81. second := httptest.NewRequest(http.MethodPost, "/v1/starrocks-clusters", bytes.NewReader(body))
  82. second.Header.Set("Content-Type", "application/json")
  83. second.Header.Set("Idempotency-Key", "sr-create-1")
  84. resp2 := httptest.NewRecorder()
  85. h.ServeHTTP(resp2, second)
  86. if resp2.Code != http.StatusAccepted {
  87. t.Fatalf("expected 202, got %d", resp2.Code)
  88. }
  89. var p1 map[string]string
  90. var p2 map[string]string
  91. _ = json.Unmarshal(resp1.Body.Bytes(), &p1)
  92. _ = json.Unmarshal(resp2.Body.Bytes(), &p2)
  93. if p1["operationId"] == "" || p2["operationId"] == "" {
  94. t.Fatalf("operationId should not be empty")
  95. }
  96. if p1["operationId"] != p2["operationId"] {
  97. t.Fatalf("expected idempotent same operationId, got %s and %s", p1["operationId"], p2["operationId"])
  98. }
  99. }
  100. func TestGetStarRocksCluster(t *testing.T) {
  101. cluster := &orcv1.StarRocksVirtualCluster{
  102. ObjectMeta: metav1.ObjectMeta{Name: "sr-get"},
  103. Spec: orcv1.StarRocksVirtualClusterSpec{
  104. FeNodeRefs: []string{"node-a"},
  105. BeNodeRefs: []string{"node-b"},
  106. },
  107. Status: orcv1.StarRocksVirtualClusterStatus{
  108. Phase: orcv1.StarRocksClusterPhaseReady,
  109. Namespace: "starrocks-sr-get",
  110. },
  111. }
  112. h := newTestServerWithObjects(t, cluster)
  113. req := httptest.NewRequest(http.MethodGet, "/v1/starrocks-clusters/sr-get", nil)
  114. resp := httptest.NewRecorder()
  115. h.ServeHTTP(resp, req)
  116. if resp.Code != http.StatusOK {
  117. t.Fatalf("expected 200, got %d", resp.Code)
  118. }
  119. var got orcv1.StarRocksVirtualCluster
  120. if err := json.Unmarshal(resp.Body.Bytes(), &got); err != nil {
  121. t.Fatalf("unmarshal response: %v", err)
  122. }
  123. if got.Name != "sr-get" {
  124. t.Fatalf("expected cluster name sr-get, got %s", got.Name)
  125. }
  126. }
  127. func TestReleaseStarRocksClusterAccepted(t *testing.T) {
  128. h := newTestServer(t)
  129. req := httptest.NewRequest(http.MethodPost, "/v1/starrocks-clusters/sr-1/release", nil)
  130. req.Header.Set("Idempotency-Key", "sr-release-1")
  131. resp := httptest.NewRecorder()
  132. h.ServeHTTP(resp, req)
  133. if resp.Code != http.StatusAccepted {
  134. t.Fatalf("expected 202, got %d", resp.Code)
  135. }
  136. }
  137. func TestScaleOutStarRocksClusterRequiresNodeIDs(t *testing.T) {
  138. h := newTestServer(t)
  139. req := httptest.NewRequest(http.MethodPost, "/v1/starrocks-clusters/sr-1/scale-out", bytes.NewReader([]byte(`{}`)))
  140. req.Header.Set("Content-Type", "application/json")
  141. req.Header.Set("Idempotency-Key", "sr-scale-out-1")
  142. resp := httptest.NewRecorder()
  143. h.ServeHTTP(resp, req)
  144. if resp.Code != http.StatusBadRequest {
  145. t.Fatalf("expected 400, got %d", resp.Code)
  146. }
  147. }
  148. func TestScaleInStarRocksClusterAccepted(t *testing.T) {
  149. h := newTestServer(t)
  150. req := httptest.NewRequest(http.MethodPost, "/v1/starrocks-clusters/sr-1/scale-in", bytes.NewReader([]byte(`{"nodeIds":["node-b"]}`)))
  151. req.Header.Set("Content-Type", "application/json")
  152. req.Header.Set("Idempotency-Key", "sr-scale-in-1")
  153. resp := httptest.NewRecorder()
  154. h.ServeHTTP(resp, req)
  155. if resp.Code != http.StatusAccepted {
  156. t.Fatalf("expected 202, got %d", resp.Code)
  157. }
  158. }
  159. func TestSubmitStarRocksJobRequiresIdempotencyKey(t *testing.T) {
  160. h := newTestServer(t)
  161. body := []byte(`{"clusterId":"sr-1","jobSpec":{"template":{"spec":{"restartPolicy":"Never","containers":[{"name":"task","image":"busybox","command":["sh","-c","echo ok"]}]}}}}`)
  162. req := httptest.NewRequest(http.MethodPost, "/v1/starrocks-jobs", bytes.NewReader(body))
  163. req.Header.Set("Content-Type", "application/json")
  164. resp := httptest.NewRecorder()
  165. h.ServeHTTP(resp, req)
  166. if resp.Code != http.StatusBadRequest {
  167. t.Fatalf("expected 400, got %d", resp.Code)
  168. }
  169. }
  170. func TestSubmitStarRocksJobAccepted(t *testing.T) {
  171. h := newTestServer(t)
  172. body := []byte(`{"clusterId":"sr-1","jobSpec":{"template":{"spec":{"restartPolicy":"Never","containers":[{"name":"task","image":"busybox","command":["sh","-c","echo ok"]}]}}}}`)
  173. req := httptest.NewRequest(http.MethodPost, "/v1/starrocks-jobs", bytes.NewReader(body))
  174. req.Header.Set("Content-Type", "application/json")
  175. req.Header.Set("Idempotency-Key", "sr-job-1")
  176. resp := httptest.NewRecorder()
  177. h.ServeHTTP(resp, req)
  178. if resp.Code != http.StatusAccepted {
  179. t.Fatalf("expected 202, got %d", resp.Code)
  180. }
  181. }
  182. func TestGetStarRocksJobStatus(t *testing.T) {
  183. op := &orcv1.ClusterOperation{
  184. ObjectMeta: metav1.ObjectMeta{Name: "op-submit-starrocks-1"},
  185. Spec: orcv1.ClusterOperationSpec{
  186. Type: orcv1.OperationSubmitStarRocksJob,
  187. SubmitStarRocks: &orcv1.SubmitStarRocksJobSpec{
  188. ClusterName: "sr-1",
  189. },
  190. },
  191. Status: orcv1.ClusterOperationStatus{
  192. Phase: orcv1.OperationPhaseRunning,
  193. Message: "submitted",
  194. },
  195. }
  196. h := newTestServerWithObjects(t, op)
  197. req := httptest.NewRequest(http.MethodGet, "/v1/starrocks-jobs/operations/op-submit-starrocks-1", nil)
  198. resp := httptest.NewRecorder()
  199. h.ServeHTTP(resp, req)
  200. if resp.Code != http.StatusOK {
  201. t.Fatalf("expected 200, got %d, body=%s", resp.Code, resp.Body.String())
  202. }
  203. var payload map[string]interface{}
  204. if err := json.Unmarshal(resp.Body.Bytes(), &payload); err != nil {
  205. t.Fatalf("unmarshal response: %v", err)
  206. }
  207. if payload["operationId"] != "op-submit-starrocks-1" {
  208. t.Fatalf("unexpected operationId: %v", payload["operationId"])
  209. }
  210. }
  211. func newTestServer(t *testing.T) http.Handler {
  212. t.Helper()
  213. return newTestServerWithObjects(t)
  214. }