| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307 |
- /*
- Copyright © The ESO Authors
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- https://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- // Package pushsecret implements the controller for managing PushSecret resources.
- package pushsecret
- import (
- "bytes"
- "context"
- "errors"
- "fmt"
- "maps"
- "regexp"
- "slices"
- "strings"
- "text/template"
- "time"
- "github.com/go-logr/logr"
- v1 "k8s.io/api/core/v1"
- apierrors "k8s.io/apimachinery/pkg/api/errors"
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "k8s.io/apimachinery/pkg/labels"
- "k8s.io/apimachinery/pkg/runtime"
- "k8s.io/apimachinery/pkg/types"
- "k8s.io/client-go/rest"
- "k8s.io/client-go/tools/record"
- ctrl "sigs.k8s.io/controller-runtime"
- "sigs.k8s.io/controller-runtime/pkg/builder"
- "sigs.k8s.io/controller-runtime/pkg/client"
- "sigs.k8s.io/controller-runtime/pkg/controller"
- "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
- "sigs.k8s.io/controller-runtime/pkg/event"
- "sigs.k8s.io/controller-runtime/pkg/predicate"
- esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
- esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
- genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
- ctrlmetrics "github.com/external-secrets/external-secrets/pkg/controllers/metrics"
- "github.com/external-secrets/external-secrets/pkg/controllers/pushsecret/psmetrics"
- "github.com/external-secrets/external-secrets/pkg/controllers/secretstore"
- ctrlutil "github.com/external-secrets/external-secrets/pkg/controllers/util"
- "github.com/external-secrets/external-secrets/runtime/esutils"
- "github.com/external-secrets/external-secrets/runtime/esutils/resolvers"
- "github.com/external-secrets/external-secrets/runtime/statemanager"
- estemplate "github.com/external-secrets/external-secrets/runtime/template/v2"
- "github.com/external-secrets/external-secrets/runtime/util/locks"
- // Load registered generators.
- _ "github.com/external-secrets/external-secrets/pkg/register"
- )
- const (
- errFailedGetSecret = "could not get source secret"
- errPatchStatus = "error merging"
- errGetSecretStore = "could not get SecretStore %q, %w"
- errGetClusterSecretStore = "could not get ClusterSecretStore %q, %w"
- errSetSecretFailed = "could not write remote ref %v to target secretstore %v: %v"
- errFailedSetSecret = "set secret failed: %v"
- errConvert = "could not apply conversion strategy to keys: %v"
- pushSecretFinalizer = "pushsecret.externalsecrets.io/finalizer"
- errCloudNotUpdateFinalizer = "could not update finalizers: %w"
- bundleSourceKey = "(bundle)"
- )
- // Reconciler is the controller for PushSecret resources.
- // It manages the lifecycle of PushSecrets, ensuring that secrets are pushed to
- // specified secret stores according to the defined policies and templates.
- type Reconciler struct {
- client.Client
- Log logr.Logger
- Scheme *runtime.Scheme
- recorder record.EventRecorder
- RestConfig *rest.Config
- RequeueInterval time.Duration
- ControllerClass string
- }
- // storeInfo holds the identifying attributes of a secret store for per-store processing.
- type storeInfo struct {
- Name string
- Kind string
- Labels map[string]string
- }
- // SetupWithManager sets up the controller with the Manager.
- // It configures the controller to watch PushSecret resources and
- // manages indexing for efficient lookups based on secret stores and deletion policies.
- func (r *Reconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opts controller.Options) error {
- r.recorder = mgr.GetEventRecorderFor("pushsecret")
- // Index PushSecrets by the stores they have pushed to (for finalizer management on store deletion)
- // Refer to common.go for more details on the index function
- if err := mgr.GetFieldIndexer().IndexField(ctx, &esapi.PushSecret{}, "status.syncedPushSecrets", func(obj client.Object) []string {
- ps := obj.(*esapi.PushSecret)
- // Only index PushSecrets with DeletionPolicy=Delete for efficiency
- if ps.Spec.DeletionPolicy != esapi.PushSecretDeletionPolicyDelete {
- return nil
- }
- // Format is typically "Kind/Name" (e.g., "SecretStore/store1", "ClusterSecretStore/clusterstore1")
- storeKeys := make([]string, 0, len(ps.Status.SyncedPushSecrets))
- for storeKey := range ps.Status.SyncedPushSecrets {
- storeKeys = append(storeKeys, storeKey)
- }
- return storeKeys
- }); err != nil {
- return err
- }
- // Index PushSecrets by deletionPolicy for quick filtering
- if err := mgr.GetFieldIndexer().IndexField(ctx, &esapi.PushSecret{}, "spec.deletionPolicy", func(obj client.Object) []string {
- ps := obj.(*esapi.PushSecret)
- return []string{string(ps.Spec.DeletionPolicy)}
- }); err != nil {
- return err
- }
- return ctrl.NewControllerManagedBy(mgr).
- WithOptions(opts).
- For(&esapi.PushSecret{}, builder.WithPredicates(pushSecretWatchPredicate())).
- Complete(r)
- }
- func pushSecretWatchPredicate() predicate.Predicate {
- return predicate.Funcs{
- CreateFunc: func(event.CreateEvent) bool {
- return true
- },
- DeleteFunc: func(event.DeleteEvent) bool {
- return true
- },
- UpdateFunc: func(e event.UpdateEvent) bool {
- if e.ObjectOld == nil || e.ObjectNew == nil {
- return true
- }
- return shouldReconcilePushSecretUpdate(e.ObjectOld, e.ObjectNew)
- },
- }
- }
- func shouldReconcilePushSecretUpdate(oldObj, newObj client.Object) bool {
- if oldObj.GetGeneration() != newObj.GetGeneration() {
- return true
- }
- if !maps.Equal(oldObj.GetLabels(), newObj.GetLabels()) {
- return true
- }
- if !maps.Equal(oldObj.GetAnnotations(), newObj.GetAnnotations()) {
- return true
- }
- oldDeleting := oldObj.GetDeletionTimestamp() != nil
- newDeleting := newObj.GetDeletionTimestamp() != nil
- return oldDeleting != newDeleting
- }
- // Reconcile is part of the main kubernetes reconciliation loop which aims to
- // move the current state of the cluster closer to the desired state.
- // For more details, check Reconcile and its Result here:
- // - https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/reconcile
- func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
- log := r.Log.WithValues("pushsecret", req.NamespacedName)
- resourceLabels := ctrlmetrics.RefineNonConditionMetricLabels(map[string]string{"name": req.Name, "namespace": req.Namespace})
- start := time.Now()
- pushSecretReconcileDuration := psmetrics.GetGaugeVec(psmetrics.PushSecretReconcileDurationKey)
- defer func() { pushSecretReconcileDuration.With(resourceLabels).Set(float64(time.Since(start))) }()
- var ps esapi.PushSecret
- mgr := secretstore.NewManager(r.Client, r.ControllerClass, false)
- defer func() {
- _ = mgr.Close(ctx)
- }()
- if err := r.Get(ctx, req.NamespacedName, &ps); err != nil {
- if apierrors.IsNotFound(err) {
- return ctrl.Result{}, nil
- }
- msg := "unable to get PushSecret"
- r.recorder.Event(&ps, v1.EventTypeWarning, esapi.ReasonErrored, msg)
- log.Error(err, msg)
- return ctrl.Result{}, fmt.Errorf("get resource: %w", err)
- }
- refreshInt := r.RequeueInterval
- if ps.Spec.RefreshInterval != nil {
- refreshInt = ps.Spec.RefreshInterval.Duration
- }
- p := client.MergeFrom(ps.DeepCopy())
- defer func() {
- err := r.Client.Status().Patch(ctx, &ps, p)
- if err != nil && !apierrors.IsNotFound(err) {
- log.Error(err, errPatchStatus)
- }
- }()
- switch ps.Spec.DeletionPolicy {
- case esapi.PushSecretDeletionPolicyDelete:
- // finalizer logic. Only added if we should delete the secrets
- if ps.ObjectMeta.DeletionTimestamp.IsZero() {
- if added := controllerutil.AddFinalizer(&ps, pushSecretFinalizer); added {
- if err := r.Client.Update(ctx, &ps, &client.UpdateOptions{}); err != nil {
- return ctrl.Result{}, fmt.Errorf(errCloudNotUpdateFinalizer, err)
- }
- return ctrl.Result{Requeue: true}, nil
- }
- } else if controllerutil.ContainsFinalizer(&ps, pushSecretFinalizer) {
- // trigger a cleanup with no Synced Map
- badState, err := r.DeleteSecretFromProviders(ctx, &ps, esapi.SyncedPushSecretsMap{}, mgr)
- if err != nil {
- msg := fmt.Sprintf("Failed to Delete Secrets from Provider: %v", err)
- r.markAsFailed(msg, &ps, badState)
- return ctrl.Result{}, err
- }
- controllerutil.RemoveFinalizer(&ps, pushSecretFinalizer)
- if err := r.Client.Update(ctx, &ps, &client.UpdateOptions{}); err != nil {
- return ctrl.Result{}, fmt.Errorf("could not update finalizers: %w", err)
- }
- return ctrl.Result{}, nil
- }
- case esapi.PushSecretDeletionPolicyNone:
- if controllerutil.ContainsFinalizer(&ps, pushSecretFinalizer) {
- controllerutil.RemoveFinalizer(&ps, pushSecretFinalizer)
- if err := r.Client.Update(ctx, &ps, &client.UpdateOptions{}); err != nil {
- return ctrl.Result{}, fmt.Errorf(errCloudNotUpdateFinalizer, err)
- }
- }
- default:
- }
- timeSinceLastRefresh := 0 * time.Second
- if !ps.Status.RefreshTime.IsZero() {
- timeSinceLastRefresh = time.Since(ps.Status.RefreshTime.Time)
- }
- if !shouldRefresh(ps) {
- refreshInt = (ps.Spec.RefreshInterval.Duration - timeSinceLastRefresh) + 5*time.Second
- log.V(1).Info("skipping refresh", "rv", ctrlutil.GetResourceVersion(ps.ObjectMeta), "nr", refreshInt.Seconds())
- return ctrl.Result{RequeueAfter: refreshInt}, nil
- }
- if err := validateDataToStoreRefs(ps.Spec.DataTo, ps.Spec.SecretStoreRefs); err != nil {
- r.markAsFailed(err.Error(), &ps, nil)
- return ctrl.Result{}, err
- }
- secrets, err := r.resolveSecrets(ctx, &ps)
- if err != nil {
- isSecretSelector := ps.Spec.Selector.Secret != nil && ps.Spec.Selector.Secret.Name != ""
- if apierrors.IsNotFound(err) && isSecretSelector &&
- ps.Spec.DeletionPolicy == esapi.PushSecretDeletionPolicyDelete &&
- len(ps.Status.SyncedPushSecrets) > 0 {
- return ctrl.Result{}, r.handleSourceSecretDeleted(ctx, &ps, mgr)
- }
- r.markAsFailed(errFailedGetSecret, &ps, nil)
- return ctrl.Result{}, err
- }
- secretStores, err := r.GetSecretStores(ctx, ps)
- if err != nil {
- r.markAsFailed(err.Error(), &ps, nil)
- return ctrl.Result{}, err
- }
- // Filter out SecretStores that are being deleted to avoid finalizer conflicts
- activeSecretStores := make(map[esapi.PushSecretStoreRef]esv1.GenericStore, len(secretStores))
- for ref, store := range secretStores {
- // Skip stores that are being deleted
- if !store.GetDeletionTimestamp().IsZero() {
- log.Info("skipping SecretStore that is being deleted", "storeName", store.GetName(), "storeKind", store.GetKind())
- continue
- }
- activeSecretStores[ref] = store
- }
- secretStores, err = removeUnmanagedStores(ctx, req.Namespace, r, activeSecretStores)
- if err != nil {
- r.markAsFailed(err.Error(), &ps, nil)
- return ctrl.Result{}, err
- }
- // if no stores are managed by this controller
- if len(secretStores) == 0 {
- return ctrl.Result{}, nil
- }
- if err := validateDataToMatchesResolvedStores(ps.Spec.DataTo, secretStores); err != nil {
- r.markAsFailed(err.Error(), &ps, nil)
- return ctrl.Result{}, err
- }
- allSyncedSecrets := make(esapi.SyncedPushSecretsMap)
- for _, secret := range secrets {
- if err := r.applyTemplate(ctx, &ps, &secret); err != nil {
- return ctrl.Result{}, err
- }
- syncedSecrets, err := r.PushSecretToProviders(ctx, secretStores, ps, &secret, mgr)
- if err != nil {
- if errors.Is(err, locks.ErrConflict) {
- log.Info("retry to acquire lock to update the secret later", "error", err)
- return ctrl.Result{Requeue: true}, nil
- }
- totalSecrets := mergeSecretState(syncedSecrets, ps.Status.SyncedPushSecrets)
- msg := fmt.Sprintf(errFailedSetSecret, err)
- r.markAsFailed(msg, &ps, totalSecrets)
- return ctrl.Result{}, err
- }
- switch ps.Spec.DeletionPolicy {
- case esapi.PushSecretDeletionPolicyDelete:
- badSyncState, err := r.DeleteSecretFromProviders(ctx, &ps, syncedSecrets, mgr)
- if err != nil {
- msg := fmt.Sprintf("Failed to Delete Secrets from Provider: %v", err)
- r.markAsFailed(msg, &ps, badSyncState)
- return ctrl.Result{}, err
- }
- case esapi.PushSecretDeletionPolicyNone:
- default:
- }
- allSyncedSecrets = mergeSecretState(allSyncedSecrets, syncedSecrets)
- }
- r.markAsDone(&ps, allSyncedSecrets, start)
- return ctrl.Result{RequeueAfter: refreshInt}, nil
- }
- // handleSourceSecretDeleted cleans up provider secrets when source Secret is unavailable.
- func (r *Reconciler) handleSourceSecretDeleted(ctx context.Context, ps *esapi.PushSecret, mgr *secretstore.Manager) error {
- log := r.Log.WithValues("pushsecret", client.ObjectKeyFromObject(ps))
- log.Info("source secret unavailable, cleaning up provider secrets", "syncedSecrets", len(ps.Status.SyncedPushSecrets))
- badState, err := r.DeleteSecretFromProviders(ctx, ps, esapi.SyncedPushSecretsMap{}, mgr)
- if err != nil {
- msg := fmt.Sprintf("failed to cleanup provider secrets: %v", err)
- r.markAsFailed(msg, ps, badState)
- return err
- }
- r.setSecrets(ps, esapi.SyncedPushSecretsMap{})
- r.markAsSourceDeleted(ps)
- return nil
- }
- func shouldRefresh(ps esapi.PushSecret) bool {
- if ps.Status.SyncedResourceVersion != ctrlutil.GetResourceVersion(ps.ObjectMeta) {
- return true
- }
- if ps.Spec.RefreshInterval.Duration == 0 && ps.Status.SyncedResourceVersion != "" {
- return false
- }
- if ps.Status.RefreshTime.IsZero() {
- return true
- }
- return ps.Status.RefreshTime.Add(ps.Spec.RefreshInterval.Duration).Before(time.Now())
- }
- func (r *Reconciler) markAsFailed(msg string, ps *esapi.PushSecret, syncState esapi.SyncedPushSecretsMap) {
- cond := NewPushSecretCondition(esapi.PushSecretReady, v1.ConditionFalse, esapi.ReasonErrored, msg)
- SetPushSecretCondition(ps, *cond)
- if syncState != nil {
- r.setSecrets(ps, syncState)
- }
- r.recorder.Event(ps, v1.EventTypeWarning, esapi.ReasonErrored, msg)
- }
- func (r *Reconciler) markAsSourceDeleted(ps *esapi.PushSecret) {
- msg := "source secret deleted; provider secrets cleaned up"
- cond := NewPushSecretCondition(esapi.PushSecretReady, v1.ConditionFalse, esapi.ReasonSourceDeleted, msg)
- SetPushSecretCondition(ps, *cond)
- r.recorder.Event(ps, v1.EventTypeNormal, esapi.ReasonSourceDeleted, msg)
- }
- func (r *Reconciler) markAsDone(ps *esapi.PushSecret, secrets esapi.SyncedPushSecretsMap, start time.Time) {
- msg := "PushSecret synced successfully"
- if ps.Spec.UpdatePolicy == esapi.PushSecretUpdatePolicyIfNotExists {
- msg += ". Existing secrets in providers unchanged."
- }
- cond := NewPushSecretCondition(esapi.PushSecretReady, v1.ConditionTrue, esapi.ReasonSynced, msg)
- SetPushSecretCondition(ps, *cond)
- r.setSecrets(ps, secrets)
- ps.Status.RefreshTime = metav1.NewTime(start)
- ps.Status.SyncedResourceVersion = ctrlutil.GetResourceVersion(ps.ObjectMeta)
- r.recorder.Event(ps, v1.EventTypeNormal, esapi.ReasonSynced, msg)
- }
- func (r *Reconciler) setSecrets(ps *esapi.PushSecret, status esapi.SyncedPushSecretsMap) {
- ps.Status.SyncedPushSecrets = status
- }
- func mergeSecretState(newMap, old esapi.SyncedPushSecretsMap) esapi.SyncedPushSecretsMap {
- if newMap == nil {
- return old
- }
- out := newMap.DeepCopy()
- for k, v := range old {
- _, ok := out[k]
- if !ok {
- out[k] = make(map[string]esapi.PushSecretData)
- }
- maps.Insert(out[k], maps.All(v))
- }
- return out
- }
- // DeleteSecretFromProviders removes secrets from providers that are no longer needed.
- // It compares the existing synced secrets in the PushSecret status with the new desired state,
- // and deletes any secrets that are no longer present in the new state.
- func (r *Reconciler) DeleteSecretFromProviders(ctx context.Context, ps *esapi.PushSecret, newMap esapi.SyncedPushSecretsMap, mgr *secretstore.Manager) (esapi.SyncedPushSecretsMap, error) {
- out := mergeSecretState(newMap, ps.Status.SyncedPushSecrets)
- for storeName, oldData := range ps.Status.SyncedPushSecrets {
- storeRef := esv1.SecretStoreRef{
- Name: strings.Split(storeName, "/")[1],
- Kind: strings.Split(storeName, "/")[0],
- }
- client, err := mgr.Get(ctx, storeRef, ps.Namespace, nil)
- if err != nil {
- return out, fmt.Errorf("could not get secrets client for store %v: %w", storeName, err)
- }
- newData, ok := newMap[storeName]
- if !ok {
- err = r.DeleteAllSecretsFromStore(ctx, client, oldData)
- if err != nil {
- return out, err
- }
- delete(out, storeName)
- continue
- }
- for oldEntry, oldRef := range oldData {
- _, ok := newData[oldEntry]
- if !ok {
- err = r.DeleteSecretFromStore(ctx, client, oldRef)
- if err != nil {
- return out, err
- }
- delete(out[storeName], oldEntry)
- }
- }
- }
- return out, nil
- }
- // DeleteAllSecretsFromStore removes all secrets from a given secret store.
- func (r *Reconciler) DeleteAllSecretsFromStore(ctx context.Context, client esv1.SecretsClient, data map[string]esapi.PushSecretData) error {
- for _, v := range data {
- err := r.DeleteSecretFromStore(ctx, client, v)
- if err != nil {
- return err
- }
- }
- return nil
- }
- // DeleteSecretFromStore removes a specific secret from a given secret store.
- func (r *Reconciler) DeleteSecretFromStore(ctx context.Context, client esv1.SecretsClient, data esapi.PushSecretData) error {
- return client.DeleteSecret(ctx, data.Match.RemoteRef)
- }
- // PushSecretToProviders pushes the secret data to the specified secret stores.
- // It iterates over each store and handles the push operation according to the
- // defined update policies and conversion strategies.
- func (r *Reconciler) PushSecretToProviders(
- ctx context.Context,
- stores map[esapi.PushSecretStoreRef]esv1.GenericStore,
- ps esapi.PushSecret,
- secret *v1.Secret,
- mgr *secretstore.Manager,
- ) (esapi.SyncedPushSecretsMap, error) {
- out := make(esapi.SyncedPushSecretsMap)
- var err error
- for ref, store := range stores {
- si := storeInfo{Name: store.GetName(), Kind: ref.Kind, Labels: store.GetLabels()}
- out, err = r.handlePushSecretDataForStore(ctx, ps, secret, out, mgr, si)
- if err != nil {
- return out, err
- }
- }
- return out, nil
- }
- func (r *Reconciler) handlePushSecretDataForStore(
- ctx context.Context,
- ps esapi.PushSecret,
- secret *v1.Secret,
- out esapi.SyncedPushSecretsMap,
- mgr *secretstore.Manager,
- si storeInfo,
- ) (esapi.SyncedPushSecretsMap, error) {
- storeKey := fmt.Sprintf("%v/%v", si.Kind, si.Name)
- out[storeKey] = make(map[string]esapi.PushSecretData)
- storeRef := esv1.SecretStoreRef{
- Name: si.Name,
- Kind: si.Kind,
- }
- secretClient, err := mgr.Get(ctx, storeRef, ps.GetNamespace(), nil)
- if err != nil {
- return out, fmt.Errorf("could not get secrets client for store %v: %w", si.Name, err)
- }
- storeSecret := secret.DeepCopy()
- filteredDataTo, err := filterDataToForStore(ps.Spec.DataTo, si.Name, si.Kind, si.Labels)
- if err != nil {
- return out, fmt.Errorf("failed to filter dataTo: %w", err)
- }
- dataToEntries, bundleOverrides, err := r.expandDataTo(storeSecret, filteredDataTo)
- if err != nil {
- return out, fmt.Errorf("failed to expand dataTo: %w", err)
- }
- allData, err := mergeDataEntries(dataToEntries, ps.Spec.Data, storeSecret)
- if err != nil {
- return out, fmt.Errorf("failed to merge data entries: %w", err)
- }
- originalStoreSecretData := storeSecret.Data
- for _, data := range allData {
- params := pushEntryParams{
- data: data,
- updatePolicy: ps.Spec.UpdatePolicy,
- originalData: originalStoreSecretData,
- dataOverride: bundleOverrides[statusRef(data)],
- storeName: si.Name,
- }
- if err := r.pushSecretEntry(ctx, secretClient, storeSecret, params); err != nil {
- return out, err
- }
- out[storeKey][statusRef(data)] = data
- }
- return out, nil
- }
- // pushEntryParams groups the parameters for pushSecretEntry to keep the
- // function signature within the recommended parameter count.
- type pushEntryParams struct {
- data esapi.PushSecretData
- updatePolicy esapi.PushSecretUpdatePolicy
- originalData map[string][]byte
- dataOverride map[string][]byte
- storeName string
- }
- // pushSecretEntry converts, validates, and pushes a single data entry to the provider.
- // If the update policy is IfNotExists and the secret already exists, the push is skipped.
- // params.dataOverride, when non-nil, replaces params.originalData for the conversion step —
- // used by bundle entries (dataTo with remoteKey) to restrict the pushed payload to matched keys only.
- func (r *Reconciler) pushSecretEntry(
- ctx context.Context,
- secretClient esv1.SecretsClient,
- storeSecret *v1.Secret,
- params pushEntryParams,
- ) error {
- sourceData := params.originalData
- if params.dataOverride != nil {
- sourceData = params.dataOverride
- }
- secretData, err := esutils.ReverseKeys(params.data.ConversionStrategy, sourceData)
- if err != nil {
- return fmt.Errorf(errConvert, err)
- }
- key := params.data.GetSecretKey()
- if !secretKeyExists(key, secretData) {
- return fmt.Errorf("secret key %v does not exist", key)
- }
- if params.updatePolicy == esapi.PushSecretUpdatePolicyIfNotExists {
- exists, err := secretClient.SecretExists(ctx, params.data.Match.RemoteRef)
- if err != nil {
- return fmt.Errorf("could not verify if secret exists in store: %w", err)
- }
- if exists {
- return nil
- }
- }
- localSecret := storeSecret.DeepCopy()
- localSecret.Data = secretData
- if err := secretClient.PushSecret(ctx, localSecret, params.data); err != nil {
- return fmt.Errorf(errSetSecretFailed, key, params.storeName, err)
- }
- return nil
- }
- func secretKeyExists(key string, data map[string][]byte) bool {
- _, ok := data[key]
- return key == "" || ok
- }
- const defaultGeneratorStateKey = "__pushsecret"
- func (r *Reconciler) resolveSecrets(ctx context.Context, ps *esapi.PushSecret) ([]v1.Secret, error) {
- var err error
- generatorState := statemanager.New(ctx, r.Client, r.Scheme, ps.Namespace, ps)
- defer func() {
- if err != nil {
- if err := generatorState.Rollback(); err != nil {
- r.Log.Error(err, "error rolling back generator state")
- }
- return
- }
- if err := generatorState.Commit(); err != nil {
- r.Log.Error(err, "error committing generator state")
- }
- }()
- switch {
- case ps.Spec.Selector.Secret != nil && ps.Spec.Selector.Secret.Name != "":
- secretName := types.NamespacedName{Name: ps.Spec.Selector.Secret.Name, Namespace: ps.Namespace}
- secret := &v1.Secret{}
- if err := r.Client.Get(ctx, secretName, secret); err != nil {
- return nil, err
- }
- generatorState.EnqueueFlagLatestStateForGC(defaultGeneratorStateKey)
- return []v1.Secret{*secret}, nil
- case ps.Spec.Selector.GeneratorRef != nil:
- secret, err := r.resolveSecretFromGenerator(ctx, ps.Namespace, ps.Spec.Selector.GeneratorRef, generatorState)
- if err != nil {
- return nil, fmt.Errorf("could not resolve secret from generator ref %v: %w", ps.Spec.Selector.GeneratorRef, err)
- }
- return []v1.Secret{*secret}, nil
- case ps.Spec.Selector.Secret != nil && ps.Spec.Selector.Secret.Selector != nil:
- labelSelector, err := metav1.LabelSelectorAsSelector(ps.Spec.Selector.Secret.Selector)
- if err != nil {
- return nil, err
- }
- var secretList v1.SecretList
- err = r.List(ctx, &secretList, &client.ListOptions{LabelSelector: labelSelector, Namespace: ps.Namespace})
- if err != nil {
- return nil, err
- }
- return secretList.Items, err
- }
- return nil, errors.New("no secret selector provided")
- }
- func (r *Reconciler) resolveSecretFromGenerator(ctx context.Context, namespace string, generatorRef *esv1.GeneratorRef, generatorState *statemanager.Manager) (*v1.Secret, error) {
- gen, genResource, err := resolvers.GeneratorRef(ctx, r.Client, r.Scheme, namespace, generatorRef)
- if err != nil {
- return nil, fmt.Errorf("unable to resolve generator: %w", err)
- }
- var prevState *genv1alpha1.GeneratorState
- if generatorState != nil {
- prevState, err = generatorState.GetLatestState(defaultGeneratorStateKey)
- if err != nil {
- return nil, fmt.Errorf("unable to get latest state: %w", err)
- }
- }
- secretMap, newState, err := gen.Generate(ctx, genResource, r.Client, namespace)
- if err != nil {
- return nil, fmt.Errorf("unable to generate: %w", err)
- }
- if prevState != nil && generatorState != nil {
- generatorState.EnqueueMoveStateToGC(defaultGeneratorStateKey)
- }
- if generatorState != nil {
- generatorState.EnqueueSetLatest(ctx, defaultGeneratorStateKey, namespace, genResource, gen, newState)
- }
- return &v1.Secret{
- ObjectMeta: metav1.ObjectMeta{
- Name: "___generated-secret",
- Namespace: namespace,
- },
- Data: secretMap,
- }, err
- }
- // GetSecretStores retrieves the SecretStore and ClusterSecretStore resources
- // referenced in the PushSecret. It supports both direct references by name
- // and label selectors to find multiple stores.
- func (r *Reconciler) GetSecretStores(ctx context.Context, ps esapi.PushSecret) (map[esapi.PushSecretStoreRef]esv1.GenericStore, error) {
- stores := make(map[esapi.PushSecretStoreRef]esv1.GenericStore)
- for _, refStore := range ps.Spec.SecretStoreRefs {
- if refStore.LabelSelector != nil {
- labelSelector, err := metav1.LabelSelectorAsSelector(refStore.LabelSelector)
- if err != nil {
- return nil, fmt.Errorf("could not convert labels: %w", err)
- }
- if refStore.Kind == esv1.ClusterSecretStoreKind {
- clusterSecretStoreList := esv1.ClusterSecretStoreList{}
- err = r.List(ctx, &clusterSecretStoreList, &client.ListOptions{LabelSelector: labelSelector})
- if err != nil {
- return nil, fmt.Errorf("could not list cluster Secret Stores: %w", err)
- }
- for k, v := range clusterSecretStoreList.Items {
- key := esapi.PushSecretStoreRef{
- Name: v.Name,
- Kind: esv1.ClusterSecretStoreKind,
- }
- stores[key] = &clusterSecretStoreList.Items[k]
- }
- } else {
- secretStoreList := esv1.SecretStoreList{}
- err = r.List(ctx, &secretStoreList, &client.ListOptions{LabelSelector: labelSelector, Namespace: ps.Namespace})
- if err != nil {
- return nil, fmt.Errorf("could not list Secret Stores: %w", err)
- }
- for k, v := range secretStoreList.Items {
- key := esapi.PushSecretStoreRef{
- Name: v.Name,
- Kind: esv1.SecretStoreKind,
- }
- stores[key] = &secretStoreList.Items[k]
- }
- }
- } else {
- store, err := r.getSecretStoreFromName(ctx, refStore, ps.Namespace)
- if err != nil {
- return nil, err
- }
- stores[refStore] = store
- }
- }
- return stores, nil
- }
- func (r *Reconciler) getSecretStoreFromName(ctx context.Context, refStore esapi.PushSecretStoreRef, ns string) (esv1.GenericStore, error) {
- if refStore.Name == "" {
- return nil, errors.New("refStore Name must be provided")
- }
- ref := types.NamespacedName{
- Name: refStore.Name,
- }
- if refStore.Kind == esv1.ClusterSecretStoreKind {
- var store esv1.ClusterSecretStore
- err := r.Get(ctx, ref, &store)
- if err != nil {
- return nil, fmt.Errorf(errGetClusterSecretStore, ref.Name, err)
- }
- return &store, nil
- }
- ref.Namespace = ns
- var store esv1.SecretStore
- err := r.Get(ctx, ref, &store)
- if err != nil {
- return nil, fmt.Errorf(errGetSecretStore, ref.Name, err)
- }
- return &store, nil
- }
- // NewPushSecretCondition creates a new PushSecret condition.
- func NewPushSecretCondition(condType esapi.PushSecretConditionType, status v1.ConditionStatus, reason, message string) *esapi.PushSecretStatusCondition {
- return &esapi.PushSecretStatusCondition{
- Type: condType,
- Status: status,
- LastTransitionTime: metav1.Now(),
- Reason: reason,
- Message: message,
- }
- }
- // SetPushSecretCondition updates the PushSecret to include the provided condition.
- func SetPushSecretCondition(ps *esapi.PushSecret, condition esapi.PushSecretStatusCondition) {
- currentCond := GetPushSecretCondition(ps.Status.Conditions, condition.Type)
- if currentCond != nil && currentCond.Status == condition.Status &&
- currentCond.Reason == condition.Reason && currentCond.Message == condition.Message {
- psmetrics.UpdatePushSecretCondition(ps, &condition, 1.0)
- return
- }
- // Do not update lastTransitionTime if the status of the condition doesn't change.
- if currentCond != nil && currentCond.Status == condition.Status {
- condition.LastTransitionTime = currentCond.LastTransitionTime
- }
- ps.Status.Conditions = append(FilterOutCondition(ps.Status.Conditions, condition.Type), condition)
- if currentCond != nil {
- psmetrics.UpdatePushSecretCondition(ps, currentCond, 0.0)
- }
- psmetrics.UpdatePushSecretCondition(ps, &condition, 1.0)
- }
- // FilterOutCondition returns an empty set of conditions with the provided type.
- func FilterOutCondition(conditions []esapi.PushSecretStatusCondition, condType esapi.PushSecretConditionType) []esapi.PushSecretStatusCondition {
- newConditions := make([]esapi.PushSecretStatusCondition, 0, len(conditions))
- for _, c := range conditions {
- if c.Type == condType {
- continue
- }
- newConditions = append(newConditions, c)
- }
- return newConditions
- }
- // GetPushSecretCondition returns the condition with the provided type.
- func GetPushSecretCondition(conditions []esapi.PushSecretStatusCondition, condType esapi.PushSecretConditionType) *esapi.PushSecretStatusCondition {
- for i := range conditions {
- c := conditions[i]
- if c.Type == condType {
- return &c
- }
- }
- return nil
- }
- func statusRef(ref esv1.PushSecretData) string {
- if ref.GetProperty() != "" {
- return ref.GetRemoteKey() + "/" + ref.GetProperty()
- }
- return ref.GetRemoteKey()
- }
- // removeUnmanagedStores iterates over all SecretStore references and evaluates the controllerClass property.
- // Returns a map containing only managed stores.
- func removeUnmanagedStores(ctx context.Context, namespace string, r *Reconciler, ss map[esapi.PushSecretStoreRef]esv1.GenericStore) (map[esapi.PushSecretStoreRef]esv1.GenericStore, error) {
- for ref := range ss {
- var store esv1.GenericStore
- switch ref.Kind {
- case esv1.SecretStoreKind:
- store = &esv1.SecretStore{}
- case esv1.ClusterSecretStoreKind:
- store = &esv1.ClusterSecretStore{}
- namespace = ""
- }
- err := r.Client.Get(ctx, types.NamespacedName{
- Name: ref.Name,
- Namespace: namespace,
- }, store)
- if err != nil {
- return ss, err
- }
- class := store.GetSpec().Controller
- if class != "" && class != r.ControllerClass {
- delete(ss, ref)
- }
- }
- return ss, nil
- }
- // matchKeys filters secret keys based on the provided match pattern.
- // If pattern is nil or empty, all keys are matched.
- func matchKeys(allKeys []string, match *esapi.PushSecretDataToMatch) ([]string, error) {
- if match == nil || match.RegExp == "" {
- return allKeys, nil
- }
- re, err := regexp.Compile(match.RegExp)
- if err != nil {
- return nil, fmt.Errorf("failed to compile regexp pattern %q: %w", match.RegExp, err)
- }
- matched := make([]string, 0)
- for _, key := range allKeys {
- if re.MatchString(key) {
- matched = append(matched, key)
- }
- }
- return matched, nil
- }
- // filterDataToForStore returns dataTo entries that target the given store.
- func filterDataToForStore(dataToList []esapi.PushSecretDataTo, storeName, storeKind string, storeLabels map[string]string) ([]esapi.PushSecretDataTo, error) {
- filtered := make([]esapi.PushSecretDataTo, 0, len(dataToList))
- for i, dataTo := range dataToList {
- matches, err := dataToMatchesStore(dataTo, storeName, storeKind, storeLabels)
- if err != nil {
- return nil, fmt.Errorf("dataTo[%d]: %w", i, err)
- }
- if matches {
- filtered = append(filtered, dataTo)
- }
- }
- return filtered, nil
- }
- // dataToMatchesStore reports whether a single dataTo entry targets the given store.
- func dataToMatchesStore(dataTo esapi.PushSecretDataTo, storeName, storeKind string, storeLabels map[string]string) (bool, error) {
- if dataTo.StoreRef == nil {
- return false, fmt.Errorf("storeRef is required")
- }
- refKind := dataTo.StoreRef.Kind
- if refKind == "" {
- refKind = esv1.SecretStoreKind
- }
- if dataTo.StoreRef.Name != "" {
- return dataTo.StoreRef.Name == storeName && refKind == storeKind, nil
- }
- if dataTo.StoreRef.LabelSelector == nil {
- return false, nil
- }
- selector, err := metav1.LabelSelectorAsSelector(dataTo.StoreRef.LabelSelector)
- if err != nil {
- return false, fmt.Errorf("invalid labelSelector: %w", err)
- }
- return refKind == storeKind && selector.Matches(labels.Set(storeLabels)), nil
- }
- // expandDataTo expands dataTo entries into individual PushSecretData entries.
- //
- // Two modes are supported per dataTo entry:
- //
- // Per-key mode (default, no remoteKey set): each matched key becomes a separate entry
- // pushed independently. This enables individual key transformation, per-key status
- // tracking, granular deletion, and compatibility with all providers.
- //
- // Bundle mode (remoteKey set): all matched keys are bundled into a single provider
- // secret at the given remoteKey path as a JSON object. A single PushSecretData entry
- // with SecretKey="" is produced, and the bundleOverrides map carries the filtered
- // key set so only matched keys appear in the pushed JSON blob.
- //
- // Returns the expanded entries, a bundleOverrides map (remoteKey -> filtered data),
- // and any error.
- func (r *Reconciler) expandDataTo(secret *v1.Secret, dataToList []esapi.PushSecretDataTo) ([]esapi.PushSecretData, map[string]map[string][]byte, error) {
- if len(dataToList) == 0 {
- return nil, nil, nil
- }
- allData := make([]esapi.PushSecretData, 0)
- bundleOverrides := make(map[string]map[string][]byte)
- overallRemoteKeys := make(map[string]string)
- for i, dataTo := range dataToList {
- entries, keyMap, filteredData, err := r.expandSingleDataTo(secret, dataTo)
- if err != nil {
- return nil, nil, fmt.Errorf("dataTo[%d]: %w", i, err)
- }
- if len(entries) == 0 {
- r.Log.Info("dataTo entry matched no keys", "index", i)
- continue
- }
- if err := registerRemoteKeys(overallRemoteKeys, keyMap, i); err != nil {
- return nil, nil, err
- }
- recordBundleOverrides(bundleOverrides, entries, filteredData)
- allData = append(allData, entries...)
- r.Log.Info("expanded dataTo entry", "index", i, "matchedKeys", len(entries), "created", len(keyMap))
- }
- return allData, bundleOverrides, nil
- }
- // registerRemoteKeys checks for duplicate remote keys across dataTo entries and
- // records new mappings. Returns an error if a duplicate is found.
- func registerRemoteKeys(seen, keyMap map[string]string, index int) error {
- for sourceKey, remoteKey := range keyMap {
- if existingSource, exists := seen[remoteKey]; exists {
- return fmt.Errorf("dataTo[%d]: duplicate remote key %q from source key %q (conflicts with %s)", index, remoteKey, sourceKey, existingSource)
- }
- seen[remoteKey] = fmt.Sprintf("dataTo[%d]:%s", index, sourceKey)
- }
- return nil
- }
- // recordBundleOverrides associates filtered data with bundle entries so only
- // matched keys appear in the pushed JSON blob.
- func recordBundleOverrides(overrides map[string]map[string][]byte, entries []esapi.PushSecretData, filteredData map[string][]byte) {
- if filteredData == nil {
- return
- }
- for _, entry := range entries {
- overrides[statusRef(entry)] = filteredData
- }
- }
- // expandSingleDataTo processes a single dataTo entry: converts keys, matches them
- // against the pattern, applies rewrites, validates remote keys, and builds the
- // resulting PushSecretData entries along with the source-to-remote key mapping.
- //
- // Bundle mode: when dataTo.RemoteKey is set, all matched keys are bundled into a
- // single PushSecretData entry with SecretKey="" targeting dataTo.RemoteKey. The
- // third return value carries the filtered (matched+converted) key data so that
- // only matched keys appear in the JSON blob pushed to the provider.
- //
- // Per-key mode: when dataTo.RemoteKey is empty, one PushSecretData entry is
- // produced per matched key. The third return value is nil.
- func (r *Reconciler) expandSingleDataTo(secret *v1.Secret, dataTo esapi.PushSecretDataTo) ([]esapi.PushSecretData, map[string]string, map[string][]byte, error) {
- if dataTo.RemoteKey != "" && len(dataTo.Rewrite) > 0 {
- return nil, nil, nil, fmt.Errorf("remoteKey and rewrite are mutually exclusive: rewrite is only supported in per-key mode (without remoteKey)")
- }
- convertedData, err := esutils.ReverseKeys(dataTo.ConversionStrategy, secret.Data)
- if err != nil {
- return nil, nil, nil, fmt.Errorf("conversion failed: %w", err)
- }
- // Map converted keys back to the original K8s secret keys. The resulting
- // PushSecretData entries store the original key so that
- // resolveSourceKeyConflicts can compare dataTo entries against explicit
- // data entries in the same key space. ConversionStrategy is set to None on
- // expanded entries because the conversion was already applied during
- // matching and rewriting; handlePushSecretDataForStore will look up the
- // original key directly in the unconverted secret data.
- convertedToOriginal := make(map[string]string, len(secret.Data))
- for origKey := range secret.Data {
- convKey := esutils.ReverseKey(dataTo.ConversionStrategy, origKey)
- convertedToOriginal[convKey] = origKey
- }
- allKeys := make([]string, 0, len(convertedData))
- for key := range convertedData {
- allKeys = append(allKeys, key)
- }
- slices.Sort(allKeys)
- matchedKeys, err := matchKeys(allKeys, dataTo.Match)
- if err != nil {
- return nil, nil, nil, fmt.Errorf("match failed: %w", err)
- }
- if len(matchedKeys) == 0 {
- return nil, nil, nil, nil
- }
- matchedData := make(map[string][]byte, len(matchedKeys))
- for _, key := range matchedKeys {
- matchedData[key] = convertedData[key]
- }
- if dataTo.RemoteKey != "" {
- keyMap := map[string]string{bundleSourceKey: dataTo.RemoteKey}
- entry := esapi.PushSecretData{
- Match: esapi.PushSecretMatch{
- SecretKey: "",
- RemoteRef: esapi.PushSecretRemoteRef{
- RemoteKey: dataTo.RemoteKey,
- },
- },
- Metadata: dataTo.Metadata,
- ConversionStrategy: esapi.PushSecretConversionNone,
- }
- return []esapi.PushSecretData{entry}, keyMap, matchedData, nil
- }
- keyMap, err := rewriteWithKeyMapping(dataTo.Rewrite, matchedData)
- if err != nil {
- return nil, nil, nil, fmt.Errorf("rewrite failed: %w", err)
- }
- for sourceKey, remoteKey := range keyMap {
- if remoteKey == "" {
- return nil, nil, nil, fmt.Errorf("empty remote key produced for source key %q", sourceKey)
- }
- }
- sortedKeys := slices.Sorted(maps.Keys(keyMap))
- entries := make([]esapi.PushSecretData, 0, len(keyMap))
- for _, convertedKey := range sortedKeys {
- entries = append(entries, esapi.PushSecretData{
- Match: esapi.PushSecretMatch{
- SecretKey: convertedToOriginal[convertedKey],
- RemoteRef: esapi.PushSecretRemoteRef{
- RemoteKey: keyMap[convertedKey],
- },
- },
- Metadata: dataTo.Metadata,
- ConversionStrategy: esapi.PushSecretConversionNone,
- })
- }
- return entries, keyMap, nil, nil
- }
- // validateDataToStoreRefs checks that each dataTo entry has a valid storeRef.
- func validateDataToStoreRefs(dataToList []esapi.PushSecretDataTo, storeRefs []esapi.PushSecretStoreRef) error {
- for i, d := range dataToList {
- if d.StoreRef == nil {
- return fmt.Errorf("dataTo[%d]: storeRef is required", i)
- }
- if d.StoreRef.Name == "" && d.StoreRef.LabelSelector == nil {
- return fmt.Errorf("dataTo[%d]: storeRef must have name or labelSelector", i)
- }
- if d.StoreRef.Name != "" && !storeRefExistsInList(d.StoreRef, storeRefs) {
- return fmt.Errorf("dataTo[%d]: storeRef %q not found in secretStoreRefs", i, d.StoreRef.Name)
- }
- }
- return nil
- }
- // storeRefExistsInList checks if a named ref matches any named entry in storeRefs.
- func storeRefExistsInList(ref *esapi.PushSecretStoreRef, storeRefs []esapi.PushSecretStoreRef) bool {
- refKind := ref.Kind
- if refKind == "" {
- refKind = esv1.SecretStoreKind
- }
- for _, sr := range storeRefs {
- if sr.Name == "" {
- continue
- }
- srKind := sr.Kind
- if srKind == "" {
- srKind = esv1.SecretStoreKind
- }
- if srKind == refKind && sr.Name == ref.Name {
- return true
- }
- }
- return false
- }
- // validateDataToMatchesResolvedStores checks that every dataTo entry with a
- // labelSelector actually matches at least one resolved store. Without this,
- // a misconfigured labelSelector silently becomes a no-op.
- func validateDataToMatchesResolvedStores(dataToList []esapi.PushSecretDataTo, stores map[esapi.PushSecretStoreRef]esv1.GenericStore) error {
- for i, dataTo := range dataToList {
- if dataTo.StoreRef == nil || dataTo.StoreRef.LabelSelector == nil {
- continue
- }
- if dataTo.StoreRef.Name != "" {
- continue
- }
- selector, err := metav1.LabelSelectorAsSelector(dataTo.StoreRef.LabelSelector)
- if err != nil {
- return fmt.Errorf("dataTo[%d]: invalid labelSelector: %w", i, err)
- }
- refKind := dataTo.StoreRef.Kind
- if refKind == "" {
- refKind = esv1.SecretStoreKind
- }
- if !anyStoreMatchesSelector(refKind, selector, stores) {
- return fmt.Errorf("dataTo[%d]: labelSelector does not match any store in secretStoreRefs", i)
- }
- }
- return nil
- }
- // anyStoreMatchesSelector returns true if at least one resolved store matches
- // the given kind and label selector.
- func anyStoreMatchesSelector(kind string, selector labels.Selector, stores map[esapi.PushSecretStoreRef]esv1.GenericStore) bool {
- for ref, store := range stores {
- if ref.Kind == kind && selector.Matches(labels.Set(store.GetLabels())) {
- return true
- }
- }
- return false
- }
- // rewriteWithKeyMapping applies rewrites and returns originalKey -> rewrittenKey mapping.
- func rewriteWithKeyMapping(rewrites []esapi.PushSecretRewrite, data map[string][]byte) (map[string]string, error) {
- keyMap := make(map[string]string, len(data))
- for k := range data {
- keyMap[k] = k
- }
- for i, op := range rewrites {
- applyFn, err := compileRewrite(op)
- if err != nil {
- return nil, fmt.Errorf("rewrite[%d]: %w", i, err)
- }
- newKeyMap := make(map[string]string, len(keyMap))
- for origKey, currentKey := range keyMap {
- newKey, err := applyFn(currentKey)
- if err != nil {
- return nil, fmt.Errorf("rewrite[%d] on key %q: %w", i, currentKey, err)
- }
- newKeyMap[origKey] = newKey
- }
- keyMap = newKeyMap
- }
- return keyMap, nil
- }
- // compileRewrite pre-compiles a rewrite operation (regexp or template) and
- // returns a function that applies it to a key. This avoids re-compiling the
- // same regexp or re-parsing the same template for every key.
- func compileRewrite(op esapi.PushSecretRewrite) (func(string) (string, error), error) {
- switch {
- case op.Regexp != nil:
- re, err := regexp.Compile(op.Regexp.Source)
- if err != nil {
- return nil, fmt.Errorf("invalid regexp %q: %w", op.Regexp.Source, err)
- }
- target := op.Regexp.Target
- return func(key string) (string, error) {
- return re.ReplaceAllString(key, target), nil
- }, nil
- case op.Transform != nil:
- tmpl, err := template.New("t").Funcs(estemplate.FuncMap()).Parse(op.Transform.Template)
- if err != nil {
- return nil, fmt.Errorf("invalid template: %w", err)
- }
- return func(key string) (string, error) {
- var buf bytes.Buffer
- if err := tmpl.Execute(&buf, map[string]string{"value": key}); err != nil {
- return "", fmt.Errorf("template exec: %w", err)
- }
- return buf.String(), nil
- }, nil
- default:
- return func(key string) (string, error) { return key, nil }, nil
- }
- }
- // resolveSourceKeyConflicts merges dataTo and explicit data entries.
- // When both reference the same source secret key, explicit data wins.
- // Comparison is done using the original (raw) K8s secret key. DataTo entries
- // already store the original key; explicit data entries may store a converted
- // key when ConversionStrategy is set, so we normalize them via the secret.
- func resolveSourceKeyConflicts(dataToEntries, explicitData []esapi.PushSecretData, secret *v1.Secret) []esapi.PushSecretData {
- explicitOriginalKeys := make(map[string]struct{}, len(explicitData))
- for _, data := range explicitData {
- origKey := resolveOriginalKey(data, secret)
- explicitOriginalKeys[origKey] = struct{}{}
- }
- result := make([]esapi.PushSecretData, 0, len(dataToEntries)+len(explicitData))
- for _, data := range dataToEntries {
- if _, exists := explicitOriginalKeys[data.GetSecretKey()]; !exists {
- result = append(result, data)
- }
- }
- return append(result, explicitData...)
- }
- // resolveOriginalKey returns the raw K8s secret key for a PushSecretData entry.
- // If the entry uses a ConversionStrategy, SecretKey is the converted (decoded)
- // form, so we find the original key by converting each raw key and matching.
- // If no conversion is active, SecretKey is already the original key.
- func resolveOriginalKey(data esapi.PushSecretData, secret *v1.Secret) string {
- key := data.GetSecretKey()
- if data.ConversionStrategy == "" || data.ConversionStrategy == esapi.PushSecretConversionNone {
- return key
- }
- for origKey := range secret.Data {
- if esutils.ReverseKey(data.ConversionStrategy, origKey) == key {
- return origKey
- }
- }
- return key
- }
- // validateRemoteKeyUniqueness ensures no two entries push to the same remote location.
- // The remote location is defined by (remoteKey, property) tuple.
- func validateRemoteKeyUniqueness(entries []esapi.PushSecretData) error {
- type remoteLocation struct {
- remoteKey string
- property string
- }
- seen := make(map[remoteLocation]string) // location -> source key (for error message)
- for _, data := range entries {
- loc := remoteLocation{
- remoteKey: data.GetRemoteKey(),
- property: data.GetProperty(),
- }
- sourceKey := data.GetSecretKey()
- if existingSource, exists := seen[loc]; exists {
- if loc.property != "" {
- return fmt.Errorf(
- "duplicate remote key %q with property %q: source keys %q and %q both map to the same destination",
- loc.remoteKey, loc.property, existingSource, sourceKey)
- }
- return fmt.Errorf(
- "duplicate remote key %q: source keys %q and %q both map to the same destination",
- loc.remoteKey, existingSource, sourceKey)
- }
- seen[loc] = sourceKey
- }
- return nil
- }
- // mergeDataEntries combines dataTo and explicit data entries.
- // It resolves source key conflicts (explicit wins) and validates no duplicate remote destinations.
- func mergeDataEntries(dataToEntries, explicitData []esapi.PushSecretData, secret *v1.Secret) ([]esapi.PushSecretData, error) {
- merged := resolveSourceKeyConflicts(dataToEntries, explicitData, secret)
- if err := validateRemoteKeyUniqueness(merged); err != nil {
- return nil, err
- }
- return merged, nil
- }
|