clusterexternalsecret_controller.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  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. // Skip namespace if it's being deleted
  172. if namespace.DeletionTimestamp != nil {
  173. log.Info("skipping namespace as it is being deleted", "namespace", namespace.Name)
  174. continue
  175. }
  176. var existingES esv1.ExternalSecret
  177. err := r.Get(ctx, types.NamespacedName{
  178. Name: esName,
  179. Namespace: namespace.Name,
  180. }, &existingES)
  181. if err != nil && !apierrors.IsNotFound(err) {
  182. log.Error(err, errGetExistingES)
  183. failedNamespaces[namespace.Name] = err
  184. continue
  185. }
  186. if err == nil && !isExternalSecretOwnedBy(&existingES, clusterExternalSecret.Name) {
  187. failedNamespaces[namespace.Name] = errors.New("external secret already exists in namespace")
  188. continue
  189. }
  190. if err := r.createOrUpdateExternalSecret(ctx, clusterExternalSecret, namespace, esName, clusterExternalSecret.Spec.ExternalSecretMetadata); err != nil {
  191. // If conflict, don't log as error - just add to failed namespaces for retry
  192. if apierrors.IsConflict(err) {
  193. log.V(1).Info("conflict while updating namespace, will retry", "namespace", namespace.Name)
  194. failedNamespaces[namespace.Name] = err
  195. continue
  196. }
  197. log.Error(err, "failed to create or update external secret")
  198. failedNamespaces[namespace.Name] = err
  199. continue
  200. }
  201. provisionedNamespaces = append(provisionedNamespaces, namespace.Name)
  202. }
  203. return provisionedNamespaces
  204. }
  205. func (r *Reconciler) removeOldSecrets(ctx context.Context, log logr.Logger, clusterExternalSecret *esv1.ClusterExternalSecret, prevName string) error {
  206. var (
  207. failedNamespaces = map[string]error{}
  208. lastErr error
  209. )
  210. for _, ns := range clusterExternalSecret.Status.ProvisionedNamespaces {
  211. if err := r.deleteExternalSecret(ctx, prevName, clusterExternalSecret.Name, ns); err != nil {
  212. log.Error(err, "could not delete ExternalSecret")
  213. failedNamespaces[ns] = err
  214. lastErr = err
  215. }
  216. }
  217. if len(failedNamespaces) > 0 {
  218. condition := NewClusterExternalSecretCondition(failedNamespaces)
  219. SetClusterExternalSecretCondition(clusterExternalSecret, *condition)
  220. clusterExternalSecret.Status.FailedNamespaces = toNamespaceFailures(failedNamespaces)
  221. return lastErr
  222. }
  223. return nil
  224. }
  225. // cleanupExternalSecrets removes all ExternalSecrets created by this ClusterExternalSecret
  226. // and removes the namespace finalizers we added. This uses a dual-finalizer strategy:
  227. // 1. ClusterExternalSecret has a finalizer to ensure cleanup happens
  228. // 2. Each namespace gets a CES-specific finalizer to prevent deletion race conditions
  229. // The namespace finalizer is named with the CES name to allow multiple CES resources
  230. // to provision into the same namespace independently.
  231. func (r *Reconciler) cleanupExternalSecrets(ctx context.Context, log logr.Logger, clusterExternalSecret *esv1.ClusterExternalSecret) error {
  232. esName := r.getExternalSecretName(clusterExternalSecret)
  233. var err error
  234. for _, ns := range clusterExternalSecret.Status.ProvisionedNamespaces {
  235. if cleanupErr := r.cleanupNamespaceResources(ctx, log, clusterExternalSecret, ns, esName); cleanupErr != nil {
  236. err = errors.Join(err, cleanupErr)
  237. }
  238. }
  239. return err
  240. }
  241. // getExternalSecretName returns the name to use for ExternalSecrets.
  242. func (r *Reconciler) getExternalSecretName(clusterExternalSecret *esv1.ClusterExternalSecret) string {
  243. if clusterExternalSecret.Status.ExternalSecretName != "" {
  244. return clusterExternalSecret.Status.ExternalSecretName
  245. }
  246. return clusterExternalSecret.ObjectMeta.Name
  247. }
  248. // cleanupNamespaceResources handles cleanup for a single namespace.
  249. func (r *Reconciler) cleanupNamespaceResources(ctx context.Context, log logr.Logger, clusterExternalSecret *esv1.ClusterExternalSecret, namespaceName, esName string) error {
  250. // Get the namespace
  251. namespace, err := r.getNamespace(ctx, namespaceName)
  252. if err != nil {
  253. if apierrors.IsNotFound(err) {
  254. // Namespace already deleted, that's ok
  255. return nil
  256. }
  257. return fmt.Errorf("failed to get namespace %s: %w", namespaceName, err)
  258. }
  259. // Remove namespace finalizer
  260. if err := r.removeNamespaceFinalizer(ctx, log, namespace, clusterExternalSecret.Name); err != nil {
  261. return err
  262. }
  263. // Delete ExternalSecret
  264. if err := r.deleteExternalSecret(ctx, esName, clusterExternalSecret.Name, namespaceName); err != nil {
  265. return fmt.Errorf("failed to delete ExternalSecret in namespace %s: %w", namespaceName, err)
  266. }
  267. return nil
  268. }
  269. // getNamespace fetches a namespace by name.
  270. func (r *Reconciler) getNamespace(ctx context.Context, name string) (*v1.Namespace, error) {
  271. var namespace v1.Namespace
  272. err := r.Get(ctx, types.NamespacedName{Name: name}, &namespace)
  273. return &namespace, err
  274. }
  275. // removeNamespaceFinalizer removes the CES-specific finalizer from a namespace.
  276. func (r *Reconciler) removeNamespaceFinalizer(ctx context.Context, log logr.Logger, namespace *v1.Namespace, cesName string) error {
  277. finalizer := r.buildCESFinalizer(cesName)
  278. if !controllerutil.ContainsFinalizer(namespace, finalizer) {
  279. return nil // Finalizer doesn't exist, nothing to do
  280. }
  281. return r.updateNamespaceRemoveFinalizer(ctx, log, namespace.Name, finalizer)
  282. }
  283. // buildCESFinalizer creates the finalizer name for a CES.
  284. func (r *Reconciler) buildCESFinalizer(cesName string) string {
  285. return "externalsecrets.external-secrets.io/ces-" + cesName
  286. }
  287. // updateNamespaceRemoveFinalizer removes a finalizer from a namespace with conflict handling.
  288. func (r *Reconciler) updateNamespaceRemoveFinalizer(ctx context.Context, log logr.Logger, namespaceName, finalizer string) error {
  289. // Fetch the latest namespace to avoid conflicts
  290. namespace, err := r.getNamespace(ctx, namespaceName)
  291. if err != nil {
  292. if apierrors.IsNotFound(err) {
  293. return nil // Namespace deleted, that's OK
  294. }
  295. return fmt.Errorf("failed to get latest namespace %s: %w", namespaceName, err)
  296. }
  297. // Only update if the finalizer was actually removed
  298. if updated := controllerutil.RemoveFinalizer(namespace, finalizer); updated {
  299. if err := r.Update(ctx, namespace); err != nil {
  300. // Ignore NotFound (namespace deleted) and Conflict (will retry)
  301. if apierrors.IsNotFound(err) || apierrors.IsConflict(err) {
  302. log.V(1).Info("ignoring expected error during finalizer removal",
  303. "namespace", namespaceName,
  304. "error", err.Error())
  305. return nil
  306. }
  307. return fmt.Errorf("failed to remove finalizer from namespace %s: %w", namespaceName, err)
  308. }
  309. }
  310. return nil
  311. }
  312. func (r *Reconciler) createOrUpdateExternalSecret(ctx context.Context, clusterExternalSecret *esv1.ClusterExternalSecret, namespace v1.Namespace, esName string, esMetadata esv1.ExternalSecretMetadata) error {
  313. // Add namespace finalizer first to prevent deletion race conditions
  314. if err := r.ensureNamespaceFinalizer(ctx, &namespace, clusterExternalSecret.Name); err != nil {
  315. return err
  316. }
  317. // Create or update the ExternalSecret
  318. externalSecret := &esv1.ExternalSecret{
  319. ObjectMeta: metav1.ObjectMeta{
  320. Namespace: namespace.Name,
  321. Name: esName,
  322. },
  323. }
  324. mutateFunc := func() error {
  325. externalSecret.Labels = esMetadata.Labels
  326. externalSecret.Annotations = esMetadata.Annotations
  327. if value, ok := clusterExternalSecret.Annotations[esv1.AnnotationForceSync]; ok {
  328. if externalSecret.Annotations == nil {
  329. externalSecret.Annotations = map[string]string{}
  330. }
  331. externalSecret.Annotations[esv1.AnnotationForceSync] = value
  332. } else {
  333. delete(externalSecret.Annotations, esv1.AnnotationForceSync)
  334. }
  335. externalSecret.Spec = clusterExternalSecret.Spec.ExternalSecretSpec
  336. if err := controllerutil.SetControllerReference(clusterExternalSecret, externalSecret, r.Scheme); err != nil {
  337. return fmt.Errorf("could not set the controller owner reference %w", err)
  338. }
  339. return nil
  340. }
  341. if _, err := ctrl.CreateOrUpdate(ctx, r.Client, externalSecret, mutateFunc); err != nil {
  342. return fmt.Errorf("could not create or update ExternalSecret: %w", err)
  343. }
  344. return nil
  345. }
  346. // ensureNamespaceFinalizer adds a CES-specific finalizer to the namespace if it doesn't exist.
  347. // This prevents the namespace from being deleted while we're managing ExternalSecrets in it.
  348. func (r *Reconciler) ensureNamespaceFinalizer(ctx context.Context, namespace *v1.Namespace, cesName string) error {
  349. finalizer := r.buildCESFinalizer(cesName)
  350. if controllerutil.ContainsFinalizer(namespace, finalizer) {
  351. return nil // Already has finalizer
  352. }
  353. return r.addNamespaceFinalizer(ctx, namespace.Name, finalizer)
  354. }
  355. // addNamespaceFinalizer adds a finalizer to a namespace with conflict handling.
  356. func (r *Reconciler) addNamespaceFinalizer(ctx context.Context, namespaceName, finalizer string) error {
  357. // Fetch the latest namespace to avoid conflicts
  358. namespace, err := r.getNamespace(ctx, namespaceName)
  359. if err != nil {
  360. return fmt.Errorf("could not get latest namespace: %w", err)
  361. }
  362. // Only update if the finalizer was actually added
  363. if updated := controllerutil.AddFinalizer(namespace, finalizer); updated {
  364. if err := r.Update(ctx, namespace); err != nil {
  365. // If conflict, return error to trigger requeue
  366. if apierrors.IsConflict(err) {
  367. return err
  368. }
  369. return fmt.Errorf("could not add namespace finalizer: %w", err)
  370. }
  371. }
  372. return nil
  373. }
  374. func (r *Reconciler) deleteExternalSecret(ctx context.Context, esName, cesName, namespace string) error {
  375. var existingES esv1.ExternalSecret
  376. err := r.Get(ctx, types.NamespacedName{
  377. Name: esName,
  378. Namespace: namespace,
  379. }, &existingES)
  380. if err != nil {
  381. // If we can't find it then just leave
  382. if apierrors.IsNotFound(err) {
  383. return nil
  384. }
  385. return err
  386. }
  387. if !isExternalSecretOwnedBy(&existingES, cesName) {
  388. return nil
  389. }
  390. err = r.Delete(ctx, &existingES, &client.DeleteOptions{})
  391. if err != nil {
  392. return fmt.Errorf("external secret in non matching namespace could not be deleted: %w", err)
  393. }
  394. return nil
  395. }
  396. func (r *Reconciler) deferPatch(ctx context.Context, log logr.Logger, clusterExternalSecret *esv1.ClusterExternalSecret, p client.Patch) {
  397. if err := r.Status().Patch(ctx, clusterExternalSecret, p); err != nil {
  398. log.Error(err, errPatchStatus)
  399. }
  400. }
  401. func (r *Reconciler) deleteOutdatedExternalSecrets(ctx context.Context, namespaces []v1.Namespace, esName, cesName string, provisionedNamespaces []string) map[string]error {
  402. failedNamespaces := map[string]error{}
  403. // Loop through existing namespaces first to make sure they still have our labels
  404. for _, namespace := range getRemovedNamespaces(namespaces, provisionedNamespaces) {
  405. err := r.deleteExternalSecret(ctx, esName, cesName, namespace)
  406. if err != nil {
  407. r.Log.Error(err, "unable to delete external secret")
  408. failedNamespaces[namespace] = err
  409. }
  410. }
  411. return failedNamespaces
  412. }
  413. func isExternalSecretOwnedBy(es *esv1.ExternalSecret, cesName string) bool {
  414. owner := metav1.GetControllerOf(es)
  415. return owner != nil && schema.FromAPIVersionAndKind(owner.APIVersion, owner.Kind).GroupKind().String() == esv1.ClusterExtSecretGroupKind && owner.Name == cesName
  416. }
  417. func getRemovedNamespaces(currentNSs []v1.Namespace, provisionedNSs []string) []string {
  418. currentNSSet := map[string]struct{}{}
  419. for _, currentNs := range currentNSs {
  420. currentNSSet[currentNs.Name] = struct{}{}
  421. }
  422. var removedNSs []string
  423. for _, ns := range provisionedNSs {
  424. if _, ok := currentNSSet[ns]; !ok {
  425. removedNSs = append(removedNSs, ns)
  426. }
  427. }
  428. return removedNSs
  429. }
  430. func toNamespaceFailures(failedNamespaces map[string]error) []esv1.ClusterExternalSecretNamespaceFailure {
  431. namespaceFailures := make([]esv1.ClusterExternalSecretNamespaceFailure, len(failedNamespaces))
  432. i := 0
  433. for namespace, err := range failedNamespaces {
  434. namespaceFailures[i] = esv1.ClusterExternalSecretNamespaceFailure{
  435. Namespace: namespace,
  436. Reason: err.Error(),
  437. }
  438. i++
  439. }
  440. sort.Slice(namespaceFailures, func(i, j int) bool { return namespaceFailures[i].Namespace < namespaceFailures[j].Namespace })
  441. return namespaceFailures
  442. }
  443. // SetupWithManager sets up the controller with the Manager.
  444. func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error {
  445. return ctrl.NewControllerManagedBy(mgr).
  446. WithOptions(opts).
  447. For(&esv1.ClusterExternalSecret{}).
  448. Owns(&esv1.ExternalSecret{}).
  449. Watches(
  450. &v1.Namespace{},
  451. handler.EnqueueRequestsFromMapFunc(r.findObjectsForNamespace),
  452. builder.WithPredicates(esutils.NamespacePredicate()),
  453. ).
  454. Complete(r)
  455. }
  456. func (r *Reconciler) findObjectsForNamespace(ctx context.Context, namespace client.Object) []reconcile.Request {
  457. var clusterExternalSecrets esv1.ClusterExternalSecretList
  458. if err := r.List(ctx, &clusterExternalSecrets); err != nil {
  459. r.Log.Error(err, errGetCES)
  460. return []reconcile.Request{}
  461. }
  462. return r.queueRequestsForItem(&clusterExternalSecrets, namespace)
  463. }
  464. func (r *Reconciler) queueRequestsForItem(clusterExternalSecrets *esv1.ClusterExternalSecretList, namespace client.Object) []reconcile.Request {
  465. var requests []reconcile.Request
  466. for i := range clusterExternalSecrets.Items {
  467. clusterExternalSecret := clusterExternalSecrets.Items[i]
  468. var selectors []*metav1.LabelSelector
  469. if s := clusterExternalSecret.Spec.NamespaceSelector; s != nil {
  470. selectors = append(selectors, s)
  471. }
  472. selectors = append(selectors, clusterExternalSecret.Spec.NamespaceSelectors...)
  473. var selected bool
  474. for _, selector := range selectors {
  475. labelSelector, err := metav1.LabelSelectorAsSelector(selector)
  476. if err != nil {
  477. r.Log.Error(err, errConvertLabelSelector)
  478. continue
  479. }
  480. if labelSelector.Matches(labels.Set(namespace.GetLabels())) {
  481. requests = append(requests, reconcile.Request{
  482. NamespacedName: types.NamespacedName{
  483. Name: clusterExternalSecret.GetName(),
  484. Namespace: clusterExternalSecret.GetNamespace(),
  485. },
  486. })
  487. selected = true
  488. break
  489. }
  490. }
  491. // Prevent the object from being added twice if it happens to be listed
  492. // by Namespaces selector as well.
  493. if selected {
  494. continue
  495. }
  496. if slices.Contains(clusterExternalSecret.Spec.Namespaces, namespace.GetName()) {
  497. requests = append(requests, reconcile.Request{
  498. NamespacedName: types.NamespacedName{
  499. Name: clusterExternalSecret.GetName(),
  500. Namespace: clusterExternalSecret.GetNamespace(),
  501. },
  502. })
  503. }
  504. }
  505. return requests
  506. }