| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270 |
- /*
- 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/client"
- "sigs.k8s.io/controller-runtime/pkg/controller"
- "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
- 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{}).
- Complete(r)
- }
- // 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
- }
|