server_remaining_test.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. package api_test
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "net/http"
  6. "net/http/httptest"
  7. "testing"
  8. corev1 "k8s.io/api/core/v1"
  9. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  10. "k8s.io/apimachinery/pkg/runtime"
  11. utilruntime "k8s.io/apimachinery/pkg/util/runtime"
  12. ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
  13. ctrlclientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
  14. orcv1 "github.com/cyrus/spark-orchestrator/api/v1alpha1"
  15. api "github.com/cyrus/spark-orchestrator/internal/api"
  16. "github.com/cyrus/spark-orchestrator/internal/service"
  17. )
  18. func TestListNodesOK(t *testing.T) {
  19. node := &orcv1.NodeInventory{
  20. ObjectMeta: metav1.ObjectMeta{Name: "node-a"},
  21. Spec: orcv1.NodeInventorySpec{
  22. Address: "10.0.0.10",
  23. SSHSecretRef: "orchestrator-system/ssh-node-a",
  24. Capacity: orcv1.NodeCapacity{CPUCores: 8, RamGB: 32, SSDGB: 256},
  25. },
  26. Status: orcv1.NodeInventoryStatus{
  27. Phase: orcv1.NodePhaseIdle,
  28. KubernetesNodeName: "k8s-node-a",
  29. },
  30. }
  31. h := newTestServerWithObjects(t, node)
  32. req := httptest.NewRequest(http.MethodGet, "/v1/nodes", nil)
  33. resp := httptest.NewRecorder()
  34. h.ServeHTTP(resp, req)
  35. if resp.Code != http.StatusOK {
  36. t.Fatalf("expected 200, got %d", resp.Code)
  37. }
  38. var payload struct {
  39. Items []orcv1.NodeInventory `json:"items"`
  40. }
  41. if err := json.Unmarshal(resp.Body.Bytes(), &payload); err != nil {
  42. t.Fatalf("unmarshal response: %v", err)
  43. }
  44. if len(payload.Items) != 1 {
  45. t.Fatalf("expected 1 node, got %d", len(payload.Items))
  46. }
  47. if payload.Items[0].Name != "node-a" {
  48. t.Fatalf("expected node-a, got %s", payload.Items[0].Name)
  49. }
  50. }
  51. func TestDeleteNodeAccepted(t *testing.T) {
  52. h := newTestServer(t)
  53. req := httptest.NewRequest(http.MethodDelete, "/v1/nodes/node-a", nil)
  54. req.Header.Set("Idempotency-Key", "delete-node-1")
  55. resp := httptest.NewRecorder()
  56. h.ServeHTTP(resp, req)
  57. if resp.Code != http.StatusAccepted {
  58. t.Fatalf("expected 202, got %d", resp.Code)
  59. }
  60. var payload map[string]string
  61. _ = json.Unmarshal(resp.Body.Bytes(), &payload)
  62. if payload["resourceId"] != "node-a" {
  63. t.Fatalf("expected resourceId=node-a, got %q", payload["resourceId"])
  64. }
  65. if payload["operationId"] == "" {
  66. t.Fatal("operationId should not be empty")
  67. }
  68. }
  69. func TestCreateClusterAcceptedAndGetOperation(t *testing.T) {
  70. h := newTestServer(t)
  71. body := []byte(`{"clusterId":"spark-c1","nodeIds":["node-a"],"sparkConfig":{"k":"v"}}`)
  72. req := httptest.NewRequest(http.MethodPost, "/v1/spark-clusters", bytes.NewReader(body))
  73. req.Header.Set("Content-Type", "application/json")
  74. req.Header.Set("Idempotency-Key", "create-cluster-1")
  75. resp := httptest.NewRecorder()
  76. h.ServeHTTP(resp, req)
  77. if resp.Code != http.StatusAccepted {
  78. t.Fatalf("expected 202, got %d", resp.Code)
  79. }
  80. var createResp map[string]string
  81. _ = json.Unmarshal(resp.Body.Bytes(), &createResp)
  82. opID := createResp["operationId"]
  83. if opID == "" {
  84. t.Fatal("operationId should not be empty")
  85. }
  86. getReq := httptest.NewRequest(http.MethodGet, "/v1/operations/"+opID, nil)
  87. getResp := httptest.NewRecorder()
  88. h.ServeHTTP(getResp, getReq)
  89. if getResp.Code != http.StatusOK {
  90. t.Fatalf("expected 200, got %d", getResp.Code)
  91. }
  92. }
  93. func TestGetClusterOK(t *testing.T) {
  94. cluster := &orcv1.SparkVirtualCluster{
  95. ObjectMeta: metav1.ObjectMeta{Name: "spark-c1"},
  96. Spec: orcv1.SparkVirtualClusterSpec{
  97. NodeRefs: []string{"node-a"},
  98. },
  99. Status: orcv1.SparkVirtualClusterStatus{
  100. Phase: orcv1.SparkClusterPhaseReady,
  101. },
  102. }
  103. h := newTestServerWithObjects(t, cluster)
  104. req := httptest.NewRequest(http.MethodGet, "/v1/spark-clusters/spark-c1", nil)
  105. resp := httptest.NewRecorder()
  106. h.ServeHTTP(resp, req)
  107. if resp.Code != http.StatusOK {
  108. t.Fatalf("expected 200, got %d", resp.Code)
  109. }
  110. }
  111. func TestReleaseScaleAndSubmitJobAccepted(t *testing.T) {
  112. h := newTestServer(t)
  113. testCases := []struct {
  114. name string
  115. method string
  116. path string
  117. idemKey string
  118. body string
  119. }{
  120. {
  121. name: "release cluster",
  122. method: http.MethodPost,
  123. path: "/v1/spark-clusters/spark-c1/release",
  124. idemKey: "release-1",
  125. body: `{}`,
  126. },
  127. {
  128. name: "scale out",
  129. method: http.MethodPost,
  130. path: "/v1/spark-clusters/spark-c1/scale-out",
  131. idemKey: "scale-out-1",
  132. body: `{"nodeIds":["node-a","node-b"]}`,
  133. },
  134. {
  135. name: "scale in",
  136. method: http.MethodPost,
  137. path: "/v1/spark-clusters/spark-c1/scale-in",
  138. idemKey: "scale-in-1",
  139. body: `{"nodeIds":["node-a"]}`,
  140. },
  141. {
  142. name: "submit spark job",
  143. method: http.MethodPost,
  144. path: "/v1/spark-jobs",
  145. idemKey: "submit-job-1",
  146. body: `{"clusterId":"spark-c1","sparkApplicationSpec":{"type":"Scala","mode":"cluster"}}`,
  147. },
  148. }
  149. for _, tc := range testCases {
  150. t.Run(tc.name, func(t *testing.T) {
  151. req := httptest.NewRequest(tc.method, tc.path, bytes.NewReader([]byte(tc.body)))
  152. req.Header.Set("Content-Type", "application/json")
  153. req.Header.Set("Idempotency-Key", tc.idemKey)
  154. resp := httptest.NewRecorder()
  155. h.ServeHTTP(resp, req)
  156. if resp.Code != http.StatusAccepted {
  157. t.Fatalf("expected 202, got %d, body=%s", resp.Code, resp.Body.String())
  158. }
  159. })
  160. }
  161. }
  162. func newTestServerWithObjects(t *testing.T, objs ...ctrlclient.Object) http.Handler {
  163. t.Helper()
  164. scheme := runtime.NewScheme()
  165. utilruntime.Must(corev1.AddToScheme(scheme))
  166. utilruntime.Must(orcv1.AddToScheme(scheme))
  167. builder := ctrlclientfake.NewClientBuilder().WithScheme(scheme)
  168. if len(objs) > 0 {
  169. builder = builder.WithObjects(objs...)
  170. }
  171. fakeClient := builder.Build()
  172. svc := service.NewOrchestratorService(fakeClient, "orchestrator-system")
  173. return api.NewServer(svc).Router()
  174. }