|
|
@@ -30,12 +30,18 @@ import (
|
|
|
"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"
|
|
|
@@ -57,34 +63,65 @@ import (
|
|
|
)
|
|
|
|
|
|
const (
|
|
|
- fieldOwnerTemplate = "externalsecrets.external-secrets.io/%v"
|
|
|
- errGetES = "could not get ExternalSecret"
|
|
|
- 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)"
|
|
|
- errUpdateSecret = "could not update Secret"
|
|
|
- errPatchStatus = "unable to patch status"
|
|
|
- errGetExistingSecret = "could not get existing secret: %w"
|
|
|
- errSetCtrlReference = "could not set ExternalSecret controller reference: %w"
|
|
|
- errFetchTplFrom = "error fetching templateFrom data: %w"
|
|
|
- errGetSecretData = "could not get secret data from provider"
|
|
|
- errDeleteSecret = "could not delete secret"
|
|
|
- errApplyTemplate = "could not apply template: %w"
|
|
|
- errExecTpl = "could not execute template: %w"
|
|
|
- errInvalidCreatePolicy = "invalid creationPolicy=%s. Can not delete secret i do not own"
|
|
|
- errPolicyMergeNotFound = "the desired secret %s was not found. With creationPolicy=Merge the secret won't be created"
|
|
|
- errPolicyMergeGetSecret = "unable to get secret %s: %w"
|
|
|
- errPolicyMergeMutate = "unable to mutate secret %s: %w"
|
|
|
- errPolicyMergePatch = "unable to patch secret %s: %w"
|
|
|
+ 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"
|
|
|
)
|
|
|
|
|
|
-const externalSecretSecretNameKey = ".spec.target.name"
|
|
|
+// 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
|
|
|
@@ -98,7 +135,7 @@ type Reconciler struct {
|
|
|
// 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) (ctrl.Result, error) {
|
|
|
+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})
|
|
|
@@ -112,11 +149,13 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
|
|
|
esmetrics.GetCounterVec(esmetrics.SyncCallsKey).With(resourceLabels).Inc()
|
|
|
}()
|
|
|
|
|
|
- var externalSecret esv1beta1.ExternalSecret
|
|
|
- err := r.Get(ctx, req.NamespacedName, &externalSecret)
|
|
|
-
|
|
|
+ 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{
|
|
|
@@ -128,114 +167,160 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
|
|
|
return ctrl.Result{}, nil
|
|
|
}
|
|
|
|
|
|
- log.Error(err, errGetES)
|
|
|
+ log.Error(err, logErrorGetES)
|
|
|
syncCallsError.With(resourceLabels).Inc()
|
|
|
-
|
|
|
return ctrl.Result{}, err
|
|
|
}
|
|
|
|
|
|
- // See https://github.com/external-secrets/external-secrets/issues/3604
|
|
|
- // We fetch the ExternalSecret resource above, however the status subresource is inconsistent.
|
|
|
- // We have to explicitly fetch it, otherwise it may be missing and will cause
|
|
|
- // unexpected side effects.
|
|
|
- err = r.SubResource("status").Get(ctx, &externalSecret, &externalSecret)
|
|
|
- if err != nil {
|
|
|
- log.Error(err, "failed to get status subresource")
|
|
|
- return ctrl.Result{}, err
|
|
|
- }
|
|
|
-
|
|
|
- timeSinceLastRefresh := 0 * time.Second
|
|
|
- if !externalSecret.Status.RefreshTime.IsZero() {
|
|
|
- timeSinceLastRefresh = time.Since(externalSecret.Status.RefreshTime.Time)
|
|
|
- }
|
|
|
-
|
|
|
// skip reconciliation if deletion timestamp is set on external secret
|
|
|
- if externalSecret.DeletionTimestamp != nil {
|
|
|
- log.Info("skipping as it is in deletion")
|
|
|
+ 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.Info("skipping cluster secret store as it is disabled")
|
|
|
+ log.V(1).Info("skipping ExternalSecret, ClusterSecretStore feature is disabled")
|
|
|
return ctrl.Result{}, nil
|
|
|
}
|
|
|
|
|
|
- // skip when pointing to an unmanaged store
|
|
|
+ // 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.Info("skipping unmanaged store as it points to a unmanaged controllerClass")
|
|
|
+ log.V(1).Info("skipping ExternalSecret, uses unmanaged SecretStore")
|
|
|
return ctrl.Result{}, nil
|
|
|
}
|
|
|
|
|
|
- refreshInt := r.RequeueInterval
|
|
|
- if externalSecret.Spec.RefreshInterval != nil {
|
|
|
- refreshInt = externalSecret.Spec.RefreshInterval.Duration
|
|
|
- }
|
|
|
-
|
|
|
- // Target Secret Name should default to the ExternalSecret name if not explicitly specified
|
|
|
+ // the target secret name defaults to the ExternalSecret name, if not explicitly set
|
|
|
secretName := externalSecret.Spec.Target.Name
|
|
|
if secretName == "" {
|
|
|
- secretName = externalSecret.ObjectMeta.Name
|
|
|
+ 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
|
|
|
}
|
|
|
|
|
|
- // fetch external secret, we need to ensure that it exists, and it's hashmap corresponds
|
|
|
- var existingSecret v1.Secret
|
|
|
- err = r.Get(ctx, types.NamespacedName{
|
|
|
- Name: secretName,
|
|
|
- Namespace: externalSecret.Namespace,
|
|
|
- }, &existingSecret)
|
|
|
+ // 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, errGetExistingSecret)
|
|
|
+ log.Error(err, logErrorGetSecret, "secretName", secretName, "secretNamespace", externalSecret.Namespace)
|
|
|
+ syncCallsError.With(resourceLabels).Inc()
|
|
|
return ctrl.Result{}, err
|
|
|
}
|
|
|
|
|
|
- // refresh should be skipped if
|
|
|
- // 1. resource generation hasn't changed
|
|
|
- // 2. refresh interval is 0
|
|
|
- // 3. if we're still within refresh-interval
|
|
|
- if !shouldRefresh(externalSecret) && isSecretValid(existingSecret) {
|
|
|
- refreshInt = (externalSecret.Spec.RefreshInterval.Duration - timeSinceLastRefresh) + 5*time.Second
|
|
|
- log.V(1).Info("skipping refresh", "rv", getResourceVersion(externalSecret), "nr", refreshInt.Seconds())
|
|
|
- return ctrl.Result{RequeueAfter: refreshInt}, nil
|
|
|
+ // 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
|
|
|
}
|
|
|
- if !shouldReconcile(externalSecret) {
|
|
|
- log.V(1).Info("stopping reconciling", "rv", getResourceVersion(externalSecret))
|
|
|
- return ctrl.Result{}, nil
|
|
|
+
|
|
|
+ // 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
|
|
|
}
|
|
|
|
|
|
- // patch status when done processing
|
|
|
- p := client.MergeFrom(externalSecret.DeepCopy())
|
|
|
+ // 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() {
|
|
|
- err = r.Status().Patch(ctx, &externalSecret, p)
|
|
|
- if err != nil {
|
|
|
- log.Error(err, errPatchStatus)
|
|
|
+ // if the status has not changed, we don't need to update it
|
|
|
+ if equality.Semantic.DeepEqual(currentStatus, externalSecret.Status) {
|
|
|
+ return
|
|
|
}
|
|
|
- }()
|
|
|
|
|
|
- secret := &v1.Secret{
|
|
|
- ObjectMeta: metav1.ObjectMeta{
|
|
|
- Name: secretName,
|
|
|
- Namespace: externalSecret.Namespace,
|
|
|
- },
|
|
|
- Immutable: &externalSecret.Spec.Target.Immutable,
|
|
|
- Data: make(map[string][]byte),
|
|
|
- }
|
|
|
+ // 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
|
|
|
+ }
|
|
|
|
|
|
- dataMap, err := r.getProviderSecretData(ctx, &externalSecret)
|
|
|
- if err != nil {
|
|
|
- r.markAsFailed(log, errGetSecretData, err, &externalSecret, syncCallsError.With(resourceLabels))
|
|
|
- return ctrl.Result{}, err
|
|
|
- }
|
|
|
+ // if we got an update conflict, we should requeue immediately
|
|
|
+ if apierrors.IsConflict(updateErr) {
|
|
|
+ log.V(1).Info("conflict while updating status, will requeue")
|
|
|
|
|
|
- // secret data was not modified.
|
|
|
- if errors.Is(err, esv1beta1.NotModifiedErr) {
|
|
|
- log.Info("secret was not modified as a NotModified was returned by the provider")
|
|
|
- r.markAsDone(&externalSecret, start, log)
|
|
|
+ // 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
|
|
|
+ }
|
|
|
|
|
|
- return ctrl.Result{}, nil
|
|
|
+ // 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.
|
|
|
@@ -243,230 +328,392 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
|
|
|
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
|
|
|
- if externalSecret.Spec.Target.CreationPolicy != esv1beta1.CreatePolicyOwner {
|
|
|
- err := fmt.Errorf(errInvalidCreatePolicy, externalSecret.Spec.Target.CreationPolicy)
|
|
|
- r.markAsFailed(log, errDeleteSecret, err, &externalSecret, syncCallsError.With(resourceLabels))
|
|
|
- return ctrl.Result{}, err
|
|
|
+ // 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
|
|
|
}
|
|
|
|
|
|
- if err := r.Delete(ctx, secret); err != nil && !apierrors.IsNotFound(err) {
|
|
|
- r.markAsFailed(log, errDeleteSecret, err, &externalSecret, syncCallsError.With(resourceLabels))
|
|
|
- return ctrl.Result{}, err
|
|
|
+ // 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
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- conditionSynced := NewExternalSecretCondition(esv1beta1.ExternalSecretReady, v1.ConditionTrue, esv1beta1.ConditionReasonSecretDeleted, "secret deleted due to DeletionPolicy")
|
|
|
- SetExternalSecretCondition(&externalSecret, *conditionSynced)
|
|
|
- return ctrl.Result{RequeueAfter: refreshInt}, nil
|
|
|
+ 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)
|
|
|
- return ctrl.Result{RequeueAfter: refreshInt}, nil
|
|
|
+ r.markAsDone(externalSecret, start, log, esv1beta1.ConditionReasonSecretSynced, msgSyncedRetain)
|
|
|
+ return r.getRequeueResult(externalSecret), nil
|
|
|
// noop, handled below
|
|
|
case esv1beta1.DeletionPolicyMerge:
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- mutationFunc := func() error {
|
|
|
+ // 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.ObjectMeta, r.Scheme)
|
|
|
+ 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(errSetCtrlReference, err)
|
|
|
+ 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)
|
|
|
}
|
|
|
- // diff existing keys
|
|
|
- keys, err := getManagedDataKeys(&existingSecret, externalSecret.Name)
|
|
|
+
|
|
|
+ // get the list of keys that are managed by this ExternalSecret
|
|
|
+ keys, err := getManagedDataKeys(secret, externalSecret.Name)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
- // Sanitize data map for any updates on the ES
|
|
|
+
|
|
|
+ // 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 {
|
|
|
- if dataMap[key] == nil {
|
|
|
- secret.Data[key] = nil
|
|
|
- // Sanitizing any templated / updated keys
|
|
|
- delete(secret.Data, key)
|
|
|
- }
|
|
|
+ delete(secret.Data, key)
|
|
|
}
|
|
|
- err = r.applyTemplate(ctx, &externalSecret, secret, dataMap)
|
|
|
+
|
|
|
+ // 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.Annotations[esv1beta1.AnnotationDataHash] = r.computeDataHashAnnotation(&existingSecret, secret)
|
|
|
+ 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:
|
|
|
- err = r.patchSecret(ctx, secret, mutationFunc, &externalSecret)
|
|
|
- if err == nil {
|
|
|
- externalSecret.Status.Binding = v1.LocalObjectReference{Name: secret.Name}
|
|
|
+ // 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:
|
|
|
- var created bool
|
|
|
- created, err = r.createOrUpdateSecret(ctx, secret, mutationFunc, &externalSecret)
|
|
|
- if err == nil {
|
|
|
- externalSecret.Status.Binding = v1.LocalObjectReference{Name: secret.Name}
|
|
|
- }
|
|
|
- // cleanup orphaned secrets
|
|
|
- if created {
|
|
|
- delErr := deleteOrphanedSecrets(ctx, r.Client, &externalSecret)
|
|
|
- if delErr != nil {
|
|
|
- msg := fmt.Sprintf("failed to clean up orphaned secrets: %v", delErr)
|
|
|
- r.markAsFailed(log, msg, delErr, &externalSecret, syncCallsError.With(resourceLabels))
|
|
|
- return ctrl.Result{}, delErr
|
|
|
+ // 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 {
|
|
|
- r.markAsFailed(log, errUpdateSecret, err, &externalSecret, syncCallsError.With(resourceLabels))
|
|
|
+ // 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)
|
|
|
+ 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}
|
|
|
+ }
|
|
|
|
|
|
- return ctrl.Result{
|
|
|
- RequeueAfter: refreshInt,
|
|
|
- }, nil
|
|
|
+ // otherwise, requeue immediately
|
|
|
+ return ctrl.Result{Requeue: true}
|
|
|
}
|
|
|
|
|
|
-func (r *Reconciler) markAsDone(externalSecret *esv1beta1.ExternalSecret, start time.Time, log logr.Logger) {
|
|
|
- conditionSynced := NewExternalSecretCondition(esv1beta1.ExternalSecretReady, v1.ConditionTrue, esv1beta1.ConditionReasonSecretSynced, "Secret was synced")
|
|
|
- currCond := GetExternalSecretCondition(externalSecret.Status, esv1beta1.ExternalSecretReady)
|
|
|
- SetExternalSecretCondition(externalSecret, *conditionSynced)
|
|
|
+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 currCond == nil || currCond.Status != conditionSynced.Status {
|
|
|
- log.Info("reconciled secret") // Log once if on success in any verbosity
|
|
|
+ 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") // Log all reconciliation cycles if higher verbosity applied
|
|
|
+ log.V(1).Info("reconciled secret")
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-func (r *Reconciler) markAsFailed(log logr.Logger, msg string, err error, externalSecret *esv1beta1.ExternalSecret, counter prometheus.Counter) {
|
|
|
- log.Error(err, msg)
|
|
|
+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) error {
|
|
|
- secretList := v1.SecretList{}
|
|
|
- lblValue := utils.ObjectHash(fmt.Sprintf("%v/%v", externalSecret.Namespace, externalSecret.Name))
|
|
|
- ls := &metav1.LabelSelector{
|
|
|
- MatchLabels: map[string]string{
|
|
|
- esv1beta1.LabelOwner: lblValue,
|
|
|
- },
|
|
|
- }
|
|
|
- labelSelector, err := metav1.LabelSelectorAsSelector(ls)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
+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,
|
|
|
}
|
|
|
- err = cl.List(ctx, &secretList, &client.ListOptions{LabelSelector: labelSelector})
|
|
|
- if err != nil {
|
|
|
+ if err := cl.List(ctx, secretListPartial, listOpts); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
- for key, secret := range secretList.Items {
|
|
|
- if externalSecret.Spec.Target.Name != "" && secret.Name != externalSecret.Spec.Target.Name {
|
|
|
- err = cl.Delete(ctx, &secretList.Items[key])
|
|
|
- if err != nil {
|
|
|
+
|
|
|
+ // 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
|
|
|
}
|
|
|
|
|
|
-func (r *Reconciler) createOrUpdateSecret(ctx context.Context, secret *v1.Secret, mutationFunc func() error, es *esv1beta1.ExternalSecret) (bool, error) {
|
|
|
+// 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)
|
|
|
- key := client.ObjectKeyFromObject(secret)
|
|
|
- if err := r.Client.Get(ctx, key, secret); err != nil {
|
|
|
- if !apierrors.IsNotFound(err) {
|
|
|
- return false, err
|
|
|
- }
|
|
|
- if err := mutationFunc(); err != nil {
|
|
|
- return false, err
|
|
|
- }
|
|
|
- // Setting Field Owner even for CreationPolicy==Create
|
|
|
- if err := r.Client.Create(ctx, secret, client.FieldOwner(fqdn)); err != nil {
|
|
|
- return false, err
|
|
|
- }
|
|
|
- r.recorder.Event(es, v1.EventTypeNormal, esv1beta1.ReasonCreated, "Created Secret")
|
|
|
- return true, nil
|
|
|
- }
|
|
|
|
|
|
- existing := secret.DeepCopyObject()
|
|
|
- if err := mutationFunc(); err != nil {
|
|
|
- return false, err
|
|
|
+ // define and mutate the new secret
|
|
|
+ newSecret := &v1.Secret{
|
|
|
+ ObjectMeta: metav1.ObjectMeta{
|
|
|
+ Name: secretName,
|
|
|
+ Namespace: es.Namespace,
|
|
|
+ },
|
|
|
+ Data: make(map[string][]byte),
|
|
|
}
|
|
|
-
|
|
|
- if equality.Semantic.DeepEqual(existing, secret) {
|
|
|
- return false, nil
|
|
|
+ if err := mutationFunc(newSecret); err != nil {
|
|
|
+ return err
|
|
|
}
|
|
|
|
|
|
- if err := r.Client.Update(ctx, secret, client.FieldOwner(fqdn)); err != nil {
|
|
|
- return false, err
|
|
|
+ // note, we set field owner even for Create
|
|
|
+ if err := r.Create(ctx, newSecret, client.FieldOwner(fqdn)); err != nil {
|
|
|
+ return err
|
|
|
}
|
|
|
- r.recorder.Event(es, v1.EventTypeNormal, esv1beta1.ReasonUpdated, "Updated Secret")
|
|
|
- return false, nil
|
|
|
+
|
|
|
+ // 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) patchSecret(ctx context.Context, secret *v1.Secret, mutationFunc func() error, es *esv1beta1.ExternalSecret) error {
|
|
|
+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)
|
|
|
- err := r.Client.Get(ctx, client.ObjectKeyFromObject(secret), secret.DeepCopy())
|
|
|
- if apierrors.IsNotFound(err) {
|
|
|
- return fmt.Errorf(errPolicyMergeNotFound, secret.Name)
|
|
|
- }
|
|
|
- if err != nil {
|
|
|
- return fmt.Errorf(errPolicyMergeGetSecret, secret.Name, err)
|
|
|
- }
|
|
|
|
|
|
- existing := secret.DeepCopyObject()
|
|
|
- if err = mutationFunc(); err != nil {
|
|
|
- return fmt.Errorf(errPolicyMergeMutate, secret.Name, err)
|
|
|
+ // 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)
|
|
|
}
|
|
|
|
|
|
- // GVK is missing in the Secret, see:
|
|
|
- // https://github.com/kubernetes-sigs/controller-runtime/issues/526
|
|
|
- // https://github.com/kubernetes-sigs/controller-runtime/issues/1517
|
|
|
- // https://github.com/kubernetes/kubernetes/issues/80609
|
|
|
- // we need to manually set it before doing a Patch() as it depends on the GVK
|
|
|
- gvks, unversioned, err := r.Scheme.ObjectKinds(secret)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
- if !unversioned && len(gvks) == 1 {
|
|
|
- secret.SetGroupVersionKind(gvks[0])
|
|
|
+ // 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 equality.Semantic.DeepEqual(existing, secret) {
|
|
|
+ // if the secret does not need to be updated, return early
|
|
|
+ if equality.Semantic.DeepEqual(existingSecret, updatedSecret) {
|
|
|
return nil
|
|
|
}
|
|
|
- // Cleaning up Managed fields manually as to keep patch coherence
|
|
|
- secret.ObjectMeta.ManagedFields = nil
|
|
|
- // we're not able to resolve conflicts so we force ownership
|
|
|
- // see: https://kubernetes.io/docs/reference/using-api/server-side-apply/#using-server-side-apply-in-a-controller
|
|
|
- if err := r.Client.Patch(ctx, secret, client.Apply, client.FieldOwner(fqdn), client.ForceOwnership); err != nil {
|
|
|
- return fmt.Errorf(errPolicyMergePatch, secret.Name, err)
|
|
|
+
|
|
|
+ // 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"]
|
|
|
@@ -508,29 +755,31 @@ func getManagedFieldKeys(
|
|
|
return keys, nil
|
|
|
}
|
|
|
|
|
|
-func getResourceVersion(es esv1beta1.ExternalSecret) string {
|
|
|
+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
|
|
|
}
|
|
|
- return utils.ObjectHash(meta{
|
|
|
+ objectMeta := meta{
|
|
|
annotations: m.Annotations,
|
|
|
labels: m.Labels,
|
|
|
- })
|
|
|
+ }
|
|
|
+ return utils.ObjectHash(objectMeta)
|
|
|
}
|
|
|
|
|
|
-func shouldSkipClusterSecretStore(r *Reconciler, es esv1beta1.ExternalSecret) bool {
|
|
|
+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) {
|
|
|
+func shouldSkipUnmanagedStore(ctx context.Context, namespace string, r *Reconciler, es *esv1beta1.ExternalSecret) (bool, error) {
|
|
|
var storeList []esv1beta1.SecretStoreRef
|
|
|
|
|
|
if es.Spec.SecretStoreRef.Name != "" {
|
|
|
@@ -552,6 +801,10 @@ func shouldSkipUnmanagedStore(ctx context.Context, namespace string, r *Reconcil
|
|
|
if ref.SourceRef != nil && ref.SourceRef.GeneratorRef != nil {
|
|
|
_, obj, err := resolvers.GeneratorRef(ctx, r.RestConfig, namespace, ref.SourceRef.GeneratorRef)
|
|
|
if err != nil {
|
|
|
+ if apierrors.IsNotFound(err) {
|
|
|
+ // skip non-existent generators
|
|
|
+ continue
|
|
|
+ }
|
|
|
return false, err
|
|
|
}
|
|
|
skipGenerator, err := shouldSkipGenerator(r, obj)
|
|
|
@@ -575,11 +828,15 @@ func shouldSkipUnmanagedStore(ctx context.Context, namespace string, r *Reconcil
|
|
|
namespace = ""
|
|
|
}
|
|
|
|
|
|
- err := r.Client.Get(ctx, types.NamespacedName{
|
|
|
+ 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
|
|
|
@@ -590,110 +847,162 @@ func shouldSkipUnmanagedStore(ctx context.Context, namespace string, r *Reconcil
|
|
|
return false, nil
|
|
|
}
|
|
|
|
|
|
-func shouldRefresh(es esv1beta1.ExternalSecret) bool {
|
|
|
- // refresh if resource version changed
|
|
|
+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
|
|
|
}
|
|
|
|
|
|
- // skip refresh if refresh interval is 0
|
|
|
- if es.Spec.RefreshInterval.Duration == 0 && es.Status.SyncedResourceVersion != "" {
|
|
|
- return false
|
|
|
- }
|
|
|
+ // if the last refresh time is zero, we should refresh
|
|
|
if es.Status.RefreshTime.IsZero() {
|
|
|
return true
|
|
|
}
|
|
|
- return es.Status.RefreshTime.Add(es.Spec.RefreshInterval.Duration).Before(time.Now())
|
|
|
-}
|
|
|
|
|
|
-func shouldReconcile(es esv1beta1.ExternalSecret) bool {
|
|
|
- if es.Spec.Target.Immutable && hasSyncedCondition(es) {
|
|
|
- return false
|
|
|
+ // if the last refresh time is in the future, we should refresh
|
|
|
+ if es.Status.RefreshTime.Time.After(time.Now()) {
|
|
|
+ return true
|
|
|
}
|
|
|
- return true
|
|
|
-}
|
|
|
|
|
|
-func hasSyncedCondition(es esv1beta1.ExternalSecret) bool {
|
|
|
- for _, condition := range es.Status.Conditions {
|
|
|
- if condition.Reason == "SecretSynced" {
|
|
|
- return true
|
|
|
- }
|
|
|
- }
|
|
|
- return false
|
|
|
+ // 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, or annotations as not set, we need to refresh
|
|
|
- if existingSecret.UID == "" || existingSecret.Annotations == nil {
|
|
|
+func isSecretValid(existingSecret *v1.Secret) bool {
|
|
|
+ // if target secret doesn't exist, we need to refresh
|
|
|
+ if existingSecret.UID == "" {
|
|
|
return false
|
|
|
}
|
|
|
|
|
|
- // if the calculated hash is different from the calculation, then it's invalid
|
|
|
- if existingSecret.Annotations[esv1beta1.AnnotationDataHash] != utils.ObjectHash(existingSecret.Data) {
|
|
|
+ // if the managed label is missing or incorrect, then it's invalid
|
|
|
+ if existingSecret.Labels[esv1beta1.LabelManaged] != esv1beta1.LabelManagedValue {
|
|
|
return false
|
|
|
}
|
|
|
- return true
|
|
|
-}
|
|
|
|
|
|
-// computeDataHashAnnotation generate a hash of the secret data combining the old key with the new keys to add or override.
|
|
|
-func (r *Reconciler) computeDataHashAnnotation(existing, secret *v1.Secret) string {
|
|
|
- data := make(map[string][]byte)
|
|
|
- maps.Insert(data, maps.All(existing.Data))
|
|
|
- maps.Insert(data, maps.All(secret.Data))
|
|
|
+ // 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 utils.ObjectHash(data)
|
|
|
+ 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 .Spec.Target.Name to reconcile ExternalSecrets effectively when secrets have changed
|
|
|
- if err := mgr.GetFieldIndexer().IndexField(context.Background(), &esv1beta1.ExternalSecret{}, externalSecretSecretNameKey, func(obj client.Object) []string {
|
|
|
+ // 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 name := es.Spec.Target.Name; name != "" {
|
|
|
- return []string{name}
|
|
|
+ // 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{}).
|
|
|
- // Cannot use Owns since the controller does not set owner reference when creation policy is not Owner
|
|
|
- Watches(
|
|
|
+ // 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{}),
|
|
|
- builder.OnlyMetadata,
|
|
|
+ builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}, secretHasESLabel),
|
|
|
).
|
|
|
Complete(r)
|
|
|
}
|
|
|
|
|
|
func (r *Reconciler) findObjectsForSecret(ctx context.Context, secret client.Object) []reconcile.Request {
|
|
|
- var externalSecrets esv1beta1.ExternalSecretList
|
|
|
- err := r.List(
|
|
|
- ctx,
|
|
|
- &externalSecrets,
|
|
|
- client.InNamespace(secret.GetNamespace()),
|
|
|
- client.MatchingFields{externalSecretSecretNameKey: secret.GetName()},
|
|
|
- )
|
|
|
+ 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(externalSecrets.Items))
|
|
|
- for i := range externalSecrets.Items {
|
|
|
+ requests := make([]reconcile.Request, len(externalSecretsList.Items))
|
|
|
+ for i, item := range externalSecretsList.Items {
|
|
|
requests[i] = reconcile.Request{
|
|
|
NamespacedName: types.NamespacedName{
|
|
|
- Name: externalSecrets.Items[i].GetName(),
|
|
|
- Namespace: externalSecrets.Items[i].GetNamespace(),
|
|
|
+ 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
|
|
|
+}
|