1
0
Fork 0
mirror of https://github.com/external-secrets/external-secrets.git synced 2024-12-14 11:57:59 +00:00

feat: significantly reduce api calls and introduce partial secret cache (#4086)

* feat: reduce api calls and introduce partial secret cache

Signed-off-by: Mathew Wicks <5735406+thesuperzapper@users.noreply.github.com>

* updates from review 1

Signed-off-by: Mathew Wicks <5735406+thesuperzapper@users.noreply.github.com>

* updates from review 2

Signed-off-by: Mathew Wicks <5735406+thesuperzapper@users.noreply.github.com>

* fix updating CreationPolicy after secret creation

Signed-off-by: Mathew Wicks <5735406+thesuperzapper@users.noreply.github.com>

* updates from review 3

Signed-off-by: Mathew Wicks <5735406+thesuperzapper@users.noreply.github.com>

* prevent loop when two ES claim Owner on the same target secret

Signed-off-by: Mathew Wicks <5735406+thesuperzapper@users.noreply.github.com>

* updates from review 4

Signed-off-by: Mathew Wicks <5735406+thesuperzapper@users.noreply.github.com>

* fix ClusterSecretStore not ready message

Signed-off-by: Mathew Wicks <5735406+thesuperzapper@users.noreply.github.com>

---------

Signed-off-by: Mathew Wicks <5735406+thesuperzapper@users.noreply.github.com>
Co-authored-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
This commit is contained in:
Mathew Wicks 2024-11-24 13:53:53 -08:00 committed by GitHub
parent bea0fb6361
commit ac26166ac9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 791 additions and 449 deletions

View file

@ -427,6 +427,8 @@ const (
ConditionReasonSecretSyncedError = "SecretSyncedError" ConditionReasonSecretSyncedError = "SecretSyncedError"
// ConditionReasonSecretDeleted indicates that the secret has been deleted. // ConditionReasonSecretDeleted indicates that the secret has been deleted.
ConditionReasonSecretDeleted = "SecretDeleted" ConditionReasonSecretDeleted = "SecretDeleted"
// ConditionReasonSecretMissing indicates that the secret is missing.
ConditionReasonSecretMissing = "SecretMissing"
ReasonUpdateFailed = "UpdateFailed" ReasonUpdateFailed = "UpdateFailed"
ReasonDeprecated = "ParameterDeprecated" ReasonDeprecated = "ParameterDeprecated"
@ -470,10 +472,14 @@ type ExternalSecret struct {
} }
const ( const (
// AnnotationDataHash is used to ensure consistency. // AnnotationDataHash all secrets managed by an ExternalSecret have this annotation with the hash of their data.
AnnotationDataHash = "reconcile.external-secrets.io/data-hash" AnnotationDataHash = "reconcile.external-secrets.io/data-hash"
// LabelOwner points to the owning ExternalSecret resource
// and is used to manage the lifecycle of a Secret // LabelManaged all secrets managed by an ExternalSecret will have this label equal to "true".
LabelManaged = "reconcile.external-secrets.io/managed"
LabelManagedValue = "true"
// LabelOwner points to the owning ExternalSecret resource when CreationPolicy=Owner.
LabelOwner = "reconcile.external-secrets.io/created-by" LabelOwner = "reconcile.external-secrets.io/created-by"
) )

View file

@ -64,19 +64,27 @@ var certcontrollerCmd = &cobra.Command{
logger := zap.New(zap.UseFlagOptions(&opts)) logger := zap.New(zap.UseFlagOptions(&opts))
ctrl.SetLogger(logger) ctrl.SetLogger(logger)
cacheOptions := cache.Options{} // completely disable caching of Secrets and ConfigMaps to save memory
// see: https://github.com/external-secrets/external-secrets/issues/721
clientCacheDisableFor := make([]client.Object, 0)
clientCacheDisableFor = append(clientCacheDisableFor, &v1.Secret{}, &v1.ConfigMap{})
// in large clusters, the CRDs and ValidatingWebhookConfigurations can take up a lot of memory
// see: https://github.com/external-secrets/external-secrets/pull/3588
cacheByObject := make(map[client.Object]cache.ByObject)
if enablePartialCache { if enablePartialCache {
cacheOptions.ByObject = map[client.Object]cache.ByObject{ // only cache ValidatingWebhookConfiguration with "external-secrets.io/component=webhook" label
&admissionregistration.ValidatingWebhookConfiguration{}: { cacheByObject[&admissionregistration.ValidatingWebhookConfiguration{}] = cache.ByObject{
Label: labels.SelectorFromSet(map[string]string{ Label: labels.SelectorFromSet(labels.Set{
constants.WellKnownLabelKey: constants.WellKnownLabelValueWebhook, constants.WellKnownLabelKey: constants.WellKnownLabelValueWebhook,
}), }),
}, }
&apiextensions.CustomResourceDefinition{}: {
Label: labels.SelectorFromSet(map[string]string{ // only cache CustomResourceDefinition with "external-secrets.io/component=controller" label
constants.WellKnownLabelKey: constants.WellKnownLabelValueController, cacheByObject[&apiextensions.CustomResourceDefinition{}] = cache.ByObject{
}), Label: labels.SelectorFromSet(labels.Set{
}, constants.WellKnownLabelKey: constants.WellKnownLabelValueController,
}),
} }
} }
@ -91,18 +99,12 @@ var certcontrollerCmd = &cobra.Command{
HealthProbeBindAddress: healthzAddr, HealthProbeBindAddress: healthzAddr,
LeaderElection: enableLeaderElection, LeaderElection: enableLeaderElection,
LeaderElectionID: "crd-certs-controller", LeaderElectionID: "crd-certs-controller",
Cache: cacheOptions, Cache: cache.Options{
ByObject: cacheByObject,
},
Client: client.Options{ Client: client.Options{
Cache: &client.CacheOptions{ Cache: &client.CacheOptions{
DisableFor: []client.Object{ DisableFor: clientCacheDisableFor,
// the client creates a ListWatch for all resource kinds that
// are requested with .Get().
// We want to avoid to cache all secrets or configmaps in memory.
// The ES controller uses v1.PartialObjectMetadata for the secrets
// that he owns.
// see #721
&v1.Secret{},
},
}, },
}, },
}) })

View file

@ -64,6 +64,7 @@ var (
enableLeaderElection bool enableLeaderElection bool
enableSecretsCache bool enableSecretsCache bool
enableConfigMapsCache bool enableConfigMapsCache bool
enableManagedSecretsCache bool
enablePartialCache bool enablePartialCache bool
concurrent int concurrent int
port int port int
@ -107,19 +108,6 @@ var rootCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
var lvl zapcore.Level var lvl zapcore.Level
var enc zapcore.TimeEncoder var enc zapcore.TimeEncoder
// the client creates a ListWatch for all resource kinds that
// are requested with .Get().
// We want to avoid to cache all secrets or configmaps in memory.
// The ES controller uses v1.PartialObjectMetadata for the secrets
// that he owns.
// see #721
cacheList := make([]client.Object, 0)
if !enableSecretsCache {
cacheList = append(cacheList, &v1.Secret{})
}
if !enableConfigMapsCache {
cacheList = append(cacheList, &v1.ConfigMap{})
}
lvlErr := lvl.UnmarshalText([]byte(loglevel)) lvlErr := lvl.UnmarshalText([]byte(loglevel))
if lvlErr != nil { if lvlErr != nil {
setupLog.Error(lvlErr, "error unmarshalling loglevel") setupLog.Error(lvlErr, "error unmarshalling loglevel")
@ -141,6 +129,21 @@ var rootCmd = &cobra.Command{
config := ctrl.GetConfigOrDie() config := ctrl.GetConfigOrDie()
config.QPS = clientQPS config.QPS = clientQPS
config.Burst = clientBurst config.Burst = clientBurst
// the client creates a ListWatch for resources that are requested with .Get() or .List()
// some users might want to completely disable caching of Secrets and ConfigMaps
// to decrease memory usage at the expense of high Kubernetes API usage
// see: https://github.com/external-secrets/external-secrets/issues/721
clientCacheDisableFor := make([]client.Object, 0)
if !enableSecretsCache {
// dont cache any secrets
clientCacheDisableFor = append(clientCacheDisableFor, &v1.Secret{})
}
if !enableConfigMapsCache {
// dont cache any configmaps
clientCacheDisableFor = append(clientCacheDisableFor, &v1.ConfigMap{})
}
ctrlOpts := ctrl.Options{ ctrlOpts := ctrl.Options{
Scheme: scheme, Scheme: scheme,
Metrics: server.Options{ Metrics: server.Options{
@ -151,7 +154,7 @@ var rootCmd = &cobra.Command{
}), }),
Client: client.Options{ Client: client.Options{
Cache: &client.CacheOptions{ Cache: &client.CacheOptions{
DisableFor: cacheList, DisableFor: clientCacheDisableFor,
}, },
}, },
LeaderElection: enableLeaderElection, LeaderElection: enableLeaderElection,
@ -168,6 +171,19 @@ var rootCmd = &cobra.Command{
os.Exit(1) os.Exit(1)
} }
// we create a special client for accessing secrets in the ExternalSecret reconcile loop.
// by default, it is the same as the normal client, but if `--enable-managed-secrets-caching`
// is set, we use a special client that only caches secrets managed by an ExternalSecret.
// if we are already caching all secrets, we don't need to use the special client.
secretClient := mgr.GetClient()
if enableManagedSecretsCache && !enableSecretsCache {
secretClient, err = externalsecret.BuildManagedSecretClient(mgr)
if err != nil {
setupLog.Error(err, "unable to create managed secret client")
os.Exit(1)
}
}
ssmetrics.SetUpMetrics() ssmetrics.SetUpMetrics()
if err = (&secretstore.StoreReconciler{ if err = (&secretstore.StoreReconciler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
@ -198,6 +214,7 @@ var rootCmd = &cobra.Command{
} }
if err = (&externalsecret.Reconciler{ if err = (&externalsecret.Reconciler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
SecretClient: secretClient,
Log: ctrl.Log.WithName("controllers").WithName("ExternalSecret"), Log: ctrl.Log.WithName("controllers").WithName("ExternalSecret"),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
RestConfig: mgr.GetConfig(), RestConfig: mgr.GetConfig(),
@ -277,8 +294,9 @@ func init() {
rootCmd.Flags().BoolVar(&enableClusterStoreReconciler, "enable-cluster-store-reconciler", true, "Enable cluster store reconciler.") rootCmd.Flags().BoolVar(&enableClusterStoreReconciler, "enable-cluster-store-reconciler", true, "Enable cluster store reconciler.")
rootCmd.Flags().BoolVar(&enableClusterExternalSecretReconciler, "enable-cluster-external-secret-reconciler", true, "Enable cluster external secret reconciler.") rootCmd.Flags().BoolVar(&enableClusterExternalSecretReconciler, "enable-cluster-external-secret-reconciler", true, "Enable cluster external secret reconciler.")
rootCmd.Flags().BoolVar(&enablePushSecretReconciler, "enable-push-secret-reconciler", true, "Enable push secret reconciler.") rootCmd.Flags().BoolVar(&enablePushSecretReconciler, "enable-push-secret-reconciler", true, "Enable push secret reconciler.")
rootCmd.Flags().BoolVar(&enableSecretsCache, "enable-secrets-caching", false, "Enable secrets caching for external-secrets pod.") rootCmd.Flags().BoolVar(&enableSecretsCache, "enable-secrets-caching", false, "Enable secrets caching for ALL secrets in the cluster (WARNING: can increase memory usage).")
rootCmd.Flags().BoolVar(&enableConfigMapsCache, "enable-configmaps-caching", false, "Enable secrets caching for external-secrets pod.") rootCmd.Flags().BoolVar(&enableConfigMapsCache, "enable-configmaps-caching", false, "Enable configmaps caching for ALL configmaps in the cluster (WARNING: can increase memory usage).")
rootCmd.Flags().BoolVar(&enableManagedSecretsCache, "enable-managed-secrets-caching", true, "Enable secrets caching for secrets managed by an ExternalSecret")
rootCmd.Flags().DurationVar(&storeRequeueInterval, "store-requeue-interval", time.Minute*5, "Default Time duration between reconciling (Cluster)SecretStores") rootCmd.Flags().DurationVar(&storeRequeueInterval, "store-requeue-interval", time.Minute*5, "Default Time duration between reconciling (Cluster)SecretStores")
rootCmd.Flags().BoolVar(&enableFloodGate, "enable-flood-gate", true, "Enable flood gate. External secret will be reconciled only if the ClusterStore or Store have an healthy or unknown state.") rootCmd.Flags().BoolVar(&enableFloodGate, "enable-flood-gate", true, "Enable flood gate. External secret will be reconciled only if the ClusterStore or Store have an healthy or unknown state.")
rootCmd.Flags().BoolVar(&enableExtendedMetricLabels, "enable-extended-metric-labels", false, "Enable recommended kubernetes annotations as labels in metrics.") rootCmd.Flags().BoolVar(&enableExtendedMetricLabels, "enable-extended-metric-labels", false, "Enable recommended kubernetes annotations as labels in metrics.")

View file

@ -12,7 +12,7 @@ The external-secrets binary includes three components: `core controller`, `certc
The core controller is invoked without a subcommand and can be configured with the following flags: The core controller is invoked without a subcommand and can be configured with the following flags:
| Name | Type | Default | Description | | Name | Type | Default | Description |
| --------------------------------------------- | -------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | |-----------------------------------------------|----------|-------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `--client-burst` | int | uses rest client default (10) | Maximum Burst allowed to be passed to rest.Client | | `--client-burst` | int | uses rest client default (10) | Maximum Burst allowed to be passed to rest.Client |
| `--client-qps` | float32 | uses rest client default (5) | QPS configuration to be passed to rest.Client | | `--client-qps` | float32 | uses rest client default (5) | QPS configuration to be passed to rest.Client |
| `--concurrent` | int | 1 | The number of concurrent reconciles. | | `--concurrent` | int | 1 | The number of concurrent reconciles. |
@ -20,15 +20,16 @@ The core controller is invoked without a subcommand and can be configured with t
| `--enable-cluster-external-secret-reconciler` | boolean | true | Enables the cluster external secret reconciler. | | `--enable-cluster-external-secret-reconciler` | boolean | true | Enables the cluster external secret reconciler. |
| `--enable-cluster-store-reconciler` | boolean | true | Enables the cluster store reconciler. | | `--enable-cluster-store-reconciler` | boolean | true | Enables the cluster store reconciler. |
| `--enable-push-secret-reconciler` | boolean | true | Enables the push secret reconciler. | | `--enable-push-secret-reconciler` | boolean | true | Enables the push secret reconciler. |
| `--enable-secrets-caching` | boolean | false | Enables the secrets caching for external-secrets pod. | | `--enable-secrets-caching` | boolean | false | Enable secrets caching for ALL secrets in the cluster (WARNING: can increase memory usage). |
| `--enable-configmaps-caching` | boolean | false | Enables the ConfigMap caching for external-secrets pod. | | `--enable-configmaps-caching` | boolean | false | Enable configmaps caching for ALL configmaps in the cluster (WARNING: can increase memory usage). |
| `--enable-managed-secrets-caching` | boolean | true | Enable secrets caching for secrets managed by an ExternalSecret. |
| `--enable-flood-gate` | boolean | true | Enable flood gate. External secret will be reconciled only if the ClusterStore or Store have an healthy or unknown state. | | `--enable-flood-gate` | boolean | true | Enable flood gate. External secret will be reconciled only if the ClusterStore or Store have an healthy or unknown state. |
| `--enable-extended-metric-labels` | boolean | true | Enable recommended kubernetes annotations as labels in metrics. | | `--enable-extended-metric-labels` | boolean | true | Enable recommended kubernetes annotations as labels in metrics. |
| `--enable-leader-election` | boolean | false | Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager. | | `--enable-leader-election` | boolean | false | Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager. |
| `--experimental-enable-aws-session-cache` | boolean | false | Enable experimental AWS session cache. External secret will reuse the AWS session without creating a new one on each request. | | `--experimental-enable-aws-session-cache` | boolean | false | Enable experimental AWS session cache. External secret will reuse the AWS session without creating a new one on each request. |
| `--help` | | | help for external-secrets | | `--help` | | | help for external-secrets |
| `--loglevel` | string | info | loglevel to use, one of: debug, info, warn, error, dpanic, panic, fatal | | `--loglevel` | string | info | loglevel to use, one of: debug, info, warn, error, dpanic, panic, fatal |
| `--zap-time-encoding` | string | epoch | loglevel to use, one of: epoch, millis, nano, iso8601, rfc3339, rfc3339nano | | `--zap-time-encoding` | string | epoch | loglevel to use, one of: epoch, millis, nano, iso8601, rfc3339, rfc3339nano |
| `--metrics-addr` | string | :8080 | The address the metric endpoint binds to. | | `--metrics-addr` | string | :8080 | The address the metric endpoint binds to. |
| `--namespace` | string | - | watch external secrets scoped in the provided namespace only. ClusterSecretStore can be used but only work if it doesn't reference resources from other namespaces | | `--namespace` | string | - | watch external secrets scoped in the provided namespace only. ClusterSecretStore can be used but only work if it doesn't reference resources from other namespaces |
| `--store-requeue-interval` | duration | 5m0s | Default Time duration between reconciling (Cluster)SecretStores | | `--store-requeue-interval` | duration | 5m0s | Default Time duration between reconciling (Cluster)SecretStores |

View file

@ -105,8 +105,9 @@ func equalSecrets(exp, ts *v1.Secret) bool {
return false return false
} }
// secret contains label owner which must be ignored // secret contains labels which must be ignored
delete(ts.ObjectMeta.Labels, esv1beta1.LabelOwner) delete(ts.ObjectMeta.Labels, esv1beta1.LabelOwner)
delete(ts.ObjectMeta.Labels, esv1beta1.LabelManaged)
if len(ts.ObjectMeta.Labels) == 0 { if len(ts.ObjectMeta.Labels) == 0 {
ts.ObjectMeta.Labels = nil ts.ObjectMeta.Labels = nil
} }

View file

@ -19,7 +19,6 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/google/go-cmp/cmp"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
@ -62,14 +61,12 @@ func HasOwnerRef(meta metav1.ObjectMeta, kind, name string) bool {
return false return false
} }
func HasFieldOwnership(meta metav1.ObjectMeta, mgr, expected string) string { // FirstManagedFieldForManager returns the JSON representation of the first `metadata.managedFields` entry for a given manager.
func FirstManagedFieldForManager(meta metav1.ObjectMeta, managerName string) string {
for _, ref := range meta.ManagedFields { for _, ref := range meta.ManagedFields {
if ref.Manager == mgr { if ref.Manager == managerName {
if diff := cmp.Diff(string(ref.FieldsV1.Raw), expected); diff != "" { return ref.FieldsV1.String()
return fmt.Sprintf("(-got, +want)\n%s", diff)
}
return ""
} }
} }
return fmt.Sprintf("No managed fields managed by %s", mgr) return fmt.Sprintf("No managed fields managed by %s", managerName)
} }

File diff suppressed because it is too large Load diff

View file

@ -31,22 +31,37 @@ import (
// merge template in the following order: // merge template in the following order:
// * template.Data (highest precedence) // * template.Data (highest precedence)
// * template.templateFrom // * template.TemplateFrom
// * secret via es.data or es.dataFrom. // * secret via es.data or es.dataFrom (if template.MergePolicy is Merge, or there is no template)
// * existing secret keys (if CreationPolicy is Merge).
func (r *Reconciler) applyTemplate(ctx context.Context, es *esv1beta1.ExternalSecret, secret *v1.Secret, dataMap map[string][]byte) error { func (r *Reconciler) applyTemplate(ctx context.Context, es *esv1beta1.ExternalSecret, secret *v1.Secret, dataMap map[string][]byte) error {
// update metadata (labels, annotations) of the secret
if err := setMetadata(secret, es); err != nil { if err := setMetadata(secret, es); err != nil {
return err return err
} }
// we only keep existing keys if creation policy is Merge, otherwise we clear the secret
if es.Spec.Target.CreationPolicy != esv1beta1.CreatePolicyMerge {
secret.Data = make(map[string][]byte)
}
// no template: copy data and return // no template: copy data and return
if es.Spec.Target.Template == nil { if es.Spec.Target.Template == nil {
secret.Data = dataMap maps.Insert(secret.Data, maps.All(dataMap))
return nil return nil
} }
// Merge Policy should merge secrets
if es.Spec.Target.Template.MergePolicy == esv1beta1.MergePolicyMerge { // set the secret type if it is defined in the template, otherwise keep the existing type
if es.Spec.Target.Template.Type != "" {
secret.Type = es.Spec.Target.Template.Type
}
// when TemplateMergePolicy is Merge, or there is no data template, we include the keys from `dataMap`
noTemplate := len(es.Spec.Target.Template.Data) == 0 && len(es.Spec.Target.Template.TemplateFrom) == 0
if es.Spec.Target.Template.MergePolicy == esv1beta1.MergePolicyMerge || noTemplate {
maps.Insert(secret.Data, maps.All(dataMap)) maps.Insert(secret.Data, maps.All(dataMap))
} }
execute, err := template.EngineForVersion(es.Spec.Target.Template.EngineVersion) execute, err := template.EngineForVersion(es.Spec.Target.Template.EngineVersion)
if err != nil { if err != nil {
return err return err
@ -58,6 +73,7 @@ func (r *Reconciler) applyTemplate(ctx context.Context, es *esv1beta1.ExternalSe
DataMap: dataMap, DataMap: dataMap,
Exec: execute, Exec: execute,
} }
// apply templates defined in template.templateFrom // apply templates defined in template.templateFrom
err = p.MergeTemplateFrom(ctx, es.Namespace, es.Spec.Target.Template) err = p.MergeTemplateFrom(ctx, es.Namespace, es.Spec.Target.Template)
if err != nil { if err != nil {
@ -79,24 +95,23 @@ func (r *Reconciler) applyTemplate(ctx context.Context, es *esv1beta1.ExternalSe
if err != nil { if err != nil {
return fmt.Errorf(errExecTpl, err) return fmt.Errorf(errExecTpl, err)
} }
// if no data was provided by template fallback
// to value from the provider
if len(es.Spec.Target.Template.Data) == 0 && len(es.Spec.Target.Template.TemplateFrom) == 0 {
secret.Data = dataMap
}
return nil return nil
} }
// setMetadata sets Labels and Annotations to the given secret. // setMetadata sets Labels and Annotations to the given secret.
func setMetadata(secret *v1.Secret, es *esv1beta1.ExternalSecret) error { func setMetadata(secret *v1.Secret, es *esv1beta1.ExternalSecret) error {
// ensure that Labels and Annotations are not nil
// so it is safe to merge them
if secret.Labels == nil { if secret.Labels == nil {
secret.Labels = make(map[string]string) secret.Labels = make(map[string]string)
} }
if secret.Annotations == nil { if secret.Annotations == nil {
secret.Annotations = make(map[string]string) secret.Annotations = make(map[string]string)
} }
// Clean up Labels and Annotations added by the operator
// so that it won't leave outdated ones // remove any existing labels managed by this external secret
// this is to ensure that we don't have any stale labels
labelKeys, err := templating.GetManagedLabelKeys(secret, es.Name) labelKeys, err := templating.GetManagedLabelKeys(secret, es.Name)
if err != nil { if err != nil {
return err return err
@ -104,7 +119,6 @@ func setMetadata(secret *v1.Secret, es *esv1beta1.ExternalSecret) error {
for _, key := range labelKeys { for _, key := range labelKeys {
delete(secret.ObjectMeta.Labels, key) delete(secret.ObjectMeta.Labels, key)
} }
annotationKeys, err := templating.GetManagedAnnotationKeys(secret, es.Name) annotationKeys, err := templating.GetManagedAnnotationKeys(secret, es.Name)
if err != nil { if err != nil {
return err return err
@ -113,13 +127,14 @@ func setMetadata(secret *v1.Secret, es *esv1beta1.ExternalSecret) error {
delete(secret.ObjectMeta.Annotations, key) delete(secret.ObjectMeta.Annotations, key)
} }
// if no template is defined, copy labels and annotations from the ExternalSecret
if es.Spec.Target.Template == nil { if es.Spec.Target.Template == nil {
utils.MergeStringMap(secret.ObjectMeta.Labels, es.ObjectMeta.Labels) utils.MergeStringMap(secret.ObjectMeta.Labels, es.ObjectMeta.Labels)
utils.MergeStringMap(secret.ObjectMeta.Annotations, es.ObjectMeta.Annotations) utils.MergeStringMap(secret.ObjectMeta.Annotations, es.ObjectMeta.Annotations)
return nil return nil
} }
secret.Type = es.Spec.Target.Template.Type // copy labels and annotations from the template
utils.MergeStringMap(secret.ObjectMeta.Labels, es.Spec.Target.Template.Metadata.Labels) utils.MergeStringMap(secret.ObjectMeta.Labels, es.Spec.Target.Template.Metadata.Labels)
utils.MergeStringMap(secret.ObjectMeta.Annotations, es.Spec.Target.Template.Metadata.Annotations) utils.MergeStringMap(secret.ObjectMeta.Annotations, es.Spec.Target.Template.Metadata.Annotations)
return nil return nil

View file

@ -26,6 +26,7 @@ import (
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-cmp/cmp/cmpopts"
"github.com/onsi/gomega/format"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go" dto "github.com/prometheus/client_model/go"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
@ -89,30 +90,23 @@ var _ = Describe("Kind=secret existence logic", func() {
} }
type testCase struct { type testCase struct {
Name string Name string
Input v1.Secret Input *v1.Secret
ExpectedOutput bool ExpectedOutput bool
} }
tests := []testCase{ tests := []testCase{
{ {
Name: "Should not be valid in case of missing uid", Name: "Should not be valid in case of missing uid",
Input: v1.Secret{}, Input: &v1.Secret{},
ExpectedOutput: false, ExpectedOutput: false,
}, },
{ {
Name: "A nil annotation should not be valid", Name: "A nil annotation should not be valid",
Input: v1.Secret{ Input: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
UID: "xxx", UID: "xxx",
Annotations: map[string]string{}, Labels: map[string]string{
}, esv1beta1.LabelManaged: esv1beta1.LabelManagedValue,
}, },
ExpectedOutput: false,
},
{
Name: "A nil annotation should not be valid",
Input: v1.Secret{
ObjectMeta: metav1.ObjectMeta{
UID: "xxx",
Annotations: map[string]string{}, Annotations: map[string]string{},
}, },
}, },
@ -120,9 +114,12 @@ var _ = Describe("Kind=secret existence logic", func() {
}, },
{ {
Name: "An invalid annotation hash should not be valid", Name: "An invalid annotation hash should not be valid",
Input: v1.Secret{ Input: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
UID: "xxx", UID: "xxx",
Labels: map[string]string{
esv1beta1.LabelManaged: esv1beta1.LabelManagedValue,
},
Annotations: map[string]string{ Annotations: map[string]string{
esv1beta1.AnnotationDataHash: "xxxxxx", esv1beta1.AnnotationDataHash: "xxxxxx",
}, },
@ -131,10 +128,13 @@ var _ = Describe("Kind=secret existence logic", func() {
ExpectedOutput: false, ExpectedOutput: false,
}, },
{ {
Name: "A valid config map should return true", Name: "A valid secret should return true",
Input: v1.Secret{ Input: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
UID: "xxx", UID: "xxx",
Labels: map[string]string{
esv1beta1.LabelManaged: esv1beta1.LabelManagedValue,
},
Annotations: map[string]string{ Annotations: map[string]string{
esv1beta1.AnnotationDataHash: utils.ObjectHash(validData), esv1beta1.AnnotationDataHash: utils.ObjectHash(validData),
}, },
@ -449,9 +449,10 @@ var _ = Describe("ExternalSecret controller", Serial, func() {
Expect(string(secret.Data[existingKey])).To(Equal(existingVal)) Expect(string(secret.Data[existingKey])).To(Equal(existingVal))
Expect(string(secret.Data[targetProp])).To(Equal(secretVal)) Expect(string(secret.Data[targetProp])).To(Equal(secretVal))
Expect(secret.ObjectMeta.Labels).To(HaveLen(2)) Expect(secret.ObjectMeta.Labels).To(HaveLen(3))
Expect(secret.ObjectMeta.Labels).To(HaveKeyWithValue("existing-label-key", "existing-label-value")) Expect(secret.ObjectMeta.Labels).To(HaveKeyWithValue("existing-label-key", "existing-label-value"))
Expect(secret.ObjectMeta.Labels).To(HaveKeyWithValue("es-label-key", "es-label-value")) Expect(secret.ObjectMeta.Labels).To(HaveKeyWithValue("es-label-key", "es-label-value"))
Expect(secret.ObjectMeta.Labels).To(HaveKeyWithValue(esv1beta1.LabelManaged, esv1beta1.LabelManagedValue))
Expect(secret.ObjectMeta.Annotations).To(HaveLen(3)) Expect(secret.ObjectMeta.Annotations).To(HaveLen(3))
Expect(secret.ObjectMeta.Annotations).To(HaveKeyWithValue("existing-annotation-key", "existing-annotation-value")) Expect(secret.ObjectMeta.Annotations).To(HaveKeyWithValue("existing-annotation-key", "existing-annotation-value"))
@ -460,12 +461,15 @@ var _ = Describe("ExternalSecret controller", Serial, func() {
Expect(ctest.HasOwnerRef(secret.ObjectMeta, "ExternalSecret", ExternalSecretFQDN)).To(BeFalse()) Expect(ctest.HasOwnerRef(secret.ObjectMeta, "ExternalSecret", ExternalSecretFQDN)).To(BeFalse())
Expect(secret.ObjectMeta.ManagedFields).To(HaveLen(2)) Expect(secret.ObjectMeta.ManagedFields).To(HaveLen(2))
Expect(ctest.HasFieldOwnership( oldCharactersAroundMismatchToInclude := format.CharactersAroundMismatchToInclude
secret.ObjectMeta, format.CharactersAroundMismatchToInclude = 10
ExternalSecretFQDN, Expect(ctest.FirstManagedFieldForManager(secret.ObjectMeta, ExternalSecretFQDN)).To(
fmt.Sprintf(`{"f:data":{"f:targetProperty":{}},"f:immutable":{},"f:metadata":{"f:annotations":{"f:es-annotation-key":{},"f:%s":{}},"f:labels":{"f:es-label-key":{}}}}`, esv1beta1.AnnotationDataHash)), Equal(fmt.Sprintf(`{"f:data":{"f:targetProperty":{}},"f:metadata":{"f:annotations":{"f:es-annotation-key":{},"f:%s":{}},"f:labels":{"f:es-label-key":{},"f:%s":{}}}}`, esv1beta1.AnnotationDataHash, esv1beta1.LabelManaged)),
).To(BeEmpty()) )
Expect(ctest.HasFieldOwnership(secret.ObjectMeta, FakeManager, `{"f:data":{".":{},"f:pre-existing-key":{}},"f:metadata":{"f:annotations":{".":{},"f:existing-annotation-key":{}},"f:labels":{".":{},"f:existing-label-key":{}}},"f:type":{}}`)).To(BeEmpty()) Expect(ctest.FirstManagedFieldForManager(secret.ObjectMeta, FakeManager)).To(
Equal(`{"f:data":{".":{},"f:pre-existing-key":{}},"f:metadata":{"f:annotations":{".":{},"f:existing-annotation-key":{}},"f:labels":{".":{},"f:existing-label-key":{}}},"f:type":{}}`),
)
format.CharactersAroundMismatchToInclude = oldCharactersAroundMismatchToInclude
} }
} }
@ -548,8 +552,18 @@ var _ = Describe("ExternalSecret controller", Serial, func() {
fakeProvider.WithGetSecret([]byte(secretVal), nil) fakeProvider.WithGetSecret([]byte(secretVal), nil)
tc.checkCondition = func(es *esv1beta1.ExternalSecret) bool { tc.checkCondition = func(es *esv1beta1.ExternalSecret) bool {
cond := GetExternalSecretCondition(es.Status, esv1beta1.ExternalSecretReady) expected := []esv1beta1.ExternalSecretStatusCondition{
if cond == nil || cond.Status != v1.ConditionFalse || cond.Reason != esv1beta1.ConditionReasonSecretSyncedError { {
Type: esv1beta1.ExternalSecretReady,
Status: v1.ConditionTrue,
Reason: esv1beta1.ConditionReasonSecretMissing,
Message: msgMissing,
},
}
opts := cmpopts.IgnoreFields(esv1beta1.ExternalSecretStatusCondition{}, "LastTransitionTime")
if diff := cmp.Diff(expected, es.Status.Conditions, opts); diff != "" {
GinkgoLogr.Info("(-got, +want)\n%s", "diff", diff)
return false return false
} }
return true return true
@ -558,10 +572,10 @@ var _ = Describe("ExternalSecret controller", Serial, func() {
Eventually(func() bool { Eventually(func() bool {
Expect(testSyncCallsError.WithLabelValues(ExternalSecretName, ExternalSecretNamespace).Write(&metric)).To(Succeed()) Expect(testSyncCallsError.WithLabelValues(ExternalSecretName, ExternalSecretNamespace).Write(&metric)).To(Succeed())
Expect(testExternalSecretReconcileDuration.WithLabelValues(ExternalSecretName, ExternalSecretNamespace).Write(&metricDuration)).To(Succeed()) Expect(testExternalSecretReconcileDuration.WithLabelValues(ExternalSecretName, ExternalSecretNamespace).Write(&metricDuration)).To(Succeed())
return metric.GetCounter().GetValue() >= 2.0 && metricDuration.GetGauge().GetValue() > 0.0 return metric.GetCounter().GetValue() == 0 && metricDuration.GetGauge().GetValue() > 0.0
}, timeout, interval).Should(BeTrue()) }, timeout, interval).Should(BeTrue())
Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1beta1.ExternalSecretReady, v1.ConditionFalse, 1.0)).To(BeTrue()) Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1beta1.ExternalSecretReady, v1.ConditionFalse, 0.0)).To(BeTrue())
Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1beta1.ExternalSecretReady, v1.ConditionTrue, 0.0)).To(BeTrue()) Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1beta1.ExternalSecretReady, v1.ConditionTrue, 1.0)).To(BeTrue())
} }
} }
@ -591,11 +605,12 @@ var _ = Describe("ExternalSecret controller", Serial, func() {
// check owner/managedFields // check owner/managedFields
Expect(ctest.HasOwnerRef(secret.ObjectMeta, "ExternalSecret", ExternalSecretFQDN)).To(BeFalse()) Expect(ctest.HasOwnerRef(secret.ObjectMeta, "ExternalSecret", ExternalSecretFQDN)).To(BeFalse())
Expect(secret.ObjectMeta.ManagedFields).To(HaveLen(2)) Expect(secret.ObjectMeta.ManagedFields).To(HaveLen(2))
Expect(ctest.HasFieldOwnership( oldCharactersAroundMismatchToInclude := format.CharactersAroundMismatchToInclude
secret.ObjectMeta, format.CharactersAroundMismatchToInclude = 10
ExternalSecretFQDN, Expect(ctest.FirstManagedFieldForManager(secret.ObjectMeta, ExternalSecretFQDN)).To(
fmt.Sprintf("{\"f:data\":{\"f:targetProperty\":{}},\"f:immutable\":{},\"f:metadata\":{\"f:annotations\":{\"f:%s\":{}}}}", esv1beta1.AnnotationDataHash)), Equal(fmt.Sprintf(`{"f:data":{"f:targetProperty":{}},"f:metadata":{"f:annotations":{".":{},"f:%s":{}},"f:labels":{".":{},"f:%s":{}}}}`, esv1beta1.AnnotationDataHash, esv1beta1.LabelManaged)),
).To(BeEmpty()) )
format.CharactersAroundMismatchToInclude = oldCharactersAroundMismatchToInclude
} }
} }
@ -1394,7 +1409,7 @@ var _ = Describe("ExternalSecret controller", Serial, func() {
Type: esv1beta1.ExternalSecretReady, Type: esv1beta1.ExternalSecretReady,
Status: v1.ConditionTrue, Status: v1.ConditionTrue,
Reason: esv1beta1.ConditionReasonSecretSynced, Reason: esv1beta1.ConditionReasonSecretSynced,
Message: "Secret was synced", Message: msgSyncedRetain,
}, },
} }
@ -1841,7 +1856,7 @@ var _ = Describe("ExternalSecret controller", Serial, func() {
&Reconciler{ &Reconciler{
ClusterSecretStoreEnabled: false, ClusterSecretStoreEnabled: false,
}, },
*tc.externalSecret, tc.externalSecret,
)).To(BeTrue()) )).To(BeTrue())
tc.checkCondition = func(es *esv1beta1.ExternalSecret) bool { tc.checkCondition = func(es *esv1beta1.ExternalSecret) bool {
@ -2315,14 +2330,17 @@ var _ = Describe("ExternalSecret controller", Serial, func() {
var _ = Describe("ExternalSecret refresh logic", func() { var _ = Describe("ExternalSecret refresh logic", func() {
Context("secret refresh", func() { Context("secret refresh", func() {
It("should refresh when resource version does not match", func() { It("should refresh when resource version does not match", func() {
Expect(shouldRefresh(esv1beta1.ExternalSecret{ Expect(shouldRefresh(&esv1beta1.ExternalSecret{
Spec: esv1beta1.ExternalSecretSpec{
RefreshInterval: &metav1.Duration{Duration: time.Minute},
},
Status: esv1beta1.ExternalSecretStatus{ Status: esv1beta1.ExternalSecretStatus{
SyncedResourceVersion: "some resource version", SyncedResourceVersion: "some resource version",
}, },
})).To(BeTrue()) })).To(BeTrue())
}) })
It("should refresh when labels change", func() { It("should refresh when labels change", func() {
es := esv1beta1.ExternalSecret{ es := &esv1beta1.ExternalSecret{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Generation: 1, Generation: 1,
Labels: map[string]string{ Labels: map[string]string{
@ -2346,7 +2364,7 @@ var _ = Describe("ExternalSecret refresh logic", func() {
}) })
It("should refresh when annotations change", func() { It("should refresh when annotations change", func() {
es := esv1beta1.ExternalSecret{ es := &esv1beta1.ExternalSecret{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Generation: 1, Generation: 1,
Annotations: map[string]string{ Annotations: map[string]string{
@ -2370,12 +2388,12 @@ var _ = Describe("ExternalSecret refresh logic", func() {
}) })
It("should refresh when generation has changed", func() { It("should refresh when generation has changed", func() {
es := esv1beta1.ExternalSecret{ es := &esv1beta1.ExternalSecret{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Generation: 1, Generation: 1,
}, },
Spec: esv1beta1.ExternalSecretSpec{ Spec: esv1beta1.ExternalSecretSpec{
RefreshInterval: &metav1.Duration{Duration: 0}, RefreshInterval: &metav1.Duration{Duration: time.Minute},
}, },
Status: esv1beta1.ExternalSecretStatus{ Status: esv1beta1.ExternalSecretStatus{
RefreshTime: metav1.Now(), RefreshTime: metav1.Now(),
@ -2390,7 +2408,7 @@ var _ = Describe("ExternalSecret refresh logic", func() {
}) })
It("should skip refresh when refreshInterval is 0", func() { It("should skip refresh when refreshInterval is 0", func() {
es := esv1beta1.ExternalSecret{ es := &esv1beta1.ExternalSecret{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Generation: 1, Generation: 1,
}, },
@ -2405,7 +2423,7 @@ var _ = Describe("ExternalSecret refresh logic", func() {
}) })
It("should refresh when refresh interval has passed", func() { It("should refresh when refresh interval has passed", func() {
es := esv1beta1.ExternalSecret{ es := &esv1beta1.ExternalSecret{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Generation: 1, Generation: 1,
}, },
@ -2422,7 +2440,7 @@ var _ = Describe("ExternalSecret refresh logic", func() {
}) })
It("should refresh when no refresh time was set", func() { It("should refresh when no refresh time was set", func() {
es := esv1beta1.ExternalSecret{ es := &esv1beta1.ExternalSecret{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Generation: 1, Generation: 1,
}, },
@ -2502,43 +2520,6 @@ var _ = Describe("ExternalSecret refresh logic", func() {
}) })
}) })
var _ = Describe("Controller Reconcile logic", func() {
Context("controller reconcile", func() {
It("should reconcile when resource is not synced", func() {
Expect(shouldReconcile(esv1beta1.ExternalSecret{
Status: esv1beta1.ExternalSecretStatus{
SyncedResourceVersion: "some resource version",
Conditions: []esv1beta1.ExternalSecretStatusCondition{{Reason: "NotASecretSynced"}},
},
})).To(BeTrue())
})
It("should reconcile when secret isn't immutable", func() {
Expect(shouldReconcile(esv1beta1.ExternalSecret{
Spec: esv1beta1.ExternalSecretSpec{
Target: esv1beta1.ExternalSecretTarget{
Immutable: false,
},
},
})).To(BeTrue())
})
It("should not reconcile if secret is immutable and has synced condition", func() {
Expect(shouldReconcile(esv1beta1.ExternalSecret{
Spec: esv1beta1.ExternalSecretSpec{
Target: esv1beta1.ExternalSecretTarget{
Immutable: true,
},
},
Status: esv1beta1.ExternalSecretStatus{
SyncedResourceVersion: "some resource version",
Conditions: []esv1beta1.ExternalSecretStatusCondition{{Reason: "SecretSynced"}},
},
})).To(BeFalse())
})
})
})
func externalSecretConditionShouldBe(name, ns string, ct esv1beta1.ExternalSecretConditionType, cs v1.ConditionStatus, v float64) bool { func externalSecretConditionShouldBe(name, ns string, ct esv1beta1.ExternalSecretConditionType, cs v1.ConditionStatus, v float64) bool {
return Eventually(func() float64 { return Eventually(func() float64 {
Expect(testExternalSecretCondition.WithLabelValues(name, ns, string(ct), string(cs)).Write(&metric)).To(Succeed()) Expect(testExternalSecretCondition.WithLabelValues(name, ns, string(ct), string(cs)).Write(&metric)).To(Succeed())

View file

@ -83,7 +83,13 @@ var _ = BeforeSuite(func() {
}, },
Client: client.Options{ Client: client.Options{
Cache: &client.CacheOptions{ Cache: &client.CacheOptions{
DisableFor: []client.Object{&v1.Secret{}, &v1.ConfigMap{}}, // the client creates a ListWatch for resources that are requested with .Get() or .List()
// we disable caching in the production code, so we disable it here as well for consistency
// see: https://github.com/external-secrets/external-secrets/issues/721
DisableFor: []client.Object{
&v1.Secret{},
&v1.ConfigMap{},
},
}, },
}, },
}) })
@ -95,8 +101,14 @@ var _ = BeforeSuite(func() {
Expect(k8sClient).ToNot(BeNil()) Expect(k8sClient).ToNot(BeNil())
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
// by default, we use a separate cached client for secrets that are managed by the controller
// so we should test under the same conditions
secretClient, err := BuildManagedSecretClient(k8sManager)
Expect(err).ToNot(HaveOccurred())
err = (&Reconciler{ err = (&Reconciler{
Client: k8sManager.GetClient(), Client: k8sManager.GetClient(),
SecretClient: secretClient,
RestConfig: cfg, RestConfig: cfg,
Scheme: k8sManager.GetScheme(), Scheme: k8sManager.GetScheme(),
Log: ctrl.Log.WithName("controllers").WithName("ExternalSecrets"), Log: ctrl.Log.WithName("controllers").WithName("ExternalSecrets"),

View file

@ -35,7 +35,7 @@ import (
const ( const (
errGetClusterSecretStore = "could not get ClusterSecretStore %q, %w" errGetClusterSecretStore = "could not get ClusterSecretStore %q, %w"
errGetSecretStore = "could not get SecretStore %q, %w" errGetSecretStore = "could not get SecretStore %q, %w"
errSecretStoreNotReady = "the desired SecretStore %s is not ready" errSecretStoreNotReady = "%s %q is not ready"
errClusterStoreMismatch = "using cluster store %q is not allowed from namespace %q: denied by spec.condition" errClusterStoreMismatch = "using cluster store %q is not allowed from namespace %q: denied by spec.condition"
) )
@ -271,7 +271,7 @@ func assertStoreIsUsable(store esv1beta1.GenericStore) error {
} }
condition := GetSecretStoreCondition(store.GetStatus(), esv1beta1.SecretStoreReady) condition := GetSecretStoreCondition(store.GetStatus(), esv1beta1.SecretStoreReady)
if condition == nil || condition.Status != v1.ConditionTrue { if condition == nil || condition.Status != v1.ConditionTrue {
return fmt.Errorf(errSecretStoreNotReady, store.GetName()) return fmt.Errorf(errSecretStoreNotReady, store.GetKind(), store.GetName())
} }
return nil return nil
} }