clusteroperation_controller.go 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804
  1. package controller
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "sort"
  7. "strings"
  8. "time"
  9. corev1 "k8s.io/api/core/v1"
  10. apierrors "k8s.io/apimachinery/pkg/api/errors"
  11. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  12. "k8s.io/apimachinery/pkg/runtime"
  13. "k8s.io/apimachinery/pkg/types"
  14. ctrl "sigs.k8s.io/controller-runtime"
  15. "sigs.k8s.io/controller-runtime/pkg/client"
  16. "sigs.k8s.io/controller-runtime/pkg/log"
  17. orcv1 "github.com/cyrus/spark-orchestrator/api/v1alpha1"
  18. "github.com/cyrus/spark-orchestrator/internal/k8s"
  19. "github.com/cyrus/spark-orchestrator/internal/util"
  20. )
  21. type ClusterOperationReconciler struct {
  22. client.Client
  23. Scheme *runtime.Scheme
  24. }
  25. const (
  26. sparkNamespaceOwnerKind = "spark"
  27. starRocksNamespaceOwnerKind = "starrocks"
  28. )
  29. func (r *ClusterOperationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
  30. logger := log.FromContext(ctx).WithValues("clusterOperation", req.Name)
  31. var op orcv1.ClusterOperation
  32. if err := r.Get(ctx, req.NamespacedName, &op); err != nil {
  33. return ctrl.Result{}, client.IgnoreNotFound(err)
  34. }
  35. if op.Status.Phase == orcv1.OperationPhaseSucceeded || op.Status.Phase == orcv1.OperationPhaseFailed {
  36. return ctrl.Result{}, nil
  37. }
  38. if op.Status.Phase == "" || op.Status.Phase == orcv1.OperationPhasePending {
  39. now := metav1.Now()
  40. op.Status.Phase = orcv1.OperationPhaseRunning
  41. op.Status.StartedAt = &now
  42. op.Status.Step = "Started"
  43. op.Status.Message = "Operation accepted"
  44. setCondition(&op.Status.Conditions, orcv1.NewCondition(orcv1.ConditionRunning, "Started", "Operation is running", metav1.ConditionTrue, op.Generation))
  45. if err := r.Status().Update(ctx, &op); err != nil {
  46. return ctrl.Result{}, err
  47. }
  48. }
  49. done, requeueAfter, resourceRef, message, err := r.handleOperation(ctx, &op)
  50. if err != nil {
  51. logger.Error(err, "operation execution failed")
  52. now := metav1.Now()
  53. op.Status.Phase = orcv1.OperationPhaseFailed
  54. op.Status.Message = err.Error()
  55. op.Status.Step = "Failed"
  56. op.Status.CompletedAt = &now
  57. op.Status.RetryCount++
  58. setCondition(&op.Status.Conditions, orcv1.NewCondition(orcv1.ConditionFailed, "ExecutionFailed", err.Error(), metav1.ConditionTrue, op.Generation))
  59. _ = r.Status().Update(ctx, &op)
  60. return ctrl.Result{}, nil
  61. }
  62. if resourceRef != "" {
  63. op.Status.ResourceRef = resourceRef
  64. }
  65. if message != "" {
  66. op.Status.Message = message
  67. }
  68. if done {
  69. now := metav1.Now()
  70. op.Status.Phase = orcv1.OperationPhaseSucceeded
  71. op.Status.Step = "Completed"
  72. op.Status.CompletedAt = &now
  73. setCondition(&op.Status.Conditions, orcv1.NewCondition(orcv1.ConditionReady, "Completed", "Operation completed successfully", metav1.ConditionTrue, op.Generation))
  74. if err := r.Status().Update(ctx, &op); err != nil {
  75. return ctrl.Result{}, err
  76. }
  77. return ctrl.Result{}, nil
  78. }
  79. op.Status.Step = "Running"
  80. if err := r.Status().Update(ctx, &op); err != nil {
  81. return ctrl.Result{}, err
  82. }
  83. if requeueAfter <= 0 {
  84. requeueAfter = 5 * time.Second
  85. }
  86. return ctrl.Result{RequeueAfter: requeueAfter}, nil
  87. }
  88. func (r *ClusterOperationReconciler) handleOperation(ctx context.Context, op *orcv1.ClusterOperation) (bool, time.Duration, string, string, error) {
  89. switch op.Spec.Type {
  90. case orcv1.OperationAddNode:
  91. return r.handleAddNode(ctx, op)
  92. case orcv1.OperationDeleteNode:
  93. return r.handleDeleteNode(ctx, op)
  94. case orcv1.OperationCreateCluster:
  95. return r.handleCreateCluster(ctx, op)
  96. case orcv1.OperationScaleOut:
  97. return r.handleScaleOut(ctx, op)
  98. case orcv1.OperationScaleIn:
  99. return r.handleScaleIn(ctx, op)
  100. case orcv1.OperationReleaseCluster:
  101. return r.handleReleaseCluster(ctx, op)
  102. case orcv1.OperationSubmitSparkJob:
  103. return r.handleSubmitSparkJob(ctx, op)
  104. case orcv1.OperationSubmitStarRocksJob:
  105. return r.handleSubmitStarRocksJob(ctx, op)
  106. case orcv1.OperationCreateStarRocksCluster:
  107. return r.handleCreateStarRocksCluster(ctx, op)
  108. case orcv1.OperationScaleOutStarRocksCluster:
  109. return r.handleScaleOutStarRocksCluster(ctx, op)
  110. case orcv1.OperationScaleInStarRocksCluster:
  111. return r.handleScaleInStarRocksCluster(ctx, op)
  112. case orcv1.OperationReleaseStarRocksCluster:
  113. return r.handleReleaseStarRocksCluster(ctx, op)
  114. default:
  115. return false, 0, "", "", fmt.Errorf("unsupported operation type %s", op.Spec.Type)
  116. }
  117. }
  118. func (r *ClusterOperationReconciler) handleAddNode(ctx context.Context, op *orcv1.ClusterOperation) (bool, time.Duration, string, string, error) {
  119. if op.Spec.AddNode == nil {
  120. return false, 0, "", "", fmt.Errorf("addNode payload is required")
  121. }
  122. nodeName := op.Spec.AddNode.NodeName
  123. if nodeName == "" {
  124. return false, 0, "", "", fmt.Errorf("addNode.nodeName is required")
  125. }
  126. var nodeInv orcv1.NodeInventory
  127. err := r.Get(ctx, types.NamespacedName{Name: nodeName}, &nodeInv)
  128. if apierrors.IsNotFound(err) {
  129. nodeInv = orcv1.NodeInventory{
  130. ObjectMeta: metav1.ObjectMeta{
  131. Name: nodeName,
  132. Labels: map[string]string{
  133. util.LabelManagedBy: util.ManagedByValue,
  134. },
  135. },
  136. Spec: orcv1.NodeInventorySpec{
  137. Address: op.Spec.AddNode.Address,
  138. Capacity: op.Spec.AddNode.Capacity,
  139. SSHSecretRef: op.Spec.AddNode.SSHSecretRef,
  140. Bootstrap: orcv1.BootstrapSpec{
  141. JoinCommandOverride: op.Spec.AddNode.JoinCommandOverride,
  142. },
  143. },
  144. }
  145. if err := r.Create(ctx, &nodeInv); err != nil {
  146. return false, 0, "", "", err
  147. }
  148. return false, 3 * time.Second, nodeName, "NodeInventory created, waiting for onboarding", nil
  149. }
  150. if err != nil {
  151. return false, 0, "", "", err
  152. }
  153. switch nodeInv.Status.Phase {
  154. case orcv1.NodePhaseIdle:
  155. return true, 0, nodeName, "Node is idle and ready", nil
  156. case orcv1.NodePhaseFailed:
  157. if nodeInv.Status.LastError != "" {
  158. return false, 8 * time.Second, nodeName, fmt.Sprintf("Node onboarding retrying: %s", nodeInv.Status.LastError), nil
  159. }
  160. return false, 8 * time.Second, nodeName, "Node onboarding retrying", nil
  161. default:
  162. return false, 5 * time.Second, nodeName, fmt.Sprintf("Node phase=%s", nodeInv.Status.Phase), nil
  163. }
  164. }
  165. func (r *ClusterOperationReconciler) handleDeleteNode(ctx context.Context, op *orcv1.ClusterOperation) (bool, time.Duration, string, string, error) {
  166. nodeID := op.Spec.DeleteNode
  167. if nodeID == "" {
  168. return false, 0, "", "", fmt.Errorf("deleteNode is required")
  169. }
  170. var nodeInv orcv1.NodeInventory
  171. err := r.Get(ctx, types.NamespacedName{Name: nodeID}, &nodeInv)
  172. if apierrors.IsNotFound(err) {
  173. return true, 0, nodeID, "Node already removed", nil
  174. }
  175. if err != nil {
  176. return false, 0, "", "", err
  177. }
  178. if err := validateNodeDeletable(&nodeInv); err != nil {
  179. return false, 0, nodeID, "", err
  180. }
  181. nodeName := nodeInv.Status.KubernetesNodeName
  182. if nodeName == "" {
  183. resolvedNodeName, err := k8s.FindNodeByInternalIP(ctx, r.Client, nodeInv.Spec.Address)
  184. if err == nil {
  185. nodeName = resolvedNodeName
  186. } else if !apierrors.IsNotFound(err) {
  187. return false, 0, "", "", err
  188. }
  189. }
  190. if nodeName != "" {
  191. if err := k8s.ClearNodeIsolation(ctx, r.Client, nodeName); err != nil {
  192. return false, 0, "", "", err
  193. }
  194. if err := k8s.DeleteNodeIfExists(ctx, r.Client, nodeName); err != nil {
  195. return false, 0, "", "", err
  196. }
  197. }
  198. if err := r.Delete(ctx, &nodeInv); err != nil {
  199. return false, 0, "", "", err
  200. }
  201. return true, 0, nodeID, "Node removed from idle pool and cluster", nil
  202. }
  203. func validateNodeDeletable(nodeInv *orcv1.NodeInventory) error {
  204. if nodeInv == nil {
  205. return fmt.Errorf("node inventory is empty")
  206. }
  207. phase := nodeInv.Status.Phase
  208. if phase == orcv1.NodePhaseIdle {
  209. return nil
  210. }
  211. if nodeInv.Status.ClusterRef != "" {
  212. return fmt.Errorf("node %s is allocated to cluster %s, current phase=%s",
  213. nodeInv.Name,
  214. nodeInv.Status.ClusterRef,
  215. phase)
  216. }
  217. switch phase {
  218. case orcv1.NodePhaseNew, orcv1.NodePhaseProvisioning, orcv1.NodePhaseFailed:
  219. return nil
  220. default:
  221. return fmt.Errorf("node %s is not deletable, current phase=%s", nodeInv.Name, phase)
  222. }
  223. }
  224. func (r *ClusterOperationReconciler) handleCreateCluster(ctx context.Context, op *orcv1.ClusterOperation) (bool, time.Duration, string, string, error) {
  225. if op.Spec.Create == nil {
  226. return false, 0, "", "", fmt.Errorf("create payload is required")
  227. }
  228. create := op.Spec.Create
  229. clusterName := create.ClusterName
  230. if clusterName == "" {
  231. return false, 0, "", "", fmt.Errorf("create.clusterName is required")
  232. }
  233. var cluster orcv1.SparkVirtualCluster
  234. err := r.Get(ctx, types.NamespacedName{Name: clusterName}, &cluster)
  235. if apierrors.IsNotFound(err) {
  236. cluster = orcv1.SparkVirtualCluster{
  237. ObjectMeta: metav1.ObjectMeta{
  238. Name: clusterName,
  239. Labels: map[string]string{
  240. util.LabelManagedBy: util.ManagedByValue,
  241. },
  242. },
  243. Spec: orcv1.SparkVirtualClusterSpec{
  244. Namespace: create.Namespace,
  245. NodeRefs: create.NodeIDs,
  246. SparkConfig: create.SparkConfig,
  247. DesiredState: orcv1.VirtualClusterDesiredActive,
  248. ScaleInPolicy: orcv1.ScaleInPolicyBlockIfRunning,
  249. },
  250. }
  251. if err := r.Create(ctx, &cluster); err != nil {
  252. return false, 0, "", "", err
  253. }
  254. return false, 4 * time.Second, clusterName, "Cluster resource created", nil
  255. }
  256. if err != nil {
  257. return false, 0, "", "", err
  258. }
  259. if cluster.Status.Phase == orcv1.SparkClusterPhaseFailed {
  260. return false, 0, clusterName, "", fmt.Errorf("cluster failed: %s", cluster.Status.LastError)
  261. }
  262. if cluster.Status.Phase == orcv1.SparkClusterPhaseReady {
  263. return true, 0, clusterName, "Cluster is ready", nil
  264. }
  265. return false, 5 * time.Second, clusterName, fmt.Sprintf("Cluster phase=%s", cluster.Status.Phase), nil
  266. }
  267. func (r *ClusterOperationReconciler) handleScaleOut(ctx context.Context, op *orcv1.ClusterOperation) (bool, time.Duration, string, string, error) {
  268. if op.Spec.Resize == nil {
  269. return false, 0, "", "", fmt.Errorf("resize payload is required")
  270. }
  271. clusterName := op.Spec.Resize.ClusterName
  272. if clusterName == "" {
  273. return false, 0, "", "", fmt.Errorf("resize.clusterName is required")
  274. }
  275. var cluster orcv1.SparkVirtualCluster
  276. if err := r.Get(ctx, types.NamespacedName{Name: clusterName}, &cluster); err != nil {
  277. return false, 0, "", "", err
  278. }
  279. desired := normalizeNodeList(append(cluster.Spec.NodeRefs, op.Spec.Resize.NodeIDs...))
  280. if !equalNodeLists(normalizeNodeList(cluster.Spec.NodeRefs), desired) {
  281. cluster.Spec.NodeRefs = desired
  282. cluster.Spec.DesiredState = orcv1.VirtualClusterDesiredActive
  283. if err := r.Update(ctx, &cluster); err != nil {
  284. return false, 0, "", "", err
  285. }
  286. return false, 4 * time.Second, clusterName, "Scale-out request applied", nil
  287. }
  288. if cluster.Status.Phase == orcv1.SparkClusterPhaseReady {
  289. for _, nodeID := range op.Spec.Resize.NodeIDs {
  290. if !util.ContainsString(cluster.Status.AssignedNodes, nodeID) {
  291. return false, 4 * time.Second, clusterName, "Waiting nodes to be assigned", nil
  292. }
  293. }
  294. return true, 0, clusterName, "Scale-out completed", nil
  295. }
  296. return false, 5 * time.Second, clusterName, fmt.Sprintf("Cluster phase=%s", cluster.Status.Phase), nil
  297. }
  298. func (r *ClusterOperationReconciler) handleScaleIn(ctx context.Context, op *orcv1.ClusterOperation) (bool, time.Duration, string, string, error) {
  299. if op.Spec.Resize == nil {
  300. return false, 0, "", "", fmt.Errorf("resize payload is required")
  301. }
  302. clusterName := op.Spec.Resize.ClusterName
  303. if clusterName == "" {
  304. return false, 0, "", "", fmt.Errorf("resize.clusterName is required")
  305. }
  306. var cluster orcv1.SparkVirtualCluster
  307. if err := r.Get(ctx, types.NamespacedName{Name: clusterName}, &cluster); err != nil {
  308. return false, 0, "", "", err
  309. }
  310. if err := r.ensureNoRunningPodsOnNodes(ctx, &cluster, op.Spec.Resize.NodeIDs); err != nil {
  311. return false, 0, clusterName, "", err
  312. }
  313. removeSet := map[string]struct{}{}
  314. for _, node := range op.Spec.Resize.NodeIDs {
  315. removeSet[node] = struct{}{}
  316. }
  317. desired := util.RemoveStrings(normalizeNodeList(cluster.Spec.NodeRefs), removeSet)
  318. if !equalNodeLists(normalizeNodeList(cluster.Spec.NodeRefs), desired) {
  319. cluster.Spec.NodeRefs = desired
  320. if err := r.Update(ctx, &cluster); err != nil {
  321. return false, 0, "", "", err
  322. }
  323. return false, 4 * time.Second, clusterName, "Scale-in request applied", nil
  324. }
  325. if cluster.Status.Phase == orcv1.SparkClusterPhaseReady {
  326. for _, nodeID := range op.Spec.Resize.NodeIDs {
  327. if util.ContainsString(cluster.Status.AssignedNodes, nodeID) {
  328. return false, 4 * time.Second, clusterName, "Waiting nodes to be released", nil
  329. }
  330. }
  331. return true, 0, clusterName, "Scale-in completed", nil
  332. }
  333. return false, 5 * time.Second, clusterName, fmt.Sprintf("Cluster phase=%s", cluster.Status.Phase), nil
  334. }
  335. func (r *ClusterOperationReconciler) handleReleaseCluster(ctx context.Context, op *orcv1.ClusterOperation) (bool, time.Duration, string, string, error) {
  336. if op.Spec.Release == nil {
  337. return false, 0, "", "", fmt.Errorf("release payload is required")
  338. }
  339. clusterName := op.Spec.Release.ClusterName
  340. if clusterName == "" {
  341. return false, 0, "", "", fmt.Errorf("release.clusterName is required")
  342. }
  343. var cluster orcv1.SparkVirtualCluster
  344. err := r.Get(ctx, types.NamespacedName{Name: clusterName}, &cluster)
  345. if apierrors.IsNotFound(err) {
  346. ns := util.SafeName("spark", clusterName)
  347. cleanupMessage := r.tryDeleteOwnedNamespace(ctx, ns, sparkNamespaceOwnerKind, clusterName)
  348. if cleanupMessage != "" {
  349. return true, 0, clusterName, "Cluster already released; " + cleanupMessage, nil
  350. }
  351. return true, 0, clusterName, "Cluster already released", nil
  352. }
  353. if err != nil {
  354. return false, 0, "", "", err
  355. }
  356. if err := r.ensureNoRunningPodsOnNodes(ctx, &cluster, normalizeNodeList(cluster.Status.AssignedNodes)); err != nil {
  357. return false, 0, clusterName, "", err
  358. }
  359. // Clean up SparkApplications before releasing the cluster
  360. ns := cluster.Status.Namespace
  361. if ns == "" {
  362. ns = cluster.Spec.Namespace
  363. }
  364. if ns != "" {
  365. if deleted, err := k8s.DeleteSparkApplicationsByCluster(ctx, r.Client, ns, clusterName); err != nil {
  366. return false, 0, clusterName, "", fmt.Errorf("failed to delete spark applications: %w", err)
  367. } else if deleted > 0 {
  368. return false, 2 * time.Second, clusterName, fmt.Sprintf("Deleted %d SparkApplications, proceeding with release", deleted), nil
  369. }
  370. }
  371. if cluster.Spec.DesiredState != orcv1.VirtualClusterDesiredReleased {
  372. cluster.Spec.DesiredState = orcv1.VirtualClusterDesiredReleased
  373. if err := r.Update(ctx, &cluster); err != nil {
  374. return false, 0, "", "", err
  375. }
  376. return false, 4 * time.Second, clusterName, "Release requested", nil
  377. }
  378. if cluster.Status.Phase != orcv1.SparkClusterPhaseReleased {
  379. return false, 4 * time.Second, clusterName, fmt.Sprintf("Cluster phase=%s", cluster.Status.Phase), nil
  380. }
  381. if err := r.Delete(ctx, &cluster); err != nil && !apierrors.IsNotFound(err) {
  382. return false, 0, "", "", err
  383. }
  384. cleanupMessage := r.tryDeleteOwnedNamespace(ctx, ns, sparkNamespaceOwnerKind, clusterName)
  385. if cleanupMessage != "" {
  386. return true, 0, clusterName, "Cluster released; " + cleanupMessage, nil
  387. }
  388. return true, 0, clusterName, "Cluster released", nil
  389. }
  390. func (r *ClusterOperationReconciler) handleSubmitSparkJob(ctx context.Context, op *orcv1.ClusterOperation) (bool, time.Duration, string, string, error) {
  391. if op.Spec.SubmitJob == nil {
  392. return false, 0, "", "", fmt.Errorf("submitJob payload is required")
  393. }
  394. clusterName := op.Spec.SubmitJob.ClusterName
  395. if clusterName == "" {
  396. return false, 0, "", "", fmt.Errorf("submitJob.clusterName is required")
  397. }
  398. var cluster orcv1.SparkVirtualCluster
  399. if err := r.Get(ctx, types.NamespacedName{Name: clusterName}, &cluster); err != nil {
  400. return false, 0, "", "", err
  401. }
  402. if cluster.Status.Phase != orcv1.SparkClusterPhaseReady {
  403. return false, 5 * time.Second, clusterName, fmt.Sprintf("Cluster phase=%s", cluster.Status.Phase), nil
  404. }
  405. resource := op.Status.ResourceRef
  406. if resource == "" {
  407. appName := util.SafeName("spark-job", op.Name)
  408. if err := k8s.CreateSparkApplication(ctx, r.Client, &cluster, appName, op.Spec.SubmitJob.SparkApplicationSpec); err != nil {
  409. if strings.Contains(err.Error(), "already exists") {
  410. resource = fmt.Sprintf("%s/%s", cluster.Status.Namespace, appName)
  411. return true, 0, resource, "SparkApplication already exists", nil
  412. }
  413. return false, 0, "", "", err
  414. }
  415. resource = fmt.Sprintf("%s/%s", cluster.Status.Namespace, appName)
  416. return true, 0, resource, "SparkApplication submitted", nil
  417. }
  418. return true, 0, resource, "SparkApplication submitted", nil
  419. }
  420. func (r *ClusterOperationReconciler) handleSubmitStarRocksJob(ctx context.Context, op *orcv1.ClusterOperation) (bool, time.Duration, string, string, error) {
  421. if op.Spec.SubmitStarRocks == nil {
  422. return false, 0, "", "", fmt.Errorf("submitStarRocks payload is required")
  423. }
  424. clusterName := op.Spec.SubmitStarRocks.ClusterName
  425. if clusterName == "" {
  426. return false, 0, "", "", fmt.Errorf("submitStarRocks.clusterName is required")
  427. }
  428. var cluster orcv1.StarRocksVirtualCluster
  429. if err := r.Get(ctx, types.NamespacedName{Name: clusterName}, &cluster); err != nil {
  430. return false, 0, "", "", err
  431. }
  432. if cluster.Status.Phase != orcv1.StarRocksClusterPhaseReady {
  433. return false, 5 * time.Second, clusterName, fmt.Sprintf("StarRocks cluster phase=%s", cluster.Status.Phase), nil
  434. }
  435. ns := cluster.Status.Namespace
  436. if ns == "" {
  437. ns = cluster.Spec.Namespace
  438. }
  439. if ns == "" {
  440. ns = util.SafeName("starrocks", cluster.Name)
  441. }
  442. resource := op.Status.ResourceRef
  443. if resource == "" {
  444. taskName := util.SafeName("starrocks-task", op.Name)
  445. if err := k8s.CreateStarRocksTask(ctx, r.Client, &cluster, taskName, op.Spec.SubmitStarRocks.JobSpec); err != nil {
  446. if strings.Contains(err.Error(), "already exists") {
  447. resource = fmt.Sprintf("%s/%s", ns, taskName)
  448. return true, 0, resource, "StarRocks task already exists", nil
  449. }
  450. return false, 0, "", "", err
  451. }
  452. resource = fmt.Sprintf("%s/%s", ns, taskName)
  453. return true, 0, resource, "StarRocks task submitted", nil
  454. }
  455. return true, 0, resource, "StarRocks task submitted", nil
  456. }
  457. func (r *ClusterOperationReconciler) handleCreateStarRocksCluster(ctx context.Context, op *orcv1.ClusterOperation) (bool, time.Duration, string, string, error) {
  458. if op.Spec.CreateStarRocks == nil {
  459. return false, 0, "", "", fmt.Errorf("createStarRocks payload is required")
  460. }
  461. create := op.Spec.CreateStarRocks
  462. clusterName := create.ClusterName
  463. if clusterName == "" {
  464. return false, 0, "", "", fmt.Errorf("createStarRocks.clusterName is required")
  465. }
  466. if len(create.FeNodeIDs) == 0 {
  467. return false, 0, "", "", fmt.Errorf("createStarRocks.feNodeIds is required")
  468. }
  469. if len(create.BeNodeIDs) == 0 {
  470. return false, 0, "", "", fmt.Errorf("createStarRocks.beNodeIds is required")
  471. }
  472. var cluster orcv1.StarRocksVirtualCluster
  473. err := r.Get(ctx, types.NamespacedName{Name: clusterName}, &cluster)
  474. if apierrors.IsNotFound(err) {
  475. cluster = orcv1.StarRocksVirtualCluster{
  476. ObjectMeta: metav1.ObjectMeta{
  477. Name: clusterName,
  478. Labels: map[string]string{
  479. util.LabelManagedBy: util.ManagedByValue,
  480. },
  481. },
  482. Spec: orcv1.StarRocksVirtualClusterSpec{
  483. Namespace: create.Namespace,
  484. FeNodeRefs: create.FeNodeIDs,
  485. BeNodeRefs: create.BeNodeIDs,
  486. DesiredState: orcv1.VirtualClusterDesiredActive,
  487. ScaleInPolicy: orcv1.ScaleInPolicyBlockIfRunning,
  488. StarRocksConfig: create.StarRocksConfig,
  489. },
  490. }
  491. if err := r.Create(ctx, &cluster); err != nil {
  492. return false, 0, "", "", err
  493. }
  494. return false, 4 * time.Second, clusterName, "StarRocks cluster resource created", nil
  495. }
  496. if err != nil {
  497. return false, 0, "", "", err
  498. }
  499. if cluster.Status.Phase == orcv1.StarRocksClusterPhaseFailed {
  500. return false, 0, clusterName, "", fmt.Errorf("starrocks cluster failed: %s", cluster.Status.LastError)
  501. }
  502. if cluster.Status.Phase == orcv1.StarRocksClusterPhaseReady {
  503. return true, 0, clusterName, "StarRocks cluster is ready", nil
  504. }
  505. return false, 5 * time.Second, clusterName, fmt.Sprintf("StarRocks cluster phase=%s", cluster.Status.Phase), nil
  506. }
  507. func (r *ClusterOperationReconciler) handleScaleOutStarRocksCluster(ctx context.Context, op *orcv1.ClusterOperation) (bool, time.Duration, string, string, error) {
  508. if op.Spec.ResizeStarRocks == nil {
  509. return false, 0, "", "", fmt.Errorf("resizeStarRocks payload is required")
  510. }
  511. clusterName := op.Spec.ResizeStarRocks.ClusterName
  512. if clusterName == "" {
  513. return false, 0, "", "", fmt.Errorf("resizeStarRocks.clusterName is required")
  514. }
  515. var cluster orcv1.StarRocksVirtualCluster
  516. if err := r.Get(ctx, types.NamespacedName{Name: clusterName}, &cluster); err != nil {
  517. return false, 0, "", "", err
  518. }
  519. if cluster.Status.Phase == orcv1.StarRocksClusterPhaseFailed {
  520. return false, 0, clusterName, "", fmt.Errorf("starrocks cluster failed: %s", cluster.Status.LastError)
  521. }
  522. desired := normalizeNodeList(append(cluster.Spec.BeNodeRefs, op.Spec.ResizeStarRocks.NodeIDs...))
  523. if !equalNodeLists(normalizeNodeList(cluster.Spec.BeNodeRefs), desired) {
  524. cluster.Spec.BeNodeRefs = desired
  525. cluster.Spec.DesiredState = orcv1.VirtualClusterDesiredActive
  526. if err := r.Update(ctx, &cluster); err != nil {
  527. return false, 0, "", "", err
  528. }
  529. return false, 4 * time.Second, clusterName, "StarRocks BE scale-out request applied", nil
  530. }
  531. if cluster.Status.Phase == orcv1.StarRocksClusterPhaseReady {
  532. for _, nodeID := range op.Spec.ResizeStarRocks.NodeIDs {
  533. if !util.ContainsString(cluster.Status.AssignedBeNodes, nodeID) {
  534. return false, 4 * time.Second, clusterName, "Waiting StarRocks BE nodes to be assigned", nil
  535. }
  536. }
  537. return true, 0, clusterName, "StarRocks BE scale-out completed", nil
  538. }
  539. return false, 5 * time.Second, clusterName, fmt.Sprintf("StarRocks cluster phase=%s", cluster.Status.Phase), nil
  540. }
  541. func (r *ClusterOperationReconciler) handleScaleInStarRocksCluster(ctx context.Context, op *orcv1.ClusterOperation) (bool, time.Duration, string, string, error) {
  542. if op.Spec.ResizeStarRocks == nil {
  543. return false, 0, "", "", fmt.Errorf("resizeStarRocks payload is required")
  544. }
  545. clusterName := op.Spec.ResizeStarRocks.ClusterName
  546. if clusterName == "" {
  547. return false, 0, "", "", fmt.Errorf("resizeStarRocks.clusterName is required")
  548. }
  549. if len(op.Spec.ResizeStarRocks.NodeIDs) == 0 {
  550. return false, 0, "", "", fmt.Errorf("resizeStarRocks.nodeIds are required")
  551. }
  552. var cluster orcv1.StarRocksVirtualCluster
  553. if err := r.Get(ctx, types.NamespacedName{Name: clusterName}, &cluster); err != nil {
  554. return false, 0, "", "", err
  555. }
  556. if cluster.Status.Phase == orcv1.StarRocksClusterPhaseFailed {
  557. return false, 0, clusterName, "", fmt.Errorf("starrocks cluster failed: %s", cluster.Status.LastError)
  558. }
  559. ns := cluster.Status.Namespace
  560. if ns == "" {
  561. ns = cluster.Spec.Namespace
  562. }
  563. if ns == "" {
  564. ns = util.SafeName("starrocks", cluster.Name)
  565. }
  566. for _, nodeID := range op.Spec.ResizeStarRocks.NodeIDs {
  567. if util.ContainsString(cluster.Spec.BeNodeRefs, nodeID) {
  568. if err := r.ensureNoRunningPodsOnNodesInNamespace(ctx, ns, []string{nodeID}); err != nil {
  569. return false, 0, clusterName, "", err
  570. }
  571. cluster.Spec.BeNodeRefs = removeNodeFromList(cluster.Spec.BeNodeRefs, nodeID)
  572. cluster.Spec.DesiredState = orcv1.VirtualClusterDesiredActive
  573. if err := r.Update(ctx, &cluster); err != nil {
  574. return false, 0, "", "", err
  575. }
  576. return false, 4 * time.Second, clusterName, fmt.Sprintf("StarRocks BE scale-in step applied for node %s", nodeID), nil
  577. }
  578. if util.ContainsString(cluster.Status.AssignedBeNodes, nodeID) {
  579. return false, 4 * time.Second, clusterName, fmt.Sprintf("Waiting StarRocks BE node to be released: %s", nodeID), nil
  580. }
  581. }
  582. if cluster.Status.Phase == orcv1.StarRocksClusterPhaseReady {
  583. return true, 0, clusterName, "StarRocks BE scale-in completed", nil
  584. }
  585. return false, 5 * time.Second, clusterName, fmt.Sprintf("StarRocks cluster phase=%s", cluster.Status.Phase), nil
  586. }
  587. func (r *ClusterOperationReconciler) handleReleaseStarRocksCluster(ctx context.Context, op *orcv1.ClusterOperation) (bool, time.Duration, string, string, error) {
  588. if op.Spec.ReleaseStarRocks == nil {
  589. return false, 0, "", "", fmt.Errorf("releaseStarRocks payload is required")
  590. }
  591. clusterName := op.Spec.ReleaseStarRocks.ClusterName
  592. if clusterName == "" {
  593. return false, 0, "", "", fmt.Errorf("releaseStarRocks.clusterName is required")
  594. }
  595. var cluster orcv1.StarRocksVirtualCluster
  596. err := r.Get(ctx, types.NamespacedName{Name: clusterName}, &cluster)
  597. if apierrors.IsNotFound(err) {
  598. ns := util.SafeName("starrocks", clusterName)
  599. cleanupMessage := r.tryDeleteOwnedNamespace(ctx, ns, starRocksNamespaceOwnerKind, clusterName)
  600. if cleanupMessage != "" {
  601. return true, 0, clusterName, "StarRocks cluster already released; " + cleanupMessage, nil
  602. }
  603. return true, 0, clusterName, "StarRocks cluster already released", nil
  604. }
  605. if err != nil {
  606. return false, 0, "", "", err
  607. }
  608. allAssigned := normalizeNodeList(append(cluster.Status.AssignedFeNodes, cluster.Status.AssignedBeNodes...))
  609. ns := cluster.Status.Namespace
  610. if ns == "" {
  611. ns = cluster.Spec.Namespace
  612. }
  613. if ns == "" {
  614. ns = util.SafeName("starrocks", cluster.Name)
  615. }
  616. if err := r.ensureNoRunningPodsOnNodesInNamespace(ctx, ns, allAssigned); err != nil {
  617. return false, 0, clusterName, "", err
  618. }
  619. if cluster.Spec.DesiredState != orcv1.VirtualClusterDesiredReleased {
  620. cluster.Spec.DesiredState = orcv1.VirtualClusterDesiredReleased
  621. if err := r.Update(ctx, &cluster); err != nil {
  622. return false, 0, "", "", err
  623. }
  624. return false, 4 * time.Second, clusterName, "StarRocks cluster release requested", nil
  625. }
  626. if cluster.Status.Phase != orcv1.StarRocksClusterPhaseReleased {
  627. return false, 4 * time.Second, clusterName, fmt.Sprintf("StarRocks cluster phase=%s", cluster.Status.Phase), nil
  628. }
  629. if err := r.Delete(ctx, &cluster); err != nil && !apierrors.IsNotFound(err) {
  630. return false, 0, "", "", err
  631. }
  632. cleanupMessage := r.tryDeleteOwnedNamespace(ctx, ns, starRocksNamespaceOwnerKind, clusterName)
  633. if cleanupMessage != "" {
  634. return true, 0, clusterName, "StarRocks cluster released; " + cleanupMessage, nil
  635. }
  636. return true, 0, clusterName, "StarRocks cluster released", nil
  637. }
  638. func (r *ClusterOperationReconciler) tryDeleteOwnedNamespace(
  639. ctx context.Context,
  640. namespace string,
  641. ownerKind string,
  642. ownerName string,
  643. ) string {
  644. if strings.TrimSpace(namespace) == "" {
  645. return ""
  646. }
  647. var ns corev1.Namespace
  648. if err := r.Get(ctx, types.NamespacedName{Name: namespace}, &ns); err != nil {
  649. if apierrors.IsNotFound(err) {
  650. return "namespace already deleted"
  651. }
  652. return fmt.Sprintf("namespace cleanup skipped: %v", err)
  653. }
  654. labels := ns.GetLabels()
  655. if labels[util.LabelManagedBy] != util.ManagedByValue {
  656. return "namespace cleanup skipped: not managed by orchestrator"
  657. }
  658. if labels[util.LabelNamespaceAutoDelete] != "true" {
  659. return "namespace cleanup skipped: auto-delete disabled"
  660. }
  661. if labels[util.LabelNamespaceOwnerKind] != ownerKind || labels[util.LabelNamespaceOwnerName] != ownerName {
  662. return "namespace cleanup skipped: namespace owner mismatch"
  663. }
  664. if ns.DeletionTimestamp != nil {
  665. return "namespace deletion in progress"
  666. }
  667. if err := r.Delete(ctx, &ns); err != nil {
  668. if apierrors.IsNotFound(err) {
  669. return "namespace already deleted"
  670. }
  671. return fmt.Sprintf("namespace cleanup skipped: %v", err)
  672. }
  673. return "namespace deletion requested"
  674. }
  675. func (r *ClusterOperationReconciler) ensureNoRunningPodsOnNodes(ctx context.Context, cluster *orcv1.SparkVirtualCluster, nodeIDs []string) error {
  676. ns := cluster.Status.Namespace
  677. if ns == "" {
  678. ns = cluster.Spec.Namespace
  679. }
  680. return r.ensureNoRunningPodsOnNodesInNamespace(ctx, ns, nodeIDs)
  681. }
  682. func (r *ClusterOperationReconciler) ensureNoRunningPodsOnNodesInNamespace(ctx context.Context, namespace string, nodeIDs []string) error {
  683. if namespace == "" || len(nodeIDs) == 0 {
  684. return nil
  685. }
  686. nodeSet := map[string]struct{}{}
  687. for _, n := range nodeIDs {
  688. nodeSet[n] = struct{}{}
  689. }
  690. k8sNames := map[string]struct{}{}
  691. for _, nodeID := range nodeIDs {
  692. var inv orcv1.NodeInventory
  693. if err := r.Get(ctx, types.NamespacedName{Name: nodeID}, &inv); err == nil && inv.Status.KubernetesNodeName != "" {
  694. k8sNames[inv.Status.KubernetesNodeName] = struct{}{}
  695. }
  696. }
  697. var pods corev1.PodList
  698. if err := r.List(ctx, &pods, client.InNamespace(namespace)); err != nil {
  699. return err
  700. }
  701. blockers := make([]string, 0)
  702. for i := range pods.Items {
  703. pod := &pods.Items[i]
  704. if pod.Spec.NodeName == "" {
  705. continue
  706. }
  707. if pod.Labels != nil && pod.Labels[util.LabelManagedBy] == util.ManagedByValue {
  708. // Skip managed infrastructure pods: prepull DaemonSets and StarRocks FE/BE pods.
  709. if pod.Labels[util.LabelComponent] == util.ComponentSparkPrePull {
  710. continue
  711. }
  712. if pod.Labels[util.LabelComponent] == util.ComponentSparkIvyWarmup {
  713. continue
  714. }
  715. if _, hasStarRocksComponent := pod.Labels[util.LabelStarRocksComponent]; hasStarRocksComponent {
  716. continue
  717. }
  718. }
  719. if _, ok := nodeSet[pod.Spec.NodeName]; !ok {
  720. if _, ok := k8sNames[pod.Spec.NodeName]; !ok {
  721. continue
  722. }
  723. }
  724. if pod.Status.Phase == corev1.PodRunning || pod.Status.Phase == corev1.PodPending {
  725. blockers = append(blockers, pod.Name)
  726. }
  727. }
  728. if len(blockers) > 0 {
  729. sort.Strings(blockers)
  730. return errors.New("block-if-running policy violated: running pods found: " + strings.Join(blockers, ","))
  731. }
  732. return nil
  733. }
  734. func removeNodeFromList(nodes []string, target string) []string {
  735. if len(nodes) == 0 {
  736. return nodes
  737. }
  738. out := make([]string, 0, len(nodes))
  739. for _, node := range nodes {
  740. if node == target {
  741. continue
  742. }
  743. out = append(out, node)
  744. }
  745. return out
  746. }
  747. func (r *ClusterOperationReconciler) SetupWithManager(mgr ctrl.Manager) error {
  748. return ctrl.NewControllerManagedBy(mgr).
  749. For(&orcv1.ClusterOperation{}).
  750. Complete(r)
  751. }