clusterpushsecret_controller.go 11 KB

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