clusterexternalsecret_controller.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  1. /*
  2. Copyright © 2025 ESO Maintainer Team
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. https://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. // Package clusterexternalsecret implements a controller for managing ClusterExternalSecret resources,
  14. // which allow creating ExternalSecrets across multiple namespaces.
  15. package clusterexternalsecret
  16. import (
  17. "context"
  18. "errors"
  19. "fmt"
  20. "slices"
  21. "sort"
  22. "time"
  23. "github.com/go-logr/logr"
  24. v1 "k8s.io/api/core/v1"
  25. apierrors "k8s.io/apimachinery/pkg/api/errors"
  26. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  27. "k8s.io/apimachinery/pkg/labels"
  28. "k8s.io/apimachinery/pkg/runtime"
  29. "k8s.io/apimachinery/pkg/runtime/schema"
  30. "k8s.io/apimachinery/pkg/types"
  31. ctrl "sigs.k8s.io/controller-runtime"
  32. "sigs.k8s.io/controller-runtime/pkg/builder"
  33. "sigs.k8s.io/controller-runtime/pkg/client"
  34. "sigs.k8s.io/controller-runtime/pkg/controller"
  35. "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
  36. "sigs.k8s.io/controller-runtime/pkg/handler"
  37. "sigs.k8s.io/controller-runtime/pkg/reconcile"
  38. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  39. "github.com/external-secrets/external-secrets/pkg/controllers/clusterexternalsecret/cesmetrics"
  40. ctrlmetrics "github.com/external-secrets/external-secrets/pkg/controllers/metrics"
  41. "github.com/external-secrets/external-secrets/pkg/esutils"
  42. )
  43. // Reconciler reconciles a ClusterExternalSecret object.
  44. type Reconciler struct {
  45. client.Client
  46. Log logr.Logger
  47. Scheme *runtime.Scheme
  48. RequeueInterval time.Duration
  49. }
  50. const (
  51. errGetCES = "could not get ClusterExternalSecret"
  52. errPatchStatus = "unable to patch status"
  53. errConvertLabelSelector = "unable to convert labelselector"
  54. errGetExistingES = "could not get existing ExternalSecret"
  55. errNamespacesFailed = "one or more namespaces failed"
  56. // ClusterExternalSecretFinalizer is the finalizer for ClusterExternalSecret resources.
  57. // This finalizer ensures that all ExternalSecrets created by the ClusterExternalSecret
  58. // are properly cleaned up before the ClusterExternalSecret is deleted, preventing orphaned resources.
  59. ClusterExternalSecretFinalizer = "externalsecrets.external-secrets.io/clusterexternalsecret-cleanup"
  60. )
  61. // Reconcile is part of the main kubernetes reconciliation loop which aims to
  62. // move the current state of the cluster closer to the desired state.
  63. //
  64. // For more details, check Reconcile and its Result here:
  65. // - https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/reconcile
  66. func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
  67. log := r.Log.WithValues("ClusterExternalSecret", req.NamespacedName)
  68. resourceLabels := ctrlmetrics.RefineNonConditionMetricLabels(map[string]string{"name": req.Name, "namespace": req.Namespace})
  69. start := time.Now()
  70. externalSecretReconcileDuration := cesmetrics.GetGaugeVec(cesmetrics.ClusterExternalSecretReconcileDurationKey)
  71. defer func() { externalSecretReconcileDuration.With(resourceLabels).Set(float64(time.Since(start))) }()
  72. var clusterExternalSecret esv1.ClusterExternalSecret
  73. err := r.Get(ctx, req.NamespacedName, &clusterExternalSecret)
  74. if err != nil {
  75. if apierrors.IsNotFound(err) {
  76. cesmetrics.RemoveMetrics(req.Namespace, req.Name)
  77. return ctrl.Result{}, nil
  78. }
  79. log.Error(err, errGetCES)
  80. return ctrl.Result{}, err
  81. }
  82. // Handle deletion with finalizer
  83. // When a ClusterExternalSecret is being deleted, we need to ensure all created ExternalSecrets
  84. // and namespace finalizers are cleaned up to prevent resource leaks and namespace deletion blocking.
  85. if clusterExternalSecret.DeletionTimestamp != nil {
  86. // Always attempt cleanup to handle edge case where finalizer might be removed externally
  87. if err := r.cleanupExternalSecrets(ctx, log, &clusterExternalSecret); err != nil {
  88. log.Error(err, "failed to cleanup ExternalSecrets")
  89. return ctrl.Result{}, err
  90. }
  91. // Remove finalizer from ClusterExternalSecret if it exists
  92. if updated := controllerutil.RemoveFinalizer(&clusterExternalSecret, ClusterExternalSecretFinalizer); updated {
  93. if err := r.Update(ctx, &clusterExternalSecret); err != nil {
  94. return ctrl.Result{}, err
  95. }
  96. }
  97. return ctrl.Result{}, nil
  98. }
  99. // Add finalizer if it doesn't exist
  100. // This ensures the ClusterExternalSecret cannot be deleted until we've cleaned up all
  101. // ExternalSecrets it created and removed our finalizers from namespaces.
  102. if updated := controllerutil.AddFinalizer(&clusterExternalSecret, ClusterExternalSecretFinalizer); updated {
  103. if err := r.Update(ctx, &clusterExternalSecret); err != nil {
  104. return ctrl.Result{}, err
  105. }
  106. // Return immediately after update to let the change propagate
  107. return ctrl.Result{}, nil
  108. }
  109. p := client.MergeFrom(clusterExternalSecret.DeepCopy())
  110. defer r.deferPatch(ctx, log, &clusterExternalSecret, p)
  111. return r.reconcile(ctx, log, &clusterExternalSecret)
  112. }
  113. func (r *Reconciler) reconcile(ctx context.Context, log logr.Logger, clusterExternalSecret *esv1.ClusterExternalSecret) (ctrl.Result, error) {
  114. refreshInt := r.RequeueInterval
  115. if clusterExternalSecret.Spec.RefreshInterval != nil {
  116. refreshInt = clusterExternalSecret.Spec.RefreshInterval.Duration
  117. }
  118. esName := clusterExternalSecret.Spec.ExternalSecretName
  119. if esName == "" {
  120. esName = clusterExternalSecret.ObjectMeta.Name
  121. }
  122. if prevName := clusterExternalSecret.Status.ExternalSecretName; prevName != "" && prevName != esName {
  123. // ExternalSecretName has changed, so remove the old ones
  124. if err := r.removeOldSecrets(ctx, log, clusterExternalSecret, prevName); err != nil {
  125. return ctrl.Result{}, err
  126. }
  127. }
  128. clusterExternalSecret.Status.ExternalSecretName = esName
  129. selectors := []*metav1.LabelSelector{}
  130. if s := clusterExternalSecret.Spec.NamespaceSelector; s != nil {
  131. selectors = append(selectors, s)
  132. }
  133. selectors = append(selectors, clusterExternalSecret.Spec.NamespaceSelectors...)
  134. namespaces, err := esutils.GetTargetNamespaces(ctx, r.Client, clusterExternalSecret.Spec.Namespaces, selectors)
  135. if err != nil {
  136. log.Error(err, "failed to get target Namespaces")
  137. failedNamespaces := map[string]error{
  138. "unknown": err,
  139. }
  140. condition := NewClusterExternalSecretCondition(failedNamespaces)
  141. SetClusterExternalSecretCondition(clusterExternalSecret, *condition)
  142. clusterExternalSecret.Status.FailedNamespaces = toNamespaceFailures(failedNamespaces)
  143. return ctrl.Result{}, err
  144. }
  145. failedNamespaces := r.deleteOutdatedExternalSecrets(ctx, namespaces, esName, clusterExternalSecret.Name, clusterExternalSecret.Status.ProvisionedNamespaces)
  146. provisionedNamespaces := r.gatherProvisionedNamespaces(ctx, log, clusterExternalSecret, namespaces, esName, failedNamespaces)
  147. condition := NewClusterExternalSecretCondition(failedNamespaces)
  148. SetClusterExternalSecretCondition(clusterExternalSecret, *condition)
  149. clusterExternalSecret.Status.FailedNamespaces = toNamespaceFailures(failedNamespaces)
  150. sort.Strings(provisionedNamespaces)
  151. clusterExternalSecret.Status.ProvisionedNamespaces = provisionedNamespaces
  152. // Check if any failures are due to conflicts - if so, requeue immediately
  153. for _, err := range failedNamespaces {
  154. if apierrors.IsConflict(err) {
  155. log.V(1).Info("conflict detected, requeuing immediately")
  156. return ctrl.Result{}, fmt.Errorf("conflict detected, will retry: %w", err)
  157. }
  158. }
  159. return ctrl.Result{RequeueAfter: refreshInt}, nil
  160. }
  161. func (r *Reconciler) gatherProvisionedNamespaces(
  162. ctx context.Context,
  163. log logr.Logger,
  164. clusterExternalSecret *esv1.ClusterExternalSecret,
  165. namespaces []v1.Namespace,
  166. esName string,
  167. failedNamespaces map[string]error,
  168. ) []string {
  169. var provisionedNamespaces []string //nolint:prealloc // we don't know the size
  170. for _, namespace := range namespaces {
  171. // If namespace is being deleted, remove our finalizer to allow deletion to proceed
  172. if namespace.DeletionTimestamp != nil {
  173. log.Info("namespace is being deleted, removing finalizer", "namespace", namespace.Name)
  174. if err := r.removeNamespaceFinalizer(ctx, log, &namespace, clusterExternalSecret.Name); err != nil {
  175. log.Error(err, "failed to remove finalizer from terminating namespace", "namespace", namespace.Name)
  176. // Don't add to failedNamespaces - this is cleanup, not provisioning
  177. }
  178. continue
  179. }
  180. var existingES esv1.ExternalSecret
  181. err := r.Get(ctx, types.NamespacedName{
  182. Name: esName,
  183. Namespace: namespace.Name,
  184. }, &existingES)
  185. if err != nil && !apierrors.IsNotFound(err) {
  186. log.Error(err, errGetExistingES)
  187. failedNamespaces[namespace.Name] = err
  188. continue
  189. }
  190. if err == nil && !isExternalSecretOwnedBy(&existingES, clusterExternalSecret.Name) {
  191. failedNamespaces[namespace.Name] = errors.New("external secret already exists in namespace")
  192. continue
  193. }
  194. if err := r.createOrUpdateExternalSecret(ctx, clusterExternalSecret, namespace, esName, clusterExternalSecret.Spec.ExternalSecretMetadata); err != nil {
  195. // If conflict, don't log as error - just add to failed namespaces for retry
  196. if apierrors.IsConflict(err) {
  197. log.V(1).Info("conflict while updating namespace, will retry", "namespace", namespace.Name)
  198. failedNamespaces[namespace.Name] = err
  199. continue
  200. }
  201. log.Error(err, "failed to create or update external secret")
  202. failedNamespaces[namespace.Name] = err
  203. continue
  204. }
  205. provisionedNamespaces = append(provisionedNamespaces, namespace.Name)
  206. }
  207. return provisionedNamespaces
  208. }
  209. func (r *Reconciler) removeOldSecrets(ctx context.Context, log logr.Logger, clusterExternalSecret *esv1.ClusterExternalSecret, prevName string) error {
  210. var (
  211. failedNamespaces = map[string]error{}
  212. lastErr error
  213. )
  214. for _, ns := range clusterExternalSecret.Status.ProvisionedNamespaces {
  215. if err := r.deleteExternalSecret(ctx, prevName, clusterExternalSecret.Name, ns); err != nil {
  216. log.Error(err, "could not delete ExternalSecret")
  217. failedNamespaces[ns] = err
  218. lastErr = err
  219. }
  220. }
  221. if len(failedNamespaces) > 0 {
  222. condition := NewClusterExternalSecretCondition(failedNamespaces)
  223. SetClusterExternalSecretCondition(clusterExternalSecret, *condition)
  224. clusterExternalSecret.Status.FailedNamespaces = toNamespaceFailures(failedNamespaces)
  225. return lastErr
  226. }
  227. return nil
  228. }
  229. // cleanupExternalSecrets removes all ExternalSecrets created by this ClusterExternalSecret
  230. // and removes the namespace finalizers we added. This uses a dual-finalizer strategy:
  231. // 1. ClusterExternalSecret has a finalizer to ensure cleanup happens
  232. // 2. Each namespace gets a CES-specific finalizer to prevent deletion race conditions
  233. // The namespace finalizer is named with the CES name to allow multiple CES resources
  234. // to provision into the same namespace independently.
  235. func (r *Reconciler) cleanupExternalSecrets(ctx context.Context, log logr.Logger, clusterExternalSecret *esv1.ClusterExternalSecret) error {
  236. esName := r.getExternalSecretName(clusterExternalSecret)
  237. var err error
  238. for _, ns := range clusterExternalSecret.Status.ProvisionedNamespaces {
  239. if cleanupErr := r.cleanupNamespaceResources(ctx, log, clusterExternalSecret, ns, esName); cleanupErr != nil {
  240. err = errors.Join(err, cleanupErr)
  241. }
  242. }
  243. return err
  244. }
  245. // getExternalSecretName returns the name to use for ExternalSecrets.
  246. func (r *Reconciler) getExternalSecretName(clusterExternalSecret *esv1.ClusterExternalSecret) string {
  247. if clusterExternalSecret.Status.ExternalSecretName != "" {
  248. return clusterExternalSecret.Status.ExternalSecretName
  249. }
  250. return clusterExternalSecret.ObjectMeta.Name
  251. }
  252. // cleanupNamespaceResources handles cleanup for a single namespace.
  253. func (r *Reconciler) cleanupNamespaceResources(ctx context.Context, log logr.Logger, clusterExternalSecret *esv1.ClusterExternalSecret, namespaceName, esName string) error {
  254. // Get the namespace
  255. namespace, err := r.getNamespace(ctx, namespaceName)
  256. if err != nil {
  257. if apierrors.IsNotFound(err) {
  258. // Namespace already deleted, that's ok
  259. return nil
  260. }
  261. return fmt.Errorf("failed to get namespace %s: %w", namespaceName, err)
  262. }
  263. // Remove namespace finalizer
  264. if err := r.removeNamespaceFinalizer(ctx, log, namespace, clusterExternalSecret.Name); err != nil {
  265. return err
  266. }
  267. // Delete ExternalSecret
  268. if err := r.deleteExternalSecret(ctx, esName, clusterExternalSecret.Name, namespaceName); err != nil {
  269. return fmt.Errorf("failed to delete ExternalSecret in namespace %s: %w", namespaceName, err)
  270. }
  271. return nil
  272. }
  273. // getNamespace fetches a namespace by name.
  274. func (r *Reconciler) getNamespace(ctx context.Context, name string) (*v1.Namespace, error) {
  275. var namespace v1.Namespace
  276. err := r.Get(ctx, types.NamespacedName{Name: name}, &namespace)
  277. return &namespace, err
  278. }
  279. // removeNamespaceFinalizer removes the CES-specific finalizer from a namespace.
  280. func (r *Reconciler) removeNamespaceFinalizer(ctx context.Context, log logr.Logger, namespace *v1.Namespace, cesName string) error {
  281. finalizer := r.buildCESFinalizer(cesName)
  282. if !controllerutil.ContainsFinalizer(namespace, finalizer) {
  283. return nil // Finalizer doesn't exist, nothing to do
  284. }
  285. return r.updateNamespaceRemoveFinalizer(ctx, log, namespace.Name, finalizer)
  286. }
  287. // buildCESFinalizer creates the finalizer name for a CES.
  288. func (r *Reconciler) buildCESFinalizer(cesName string) string {
  289. return "externalsecrets.external-secrets.io/ces-" + cesName
  290. }
  291. // updateNamespaceRemoveFinalizer removes a finalizer from a namespace with conflict handling.
  292. func (r *Reconciler) updateNamespaceRemoveFinalizer(ctx context.Context, log logr.Logger, namespaceName, finalizer string) error {
  293. // Fetch the latest namespace to avoid conflicts
  294. namespace, err := r.getNamespace(ctx, namespaceName)
  295. if err != nil {
  296. if apierrors.IsNotFound(err) {
  297. return nil // Namespace deleted, that's OK
  298. }
  299. return fmt.Errorf("failed to get latest namespace %s: %w", namespaceName, err)
  300. }
  301. // Only update if the finalizer was actually removed
  302. if updated := controllerutil.RemoveFinalizer(namespace, finalizer); updated {
  303. if err := r.Update(ctx, namespace); err != nil {
  304. // Ignore NotFound (namespace deleted)
  305. if apierrors.IsNotFound(err) {
  306. log.V(1).Info("ignoring expected error during finalizer removal",
  307. "namespace", namespaceName,
  308. "error", err.Error())
  309. return nil
  310. }
  311. return fmt.Errorf("failed to remove finalizer from namespace %s: %w", namespaceName, err)
  312. }
  313. }
  314. return nil
  315. }
  316. func (r *Reconciler) createOrUpdateExternalSecret(ctx context.Context, clusterExternalSecret *esv1.ClusterExternalSecret, namespace v1.Namespace, esName string, esMetadata esv1.ExternalSecretMetadata) error {
  317. // Add namespace finalizer first to prevent deletion race conditions
  318. if err := r.ensureNamespaceFinalizer(ctx, &namespace, clusterExternalSecret.Name); err != nil {
  319. return err
  320. }
  321. // Create or update the ExternalSecret
  322. externalSecret := &esv1.ExternalSecret{
  323. ObjectMeta: metav1.ObjectMeta{
  324. Namespace: namespace.Name,
  325. Name: esName,
  326. },
  327. }
  328. mutateFunc := func() error {
  329. externalSecret.Labels = esMetadata.Labels
  330. externalSecret.Annotations = esMetadata.Annotations
  331. if value, ok := clusterExternalSecret.Annotations[esv1.AnnotationForceSync]; ok {
  332. if externalSecret.Annotations == nil {
  333. externalSecret.Annotations = map[string]string{}
  334. }
  335. externalSecret.Annotations[esv1.AnnotationForceSync] = value
  336. } else {
  337. delete(externalSecret.Annotations, esv1.AnnotationForceSync)
  338. }
  339. externalSecret.Spec = clusterExternalSecret.Spec.ExternalSecretSpec
  340. if err := controllerutil.SetControllerReference(clusterExternalSecret, externalSecret, r.Scheme); err != nil {
  341. return fmt.Errorf("could not set the controller owner reference %w", err)
  342. }
  343. return nil
  344. }
  345. if _, err := ctrl.CreateOrUpdate(ctx, r.Client, externalSecret, mutateFunc); err != nil {
  346. return fmt.Errorf("could not create or update ExternalSecret: %w", err)
  347. }
  348. return nil
  349. }
  350. // ensureNamespaceFinalizer adds a CES-specific finalizer to the namespace if it doesn't exist.
  351. // This prevents the namespace from being deleted while we're managing ExternalSecrets in it.
  352. func (r *Reconciler) ensureNamespaceFinalizer(ctx context.Context, namespace *v1.Namespace, cesName string) error {
  353. finalizer := r.buildCESFinalizer(cesName)
  354. if controllerutil.ContainsFinalizer(namespace, finalizer) {
  355. return nil // Already has finalizer
  356. }
  357. return r.addNamespaceFinalizer(ctx, namespace.Name, finalizer)
  358. }
  359. // addNamespaceFinalizer adds a finalizer to a namespace with conflict handling.
  360. func (r *Reconciler) addNamespaceFinalizer(ctx context.Context, namespaceName, finalizer string) error {
  361. // Fetch the latest namespace to avoid conflicts
  362. namespace, err := r.getNamespace(ctx, namespaceName)
  363. if err != nil {
  364. return fmt.Errorf("could not get latest namespace: %w", err)
  365. }
  366. // Only update if the finalizer was actually added
  367. if updated := controllerutil.AddFinalizer(namespace, finalizer); updated {
  368. if err := r.Update(ctx, namespace); err != nil {
  369. // If conflict, return error to trigger requeue
  370. if apierrors.IsConflict(err) {
  371. return err
  372. }
  373. return fmt.Errorf("could not add namespace finalizer: %w", err)
  374. }
  375. }
  376. return nil
  377. }
  378. func (r *Reconciler) deleteExternalSecret(ctx context.Context, esName, cesName, namespace string) error {
  379. var existingES esv1.ExternalSecret
  380. err := r.Get(ctx, types.NamespacedName{
  381. Name: esName,
  382. Namespace: namespace,
  383. }, &existingES)
  384. if err != nil {
  385. // If we can't find it then just leave
  386. if apierrors.IsNotFound(err) {
  387. return nil
  388. }
  389. return err
  390. }
  391. if !isExternalSecretOwnedBy(&existingES, cesName) {
  392. return nil
  393. }
  394. err = r.Delete(ctx, &existingES, &client.DeleteOptions{})
  395. if err != nil {
  396. return fmt.Errorf("external secret in non matching namespace could not be deleted: %w", err)
  397. }
  398. return nil
  399. }
  400. func (r *Reconciler) deferPatch(ctx context.Context, log logr.Logger, clusterExternalSecret *esv1.ClusterExternalSecret, p client.Patch) {
  401. if err := r.Status().Patch(ctx, clusterExternalSecret, p); err != nil {
  402. log.Error(err, errPatchStatus)
  403. }
  404. }
  405. func (r *Reconciler) deleteOutdatedExternalSecrets(ctx context.Context, namespaces []v1.Namespace, esName, cesName string, provisionedNamespaces []string) map[string]error {
  406. failedNamespaces := map[string]error{}
  407. // Loop through existing namespaces first to make sure they still have our labels
  408. for _, namespace := range getRemovedNamespaces(namespaces, provisionedNamespaces) {
  409. err := r.deleteExternalSecret(ctx, esName, cesName, namespace)
  410. if err != nil {
  411. r.Log.Error(err, "unable to delete external secret")
  412. failedNamespaces[namespace] = err
  413. }
  414. }
  415. return failedNamespaces
  416. }
  417. func isExternalSecretOwnedBy(es *esv1.ExternalSecret, cesName string) bool {
  418. owner := metav1.GetControllerOf(es)
  419. return owner != nil && schema.FromAPIVersionAndKind(owner.APIVersion, owner.Kind).GroupKind().String() == esv1.ClusterExtSecretGroupKind && owner.Name == cesName
  420. }
  421. func getRemovedNamespaces(currentNSs []v1.Namespace, provisionedNSs []string) []string {
  422. currentNSSet := map[string]struct{}{}
  423. for _, currentNs := range currentNSs {
  424. currentNSSet[currentNs.Name] = struct{}{}
  425. }
  426. var removedNSs []string
  427. for _, ns := range provisionedNSs {
  428. if _, ok := currentNSSet[ns]; !ok {
  429. removedNSs = append(removedNSs, ns)
  430. }
  431. }
  432. return removedNSs
  433. }
  434. func toNamespaceFailures(failedNamespaces map[string]error) []esv1.ClusterExternalSecretNamespaceFailure {
  435. namespaceFailures := make([]esv1.ClusterExternalSecretNamespaceFailure, len(failedNamespaces))
  436. i := 0
  437. for namespace, err := range failedNamespaces {
  438. namespaceFailures[i] = esv1.ClusterExternalSecretNamespaceFailure{
  439. Namespace: namespace,
  440. Reason: err.Error(),
  441. }
  442. i++
  443. }
  444. sort.Slice(namespaceFailures, func(i, j int) bool { return namespaceFailures[i].Namespace < namespaceFailures[j].Namespace })
  445. return namespaceFailures
  446. }
  447. // SetupWithManager sets up the controller with the Manager.
  448. func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error {
  449. return ctrl.NewControllerManagedBy(mgr).
  450. WithOptions(opts).
  451. For(&esv1.ClusterExternalSecret{}).
  452. Owns(&esv1.ExternalSecret{}).
  453. Watches(
  454. &v1.Namespace{},
  455. handler.EnqueueRequestsFromMapFunc(r.findObjectsForNamespace),
  456. builder.WithPredicates(esutils.NamespacePredicate()),
  457. ).
  458. Complete(r)
  459. }
  460. func (r *Reconciler) findObjectsForNamespace(ctx context.Context, namespace client.Object) []reconcile.Request {
  461. var clusterExternalSecrets esv1.ClusterExternalSecretList
  462. if err := r.List(ctx, &clusterExternalSecrets); err != nil {
  463. r.Log.Error(err, errGetCES)
  464. return []reconcile.Request{}
  465. }
  466. return r.queueRequestsForItem(&clusterExternalSecrets, namespace)
  467. }
  468. func (r *Reconciler) queueRequestsForItem(clusterExternalSecrets *esv1.ClusterExternalSecretList, namespace client.Object) []reconcile.Request {
  469. var requests []reconcile.Request
  470. for i := range clusterExternalSecrets.Items {
  471. clusterExternalSecret := clusterExternalSecrets.Items[i]
  472. var selectors []*metav1.LabelSelector
  473. if s := clusterExternalSecret.Spec.NamespaceSelector; s != nil {
  474. selectors = append(selectors, s)
  475. }
  476. selectors = append(selectors, clusterExternalSecret.Spec.NamespaceSelectors...)
  477. var selected bool
  478. for _, selector := range selectors {
  479. labelSelector, err := metav1.LabelSelectorAsSelector(selector)
  480. if err != nil {
  481. r.Log.Error(err, errConvertLabelSelector)
  482. continue
  483. }
  484. if labelSelector.Matches(labels.Set(namespace.GetLabels())) {
  485. requests = append(requests, reconcile.Request{
  486. NamespacedName: types.NamespacedName{
  487. Name: clusterExternalSecret.GetName(),
  488. Namespace: clusterExternalSecret.GetNamespace(),
  489. },
  490. })
  491. selected = true
  492. break
  493. }
  494. }
  495. // Prevent the object from being added twice if it happens to be listed
  496. // by Namespaces selector as well.
  497. if selected {
  498. continue
  499. }
  500. if slices.Contains(clusterExternalSecret.Spec.Namespaces, namespace.GetName()) {
  501. requests = append(requests, reconcile.Request{
  502. NamespacedName: types.NamespacedName{
  503. Name: clusterExternalSecret.GetName(),
  504. Namespace: clusterExternalSecret.GetNamespace(),
  505. },
  506. })
  507. }
  508. }
  509. return requests
  510. }