clusterexternalsecret_controller.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. /*
  2. Licensed under the Apache License, Version 2.0 (the "License");
  3. you may not use this file except in compliance with the License.
  4. You may obtain a copy of the License at
  5. http://www.apache.org/licenses/LICENSE-2.0
  6. Unless required by applicable law or agreed to in writing, software
  7. distributed under the License is distributed on an "AS IS" BASIS,
  8. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  9. See the License for the specific language governing permissions and
  10. limitations under the License.
  11. */
  12. package clusterexternalsecret
  13. import (
  14. "context"
  15. "errors"
  16. "fmt"
  17. "slices"
  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/runtime/schema"
  27. "k8s.io/apimachinery/pkg/types"
  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. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  36. "github.com/external-secrets/external-secrets/pkg/controllers/clusterexternalsecret/cesmetrics"
  37. ctrlmetrics "github.com/external-secrets/external-secrets/pkg/controllers/metrics"
  38. "github.com/external-secrets/external-secrets/pkg/utils"
  39. )
  40. // Reconciler reconciles a ClusterExternalSecret object.
  41. type Reconciler struct {
  42. client.Client
  43. Log logr.Logger
  44. Scheme *runtime.Scheme
  45. RequeueInterval time.Duration
  46. }
  47. const (
  48. errGetCES = "could not get ClusterExternalSecret"
  49. errPatchStatus = "unable to patch status"
  50. errConvertLabelSelector = "unable to convert labelselector"
  51. errGetExistingES = "could not get existing ExternalSecret"
  52. errNamespacesFailed = "one or more namespaces failed"
  53. )
  54. func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
  55. log := r.Log.WithValues("ClusterExternalSecret", req.NamespacedName)
  56. resourceLabels := ctrlmetrics.RefineNonConditionMetricLabels(map[string]string{"name": req.Name, "namespace": req.Namespace})
  57. start := time.Now()
  58. externalSecretReconcileDuration := cesmetrics.GetGaugeVec(cesmetrics.ClusterExternalSecretReconcileDurationKey)
  59. defer func() { externalSecretReconcileDuration.With(resourceLabels).Set(float64(time.Since(start))) }()
  60. var clusterExternalSecret esv1.ClusterExternalSecret
  61. err := r.Get(ctx, req.NamespacedName, &clusterExternalSecret)
  62. if err != nil {
  63. if apierrors.IsNotFound(err) {
  64. cesmetrics.RemoveMetrics(req.Namespace, req.Name)
  65. return ctrl.Result{}, nil
  66. }
  67. log.Error(err, errGetCES)
  68. return ctrl.Result{}, err
  69. }
  70. // skip reconciliation if deletion timestamp is set on cluster external secret
  71. if clusterExternalSecret.DeletionTimestamp != nil {
  72. log.Info("skipping as it is in deletion")
  73. return ctrl.Result{}, nil
  74. }
  75. p := client.MergeFrom(clusterExternalSecret.DeepCopy())
  76. defer r.deferPatch(ctx, log, &clusterExternalSecret, p)
  77. return r.reconcile(ctx, log, &clusterExternalSecret)
  78. }
  79. func (r *Reconciler) reconcile(ctx context.Context, log logr.Logger, clusterExternalSecret *esv1.ClusterExternalSecret) (ctrl.Result, error) {
  80. refreshInt := r.RequeueInterval
  81. if clusterExternalSecret.Spec.RefreshInterval != nil {
  82. refreshInt = clusterExternalSecret.Spec.RefreshInterval.Duration
  83. }
  84. esName := clusterExternalSecret.Spec.ExternalSecretName
  85. if esName == "" {
  86. esName = clusterExternalSecret.ObjectMeta.Name
  87. }
  88. if prevName := clusterExternalSecret.Status.ExternalSecretName; prevName != "" && prevName != esName {
  89. // ExternalSecretName has changed, so remove the old ones
  90. if err := r.removeOldSecrets(ctx, log, clusterExternalSecret, prevName); err != nil {
  91. return ctrl.Result{}, err
  92. }
  93. }
  94. clusterExternalSecret.Status.ExternalSecretName = esName
  95. selectors := []*metav1.LabelSelector{}
  96. if s := clusterExternalSecret.Spec.NamespaceSelector; s != nil {
  97. selectors = append(selectors, s)
  98. }
  99. selectors = append(selectors, clusterExternalSecret.Spec.NamespaceSelectors...)
  100. namespaces, err := utils.GetTargetNamespaces(ctx, r.Client, clusterExternalSecret.Spec.Namespaces, selectors)
  101. if err != nil {
  102. log.Error(err, "failed to get target Namespaces")
  103. failedNamespaces := map[string]error{
  104. "unknown": err,
  105. }
  106. condition := NewClusterExternalSecretCondition(failedNamespaces)
  107. SetClusterExternalSecretCondition(clusterExternalSecret, *condition)
  108. clusterExternalSecret.Status.FailedNamespaces = toNamespaceFailures(failedNamespaces)
  109. return ctrl.Result{}, err
  110. }
  111. failedNamespaces := r.deleteOutdatedExternalSecrets(ctx, namespaces, esName, clusterExternalSecret.Name, clusterExternalSecret.Status.ProvisionedNamespaces)
  112. provisionedNamespaces := r.gatherProvisionedNamespaces(ctx, log, clusterExternalSecret, namespaces, esName, failedNamespaces)
  113. condition := NewClusterExternalSecretCondition(failedNamespaces)
  114. SetClusterExternalSecretCondition(clusterExternalSecret, *condition)
  115. clusterExternalSecret.Status.FailedNamespaces = toNamespaceFailures(failedNamespaces)
  116. sort.Strings(provisionedNamespaces)
  117. clusterExternalSecret.Status.ProvisionedNamespaces = provisionedNamespaces
  118. return ctrl.Result{RequeueAfter: refreshInt}, nil
  119. }
  120. func (r *Reconciler) gatherProvisionedNamespaces(
  121. ctx context.Context,
  122. log logr.Logger,
  123. clusterExternalSecret *esv1.ClusterExternalSecret,
  124. namespaces []v1.Namespace,
  125. esName string,
  126. failedNamespaces map[string]error,
  127. ) []string {
  128. var provisionedNamespaces []string //nolint:prealloc // we don't know the size
  129. for _, namespace := range namespaces {
  130. var existingES esv1.ExternalSecret
  131. err := r.Get(ctx, types.NamespacedName{
  132. Name: esName,
  133. Namespace: namespace.Name,
  134. }, &existingES)
  135. if err != nil && !apierrors.IsNotFound(err) {
  136. log.Error(err, errGetExistingES)
  137. failedNamespaces[namespace.Name] = err
  138. continue
  139. }
  140. if err == nil && !isExternalSecretOwnedBy(&existingES, clusterExternalSecret.Name) {
  141. failedNamespaces[namespace.Name] = errors.New("external secret already exists in namespace")
  142. continue
  143. }
  144. if err := r.createOrUpdateExternalSecret(ctx, clusterExternalSecret, namespace, esName, clusterExternalSecret.Spec.ExternalSecretMetadata); err != nil {
  145. log.Error(err, "failed to create or update external secret")
  146. failedNamespaces[namespace.Name] = err
  147. continue
  148. }
  149. provisionedNamespaces = append(provisionedNamespaces, namespace.Name)
  150. }
  151. return provisionedNamespaces
  152. }
  153. func (r *Reconciler) removeOldSecrets(ctx context.Context, log logr.Logger, clusterExternalSecret *esv1.ClusterExternalSecret, prevName string) error {
  154. var (
  155. failedNamespaces = map[string]error{}
  156. lastErr error
  157. )
  158. for _, ns := range clusterExternalSecret.Status.ProvisionedNamespaces {
  159. if err := r.deleteExternalSecret(ctx, prevName, clusterExternalSecret.Name, ns); err != nil {
  160. log.Error(err, "could not delete ExternalSecret")
  161. failedNamespaces[ns] = err
  162. lastErr = err
  163. }
  164. }
  165. if len(failedNamespaces) > 0 {
  166. condition := NewClusterExternalSecretCondition(failedNamespaces)
  167. SetClusterExternalSecretCondition(clusterExternalSecret, *condition)
  168. clusterExternalSecret.Status.FailedNamespaces = toNamespaceFailures(failedNamespaces)
  169. return lastErr
  170. }
  171. return nil
  172. }
  173. func (r *Reconciler) createOrUpdateExternalSecret(ctx context.Context, clusterExternalSecret *esv1.ClusterExternalSecret, namespace v1.Namespace, esName string, esMetadata esv1.ExternalSecretMetadata) error {
  174. externalSecret := &esv1.ExternalSecret{
  175. ObjectMeta: metav1.ObjectMeta{
  176. Namespace: namespace.Name,
  177. Name: esName,
  178. },
  179. }
  180. mutateFunc := func() error {
  181. externalSecret.Labels = esMetadata.Labels
  182. externalSecret.Annotations = esMetadata.Annotations
  183. if value, ok := clusterExternalSecret.Annotations[esv1.AnnotationForceSync]; ok {
  184. if externalSecret.Annotations == nil {
  185. externalSecret.Annotations = map[string]string{}
  186. }
  187. externalSecret.Annotations[esv1.AnnotationForceSync] = value
  188. } else {
  189. delete(externalSecret.Annotations, esv1.AnnotationForceSync)
  190. }
  191. externalSecret.Spec = clusterExternalSecret.Spec.ExternalSecretSpec
  192. if err := controllerutil.SetControllerReference(clusterExternalSecret, externalSecret, r.Scheme); err != nil {
  193. return fmt.Errorf("could not set the controller owner reference %w", err)
  194. }
  195. return nil
  196. }
  197. if _, err := ctrl.CreateOrUpdate(ctx, r.Client, externalSecret, mutateFunc); err != nil {
  198. return fmt.Errorf("could not create or update ExternalSecret: %w", err)
  199. }
  200. return nil
  201. }
  202. func (r *Reconciler) deleteExternalSecret(ctx context.Context, esName, cesName, namespace string) error {
  203. var existingES esv1.ExternalSecret
  204. err := r.Get(ctx, types.NamespacedName{
  205. Name: esName,
  206. Namespace: namespace,
  207. }, &existingES)
  208. if err != nil {
  209. // If we can't find it then just leave
  210. if apierrors.IsNotFound(err) {
  211. return nil
  212. }
  213. return err
  214. }
  215. if !isExternalSecretOwnedBy(&existingES, cesName) {
  216. return nil
  217. }
  218. err = r.Delete(ctx, &existingES, &client.DeleteOptions{})
  219. if err != nil {
  220. return fmt.Errorf("external secret in non matching namespace could not be deleted: %w", err)
  221. }
  222. return nil
  223. }
  224. func (r *Reconciler) deferPatch(ctx context.Context, log logr.Logger, clusterExternalSecret *esv1.ClusterExternalSecret, p client.Patch) {
  225. if err := r.Status().Patch(ctx, clusterExternalSecret, p); err != nil {
  226. log.Error(err, errPatchStatus)
  227. }
  228. }
  229. func (r *Reconciler) deleteOutdatedExternalSecrets(ctx context.Context, namespaces []v1.Namespace, esName, cesName string, provisionedNamespaces []string) map[string]error {
  230. failedNamespaces := map[string]error{}
  231. // Loop through existing namespaces first to make sure they still have our labels
  232. for _, namespace := range getRemovedNamespaces(namespaces, provisionedNamespaces) {
  233. err := r.deleteExternalSecret(ctx, esName, cesName, namespace)
  234. if err != nil {
  235. r.Log.Error(err, "unable to delete external secret")
  236. failedNamespaces[namespace] = err
  237. }
  238. }
  239. return failedNamespaces
  240. }
  241. func isExternalSecretOwnedBy(es *esv1.ExternalSecret, cesName string) bool {
  242. owner := metav1.GetControllerOf(es)
  243. return owner != nil && schema.FromAPIVersionAndKind(owner.APIVersion, owner.Kind).GroupKind().String() == esv1.ClusterExtSecretGroupKind && owner.Name == cesName
  244. }
  245. func getRemovedNamespaces(currentNSs []v1.Namespace, provisionedNSs []string) []string {
  246. currentNSSet := map[string]struct{}{}
  247. for _, currentNs := range currentNSs {
  248. currentNSSet[currentNs.Name] = struct{}{}
  249. }
  250. var removedNSs []string
  251. for _, ns := range provisionedNSs {
  252. if _, ok := currentNSSet[ns]; !ok {
  253. removedNSs = append(removedNSs, ns)
  254. }
  255. }
  256. return removedNSs
  257. }
  258. func toNamespaceFailures(failedNamespaces map[string]error) []esv1.ClusterExternalSecretNamespaceFailure {
  259. namespaceFailures := make([]esv1.ClusterExternalSecretNamespaceFailure, len(failedNamespaces))
  260. i := 0
  261. for namespace, err := range failedNamespaces {
  262. namespaceFailures[i] = esv1.ClusterExternalSecretNamespaceFailure{
  263. Namespace: namespace,
  264. Reason: err.Error(),
  265. }
  266. i++
  267. }
  268. sort.Slice(namespaceFailures, func(i, j int) bool { return namespaceFailures[i].Namespace < namespaceFailures[j].Namespace })
  269. return namespaceFailures
  270. }
  271. // SetupWithManager sets up the controller with the Manager.
  272. func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error {
  273. return ctrl.NewControllerManagedBy(mgr).
  274. WithOptions(opts).
  275. For(&esv1.ClusterExternalSecret{}).
  276. Owns(&esv1.ExternalSecret{}).
  277. Watches(
  278. &v1.Namespace{},
  279. handler.EnqueueRequestsFromMapFunc(r.findObjectsForNamespace),
  280. builder.WithPredicates(utils.NamespacePredicate()),
  281. ).
  282. Complete(r)
  283. }
  284. func (r *Reconciler) findObjectsForNamespace(ctx context.Context, namespace client.Object) []reconcile.Request {
  285. var clusterExternalSecrets esv1.ClusterExternalSecretList
  286. if err := r.List(ctx, &clusterExternalSecrets); err != nil {
  287. r.Log.Error(err, errGetCES)
  288. return []reconcile.Request{}
  289. }
  290. return r.queueRequestsForItem(&clusterExternalSecrets, namespace)
  291. }
  292. func (r *Reconciler) queueRequestsForItem(clusterExternalSecrets *esv1.ClusterExternalSecretList, namespace client.Object) []reconcile.Request {
  293. var requests []reconcile.Request
  294. for i := range clusterExternalSecrets.Items {
  295. clusterExternalSecret := clusterExternalSecrets.Items[i]
  296. var selectors []*metav1.LabelSelector
  297. if s := clusterExternalSecret.Spec.NamespaceSelector; s != nil {
  298. selectors = append(selectors, s)
  299. }
  300. selectors = append(selectors, clusterExternalSecret.Spec.NamespaceSelectors...)
  301. var selected bool
  302. for _, selector := range selectors {
  303. labelSelector, err := metav1.LabelSelectorAsSelector(selector)
  304. if err != nil {
  305. r.Log.Error(err, errConvertLabelSelector)
  306. continue
  307. }
  308. if labelSelector.Matches(labels.Set(namespace.GetLabels())) {
  309. requests = append(requests, reconcile.Request{
  310. NamespacedName: types.NamespacedName{
  311. Name: clusterExternalSecret.GetName(),
  312. Namespace: clusterExternalSecret.GetNamespace(),
  313. },
  314. })
  315. selected = true
  316. break
  317. }
  318. }
  319. // Prevent the object from being added twice if it happens to be listed
  320. // by Namespaces selector as well.
  321. if selected {
  322. continue
  323. }
  324. if slices.Contains(clusterExternalSecret.Spec.Namespaces, namespace.GetName()) {
  325. requests = append(requests, reconcile.Request{
  326. NamespacedName: types.NamespacedName{
  327. Name: clusterExternalSecret.GetName(),
  328. Namespace: clusterExternalSecret.GetNamespace(),
  329. },
  330. })
  331. }
  332. }
  333. return requests
  334. }