common.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  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 secretstore
  13. import (
  14. "context"
  15. "errors"
  16. "fmt"
  17. "time"
  18. "github.com/go-logr/logr"
  19. v1 "k8s.io/api/core/v1"
  20. "k8s.io/apimachinery/pkg/fields"
  21. "k8s.io/apimachinery/pkg/types"
  22. "k8s.io/client-go/tools/record"
  23. ctrl "sigs.k8s.io/controller-runtime"
  24. "sigs.k8s.io/controller-runtime/pkg/client"
  25. "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
  26. ctrlreconcile "sigs.k8s.io/controller-runtime/pkg/reconcile"
  27. esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  28. esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
  29. "github.com/external-secrets/external-secrets/pkg/controllers/secretstore/metrics"
  30. _ "github.com/external-secrets/external-secrets/pkg/provider/register"
  31. )
  32. const (
  33. errStoreClient = "could not get provider client: %w"
  34. errValidationFailed = "could not validate provider: %w"
  35. errValidationUnknown = "could not determine validation status"
  36. errPatchStatus = "unable to patch status: %w"
  37. errUnableCreateClient = "unable to create client"
  38. errUnableValidateStore = "unable to validate store"
  39. msgStoreValidated = "store validated"
  40. msgStoreNotMaintained = "store isn't currently maintained. Please plan and prepare accordingly."
  41. // Finalizer for SecretStores when they have PushSecrets with DeletionPolicy=Delete.
  42. secretStoreFinalizer = "secretstore.externalsecrets.io/finalizer"
  43. )
  44. var validationUnknownError = errors.New("could not determine validation status")
  45. type Opts struct {
  46. ControllerClass string
  47. GaugeVecGetter metrics.GaugeVevGetter
  48. Recorder record.EventRecorder
  49. RequeueInterval time.Duration
  50. }
  51. func reconcile(ctx context.Context, req ctrl.Request, ss esapi.GenericStore, cl client.Client, isPushSecretEnabled bool, log logr.Logger, opts Opts) (ctrl.Result, error) {
  52. if !ShouldProcessStore(ss, opts.ControllerClass) {
  53. log.V(1).Info("skip store")
  54. return ctrl.Result{}, nil
  55. }
  56. // Manage finalizer if PushSecret feature is enabled.
  57. if isPushSecretEnabled {
  58. finalizersUpdated, err := handleFinalizer(ctx, cl, ss)
  59. if err != nil {
  60. return ctrl.Result{}, err
  61. }
  62. if finalizersUpdated {
  63. log.V(1).Info("updating resource with finalizer changes")
  64. if err := cl.Update(ctx, ss); err != nil {
  65. return ctrl.Result{}, err
  66. }
  67. }
  68. }
  69. requeueInterval := opts.RequeueInterval
  70. if ss.GetSpec().RefreshInterval != 0 {
  71. requeueInterval = time.Second * time.Duration(ss.GetSpec().RefreshInterval)
  72. }
  73. // patch status when done processing
  74. p := client.MergeFrom(ss.Copy())
  75. defer func() {
  76. err := cl.Status().Patch(ctx, ss, p)
  77. if err != nil {
  78. log.Error(err, errPatchStatus)
  79. }
  80. }()
  81. // validateStore modifies the store conditions
  82. // we have to patch the status
  83. log.V(1).Info("validating")
  84. err := validateStore(ctx, req.Namespace, opts.ControllerClass, ss, cl, opts.GaugeVecGetter, opts.Recorder)
  85. if err != nil {
  86. log.Error(err, "unable to validate store")
  87. // in case of validation status unknown, validateStore will mark
  88. // the store as ready but we should show ReasonValidationUnknown
  89. if errors.Is(err, validationUnknownError) {
  90. return ctrl.Result{RequeueAfter: requeueInterval}, nil
  91. }
  92. return ctrl.Result{}, err
  93. }
  94. storeProvider, err := esapi.GetProvider(ss)
  95. if err != nil {
  96. return ctrl.Result{}, err
  97. }
  98. isMaintained, err := esapi.GetMaintenanceStatus(ss)
  99. if err != nil {
  100. return ctrl.Result{}, err
  101. }
  102. annotations := ss.GetAnnotations()
  103. _, ok := annotations["external-secrets.io/ignore-maintenance-checks"]
  104. if !bool(isMaintained) && !ok {
  105. opts.Recorder.Event(ss, v1.EventTypeWarning, esapi.StoreUnmaintained, msgStoreNotMaintained)
  106. }
  107. capStatus := esapi.SecretStoreStatus{
  108. Capabilities: storeProvider.Capabilities(),
  109. Conditions: ss.GetStatus().Conditions,
  110. }
  111. ss.SetStatus(capStatus)
  112. opts.Recorder.Event(ss, v1.EventTypeNormal, esapi.ReasonStoreValid, msgStoreValidated)
  113. cond := NewSecretStoreCondition(esapi.SecretStoreReady, v1.ConditionTrue, esapi.ReasonStoreValid, msgStoreValidated)
  114. SetExternalSecretCondition(ss, *cond, opts.GaugeVecGetter)
  115. return ctrl.Result{
  116. RequeueAfter: requeueInterval,
  117. }, err
  118. }
  119. // validateStore tries to construct a new client
  120. // if it fails sets a condition and writes events.
  121. func validateStore(ctx context.Context, namespace, controllerClass string, store esapi.GenericStore,
  122. client client.Client, gaugeVecGetter metrics.GaugeVevGetter, recorder record.EventRecorder) error {
  123. mgr := NewManager(client, controllerClass, false)
  124. defer func() {
  125. _ = mgr.Close(ctx)
  126. }()
  127. cl, err := mgr.GetFromStore(ctx, store, namespace)
  128. if err != nil {
  129. cond := NewSecretStoreCondition(esapi.SecretStoreReady, v1.ConditionFalse, esapi.ReasonInvalidProviderConfig, errUnableCreateClient)
  130. SetExternalSecretCondition(store, *cond, gaugeVecGetter)
  131. recorder.Event(store, v1.EventTypeWarning, esapi.ReasonInvalidProviderConfig, err.Error())
  132. return fmt.Errorf(errStoreClient, err)
  133. }
  134. validationResult, err := cl.Validate()
  135. if err != nil {
  136. if validationResult == esapi.ValidationResultUnknown {
  137. cond := NewSecretStoreCondition(esapi.SecretStoreReady, v1.ConditionTrue, esapi.ReasonValidationUnknown, errValidationUnknown)
  138. SetExternalSecretCondition(store, *cond, gaugeVecGetter)
  139. recorder.Event(store, v1.EventTypeWarning, esapi.ReasonValidationUnknown, err.Error())
  140. return validationUnknownError
  141. }
  142. cond := NewSecretStoreCondition(esapi.SecretStoreReady, v1.ConditionFalse, esapi.ReasonInvalidProviderConfig, errUnableValidateStore)
  143. SetExternalSecretCondition(store, *cond, gaugeVecGetter)
  144. recorder.Event(store, v1.EventTypeWarning, esapi.ReasonInvalidProviderConfig, err.Error())
  145. return fmt.Errorf(errValidationFailed, err)
  146. }
  147. return nil
  148. }
  149. // ShouldProcessStore returns true if the store should be processed.
  150. func ShouldProcessStore(store esapi.GenericStore, class string) bool {
  151. if store == nil || store.GetSpec().Controller == "" || store.GetSpec().Controller == class {
  152. return true
  153. }
  154. return false
  155. }
  156. // handleFinalizer manages the finalizer for ClusterSecretStores and SecretStores.
  157. func handleFinalizer(ctx context.Context, cl client.Client, store esapi.GenericStore) (finalizersUpdated bool, err error) {
  158. log := logr.FromContextOrDiscard(ctx)
  159. hasPushSecretsWithDeletePolicy, err := hasPushSecretsWithDeletePolicy(ctx, cl, store)
  160. if err != nil {
  161. return false, fmt.Errorf("failed to check PushSecrets: %w", err)
  162. }
  163. storeKind := store.GetKind()
  164. // If the store is being deleted and has the finalizer, check if we can remove it
  165. if !store.GetObjectMeta().DeletionTimestamp.IsZero() {
  166. if hasPushSecretsWithDeletePolicy {
  167. log.Info("cannot remove finalizer, there are still PushSecrets with DeletionPolicy=Delete that reference this store")
  168. return false, nil
  169. }
  170. if controllerutil.RemoveFinalizer(store, secretStoreFinalizer) {
  171. log.Info(fmt.Sprintf("removed finalizer from %s during deletion", storeKind))
  172. return true, nil
  173. }
  174. return false, nil
  175. }
  176. // If the store is not being deleted, manage the finalizer based on PushSecrets
  177. if hasPushSecretsWithDeletePolicy {
  178. if controllerutil.AddFinalizer(store, secretStoreFinalizer) {
  179. log.Info(fmt.Sprintf("added finalizer to %s due to PushSecrets with DeletionPolicy=Delete", storeKind))
  180. return true, nil
  181. }
  182. } else {
  183. if controllerutil.RemoveFinalizer(store, secretStoreFinalizer) {
  184. log.Info(fmt.Sprintf("removed finalizer from %s, no more PushSecrets with DeletionPolicy=Delete", storeKind))
  185. return true, nil
  186. }
  187. }
  188. return false, nil
  189. }
  190. // hasPushSecretsWithDeletePolicy checks if there are any PushSecrets with DeletionPolicy=Delete
  191. // that reference this SecretStore using the controller-runtime index.
  192. func hasPushSecretsWithDeletePolicy(ctx context.Context, cl client.Client, store esapi.GenericStore) (bool, error) {
  193. // Search for PushSecrets that have already synced from this store.
  194. found, err := hasSyncedPushSecrets(ctx, cl, store)
  195. if err != nil {
  196. return false, fmt.Errorf("failed to check for synced push secrets: %w", err)
  197. }
  198. if found {
  199. return true, nil
  200. }
  201. // Search for PushSecrets that reference this store, but may not have synced yet.
  202. found, err = hasUnsyncedPushSecretRefs(ctx, cl, store)
  203. if err != nil {
  204. return false, fmt.Errorf("failed to check for unsynced push secret refs: %w", err)
  205. }
  206. return found, nil
  207. }
  208. // hasSyncedPushSecrets uses the 'status.syncedPushSecrets' index from PushSecrets to efficiently find
  209. // PushSecrets with DeletionPolicy=Delete that have already been synced from the given store.
  210. func hasSyncedPushSecrets(ctx context.Context, cl client.Client, store esapi.GenericStore) (bool, error) {
  211. storeKey := fmt.Sprintf("%s/%s", store.GetKind(), store.GetName())
  212. opts := &client.ListOptions{
  213. FieldSelector: fields.OneTermEqualSelector("status.syncedPushSecrets", storeKey),
  214. }
  215. if store.GetKind() == esapi.SecretStoreKind {
  216. opts.Namespace = store.GetNamespace()
  217. }
  218. var pushSecretList esv1alpha1.PushSecretList
  219. if err := cl.List(ctx, &pushSecretList, opts); err != nil {
  220. return false, err
  221. }
  222. // If any PushSecrets are found, return true. The index ensures they have DeletionPolicy=Delete.
  223. return len(pushSecretList.Items) > 0, nil
  224. }
  225. // hasUnsyncedPushSecretRefs searches for all PushSecrets with DeletionPolicy=Delete
  226. // and checks if any of them reference the given store (by name or labelSelector).
  227. // This is necessary for cases where the reference exists, but synchronization has not occurred yet.
  228. func hasUnsyncedPushSecretRefs(ctx context.Context, cl client.Client, store esapi.GenericStore) (bool, error) {
  229. opts := &client.ListOptions{
  230. FieldSelector: fields.OneTermEqualSelector("spec.deletionPolicy", string(esv1alpha1.PushSecretDeletionPolicyDelete)),
  231. }
  232. if store.GetKind() == esapi.SecretStoreKind {
  233. opts.Namespace = store.GetNamespace()
  234. }
  235. var pushSecretList esv1alpha1.PushSecretList
  236. if err := cl.List(ctx, &pushSecretList, opts); err != nil {
  237. return false, err
  238. }
  239. for _, ps := range pushSecretList.Items {
  240. for _, storeRef := range ps.Spec.SecretStoreRefs {
  241. if storeMatchesRef(store, storeRef) {
  242. return true, nil
  243. }
  244. }
  245. }
  246. return false, nil
  247. }
  248. // findStoresForPushSecret finds SecretStores or ClusterSecretStores that should be reconciled when a PushSecret changes.
  249. func findStoresForPushSecret(ctx context.Context, c client.Client, obj client.Object, storeList client.ObjectList) []ctrlreconcile.Request {
  250. ps, ok := obj.(*esv1alpha1.PushSecret)
  251. if !ok {
  252. return nil
  253. }
  254. var isClusterScoped bool
  255. switch storeList.(type) {
  256. case *esapi.ClusterSecretStoreList:
  257. isClusterScoped = true
  258. case *esapi.SecretStoreList:
  259. isClusterScoped = false
  260. default:
  261. return nil
  262. }
  263. listOpts := make([]client.ListOption, 0)
  264. if !isClusterScoped {
  265. listOpts = append(listOpts, client.InNamespace(ps.GetNamespace()))
  266. }
  267. if err := c.List(ctx, storeList, listOpts...); err != nil {
  268. return nil
  269. }
  270. requests := make([]ctrlreconcile.Request, 0)
  271. var stores []esapi.GenericStore
  272. switch sl := storeList.(type) {
  273. case *esapi.SecretStoreList:
  274. for i := range sl.Items {
  275. stores = append(stores, &sl.Items[i])
  276. }
  277. case *esapi.ClusterSecretStoreList:
  278. for i := range sl.Items {
  279. stores = append(stores, &sl.Items[i])
  280. }
  281. }
  282. for _, store := range stores {
  283. if shouldReconcileSecretStoreForPushSecret(store, ps) {
  284. req := ctrlreconcile.Request{
  285. NamespacedName: types.NamespacedName{
  286. Name: store.GetName(),
  287. },
  288. }
  289. if !isClusterScoped {
  290. req.NamespacedName.Namespace = store.GetNamespace()
  291. }
  292. requests = append(requests, req)
  293. }
  294. }
  295. return requests
  296. }