| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012 |
- /*
- 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
- http://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
- 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/fields"
- "k8s.io/apimachinery/pkg/labels"
- "k8s.io/apimachinery/pkg/runtime"
- "k8s.io/apimachinery/pkg/runtime/schema"
- "k8s.io/apimachinery/pkg/selection"
- "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/cache"
- "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"
- esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
- // Metrics.
- "github.com/external-secrets/external-secrets/pkg/controllers/externalsecret/esmetrics"
- ctrlmetrics "github.com/external-secrets/external-secrets/pkg/controllers/metrics"
- "github.com/external-secrets/external-secrets/pkg/utils"
- "github.com/external-secrets/external-secrets/pkg/utils/resolvers"
- // Loading registered generators.
- _ "github.com/external-secrets/external-secrets/pkg/generator/register"
- // Loading registered providers.
- _ "github.com/external-secrets/external-secrets/pkg/provider/register"
- )
- const (
- fieldOwnerTemplate = "externalsecrets.external-secrets.io/%v"
- // 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 = "could not apply conversion strategy to keys: %v"
- errDecode = "could not apply decoding strategy to %v[%d]: %v"
- errGenerate = "could not generate [%d]: %w"
- errRewrite = "could not rewrite spec.dataFrom[%d]: %v"
- errInvalidKeys = "secret keys from spec.dataFrom.%v[%d] can only have alphanumeric, '-', '_' or '.' characters. Convert them using rewrite (https://external-secrets.io/latest/guides/datafrom-rewrite/)"
- 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"
- )
- // 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"
- // 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
- recorder record.EventRecorder
- }
- // 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 := &esv1beta1.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(esv1beta1.ExternalSecretDeleted, v1.ConditionFalse, esv1beta1.ConditionReasonSecretDeleted, "Secret was deleted")
- SetExternalSecretCondition(&esv1beta1.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
- }
- // skip reconciliation if deletion timestamp is set on external secret
- if !externalSecret.GetDeletionTimestamp().IsZero() {
- log.V(1).Info("skipping ExternalSecret, it is marked for deletion")
- return ctrl.Result{}, nil
- }
- // 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
- }
- // 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[esv1beta1.LabelManaged] != esv1beta1.LabelManagedValue {
- fqdn := fmt.Sprintf(fieldOwnerTemplate, externalSecret.Name)
- patch := client.MergeFrom(secretPartial.DeepCopy())
- if secretPartial.Labels == nil {
- secretPartial.Labels = make(map[string]string)
- }
- secretPartial.Labels[esv1beta1.LabelManaged] = esv1beta1.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) {
- 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))
- 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 esv1beta1.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 != esv1beta1.CreatePolicyOwner {
- err := fmt.Errorf(errDeleteCreatePolicy, secretName, creationPolicy)
- r.markAsFailed(msgErrorDeleteSecret, err, externalSecret, syncCallsError.With(resourceLabels))
- return ctrl.Result{}, nil
- }
- // delete the secret, if it exists
- if existingSecret.UID != "" {
- if err := r.Delete(ctx, existingSecret); err != nil && !apierrors.IsNotFound(err) {
- r.markAsFailed(msgErrorDeleteSecret, err, externalSecret, syncCallsError.With(resourceLabels))
- return ctrl.Result{}, err
- }
- }
- r.markAsDone(externalSecret, start, log, esv1beta1.ConditionReasonSecretDeleted, msgDeleted)
- return r.getRequeueResult(externalSecret), nil
- // In case provider secrets don't exist the kubernetes secret will be kept as-is.
- case esv1beta1.DeletionPolicyRetain:
- r.markAsDone(externalSecret, start, log, esv1beta1.ConditionReasonSecretSynced, msgSyncedRetain)
- return r.getRequeueResult(externalSecret), nil
- // noop, handled below
- case esv1beta1.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() == esv1beta1.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 == esv1beta1.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 != esv1beta1.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)
- }
- // 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)
- }
- // set the immutable flag on the secret if requested by the ExternalSecret
- if externalSecret.Spec.Target.Immutable {
- secret.Immutable = ptr.To(true)
- }
- // 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 == esv1beta1.CreatePolicyOwner {
- lblValue := utils.ObjectHash(fmt.Sprintf("%v/%v", externalSecret.Namespace, externalSecret.Name))
- secret.Labels[esv1beta1.LabelOwner] = lblValue
- } else {
- // the label should not be set if the creation policy is not Owner
- delete(secret.Labels, esv1beta1.LabelOwner)
- }
- secret.Labels[esv1beta1.LabelManaged] = esv1beta1.LabelManagedValue
- secret.Annotations[esv1beta1.AnnotationDataHash] = utils.ObjectHash(secret.Data)
- return nil
- }
- switch externalSecret.Spec.Target.CreationPolicy { //nolint:exhaustive
- case esv1beta1.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, esv1beta1.ConditionReasonSecretMissing, msgMissing)
- return r.getRequeueResult(externalSecret), nil
- }
- case esv1beta1.CreatePolicyNone:
- log.V(1).Info("secret creation skipped due to creationPolicy=None")
- err = nil
- default:
- // create the secret, if it does not exist
- if existingSecret.UID == "" {
- err = r.createSecret(ctx, mutationFunc, externalSecret, secretName)
- // we may have orphaned secrets to clean up,
- // for example, if the target secret name was changed
- if err == nil {
- delErr := deleteOrphanedSecrets(ctx, r.Client, externalSecret, secretName)
- if delErr != nil {
- r.markAsFailed(msgErrorDeleteOrphaned, delErr, externalSecret, syncCallsError.With(resourceLabels))
- return ctrl.Result{}, delErr
- }
- }
- } else {
- // update the secret, if it exists
- 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))
- 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))
- 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))
- return ctrl.Result{}, nil
- }
- r.markAsFailed(msgErrorUpdateSecret, err, externalSecret, syncCallsError.With(resourceLabels))
- return ctrl.Result{}, err
- }
- r.markAsDone(externalSecret, start, log, esv1beta1.ConditionReasonSecretSynced, msgSynced)
- return r.getRequeueResult(externalSecret), nil
- }
- // getRequeueResult create a result with requeueAfter based on the ExternalSecret refresh interval.
- func (r *Reconciler) getRequeueResult(externalSecret *esv1beta1.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 *esv1beta1.ExternalSecret, start time.Time, log logr.Logger, reason, msg string) {
- oldReadyCondition := GetExternalSecretCondition(externalSecret.Status, esv1beta1.ExternalSecretReady)
- newReadyCondition := NewExternalSecretCondition(esv1beta1.ExternalSecretReady, v1.ConditionTrue, reason, msg)
- SetExternalSecretCondition(externalSecret, *newReadyCondition)
- externalSecret.Status.RefreshTime = metav1.NewTime(start)
- externalSecret.Status.SyncedResourceVersion = getResourceVersion(externalSecret)
- // 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 == esv1beta1.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 *esv1beta1.ExternalSecret, counter prometheus.Counter) {
- r.recorder.Event(externalSecret, v1.EventTypeWarning, esv1beta1.ReasonUpdateFailed, err.Error())
- conditionSynced := NewExternalSecretCondition(esv1beta1.ExternalSecretReady, v1.ConditionFalse, esv1beta1.ConditionReasonSecretSyncedError, msg)
- SetExternalSecretCondition(externalSecret, *conditionSynced)
- counter.Inc()
- }
- func deleteOrphanedSecrets(ctx context.Context, cl client.Client, externalSecret *esv1beta1.ExternalSecret, secretName string) error {
- ownerLabel := utils.ObjectHash(fmt.Sprintf("%v/%v", externalSecret.Namespace, externalSecret.Name))
- secretListPartial := &metav1.PartialObjectMetadataList{}
- secretListPartial.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("SecretList"))
- listOpts := &client.ListOptions{
- LabelSelector: labels.SelectorFromSet(map[string]string{
- esv1beta1.LabelOwner: ownerLabel,
- }),
- Namespace: externalSecret.Namespace,
- }
- if err := cl.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 {
- if err := cl.Delete(ctx, &secretPartial); err != nil {
- return err
- }
- }
- }
- 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 *esv1beta1.ExternalSecret, secretName string) error {
- fqdn := fmt.Sprintf(fieldOwnerTemplate, es.Name)
- // define and mutate the new secret
- newSecret := &v1.Secret{
- ObjectMeta: metav1.ObjectMeta{
- Name: secretName,
- Namespace: es.Namespace,
- },
- 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, esv1beta1.ReasonCreated, "Created Secret")
- return nil
- }
- func (r *Reconciler) updateSecret(ctx context.Context, existingSecret *v1.Secret, mutationFunc func(secret *v1.Secret) error, es *esv1beta1.ExternalSecret, secretName string) error {
- fqdn := fmt.Sprintf(fieldOwnerTemplate, 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, esv1beta1.ReasonUpdated, "Updated Secret")
- 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 := fmt.Sprintf(fieldOwnerTemplate, 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 getResourceVersion(es *esv1beta1.ExternalSecret) string {
- return fmt.Sprintf("%d-%s", es.ObjectMeta.GetGeneration(), hashMeta(es.ObjectMeta))
- }
- // hashMeta returns a consistent hash of the `metadata.labels` and `metadata.annotations` fields of the given object.
- func hashMeta(m metav1.ObjectMeta) string {
- type meta struct {
- annotations map[string]string
- labels map[string]string
- }
- objectMeta := meta{
- annotations: m.Annotations,
- labels: m.Labels,
- }
- return utils.ObjectHash(objectMeta)
- }
- func shouldSkipClusterSecretStore(r *Reconciler, es *esv1beta1.ExternalSecret) bool {
- return !r.ClusterSecretStoreEnabled && es.Spec.SecretStoreRef.Kind == esv1beta1.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 *esv1beta1.ExternalSecret) (bool, error) {
- var storeList []esv1beta1.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 esv1beta1.GenericStore
- switch ref.Kind {
- case esv1beta1.SecretStoreKind, "":
- store = &esv1beta1.SecretStore{}
- case esv1beta1.ClusterSecretStoreKind:
- store = &esv1beta1.ClusterSecretStore{}
- namespace = ""
- }
- 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 *esv1beta1.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 != getResourceVersion(es) {
- 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) bool {
- // if target secret doesn't exist, we need to refresh
- if existingSecret.UID == "" {
- return false
- }
- // if the managed label is missing or incorrect, then it's invalid
- if existingSecret.Labels[esv1beta1.LabelManaged] != esv1beta1.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[esv1beta1.AnnotationDataHash] != utils.ObjectHash(existingSecret.Data) {
- return false
- }
- return true
- }
- // SetupWithManager returns a new controller builder that will be started by the provided Manager.
- func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error {
- r.recorder = mgr.GetEventRecorderFor("external-secrets")
- // 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(context.Background(), &esv1beta1.ExternalSecret{}, indexESTargetSecretNameField, func(obj client.Object) []string {
- es := obj.(*esv1beta1.ExternalSecret)
- // 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
- }
- // predicate function to ignore secret events unless they have the "managed" label
- secretHasESLabel := predicate.NewPredicateFuncs(func(object client.Object) bool {
- value, hasLabel := object.GetLabels()[esv1beta1.LabelManaged]
- return hasLabel && value == esv1beta1.LabelManagedValue
- })
- return ctrl.NewControllerManagedBy(mgr).
- WithOptions(opts).
- For(&esv1beta1.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),
- ).
- Complete(r)
- }
- func (r *Reconciler) findObjectsForSecret(ctx context.Context, secret client.Object) []reconcile.Request {
- externalSecretsList := &esv1beta1.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, item := range externalSecretsList.Items {
- requests[i] = reconcile.Request{
- NamespacedName: types.NamespacedName{
- Name: item.GetName(),
- Namespace: item.GetNamespace(),
- },
- }
- }
- return requests
- }
- func BuildManagedSecretClient(mgr ctrl.Manager) (client.Client, error) {
- // secrets we manage will have the `reconcile.external-secrets.io/managed=true` label
- managedLabelReq, _ := labels.NewRequirement(esv1beta1.LabelManaged, selection.Equals, []string{esv1beta1.LabelManagedValue})
- managedLabelSelector := labels.NewSelector().Add(*managedLabelReq)
- // create a new cache with a label selector for managed secrets
- // NOTE: this means that the cache/client will be unable to see secrets without the "managed" label
- secretCacheOpts := cache.Options{
- HTTPClient: mgr.GetHTTPClient(),
- Scheme: mgr.GetScheme(),
- Mapper: mgr.GetRESTMapper(),
- ByObject: map[client.Object]cache.ByObject{
- &v1.Secret{}: {
- Label: managedLabelSelector,
- },
- },
- // this requires us to explicitly start an informer for each object type
- // and helps avoid people mistakenly using the secret client for other resources
- ReaderFailOnMissingInformer: true,
- }
- secretCache, err := cache.New(mgr.GetConfig(), secretCacheOpts)
- if err != nil {
- return nil, err
- }
- // start an informer for secrets
- // this is required because we set ReaderFailOnMissingInformer to true
- _, err = secretCache.GetInformer(context.Background(), &v1.Secret{})
- if err != nil {
- return nil, err
- }
- // add the secret cache to the manager, so that it starts at the same time
- err = mgr.Add(secretCache)
- if err != nil {
- return nil, err
- }
- // create a new client that uses the secret cache
- secretClient, err := client.New(mgr.GetConfig(), client.Options{
- HTTPClient: mgr.GetHTTPClient(),
- Scheme: mgr.GetScheme(),
- Mapper: mgr.GetRESTMapper(),
- Cache: &client.CacheOptions{
- Reader: secretCache,
- },
- })
- if err != nil {
- return nil, err
- }
- return secretClient, nil
- }
|