sparkvirtualcluster_controller.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655
  1. package controller
  2. import (
  3. "context"
  4. "fmt"
  5. "sort"
  6. "strings"
  7. "time"
  8. appsv1 "k8s.io/api/apps/v1"
  9. corev1 "k8s.io/api/core/v1"
  10. apierrors "k8s.io/apimachinery/pkg/api/errors"
  11. "k8s.io/apimachinery/pkg/api/resource"
  12. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  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. type SparkVirtualClusterReconciler struct {
  24. client.Client
  25. Scheme *runtime.Scheme
  26. }
  27. const (
  28. sparkPrePullStateConfigMap = "spark-prepull-state"
  29. sparkNamespaceOwnerKindLabel = "spark"
  30. )
  31. func (r *SparkVirtualClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
  32. logger := log.FromContext(ctx).WithValues("sparkVirtualCluster", req.Name)
  33. var cluster orcv1.SparkVirtualCluster
  34. if err := r.Get(ctx, req.NamespacedName, &cluster); err != nil {
  35. return ctrl.Result{}, client.IgnoreNotFound(err)
  36. }
  37. if cluster.Spec.DesiredState == "" {
  38. cluster.Spec.DesiredState = orcv1.VirtualClusterDesiredActive
  39. }
  40. if cluster.Spec.ScaleInPolicy == "" {
  41. cluster.Spec.ScaleInPolicy = orcv1.ScaleInPolicyBlockIfRunning
  42. }
  43. if cluster.DeletionTimestamp.IsZero() {
  44. if !controllerutil.ContainsFinalizer(&cluster, util.FinalizerVirtualCluster) {
  45. controllerutil.AddFinalizer(&cluster, util.FinalizerVirtualCluster)
  46. if err := r.Update(ctx, &cluster); err != nil {
  47. return ctrl.Result{}, err
  48. }
  49. }
  50. } else {
  51. if controllerutil.ContainsFinalizer(&cluster, util.FinalizerVirtualCluster) {
  52. if err := r.cleanupCluster(ctx, &cluster); err != nil {
  53. return ctrl.Result{}, err
  54. }
  55. controllerutil.RemoveFinalizer(&cluster, util.FinalizerVirtualCluster)
  56. if err := r.Update(ctx, &cluster); err != nil {
  57. return ctrl.Result{}, err
  58. }
  59. }
  60. return ctrl.Result{}, nil
  61. }
  62. if cluster.Spec.DesiredState == orcv1.VirtualClusterDesiredReleased {
  63. cluster.Status.Phase = orcv1.SparkClusterPhaseReleasing
  64. cluster.Status.ObservedAt = metav1.Now()
  65. if err := r.Status().Update(ctx, &cluster); err != nil {
  66. return ctrl.Result{}, err
  67. }
  68. if err := r.cleanupCluster(ctx, &cluster); err != nil {
  69. cluster.Status.Phase = orcv1.SparkClusterPhaseFailed
  70. cluster.Status.LastError = err.Error()
  71. cluster.Status.ObservedAt = metav1.Now()
  72. _ = r.Status().Update(ctx, &cluster)
  73. return ctrl.Result{}, err
  74. }
  75. cluster.Status.Phase = orcv1.SparkClusterPhaseReleased
  76. cluster.Status.AssignedNodes = nil
  77. cluster.Status.LastError = ""
  78. cluster.Status.ObservedAt = metav1.Now()
  79. setCondition(&cluster.Status.Conditions, orcv1.NewCondition(orcv1.ConditionReady, "Released", "Cluster released and nodes are idle", metav1.ConditionFalse, cluster.Generation))
  80. if err := r.Status().Update(ctx, &cluster); err != nil {
  81. return ctrl.Result{}, err
  82. }
  83. return ctrl.Result{}, nil
  84. }
  85. ns := cluster.Spec.Namespace
  86. if ns == "" {
  87. ns = util.SafeName("spark", cluster.Name)
  88. if err := k8s.EnsureNamespaceOwnedByCluster(ctx, r.Client, ns, sparkNamespaceOwnerKindLabel, cluster.Name); err != nil {
  89. return ctrl.Result{}, err
  90. }
  91. } else {
  92. if err := k8s.EnsureNamespace(ctx, r.Client, ns); err != nil {
  93. return ctrl.Result{}, err
  94. }
  95. }
  96. if err := k8s.EnsureSparkOperatorRBAC(ctx, r.Client, ns); err != nil {
  97. return ctrl.Result{}, err
  98. }
  99. targetNodes := normalizeNodeList(cluster.Spec.NodeRefs)
  100. previousNodes := normalizeNodeList(cluster.Status.AssignedNodes)
  101. toRelease := diffNodes(previousNodes, targetNodes)
  102. for _, nodeID := range toRelease {
  103. if err := r.releaseNode(ctx, nodeID); err != nil {
  104. return ctrl.Result{}, err
  105. }
  106. }
  107. for _, nodeID := range targetNodes {
  108. if err := r.allocateNode(ctx, &cluster, nodeID); err != nil {
  109. cluster.Status.Phase = orcv1.SparkClusterPhaseFailed
  110. cluster.Status.LastError = err.Error()
  111. cluster.Status.ObservedAt = metav1.Now()
  112. _ = r.Status().Update(ctx, &cluster)
  113. logger.Error(err, "allocate node failed", "nodeID", nodeID)
  114. return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
  115. }
  116. }
  117. prePullReady, prePullMessage, err := r.ensureSparkImagePrePull(ctx, &cluster, ns, targetNodes)
  118. if err != nil {
  119. cluster.Status.Phase = orcv1.SparkClusterPhaseFailed
  120. cluster.Status.LastError = err.Error()
  121. cluster.Status.Namespace = ns
  122. cluster.Status.AssignedNodes = targetNodes
  123. cluster.Status.ObservedAt = metav1.Now()
  124. setCondition(&cluster.Status.Conditions, orcv1.NewCondition(orcv1.ConditionFailed, "PrePullFailed", err.Error(), metav1.ConditionTrue, cluster.Generation))
  125. _ = r.Status().Update(ctx, &cluster)
  126. return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
  127. }
  128. if !prePullReady {
  129. cluster.Status.Phase = orcv1.SparkClusterPhaseCreating
  130. cluster.Status.Namespace = ns
  131. cluster.Status.AssignedNodes = targetNodes
  132. cluster.Status.LastError = ""
  133. cluster.Status.ObservedAt = metav1.Now()
  134. setCondition(&cluster.Status.Conditions, orcv1.NewCondition(orcv1.ConditionRunning, "PrePulling", prePullMessage, metav1.ConditionTrue, cluster.Generation))
  135. if err := r.Status().Update(ctx, &cluster); err != nil {
  136. return ctrl.Result{}, err
  137. }
  138. return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
  139. }
  140. warmupReady, warmupMessage, err := r.ensureSparkIvyWarmup(ctx, &cluster, ns, targetNodes)
  141. if err != nil {
  142. cluster.Status.Phase = orcv1.SparkClusterPhaseFailed
  143. cluster.Status.LastError = err.Error()
  144. cluster.Status.Namespace = ns
  145. cluster.Status.AssignedNodes = targetNodes
  146. cluster.Status.ObservedAt = metav1.Now()
  147. setCondition(&cluster.Status.Conditions, orcv1.NewCondition(orcv1.ConditionFailed, "IvyWarmupFailed", err.Error(), metav1.ConditionTrue, cluster.Generation))
  148. _ = r.Status().Update(ctx, &cluster)
  149. return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
  150. }
  151. if !warmupReady {
  152. cluster.Status.Phase = orcv1.SparkClusterPhaseCreating
  153. cluster.Status.Namespace = ns
  154. cluster.Status.AssignedNodes = targetNodes
  155. cluster.Status.LastError = ""
  156. cluster.Status.ObservedAt = metav1.Now()
  157. setCondition(&cluster.Status.Conditions, orcv1.NewCondition(orcv1.ConditionRunning, "IvyWarmup", warmupMessage, metav1.ConditionTrue, cluster.Generation))
  158. if err := r.Status().Update(ctx, &cluster); err != nil {
  159. return ctrl.Result{}, err
  160. }
  161. return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
  162. }
  163. cluster.Status.Namespace = ns
  164. cluster.Status.AssignedNodes = targetNodes
  165. if len(toRelease) > 0 || !equalNodeLists(previousNodes, targetNodes) {
  166. cluster.Status.Phase = orcv1.SparkClusterPhaseScaling
  167. } else {
  168. cluster.Status.Phase = orcv1.SparkClusterPhaseReady
  169. }
  170. cluster.Status.LastError = ""
  171. cluster.Status.ObservedAt = metav1.Now()
  172. setCondition(&cluster.Status.Conditions, orcv1.NewCondition(orcv1.ConditionReady, "ClusterReady", "Cluster nodes are isolated and ready", metav1.ConditionTrue, cluster.Generation))
  173. if err := r.Status().Update(ctx, &cluster); err != nil {
  174. return ctrl.Result{}, err
  175. }
  176. if cluster.Status.Phase == orcv1.SparkClusterPhaseScaling {
  177. cluster.Status.Phase = orcv1.SparkClusterPhaseReady
  178. cluster.Status.ObservedAt = metav1.Now()
  179. _ = r.Status().Update(ctx, &cluster)
  180. }
  181. return ctrl.Result{}, nil
  182. }
  183. func (r *SparkVirtualClusterReconciler) cleanupCluster(ctx context.Context, cluster *orcv1.SparkVirtualCluster) error {
  184. ns := cluster.Status.Namespace
  185. if ns == "" {
  186. ns = cluster.Spec.Namespace
  187. }
  188. if ns == "" {
  189. ns = util.SafeName("spark", cluster.Name)
  190. }
  191. if err := r.cleanupSparkPrePullArtifacts(ctx, ns, cluster.Name); err != nil {
  192. return err
  193. }
  194. if err := r.cleanupSparkIvyWarmupArtifacts(ctx, ns, cluster.Name); err != nil {
  195. return err
  196. }
  197. nodes := cluster.Status.AssignedNodes
  198. if len(nodes) == 0 {
  199. nodes = cluster.Spec.NodeRefs
  200. }
  201. for _, nodeID := range normalizeNodeList(nodes) {
  202. if err := r.releaseNode(ctx, nodeID); err != nil {
  203. return err
  204. }
  205. }
  206. return nil
  207. }
  208. func (r *SparkVirtualClusterReconciler) allocateNode(ctx context.Context, cluster *orcv1.SparkVirtualCluster, nodeID string) error {
  209. var nodeInv orcv1.NodeInventory
  210. if err := r.Get(ctx, types.NamespacedName{Name: nodeID}, &nodeInv); err != nil {
  211. return err
  212. }
  213. if nodeInv.Status.KubernetesNodeName == "" {
  214. return fmt.Errorf("node %s has no kubernetesNodeName yet", nodeID)
  215. }
  216. clusterRef := util.SparkClusterRef(cluster.Name)
  217. if nodeInv.Status.Phase != orcv1.NodePhaseIdle &&
  218. !(nodeInv.Status.Phase == orcv1.NodePhaseAllocated &&
  219. (nodeInv.Status.ClusterRef == clusterRef || nodeInv.Status.ClusterRef == cluster.Name)) {
  220. return fmt.Errorf("node %s is not idle", nodeID)
  221. }
  222. if err := k8s.EnsureNodeIsolation(ctx, r.Client, nodeInv.Status.KubernetesNodeName, cluster.Name); err != nil {
  223. return err
  224. }
  225. nodeInv.Status.Phase = orcv1.NodePhaseAllocated
  226. nodeInv.Status.ClusterRef = clusterRef
  227. nodeInv.Status.LastError = ""
  228. nodeInv.Status.ObservedAt = metav1.Now()
  229. setCondition(&nodeInv.Status.Conditions, orcv1.NewCondition(orcv1.ConditionReady, "Allocated", fmt.Sprintf("Allocated to cluster %s", cluster.Name), metav1.ConditionTrue, nodeInv.Generation))
  230. return r.Status().Update(ctx, &nodeInv)
  231. }
  232. func (r *SparkVirtualClusterReconciler) releaseNode(ctx context.Context, nodeID string) error {
  233. var nodeInv orcv1.NodeInventory
  234. if err := r.Get(ctx, types.NamespacedName{Name: nodeID}, &nodeInv); err != nil {
  235. if apierrors.IsNotFound(err) {
  236. return nil
  237. }
  238. return err
  239. }
  240. if nodeInv.Status.KubernetesNodeName != "" {
  241. if err := k8s.ClearNodeIsolation(ctx, r.Client, nodeInv.Status.KubernetesNodeName); err != nil {
  242. return err
  243. }
  244. }
  245. nodeInv.Status.Phase = orcv1.NodePhaseIdle
  246. nodeInv.Status.ClusterRef = ""
  247. nodeInv.Status.ObservedAt = metav1.Now()
  248. setCondition(&nodeInv.Status.Conditions, orcv1.NewCondition(orcv1.ConditionReady, "Idle", "Node is idle and available", metav1.ConditionTrue, nodeInv.Generation))
  249. return r.Status().Update(ctx, &nodeInv)
  250. }
  251. func (r *SparkVirtualClusterReconciler) ensureSparkImagePrePull(
  252. ctx context.Context,
  253. cluster *orcv1.SparkVirtualCluster,
  254. namespace string,
  255. nodeIDs []string,
  256. ) (bool, string, error) {
  257. if len(nodeIDs) == 0 {
  258. return true, "No target nodes for pre-pull", nil
  259. }
  260. enabled, image, pullPolicy, err := k8s.ResolveSparkPrePullConfig(cluster.Spec.SparkConfig)
  261. if err != nil {
  262. return false, "", err
  263. }
  264. if !enabled {
  265. if err := r.cleanupSparkPrePullArtifacts(ctx, namespace, cluster.Name); err != nil {
  266. return false, "", err
  267. }
  268. return true, "Spark pre-pull is disabled", nil
  269. }
  270. token := util.RequestHash(image + "|" + strings.Join(nodeIDs, ","))
  271. stateName := util.SafeName(sparkPrePullStateConfigMap, cluster.Name)
  272. stateToken, err := r.readSparkPrePullState(ctx, namespace, stateName)
  273. if err != nil {
  274. return false, "", err
  275. }
  276. daemonSetName := util.SafeName("spark-prepull", cluster.Name, token)
  277. if err := r.cleanupSparkPrePullDaemonSets(ctx, namespace, cluster.Name, daemonSetName); err != nil {
  278. return false, "", err
  279. }
  280. if stateToken == token {
  281. return true, fmt.Sprintf("Spark image pre-pull already completed for %s", image), nil
  282. }
  283. desired := buildSparkPrePullDaemonSet(namespace, cluster.Name, daemonSetName, image, pullPolicy)
  284. var existing appsv1.DaemonSet
  285. err = r.Get(ctx, types.NamespacedName{Name: daemonSetName, Namespace: namespace}, &existing)
  286. if apierrors.IsNotFound(err) {
  287. if err := r.Create(ctx, desired); err != nil {
  288. return false, "", err
  289. }
  290. return false, fmt.Sprintf("Spark pre-pull DaemonSet created for image %s", image), nil
  291. }
  292. if err != nil {
  293. return false, "", err
  294. }
  295. if existing.Status.DesiredNumberScheduled == 0 || existing.Status.NumberReady < int32(len(nodeIDs)) {
  296. return false, fmt.Sprintf("Spark image pre-pull in progress: ready=%d/%d", existing.Status.NumberReady, len(nodeIDs)), nil
  297. }
  298. if err := r.writeSparkPrePullState(ctx, namespace, stateName, token, image); err != nil {
  299. return false, "", err
  300. }
  301. if err := r.Delete(ctx, &existing); err != nil && !apierrors.IsNotFound(err) {
  302. return false, "", err
  303. }
  304. return true, fmt.Sprintf("Spark image pre-pull completed for %s", image), nil
  305. }
  306. func (r *SparkVirtualClusterReconciler) ensureSparkIvyWarmup(
  307. ctx context.Context,
  308. cluster *orcv1.SparkVirtualCluster,
  309. namespace string,
  310. nodeIDs []string,
  311. ) (bool, string, error) {
  312. if len(nodeIDs) == 0 {
  313. return true, "No target nodes for Ivy warmup", nil
  314. }
  315. token := k8s.ResolveSparkIvyWarmupToken(cluster.Spec.SparkConfig)
  316. stateName := util.SafeName(k8s.SparkIvyCacheStateConfigMapName, cluster.Name)
  317. stateToken, err := r.readSparkIvyWarmupState(ctx, namespace, stateName)
  318. if err != nil {
  319. return false, "", err
  320. }
  321. daemonSetName := util.SafeName("spark-ivy-warmup", cluster.Name, token)
  322. if err := r.cleanupSparkIvyWarmupDaemonSets(ctx, namespace, cluster.Name, daemonSetName); err != nil {
  323. return false, "", err
  324. }
  325. if stateToken == token {
  326. return true, "Spark Ivy cache warmup already completed", nil
  327. }
  328. desired, err := k8s.BuildSparkIvyWarmupDaemonSet(namespace, cluster.Name, daemonSetName, cluster.Spec.SparkConfig)
  329. if err != nil {
  330. return false, "", err
  331. }
  332. var existing appsv1.DaemonSet
  333. err = r.Get(ctx, types.NamespacedName{Name: daemonSetName, Namespace: namespace}, &existing)
  334. if apierrors.IsNotFound(err) {
  335. if err := r.Create(ctx, desired); err != nil {
  336. return false, "", err
  337. }
  338. return false, fmt.Sprintf("Spark Ivy warmup DaemonSet created for %d nodes", len(nodeIDs)), nil
  339. }
  340. if err != nil {
  341. return false, "", err
  342. }
  343. if !k8s.SparkIvyWarmupPodSpecMatches(existing.Spec.Template.Spec, desired.Spec.Template.Spec) {
  344. patchBase := existing.DeepCopy()
  345. existing.Spec = desired.Spec
  346. if err := r.Patch(ctx, &existing, client.MergeFrom(patchBase)); err != nil {
  347. return false, "", err
  348. }
  349. return false, "Spark Ivy warmup DaemonSet updated", nil
  350. }
  351. if existing.Status.DesiredNumberScheduled == 0 || existing.Status.NumberReady < int32(len(nodeIDs)) {
  352. return false, fmt.Sprintf("Spark Ivy warmup in progress: ready=%d/%d", existing.Status.NumberReady, len(nodeIDs)), nil
  353. }
  354. if err := r.writeSparkIvyWarmupState(ctx, namespace, stateName, token); err != nil {
  355. return false, "", err
  356. }
  357. if err := r.Delete(ctx, &existing); err != nil && !apierrors.IsNotFound(err) {
  358. return false, "", err
  359. }
  360. return true, "Spark Ivy cache warmup completed", nil
  361. }
  362. func (r *SparkVirtualClusterReconciler) cleanupSparkIvyWarmupArtifacts(ctx context.Context, namespace, clusterName string) error {
  363. if namespace == "" {
  364. return nil
  365. }
  366. if err := r.cleanupSparkIvyWarmupDaemonSets(ctx, namespace, clusterName, ""); err != nil {
  367. return err
  368. }
  369. state := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{
  370. Name: util.SafeName(k8s.SparkIvyCacheStateConfigMapName, clusterName),
  371. Namespace: namespace,
  372. }}
  373. if err := r.Delete(ctx, state); err != nil && !apierrors.IsNotFound(err) {
  374. return err
  375. }
  376. return nil
  377. }
  378. func (r *SparkVirtualClusterReconciler) cleanupSparkIvyWarmupDaemonSets(ctx context.Context, namespace, clusterName, exceptName string) error {
  379. return k8s.DeleteSparkIvyWarmupDaemonSets(ctx, r.Client, namespace, clusterName, exceptName)
  380. }
  381. func (r *SparkVirtualClusterReconciler) readSparkIvyWarmupState(ctx context.Context, namespace, stateName string) (string, error) {
  382. state := &corev1.ConfigMap{}
  383. if err := r.Get(ctx, types.NamespacedName{Name: stateName, Namespace: namespace}, state); err != nil {
  384. if apierrors.IsNotFound(err) {
  385. return "", nil
  386. }
  387. return "", err
  388. }
  389. return strings.TrimSpace(state.Data["token"]), nil
  390. }
  391. func (r *SparkVirtualClusterReconciler) writeSparkIvyWarmupState(ctx context.Context, namespace, stateName, token string) error {
  392. if namespace == "" || stateName == "" {
  393. return nil
  394. }
  395. state := &corev1.ConfigMap{}
  396. key := types.NamespacedName{Name: stateName, Namespace: namespace}
  397. err := r.Get(ctx, key, state)
  398. if apierrors.IsNotFound(err) {
  399. state = &corev1.ConfigMap{
  400. ObjectMeta: metav1.ObjectMeta{
  401. Name: stateName,
  402. Namespace: namespace,
  403. Labels: map[string]string{
  404. util.LabelManagedBy: util.ManagedByValue,
  405. },
  406. },
  407. Data: map[string]string{
  408. "token": token,
  409. },
  410. }
  411. return r.Create(ctx, state)
  412. }
  413. if err != nil {
  414. return err
  415. }
  416. if state.Data == nil {
  417. state.Data = map[string]string{}
  418. }
  419. state.Data["token"] = token
  420. return r.Update(ctx, state)
  421. }
  422. func (r *SparkVirtualClusterReconciler) cleanupSparkPrePullArtifacts(ctx context.Context, namespace, clusterName string) error {
  423. if namespace == "" {
  424. return nil
  425. }
  426. if err := r.cleanupSparkPrePullDaemonSets(ctx, namespace, clusterName, ""); err != nil {
  427. return err
  428. }
  429. state := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{
  430. Name: util.SafeName(sparkPrePullStateConfigMap, clusterName),
  431. Namespace: namespace,
  432. }}
  433. if err := r.Delete(ctx, state); err != nil && !apierrors.IsNotFound(err) {
  434. return err
  435. }
  436. return nil
  437. }
  438. func (r *SparkVirtualClusterReconciler) cleanupSparkPrePullDaemonSets(ctx context.Context, namespace, clusterName, exceptName string) error {
  439. if namespace == "" {
  440. return nil
  441. }
  442. var list appsv1.DaemonSetList
  443. if err := r.List(
  444. ctx,
  445. &list,
  446. client.InNamespace(namespace),
  447. client.MatchingLabels{
  448. util.LabelManagedBy: util.ManagedByValue,
  449. util.LabelClusterID: clusterName,
  450. util.LabelComponent: util.ComponentSparkPrePull,
  451. },
  452. ); err != nil {
  453. return err
  454. }
  455. for i := range list.Items {
  456. ds := &list.Items[i]
  457. if exceptName != "" && ds.Name == exceptName {
  458. continue
  459. }
  460. if err := r.Delete(ctx, ds); err != nil && !apierrors.IsNotFound(err) {
  461. return err
  462. }
  463. }
  464. return nil
  465. }
  466. func (r *SparkVirtualClusterReconciler) readSparkPrePullState(ctx context.Context, namespace, stateName string) (string, error) {
  467. state := &corev1.ConfigMap{}
  468. if err := r.Get(ctx, types.NamespacedName{Name: stateName, Namespace: namespace}, state); err != nil {
  469. if apierrors.IsNotFound(err) {
  470. return "", nil
  471. }
  472. return "", err
  473. }
  474. return strings.TrimSpace(state.Data["token"]), nil
  475. }
  476. func (r *SparkVirtualClusterReconciler) writeSparkPrePullState(ctx context.Context, namespace, stateName, token, image string) error {
  477. if namespace == "" || stateName == "" {
  478. return nil
  479. }
  480. state := &corev1.ConfigMap{}
  481. key := types.NamespacedName{Name: stateName, Namespace: namespace}
  482. err := r.Get(ctx, key, state)
  483. if apierrors.IsNotFound(err) {
  484. state = &corev1.ConfigMap{
  485. ObjectMeta: metav1.ObjectMeta{
  486. Name: stateName,
  487. Namespace: namespace,
  488. Labels: map[string]string{
  489. util.LabelManagedBy: util.ManagedByValue,
  490. },
  491. },
  492. Data: map[string]string{
  493. "token": token,
  494. "image": image,
  495. },
  496. }
  497. return r.Create(ctx, state)
  498. }
  499. if err != nil {
  500. return err
  501. }
  502. if state.Data == nil {
  503. state.Data = map[string]string{}
  504. }
  505. state.Data["token"] = token
  506. state.Data["image"] = image
  507. return r.Update(ctx, state)
  508. }
  509. func buildSparkPrePullDaemonSet(namespace, clusterName, daemonSetName, image string, pullPolicy corev1.PullPolicy) *appsv1.DaemonSet {
  510. labels := map[string]string{
  511. util.LabelManagedBy: util.ManagedByValue,
  512. util.LabelClusterID: clusterName,
  513. util.LabelComponent: util.ComponentSparkPrePull,
  514. "orchestrator.spark.cyrus.io/prepull-daemon": daemonSetName,
  515. }
  516. return &appsv1.DaemonSet{
  517. ObjectMeta: metav1.ObjectMeta{
  518. Name: daemonSetName,
  519. Namespace: namespace,
  520. Labels: labels,
  521. },
  522. Spec: appsv1.DaemonSetSpec{
  523. Selector: &metav1.LabelSelector{
  524. MatchLabels: map[string]string{
  525. "orchestrator.spark.cyrus.io/prepull-daemon": daemonSetName,
  526. },
  527. },
  528. Template: corev1.PodTemplateSpec{
  529. ObjectMeta: metav1.ObjectMeta{
  530. Labels: labels,
  531. },
  532. Spec: corev1.PodSpec{
  533. RestartPolicy: corev1.RestartPolicyAlways,
  534. NodeSelector: map[string]string{
  535. util.LabelClusterID: clusterName,
  536. },
  537. Tolerations: []corev1.Toleration{
  538. {
  539. Key: util.TaintKeySparkCluster,
  540. Operator: corev1.TolerationOpEqual,
  541. Value: clusterName,
  542. Effect: corev1.TaintEffectNoSchedule,
  543. },
  544. },
  545. Containers: []corev1.Container{
  546. {
  547. Name: "prepull",
  548. Image: image,
  549. ImagePullPolicy: pullPolicy,
  550. Command: []string{"/bin/sh", "-c", "trap : TERM INT; sleep 3600 & wait"},
  551. Resources: corev1.ResourceRequirements{
  552. Requests: corev1.ResourceList{
  553. corev1.ResourceCPU: resourceMustParse("10m"),
  554. corev1.ResourceMemory: resourceMustParse("32Mi"),
  555. },
  556. Limits: corev1.ResourceList{
  557. corev1.ResourceCPU: resourceMustParse("100m"),
  558. corev1.ResourceMemory: resourceMustParse("128Mi"),
  559. },
  560. },
  561. },
  562. },
  563. },
  564. },
  565. },
  566. }
  567. }
  568. func resourceMustParse(v string) resource.Quantity {
  569. return resource.MustParse(v)
  570. }
  571. func (r *SparkVirtualClusterReconciler) SetupWithManager(mgr ctrl.Manager) error {
  572. return ctrl.NewControllerManagedBy(mgr).
  573. For(&orcv1.SparkVirtualCluster{}).
  574. Complete(r)
  575. }
  576. func normalizeNodeList(nodes []string) []string {
  577. unique := map[string]struct{}{}
  578. for _, node := range nodes {
  579. if node == "" {
  580. continue
  581. }
  582. unique[node] = struct{}{}
  583. }
  584. out := make([]string, 0, len(unique))
  585. for node := range unique {
  586. out = append(out, node)
  587. }
  588. sort.Strings(out)
  589. return out
  590. }
  591. func diffNodes(oldNodes, newNodes []string) []string {
  592. set := map[string]struct{}{}
  593. for _, n := range newNodes {
  594. set[n] = struct{}{}
  595. }
  596. out := make([]string, 0)
  597. for _, old := range oldNodes {
  598. if _, ok := set[old]; !ok {
  599. out = append(out, old)
  600. }
  601. }
  602. return out
  603. }
  604. func equalNodeLists(a, b []string) bool {
  605. if len(a) != len(b) {
  606. return false
  607. }
  608. for i := range a {
  609. if a[i] != b[i] {
  610. return false
  611. }
  612. }
  613. return true
  614. }