Browse Source

fix(externalsecret): infinite reconcile loop with Merge secret (#2525)

* fix(externalsecret): infinite reconcile loop with Merge secret

Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>

* code review

Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>

* lint

Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>

* add unit tests

Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>

* lint

Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>

* Use objectHash instead of value

Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>

---------

Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>
Alexandre Gaudreault 2 years ago
parent
commit
21928a45b9

+ 1 - 0
ADOPTERS.md

@@ -7,6 +7,7 @@
 - [DaangnPay](https://www.daangnpay.com/)
 - [DaangnPay](https://www.daangnpay.com/)
 - [Epidemic Sound](https://www.epidemicsound.com/)
 - [Epidemic Sound](https://www.epidemicsound.com/)
 - [Form3](https://www.form3.tech/)
 - [Form3](https://www.form3.tech/)
+- [GoTo](https://www.goto.com/)
 - [Heureka Group](https://heureka.group)
 - [Heureka Group](https://heureka.group)
 - [K8S Website Infra](https://k8s.io/)
 - [K8S Website Infra](https://k8s.io/)
 - [Mercedes-Benz Tech Innovation](https://www.mercedes-benz-techinnovation.com/)
 - [Mercedes-Benz Tech Innovation](https://www.mercedes-benz-techinnovation.com/)

+ 1 - 1
docs/contributing/devguide.md

@@ -77,7 +77,7 @@ export IMAGE=$(make docker.imagename)
 make docker.build
 make docker.build
 
 
 # Load docker image into local kind cluster
 # Load docker image into local kind cluster
-kind load docker-image $IMAGE:$TAG -n external-secrets
+kind load docker-image $IMAGE:$TAG --name external-secrets
 
 
 # (Optional) Pull the image from GitHub Repo to copy into kind
 # (Optional) Pull the image from GitHub Repo to copy into kind
 # docker pull ghcr.io/external-secrets/external-secrets:v0.8.2
 # docker pull ghcr.io/external-secrets/external-secrets:v0.8.2

+ 14 - 0
pkg/controllers/externalsecret/externalsecret_controller.go

@@ -281,6 +281,8 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
 			secret.Labels[esv1beta1.LabelOwner] = lblValue
 			secret.Labels[esv1beta1.LabelOwner] = lblValue
 		}
 		}
 
 
+		secret.Annotations[esv1beta1.AnnotationDataHash] = r.computeDataHashAnnotation(&existingSecret, secret)
+
 		return nil
 		return nil
 	}
 	}
 
 
@@ -599,6 +601,18 @@ func isSecretValid(existingSecret v1.Secret) bool {
 	return true
 	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)
+	for k, v := range existing.Data {
+		data[k] = v
+	}
+	for k, v := range secret.Data {
+		data[k] = v
+	}
+	return utils.ObjectHash(data)
+}
+
 // SetupWithManager returns a new controller builder that will be started by the provided Manager.
 // 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 {
 func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error {
 	r.recorder = mgr.GetEventRecorderFor("external-secrets")
 	r.recorder = mgr.GetEventRecorderFor("external-secrets")

+ 0 - 2
pkg/controllers/externalsecret/externalsecret_controller_template.go

@@ -152,7 +152,6 @@ func (r *Reconciler) applyTemplate(ctx context.Context, es *esv1beta1.ExternalSe
 	// 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
 		secret.Data = dataMap
-		secret.Annotations[esv1beta1.AnnotationDataHash] = utils.ObjectHash(secret.Data)
 		return nil
 		return nil
 	}
 	}
 	// Merge Policy should merge secrets
 	// Merge Policy should merge secrets
@@ -198,7 +197,6 @@ func (r *Reconciler) applyTemplate(ctx context.Context, es *esv1beta1.ExternalSe
 	if len(es.Spec.Target.Template.Data) == 0 && len(es.Spec.Target.Template.TemplateFrom) == 0 {
 	if len(es.Spec.Target.Template.Data) == 0 && len(es.Spec.Target.Template.TemplateFrom) == 0 {
 		secret.Data = dataMap
 		secret.Data = dataMap
 	}
 	}
-	secret.Annotations[esv1beta1.AnnotationDataHash] = utils.ObjectHash(secret.Data)
 	return nil
 	return nil
 }
 }
 
 

+ 46 - 17
pkg/controllers/externalsecret/externalsecret_controller_test.go

@@ -39,6 +39,7 @@ import (
 	"github.com/external-secrets/external-secrets/pkg/controllers/externalsecret/esmetrics"
 	"github.com/external-secrets/external-secrets/pkg/controllers/externalsecret/esmetrics"
 	ctrlmetrics "github.com/external-secrets/external-secrets/pkg/controllers/metrics"
 	ctrlmetrics "github.com/external-secrets/external-secrets/pkg/controllers/metrics"
 	"github.com/external-secrets/external-secrets/pkg/provider/testing/fake"
 	"github.com/external-secrets/external-secrets/pkg/provider/testing/fake"
+	"github.com/external-secrets/external-secrets/pkg/utils"
 )
 )
 
 
 var (
 var (
@@ -77,6 +78,10 @@ type testCase struct {
 type testTweaks func(*testCase)
 type testTweaks func(*testCase)
 
 
 var _ = Describe("Kind=secret existence logic", func() {
 var _ = Describe("Kind=secret existence logic", func() {
+	validData := map[string][]byte{
+		"foo": []byte("value1"),
+		"bar": []byte("value2"),
+	}
 	type testCase struct {
 	type testCase struct {
 		Name           string
 		Name           string
 		Input          v1.Secret
 		Input          v1.Secret
@@ -126,13 +131,10 @@ var _ = Describe("Kind=secret existence logic", func() {
 				ObjectMeta: metav1.ObjectMeta{
 				ObjectMeta: metav1.ObjectMeta{
 					UID: "xxx",
 					UID: "xxx",
 					Annotations: map[string]string{
 					Annotations: map[string]string{
-						esv1beta1.AnnotationDataHash: "caa0155759a6a9b3b6ada5a6883ee2bb",
+						esv1beta1.AnnotationDataHash: utils.ObjectHash(validData),
 					},
 					},
 				},
 				},
-				Data: map[string][]byte{
-					"foo": []byte("value1"),
-					"bar": []byte("value2"),
-				},
+				Data: validData,
 			},
 			},
 			ExpectedOutput: true,
 			ExpectedOutput: true,
 		},
 		},
@@ -210,10 +212,14 @@ var _ = Describe("ExternalSecret controller", Serial, func() {
 		},
 		},
 	)
 	)
 
 
-	const secretVal = "some-value"
-	const targetProp = "targetProperty"
-	const remoteKey = "barz"
-	const remoteProperty = "bang"
+	const (
+		secretVal      = "some-value"
+		targetProp     = "targetProperty"
+		remoteKey      = "barz"
+		remoteProperty = "bang"
+		existingKey    = "pre-existing-key"
+		existingVal    = "pre-existing-value"
+	)
 
 
 	makeDefaultTestcase := func() *testCase {
 	makeDefaultTestcase := func() *testCase {
 		return &testCase{
 		return &testCase{
@@ -352,8 +358,6 @@ var _ = Describe("ExternalSecret controller", Serial, func() {
 	// metadata.managedFields with the correct owner should be added to the secret
 	// metadata.managedFields with the correct owner should be added to the secret
 	mergeWithSecret := func(tc *testCase) {
 	mergeWithSecret := func(tc *testCase) {
 		const secretVal = "someValue"
 		const secretVal = "someValue"
-		const existingKey = "pre-existing-key"
-		existingVal := "pre-existing-value"
 		tc.externalSecret.Spec.Target.CreationPolicy = esv1beta1.CreatePolicyMerge
 		tc.externalSecret.Spec.Target.CreationPolicy = esv1beta1.CreatePolicyMerge
 
 
 		// create secret beforehand
 		// create secret beforehand
@@ -393,8 +397,6 @@ var _ = Describe("ExternalSecret controller", Serial, func() {
 
 
 	// should not update if no changes
 	// should not update if no changes
 	mergeWithSecretNoChange := func(tc *testCase) {
 	mergeWithSecretNoChange := func(tc *testCase) {
-		const existingKey = "pre-existing-key"
-		existingVal := "someValue"
 		tc.externalSecret.Spec.Target.CreationPolicy = esv1beta1.CreatePolicyMerge
 		tc.externalSecret.Spec.Target.CreationPolicy = esv1beta1.CreatePolicyMerge
 
 
 		// create secret beforehand
 		// create secret beforehand
@@ -461,7 +463,6 @@ var _ = Describe("ExternalSecret controller", Serial, func() {
 		const secretVal = "someValue"
 		const secretVal = "someValue"
 		// this should confict
 		// this should confict
 		const existingKey = targetProp
 		const existingKey = targetProp
-		existingVal := "pre-existing-value"
 		tc.externalSecret.Spec.Target.CreationPolicy = esv1beta1.CreatePolicyMerge
 		tc.externalSecret.Spec.Target.CreationPolicy = esv1beta1.CreatePolicyMerge
 
 
 		// create secret beforehand
 		// create secret beforehand
@@ -1260,8 +1261,6 @@ var _ = Describe("ExternalSecret controller", Serial, func() {
 	// if provider secret gets deleted only the managed field should get deleted
 	// if provider secret gets deleted only the managed field should get deleted
 	deleteSecretPolicyMerge := func(tc *testCase) {
 	deleteSecretPolicyMerge := func(tc *testCase) {
 		const secretVal = "someValue"
 		const secretVal = "someValue"
-		const existingKey = "some-existing-key"
-		existingVal := "some-existing-value"
 		tc.externalSecret.Spec.RefreshInterval = &metav1.Duration{Duration: time.Second}
 		tc.externalSecret.Spec.RefreshInterval = &metav1.Duration{Duration: time.Second}
 		tc.externalSecret.Spec.Target.CreationPolicy = esv1beta1.CreatePolicyMerge
 		tc.externalSecret.Spec.Target.CreationPolicy = esv1beta1.CreatePolicyMerge
 		tc.externalSecret.Spec.Target.DeletionPolicy = esv1beta1.DeletionPolicyMerge
 		tc.externalSecret.Spec.Target.DeletionPolicy = esv1beta1.DeletionPolicyMerge
@@ -1736,7 +1735,36 @@ var _ = Describe("ExternalSecret controller", Serial, func() {
 		const secretVal = "someValue"
 		const secretVal = "someValue"
 		fakeProvider.WithGetSecret([]byte(secretVal), nil)
 		fakeProvider.WithGetSecret([]byte(secretVal), nil)
 		tc.checkSecret = func(es *esv1beta1.ExternalSecret, secret *v1.Secret) {
 		tc.checkSecret = func(es *esv1beta1.ExternalSecret, secret *v1.Secret) {
-			Expect(secret.Annotations[esv1beta1.AnnotationDataHash]).To(Equal("9d30b95ca81e156f9454b5ef3bfcc6ee"))
+			expectedHash := utils.ObjectHash(map[string][]byte{
+				targetProp: []byte(secretVal),
+			})
+			Expect(secret.Annotations[esv1beta1.AnnotationDataHash]).To(Equal(expectedHash))
+		}
+	}
+
+	// Checks that secret annotation has been written based on the all the data for merge keys
+	checkMergeSecretDataHashAnnotation := func(tc *testCase) {
+		const secretVal = "someValue"
+		tc.externalSecret.Spec.Target.CreationPolicy = esv1beta1.CreatePolicyMerge
+
+		// create secret beforehand
+		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) {
+			expectedHash := utils.ObjectHash(map[string][]byte{
+				existingKey: []byte(existingVal),
+				targetProp:  []byte(secretVal),
+			})
+			Expect(secret.Annotations[esv1beta1.AnnotationDataHash]).To(Equal(expectedHash))
 		}
 		}
 	}
 	}
 
 
@@ -2069,6 +2097,7 @@ var _ = Describe("ExternalSecret controller", Serial, func() {
 		},
 		},
 		Entry("should recreate deleted secret", checkDeletion),
 		Entry("should recreate deleted secret", checkDeletion),
 		Entry("should create proper hash annotation for the external secret", checkSecretDataHashAnnotation),
 		Entry("should create proper hash annotation for the external secret", checkSecretDataHashAnnotation),
+		Entry("should create proper hash annotation for the external secret with creationPolicy=Merge", checkMergeSecretDataHashAnnotation),
 		Entry("es deletes orphaned secrets", deleteOrphanedSecrets),
 		Entry("es deletes orphaned secrets", deleteOrphanedSecrets),
 		Entry("should refresh when the hash annotation doesn't correspond to secret data", checkSecretDataHashAnnotationChange),
 		Entry("should refresh when the hash annotation doesn't correspond to secret data", checkSecretDataHashAnnotationChange),
 		Entry("should use external secret name if target secret name isn't defined", syncWithoutTargetName),
 		Entry("should use external secret name if target secret name isn't defined", syncWithoutTargetName),