starrocksvirtualcluster_controller.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645
  1. package controller
  2. import (
  3. "context"
  4. "fmt"
  5. "reflect"
  6. "sort"
  7. "strconv"
  8. "strings"
  9. "time"
  10. apierrors "k8s.io/apimachinery/pkg/api/errors"
  11. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  12. "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
  13. "k8s.io/apimachinery/pkg/runtime"
  14. "k8s.io/apimachinery/pkg/types"
  15. ctrl "sigs.k8s.io/controller-runtime"
  16. "sigs.k8s.io/controller-runtime/pkg/client"
  17. "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
  18. "sigs.k8s.io/controller-runtime/pkg/log"
  19. orcv1 "github.com/cyrus/spark-orchestrator/api/v1alpha1"
  20. "github.com/cyrus/spark-orchestrator/internal/k8s"
  21. "github.com/cyrus/spark-orchestrator/internal/util"
  22. )
  23. const (
  24. starRocksClusterAPIVersion = "starrocks.com/v1"
  25. starRocksClusterKind = "StarRocksCluster"
  26. starRocksDefaultFeImage = "starrocks/fe-ubuntu:3.3.10"
  27. starRocksDefaultBeImage = "starrocks/be-ubuntu:3.3.10"
  28. starRocksNamespaceOwnerKindLabel = "starrocks"
  29. awsChecksumCalculationWhenNeeded = "WHEN_REQUIRED"
  30. )
  31. type StarRocksVirtualClusterReconciler struct {
  32. client.Client
  33. Scheme *runtime.Scheme
  34. PolarisBootstrap PolarisCatalogBootstrapConfig
  35. }
  36. func (r *StarRocksVirtualClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
  37. logger := log.FromContext(ctx).WithValues("starRocksVirtualCluster", req.Name)
  38. var cluster orcv1.StarRocksVirtualCluster
  39. if err := r.Get(ctx, req.NamespacedName, &cluster); err != nil {
  40. return ctrl.Result{}, client.IgnoreNotFound(err)
  41. }
  42. if cluster.Spec.DesiredState == "" {
  43. cluster.Spec.DesiredState = orcv1.VirtualClusterDesiredActive
  44. }
  45. if cluster.Spec.ScaleInPolicy == "" {
  46. cluster.Spec.ScaleInPolicy = orcv1.ScaleInPolicyBlockIfRunning
  47. }
  48. if cluster.DeletionTimestamp.IsZero() {
  49. if !controllerutil.ContainsFinalizer(&cluster, util.FinalizerStarRocksCluster) {
  50. controllerutil.AddFinalizer(&cluster, util.FinalizerStarRocksCluster)
  51. if err := r.Update(ctx, &cluster); err != nil {
  52. return ctrl.Result{}, err
  53. }
  54. }
  55. } else {
  56. if controllerutil.ContainsFinalizer(&cluster, util.FinalizerStarRocksCluster) {
  57. ns := cluster.Status.Namespace
  58. if ns == "" {
  59. ns = resolvedStarRocksNamespace(cluster.Name, cluster.Spec.Namespace)
  60. }
  61. if err := r.cleanupCluster(ctx, &cluster, ns); err != nil {
  62. return ctrl.Result{}, err
  63. }
  64. controllerutil.RemoveFinalizer(&cluster, util.FinalizerStarRocksCluster)
  65. if err := r.Update(ctx, &cluster); err != nil {
  66. return ctrl.Result{}, err
  67. }
  68. }
  69. return ctrl.Result{}, nil
  70. }
  71. ns := cluster.Spec.Namespace
  72. if ns == "" {
  73. ns = resolvedStarRocksNamespace(cluster.Name, "")
  74. if err := k8s.EnsureNamespaceOwnedByCluster(ctx, r.Client, ns, starRocksNamespaceOwnerKindLabel, cluster.Name); err != nil {
  75. return ctrl.Result{}, err
  76. }
  77. } else {
  78. if err := k8s.EnsureNamespace(ctx, r.Client, ns); err != nil {
  79. return ctrl.Result{}, err
  80. }
  81. }
  82. if cluster.Spec.DesiredState == orcv1.VirtualClusterDesiredReleased {
  83. cluster.Status.Phase = orcv1.StarRocksClusterPhaseReleasing
  84. cluster.Status.ObservedAt = metav1.Now()
  85. if err := r.Status().Update(ctx, &cluster); err != nil {
  86. return ctrl.Result{}, err
  87. }
  88. if err := r.cleanupCluster(ctx, &cluster, ns); err != nil {
  89. cluster.Status.Phase = orcv1.StarRocksClusterPhaseFailed
  90. cluster.Status.LastError = err.Error()
  91. cluster.Status.ObservedAt = metav1.Now()
  92. _ = r.Status().Update(ctx, &cluster)
  93. return ctrl.Result{}, err
  94. }
  95. cluster.Status.Phase = orcv1.StarRocksClusterPhaseReleased
  96. cluster.Status.AssignedFeNodes = nil
  97. cluster.Status.AssignedBeNodes = nil
  98. cluster.Status.LastError = ""
  99. cluster.Status.Namespace = ns
  100. cluster.Status.ObservedAt = metav1.Now()
  101. setCondition(&cluster.Status.Conditions, orcv1.NewCondition(orcv1.ConditionReady, "Released", "Cluster released and nodes are idle", metav1.ConditionFalse, cluster.Generation))
  102. if err := r.Status().Update(ctx, &cluster); err != nil {
  103. return ctrl.Result{}, err
  104. }
  105. return ctrl.Result{}, nil
  106. }
  107. targetFeNodes := normalizeNodeList(cluster.Spec.FeNodeRefs)
  108. targetBeNodes := normalizeNodeList(cluster.Spec.BeNodeRefs)
  109. if len(targetFeNodes) == 0 {
  110. return r.failCluster(ctx, &cluster, fmt.Errorf("feNodeRefs must not be empty"))
  111. }
  112. if len(targetBeNodes) == 0 {
  113. return r.failCluster(ctx, &cluster, fmt.Errorf("beNodeRefs must not be empty"))
  114. }
  115. if overlap := overlappingNodes(targetFeNodes, targetBeNodes); len(overlap) > 0 {
  116. return r.failCluster(ctx, &cluster, fmt.Errorf("fe and be nodes cannot overlap: %s", strings.Join(overlap, ",")))
  117. }
  118. previousFeNodes := normalizeNodeList(cluster.Status.AssignedFeNodes)
  119. previousBeNodes := normalizeNodeList(cluster.Status.AssignedBeNodes)
  120. previousNodes := normalizeNodeList(append(previousFeNodes, previousBeNodes...))
  121. targetNodes := normalizeNodeList(append(targetFeNodes, targetBeNodes...))
  122. toRelease := diffNodes(previousNodes, targetNodes)
  123. for _, nodeID := range toRelease {
  124. if err := r.releaseNode(ctx, cluster.Name, nodeID); err != nil {
  125. return r.failCluster(ctx, &cluster, err)
  126. }
  127. }
  128. for _, nodeID := range targetNodes {
  129. if err := r.allocateNode(ctx, cluster.Name, nodeID); err != nil {
  130. logger.Error(err, "allocate node failed", "nodeID", nodeID)
  131. return r.failCluster(ctx, &cluster, err)
  132. }
  133. }
  134. feInventories, err := r.loadNodeInventories(ctx, targetFeNodes)
  135. if err != nil {
  136. return r.failCluster(ctx, &cluster, err)
  137. }
  138. beInventories, err := r.loadNodeInventories(ctx, targetBeNodes)
  139. if err != nil {
  140. return r.failCluster(ctx, &cluster, err)
  141. }
  142. cfg, err := resolveStarRocksConfig(&cluster, feInventories, beInventories)
  143. if err != nil {
  144. return r.failCluster(ctx, &cluster, err)
  145. }
  146. feKubeNodes, err := inventoryNodeNames(feInventories)
  147. if err != nil {
  148. return r.failCluster(ctx, &cluster, err)
  149. }
  150. beKubeNodes, err := inventoryNodeNames(beInventories)
  151. if err != nil {
  152. return r.failCluster(ctx, &cluster, err)
  153. }
  154. if err := r.applyStarRocksClusterResource(ctx, cluster.Name, ns, feKubeNodes, beKubeNodes, cfg); err != nil {
  155. return r.failCluster(ctx, &cluster, err)
  156. }
  157. externalPhase, externalReason, err := r.getStarRocksClusterStatus(ctx, cluster.Name, ns)
  158. if err != nil && !apierrors.IsNotFound(err) {
  159. return r.failCluster(ctx, &cluster, err)
  160. }
  161. cluster.Status.Namespace = ns
  162. cluster.Status.AssignedFeNodes = targetFeNodes
  163. cluster.Status.AssignedBeNodes = targetBeNodes
  164. cluster.Status.LastError = ""
  165. cluster.Status.ObservedAt = metav1.Now()
  166. switch strings.ToLower(externalPhase) {
  167. case "running":
  168. bootstrapReady, bootstrapMessage, bootstrapErr := r.ensurePolarisCatalogBootstrap(ctx, &cluster, ns)
  169. if bootstrapErr != nil {
  170. return r.failCluster(ctx, &cluster, bootstrapErr)
  171. }
  172. if bootstrapReady == false {
  173. cluster.Status.Phase = orcv1.StarRocksClusterPhaseCreating
  174. if strings.TrimSpace(bootstrapMessage) == "" {
  175. bootstrapMessage = "Waiting polaris catalog bootstrap"
  176. }
  177. setCondition(&cluster.Status.Conditions, orcv1.NewCondition(orcv1.ConditionRunning, "BootstrapPolarisCatalog", bootstrapMessage, metav1.ConditionTrue, cluster.Generation))
  178. break
  179. }
  180. cluster.Status.Phase = orcv1.StarRocksClusterPhaseReady
  181. setCondition(&cluster.Status.Conditions, orcv1.NewCondition(orcv1.ConditionReady, "ClusterReady", "StarRocks cluster is running", metav1.ConditionTrue, cluster.Generation))
  182. case "failed":
  183. cluster.Status.Phase = orcv1.StarRocksClusterPhaseFailed
  184. cluster.Status.LastError = externalReason
  185. setCondition(&cluster.Status.Conditions, orcv1.NewCondition(orcv1.ConditionFailed, "ExternalFailed", externalReason, metav1.ConditionTrue, cluster.Generation))
  186. default:
  187. if len(toRelease) > 0 ||
  188. !equalNodeLists(previousFeNodes, targetFeNodes) ||
  189. !equalNodeLists(previousBeNodes, targetBeNodes) {
  190. cluster.Status.Phase = orcv1.StarRocksClusterPhaseScaling
  191. } else {
  192. cluster.Status.Phase = orcv1.StarRocksClusterPhaseCreating
  193. }
  194. setCondition(&cluster.Status.Conditions, orcv1.NewCondition(orcv1.ConditionRunning, "Reconciling", "Waiting StarRocks operator reconcile", metav1.ConditionTrue, cluster.Generation))
  195. }
  196. if err := r.Status().Update(ctx, &cluster); err != nil {
  197. return ctrl.Result{}, err
  198. }
  199. if cluster.Status.Phase != orcv1.StarRocksClusterPhaseReady && cluster.Status.Phase != orcv1.StarRocksClusterPhaseFailed {
  200. return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
  201. }
  202. return ctrl.Result{}, nil
  203. }
  204. func (r *StarRocksVirtualClusterReconciler) failCluster(ctx context.Context, cluster *orcv1.StarRocksVirtualCluster, err error) (ctrl.Result, error) {
  205. cluster.Status.Phase = orcv1.StarRocksClusterPhaseFailed
  206. cluster.Status.LastError = err.Error()
  207. cluster.Status.ObservedAt = metav1.Now()
  208. setCondition(&cluster.Status.Conditions, orcv1.NewCondition(orcv1.ConditionFailed, "ReconcileFailed", err.Error(), metav1.ConditionTrue, cluster.Generation))
  209. _ = r.Status().Update(ctx, cluster)
  210. return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
  211. }
  212. func (r *StarRocksVirtualClusterReconciler) cleanupCluster(ctx context.Context, cluster *orcv1.StarRocksVirtualCluster, namespace string) error {
  213. if err := r.deleteStarRocksClusterResource(ctx, cluster.Name, namespace); err != nil {
  214. return err
  215. }
  216. nodes := normalizeNodeList(append(cluster.Status.AssignedFeNodes, cluster.Status.AssignedBeNodes...))
  217. if len(nodes) == 0 {
  218. nodes = normalizeNodeList(append(cluster.Spec.FeNodeRefs, cluster.Spec.BeNodeRefs...))
  219. }
  220. for _, nodeID := range nodes {
  221. if err := r.releaseNode(ctx, cluster.Name, nodeID); err != nil {
  222. return err
  223. }
  224. }
  225. return nil
  226. }
  227. func (r *StarRocksVirtualClusterReconciler) allocateNode(ctx context.Context, clusterName, nodeID string) error {
  228. var nodeInv orcv1.NodeInventory
  229. if err := r.Get(ctx, types.NamespacedName{Name: nodeID}, &nodeInv); err != nil {
  230. return err
  231. }
  232. if nodeInv.Status.KubernetesNodeName == "" {
  233. return fmt.Errorf("node %s has no kubernetesNodeName yet", nodeID)
  234. }
  235. clusterRef := util.StarRocksClusterRef(clusterName)
  236. if nodeInv.Status.Phase != orcv1.NodePhaseIdle &&
  237. !(nodeInv.Status.Phase == orcv1.NodePhaseAllocated &&
  238. (nodeInv.Status.ClusterRef == clusterRef || nodeInv.Status.ClusterRef == clusterName)) {
  239. return fmt.Errorf("node %s is not idle", nodeID)
  240. }
  241. if err := k8s.EnsureStarRocksNodeIsolation(ctx, r.Client, nodeInv.Status.KubernetesNodeName, clusterName); err != nil {
  242. return err
  243. }
  244. nodeInv.Status.Phase = orcv1.NodePhaseAllocated
  245. nodeInv.Status.ClusterRef = clusterRef
  246. nodeInv.Status.LastError = ""
  247. nodeInv.Status.ObservedAt = metav1.Now()
  248. setCondition(&nodeInv.Status.Conditions, orcv1.NewCondition(orcv1.ConditionReady, "Allocated", fmt.Sprintf("Allocated to starrocks cluster %s", clusterName), metav1.ConditionTrue, nodeInv.Generation))
  249. return r.Status().Update(ctx, &nodeInv)
  250. }
  251. func (r *StarRocksVirtualClusterReconciler) releaseNode(ctx context.Context, clusterName, nodeID string) error {
  252. var nodeInv orcv1.NodeInventory
  253. if err := r.Get(ctx, types.NamespacedName{Name: nodeID}, &nodeInv); err != nil {
  254. if apierrors.IsNotFound(err) {
  255. return nil
  256. }
  257. return err
  258. }
  259. clusterRef := util.StarRocksClusterRef(clusterName)
  260. if nodeInv.Status.ClusterRef != "" &&
  261. nodeInv.Status.ClusterRef != clusterName &&
  262. nodeInv.Status.ClusterRef != clusterRef {
  263. return fmt.Errorf("node %s is allocated to another cluster: %s", nodeID, nodeInv.Status.ClusterRef)
  264. }
  265. if nodeInv.Status.KubernetesNodeName != "" {
  266. if err := k8s.ClearStarRocksNodeIsolation(ctx, r.Client, nodeInv.Status.KubernetesNodeName); err != nil {
  267. return err
  268. }
  269. }
  270. nodeInv.Status.Phase = orcv1.NodePhaseIdle
  271. nodeInv.Status.ClusterRef = ""
  272. nodeInv.Status.ObservedAt = metav1.Now()
  273. setCondition(&nodeInv.Status.Conditions, orcv1.NewCondition(orcv1.ConditionReady, "Idle", "Node is idle and available", metav1.ConditionTrue, nodeInv.Generation))
  274. return r.Status().Update(ctx, &nodeInv)
  275. }
  276. func (r *StarRocksVirtualClusterReconciler) loadNodeInventories(ctx context.Context, nodeIDs []string) ([]orcv1.NodeInventory, error) {
  277. inventories := make([]orcv1.NodeInventory, 0, len(nodeIDs))
  278. for _, nodeID := range nodeIDs {
  279. var nodeInv orcv1.NodeInventory
  280. if err := r.Get(ctx, types.NamespacedName{Name: nodeID}, &nodeInv); err != nil {
  281. return nil, err
  282. }
  283. inventories = append(inventories, nodeInv)
  284. }
  285. return inventories, nil
  286. }
  287. func inventoryNodeNames(inventories []orcv1.NodeInventory) ([]string, error) {
  288. nodeNames := make([]string, 0, len(inventories))
  289. for _, inv := range inventories {
  290. if inv.Status.KubernetesNodeName == "" {
  291. return nil, fmt.Errorf("node %s has no kubernetesNodeName yet", inv.Name)
  292. }
  293. nodeNames = append(nodeNames, inv.Status.KubernetesNodeName)
  294. }
  295. sort.Strings(nodeNames)
  296. return nodeNames, nil
  297. }
  298. func resolveStarRocksConfig(cluster *orcv1.StarRocksVirtualCluster, feNodes, beNodes []orcv1.NodeInventory) (orcv1.StarRocksClusterConfig, error) {
  299. cfg := cluster.Spec.StarRocksConfig
  300. if cfg.FeImage == "" {
  301. cfg.FeImage = starRocksDefaultFeImage
  302. }
  303. if cfg.BeImage == "" {
  304. cfg.BeImage = starRocksDefaultBeImage
  305. }
  306. feDerived, err := deriveMinResources(feNodes)
  307. if err != nil {
  308. return orcv1.StarRocksClusterConfig{}, fmt.Errorf("derive fe resources: %w", err)
  309. }
  310. beDerived, err := deriveMinResources(beNodes)
  311. if err != nil {
  312. return orcv1.StarRocksClusterConfig{}, fmt.Errorf("derive be resources: %w", err)
  313. }
  314. cfg.FeResources = mergeResourceSpec(feDerived, cfg.FeResources)
  315. cfg.BeResources = mergeResourceSpec(beDerived, cfg.BeResources)
  316. return cfg, nil
  317. }
  318. func deriveMinResources(nodes []orcv1.NodeInventory) (*orcv1.StarRocksResourceSpec, error) {
  319. if len(nodes) == 0 {
  320. return nil, fmt.Errorf("node list is empty")
  321. }
  322. minCPU := nodes[0].Spec.Capacity.CPUCores
  323. minRAM := nodes[0].Spec.Capacity.RamGB
  324. for i := 1; i < len(nodes); i++ {
  325. if nodes[i].Spec.Capacity.CPUCores < minCPU {
  326. minCPU = nodes[i].Spec.Capacity.CPUCores
  327. }
  328. if nodes[i].Spec.Capacity.RamGB < minRAM {
  329. minRAM = nodes[i].Spec.Capacity.RamGB
  330. }
  331. }
  332. if minCPU <= 0 || minRAM <= 0 {
  333. return nil, fmt.Errorf("invalid node capacity cpu=%d ram=%d", minCPU, minRAM)
  334. }
  335. // Use 75% of CPU/RAM for requests to leave headroom for OS/system processes.
  336. reqCPU := minCPU * 3 / 4
  337. if reqCPU < 1 {
  338. reqCPU = 1
  339. }
  340. reqRAM := minRAM * 3 / 4
  341. if reqRAM < 1 {
  342. reqRAM = 1
  343. }
  344. return &orcv1.StarRocksResourceSpec{
  345. Requests: orcv1.StarRocksResourcePair{
  346. CPU: strconv.Itoa(int(reqCPU)),
  347. Memory: fmt.Sprintf("%dGi", reqRAM),
  348. },
  349. Limits: orcv1.StarRocksResourcePair{
  350. CPU: strconv.Itoa(int(minCPU)),
  351. Memory: fmt.Sprintf("%dGi", minRAM),
  352. },
  353. }, nil
  354. }
  355. func mergeResourceSpec(base, override *orcv1.StarRocksResourceSpec) *orcv1.StarRocksResourceSpec {
  356. if base == nil && override == nil {
  357. return nil
  358. }
  359. if base == nil {
  360. copy := *override
  361. return &copy
  362. }
  363. out := *base
  364. if override == nil {
  365. return &out
  366. }
  367. if override.Requests.CPU != "" {
  368. out.Requests.CPU = override.Requests.CPU
  369. }
  370. if override.Requests.Memory != "" {
  371. out.Requests.Memory = override.Requests.Memory
  372. }
  373. if override.Limits.CPU != "" {
  374. out.Limits.CPU = override.Limits.CPU
  375. }
  376. if override.Limits.Memory != "" {
  377. out.Limits.Memory = override.Limits.Memory
  378. }
  379. return &out
  380. }
  381. func overlappingNodes(feNodeIDs, beNodeIDs []string) []string {
  382. feSet := map[string]struct{}{}
  383. for _, nodeID := range feNodeIDs {
  384. feSet[nodeID] = struct{}{}
  385. }
  386. out := make([]string, 0)
  387. for _, nodeID := range beNodeIDs {
  388. if _, ok := feSet[nodeID]; ok {
  389. out = append(out, nodeID)
  390. }
  391. }
  392. sort.Strings(out)
  393. return out
  394. }
  395. func resolvedStarRocksNamespace(clusterName, configuredNamespace string) string {
  396. if configuredNamespace != "" {
  397. return configuredNamespace
  398. }
  399. return util.SafeName("starrocks", clusterName)
  400. }
  401. func (r *StarRocksVirtualClusterReconciler) applyStarRocksClusterResource(
  402. ctx context.Context,
  403. clusterName string,
  404. namespace string,
  405. feNodeNames []string,
  406. beNodeNames []string,
  407. cfg orcv1.StarRocksClusterConfig,
  408. ) error {
  409. desired := buildStarRocksClusterResource(clusterName, namespace, feNodeNames, beNodeNames, cfg)
  410. current := &unstructured.Unstructured{}
  411. current.SetAPIVersion(starRocksClusterAPIVersion)
  412. current.SetKind(starRocksClusterKind)
  413. if err := r.Get(ctx, types.NamespacedName{Name: clusterName, Namespace: namespace}, current); err != nil {
  414. if !apierrors.IsNotFound(err) {
  415. return err
  416. }
  417. return r.Create(ctx, desired)
  418. }
  419. labels := current.GetLabels()
  420. if labels == nil {
  421. labels = map[string]string{}
  422. }
  423. updated := false
  424. for k, v := range desired.GetLabels() {
  425. if labels[k] != v {
  426. labels[k] = v
  427. updated = true
  428. }
  429. }
  430. if !reflect.DeepEqual(current.Object["spec"], desired.Object["spec"]) {
  431. current.Object["spec"] = desired.Object["spec"]
  432. updated = true
  433. }
  434. if !updated {
  435. return nil
  436. }
  437. current.SetLabels(labels)
  438. return r.Update(ctx, current)
  439. }
  440. func (r *StarRocksVirtualClusterReconciler) deleteStarRocksClusterResource(ctx context.Context, clusterName, namespace string) error {
  441. resource := &unstructured.Unstructured{}
  442. resource.SetAPIVersion(starRocksClusterAPIVersion)
  443. resource.SetKind(starRocksClusterKind)
  444. resource.SetName(clusterName)
  445. resource.SetNamespace(namespace)
  446. if err := r.Delete(ctx, resource); err != nil && !apierrors.IsNotFound(err) {
  447. return err
  448. }
  449. return nil
  450. }
  451. func (r *StarRocksVirtualClusterReconciler) getStarRocksClusterStatus(ctx context.Context, clusterName, namespace string) (string, string, error) {
  452. resource := &unstructured.Unstructured{}
  453. resource.SetAPIVersion(starRocksClusterAPIVersion)
  454. resource.SetKind(starRocksClusterKind)
  455. if err := r.Get(ctx, types.NamespacedName{Name: clusterName, Namespace: namespace}, resource); err != nil {
  456. return "", "", err
  457. }
  458. phase, _, _ := unstructured.NestedString(resource.Object, "status", "phase")
  459. reason, _, _ := unstructured.NestedString(resource.Object, "status", "reason")
  460. return phase, reason, nil
  461. }
  462. func buildStarRocksClusterResource(
  463. clusterName string,
  464. namespace string,
  465. feNodeNames []string,
  466. beNodeNames []string,
  467. cfg orcv1.StarRocksClusterConfig,
  468. ) *unstructured.Unstructured {
  469. return &unstructured.Unstructured{
  470. Object: map[string]interface{}{
  471. "apiVersion": starRocksClusterAPIVersion,
  472. "kind": starRocksClusterKind,
  473. "metadata": map[string]interface{}{
  474. "name": clusterName,
  475. "namespace": namespace,
  476. "labels": map[string]interface{}{
  477. util.LabelManagedBy: util.ManagedByValue,
  478. util.LabelStarRocksClusterID: clusterName,
  479. },
  480. },
  481. "spec": map[string]interface{}{
  482. "starRocksFeSpec": buildStarRocksComponentSpec(clusterName, "fe", cfg.FeImage, int32(len(feNodeNames)), feNodeNames, cfg.FeResources),
  483. "starRocksBeSpec": buildStarRocksComponentSpec(clusterName, "be", cfg.BeImage, int32(len(beNodeNames)), beNodeNames, cfg.BeResources),
  484. },
  485. },
  486. }
  487. }
  488. func buildStarRocksComponentSpec(
  489. clusterName string,
  490. component string,
  491. image string,
  492. replicas int32,
  493. nodeNames []string,
  494. resources *orcv1.StarRocksResourceSpec,
  495. ) map[string]interface{} {
  496. componentSpec := map[string]interface{}{
  497. "image": image,
  498. "replicas": replicas,
  499. "podLabels": map[string]interface{}{
  500. util.LabelManagedBy: util.ManagedByValue,
  501. util.LabelStarRocksClusterID: clusterName,
  502. util.LabelStarRocksComponent: component,
  503. },
  504. "nodeSelector": map[string]interface{}{
  505. util.LabelStarRocksClusterID: clusterName,
  506. },
  507. "tolerations": []interface{}{
  508. map[string]interface{}{
  509. "key": util.TaintKeyStarRocksCluster,
  510. "operator": "Equal",
  511. "value": clusterName,
  512. "effect": "NoSchedule",
  513. },
  514. },
  515. }
  516. if len(nodeNames) > 0 {
  517. componentSpec["affinity"] = buildStarRocksAffinity(clusterName, component, nodeNames)
  518. }
  519. if resources != nil {
  520. if resources.Requests.CPU != "" || resources.Requests.Memory != "" {
  521. componentSpec["requests"] = map[string]interface{}{
  522. "cpu": resources.Requests.CPU,
  523. "memory": resources.Requests.Memory,
  524. }
  525. }
  526. if resources.Limits.CPU != "" || resources.Limits.Memory != "" {
  527. componentSpec["limits"] = map[string]interface{}{
  528. "cpu": resources.Limits.CPU,
  529. "memory": resources.Limits.Memory,
  530. }
  531. }
  532. }
  533. switch component {
  534. case "fe":
  535. componentSpec["feEnvVars"] = buildAWSChecksumEnvVars()
  536. case "be":
  537. componentSpec["beEnvVars"] = buildAWSChecksumEnvVars()
  538. }
  539. return componentSpec
  540. }
  541. func buildAWSChecksumEnvVars() []interface{} {
  542. return []interface{}{
  543. map[string]interface{}{
  544. "name": "AWS_REQUEST_CHECKSUM_CALCULATION",
  545. "value": awsChecksumCalculationWhenNeeded,
  546. },
  547. map[string]interface{}{
  548. "name": "AWS_RESPONSE_CHECKSUM_VALIDATION",
  549. "value": awsChecksumCalculationWhenNeeded,
  550. },
  551. }
  552. }
  553. func buildStarRocksAffinity(clusterName, component string, nodeNames []string) map[string]interface{} {
  554. values := make([]interface{}, 0, len(nodeNames))
  555. for _, nodeName := range nodeNames {
  556. values = append(values, nodeName)
  557. }
  558. return map[string]interface{}{
  559. "nodeAffinity": map[string]interface{}{
  560. "requiredDuringSchedulingIgnoredDuringExecution": map[string]interface{}{
  561. "nodeSelectorTerms": []interface{}{
  562. map[string]interface{}{
  563. "matchExpressions": []interface{}{
  564. map[string]interface{}{
  565. "key": "kubernetes.io/hostname",
  566. "operator": "In",
  567. "values": values,
  568. },
  569. },
  570. },
  571. },
  572. },
  573. },
  574. "podAntiAffinity": map[string]interface{}{
  575. "requiredDuringSchedulingIgnoredDuringExecution": []interface{}{
  576. map[string]interface{}{
  577. "labelSelector": map[string]interface{}{
  578. "matchLabels": map[string]interface{}{
  579. util.LabelStarRocksClusterID: clusterName,
  580. util.LabelStarRocksComponent: component,
  581. },
  582. },
  583. "topologyKey": "kubernetes.io/hostname",
  584. },
  585. },
  586. },
  587. }
  588. }
  589. func (r *StarRocksVirtualClusterReconciler) SetupWithManager(mgr ctrl.Manager) error {
  590. return ctrl.NewControllerManagedBy(mgr).
  591. For(&orcv1.StarRocksVirtualCluster{}).
  592. Complete(r)
  593. }