| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310 |
- /*
- Copyright © 2025 ESO Maintainer Team
- 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 externalsecret implements the controller for managing ExternalSecret resources
- package externalsecret
- import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "maps"
- "slices"
- "strings"
- "time"
- "github.com/go-logr/logr"
- "github.com/prometheus/client_golang/prometheus"
- v1 "k8s.io/api/core/v1"
- "k8s.io/apimachinery/pkg/api/equality"
- apierrors "k8s.io/apimachinery/pkg/api/errors"
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
- "k8s.io/apimachinery/pkg/fields"
- "k8s.io/apimachinery/pkg/labels"
- "k8s.io/apimachinery/pkg/runtime"
- "k8s.io/apimachinery/pkg/runtime/schema"
- "k8s.io/apimachinery/pkg/types"
- "k8s.io/client-go/rest"
- "k8s.io/client-go/tools/record"
- "k8s.io/utils/ptr"
- 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/handler"
- "sigs.k8s.io/controller-runtime/pkg/predicate"
- "sigs.k8s.io/controller-runtime/pkg/reconcile"
- esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
- // Metrics.
- "github.com/external-secrets/external-secrets/pkg/controllers/externalsecret/esmetrics"
- ctrlmetrics "github.com/external-secrets/external-secrets/pkg/controllers/metrics"
- 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"
- // Loading registered generators.
- _ "github.com/external-secrets/external-secrets/pkg/register"
- )
- const (
- fieldOwnerTemplate = "externalsecrets.external-secrets.io/%v"
- fieldOwnerTemplateSha = "externalsecrets.external-secrets.io/sha3/%x"
- // ExternalSecretFinalizer is the finalizer for ExternalSecret resources.
- ExternalSecretFinalizer = "externalsecrets.external-secrets.io/externalsecret-cleanup"
- // condition messages for "SecretSynced" reason.
- msgSynced = "secret synced"
- msgSyncedRetain = "secret retained due to DeletionPolicy=Retain"
- // condition messages for "SecretDeleted" reason.
- msgDeleted = "secret deleted due to DeletionPolicy=Delete"
- // condition messages for "SecretMissing" reason.
- msgMissing = "secret will not be created due to CreationPolicy=Merge"
- // condition messages for "SecretSyncedError" reason.
- msgErrorGetSecretData = "could not get secret data from provider"
- msgErrorDeleteSecret = "could not delete secret"
- msgErrorDeleteOrphaned = "could not delete orphaned secrets"
- msgErrorUpdateSecret = "could not update secret"
- msgErrorUpdateImmutable = "could not update secret, target is immutable"
- msgErrorBecomeOwner = "failed to take ownership of target secret"
- msgErrorIsOwned = "target is owned by another ExternalSecret"
- // log messages.
- logErrorGetES = "unable to get ExternalSecret"
- logErrorUpdateESStatus = "unable to update ExternalSecret status"
- logErrorGetSecret = "unable to get Secret"
- logErrorPatchSecret = "unable to patch Secret"
- logErrorSecretCacheNotSynced = "controller caches for Secret are not in sync"
- logErrorUnmanagedStore = "unable to determine if store is managed"
- // error formats.
- errConvert = "error applying conversion strategy %s to keys: %w"
- errRewrite = "error applying rewrite to keys: %w"
- errDecode = "error applying decoding strategy %s to data: %w"
- errGenerate = "error using generator: %w"
- errInvalidKeys = "invalid secret keys (TIP: use rewrite or conversionStrategy to change keys): %w"
- errFetchTplFrom = "error fetching templateFrom data: %w"
- errApplyTemplate = "could not apply template: %w"
- errExecTpl = "could not execute template: %w"
- errMutate = "unable to mutate secret %s: %w"
- errUpdate = "unable to update secret %s: %w"
- errUpdateNotFound = "unable to update secret %s: not found"
- errDeleteCreatePolicy = "unable to delete secret %s: creationPolicy=%s is not Owner"
- errSecretCachesNotSynced = "controller caches for secret %s are not in sync"
- // event messages.
- eventCreated = "secret created"
- eventUpdated = "secret updated"
- eventDeleted = "secret deleted due to DeletionPolicy=Delete"
- eventDeletedOrphaned = "secret deleted because it was orphaned"
- eventMissingProviderSecret = "secret does not exist at provider using spec.dataFrom[%d]"
- eventMissingProviderSecretKey = "secret does not exist at provider using spec.dataFrom[%d] (key=%s)"
- )
- // these errors are explicitly defined so we can detect them with `errors.Is()`.
- var (
- ErrSecretImmutable = fmt.Errorf("secret is immutable")
- ErrSecretIsOwned = fmt.Errorf("secret is owned by another ExternalSecret")
- ErrSecretSetCtrlRef = fmt.Errorf("could not set controller reference on secret")
- ErrSecretRemoveCtrlRef = fmt.Errorf("could not remove controller reference on secret")
- )
- const (
- indexESTargetSecretNameField = ".metadata.targetSecretName"
- indexESTargetResourceField = ".spec.target.resource"
- )
- // Reconciler reconciles a ExternalSecret object.
- type Reconciler struct {
- client.Client
- SecretClient client.Client
- Log logr.Logger
- Scheme *runtime.Scheme
- RestConfig *rest.Config
- ControllerClass string
- RequeueInterval time.Duration
- ClusterSecretStoreEnabled bool
- EnableFloodGate bool
- EnableGeneratorState bool
- AllowGenericTargets bool
- recorder record.EventRecorder
- // informerManager manages dynamic informers for generic targets
- informerManager InformerManager
- }
- // Reconcile implements the main reconciliation loop
- // for watched objects (ExternalSecret, ClusterSecretStore and SecretStore),
- // and updates/creates a Kubernetes secret based on them.
- func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) {
- log := r.Log.WithValues("ExternalSecret", req.NamespacedName)
- resourceLabels := ctrlmetrics.RefineNonConditionMetricLabels(map[string]string{"name": req.Name, "namespace": req.Namespace})
- start := time.Now()
- syncCallsError := esmetrics.GetCounterVec(esmetrics.SyncCallsErrorKey)
- // use closures to dynamically update resourceLabels
- defer func() {
- esmetrics.GetGaugeVec(esmetrics.ExternalSecretReconcileDurationKey).With(resourceLabels).Set(float64(time.Since(start)))
- esmetrics.GetCounterVec(esmetrics.SyncCallsKey).With(resourceLabels).Inc()
- }()
- externalSecret := &esv1.ExternalSecret{}
- err = r.Get(ctx, req.NamespacedName, externalSecret)
- if err != nil {
- if apierrors.IsNotFound(err) {
- // NOTE: this does not actually set the condition on the ExternalSecret, because it does not exist
- // this is a hack to disable metrics for deleted ExternalSecrets, see:
- // https://github.com/external-secrets/external-secrets/pull/612
- conditionSynced := NewExternalSecretCondition(esv1.ExternalSecretDeleted, v1.ConditionFalse, esv1.ConditionReasonSecretDeleted, "Secret was deleted")
- SetExternalSecretCondition(&esv1.ExternalSecret{
- ObjectMeta: metav1.ObjectMeta{
- Name: req.Name,
- Namespace: req.Namespace,
- },
- }, *conditionSynced)
- return ctrl.Result{}, nil
- }
- log.Error(err, logErrorGetES)
- syncCallsError.With(resourceLabels).Inc()
- return ctrl.Result{}, err
- }
- // Handle deletion with finalizer
- if !externalSecret.GetDeletionTimestamp().IsZero() {
- // Always attempt cleanup to handle edge case where finalizer might be removed externally
- if err := r.cleanupManagedSecrets(ctx, log, externalSecret); err != nil {
- log.Error(err, "failed to cleanup managed secrets")
- return ctrl.Result{}, err
- }
- // Release informer for generic targets
- if isGenericTarget(externalSecret) && r.informerManager != nil {
- gvk := getTargetGVK(externalSecret)
- esName := types.NamespacedName{Name: externalSecret.Name, Namespace: externalSecret.Namespace}
- if err := r.informerManager.ReleaseInformer(ctx, gvk, esName); err != nil {
- log.Error(err, "failed to release informer for generic target",
- "group", gvk.Group,
- "version", gvk.Version,
- "kind", gvk.Kind)
- }
- }
- // Remove finalizer if it exists
- // Use Patch instead of Update to avoid claiming ownership of spec fields like refreshInterval
- patch := client.MergeFrom(externalSecret.DeepCopy())
- if updated := controllerutil.RemoveFinalizer(externalSecret, ExternalSecretFinalizer); updated {
- if err := r.Patch(ctx, externalSecret, patch); err != nil {
- return ctrl.Result{}, err
- }
- }
- return ctrl.Result{}, nil
- }
- // Add finalizer if it doesn't exist
- // Use Patch instead of Update to avoid claiming ownership of spec fields like refreshInterval
- patch := client.MergeFrom(externalSecret.DeepCopy())
- if updated := controllerutil.AddFinalizer(externalSecret, ExternalSecretFinalizer); updated {
- if err := r.Patch(ctx, externalSecret, patch); err != nil {
- return ctrl.Result{}, err
- }
- }
- // if extended metrics is enabled, refine the time series vector
- resourceLabels = ctrlmetrics.RefineLabels(resourceLabels, externalSecret.Labels)
- // skip this ExternalSecret if it uses a ClusterSecretStore and the feature is disabled
- if shouldSkipClusterSecretStore(r, externalSecret) {
- log.V(1).Info("skipping ExternalSecret, ClusterSecretStore feature is disabled")
- return ctrl.Result{}, nil
- }
- // skip this ExternalSecret if it uses any SecretStore not managed by this controller
- skip, err := shouldSkipUnmanagedStore(ctx, req.Namespace, r, externalSecret)
- if err != nil {
- log.Error(err, logErrorUnmanagedStore)
- syncCallsError.With(resourceLabels).Inc()
- return ctrl.Result{}, err
- }
- if skip {
- log.V(1).Info("skipping ExternalSecret, uses unmanaged SecretStore")
- return ctrl.Result{}, nil
- }
- // if this is a generic target, use a different reconciliation path
- if isGenericTarget(externalSecret) {
- // update the status of the ExternalSecret when this function returns, if needed
- currentStatus := *externalSecret.Status.DeepCopy()
- defer func() {
- if equality.Semantic.DeepEqual(currentStatus, externalSecret.Status) {
- return
- }
- updateErr := r.Status().Update(ctx, externalSecret)
- if updateErr != nil && !apierrors.IsConflict(updateErr) {
- log.Error(updateErr, logErrorUpdateESStatus)
- }
- }()
- // validate generic target configuration early
- if err := r.validateGenericTarget(log, externalSecret); err != nil {
- r.markAsFailed("invalid generic target", err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonSecretSyncedError)
- return ctrl.Result{}, nil // don't requeue as this is a configuration error that is not recoverable
- }
- return r.reconcileGenericTarget(ctx, externalSecret, log, start, resourceLabels, syncCallsError)
- }
- // the target secret name defaults to the ExternalSecret name, if not explicitly set
- secretName := externalSecret.Spec.Target.Name
- if secretName == "" {
- secretName = externalSecret.Name
- }
- // fetch the existing secret (from the partial cache)
- // - please note that the ~partial cache~ is different from the ~full cache~
- // so there can be race conditions between the two caches
- // - the WatchesMetadata(v1.Secret{}) in SetupWithManager() is using the partial cache
- // so we might receive a reconcile request before the full cache is updated
- // - furthermore, when `--enable-managed-secrets-caching` is true, the full cache
- // will ONLY include secrets with the "managed" label, so we cant use the full cache
- // to reliably determine if a secret exists or not
- secretPartial := &metav1.PartialObjectMetadata{}
- secretPartial.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("Secret"))
- err = r.Get(ctx, client.ObjectKey{Name: secretName, Namespace: externalSecret.Namespace}, secretPartial)
- if err != nil && !apierrors.IsNotFound(err) {
- log.Error(err, logErrorGetSecret, "secretName", secretName, "secretNamespace", externalSecret.Namespace)
- syncCallsError.With(resourceLabels).Inc()
- return ctrl.Result{}, err
- }
- // if the secret exists but does not have the "managed" label, add the label
- // using a PATCH so it is visible in the cache, then requeue immediately
- if secretPartial.UID != "" && secretPartial.Labels[esv1.LabelManaged] != esv1.LabelManagedValue {
- fqdn := fqdnFor(externalSecret.Name)
- patch := client.MergeFrom(secretPartial.DeepCopy())
- if secretPartial.Labels == nil {
- secretPartial.Labels = make(map[string]string)
- }
- secretPartial.Labels[esv1.LabelManaged] = esv1.LabelManagedValue
- err = r.Patch(ctx, secretPartial, patch, client.FieldOwner(fqdn))
- if err != nil {
- log.Error(err, logErrorPatchSecret, "secretName", secretName, "secretNamespace", externalSecret.Namespace)
- syncCallsError.With(resourceLabels).Inc()
- return ctrl.Result{}, err
- }
- return ctrl.Result{Requeue: true}, nil
- }
- // fetch existing secret (from the full cache)
- // NOTE: we are using the `r.SecretClient` which we only use for managed secrets.
- // when `enableManagedSecretsCache` is true, this is a cached client that only sees our managed secrets,
- // otherwise it will be the normal controller-runtime client which may be cached or make direct API calls,
- // depending on if `enabledSecretCache` is true or false.
- existingSecret := &v1.Secret{}
- err = r.SecretClient.Get(ctx, client.ObjectKey{Name: secretName, Namespace: externalSecret.Namespace}, existingSecret)
- if err != nil && !apierrors.IsNotFound(err) {
- log.Error(err, logErrorGetSecret, "secretName", secretName, "secretNamespace", externalSecret.Namespace)
- syncCallsError.With(resourceLabels).Inc()
- return ctrl.Result{}, err
- }
- // ensure the full cache is up-to-date
- // NOTE: this prevents race conditions between the partial and full cache.
- // we return an error so we get an exponential backoff if we end up looping,
- // for example, during high cluster load and frequent updates to the target secret by other controllers.
- if secretPartial.UID != existingSecret.UID || secretPartial.ResourceVersion != existingSecret.ResourceVersion {
- err = fmt.Errorf(errSecretCachesNotSynced, secretName)
- log.Error(err, logErrorSecretCacheNotSynced, "secretName", secretName, "secretNamespace", externalSecret.Namespace)
- syncCallsError.With(resourceLabels).Inc()
- return ctrl.Result{}, err
- }
- // refresh will be skipped if ALL the following conditions are met:
- // 1. refresh interval is not 0
- // 2. resource generation of the ExternalSecret has not changed
- // 3. the last refresh time of the ExternalSecret is within the refresh interval
- // 4. the target secret is valid:
- // - it exists
- // - it has the correct "managed" label
- // - it has the correct "data-hash" annotation
- if !shouldRefresh(externalSecret) && isSecretValid(existingSecret, externalSecret) {
- log.V(1).Info("skipping refresh")
- return r.getRequeueResult(externalSecret), nil
- }
- // update status of the ExternalSecret when this function returns, if needed.
- // NOTE: we use the ability of deferred functions to update named return values `result` and `err`
- // NOTE: we dereference the DeepCopy of the status field because status fields are NOT pointers,
- // so otherwise the `equality.Semantic.DeepEqual` will always return false.
- currentStatus := *externalSecret.Status.DeepCopy()
- defer func() {
- // if the status has not changed, we don't need to update it
- if equality.Semantic.DeepEqual(currentStatus, externalSecret.Status) {
- return
- }
- // update the status of the ExternalSecret, storing any error in a new variable
- // if there was no new error, we don't need to change the `result` or `err` values
- updateErr := r.Status().Update(ctx, externalSecret)
- if updateErr == nil {
- return
- }
- // if we got an update conflict, we should requeue immediately
- if apierrors.IsConflict(updateErr) {
- log.V(1).Info("conflict while updating status, will requeue")
- // we only explicitly request a requeue if the main function did not return an `err`.
- // otherwise, we get an annoying log saying that results are ignored when there is an error,
- // as errors are always retried.
- if err == nil {
- result = ctrl.Result{Requeue: true}
- }
- return
- }
- // for other errors, log and update the `err` variable if there is no error already
- // so the reconciler will requeue the request
- log.Error(updateErr, logErrorUpdateESStatus)
- if err == nil {
- err = updateErr
- }
- }()
- // retrieve the provider secret data.
- dataMap, err := r.GetProviderSecretData(ctx, externalSecret)
- if err != nil {
- r.markAsFailed(msgErrorGetSecretData, err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonSecretSyncedError)
- return ctrl.Result{}, err
- }
- // if no data was found we can delete the secret if needed.
- if len(dataMap) == 0 {
- switch externalSecret.Spec.Target.DeletionPolicy {
- // delete secret and return early.
- case esv1.DeletionPolicyDelete:
- // safeguard that we only can delete secrets we own.
- // this is also implemented in the es validation webhook.
- // NOTE: this error cant be fixed by retrying so we don't return an error (which would requeue immediately)
- creationPolicy := externalSecret.Spec.Target.CreationPolicy
- if creationPolicy != esv1.CreatePolicyOwner {
- err = fmt.Errorf(errDeleteCreatePolicy, secretName, creationPolicy)
- r.markAsFailed(msgErrorDeleteSecret, err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonSecretSyncedError)
- return ctrl.Result{}, nil
- }
- // delete the secret, if it exists
- if existingSecret.UID != "" {
- err = r.Delete(ctx, existingSecret)
- if err != nil && !apierrors.IsNotFound(err) {
- r.markAsFailed(msgErrorDeleteSecret, err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonSecretSyncedError)
- return ctrl.Result{}, err
- }
- r.recorder.Event(externalSecret, v1.EventTypeNormal, esv1.ReasonDeleted, eventDeleted)
- }
- r.markAsDone(externalSecret, start, log, esv1.ConditionReasonSecretDeleted, msgDeleted)
- return r.getRequeueResult(externalSecret), nil
- // In case provider secrets don't exist the kubernetes secret will be kept as-is.
- case esv1.DeletionPolicyRetain:
- r.markAsDone(externalSecret, start, log, esv1.ConditionReasonSecretSynced, msgSyncedRetain)
- return r.getRequeueResult(externalSecret), nil
- // noop, handled below
- case esv1.DeletionPolicyMerge:
- }
- }
- // mutationFunc is a function which can be applied to a secret to make it match the desired state.
- mutationFunc := func(secret *v1.Secret) error {
- // get information about the current owner of the secret
- // - we ignore the API version as it can change over time
- // - we ignore the UID for consistency with the SetControllerReference function
- currentOwner := metav1.GetControllerOf(secret)
- ownerIsESKind := false
- ownerIsCurrentES := false
- if currentOwner != nil {
- currentOwnerGK := schema.FromAPIVersionAndKind(currentOwner.APIVersion, currentOwner.Kind).GroupKind()
- ownerIsESKind = currentOwnerGK.String() == esv1.ExtSecretGroupKind
- ownerIsCurrentES = ownerIsESKind && currentOwner.Name == externalSecret.Name
- }
- // if another ExternalSecret is the owner, we should return an error
- // otherwise the controller will fight with itself to update the secret.
- // note, this does not prevent other controllers from owning the secret.
- if ownerIsESKind && !ownerIsCurrentES {
- return fmt.Errorf("%w: %s", ErrSecretIsOwned, currentOwner.Name)
- }
- // if the CreationPolicy is Owner, we should set ourselves as the owner of the secret
- if externalSecret.Spec.Target.CreationPolicy == esv1.CreatePolicyOwner {
- err = controllerutil.SetControllerReference(externalSecret, secret, r.Scheme)
- if err != nil {
- return fmt.Errorf("%w: %w", ErrSecretSetCtrlRef, err)
- }
- }
- // if the creation policy is not Owner, we should remove ourselves as the owner
- // this could happen if the creation policy was changed after the secret was created
- if externalSecret.Spec.Target.CreationPolicy != esv1.CreatePolicyOwner && ownerIsCurrentES {
- err = controllerutil.RemoveControllerReference(externalSecret, secret, r.Scheme)
- if err != nil {
- return fmt.Errorf("%w: %w", ErrSecretRemoveCtrlRef, err)
- }
- }
- // initialize maps within the secret so it's safe to set values
- if secret.Annotations == nil {
- secret.Annotations = make(map[string]string)
- }
- if secret.Labels == nil {
- secret.Labels = make(map[string]string)
- }
- if secret.Data == nil {
- secret.Data = make(map[string][]byte)
- }
- // set the immutable flag on the secret if requested by the ExternalSecret
- if externalSecret.Spec.Target.Immutable {
- secret.Immutable = ptr.To(true)
- }
- // only apply the template if the secret is mutable or if the secret is new (has no UID)
- // otherwise we would mutate an object that is immutable and already exists
- objectDoesNotExistOrCanBeMutated := secret.GetUID() == "" || !externalSecret.Spec.Target.Immutable
- if objectDoesNotExistOrCanBeMutated {
- // get the list of keys that are managed by this ExternalSecret
- keys, err := getManagedDataKeys(secret, externalSecret.Name)
- if err != nil {
- return err
- }
- // remove any data keys that are managed by this ExternalSecret, so we can re-add them
- // this ensures keys added by templates are not left behind when they are removed from the template
- for _, key := range keys {
- delete(secret.Data, key)
- }
- // WARNING: this will remove any labels or annotations managed by this ExternalSecret
- // so any updates to labels and annotations should be done AFTER this point
- err = r.ApplyTemplate(ctx, externalSecret, secret, dataMap)
- if err != nil {
- return fmt.Errorf(errApplyTemplate, err)
- }
- }
- // we also use a label to keep track of the owner of the secret
- // this lets us remove secrets that are no longer needed if the target secret name changes
- if externalSecret.Spec.Target.CreationPolicy == esv1.CreatePolicyOwner {
- lblValue := esutils.ObjectHash(fmt.Sprintf("%v/%v", externalSecret.Namespace, externalSecret.Name))
- secret.Labels[esv1.LabelOwner] = lblValue
- } else {
- // the label should not be set if the creation policy is not Owner
- delete(secret.Labels, esv1.LabelOwner)
- }
- secret.Labels[esv1.LabelManaged] = esv1.LabelManagedValue
- secret.Annotations[esv1.AnnotationDataHash] = esutils.ObjectHash(secret.Data)
- return nil
- }
- switch externalSecret.Spec.Target.CreationPolicy {
- case esv1.CreatePolicyNone:
- log.V(1).Info("secret creation skipped due to CreationPolicy=None")
- err = nil
- case esv1.CreatePolicyMerge:
- // update the secret, if it exists
- if existingSecret.UID != "" {
- err = r.updateSecret(ctx, existingSecret, mutationFunc, externalSecret, secretName)
- } else {
- // if the secret does not exist, we wait until the next refresh interval
- // rather than returning an error which would requeue immediately
- r.markAsDone(externalSecret, start, log, esv1.ConditionReasonSecretMissing, msgMissing)
- return r.getRequeueResult(externalSecret), nil
- }
- case esv1.CreatePolicyOrphan:
- // create the secret, if it does not exist
- if existingSecret.UID == "" {
- err = r.createSecret(ctx, mutationFunc, externalSecret, secretName)
- } else {
- // if the secret exists, we should update it
- err = r.updateSecret(ctx, existingSecret, mutationFunc, externalSecret, secretName)
- }
- case esv1.CreatePolicyOwner:
- // we may have orphaned secrets to clean up,
- // for example, if the target secret name was changed
- err = r.deleteOrphanedSecrets(ctx, externalSecret, secretName)
- if err != nil {
- r.markAsFailed(msgErrorDeleteOrphaned, err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonSecretSyncedError)
- return ctrl.Result{}, err
- }
- // create the secret, if it does not exist
- if existingSecret.UID == "" {
- err = r.createSecret(ctx, mutationFunc, externalSecret, secretName)
- } else {
- // if the secret exists, we should update it
- err = r.updateSecret(ctx, existingSecret, mutationFunc, externalSecret, secretName)
- }
- }
- if err != nil {
- // if we got an update conflict, we should requeue immediately
- if apierrors.IsConflict(err) {
- log.V(1).Info("conflict while updating secret, will requeue")
- return ctrl.Result{Requeue: true}, nil
- }
- // detect errors indicating that we failed to set ourselves as the owner of the secret
- // NOTE: this error cant be fixed by retrying so we don't return an error (which would requeue immediately)
- if errors.Is(err, ErrSecretSetCtrlRef) {
- r.markAsFailed(msgErrorBecomeOwner, err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonSecretSyncedError)
- return ctrl.Result{}, nil
- }
- // detect errors indicating that the secret has another ExternalSecret as owner
- // NOTE: this error cant be fixed by retrying so we don't return an error (which would requeue immediately)
- if errors.Is(err, ErrSecretIsOwned) {
- r.markAsFailed(msgErrorIsOwned, err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonSecretSyncedError)
- return ctrl.Result{}, nil
- }
- // detect errors indicating that the secret is immutable
- // NOTE: this error cant be fixed by retrying so we don't return an error (which would requeue immediately)
- if errors.Is(err, ErrSecretImmutable) {
- r.markAsFailed(msgErrorUpdateImmutable, err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonSecretSyncedError)
- return ctrl.Result{}, nil
- }
- r.markAsFailed(msgErrorUpdateSecret, err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonSecretSyncedError)
- return ctrl.Result{}, err
- }
- r.markAsDone(externalSecret, start, log, esv1.ConditionReasonSecretSynced, msgSynced)
- return r.getRequeueResult(externalSecret), nil
- }
- // reconcileGenericTarget handles reconciliation for generic targets (ConfigMaps, Custom Resources).
- func (r *Reconciler) reconcileGenericTarget(
- ctx context.Context,
- externalSecret *esv1.ExternalSecret,
- log logr.Logger,
- start time.Time,
- resourceLabels map[string]string,
- syncCallsError *prometheus.CounterVec,
- ) (ctrl.Result, error) {
- var existing *unstructured.Unstructured
- if externalSecret.Spec.Target.CreationPolicy == esv1.CreatePolicyMerge ||
- externalSecret.Spec.Target.CreationPolicy == esv1.CreatePolicyOrphan ||
- externalSecret.Spec.Target.CreationPolicy == esv1.CreatePolicyOwner {
- var getErr error
- existing, getErr = r.getGenericResource(ctx, log, externalSecret)
- if getErr != nil && !apierrors.IsNotFound(getErr) {
- r.markAsFailed("could not get target resource", getErr, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonResourceSyncedError)
- return ctrl.Result{}, getErr
- }
- }
- valid, err := isGenericTargetValid(existing, externalSecret)
- if err != nil {
- log.V(1).Info("unable to validate target", "error", err)
- return ctrl.Result{}, err
- }
- if !shouldRefresh(externalSecret) && valid {
- log.V(1).Info("skipping refresh of generic target")
- return r.getRequeueResult(externalSecret), nil
- }
- dataMap, err := r.GetProviderSecretData(ctx, externalSecret)
- if err != nil {
- r.markAsFailed(msgErrorGetSecretData, err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonResourceSyncedError)
- return ctrl.Result{}, err
- }
- if len(dataMap) == 0 {
- switch externalSecret.Spec.Target.DeletionPolicy {
- case esv1.DeletionPolicyDelete:
- creationPolicy := externalSecret.Spec.Target.CreationPolicy
- if creationPolicy != esv1.CreatePolicyOwner {
- err = fmt.Errorf("unable to delete resource: creationPolicy=%s is not Owner", creationPolicy)
- r.markAsFailed("could not delete resource", err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonResourceSyncedError)
- return ctrl.Result{}, nil
- }
- err = r.deleteGenericResource(ctx, log, externalSecret)
- if err != nil {
- r.markAsFailed("could not delete resource", err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonResourceSyncedError)
- return ctrl.Result{}, err
- }
- r.markAsDone(externalSecret, start, log, esv1.ConditionReasonResourceDeleted, msgDeleted)
- return r.getRequeueResult(externalSecret), nil
- case esv1.DeletionPolicyRetain:
- r.markAsDone(externalSecret, start, log, esv1.ConditionReasonResourceSynced, msgSyncedRetain)
- return r.getRequeueResult(externalSecret), nil
- case esv1.DeletionPolicyMerge:
- }
- }
- // For Merge policy with existing resource, pass it to applyTemplateToManifest
- // so templates are applied to the existing resource instead of creating a new one
- var baseObj *unstructured.Unstructured
- if externalSecret.Spec.Target.CreationPolicy == esv1.CreatePolicyMerge && existing != nil {
- baseObj = existing
- }
- // render the template for the manifest
- obj, err := r.applyTemplateToManifest(ctx, externalSecret, dataMap, baseObj)
- if err != nil {
- r.markAsFailed("could not apply template to manifest", err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonResourceSyncedError)
- return ctrl.Result{}, err
- }
- // handle creation policies
- switch externalSecret.Spec.Target.CreationPolicy {
- case esv1.CreatePolicyNone:
- log.V(1).Info("resource creation skipped due to CreationPolicy=None")
- err = nil
- case esv1.CreatePolicyMerge:
- // for Merge policy, only update if resource exists
- if existing == nil || existing.GetUID() == "" {
- r.markAsDone(externalSecret, start, log, esv1.ConditionReasonResourceMissing, "resource will not be created due to CreationPolicy=Merge")
- return r.getRequeueResult(externalSecret), nil
- }
- obj.SetResourceVersion(existing.GetResourceVersion())
- obj.SetUID(existing.GetUID())
- // update the existing resource
- err = r.updateGenericResource(ctx, log, externalSecret, obj)
- case esv1.CreatePolicyOrphan, esv1.CreatePolicyOwner:
- if existing != nil {
- obj.SetResourceVersion(existing.GetResourceVersion())
- obj.SetUID(existing.GetUID())
- err = r.updateGenericResource(ctx, log, externalSecret, obj)
- } else {
- err = r.createGenericResource(ctx, log, externalSecret, obj)
- }
- }
- if err != nil {
- // if we got an update conflict, requeue immediately
- if apierrors.IsConflict(err) {
- log.V(1).Info("conflict while updating resource, will requeue")
- return ctrl.Result{RequeueAfter: 1 * time.Second}, nil
- }
- r.markAsFailed(msgErrorUpdateSecret, err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonResourceSyncedError)
- return ctrl.Result{}, err
- }
- if externalSecret.Spec.Target.CreationPolicy != esv1.CreatePolicyNone {
- gvk := getTargetGVK(externalSecret)
- esName := types.NamespacedName{Name: externalSecret.Name, Namespace: externalSecret.Namespace}
- if _, err := r.informerManager.EnsureInformer(ctx, gvk, esName); err != nil {
- log.Error(err, "failed to register informer for generic target, drift detection may not work",
- "group", gvk.Group,
- "version", gvk.Version,
- "kind", gvk.Kind)
- }
- }
- r.markAsDone(externalSecret, start, log, esv1.ConditionReasonResourceSynced, msgSynced)
- return r.getRequeueResult(externalSecret), nil
- }
- // getRequeueResult create a result with requeueAfter based on the ExternalSecret refresh interval.
- func (r *Reconciler) getRequeueResult(externalSecret *esv1.ExternalSecret) ctrl.Result {
- // default to the global requeue interval
- // note, this will never be used because the CRD has a default value of 1 hour
- refreshInterval := r.RequeueInterval
- if externalSecret.Spec.RefreshInterval != nil {
- refreshInterval = externalSecret.Spec.RefreshInterval.Duration
- }
- // if the refresh interval is <= 0, we should not requeue
- if refreshInterval <= 0 {
- return ctrl.Result{}
- }
- // if the last refresh time is not set, requeue after the refresh interval
- // note, this should not happen, as we only call this function on ExternalSecrets
- // that have been reconciled at least once
- if externalSecret.Status.RefreshTime.IsZero() {
- return ctrl.Result{RequeueAfter: refreshInterval}
- }
- timeSinceLastRefresh := time.Since(externalSecret.Status.RefreshTime.Time)
- // if the last refresh time is in the future, we should requeue immediately
- // note, this should not happen, as we always refresh an ExternalSecret
- // that has a last refresh time in the future
- if timeSinceLastRefresh < 0 {
- return ctrl.Result{Requeue: true}
- }
- // if there is time remaining, requeue after the remaining time
- if timeSinceLastRefresh < refreshInterval {
- return ctrl.Result{RequeueAfter: refreshInterval - timeSinceLastRefresh}
- }
- // otherwise, requeue immediately
- return ctrl.Result{Requeue: true}
- }
- func (r *Reconciler) markAsDone(externalSecret *esv1.ExternalSecret, start time.Time, log logr.Logger, reason, msg string) {
- oldReadyCondition := GetExternalSecretCondition(externalSecret.Status, esv1.ExternalSecretReady)
- newReadyCondition := NewExternalSecretCondition(esv1.ExternalSecretReady, v1.ConditionTrue, reason, msg)
- SetExternalSecretCondition(externalSecret, *newReadyCondition)
- externalSecret.Status.RefreshTime = metav1.NewTime(start)
- externalSecret.Status.SyncedResourceVersion = ctrlutil.GetResourceVersion(externalSecret.ObjectMeta)
- // if the status or reason has changed, log at the appropriate verbosity level
- if oldReadyCondition == nil || oldReadyCondition.Status != newReadyCondition.Status || oldReadyCondition.Reason != newReadyCondition.Reason {
- if newReadyCondition.Reason == esv1.ConditionReasonSecretDeleted {
- log.Info("deleted secret")
- } else {
- log.Info("reconciled secret")
- }
- } else {
- log.V(1).Info("reconciled secret")
- }
- }
- func (r *Reconciler) markAsFailed(msg string, err error, externalSecret *esv1.ExternalSecret, counter prometheus.Counter, reason string) {
- r.recorder.Event(externalSecret, v1.EventTypeWarning, esv1.ReasonUpdateFailed, err.Error())
- conditionSynced := NewExternalSecretCondition(esv1.ExternalSecretReady, v1.ConditionFalse, reason, msg)
- SetExternalSecretCondition(externalSecret, *conditionSynced)
- counter.Inc()
- }
- func (r *Reconciler) cleanupManagedSecrets(ctx context.Context, log logr.Logger, externalSecret *esv1.ExternalSecret) error {
- // Only delete resources if DeletionPolicy is Delete
- if externalSecret.Spec.Target.DeletionPolicy != esv1.DeletionPolicyDelete {
- log.V(1).Info("skipping resource deletion due to DeletionPolicy", "policy", externalSecret.Spec.Target.DeletionPolicy)
- return nil
- }
- // if this is a generic target, use deleteGenericResource
- if isGenericTarget(externalSecret) {
- return r.deleteGenericResource(ctx, log, externalSecret)
- }
- // handle Secret deletion
- secretName := externalSecret.Spec.Target.Name
- if secretName == "" {
- secretName = externalSecret.Name
- }
- var secret v1.Secret
- err := r.Get(ctx, types.NamespacedName{Name: secretName, Namespace: externalSecret.Namespace}, &secret)
- if err != nil {
- if apierrors.IsNotFound(err) {
- return nil
- }
- return err
- }
- // Only delete if we own it
- if metav1.IsControlledBy(&secret, externalSecret) {
- if err := r.Delete(ctx, &secret); err != nil && !apierrors.IsNotFound(err) {
- return err
- }
- log.V(1).Info("deleted managed secret", "secret", secretName)
- }
- return nil
- }
- func (r *Reconciler) deleteOrphanedSecrets(ctx context.Context, externalSecret *esv1.ExternalSecret, secretName string) error {
- ownerLabel := esutils.ObjectHash(fmt.Sprintf("%v/%v", externalSecret.Namespace, externalSecret.Name))
- // we use a PartialObjectMetadataList to avoid loading the full secret objects
- // and because the Secrets partials are always cached due to WatchesMetadata() in SetupWithManager()
- secretListPartial := &metav1.PartialObjectMetadataList{}
- secretListPartial.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("SecretList"))
- listOpts := &client.ListOptions{
- LabelSelector: labels.SelectorFromSet(map[string]string{
- esv1.LabelOwner: ownerLabel,
- }),
- Namespace: externalSecret.Namespace,
- }
- if err := r.List(ctx, secretListPartial, listOpts); err != nil {
- return err
- }
- // delete all secrets that are not the target secret
- for _, secretPartial := range secretListPartial.Items {
- if secretPartial.GetName() != secretName {
- err := r.Delete(ctx, &secretPartial)
- if err != nil && !apierrors.IsNotFound(err) {
- return err
- }
- r.recorder.Event(externalSecret, v1.EventTypeNormal, esv1.ReasonDeleted, eventDeletedOrphaned)
- }
- }
- return nil
- }
- // createSecret creates a new secret with the given mutation function.
- func (r *Reconciler) createSecret(ctx context.Context, mutationFunc func(secret *v1.Secret) error, es *esv1.ExternalSecret, secretName string) error {
- fqdn := fqdnFor(es.Name)
- // define and mutate the new secret
- newSecret := &v1.Secret{
- ObjectMeta: metav1.ObjectMeta{
- Name: secretName,
- Namespace: es.Namespace,
- Labels: map[string]string{},
- Annotations: map[string]string{},
- },
- Data: make(map[string][]byte),
- }
- if err := mutationFunc(newSecret); err != nil {
- return err
- }
- // note, we set field owner even for Create
- if err := r.Create(ctx, newSecret, client.FieldOwner(fqdn)); err != nil {
- return err
- }
- // set the binding reference to the secret
- // https://github.com/external-secrets/external-secrets/pull/2263
- es.Status.Binding = v1.LocalObjectReference{Name: newSecret.Name}
- r.recorder.Event(es, v1.EventTypeNormal, esv1.ReasonCreated, eventCreated)
- return nil
- }
- func (r *Reconciler) updateSecret(ctx context.Context, existingSecret *v1.Secret, mutationFunc func(secret *v1.Secret) error, es *esv1.ExternalSecret, secretName string) error {
- fqdn := fqdnFor(es.Name)
- // fail if the secret does not exist
- // this should never happen because we check this before calling this function
- if existingSecret.UID == "" {
- return fmt.Errorf(errUpdateNotFound, secretName)
- }
- // set the binding reference to the secret
- // https://github.com/external-secrets/external-secrets/pull/2263
- es.Status.Binding = v1.LocalObjectReference{Name: secretName}
- // mutate a copy of the existing secret with the mutation function
- updatedSecret := existingSecret.DeepCopy()
- if err := mutationFunc(updatedSecret); err != nil {
- return fmt.Errorf(errMutate, updatedSecret.Name, err)
- }
- // if the secret does not need to be updated, return early
- if equality.Semantic.DeepEqual(existingSecret, updatedSecret) {
- return nil
- }
- // if the existing secret is immutable, we can only update the object metadata
- if ptr.Deref(existingSecret.Immutable, false) {
- // check if the metadata was changed
- metadataChanged := !equality.Semantic.DeepEqual(existingSecret.ObjectMeta, updatedSecret.ObjectMeta)
- // check if the immutable data/type was changed
- var dataChanged bool
- if metadataChanged {
- // update the `existingSecret` object with the metadata from `updatedSecret`
- // this lets us compare the objects to see if the immutable data/type was changed
- existingSecret.ObjectMeta = *updatedSecret.ObjectMeta.DeepCopy()
- dataChanged = !equality.Semantic.DeepEqual(existingSecret, updatedSecret)
- // because we use labels and annotations to keep track of the secret,
- // we need to update the metadata, regardless of if the immutable data was changed
- // NOTE: we are using the `existingSecret` object here, as we ONLY want to update the metadata,
- // and we previously copied the metadata from the `updatedSecret` object
- if err := r.Update(ctx, existingSecret, client.FieldOwner(fqdn)); err != nil {
- // if we get a conflict, we should return early to requeue immediately
- // note, we don't wrap this error so we can handle it in the caller
- if apierrors.IsConflict(err) {
- return err
- }
- return fmt.Errorf(errUpdate, existingSecret.Name, err)
- }
- } else {
- // we know there was some change in the secret (or we would have returned early)
- // we know the metadata was NOT changed (metadataChanged == false)
- // so, the only thing that could have changed is the immutable data/type fields
- dataChanged = true
- }
- // if the immutable data was changed, we should return an error
- if dataChanged {
- return fmt.Errorf(errUpdate, existingSecret.Name, ErrSecretImmutable)
- }
- }
- // update the secret
- if err := r.Update(ctx, updatedSecret, client.FieldOwner(fqdn)); err != nil {
- // if we get a conflict, we should return early to requeue immediately
- // note, we don't wrap this error so we can handle it in the caller
- if apierrors.IsConflict(err) {
- return err
- }
- return fmt.Errorf(errUpdate, updatedSecret.Name, err)
- }
- r.recorder.Event(es, v1.EventTypeNormal, esv1.ReasonUpdated, eventUpdated)
- return nil
- }
- // getManagedDataKeys returns the list of data keys in a secret which are managed by a specified owner.
- func getManagedDataKeys(secret *v1.Secret, fieldOwner string) ([]string, error) {
- return getManagedFieldKeys(secret, fieldOwner, func(fields map[string]any) []string {
- dataFields := fields["f:data"]
- if dataFields == nil {
- return nil
- }
- df, ok := dataFields.(map[string]any)
- if !ok {
- return nil
- }
- return slices.Collect(maps.Keys(df))
- })
- }
- func getManagedFieldKeys(
- secret *v1.Secret,
- fieldOwner string,
- process func(fields map[string]any) []string,
- ) ([]string, error) {
- fqdn := fqdnFor(fieldOwner)
- var keys []string
- for _, v := range secret.ObjectMeta.ManagedFields {
- if v.Manager != fqdn {
- continue
- }
- fields := make(map[string]any)
- err := json.Unmarshal(v.FieldsV1.Raw, &fields)
- if err != nil {
- return nil, fmt.Errorf("error unmarshaling managed fields: %w", err)
- }
- for _, key := range process(fields) {
- if key == "." {
- continue
- }
- keys = append(keys, strings.TrimPrefix(key, "f:"))
- }
- }
- return keys, nil
- }
- func shouldSkipClusterSecretStore(r *Reconciler, es *esv1.ExternalSecret) bool {
- return !r.ClusterSecretStoreEnabled && es.Spec.SecretStoreRef.Kind == esv1.ClusterSecretStoreKind
- }
- // shouldSkipUnmanagedStore iterates over all secretStore references in the externalSecret spec,
- // fetches the store and evaluates the controllerClass property.
- // Returns true if any storeRef points to store with a non-matching controllerClass.
- func shouldSkipUnmanagedStore(ctx context.Context, namespace string, r *Reconciler, es *esv1.ExternalSecret) (bool, error) {
- var storeList []esv1.SecretStoreRef
- if es.Spec.SecretStoreRef.Name != "" {
- storeList = append(storeList, es.Spec.SecretStoreRef)
- }
- for _, ref := range es.Spec.Data {
- if ref.SourceRef != nil {
- storeList = append(storeList, ref.SourceRef.SecretStoreRef)
- }
- }
- for _, ref := range es.Spec.DataFrom {
- if ref.SourceRef != nil && ref.SourceRef.SecretStoreRef != nil {
- storeList = append(storeList, *ref.SourceRef.SecretStoreRef)
- }
- // verify that generator's controllerClass matches
- if ref.SourceRef != nil && ref.SourceRef.GeneratorRef != nil {
- _, obj, err := resolvers.GeneratorRef(ctx, r.Client, r.Scheme, namespace, ref.SourceRef.GeneratorRef)
- if err != nil {
- if apierrors.IsNotFound(err) {
- // skip non-existent generators
- continue
- }
- if errors.Is(err, resolvers.ErrUnableToGetGenerator) {
- // skip generators that we can't get (e.g. due to being invalid)
- continue
- }
- return false, err
- }
- skipGenerator, err := shouldSkipGenerator(r, obj)
- if err != nil {
- return false, err
- }
- if skipGenerator {
- return true, nil
- }
- }
- }
- for _, ref := range storeList {
- var store esv1.GenericStore
- switch ref.Kind {
- case esv1.SecretStoreKind, "":
- store = &esv1.SecretStore{}
- case esv1.ClusterSecretStoreKind:
- store = &esv1.ClusterSecretStore{}
- namespace = ""
- default:
- return false, fmt.Errorf("unsupported secret store kind: %s", ref.Kind)
- }
- err := r.Get(ctx, types.NamespacedName{
- Name: ref.Name,
- Namespace: namespace,
- }, store)
- if err != nil {
- if apierrors.IsNotFound(err) {
- // skip non-existent stores
- continue
- }
- return false, err
- }
- class := store.GetSpec().Controller
- if class != "" && class != r.ControllerClass {
- return true, nil
- }
- }
- return false, nil
- }
- func shouldRefresh(es *esv1.ExternalSecret) bool {
- switch es.Spec.RefreshPolicy {
- case esv1.RefreshPolicyCreatedOnce:
- if es.Status.SyncedResourceVersion == "" || es.Status.RefreshTime.IsZero() {
- return true
- }
- return false
- case esv1.RefreshPolicyOnChange:
- if es.Status.SyncedResourceVersion == "" || es.Status.RefreshTime.IsZero() {
- return true
- }
- return es.Status.SyncedResourceVersion != ctrlutil.GetResourceVersion(es.ObjectMeta)
- case esv1.RefreshPolicyPeriodic:
- return shouldRefreshPeriodic(es)
- default:
- return shouldRefreshPeriodic(es)
- }
- }
- func shouldRefreshPeriodic(es *esv1.ExternalSecret) bool {
- // if the refresh interval is 0, and we have synced previously, we should not refresh
- if es.Spec.RefreshInterval.Duration <= 0 && es.Status.SyncedResourceVersion != "" {
- return false
- }
- // if the ExternalSecret has been updated, we should refresh
- if es.Status.SyncedResourceVersion != ctrlutil.GetResourceVersion(es.ObjectMeta) {
- return true
- }
- // if the last refresh time is zero, we should refresh
- if es.Status.RefreshTime.IsZero() {
- return true
- }
- // if the last refresh time is in the future, we should refresh
- if es.Status.RefreshTime.Time.After(time.Now()) {
- return true
- }
- // if the last refresh time + refresh interval is before now, we should refresh
- return es.Status.RefreshTime.Add(es.Spec.RefreshInterval.Duration).Before(time.Now())
- }
- // isSecretValid checks if the secret exists, and it's data is consistent with the calculated hash.
- func isSecretValid(existingSecret *v1.Secret, es *esv1.ExternalSecret) bool {
- // Secret is always valid with `CreationPolicy=Orphan`
- if es.Spec.Target.CreationPolicy == esv1.CreatePolicyOrphan {
- return true
- }
- if existingSecret.UID == "" {
- return false
- }
- // if the managed label is missing or incorrect, then it's invalid
- if existingSecret.Labels[esv1.LabelManaged] != esv1.LabelManagedValue {
- return false
- }
- // if the data-hash annotation is missing or incorrect, then it's invalid
- // this is how we know if the data has chanced since we last updated the secret
- if existingSecret.Annotations[esv1.AnnotationDataHash] != esutils.ObjectHash(existingSecret.Data) {
- return false
- }
- return true
- }
- func isGenericTargetValid(existingTarget *unstructured.Unstructured, es *esv1.ExternalSecret) (bool, error) {
- if es.Spec.Target.CreationPolicy == esv1.CreatePolicyOrphan {
- return true, nil
- }
- if existingTarget == nil || existingTarget.GetUID() == "" {
- return false, nil
- }
- if existingTarget.GetLabels()[esv1.LabelManaged] != esv1.LabelManagedValue {
- return false, nil
- }
- hash, err := genericTargetContentHash(existingTarget)
- if err != nil {
- return false, fmt.Errorf("failed to hash target: %w", err)
- }
- if existingTarget.GetAnnotations()[esv1.AnnotationDataHash] != hash {
- return false, nil
- }
- return true, nil
- }
- // genericTargetContentHash computes a hash over the hashable content of an unstructured object.
- // It uses the "spec" field if present, otherwise falls back to "data".
- func genericTargetContentHash(obj *unstructured.Unstructured) (string, error) {
- content := obj.Object
- switch {
- case content["spec"] != nil:
- return esutils.ObjectHash(content["spec"]), nil
- case content["data"] != nil:
- return esutils.ObjectHash(content["data"]), nil
- default:
- return "", errors.New("generic target content does not have a spec or data field for content hashing")
- }
- }
- // SetupWithManager returns a new controller builder that will be started by the provided Manager.
- func (r *Reconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opts controller.Options) error {
- r.recorder = mgr.GetEventRecorderFor("external-secrets")
- // Initialize informer manager only if generic targets are allowed
- if r.AllowGenericTargets && r.informerManager == nil {
- r.informerManager = NewInformerManager(ctx, mgr.GetCache(), r.Client, r.Log.WithName("informer-manager"))
- }
- // index ExternalSecrets based on the target secret name,
- // this lets us quickly find all ExternalSecrets which target a specific Secret
- if err := mgr.GetFieldIndexer().IndexField(ctx, &esv1.ExternalSecret{}, indexESTargetSecretNameField, func(obj client.Object) []string {
- es := obj.(*esv1.ExternalSecret)
- // Don't index generic targets here (they use indexESTargetResourceField)
- if isGenericTarget(es) {
- return nil
- }
- // if the target name is set, use that as the index
- if es.Spec.Target.Name != "" {
- return []string{es.Spec.Target.Name}
- }
- // otherwise, use the ExternalSecret name
- return []string{es.Name}
- }); err != nil {
- return err
- }
- // index ExternalSecrets based on the target resource (GVK + name)
- // this lets us quickly find all ExternalSecrets which target a specific generic resource
- if err := mgr.GetFieldIndexer().IndexField(ctx, &esv1.ExternalSecret{}, indexESTargetResourceField, func(obj client.Object) []string {
- es := obj.(*esv1.ExternalSecret)
- if !r.AllowGenericTargets || !isGenericTarget(es) {
- return nil
- }
- gvk := getTargetGVK(es)
- targetName := getTargetName(es)
- // Index format: "group/version/kind/name"
- return []string{fmt.Sprintf("%s/%s/%s/%s", gvk.Group, gvk.Version, gvk.Kind, targetName)}
- }); err != nil {
- return err
- }
- // predicate function to ignore secret events unless they have the "managed" label
- secretHasESLabel := predicate.NewPredicateFuncs(func(object client.Object) bool {
- value, hasLabel := object.GetLabels()[esv1.LabelManaged]
- return hasLabel && value == esv1.LabelManagedValue
- })
- // Build the controller
- builder := ctrl.NewControllerManagedBy(mgr).
- WithOptions(opts).
- For(&esv1.ExternalSecret{}).
- // we cant use Owns(), as we don't set ownerReferences when the creationPolicy is not Owner.
- // we use WatchesMetadata() to reduce memory usage, as otherwise we have to process full secret objects.
- WatchesMetadata(
- &v1.Secret{},
- handler.EnqueueRequestsFromMapFunc(r.findObjectsForSecret),
- builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}, secretHasESLabel),
- )
- // Watch generic targets dynamically via the informer manager
- // Only add this watch source if the feature is enabled
- if r.AllowGenericTargets {
- builder = builder.WatchesRawSource(r.informerManager.Source())
- }
- return builder.Complete(r)
- }
- func (r *Reconciler) findObjectsForSecret(ctx context.Context, secret client.Object) []reconcile.Request {
- externalSecretsList := &esv1.ExternalSecretList{}
- listOps := &client.ListOptions{
- FieldSelector: fields.OneTermEqualSelector(indexESTargetSecretNameField, secret.GetName()),
- Namespace: secret.GetNamespace(),
- }
- err := r.List(ctx, externalSecretsList, listOps)
- if err != nil {
- return []reconcile.Request{}
- }
- requests := make([]reconcile.Request, len(externalSecretsList.Items))
- for i := range externalSecretsList.Items {
- requests[i] = reconcile.Request{
- NamespacedName: types.NamespacedName{
- Name: externalSecretsList.Items[i].GetName(),
- Namespace: externalSecretsList.Items[i].GetNamespace(),
- },
- }
- }
- return requests
- }
|