Browse Source

Start reconciliation when a secret has changed (#3459)

* Start reconciliation when a secret has changed

Signed-off-by: shuheiktgw <s-kitagawa@mercari.com>

* Prolong the test timeout

Signed-off-by: shuheiktgw <s-kitagawa@mercari.com>

* Use predicate.ResourceVersionChangedPredicate instead

Signed-off-by: shuheiktgw <s-kitagawa@mercari.com>

---------

Signed-off-by: shuheiktgw <s-kitagawa@mercari.com>
Shuhei Kitagawa 1 year ago
parent
commit
30f2f902cd

+ 48 - 1
pkg/controllers/externalsecret/externalsecret_controller.go

@@ -36,6 +36,9 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller"
 	"sigs.k8s.io/controller-runtime/pkg/controller"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+	"sigs.k8s.io/controller-runtime/pkg/handler"
+	"sigs.k8s.io/controller-runtime/pkg/predicate"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	// Metrics.
 	// Metrics.
@@ -73,6 +76,8 @@ const (
 	errPolicyMergePatch     = "unable to patch secret %s: %w"
 	errPolicyMergePatch     = "unable to patch secret %s: %w"
 )
 )
 
 
+const externalSecretSecretNameKey = ".spec.target.name"
+
 // Reconciler reconciles a ExternalSecret object.
 // Reconciler reconciles a ExternalSecret object.
 type Reconciler struct {
 type Reconciler struct {
 	client.Client
 	client.Client
@@ -628,9 +633,51 @@ func (r *Reconciler) computeDataHashAnnotation(existing, secret *v1.Secret) stri
 func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error {
 func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error {
 	r.recorder = mgr.GetEventRecorderFor("external-secrets")
 	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 {
+		es := obj.(*esv1beta1.ExternalSecret)
+
+		if name := es.Spec.Target.Name; name != "" {
+			return []string{name}
+		}
+		return []string{es.Name}
+	}); err != nil {
+		return err
+	}
+
 	return ctrl.NewControllerManagedBy(mgr).
 	return ctrl.NewControllerManagedBy(mgr).
 		WithOptions(opts).
 		WithOptions(opts).
 		For(&esv1beta1.ExternalSecret{}).
 		For(&esv1beta1.ExternalSecret{}).
-		Owns(&v1.Secret{}, builder.OnlyMetadata).
+		// Cannot use Owns since the controller does not set owner reference when creation policy is not Owner
+		Watches(
+			&v1.Secret{},
+			handler.EnqueueRequestsFromMapFunc(r.findObjectsForSecret),
+			builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
+			builder.OnlyMetadata,
+		).
 		Complete(r)
 		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()},
+	)
+	if err != nil {
+		return []reconcile.Request{}
+	}
+
+	requests := make([]reconcile.Request, len(externalSecrets.Items))
+	for i := range externalSecrets.Items {
+		requests[i] = reconcile.Request{
+			NamespacedName: types.NamespacedName{
+				Name:      externalSecrets.Items[i].GetName(),
+				Namespace: externalSecrets.Items[i].GetNamespace(),
+			},
+		}
+	}
+	return requests
+}

+ 35 - 1
pkg/controllers/externalsecret/externalsecret_controller_test.go

@@ -50,7 +50,7 @@ var (
 	fakeProvider   *fake.Client
 	fakeProvider   *fake.Client
 	metric         dto.Metric
 	metric         dto.Metric
 	metricDuration dto.Metric
 	metricDuration dto.Metric
-	timeout        = time.Second * 10
+	timeout        = time.Second * 20
 	interval       = time.Millisecond * 250
 	interval       = time.Millisecond * 250
 )
 )
 
 
@@ -468,6 +468,39 @@ var _ = Describe("ExternalSecret controller", Serial, func() {
 		}
 		}
 	}
 	}
 
 
+	mergeWithSecretUpdate := func(tc *testCase) {
+		const secretVal = "someValue"
+		tc.externalSecret.Spec.Target.CreationPolicy = esv1beta1.CreatePolicyMerge
+		tc.externalSecret.Spec.RefreshInterval = &metav1.Duration{Duration: time.Hour}
+
+		Expect(k8sClient.Create(context.Background(), &v1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      ExternalSecretTargetSecretName,
+				Namespace: ExternalSecretNamespace,
+			},
+			Data: map[string][]byte{
+				existingKey: []byte(existingVal),
+			},
+		}, client.FieldOwner(FakeManager))).To(Succeed())
+
+		fakeProvider.WithGetSecret([]byte(secretVal), nil)
+		tc.checkSecret = func(es *esv1beta1.ExternalSecret, secret *v1.Secret) {
+			// Overwrite the secret value to check if the change kicks reconciliation and overwrites it again
+			Expect(k8sClient.Update(context.Background(), &v1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      ExternalSecretTargetSecretName,
+					Namespace: ExternalSecretNamespace,
+				},
+				Data: map[string][]byte{
+					existingKey: []byte("differentValue"),
+				},
+			}, client.FieldOwner(FakeManager))).To(Succeed())
+
+			Expect(string(secret.Data[existingKey])).To(Equal(existingVal))
+			Expect(string(secret.Data[targetProp])).To(Equal(secretVal))
+		}
+	}
+
 	// should not update if no changes
 	// should not update if no changes
 	mergeWithSecretNoChange := func(tc *testCase) {
 	mergeWithSecretNoChange := func(tc *testCase) {
 		tc.externalSecret.Spec.Target.CreationPolicy = esv1beta1.CreatePolicyMerge
 		tc.externalSecret.Spec.Target.CreationPolicy = esv1beta1.CreatePolicyMerge
@@ -2226,6 +2259,7 @@ var _ = Describe("ExternalSecret controller", Serial, func() {
 		Entry("should removed outdated labels and annotations", removeOutdatedLabelsAnnotations),
 		Entry("should removed outdated labels and annotations", removeOutdatedLabelsAnnotations),
 		Entry("should set prometheus counters", checkPrometheusCounters),
 		Entry("should set prometheus counters", checkPrometheusCounters),
 		Entry("should merge with existing secret using creationPolicy=Merge", mergeWithSecret),
 		Entry("should merge with existing secret using creationPolicy=Merge", mergeWithSecret),
+		Entry("should kick reconciliation when secret changes using creationPolicy=Merge", mergeWithSecretUpdate),
 		Entry("should error if secret doesn't exist when using creationPolicy=Merge", mergeWithSecretErr),
 		Entry("should error if secret doesn't exist when using creationPolicy=Merge", mergeWithSecretErr),
 		Entry("should not resolve conflicts with creationPolicy=Merge", mergeWithConflict),
 		Entry("should not resolve conflicts with creationPolicy=Merge", mergeWithConflict),
 		Entry("should not update unchanged secret using creationPolicy=Merge", mergeWithSecretNoChange),
 		Entry("should not update unchanged secret using creationPolicy=Merge", mergeWithSecretNoChange),

+ 1 - 1
pkg/controllers/externalsecret/suite_test.go

@@ -90,7 +90,7 @@ var _ = BeforeSuite(func() {
 	Expect(err).ToNot(HaveOccurred())
 	Expect(err).ToNot(HaveOccurred())
 
 
 	err = (&Reconciler{
 	err = (&Reconciler{
-		Client:                    k8sClient,
+		Client:                    k8sManager.GetClient(),
 		RestConfig:                cfg,
 		RestConfig:                cfg,
 		Scheme:                    k8sManager.GetScheme(),
 		Scheme:                    k8sManager.GetScheme(),
 		Log:                       ctrl.Log.WithName("controllers").WithName("ExternalSecrets"),
 		Log:                       ctrl.Log.WithName("controllers").WithName("ExternalSecrets"),