pushsecret_controller.go 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362
  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 pushsecret implements the controller for managing PushSecret resources.
  14. package pushsecret
  15. import (
  16. "bytes"
  17. "context"
  18. "errors"
  19. "fmt"
  20. "maps"
  21. "regexp"
  22. "slices"
  23. "strings"
  24. "text/template"
  25. "time"
  26. "github.com/go-logr/logr"
  27. v1 "k8s.io/api/core/v1"
  28. apierrors "k8s.io/apimachinery/pkg/api/errors"
  29. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  30. "k8s.io/apimachinery/pkg/labels"
  31. "k8s.io/apimachinery/pkg/runtime"
  32. "k8s.io/apimachinery/pkg/types"
  33. "k8s.io/client-go/rest"
  34. "k8s.io/client-go/tools/record"
  35. ctrl "sigs.k8s.io/controller-runtime"
  36. "sigs.k8s.io/controller-runtime/pkg/builder"
  37. "sigs.k8s.io/controller-runtime/pkg/client"
  38. "sigs.k8s.io/controller-runtime/pkg/controller"
  39. "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
  40. "sigs.k8s.io/controller-runtime/pkg/event"
  41. "sigs.k8s.io/controller-runtime/pkg/predicate"
  42. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  43. esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
  44. genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
  45. ctrlmetrics "github.com/external-secrets/external-secrets/pkg/controllers/metrics"
  46. "github.com/external-secrets/external-secrets/pkg/controllers/pushsecret/psmetrics"
  47. ctrlutil "github.com/external-secrets/external-secrets/pkg/controllers/util"
  48. "github.com/external-secrets/external-secrets/runtime/clientmanager"
  49. "github.com/external-secrets/external-secrets/runtime/esutils"
  50. "github.com/external-secrets/external-secrets/runtime/esutils/resolvers"
  51. "github.com/external-secrets/external-secrets/runtime/statemanager"
  52. estemplate "github.com/external-secrets/external-secrets/runtime/template/v2"
  53. "github.com/external-secrets/external-secrets/runtime/util/locks"
  54. // Load registered generators.
  55. _ "github.com/external-secrets/external-secrets/pkg/register"
  56. )
  57. const (
  58. errFailedGetSecret = "could not get source secret"
  59. errPatchStatus = "error merging"
  60. errGetSecretStore = "could not get SecretStore %q, %w"
  61. errGetClusterSecretStore = "could not get ClusterSecretStore %q, %w"
  62. errSetSecretFailed = "could not write remote ref %v to target secretstore %v: %v"
  63. errFailedSetSecret = "set secret failed: %v"
  64. errConvert = "could not apply conversion strategy to keys: %v"
  65. pushSecretFinalizer = "pushsecret.externalsecrets.io/finalizer"
  66. errCloudNotUpdateFinalizer = "could not update finalizers: %w"
  67. bundleSourceKey = "(bundle)"
  68. )
  69. // Reconciler is the controller for PushSecret resources.
  70. // It manages the lifecycle of PushSecrets, ensuring that secrets are pushed to
  71. // specified secret stores according to the defined policies and templates.
  72. type Reconciler struct {
  73. client.Client
  74. Log logr.Logger
  75. Scheme *runtime.Scheme
  76. recorder record.EventRecorder
  77. RestConfig *rest.Config
  78. RequeueInterval time.Duration
  79. ControllerClass string
  80. }
  81. // storeInfo holds the identifying attributes of a secret store for per-store processing.
  82. type storeInfo struct {
  83. Name string
  84. Kind string
  85. Labels map[string]string
  86. }
  87. // SetupWithManager sets up the controller with the Manager.
  88. // It configures the controller to watch PushSecret resources and
  89. // manages indexing for efficient lookups based on secret stores and deletion policies.
  90. func (r *Reconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opts controller.Options) error {
  91. r.recorder = mgr.GetEventRecorderFor("pushsecret")
  92. // Index PushSecrets by the stores they have pushed to (for finalizer management on store deletion)
  93. // Refer to common.go for more details on the index function
  94. if err := mgr.GetFieldIndexer().IndexField(ctx, &esapi.PushSecret{}, "status.syncedPushSecrets", func(obj client.Object) []string {
  95. ps := obj.(*esapi.PushSecret)
  96. // Only index PushSecrets with DeletionPolicy=Delete for efficiency
  97. if ps.Spec.DeletionPolicy != esapi.PushSecretDeletionPolicyDelete {
  98. return nil
  99. }
  100. // Format is typically "Kind/Name" (e.g., "SecretStore/store1", "ClusterSecretStore/clusterstore1")
  101. storeKeys := make([]string, 0, len(ps.Status.SyncedPushSecrets))
  102. for storeKey := range ps.Status.SyncedPushSecrets {
  103. storeKeys = append(storeKeys, storeKey)
  104. }
  105. return storeKeys
  106. }); err != nil {
  107. return err
  108. }
  109. // Index PushSecrets by deletionPolicy for quick filtering
  110. if err := mgr.GetFieldIndexer().IndexField(ctx, &esapi.PushSecret{}, "spec.deletionPolicy", func(obj client.Object) []string {
  111. ps := obj.(*esapi.PushSecret)
  112. return []string{string(ps.Spec.DeletionPolicy)}
  113. }); err != nil {
  114. return err
  115. }
  116. return ctrl.NewControllerManagedBy(mgr).
  117. WithOptions(opts).
  118. For(&esapi.PushSecret{}, builder.WithPredicates(pushSecretWatchPredicate())).
  119. Complete(r)
  120. }
  121. func pushSecretWatchPredicate() predicate.Predicate {
  122. return predicate.Funcs{
  123. CreateFunc: func(event.CreateEvent) bool {
  124. return true
  125. },
  126. DeleteFunc: func(event.DeleteEvent) bool {
  127. return true
  128. },
  129. UpdateFunc: func(e event.UpdateEvent) bool {
  130. if e.ObjectOld == nil || e.ObjectNew == nil {
  131. return true
  132. }
  133. return shouldReconcilePushSecretUpdate(e.ObjectOld, e.ObjectNew)
  134. },
  135. }
  136. }
  137. func shouldReconcilePushSecretUpdate(oldObj, newObj client.Object) bool {
  138. if oldObj.GetGeneration() != newObj.GetGeneration() {
  139. return true
  140. }
  141. if !maps.Equal(oldObj.GetLabels(), newObj.GetLabels()) {
  142. return true
  143. }
  144. if !maps.Equal(oldObj.GetAnnotations(), newObj.GetAnnotations()) {
  145. return true
  146. }
  147. oldDeleting := oldObj.GetDeletionTimestamp() != nil
  148. newDeleting := newObj.GetDeletionTimestamp() != nil
  149. return oldDeleting != newDeleting
  150. }
  151. // Reconcile is part of the main kubernetes reconciliation loop which aims to
  152. // move the current state of the cluster closer to the desired state.
  153. // For more details, check Reconcile and its Result here:
  154. // - https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/reconcile
  155. func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
  156. log := r.Log.WithValues("pushsecret", req.NamespacedName)
  157. resourceLabels := ctrlmetrics.RefineNonConditionMetricLabels(map[string]string{"name": req.Name, "namespace": req.Namespace})
  158. start := time.Now()
  159. pushSecretReconcileDuration := psmetrics.GetGaugeVec(psmetrics.PushSecretReconcileDurationKey)
  160. defer func() { pushSecretReconcileDuration.With(resourceLabels).Set(float64(time.Since(start))) }()
  161. var ps esapi.PushSecret
  162. mgr := clientmanager.NewManager(r.Client, r.ControllerClass, false)
  163. defer func() {
  164. _ = mgr.Close(ctx)
  165. }()
  166. if err := r.Get(ctx, req.NamespacedName, &ps); err != nil {
  167. if apierrors.IsNotFound(err) {
  168. return ctrl.Result{}, nil
  169. }
  170. msg := "unable to get PushSecret"
  171. r.recorder.Event(&ps, v1.EventTypeWarning, esapi.ReasonErrored, msg)
  172. log.Error(err, msg)
  173. return ctrl.Result{}, fmt.Errorf("get resource: %w", err)
  174. }
  175. refreshInt := r.RequeueInterval
  176. if ps.Spec.RefreshInterval != nil {
  177. refreshInt = ps.Spec.RefreshInterval.Duration
  178. }
  179. p := client.MergeFrom(ps.DeepCopy())
  180. defer func() {
  181. err := r.Client.Status().Patch(ctx, &ps, p)
  182. if err != nil && !apierrors.IsNotFound(err) {
  183. log.Error(err, errPatchStatus)
  184. }
  185. }()
  186. // Get secret stores early so they can be used for finalizer deletion
  187. secretStores, err := r.GetSecretStores(ctx, ps)
  188. if err != nil {
  189. r.markAsFailed(err.Error(), &ps, nil)
  190. return ctrl.Result{}, err
  191. }
  192. activeSecretStores := make(map[esapi.PushSecretStoreRef]esv1.GenericStore, len(secretStores))
  193. for ref, store := range secretStores {
  194. if !store.GetDeletionTimestamp().IsZero() {
  195. log.Info("skipping SecretStore that is being deleted", "storeName", store.GetName(), "storeKind", store.GetKind())
  196. continue
  197. }
  198. activeSecretStores[ref] = store
  199. }
  200. finalStores, err := removeUnmanagedStores(ctx, req.Namespace, r, activeSecretStores)
  201. if err != nil {
  202. r.markAsFailed(err.Error(), &ps, nil)
  203. return ctrl.Result{}, err
  204. }
  205. switch ps.Spec.DeletionPolicy {
  206. case esapi.PushSecretDeletionPolicyDelete:
  207. // finalizer logic. Only added if we should delete the secrets
  208. if ps.ObjectMeta.DeletionTimestamp.IsZero() {
  209. if added := controllerutil.AddFinalizer(&ps, pushSecretFinalizer); added {
  210. if err := r.Client.Update(ctx, &ps, &client.UpdateOptions{}); err != nil {
  211. return ctrl.Result{}, fmt.Errorf(errCloudNotUpdateFinalizer, err)
  212. }
  213. return ctrl.Result{Requeue: true}, nil
  214. }
  215. } else if controllerutil.ContainsFinalizer(&ps, pushSecretFinalizer) {
  216. // trigger a cleanup with no Synced Map
  217. badState, err := r.DeleteSecretFromProviders(ctx, &ps, esapi.SyncedPushSecretsMap{}, mgr)
  218. if err != nil {
  219. msg := fmt.Sprintf("Failed to Delete Secrets from Provider: %v", err)
  220. r.markAsFailed(msg, &ps, badState)
  221. return ctrl.Result{}, err
  222. }
  223. controllerutil.RemoveFinalizer(&ps, pushSecretFinalizer)
  224. if err := r.Client.Update(ctx, &ps, &client.UpdateOptions{}); err != nil {
  225. return ctrl.Result{}, fmt.Errorf("could not update finalizers: %w", err)
  226. }
  227. return ctrl.Result{}, nil
  228. }
  229. case esapi.PushSecretDeletionPolicyNone:
  230. if controllerutil.ContainsFinalizer(&ps, pushSecretFinalizer) {
  231. controllerutil.RemoveFinalizer(&ps, pushSecretFinalizer)
  232. if err := r.Client.Update(ctx, &ps, &client.UpdateOptions{}); err != nil {
  233. return ctrl.Result{}, fmt.Errorf(errCloudNotUpdateFinalizer, err)
  234. }
  235. }
  236. default:
  237. }
  238. timeSinceLastRefresh := 0 * time.Second
  239. if !ps.Status.RefreshTime.IsZero() {
  240. timeSinceLastRefresh = time.Since(ps.Status.RefreshTime.Time)
  241. }
  242. if !shouldRefresh(ps) {
  243. refreshInt = (ps.Spec.RefreshInterval.Duration - timeSinceLastRefresh) + 5*time.Second
  244. log.V(1).Info("skipping refresh", "rv", ctrlutil.GetResourceVersion(ps.ObjectMeta), "nr", refreshInt.Seconds())
  245. return ctrl.Result{RequeueAfter: refreshInt}, nil
  246. }
  247. if err := validateDataToStoreRefs(ps.Spec.DataTo, ps.Spec.SecretStoreRefs); err != nil {
  248. r.markAsFailed(err.Error(), &ps, nil)
  249. return ctrl.Result{}, err
  250. }
  251. // if no stores are managed by this controller
  252. if len(finalStores) == 0 {
  253. return ctrl.Result{}, nil
  254. }
  255. secrets, err := r.resolveSecrets(ctx, &ps)
  256. if err != nil {
  257. isSecretSelector := ps.Spec.Selector.Secret != nil && ps.Spec.Selector.Secret.Name != ""
  258. if apierrors.IsNotFound(err) && isSecretSelector &&
  259. ps.Spec.DeletionPolicy == esapi.PushSecretDeletionPolicyDelete &&
  260. len(ps.Status.SyncedPushSecrets) > 0 {
  261. return ctrl.Result{}, r.handleSourceSecretDeleted(ctx, &ps, mgr)
  262. }
  263. r.markAsFailed(errFailedGetSecret, &ps, nil)
  264. return ctrl.Result{}, err
  265. }
  266. resolvedStores := make([]storeInfo, 0, len(finalStores))
  267. for ref, store := range finalStores {
  268. if si, ok := resolvedStoreInfo(ref, store); ok {
  269. resolvedStores = append(resolvedStores, si)
  270. }
  271. }
  272. if err := validateDataToMatchesResolvedStores(ps.Spec.DataTo, resolvedStores); err != nil {
  273. r.markAsFailed(err.Error(), &ps, nil)
  274. return ctrl.Result{}, err
  275. }
  276. allSyncedSecrets := make(esapi.SyncedPushSecretsMap)
  277. for _, secret := range secrets {
  278. if err := r.applyTemplate(ctx, &ps, &secret); err != nil {
  279. return ctrl.Result{}, err
  280. }
  281. syncedSecrets, err := r.PushSecretToProviders(ctx, finalStores, ps, &secret, mgr)
  282. if err != nil {
  283. if errors.Is(err, locks.ErrConflict) {
  284. log.Info("retry to acquire lock to update the secret later", "error", err)
  285. return ctrl.Result{Requeue: true}, nil
  286. }
  287. totalSecrets := mergeSecretState(syncedSecrets, ps.Status.SyncedPushSecrets)
  288. msg := fmt.Sprintf(errFailedSetSecret, err)
  289. r.markAsFailed(msg, &ps, totalSecrets)
  290. return ctrl.Result{}, err
  291. }
  292. switch ps.Spec.DeletionPolicy {
  293. case esapi.PushSecretDeletionPolicyDelete:
  294. badSyncState, err := r.DeleteSecretFromProviders(ctx, &ps, syncedSecrets, mgr)
  295. if err != nil {
  296. msg := fmt.Sprintf("Failed to Delete Secrets from Provider: %v", err)
  297. r.markAsFailed(msg, &ps, badSyncState)
  298. return ctrl.Result{}, err
  299. }
  300. case esapi.PushSecretDeletionPolicyNone:
  301. default:
  302. }
  303. allSyncedSecrets = mergeSecretState(allSyncedSecrets, syncedSecrets)
  304. }
  305. r.markAsDone(&ps, allSyncedSecrets, start)
  306. return ctrl.Result{RequeueAfter: refreshInt}, nil
  307. }
  308. // handleSourceSecretDeleted cleans up provider secrets when source Secret is unavailable.
  309. func (r *Reconciler) handleSourceSecretDeleted(ctx context.Context, ps *esapi.PushSecret, mgr *clientmanager.Manager) error {
  310. log := r.Log.WithValues("pushsecret", client.ObjectKeyFromObject(ps))
  311. log.Info("source secret unavailable, cleaning up provider secrets", "syncedSecrets", len(ps.Status.SyncedPushSecrets))
  312. badState, err := r.DeleteSecretFromProviders(ctx, ps, esapi.SyncedPushSecretsMap{}, mgr)
  313. if err != nil {
  314. msg := fmt.Sprintf("failed to cleanup provider secrets: %v", err)
  315. r.markAsFailed(msg, ps, badState)
  316. return err
  317. }
  318. r.setSecrets(ps, esapi.SyncedPushSecretsMap{})
  319. r.markAsSourceDeleted(ps)
  320. return nil
  321. }
  322. func shouldRefresh(ps esapi.PushSecret) bool {
  323. if ps.Status.SyncedResourceVersion != ctrlutil.GetResourceVersion(ps.ObjectMeta) {
  324. return true
  325. }
  326. if ps.Spec.RefreshInterval.Duration == 0 && ps.Status.SyncedResourceVersion != "" {
  327. return false
  328. }
  329. if ps.Status.RefreshTime.IsZero() {
  330. return true
  331. }
  332. return ps.Status.RefreshTime.Add(ps.Spec.RefreshInterval.Duration).Before(time.Now())
  333. }
  334. func (r *Reconciler) markAsFailed(msg string, ps *esapi.PushSecret, syncState esapi.SyncedPushSecretsMap) {
  335. cond := NewPushSecretCondition(esapi.PushSecretReady, v1.ConditionFalse, esapi.ReasonErrored, msg)
  336. SetPushSecretCondition(ps, *cond)
  337. if syncState != nil {
  338. r.setSecrets(ps, syncState)
  339. }
  340. r.recorder.Event(ps, v1.EventTypeWarning, esapi.ReasonErrored, msg)
  341. }
  342. func (r *Reconciler) markAsSourceDeleted(ps *esapi.PushSecret) {
  343. msg := "source secret deleted; provider secrets cleaned up"
  344. cond := NewPushSecretCondition(esapi.PushSecretReady, v1.ConditionFalse, esapi.ReasonSourceDeleted, msg)
  345. SetPushSecretCondition(ps, *cond)
  346. r.recorder.Event(ps, v1.EventTypeNormal, esapi.ReasonSourceDeleted, msg)
  347. }
  348. func (r *Reconciler) markAsDone(ps *esapi.PushSecret, secrets esapi.SyncedPushSecretsMap, start time.Time) {
  349. msg := "PushSecret synced successfully"
  350. if ps.Spec.UpdatePolicy == esapi.PushSecretUpdatePolicyIfNotExists {
  351. msg += ". Existing secrets in providers unchanged."
  352. }
  353. cond := NewPushSecretCondition(esapi.PushSecretReady, v1.ConditionTrue, esapi.ReasonSynced, msg)
  354. SetPushSecretCondition(ps, *cond)
  355. r.setSecrets(ps, secrets)
  356. ps.Status.RefreshTime = metav1.NewTime(start)
  357. ps.Status.SyncedResourceVersion = ctrlutil.GetResourceVersion(ps.ObjectMeta)
  358. r.recorder.Event(ps, v1.EventTypeNormal, esapi.ReasonSynced, msg)
  359. }
  360. func (r *Reconciler) setSecrets(ps *esapi.PushSecret, status esapi.SyncedPushSecretsMap) {
  361. ps.Status.SyncedPushSecrets = status
  362. }
  363. func mergeSecretState(newMap, old esapi.SyncedPushSecretsMap) esapi.SyncedPushSecretsMap {
  364. if newMap == nil {
  365. return old
  366. }
  367. out := newMap.DeepCopy()
  368. for k, v := range old {
  369. _, ok := out[k]
  370. if !ok {
  371. out[k] = make(map[string]esapi.PushSecretData)
  372. }
  373. maps.Insert(out[k], maps.All(v))
  374. }
  375. return out
  376. }
  377. // DeleteSecretFromProviders removes secrets from providers that are no longer needed.
  378. // It compares the existing synced secrets in the PushSecret status with the new desired state,
  379. // and deletes any secrets that are no longer present in the new state.
  380. func (r *Reconciler) DeleteSecretFromProviders(ctx context.Context, ps *esapi.PushSecret, newMap esapi.SyncedPushSecretsMap, mgr *clientmanager.Manager) (esapi.SyncedPushSecretsMap, error) {
  381. out := mergeSecretState(newMap, ps.Status.SyncedPushSecrets)
  382. for storeName, oldData := range ps.Status.SyncedPushSecrets {
  383. storeRef := esv1.SecretStoreRef{
  384. Name: strings.Split(storeName, "/")[1],
  385. Kind: strings.Split(storeName, "/")[0],
  386. }
  387. client, err := mgr.Get(ctx, storeRef, ps.Namespace, nil)
  388. if err != nil {
  389. return out, fmt.Errorf("could not get secrets client for store %v: %w", storeName, err)
  390. }
  391. newData, ok := newMap[storeName]
  392. if !ok {
  393. err = r.DeleteAllSecretsFromStore(ctx, client, oldData)
  394. if err != nil {
  395. return out, err
  396. }
  397. delete(out, storeName)
  398. continue
  399. }
  400. for oldEntry, oldRef := range oldData {
  401. _, ok := newData[oldEntry]
  402. if !ok {
  403. err = r.DeleteSecretFromStore(ctx, client, oldRef)
  404. if err != nil {
  405. return out, err
  406. }
  407. delete(out[storeName], oldEntry)
  408. }
  409. }
  410. }
  411. return out, nil
  412. }
  413. // DeleteAllSecretsFromStore removes all secrets from a given secret store.
  414. func (r *Reconciler) DeleteAllSecretsFromStore(ctx context.Context, client esv1.SecretsClient, data map[string]esapi.PushSecretData) error {
  415. for _, v := range data {
  416. err := r.DeleteSecretFromStore(ctx, client, v)
  417. if err != nil {
  418. return err
  419. }
  420. }
  421. return nil
  422. }
  423. // DeleteSecretFromStore removes a specific secret from a given secret store.
  424. func (r *Reconciler) DeleteSecretFromStore(ctx context.Context, client esv1.SecretsClient, data esapi.PushSecretData) error {
  425. return client.DeleteSecret(ctx, data.Match.RemoteRef)
  426. }
  427. // PushSecretToProviders pushes the secret data to the specified secret stores.
  428. // It iterates over each store and handles the push operation according to the
  429. // defined update policies and conversion strategies.
  430. func (r *Reconciler) PushSecretToProviders(
  431. ctx context.Context,
  432. stores map[esapi.PushSecretStoreRef]esv1.GenericStore,
  433. ps esapi.PushSecret,
  434. secret *v1.Secret,
  435. mgr *clientmanager.Manager,
  436. ) (esapi.SyncedPushSecretsMap, error) {
  437. out := make(esapi.SyncedPushSecretsMap)
  438. var err error
  439. for ref, store := range stores {
  440. si, ok := resolvedStoreInfo(ref, store)
  441. if !ok {
  442. return out, fmt.Errorf("could not resolve store info for store %q", store.GetName())
  443. }
  444. out, err = r.handlePushSecretDataForStore(ctx, ps, secret, out, mgr, si)
  445. if err != nil {
  446. return out, err
  447. }
  448. }
  449. return out, nil
  450. }
  451. func (r *Reconciler) handlePushSecretDataForStore(
  452. ctx context.Context,
  453. ps esapi.PushSecret,
  454. secret *v1.Secret,
  455. out esapi.SyncedPushSecretsMap,
  456. mgr *clientmanager.Manager,
  457. si storeInfo,
  458. ) (esapi.SyncedPushSecretsMap, error) {
  459. storeKey := fmt.Sprintf("%v/%v", si.Kind, si.Name)
  460. out[storeKey] = make(map[string]esapi.PushSecretData)
  461. storeRef := esv1.SecretStoreRef{
  462. Name: si.Name,
  463. Kind: si.Kind,
  464. }
  465. secretClient, err := mgr.Get(ctx, storeRef, ps.GetNamespace(), nil)
  466. if err != nil {
  467. return out, fmt.Errorf("could not get secrets client for store %v: %w", si.Name, err)
  468. }
  469. storeSecret := secret.DeepCopy()
  470. filteredDataTo, err := filterDataToForStore(ps.Spec.DataTo, si.Name, si.Kind, si.Labels)
  471. if err != nil {
  472. return out, fmt.Errorf("failed to filter dataTo: %w", err)
  473. }
  474. dataToEntries, bundleOverrides, err := r.expandDataTo(storeSecret, filteredDataTo)
  475. if err != nil {
  476. return out, fmt.Errorf("failed to expand dataTo: %w", err)
  477. }
  478. allData, err := mergeDataEntries(dataToEntries, ps.Spec.Data, storeSecret)
  479. if err != nil {
  480. return out, fmt.Errorf("failed to merge data entries: %w", err)
  481. }
  482. originalStoreSecretData := storeSecret.Data
  483. for _, data := range allData {
  484. params := pushEntryParams{
  485. data: data,
  486. updatePolicy: ps.Spec.UpdatePolicy,
  487. originalData: originalStoreSecretData,
  488. dataOverride: bundleOverrides[statusRef(data)],
  489. storeName: si.Name,
  490. }
  491. if err := r.pushSecretEntry(ctx, secretClient, storeSecret, params); err != nil {
  492. return out, err
  493. }
  494. out[storeKey][statusRef(data)] = data
  495. }
  496. return out, nil
  497. }
  498. // pushEntryParams groups the parameters for pushSecretEntry to keep the
  499. // function signature within the recommended parameter count.
  500. type pushEntryParams struct {
  501. data esapi.PushSecretData
  502. updatePolicy esapi.PushSecretUpdatePolicy
  503. originalData map[string][]byte
  504. dataOverride map[string][]byte
  505. storeName string
  506. }
  507. // pushSecretEntry converts, validates, and pushes a single data entry to the provider.
  508. // If the update policy is IfNotExists and the secret already exists, the push is skipped.
  509. // params.dataOverride, when non-nil, replaces params.originalData for the conversion step —
  510. // used by bundle entries (dataTo with remoteKey) to restrict the pushed payload to matched keys only.
  511. func (r *Reconciler) pushSecretEntry(
  512. ctx context.Context,
  513. secretClient esv1.SecretsClient,
  514. storeSecret *v1.Secret,
  515. params pushEntryParams,
  516. ) error {
  517. sourceData := params.originalData
  518. if params.dataOverride != nil {
  519. sourceData = params.dataOverride
  520. }
  521. secretData, err := esutils.ReverseKeys(params.data.ConversionStrategy, sourceData)
  522. if err != nil {
  523. return fmt.Errorf(errConvert, err)
  524. }
  525. key := params.data.GetSecretKey()
  526. if !secretKeyExists(key, secretData) {
  527. return fmt.Errorf("secret key %v does not exist", key)
  528. }
  529. if params.updatePolicy == esapi.PushSecretUpdatePolicyIfNotExists {
  530. exists, err := secretClient.SecretExists(ctx, params.data.Match.RemoteRef)
  531. if err != nil {
  532. return fmt.Errorf("could not verify if secret exists in store: %w", err)
  533. }
  534. if exists {
  535. return nil
  536. }
  537. }
  538. localSecret := storeSecret.DeepCopy()
  539. localSecret.Data = secretData
  540. if err := secretClient.PushSecret(ctx, localSecret, params.data); err != nil {
  541. return fmt.Errorf(errSetSecretFailed, key, params.storeName, err)
  542. }
  543. return nil
  544. }
  545. func secretKeyExists(key string, data map[string][]byte) bool {
  546. _, ok := data[key]
  547. return key == "" || ok
  548. }
  549. const defaultGeneratorStateKey = "__pushsecret"
  550. func (r *Reconciler) resolveSecrets(ctx context.Context, ps *esapi.PushSecret) ([]v1.Secret, error) {
  551. var err error
  552. generatorState := statemanager.New(ctx, r.Client, r.Scheme, ps.Namespace, ps)
  553. defer func() {
  554. if err != nil {
  555. if err := generatorState.Rollback(); err != nil {
  556. r.Log.Error(err, "error rolling back generator state")
  557. }
  558. return
  559. }
  560. if err := generatorState.Commit(); err != nil {
  561. r.Log.Error(err, "error committing generator state")
  562. }
  563. }()
  564. switch {
  565. case ps.Spec.Selector.Secret != nil && ps.Spec.Selector.Secret.Name != "":
  566. secretName := types.NamespacedName{Name: ps.Spec.Selector.Secret.Name, Namespace: ps.Namespace}
  567. secret := &v1.Secret{}
  568. if err := r.Client.Get(ctx, secretName, secret); err != nil {
  569. return nil, err
  570. }
  571. generatorState.EnqueueFlagLatestStateForGC(defaultGeneratorStateKey)
  572. return []v1.Secret{*secret}, nil
  573. case ps.Spec.Selector.GeneratorRef != nil:
  574. secret, err := r.resolveSecretFromGenerator(ctx, ps.Namespace, ps.Spec.Selector.GeneratorRef, generatorState)
  575. if err != nil {
  576. return nil, fmt.Errorf("could not resolve secret from generator ref %v: %w", ps.Spec.Selector.GeneratorRef, err)
  577. }
  578. return []v1.Secret{*secret}, nil
  579. case ps.Spec.Selector.Secret != nil && ps.Spec.Selector.Secret.Selector != nil:
  580. labelSelector, err := metav1.LabelSelectorAsSelector(ps.Spec.Selector.Secret.Selector)
  581. if err != nil {
  582. return nil, err
  583. }
  584. var secretList v1.SecretList
  585. err = r.List(ctx, &secretList, &client.ListOptions{LabelSelector: labelSelector, Namespace: ps.Namespace})
  586. if err != nil {
  587. return nil, err
  588. }
  589. return secretList.Items, err
  590. }
  591. return nil, errors.New("no secret selector provided")
  592. }
  593. func (r *Reconciler) resolveSecretFromGenerator(ctx context.Context, namespace string, generatorRef *esv1.GeneratorRef, generatorState *statemanager.Manager) (*v1.Secret, error) {
  594. gen, genResource, err := resolvers.GeneratorRef(ctx, r.Client, r.Scheme, namespace, generatorRef)
  595. if err != nil {
  596. return nil, fmt.Errorf("unable to resolve generator: %w", err)
  597. }
  598. var prevState *genv1alpha1.GeneratorState
  599. if generatorState != nil {
  600. prevState, err = generatorState.GetLatestState(defaultGeneratorStateKey)
  601. if err != nil {
  602. return nil, fmt.Errorf("unable to get latest state: %w", err)
  603. }
  604. }
  605. secretMap, newState, err := gen.Generate(ctx, genResource, r.Client, namespace)
  606. if err != nil {
  607. return nil, fmt.Errorf("unable to generate: %w", err)
  608. }
  609. if prevState != nil && generatorState != nil {
  610. generatorState.EnqueueMoveStateToGC(defaultGeneratorStateKey)
  611. }
  612. if generatorState != nil {
  613. generatorState.EnqueueSetLatest(ctx, defaultGeneratorStateKey, namespace, genResource, gen, newState)
  614. }
  615. return &v1.Secret{
  616. ObjectMeta: metav1.ObjectMeta{
  617. Name: "___generated-secret",
  618. Namespace: namespace,
  619. },
  620. Data: secretMap,
  621. }, err
  622. }
  623. // GetSecretStores retrieves the SecretStore and ClusterSecretStore resources
  624. // referenced in the PushSecret. It supports both direct references by name
  625. // and label selectors to find multiple stores.
  626. func (r *Reconciler) GetSecretStores(ctx context.Context, ps esapi.PushSecret) (map[esapi.PushSecretStoreRef]esv1.GenericStore, error) {
  627. stores := make(map[esapi.PushSecretStoreRef]esv1.GenericStore)
  628. for _, refStore := range ps.Spec.SecretStoreRefs {
  629. if refStore.LabelSelector != nil {
  630. labelSelector, err := metav1.LabelSelectorAsSelector(refStore.LabelSelector)
  631. if err != nil {
  632. return nil, fmt.Errorf("could not convert labels: %w", err)
  633. }
  634. if refStore.Kind == esv1.ClusterSecretStoreKind {
  635. clusterSecretStoreList := esv1.ClusterSecretStoreList{}
  636. err = r.List(ctx, &clusterSecretStoreList, &client.ListOptions{LabelSelector: labelSelector})
  637. if err != nil {
  638. return nil, fmt.Errorf("could not list cluster Secret Stores: %w", err)
  639. }
  640. for k, v := range clusterSecretStoreList.Items {
  641. key := esapi.PushSecretStoreRef{
  642. Name: v.Name,
  643. Kind: esv1.ClusterSecretStoreKind,
  644. }
  645. stores[key] = &clusterSecretStoreList.Items[k]
  646. }
  647. } else {
  648. secretStoreList := esv1.SecretStoreList{}
  649. err = r.List(ctx, &secretStoreList, &client.ListOptions{LabelSelector: labelSelector, Namespace: ps.Namespace})
  650. if err != nil {
  651. return nil, fmt.Errorf("could not list Secret Stores: %w", err)
  652. }
  653. for k, v := range secretStoreList.Items {
  654. key := esapi.PushSecretStoreRef{
  655. Name: v.Name,
  656. Kind: esv1.SecretStoreKind,
  657. }
  658. stores[key] = &secretStoreList.Items[k]
  659. }
  660. }
  661. } else {
  662. store, err := r.getSecretStoreFromName(ctx, refStore, ps.Namespace)
  663. if err != nil {
  664. return nil, err
  665. }
  666. key := refStore
  667. key.Kind = resolvedPushStoreKind(refStore.Kind, store)
  668. stores[key] = store
  669. }
  670. }
  671. return stores, nil
  672. }
  673. func (r *Reconciler) getSecretStoreFromName(ctx context.Context, refStore esapi.PushSecretStoreRef, ns string) (esv1.GenericStore, error) {
  674. if refStore.Name == "" {
  675. return nil, errors.New("refStore Name must be provided")
  676. }
  677. ref := types.NamespacedName{
  678. Name: refStore.Name,
  679. }
  680. switch refStore.Kind {
  681. case "", esv1.SecretStoreKind:
  682. ref.Namespace = ns
  683. var store esv1.SecretStore
  684. err := r.Get(ctx, ref, &store)
  685. if err != nil {
  686. return nil, fmt.Errorf(errGetSecretStore, ref.Name, err)
  687. }
  688. return &store, nil
  689. case esv1.ClusterSecretStoreKind:
  690. var store esv1.ClusterSecretStore
  691. err := r.Get(ctx, ref, &store)
  692. if err != nil {
  693. return nil, fmt.Errorf(errGetClusterSecretStore, ref.Name, err)
  694. }
  695. return &store, nil
  696. default:
  697. return nil, fmt.Errorf("unsupported SecretStore kind %q", refStore.Kind)
  698. }
  699. }
  700. // NewPushSecretCondition creates a new PushSecret condition.
  701. func NewPushSecretCondition(condType esapi.PushSecretConditionType, status v1.ConditionStatus, reason, message string) *esapi.PushSecretStatusCondition {
  702. return &esapi.PushSecretStatusCondition{
  703. Type: condType,
  704. Status: status,
  705. LastTransitionTime: metav1.Now(),
  706. Reason: reason,
  707. Message: message,
  708. }
  709. }
  710. // SetPushSecretCondition updates the PushSecret to include the provided condition.
  711. func SetPushSecretCondition(ps *esapi.PushSecret, condition esapi.PushSecretStatusCondition) {
  712. currentCond := GetPushSecretCondition(ps.Status.Conditions, condition.Type)
  713. if currentCond != nil && currentCond.Status == condition.Status &&
  714. currentCond.Reason == condition.Reason && currentCond.Message == condition.Message {
  715. psmetrics.UpdatePushSecretCondition(ps, &condition, 1.0)
  716. return
  717. }
  718. // Do not update lastTransitionTime if the status of the condition doesn't change.
  719. if currentCond != nil && currentCond.Status == condition.Status {
  720. condition.LastTransitionTime = currentCond.LastTransitionTime
  721. }
  722. ps.Status.Conditions = append(FilterOutCondition(ps.Status.Conditions, condition.Type), condition)
  723. if currentCond != nil {
  724. psmetrics.UpdatePushSecretCondition(ps, currentCond, 0.0)
  725. }
  726. psmetrics.UpdatePushSecretCondition(ps, &condition, 1.0)
  727. }
  728. // FilterOutCondition returns an empty set of conditions with the provided type.
  729. func FilterOutCondition(conditions []esapi.PushSecretStatusCondition, condType esapi.PushSecretConditionType) []esapi.PushSecretStatusCondition {
  730. newConditions := make([]esapi.PushSecretStatusCondition, 0, len(conditions))
  731. for _, c := range conditions {
  732. if c.Type == condType {
  733. continue
  734. }
  735. newConditions = append(newConditions, c)
  736. }
  737. return newConditions
  738. }
  739. // GetPushSecretCondition returns the condition with the provided type.
  740. func GetPushSecretCondition(conditions []esapi.PushSecretStatusCondition, condType esapi.PushSecretConditionType) *esapi.PushSecretStatusCondition {
  741. for i := range conditions {
  742. c := conditions[i]
  743. if c.Type == condType {
  744. return &c
  745. }
  746. }
  747. return nil
  748. }
  749. func statusRef(ref esv1.PushSecretData) string {
  750. if ref.GetProperty() != "" {
  751. return ref.GetRemoteKey() + "/" + ref.GetProperty()
  752. }
  753. return ref.GetRemoteKey()
  754. }
  755. // removeUnmanagedStores iterates over all SecretStore references and evaluates the controllerClass property.
  756. // Returns a map containing only managed stores.
  757. func removeUnmanagedStores(ctx context.Context, namespace string, r *Reconciler, ss map[esapi.PushSecretStoreRef]esv1.GenericStore) (map[esapi.PushSecretStoreRef]esv1.GenericStore, error) {
  758. for ref := range ss {
  759. kind := ref.Kind
  760. if kind == "" {
  761. kind = esv1.SecretStoreKind
  762. }
  763. var store esv1.GenericStore
  764. switch kind {
  765. case esv1.SecretStoreKind:
  766. store = &esv1.SecretStore{}
  767. case esv1.ClusterSecretStoreKind:
  768. store = &esv1.ClusterSecretStore{}
  769. namespace = ""
  770. }
  771. err := r.Client.Get(ctx, types.NamespacedName{
  772. Name: ref.Name,
  773. Namespace: namespace,
  774. }, store)
  775. if err != nil {
  776. return ss, err
  777. }
  778. class := store.GetSpec().Controller
  779. if class != "" && class != r.ControllerClass {
  780. delete(ss, ref)
  781. }
  782. }
  783. return ss, nil
  784. }
  785. // matchKeys filters secret keys based on the provided match pattern.
  786. // If pattern is nil or empty, all keys are matched.
  787. func matchKeys(allKeys []string, match *esapi.PushSecretDataToMatch) ([]string, error) {
  788. if match == nil || match.RegExp == "" {
  789. return allKeys, nil
  790. }
  791. re, err := regexp.Compile(match.RegExp)
  792. if err != nil {
  793. return nil, fmt.Errorf("failed to compile regexp pattern %q: %w", match.RegExp, err)
  794. }
  795. matched := make([]string, 0)
  796. for _, key := range allKeys {
  797. if re.MatchString(key) {
  798. matched = append(matched, key)
  799. }
  800. }
  801. return matched, nil
  802. }
  803. // filterDataToForStore returns dataTo entries that target the given store.
  804. func filterDataToForStore(dataToList []esapi.PushSecretDataTo, storeName, storeKind string, storeLabels map[string]string) ([]esapi.PushSecretDataTo, error) {
  805. filtered := make([]esapi.PushSecretDataTo, 0, len(dataToList))
  806. for i, dataTo := range dataToList {
  807. matches, err := dataToMatchesStore(dataTo, storeName, storeKind, storeLabels)
  808. if err != nil {
  809. return nil, fmt.Errorf("dataTo[%d]: %w", i, err)
  810. }
  811. if matches {
  812. filtered = append(filtered, dataTo)
  813. }
  814. }
  815. return filtered, nil
  816. }
  817. // dataToMatchesStore reports whether a single dataTo entry targets the given store.
  818. func dataToMatchesStore(dataTo esapi.PushSecretDataTo, storeName, storeKind string, storeLabels map[string]string) (bool, error) {
  819. if dataTo.StoreRef == nil {
  820. return false, fmt.Errorf("storeRef is required")
  821. }
  822. refKind := dataTo.StoreRef.Kind
  823. if refKind == "" {
  824. refKind = esv1.SecretStoreKind
  825. }
  826. if dataTo.StoreRef.Name != "" {
  827. return dataTo.StoreRef.Name == storeName && refKind == storeKind, nil
  828. }
  829. if dataTo.StoreRef.LabelSelector == nil {
  830. return false, nil
  831. }
  832. selector, err := metav1.LabelSelectorAsSelector(dataTo.StoreRef.LabelSelector)
  833. if err != nil {
  834. return false, fmt.Errorf("invalid labelSelector: %w", err)
  835. }
  836. return refKind == storeKind && selector.Matches(labels.Set(storeLabels)), nil
  837. }
  838. // expandDataTo expands dataTo entries into individual PushSecretData entries.
  839. //
  840. // Two modes are supported per dataTo entry:
  841. //
  842. // Per-key mode (default, no remoteKey set): each matched key becomes a separate entry
  843. // pushed independently. This enables individual key transformation, per-key status
  844. // tracking, granular deletion, and compatibility with all providers.
  845. //
  846. // Bundle mode (remoteKey set): all matched keys are bundled into a single provider
  847. // secret at the given remoteKey path as a JSON object. A single PushSecretData entry
  848. // with SecretKey="" is produced, and the bundleOverrides map carries the filtered
  849. // key set so only matched keys appear in the pushed JSON blob.
  850. //
  851. // Returns the expanded entries, a bundleOverrides map (remoteKey -> filtered data),
  852. // and any error.
  853. func (r *Reconciler) expandDataTo(secret *v1.Secret, dataToList []esapi.PushSecretDataTo) ([]esapi.PushSecretData, map[string]map[string][]byte, error) {
  854. if len(dataToList) == 0 {
  855. return nil, nil, nil
  856. }
  857. allData := make([]esapi.PushSecretData, 0)
  858. bundleOverrides := make(map[string]map[string][]byte)
  859. overallRemoteKeys := make(map[string]string)
  860. for i, dataTo := range dataToList {
  861. entries, keyMap, filteredData, err := r.expandSingleDataTo(secret, dataTo)
  862. if err != nil {
  863. return nil, nil, fmt.Errorf("dataTo[%d]: %w", i, err)
  864. }
  865. if len(entries) == 0 {
  866. r.Log.Info("dataTo entry matched no keys", "index", i)
  867. continue
  868. }
  869. if err := registerRemoteKeys(overallRemoteKeys, keyMap, i); err != nil {
  870. return nil, nil, err
  871. }
  872. recordBundleOverrides(bundleOverrides, entries, filteredData)
  873. allData = append(allData, entries...)
  874. r.Log.Info("expanded dataTo entry", "index", i, "matchedKeys", len(entries), "created", len(keyMap))
  875. }
  876. return allData, bundleOverrides, nil
  877. }
  878. // registerRemoteKeys checks for duplicate remote keys across dataTo entries and
  879. // records new mappings. Returns an error if a duplicate is found.
  880. func registerRemoteKeys(seen, keyMap map[string]string, index int) error {
  881. for sourceKey, remoteKey := range keyMap {
  882. if existingSource, exists := seen[remoteKey]; exists {
  883. return fmt.Errorf("dataTo[%d]: duplicate remote key %q from source key %q (conflicts with %s)", index, remoteKey, sourceKey, existingSource)
  884. }
  885. seen[remoteKey] = fmt.Sprintf("dataTo[%d]:%s", index, sourceKey)
  886. }
  887. return nil
  888. }
  889. // recordBundleOverrides associates filtered data with bundle entries so only
  890. // matched keys appear in the pushed JSON blob.
  891. func recordBundleOverrides(overrides map[string]map[string][]byte, entries []esapi.PushSecretData, filteredData map[string][]byte) {
  892. if filteredData == nil {
  893. return
  894. }
  895. for _, entry := range entries {
  896. overrides[statusRef(entry)] = filteredData
  897. }
  898. }
  899. // expandSingleDataTo processes a single dataTo entry: converts keys, matches them
  900. // against the pattern, applies rewrites, validates remote keys, and builds the
  901. // resulting PushSecretData entries along with the source-to-remote key mapping.
  902. //
  903. // Bundle mode: when dataTo.RemoteKey is set, all matched keys are bundled into a
  904. // single PushSecretData entry with SecretKey="" targeting dataTo.RemoteKey. The
  905. // third return value carries the filtered (matched+converted) key data so that
  906. // only matched keys appear in the JSON blob pushed to the provider.
  907. //
  908. // Per-key mode: when dataTo.RemoteKey is empty, one PushSecretData entry is
  909. // produced per matched key. The third return value is nil.
  910. func (r *Reconciler) expandSingleDataTo(secret *v1.Secret, dataTo esapi.PushSecretDataTo) ([]esapi.PushSecretData, map[string]string, map[string][]byte, error) {
  911. if dataTo.RemoteKey != "" && len(dataTo.Rewrite) > 0 {
  912. return nil, nil, nil, fmt.Errorf("remoteKey and rewrite are mutually exclusive: rewrite is only supported in per-key mode (without remoteKey)")
  913. }
  914. convertedData, err := esutils.ReverseKeys(dataTo.ConversionStrategy, secret.Data)
  915. if err != nil {
  916. return nil, nil, nil, fmt.Errorf("conversion failed: %w", err)
  917. }
  918. // Map converted keys back to the original K8s secret keys. The resulting
  919. // PushSecretData entries store the original key so that
  920. // resolveSourceKeyConflicts can compare dataTo entries against explicit
  921. // data entries in the same key space. ConversionStrategy is set to None on
  922. // expanded entries because the conversion was already applied during
  923. // matching and rewriting; handlePushSecretDataForStore will look up the
  924. // original key directly in the unconverted secret data.
  925. convertedToOriginal := make(map[string]string, len(secret.Data))
  926. for origKey := range secret.Data {
  927. convKey := esutils.ReverseKey(dataTo.ConversionStrategy, origKey)
  928. convertedToOriginal[convKey] = origKey
  929. }
  930. allKeys := make([]string, 0, len(convertedData))
  931. for key := range convertedData {
  932. allKeys = append(allKeys, key)
  933. }
  934. slices.Sort(allKeys)
  935. matchedKeys, err := matchKeys(allKeys, dataTo.Match)
  936. if err != nil {
  937. return nil, nil, nil, fmt.Errorf("match failed: %w", err)
  938. }
  939. if len(matchedKeys) == 0 {
  940. return nil, nil, nil, nil
  941. }
  942. matchedData := make(map[string][]byte, len(matchedKeys))
  943. for _, key := range matchedKeys {
  944. matchedData[key] = convertedData[key]
  945. }
  946. if dataTo.RemoteKey != "" {
  947. keyMap := map[string]string{bundleSourceKey: dataTo.RemoteKey}
  948. entry := esapi.PushSecretData{
  949. Match: esapi.PushSecretMatch{
  950. SecretKey: "",
  951. RemoteRef: esapi.PushSecretRemoteRef{
  952. RemoteKey: dataTo.RemoteKey,
  953. },
  954. },
  955. Metadata: dataTo.Metadata,
  956. ConversionStrategy: esapi.PushSecretConversionNone,
  957. }
  958. return []esapi.PushSecretData{entry}, keyMap, matchedData, nil
  959. }
  960. keyMap, err := rewriteWithKeyMapping(dataTo.Rewrite, matchedData)
  961. if err != nil {
  962. return nil, nil, nil, fmt.Errorf("rewrite failed: %w", err)
  963. }
  964. for sourceKey, remoteKey := range keyMap {
  965. if remoteKey == "" {
  966. return nil, nil, nil, fmt.Errorf("empty remote key produced for source key %q", sourceKey)
  967. }
  968. }
  969. sortedKeys := slices.Sorted(maps.Keys(keyMap))
  970. entries := make([]esapi.PushSecretData, 0, len(keyMap))
  971. for _, convertedKey := range sortedKeys {
  972. entries = append(entries, esapi.PushSecretData{
  973. Match: esapi.PushSecretMatch{
  974. SecretKey: convertedToOriginal[convertedKey],
  975. RemoteRef: esapi.PushSecretRemoteRef{
  976. RemoteKey: keyMap[convertedKey],
  977. },
  978. },
  979. Metadata: dataTo.Metadata,
  980. ConversionStrategy: esapi.PushSecretConversionNone,
  981. })
  982. }
  983. return entries, keyMap, nil, nil
  984. }
  985. // validateDataToStoreRefs checks that each dataTo entry has a valid storeRef.
  986. func validateDataToStoreRefs(dataToList []esapi.PushSecretDataTo, storeRefs []esapi.PushSecretStoreRef) error {
  987. for i, d := range dataToList {
  988. if d.StoreRef == nil {
  989. return fmt.Errorf("dataTo[%d]: storeRef is required", i)
  990. }
  991. if d.StoreRef.Name == "" && d.StoreRef.LabelSelector == nil {
  992. return fmt.Errorf("dataTo[%d]: storeRef must have name or labelSelector", i)
  993. }
  994. if d.StoreRef.Name != "" && !storeRefExistsInList(d.StoreRef, storeRefs) {
  995. return fmt.Errorf("dataTo[%d]: storeRef %q not found in secretStoreRefs", i, d.StoreRef.Name)
  996. }
  997. }
  998. return nil
  999. }
  1000. // storeRefExistsInList checks if a named ref matches any named entry in storeRefs.
  1001. func storeRefExistsInList(ref *esapi.PushSecretStoreRef, storeRefs []esapi.PushSecretStoreRef) bool {
  1002. refKind := ref.Kind
  1003. if refKind == "" {
  1004. refKind = esv1.SecretStoreKind
  1005. }
  1006. for _, sr := range storeRefs {
  1007. if sr.Name == "" {
  1008. continue
  1009. }
  1010. srKind := sr.Kind
  1011. if srKind == "" {
  1012. srKind = esv1.SecretStoreKind
  1013. }
  1014. if srKind == refKind && sr.Name == ref.Name {
  1015. return true
  1016. }
  1017. }
  1018. return false
  1019. }
  1020. func resolvedStoreInfo(ref esapi.PushSecretStoreRef, store any) (storeInfo, bool) {
  1021. if genericStore, ok := store.(esv1.GenericStore); ok {
  1022. kind := resolvedPushStoreKind(ref.Kind, genericStore)
  1023. return storeInfo{
  1024. Name: genericStore.GetName(),
  1025. Kind: kind,
  1026. Labels: genericStore.GetLabels(),
  1027. }, true
  1028. }
  1029. if obj, ok := store.(client.Object); ok {
  1030. kind := resolvedPushStoreKind(ref.Kind, obj)
  1031. return storeInfo{
  1032. Name: obj.GetName(),
  1033. Kind: kind,
  1034. Labels: obj.GetLabels(),
  1035. }, true
  1036. }
  1037. return storeInfo{}, false
  1038. }
  1039. func resolvedPushStoreKind(refKind string, store any) string {
  1040. if refKind != "" {
  1041. return refKind
  1042. }
  1043. if genericStore, ok := store.(esv1.GenericStore); ok {
  1044. return genericStore.GetKind()
  1045. }
  1046. return esv1.SecretStoreKind
  1047. }
  1048. // validateDataToMatchesResolvedStores checks that every dataTo entry with a
  1049. // labelSelector actually matches at least one resolved store. Without this,
  1050. // a misconfigured labelSelector silently becomes a no-op.
  1051. func validateDataToMatchesResolvedStores(dataToList []esapi.PushSecretDataTo, stores []storeInfo) error {
  1052. for i, dataTo := range dataToList {
  1053. if dataTo.StoreRef == nil || dataTo.StoreRef.LabelSelector == nil {
  1054. continue
  1055. }
  1056. if dataTo.StoreRef.Name != "" {
  1057. continue
  1058. }
  1059. selector, err := metav1.LabelSelectorAsSelector(dataTo.StoreRef.LabelSelector)
  1060. if err != nil {
  1061. return fmt.Errorf("dataTo[%d]: invalid labelSelector: %w", i, err)
  1062. }
  1063. refKind := dataTo.StoreRef.Kind
  1064. if refKind == "" {
  1065. refKind = esv1.SecretStoreKind
  1066. }
  1067. if !anyStoreMatchesSelector(refKind, selector, stores) {
  1068. return fmt.Errorf("dataTo[%d]: labelSelector does not match any store in secretStoreRefs", i)
  1069. }
  1070. }
  1071. return nil
  1072. }
  1073. // anyStoreMatchesSelector returns true if at least one resolved store matches
  1074. // the given kind and label selector.
  1075. func anyStoreMatchesSelector(kind string, selector labels.Selector, stores []storeInfo) bool {
  1076. for _, store := range stores {
  1077. if store.Kind == kind && selector.Matches(labels.Set(store.Labels)) {
  1078. return true
  1079. }
  1080. }
  1081. return false
  1082. }
  1083. // rewriteWithKeyMapping applies rewrites and returns originalKey -> rewrittenKey mapping.
  1084. func rewriteWithKeyMapping(rewrites []esapi.PushSecretRewrite, data map[string][]byte) (map[string]string, error) {
  1085. keyMap := make(map[string]string, len(data))
  1086. for k := range data {
  1087. keyMap[k] = k
  1088. }
  1089. for i, op := range rewrites {
  1090. applyFn, err := compileRewrite(op)
  1091. if err != nil {
  1092. return nil, fmt.Errorf("rewrite[%d]: %w", i, err)
  1093. }
  1094. newKeyMap := make(map[string]string, len(keyMap))
  1095. for origKey, currentKey := range keyMap {
  1096. newKey, err := applyFn(currentKey)
  1097. if err != nil {
  1098. return nil, fmt.Errorf("rewrite[%d] on key %q: %w", i, currentKey, err)
  1099. }
  1100. newKeyMap[origKey] = newKey
  1101. }
  1102. keyMap = newKeyMap
  1103. }
  1104. return keyMap, nil
  1105. }
  1106. // compileRewrite pre-compiles a rewrite operation (regexp or template) and
  1107. // returns a function that applies it to a key. This avoids re-compiling the
  1108. // same regexp or re-parsing the same template for every key.
  1109. func compileRewrite(op esapi.PushSecretRewrite) (func(string) (string, error), error) {
  1110. switch {
  1111. case op.Regexp != nil:
  1112. re, err := regexp.Compile(op.Regexp.Source)
  1113. if err != nil {
  1114. return nil, fmt.Errorf("invalid regexp %q: %w", op.Regexp.Source, err)
  1115. }
  1116. target := op.Regexp.Target
  1117. return func(key string) (string, error) {
  1118. return re.ReplaceAllString(key, target), nil
  1119. }, nil
  1120. case op.Transform != nil:
  1121. tmpl, err := template.New("t").Funcs(estemplate.FuncMap()).Parse(op.Transform.Template)
  1122. if err != nil {
  1123. return nil, fmt.Errorf("invalid template: %w", err)
  1124. }
  1125. return func(key string) (string, error) {
  1126. var buf bytes.Buffer
  1127. if err := tmpl.Execute(&buf, map[string]string{"value": key}); err != nil {
  1128. return "", fmt.Errorf("template exec: %w", err)
  1129. }
  1130. return buf.String(), nil
  1131. }, nil
  1132. default:
  1133. return func(key string) (string, error) { return key, nil }, nil
  1134. }
  1135. }
  1136. // resolveSourceKeyConflicts merges dataTo and explicit data entries.
  1137. // When both reference the same source secret key, explicit data wins.
  1138. // Comparison is done using the original (raw) K8s secret key. DataTo entries
  1139. // already store the original key; explicit data entries may store a converted
  1140. // key when ConversionStrategy is set, so we normalize them via the secret.
  1141. func resolveSourceKeyConflicts(dataToEntries, explicitData []esapi.PushSecretData, secret *v1.Secret) []esapi.PushSecretData {
  1142. explicitOriginalKeys := make(map[string]struct{}, len(explicitData))
  1143. for _, data := range explicitData {
  1144. origKey := resolveOriginalKey(data, secret)
  1145. explicitOriginalKeys[origKey] = struct{}{}
  1146. }
  1147. result := make([]esapi.PushSecretData, 0, len(dataToEntries)+len(explicitData))
  1148. for _, data := range dataToEntries {
  1149. if _, exists := explicitOriginalKeys[data.GetSecretKey()]; !exists {
  1150. result = append(result, data)
  1151. }
  1152. }
  1153. return append(result, explicitData...)
  1154. }
  1155. // resolveOriginalKey returns the raw K8s secret key for a PushSecretData entry.
  1156. // If the entry uses a ConversionStrategy, SecretKey is the converted (decoded)
  1157. // form, so we find the original key by converting each raw key and matching.
  1158. // If no conversion is active, SecretKey is already the original key.
  1159. func resolveOriginalKey(data esapi.PushSecretData, secret *v1.Secret) string {
  1160. key := data.GetSecretKey()
  1161. if data.ConversionStrategy == "" || data.ConversionStrategy == esapi.PushSecretConversionNone {
  1162. return key
  1163. }
  1164. for origKey := range secret.Data {
  1165. if esutils.ReverseKey(data.ConversionStrategy, origKey) == key {
  1166. return origKey
  1167. }
  1168. }
  1169. return key
  1170. }
  1171. // validateRemoteKeyUniqueness ensures no two entries push to the same remote location.
  1172. // The remote location is defined by (remoteKey, property) tuple.
  1173. func validateRemoteKeyUniqueness(entries []esapi.PushSecretData) error {
  1174. type remoteLocation struct {
  1175. remoteKey string
  1176. property string
  1177. }
  1178. seen := make(map[remoteLocation]string) // location -> source key (for error message)
  1179. for _, data := range entries {
  1180. loc := remoteLocation{
  1181. remoteKey: data.GetRemoteKey(),
  1182. property: data.GetProperty(),
  1183. }
  1184. sourceKey := data.GetSecretKey()
  1185. if existingSource, exists := seen[loc]; exists {
  1186. if loc.property != "" {
  1187. return fmt.Errorf(
  1188. "duplicate remote key %q with property %q: source keys %q and %q both map to the same destination",
  1189. loc.remoteKey, loc.property, existingSource, sourceKey)
  1190. }
  1191. return fmt.Errorf(
  1192. "duplicate remote key %q: source keys %q and %q both map to the same destination",
  1193. loc.remoteKey, existingSource, sourceKey)
  1194. }
  1195. seen[loc] = sourceKey
  1196. }
  1197. return nil
  1198. }
  1199. // mergeDataEntries combines dataTo and explicit data entries.
  1200. // It resolves source key conflicts (explicit wins) and validates no duplicate remote destinations.
  1201. func mergeDataEntries(dataToEntries, explicitData []esapi.PushSecretData, secret *v1.Secret) ([]esapi.PushSecretData, error) {
  1202. merged := resolveSourceKeyConflicts(dataToEntries, explicitData, secret)
  1203. if err := validateRemoteKeyUniqueness(merged); err != nil {
  1204. return nil, err
  1205. }
  1206. return merged, nil
  1207. }