clusterpushsecret_controller.go 12 KB

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