clusterpushsecret_controller.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  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 clusterpushsecret
  14. import (
  15. "context"
  16. "errors"
  17. "fmt"
  18. "sort"
  19. "time"
  20. "github.com/go-logr/logr"
  21. v1 "k8s.io/api/core/v1"
  22. apierrors "k8s.io/apimachinery/pkg/api/errors"
  23. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  24. "k8s.io/apimachinery/pkg/labels"
  25. "k8s.io/apimachinery/pkg/runtime"
  26. "k8s.io/apimachinery/pkg/types"
  27. "k8s.io/client-go/tools/record"
  28. ctrl "sigs.k8s.io/controller-runtime"
  29. "sigs.k8s.io/controller-runtime/pkg/builder"
  30. "sigs.k8s.io/controller-runtime/pkg/client"
  31. "sigs.k8s.io/controller-runtime/pkg/controller"
  32. "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
  33. "sigs.k8s.io/controller-runtime/pkg/handler"
  34. "sigs.k8s.io/controller-runtime/pkg/reconcile"
  35. "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
  36. "github.com/external-secrets/external-secrets/pkg/controllers/clusterpushsecret/cpsmetrics"
  37. "github.com/external-secrets/external-secrets/pkg/controllers/pushsecret"
  38. "github.com/external-secrets/external-secrets/pkg/utils"
  39. )
  40. // Reconciler reconciles a ClusterPushSecret object.
  41. type Reconciler struct {
  42. client.Client
  43. Log logr.Logger
  44. Scheme *runtime.Scheme
  45. RequeueInterval time.Duration
  46. Recorder record.EventRecorder
  47. }
  48. const (
  49. errPatchStatus = "error merging"
  50. errGetCES = "could not get ClusterPushSecret"
  51. errConvertLabelSelector = "unable to convert label selector"
  52. errGetExistingPS = "could not get existing PushSecret"
  53. errNamespacesFailed = "one or more namespaces failed"
  54. )
  55. func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
  56. log := r.Log.WithValues("ClusterPushSecret", req.NamespacedName)
  57. var cps v1alpha1.ClusterPushSecret
  58. err := r.Get(ctx, req.NamespacedName, &cps)
  59. if err != nil {
  60. if apierrors.IsNotFound(err) {
  61. cpsmetrics.RemoveMetrics(req.Namespace, req.Name)
  62. return ctrl.Result{}, nil
  63. }
  64. log.Error(err, errGetCES)
  65. return ctrl.Result{}, err
  66. }
  67. // skip reconciliation if deletion timestamp is set on cluster external secret
  68. if cps.DeletionTimestamp != nil {
  69. log.Info("skipping as it is in deletion")
  70. return ctrl.Result{}, nil
  71. }
  72. p := client.MergeFrom(cps.DeepCopy())
  73. defer r.deferPatch(ctx, log, &cps, p)
  74. refreshInt := r.RequeueInterval
  75. if cps.Spec.RefreshInterval != nil {
  76. refreshInt = cps.Spec.RefreshInterval.Duration
  77. }
  78. esName := cps.Spec.PushSecretName
  79. if esName == "" {
  80. esName = cps.ObjectMeta.Name
  81. }
  82. if err := r.deleteOldPushSecrets(ctx, &cps, esName, log); err != nil {
  83. return ctrl.Result{}, err
  84. }
  85. cps.Status.PushSecretName = esName
  86. namespaces, err := utils.GetTargetNamespaces(ctx, r.Client, nil, cps.Spec.NamespaceSelectors)
  87. if err != nil {
  88. log.Error(err, "failed to get target Namespaces")
  89. r.markAsFailed("failed to get target Namespaces", &cps)
  90. return ctrl.Result{}, err
  91. }
  92. failedNamespaces := r.deleteOutdatedPushSecrets(ctx, namespaces, esName, cps.Name, cps.Status.ProvisionedNamespaces)
  93. provisionedNamespaces := r.updateProvisionedNamespaces(ctx, namespaces, esName, log, failedNamespaces, &cps)
  94. condition := NewClusterPushSecretCondition(failedNamespaces)
  95. SetClusterPushSecretCondition(&cps, *condition)
  96. cps.Status.FailedNamespaces = toNamespaceFailures(failedNamespaces)
  97. sort.Strings(provisionedNamespaces)
  98. cps.Status.ProvisionedNamespaces = provisionedNamespaces
  99. return ctrl.Result{RequeueAfter: refreshInt}, nil
  100. }
  101. func (r *Reconciler) updateProvisionedNamespaces(
  102. ctx context.Context,
  103. namespaces []v1.Namespace,
  104. esName string,
  105. log logr.Logger,
  106. failedNamespaces map[string]error,
  107. cps *v1alpha1.ClusterPushSecret,
  108. ) []string {
  109. var provisionedNamespaces []string //nolint:prealloc // I have no idea what the size will be.
  110. for _, namespace := range namespaces {
  111. var pushSecret v1alpha1.PushSecret
  112. err := r.Get(ctx, types.NamespacedName{
  113. Name: esName,
  114. Namespace: namespace.Name,
  115. }, &pushSecret)
  116. if err != nil && !apierrors.IsNotFound(err) {
  117. log.Error(err, errGetExistingPS)
  118. failedNamespaces[namespace.Name] = err
  119. continue
  120. }
  121. if err == nil && !isPushSecretOwnedBy(&pushSecret, cps.Name) {
  122. failedNamespaces[namespace.Name] = errors.New("push secret already exists in namespace")
  123. continue
  124. }
  125. if err := r.createOrUpdatePushSecret(ctx, cps, namespace, esName, cps.Spec.PushSecretMetadata); err != nil {
  126. log.Error(err, "failed to create or update push secret")
  127. failedNamespaces[namespace.Name] = err
  128. continue
  129. }
  130. provisionedNamespaces = append(provisionedNamespaces, namespace.Name)
  131. }
  132. return provisionedNamespaces
  133. }
  134. func (r *Reconciler) deleteOldPushSecrets(ctx context.Context, cps *v1alpha1.ClusterPushSecret, esName string, log logr.Logger) error {
  135. var lastErr error
  136. if prevName := cps.Status.PushSecretName; prevName != esName {
  137. // PushSecretName has changed, so remove the old ones
  138. failedNamespaces := map[string]error{}
  139. for _, ns := range cps.Status.ProvisionedNamespaces {
  140. if err := r.deletePushSecret(ctx, prevName, cps.Name, ns); err != nil {
  141. log.Error(err, "could not delete PushSecret")
  142. failedNamespaces[ns] = err
  143. lastErr = err
  144. }
  145. }
  146. if len(failedNamespaces) > 0 {
  147. r.markAsFailed("failed to delete push secret", cps)
  148. cps.Status.FailedNamespaces = toNamespaceFailures(failedNamespaces)
  149. return lastErr
  150. }
  151. }
  152. return nil
  153. }
  154. func (r *Reconciler) markAsFailed(msg string, ps *v1alpha1.ClusterPushSecret) {
  155. cond := pushsecret.NewPushSecretCondition(v1alpha1.PushSecretReady, v1.ConditionFalse, v1alpha1.ReasonErrored, msg)
  156. setClusterPushSecretCondition(ps, *cond)
  157. r.Recorder.Event(ps, v1.EventTypeWarning, v1alpha1.ReasonErrored, msg)
  158. }
  159. func setClusterPushSecretCondition(ps *v1alpha1.ClusterPushSecret, condition v1alpha1.PushSecretStatusCondition) {
  160. currentCond := pushsecret.GetPushSecretCondition(ps.Status.Conditions, condition.Type)
  161. if currentCond != nil && currentCond.Status == condition.Status &&
  162. currentCond.Reason == condition.Reason && currentCond.Message == condition.Message {
  163. return
  164. }
  165. // Do not update lastTransitionTime if the status of the condition doesn't change.
  166. if currentCond != nil && currentCond.Status == condition.Status {
  167. condition.LastTransitionTime = currentCond.LastTransitionTime
  168. }
  169. ps.Status.Conditions = append(pushsecret.FilterOutCondition(ps.Status.Conditions, condition.Type), condition)
  170. }
  171. func (r *Reconciler) createOrUpdatePushSecret(ctx context.Context, csp *v1alpha1.ClusterPushSecret, namespace v1.Namespace, esName string, esMetadata v1alpha1.PushSecretMetadata) error {
  172. pushSecret := &v1alpha1.PushSecret{
  173. ObjectMeta: metav1.ObjectMeta{
  174. Namespace: namespace.Name,
  175. Name: esName,
  176. },
  177. }
  178. mutateFunc := func() error {
  179. pushSecret.Labels = esMetadata.Labels
  180. pushSecret.Annotations = esMetadata.Annotations
  181. pushSecret.Spec = csp.Spec.PushSecretSpec
  182. if err := controllerutil.SetControllerReference(csp, pushSecret, r.Scheme); err != nil {
  183. return fmt.Errorf("could not set the controller owner reference %w", err)
  184. }
  185. return nil
  186. }
  187. if _, err := ctrl.CreateOrUpdate(ctx, r.Client, pushSecret, mutateFunc); err != nil {
  188. return fmt.Errorf("could not create or update push secret: %w", err)
  189. }
  190. return nil
  191. }
  192. func (r *Reconciler) deletePushSecret(ctx context.Context, esName, cesName, namespace string) error {
  193. var existingPs v1alpha1.PushSecret
  194. err := r.Get(ctx, types.NamespacedName{
  195. Name: esName,
  196. Namespace: namespace,
  197. }, &existingPs)
  198. if err != nil {
  199. // If we can't find it then just leave
  200. if apierrors.IsNotFound(err) {
  201. return nil
  202. }
  203. return err
  204. }
  205. if !isPushSecretOwnedBy(&existingPs, cesName) {
  206. return nil
  207. }
  208. err = r.Delete(ctx, &existingPs, &client.DeleteOptions{})
  209. if err != nil {
  210. return fmt.Errorf("external secret in non matching namespace could not be deleted: %w", err)
  211. }
  212. return nil
  213. }
  214. func (r *Reconciler) deferPatch(ctx context.Context, log logr.Logger, cps *v1alpha1.ClusterPushSecret, p client.Patch) {
  215. if err := r.Status().Patch(ctx, cps, p); err != nil {
  216. log.Error(err, errPatchStatus)
  217. }
  218. }
  219. func (r *Reconciler) deleteOutdatedPushSecrets(ctx context.Context, namespaces []v1.Namespace, esName, cesName string, provisionedNamespaces []string) map[string]error {
  220. failedNamespaces := map[string]error{}
  221. // Loop through existing namespaces first to make sure they still have our labels
  222. for _, namespace := range getRemovedNamespaces(namespaces, provisionedNamespaces) {
  223. err := r.deletePushSecret(ctx, esName, cesName, namespace)
  224. if err != nil {
  225. r.Log.Error(err, "unable to delete external secret")
  226. failedNamespaces[namespace] = err
  227. }
  228. }
  229. return failedNamespaces
  230. }
  231. func isPushSecretOwnedBy(ps *v1alpha1.PushSecret, cesName string) bool {
  232. owner := metav1.GetControllerOf(ps)
  233. return owner != nil && owner.APIVersion == v1alpha1.SchemeGroupVersion.String() && owner.Kind == "ClusterPushSecret" && owner.Name == cesName
  234. }
  235. func getRemovedNamespaces(currentNSs []v1.Namespace, provisionedNSs []string) []string {
  236. currentNSSet := map[string]struct{}{}
  237. for _, currentNs := range currentNSs {
  238. currentNSSet[currentNs.Name] = struct{}{}
  239. }
  240. var removedNSs []string
  241. for _, ns := range provisionedNSs {
  242. if _, ok := currentNSSet[ns]; !ok {
  243. removedNSs = append(removedNSs, ns)
  244. }
  245. }
  246. return removedNSs
  247. }
  248. func toNamespaceFailures(failedNamespaces map[string]error) []v1alpha1.ClusterPushSecretNamespaceFailure {
  249. namespaceFailures := make([]v1alpha1.ClusterPushSecretNamespaceFailure, len(failedNamespaces))
  250. i := 0
  251. for namespace, err := range failedNamespaces {
  252. namespaceFailures[i] = v1alpha1.ClusterPushSecretNamespaceFailure{
  253. Namespace: namespace,
  254. Reason: err.Error(),
  255. }
  256. i++
  257. }
  258. sort.Slice(namespaceFailures, func(i, j int) bool { return namespaceFailures[i].Namespace < namespaceFailures[j].Namespace })
  259. return namespaceFailures
  260. }
  261. // SetupWithManager sets up the controller with the Manager.
  262. func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error {
  263. return ctrl.NewControllerManagedBy(mgr).
  264. WithOptions(opts).
  265. For(&v1alpha1.ClusterPushSecret{}).
  266. Owns(&v1alpha1.PushSecret{}).
  267. Watches(
  268. &v1.Namespace{},
  269. handler.EnqueueRequestsFromMapFunc(r.findObjectsForNamespace),
  270. builder.WithPredicates(utils.NamespacePredicate()),
  271. ).
  272. Complete(r)
  273. }
  274. func (r *Reconciler) findObjectsForNamespace(ctx context.Context, namespace client.Object) []reconcile.Request {
  275. var cpsl v1alpha1.ClusterPushSecretList
  276. if err := r.List(ctx, &cpsl); err != nil {
  277. r.Log.Error(err, errGetCES)
  278. return []reconcile.Request{}
  279. }
  280. var requests []reconcile.Request
  281. for i := range cpsl.Items {
  282. cps := &cpsl.Items[i]
  283. for _, selector := range cps.Spec.NamespaceSelectors {
  284. labelSelector, err := metav1.LabelSelectorAsSelector(selector)
  285. if err != nil {
  286. r.Log.Error(err, errConvertLabelSelector)
  287. continue
  288. }
  289. if labelSelector.Matches(labels.Set(namespace.GetLabels())) {
  290. requests = append(requests, reconcile.Request{
  291. NamespacedName: types.NamespacedName{
  292. Name: cps.GetName(),
  293. Namespace: cps.GetNamespace(),
  294. },
  295. })
  296. break
  297. }
  298. }
  299. }
  300. return requests
  301. }