common.go 15 KB

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