clusteroperation_controller_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. package controller
  2. import (
  3. "context"
  4. "strings"
  5. "testing"
  6. corev1 "k8s.io/api/core/v1"
  7. apierrors "k8s.io/apimachinery/pkg/api/errors"
  8. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  9. "k8s.io/apimachinery/pkg/runtime"
  10. "k8s.io/apimachinery/pkg/types"
  11. utilruntime "k8s.io/apimachinery/pkg/util/runtime"
  12. ctrlclientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
  13. orcv1 "github.com/cyrus/spark-orchestrator/api/v1alpha1"
  14. "github.com/cyrus/spark-orchestrator/internal/util"
  15. )
  16. func TestEnsureNoRunningPodsOnNodes_Blocking(t *testing.T) {
  17. scheme := runtime.NewScheme()
  18. utilruntime.Must(corev1.AddToScheme(scheme))
  19. utilruntime.Must(orcv1.AddToScheme(scheme))
  20. cluster := &orcv1.SparkVirtualCluster{
  21. ObjectMeta: metav1.ObjectMeta{Name: "c1"},
  22. Status: orcv1.SparkVirtualClusterStatus{
  23. Namespace: "spark-c1",
  24. },
  25. }
  26. nodeInv := &orcv1.NodeInventory{
  27. ObjectMeta: metav1.ObjectMeta{Name: "node-a"},
  28. Status: orcv1.NodeInventoryStatus{KubernetesNodeName: "k8s-node-a"},
  29. }
  30. pod := &corev1.Pod{
  31. ObjectMeta: metav1.ObjectMeta{Name: "driver-1", Namespace: "spark-c1"},
  32. Spec: corev1.PodSpec{NodeName: "k8s-node-a"},
  33. Status: corev1.PodStatus{Phase: corev1.PodRunning},
  34. }
  35. fakeClient := ctrlclientfake.NewClientBuilder().WithScheme(scheme).WithObjects(cluster, nodeInv, pod).Build()
  36. r := &ClusterOperationReconciler{Client: fakeClient, Scheme: scheme}
  37. err := r.ensureNoRunningPodsOnNodes(context.Background(), cluster, []string{"node-a"})
  38. if err == nil {
  39. t.Fatalf("expected blocking error, got nil")
  40. }
  41. }
  42. func TestEnsureNoRunningPodsOnNodes_NoBlockForSucceededPod(t *testing.T) {
  43. scheme := runtime.NewScheme()
  44. utilruntime.Must(corev1.AddToScheme(scheme))
  45. utilruntime.Must(orcv1.AddToScheme(scheme))
  46. cluster := &orcv1.SparkVirtualCluster{
  47. ObjectMeta: metav1.ObjectMeta{Name: "c1"},
  48. Status: orcv1.SparkVirtualClusterStatus{
  49. Namespace: "spark-c1",
  50. },
  51. }
  52. nodeInv := &orcv1.NodeInventory{
  53. ObjectMeta: metav1.ObjectMeta{Name: "node-a"},
  54. Status: orcv1.NodeInventoryStatus{KubernetesNodeName: "k8s-node-a"},
  55. }
  56. pod := &corev1.Pod{
  57. ObjectMeta: metav1.ObjectMeta{Name: "driver-1", Namespace: "spark-c1"},
  58. Spec: corev1.PodSpec{NodeName: "k8s-node-a"},
  59. Status: corev1.PodStatus{Phase: corev1.PodSucceeded},
  60. }
  61. fakeClient := ctrlclientfake.NewClientBuilder().WithScheme(scheme).WithObjects(cluster, nodeInv, pod).Build()
  62. r := &ClusterOperationReconciler{Client: fakeClient, Scheme: scheme}
  63. err := r.ensureNoRunningPodsOnNodes(context.Background(), cluster, []string{"node-a"})
  64. if err != nil {
  65. t.Fatalf("expected no error, got %v", err)
  66. }
  67. }
  68. func TestEnsureNoRunningPodsOnNodes_IgnoreSparkPrePullPod(t *testing.T) {
  69. scheme := runtime.NewScheme()
  70. utilruntime.Must(corev1.AddToScheme(scheme))
  71. utilruntime.Must(orcv1.AddToScheme(scheme))
  72. cluster := &orcv1.SparkVirtualCluster{
  73. ObjectMeta: metav1.ObjectMeta{Name: "c1"},
  74. Status: orcv1.SparkVirtualClusterStatus{
  75. Namespace: "spark-c1",
  76. },
  77. }
  78. nodeInv := &orcv1.NodeInventory{
  79. ObjectMeta: metav1.ObjectMeta{Name: "node-a"},
  80. Status: orcv1.NodeInventoryStatus{KubernetesNodeName: "k8s-node-a"},
  81. }
  82. pod := &corev1.Pod{
  83. ObjectMeta: metav1.ObjectMeta{
  84. Name: "spark-prepull-1",
  85. Namespace: "spark-c1",
  86. Labels: map[string]string{
  87. util.LabelManagedBy: util.ManagedByValue,
  88. util.LabelComponent: util.ComponentSparkPrePull,
  89. },
  90. },
  91. Spec: corev1.PodSpec{NodeName: "k8s-node-a"},
  92. Status: corev1.PodStatus{Phase: corev1.PodRunning},
  93. }
  94. fakeClient := ctrlclientfake.NewClientBuilder().WithScheme(scheme).WithObjects(cluster, nodeInv, pod).Build()
  95. r := &ClusterOperationReconciler{Client: fakeClient, Scheme: scheme}
  96. err := r.ensureNoRunningPodsOnNodes(context.Background(), cluster, []string{"node-a"})
  97. if err != nil {
  98. t.Fatalf("expected no error for spark pre-pull pod, got %v", err)
  99. }
  100. }
  101. func TestEnsureNoRunningPodsOnNodes_IgnoreSparkIvyWarmupPod(t *testing.T) {
  102. scheme := runtime.NewScheme()
  103. utilruntime.Must(corev1.AddToScheme(scheme))
  104. utilruntime.Must(orcv1.AddToScheme(scheme))
  105. cluster := &orcv1.SparkVirtualCluster{
  106. ObjectMeta: metav1.ObjectMeta{Name: "c1"},
  107. Status: orcv1.SparkVirtualClusterStatus{
  108. Namespace: "spark-c1",
  109. },
  110. }
  111. nodeInv := &orcv1.NodeInventory{
  112. ObjectMeta: metav1.ObjectMeta{Name: "node-a"},
  113. Status: orcv1.NodeInventoryStatus{KubernetesNodeName: "k8s-node-a"},
  114. }
  115. pod := &corev1.Pod{
  116. ObjectMeta: metav1.ObjectMeta{
  117. Name: "spark-ivy-warmup-1",
  118. Namespace: "spark-c1",
  119. Labels: map[string]string{
  120. util.LabelManagedBy: util.ManagedByValue,
  121. util.LabelComponent: util.ComponentSparkIvyWarmup,
  122. },
  123. },
  124. Spec: corev1.PodSpec{NodeName: "k8s-node-a"},
  125. Status: corev1.PodStatus{Phase: corev1.PodRunning},
  126. }
  127. fakeClient := ctrlclientfake.NewClientBuilder().WithScheme(scheme).WithObjects(cluster, nodeInv, pod).Build()
  128. r := &ClusterOperationReconciler{Client: fakeClient, Scheme: scheme}
  129. err := r.ensureNoRunningPodsOnNodes(context.Background(), cluster, []string{"node-a"})
  130. if err != nil {
  131. t.Fatalf("expected no error for spark ivy warmup pod, got %v", err)
  132. }
  133. }
  134. func TestHandleScaleInStarRocksClusterSerial(t *testing.T) {
  135. scheme := runtime.NewScheme()
  136. utilruntime.Must(corev1.AddToScheme(scheme))
  137. utilruntime.Must(orcv1.AddToScheme(scheme))
  138. cluster := &orcv1.StarRocksVirtualCluster{
  139. ObjectMeta: metav1.ObjectMeta{Name: "sr-1"},
  140. Spec: orcv1.StarRocksVirtualClusterSpec{
  141. FeNodeRefs: []string{"fe-1"},
  142. BeNodeRefs: []string{"be-1", "be-2"},
  143. },
  144. Status: orcv1.StarRocksVirtualClusterStatus{
  145. Phase: orcv1.StarRocksClusterPhaseReady,
  146. Namespace: "starrocks-sr-1",
  147. AssignedFeNodes: []string{"fe-1"},
  148. AssignedBeNodes: []string{"be-1", "be-2"},
  149. },
  150. }
  151. node1 := &orcv1.NodeInventory{
  152. ObjectMeta: metav1.ObjectMeta{Name: "be-1"},
  153. Status: orcv1.NodeInventoryStatus{KubernetesNodeName: "k8s-be-1"},
  154. }
  155. node2 := &orcv1.NodeInventory{
  156. ObjectMeta: metav1.ObjectMeta{Name: "be-2"},
  157. Status: orcv1.NodeInventoryStatus{KubernetesNodeName: "k8s-be-2"},
  158. }
  159. fakeClient := ctrlclientfake.NewClientBuilder().WithScheme(scheme).WithObjects(cluster, node1, node2).Build()
  160. r := &ClusterOperationReconciler{Client: fakeClient, Scheme: scheme}
  161. op := &orcv1.ClusterOperation{
  162. Spec: orcv1.ClusterOperationSpec{
  163. ResizeStarRocks: &orcv1.ResizeStarRocksClusterSpec{
  164. ClusterName: "sr-1",
  165. NodeIDs: []string{"be-1", "be-2"},
  166. },
  167. },
  168. }
  169. done, _, _, message, err := r.handleScaleInStarRocksCluster(context.Background(), op)
  170. if err != nil {
  171. t.Fatalf("first scale-in step returned error: %v", err)
  172. }
  173. if done {
  174. t.Fatalf("first scale-in step should not complete")
  175. }
  176. if !strings.Contains(message, "be-1") {
  177. t.Fatalf("expected first step to remove be-1, got message: %s", message)
  178. }
  179. var updated orcv1.StarRocksVirtualCluster
  180. if err := r.Get(context.Background(), types.NamespacedName{Name: "sr-1"}, &updated); err != nil {
  181. t.Fatalf("get updated cluster: %v", err)
  182. }
  183. if util.ContainsString(updated.Spec.BeNodeRefs, "be-1") {
  184. t.Fatalf("be-1 should be removed first")
  185. }
  186. if !util.ContainsString(updated.Spec.BeNodeRefs, "be-2") {
  187. t.Fatalf("be-2 should remain after first serial step")
  188. }
  189. updated.Status.AssignedBeNodes = []string{"be-2"}
  190. if err := r.Update(context.Background(), &updated); err != nil {
  191. t.Fatalf("update cluster status simulation: %v", err)
  192. }
  193. done, _, _, message, err = r.handleScaleInStarRocksCluster(context.Background(), op)
  194. if err != nil {
  195. t.Fatalf("second scale-in step returned error: %v", err)
  196. }
  197. if done {
  198. t.Fatalf("second scale-in step should not complete")
  199. }
  200. if !strings.Contains(message, "be-2") {
  201. t.Fatalf("expected second step to remove be-2, got message: %s", message)
  202. }
  203. if err := r.Get(context.Background(), types.NamespacedName{Name: "sr-1"}, &updated); err != nil {
  204. t.Fatalf("get cluster after second step: %v", err)
  205. }
  206. if len(updated.Spec.BeNodeRefs) != 0 {
  207. t.Fatalf("expected all BE nodes removed, got %v", updated.Spec.BeNodeRefs)
  208. }
  209. updated.Status.AssignedBeNodes = nil
  210. if err := r.Update(context.Background(), &updated); err != nil {
  211. t.Fatalf("update final status simulation: %v", err)
  212. }
  213. done, _, _, message, err = r.handleScaleInStarRocksCluster(context.Background(), op)
  214. if err != nil {
  215. t.Fatalf("final scale-in step returned error: %v", err)
  216. }
  217. if !done {
  218. t.Fatalf("expected serial scale-in to complete")
  219. }
  220. if !strings.Contains(strings.ToLower(message), "completed") {
  221. t.Fatalf("expected completed message, got: %s", message)
  222. }
  223. }
  224. func TestHandleDeleteNode_RemovesNodeInventoryAndKubernetesNode(t *testing.T) {
  225. scheme := runtime.NewScheme()
  226. utilruntime.Must(corev1.AddToScheme(scheme))
  227. utilruntime.Must(orcv1.AddToScheme(scheme))
  228. nodeInv := &orcv1.NodeInventory{
  229. ObjectMeta: metav1.ObjectMeta{Name: "node-16"},
  230. Spec: orcv1.NodeInventorySpec{Address: "172.30.194.144"},
  231. Status: orcv1.NodeInventoryStatus{
  232. Phase: orcv1.NodePhaseIdle,
  233. KubernetesNodeName: "node-16",
  234. },
  235. }
  236. k8sNode := &corev1.Node{
  237. ObjectMeta: metav1.ObjectMeta{
  238. Name: "node-16",
  239. Labels: map[string]string{
  240. util.LabelSparkClusterID: "spark-1",
  241. util.LabelManagedBy: util.ManagedByValue,
  242. },
  243. },
  244. }
  245. op := &orcv1.ClusterOperation{
  246. Spec: orcv1.ClusterOperationSpec{
  247. DeleteNode: "node-16",
  248. },
  249. }
  250. fakeClient := ctrlclientfake.NewClientBuilder().WithScheme(scheme).WithObjects(nodeInv, k8sNode).Build()
  251. r := &ClusterOperationReconciler{Client: fakeClient, Scheme: scheme}
  252. done, _, _, message, err := r.handleDeleteNode(context.Background(), op)
  253. if err != nil {
  254. t.Fatalf("handleDeleteNode returned error: %v", err)
  255. }
  256. if !done {
  257. t.Fatalf("expected delete operation completed")
  258. }
  259. if !strings.Contains(strings.ToLower(message), "cluster") {
  260. t.Fatalf("expected cluster removal message, got: %s", message)
  261. }
  262. var invAfter orcv1.NodeInventory
  263. if err := fakeClient.Get(context.Background(), types.NamespacedName{Name: "node-16"}, &invAfter); !apierrors.IsNotFound(err) {
  264. t.Fatalf("expected node inventory deleted, get err=%v", err)
  265. }
  266. var nodeAfter corev1.Node
  267. if err := fakeClient.Get(context.Background(), types.NamespacedName{Name: "node-16"}, &nodeAfter); !apierrors.IsNotFound(err) {
  268. t.Fatalf("expected kubernetes node deleted, get err=%v", err)
  269. }
  270. }
  271. func TestHandleDeleteNode_FallbackDeleteByInternalIP(t *testing.T) {
  272. scheme := runtime.NewScheme()
  273. utilruntime.Must(corev1.AddToScheme(scheme))
  274. utilruntime.Must(orcv1.AddToScheme(scheme))
  275. nodeInv := &orcv1.NodeInventory{
  276. ObjectMeta: metav1.ObjectMeta{Name: "node-17"},
  277. Spec: orcv1.NodeInventorySpec{Address: "10.0.0.17"},
  278. Status: orcv1.NodeInventoryStatus{
  279. Phase: orcv1.NodePhaseIdle,
  280. },
  281. }
  282. k8sNode := &corev1.Node{
  283. ObjectMeta: metav1.ObjectMeta{Name: "real-node-17"},
  284. Status: corev1.NodeStatus{
  285. Addresses: []corev1.NodeAddress{
  286. {Type: corev1.NodeInternalIP, Address: "10.0.0.17"},
  287. },
  288. },
  289. }
  290. op := &orcv1.ClusterOperation{
  291. Spec: orcv1.ClusterOperationSpec{
  292. DeleteNode: "node-17",
  293. },
  294. }
  295. fakeClient := ctrlclientfake.NewClientBuilder().WithScheme(scheme).WithObjects(nodeInv, k8sNode).Build()
  296. r := &ClusterOperationReconciler{Client: fakeClient, Scheme: scheme}
  297. done, _, _, _, err := r.handleDeleteNode(context.Background(), op)
  298. if err != nil {
  299. t.Fatalf("handleDeleteNode returned error: %v", err)
  300. }
  301. if !done {
  302. t.Fatalf("expected delete operation completed")
  303. }
  304. var nodeAfter corev1.Node
  305. if err := fakeClient.Get(context.Background(), types.NamespacedName{Name: "real-node-17"}, &nodeAfter); !apierrors.IsNotFound(err) {
  306. t.Fatalf("expected fallback matched kubernetes node deleted, get err=%v", err)
  307. }
  308. }
  309. func TestHandleDeleteNode_AllowDeleteProvisioningNodeWithoutClusterRef(t *testing.T) {
  310. scheme := runtime.NewScheme()
  311. utilruntime.Must(corev1.AddToScheme(scheme))
  312. utilruntime.Must(orcv1.AddToScheme(scheme))
  313. nodeInv := &orcv1.NodeInventory{
  314. ObjectMeta: metav1.ObjectMeta{Name: "node-24"},
  315. Spec: orcv1.NodeInventorySpec{Address: "1.2.3.6"},
  316. Status: orcv1.NodeInventoryStatus{
  317. Phase: orcv1.NodePhaseProvisioning,
  318. LastError: "dial tcp 1.2.3.6:22: i/o timeout",
  319. },
  320. }
  321. op := &orcv1.ClusterOperation{
  322. Spec: orcv1.ClusterOperationSpec{
  323. DeleteNode: "node-24",
  324. },
  325. }
  326. fakeClient := ctrlclientfake.NewClientBuilder().WithScheme(scheme).WithObjects(nodeInv).Build()
  327. r := &ClusterOperationReconciler{Client: fakeClient, Scheme: scheme}
  328. done, _, _, _, err := r.handleDeleteNode(context.Background(), op)
  329. if err != nil {
  330. t.Fatalf("handleDeleteNode returned error: %v", err)
  331. }
  332. if !done {
  333. t.Fatalf("expected delete operation completed")
  334. }
  335. var invAfter orcv1.NodeInventory
  336. if err := fakeClient.Get(context.Background(), types.NamespacedName{Name: "node-24"}, &invAfter); !apierrors.IsNotFound(err) {
  337. t.Fatalf("expected node inventory deleted, get err=%v", err)
  338. }
  339. }
  340. func TestHandleAddNode_FailedPhaseShouldKeepRetrying(t *testing.T) {
  341. scheme := runtime.NewScheme()
  342. utilruntime.Must(corev1.AddToScheme(scheme))
  343. utilruntime.Must(orcv1.AddToScheme(scheme))
  344. nodeInv := &orcv1.NodeInventory{
  345. ObjectMeta: metav1.ObjectMeta{Name: "node-30"},
  346. Status: orcv1.NodeInventoryStatus{
  347. Phase: orcv1.NodePhaseFailed,
  348. LastError: "run join command: Process exited with status 1",
  349. },
  350. }
  351. op := &orcv1.ClusterOperation{
  352. Spec: orcv1.ClusterOperationSpec{
  353. AddNode: &orcv1.AddNodeSpec{
  354. NodeName: "node-30",
  355. Address: "10.0.0.30",
  356. },
  357. },
  358. }
  359. fakeClient := ctrlclientfake.NewClientBuilder().WithScheme(scheme).WithObjects(nodeInv).Build()
  360. r := &ClusterOperationReconciler{Client: fakeClient, Scheme: scheme}
  361. done, requeueAfter, resourceRef, message, err := r.handleAddNode(context.Background(), op)
  362. if err != nil {
  363. t.Fatalf("handleAddNode returned unexpected error: %v", err)
  364. }
  365. if done {
  366. t.Fatalf("failed phase should keep retrying, not done")
  367. }
  368. if resourceRef != "node-30" {
  369. t.Fatalf("unexpected resourceRef: %s", resourceRef)
  370. }
  371. if requeueAfter <= 0 {
  372. t.Fatalf("expected positive requeueAfter for retry")
  373. }
  374. if !strings.Contains(strings.ToLower(message), "retry") {
  375. t.Fatalf("expected retry message, got: %s", message)
  376. }
  377. }
  378. func TestTryDeleteOwnedNamespace_DeletesOwnedNamespace(t *testing.T) {
  379. scheme := runtime.NewScheme()
  380. utilruntime.Must(corev1.AddToScheme(scheme))
  381. utilruntime.Must(orcv1.AddToScheme(scheme))
  382. ns := &corev1.Namespace{
  383. ObjectMeta: metav1.ObjectMeta{
  384. Name: "starrocks-sr-9",
  385. Labels: map[string]string{
  386. util.LabelManagedBy: util.ManagedByValue,
  387. util.LabelNamespaceAutoDelete: "true",
  388. util.LabelNamespaceOwnerKind: "starrocks",
  389. util.LabelNamespaceOwnerName: "sr-9",
  390. },
  391. },
  392. }
  393. fakeClient := ctrlclientfake.NewClientBuilder().WithScheme(scheme).WithObjects(ns).Build()
  394. r := &ClusterOperationReconciler{Client: fakeClient, Scheme: scheme}
  395. message := r.tryDeleteOwnedNamespace(context.Background(), "starrocks-sr-9", "starrocks", "sr-9")
  396. if !strings.Contains(strings.ToLower(message), "deletion requested") {
  397. t.Fatalf("expected namespace deletion request message, got: %s", message)
  398. }
  399. var nsAfter corev1.Namespace
  400. if err := fakeClient.Get(context.Background(), types.NamespacedName{Name: "starrocks-sr-9"}, &nsAfter); !apierrors.IsNotFound(err) {
  401. t.Fatalf("expected namespace deleted, get err=%v", err)
  402. }
  403. }
  404. func TestTryDeleteOwnedNamespace_SkipsWhenOwnerMismatch(t *testing.T) {
  405. scheme := runtime.NewScheme()
  406. utilruntime.Must(corev1.AddToScheme(scheme))
  407. utilruntime.Must(orcv1.AddToScheme(scheme))
  408. ns := &corev1.Namespace{
  409. ObjectMeta: metav1.ObjectMeta{
  410. Name: "starrocks-sr-10",
  411. Labels: map[string]string{
  412. util.LabelManagedBy: util.ManagedByValue,
  413. util.LabelNamespaceAutoDelete: "true",
  414. util.LabelNamespaceOwnerKind: "starrocks",
  415. util.LabelNamespaceOwnerName: "sr-11",
  416. },
  417. },
  418. }
  419. fakeClient := ctrlclientfake.NewClientBuilder().WithScheme(scheme).WithObjects(ns).Build()
  420. r := &ClusterOperationReconciler{Client: fakeClient, Scheme: scheme}
  421. message := r.tryDeleteOwnedNamespace(context.Background(), "starrocks-sr-10", "starrocks", "sr-10")
  422. if !strings.Contains(strings.ToLower(message), "owner mismatch") {
  423. t.Fatalf("expected owner mismatch message, got: %s", message)
  424. }
  425. var nsAfter corev1.Namespace
  426. if err := fakeClient.Get(context.Background(), types.NamespacedName{Name: "starrocks-sr-10"}, &nsAfter); err != nil {
  427. t.Fatalf("expected namespace remains, get err=%v", err)
  428. }
  429. }