mirror of
https://github.com/external-secrets/external-secrets.git
synced 2024-12-14 11:57:59 +00:00
fix: dependent kind=secret are not recreated in case of deletion. (#349)
* chore: whitespace, typos, superflous aliases * fix: deleted child secret is not recreated straight away. * fix: e2e run
This commit is contained in:
parent
f851438ec7
commit
280964f84e
11 changed files with 334 additions and 75 deletions
|
@ -207,6 +207,11 @@ type ExternalSecret struct {
|
|||
Status ExternalSecretStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
// AnnotationDataHash is used to ensure consistency.
|
||||
AnnotationDataHash = "reconcile.external-secrets.io/data-hash"
|
||||
)
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// ExternalSecretList contains a list of ExternalSecret resources.
|
||||
|
|
|
@ -11,6 +11,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package framework
|
||||
|
||||
import (
|
||||
|
@ -23,6 +24,8 @@ import (
|
|||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
|
||||
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
|
||||
)
|
||||
|
||||
// WaitForSecretValue waits until a secret comes into existence and compares the secret.Data
|
||||
|
@ -52,6 +55,12 @@ func equalSecrets(exp, ts *v1.Secret) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// secret contains data hash property which must be ignored
|
||||
delete(ts.ObjectMeta.Annotations, esv1alpha1.AnnotationDataHash)
|
||||
if len(ts.ObjectMeta.Annotations) == 0 {
|
||||
ts.ObjectMeta.Annotations = nil
|
||||
}
|
||||
|
||||
expAnnotations, _ := json.Marshal(exp.ObjectMeta.Annotations)
|
||||
tsAnnotations, _ := json.Marshal(ts.ObjectMeta.Annotations)
|
||||
if !bytes.Equal(expAnnotations, tsAnnotations) {
|
||||
|
|
|
@ -11,6 +11,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package aws
|
||||
|
||||
import (
|
||||
|
|
|
@ -11,6 +11,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package aws
|
||||
|
||||
import (
|
||||
|
|
2
main.go
2
main.go
|
@ -59,7 +59,7 @@ func main() {
|
|||
var lvl zapcore.Level
|
||||
err := lvl.UnmarshalText([]byte(loglevel))
|
||||
if err != nil {
|
||||
setupLog.Error(err, "error unmarshaling loglevel")
|
||||
setupLog.Error(err, "error unmarshalling loglevel")
|
||||
os.Exit(1)
|
||||
}
|
||||
logger := zap.New(zap.Level(lvl))
|
||||
|
|
|
@ -16,9 +16,6 @@ package externalsecret
|
|||
|
||||
import (
|
||||
"context"
|
||||
|
||||
// nolint
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
|
@ -38,8 +35,8 @@ import (
|
|||
|
||||
// Loading registered providers.
|
||||
_ "github.com/external-secrets/external-secrets/pkg/provider/register"
|
||||
schema "github.com/external-secrets/external-secrets/pkg/provider/schema"
|
||||
utils "github.com/external-secrets/external-secrets/pkg/utils"
|
||||
"github.com/external-secrets/external-secrets/pkg/provider/schema"
|
||||
"github.com/external-secrets/external-secrets/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -53,6 +50,7 @@ const (
|
|||
errStoreRef = "could not get store reference"
|
||||
errStoreProvider = "could not get store provider"
|
||||
errStoreClient = "could not get provider client"
|
||||
errGetExistingSecret = "could not get existing secret: %w"
|
||||
errCloseStoreClient = "could not close provider client"
|
||||
errSetCtrlReference = "could not set ExternalSecret controller reference: %w"
|
||||
errFetchTplFrom = "error fetching templateFrom data: %w"
|
||||
|
@ -126,7 +124,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
|
|||
|
||||
// check if store should be handled by this controller instance
|
||||
if !shouldProcessStore(store, r.ControllerClass) {
|
||||
log.Info("skippig unmanaged store")
|
||||
log.Info("skipping unmanaged store")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
|
@ -158,21 +156,31 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
|
|||
refreshInt = externalSecret.Spec.RefreshInterval.Duration
|
||||
}
|
||||
|
||||
// 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) {
|
||||
log.V(1).Info("skipping refresh", "rv", getResourceVersion(externalSecret))
|
||||
return ctrl.Result{RequeueAfter: refreshInt}, nil
|
||||
}
|
||||
|
||||
// Target Secret Name should default to the ExternalSecret name if not explicitly specified
|
||||
secretName := externalSecret.Spec.Target.Name
|
||||
if secretName == "" {
|
||||
secretName = externalSecret.ObjectMeta.Name
|
||||
}
|
||||
|
||||
// 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 err != nil && !apierrors.IsNotFound(err) {
|
||||
log.Error(err, errGetExistingSecret)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
log.V(1).Info("skipping refresh", "rv", getResourceVersion(externalSecret))
|
||||
return ctrl.Result{RequeueAfter: refreshInt}, nil
|
||||
}
|
||||
|
||||
secret := &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: secretName,
|
||||
|
@ -202,7 +210,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
|
|||
return nil
|
||||
}
|
||||
|
||||
//nolint
|
||||
// nolint
|
||||
switch externalSecret.Spec.Target.CreationPolicy {
|
||||
case esv1alpha1.Merge:
|
||||
err = patchSecret(ctx, r.Client, r.Scheme, secret, mutationFunc)
|
||||
|
@ -249,7 +257,7 @@ func patchSecret(ctx context.Context, c client.Client, scheme *runtime.Scheme, s
|
|||
// 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 befor doing a Patch() as it depends on the GVK
|
||||
// we need to manually set it before doing a Patch() as it depends on the GVK
|
||||
gvks, unversioned, err := scheme.ObjectKinds(secret)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -284,12 +292,10 @@ func hashMeta(m metav1.ObjectMeta) string {
|
|||
annotations map[string]string
|
||||
labels map[string]string
|
||||
}
|
||||
h := md5.New() //nolint
|
||||
_, _ = h.Write([]byte(fmt.Sprintf("%v", meta{
|
||||
return utils.ObjectHash(meta{
|
||||
annotations: m.Annotations,
|
||||
labels: m.Labels,
|
||||
})))
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
})
|
||||
}
|
||||
|
||||
func shouldRefresh(es esv1alpha1.ExternalSecret) bool {
|
||||
|
@ -297,6 +303,7 @@ func shouldRefresh(es esv1alpha1.ExternalSecret) bool {
|
|||
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
|
||||
|
@ -307,6 +314,20 @@ func shouldRefresh(es esv1alpha1.ExternalSecret) bool {
|
|||
return !es.Status.RefreshTime.Add(es.Spec.RefreshInterval.Duration).After(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 {
|
||||
return false
|
||||
}
|
||||
|
||||
// if the calculated hash is different from the calculation, then it's invalid
|
||||
if existingSecret.Annotations[esv1alpha1.AnnotationDataHash] != utils.ObjectHash(existingSecret.Data) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// getStore returns the store with the provided ExternalSecret.
|
||||
func (r *Reconciler) getStore(ctx context.Context, externalSecret *esv1alpha1.ExternalSecret) (esv1alpha1.GenericStore, error) {
|
||||
ref := types.NamespacedName{
|
||||
|
|
|
@ -40,6 +40,7 @@ func (r *Reconciler) applyTemplate(ctx context.Context, es *esv1alpha1.ExternalS
|
|||
// no template: copy data and return
|
||||
if es.Spec.Target.Template == nil {
|
||||
secret.Data = dataMap
|
||||
secret.Annotations[esv1alpha1.AnnotationDataHash] = utils.ObjectHash(secret.Data)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -67,6 +68,7 @@ func (r *Reconciler) applyTemplate(ctx context.Context, es *esv1alpha1.ExternalS
|
|||
secret.Data[k] = v
|
||||
}
|
||||
}
|
||||
secret.Annotations[esv1alpha1.AnnotationDataHash] = utils.ObjectHash(secret.Data)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@ package externalsecret
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
|
@ -59,6 +61,74 @@ type testCase struct {
|
|||
|
||||
type testTweaks func(*testCase)
|
||||
|
||||
var _ = Describe("Kind=secret existence logic", func() {
|
||||
type testCase struct {
|
||||
Name string
|
||||
Input v1.Secret
|
||||
ExpectedOutput bool
|
||||
}
|
||||
tests := []testCase{
|
||||
{
|
||||
Name: "Should not be valid in case of missing uid",
|
||||
Input: v1.Secret{},
|
||||
ExpectedOutput: false,
|
||||
},
|
||||
{
|
||||
Name: "A nil annotation should not be valid",
|
||||
Input: v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
UID: "xxx",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
},
|
||||
ExpectedOutput: false,
|
||||
},
|
||||
{
|
||||
Name: "A nil annotation should not be valid",
|
||||
Input: v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
UID: "xxx",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
},
|
||||
ExpectedOutput: false,
|
||||
},
|
||||
{
|
||||
Name: "An invalid annotation hash should not be valid",
|
||||
Input: v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
UID: "xxx",
|
||||
Annotations: map[string]string{
|
||||
esv1alpha1.AnnotationDataHash: "xxxxxx",
|
||||
},
|
||||
},
|
||||
},
|
||||
ExpectedOutput: false,
|
||||
},
|
||||
{
|
||||
Name: "A valid config map should return true",
|
||||
Input: v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
UID: "xxx",
|
||||
Annotations: map[string]string{
|
||||
esv1alpha1.AnnotationDataHash: "caa0155759a6a9b3b6ada5a6883ee2bb",
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"foo": []byte("value1"),
|
||||
"bar": []byte("value2"),
|
||||
},
|
||||
},
|
||||
ExpectedOutput: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
It(tt.Name, func() {
|
||||
Expect(isSecretValid(tt.Input)).To(BeEquivalentTo(tt.ExpectedOutput))
|
||||
})
|
||||
}
|
||||
})
|
||||
var _ = Describe("ExternalSecret controller", func() {
|
||||
const (
|
||||
ExternalSecretName = "test-es"
|
||||
|
@ -68,6 +138,13 @@ var _ = Describe("ExternalSecret controller", func() {
|
|||
|
||||
var ExternalSecretNamespace string
|
||||
|
||||
// if we are in debug and need to increase the timeout for testing, we can do so by using an env var
|
||||
if customTimeout := os.Getenv("TEST_CUSTOM_TIMEOUT_SEC"); customTimeout != "" {
|
||||
if t, err := strconv.Atoi(customTimeout); err == nil {
|
||||
timeout = time.Second * time.Duration(t)
|
||||
}
|
||||
}
|
||||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
ExternalSecretNamespace, err = CreateNamespace("test-ns", k8sClient)
|
||||
|
@ -157,6 +234,23 @@ var _ = Describe("ExternalSecret controller", func() {
|
|||
"hihihih": "hehehe",
|
||||
}
|
||||
fakeProvider.WithGetSecret([]byte(secretVal), nil)
|
||||
tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) {
|
||||
// check value
|
||||
Expect(string(secret.Data[targetProp])).To(Equal(secretVal))
|
||||
|
||||
// check labels & annotations
|
||||
Expect(secret.ObjectMeta.Labels).To(BeEquivalentTo(es.ObjectMeta.Labels))
|
||||
for k, v := range es.ObjectMeta.Annotations {
|
||||
Expect(secret.ObjectMeta.Annotations).To(HaveKeyWithValue(k, v))
|
||||
}
|
||||
// ownerRef must not not be set!
|
||||
Expect(hasOwnerRef(secret.ObjectMeta, "ExternalSecret", ExternalSecretName)).To(BeTrue())
|
||||
}
|
||||
}
|
||||
|
||||
checkPrometheusCounters := func(tc *testCase) {
|
||||
const secretVal = "someValue"
|
||||
fakeProvider.WithGetSecret([]byte(secretVal), nil)
|
||||
tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) {
|
||||
Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionFalse, 0.0)).To(BeTrue())
|
||||
Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionTrue, 1.0)).To(BeTrue())
|
||||
|
@ -164,15 +258,6 @@ var _ = Describe("ExternalSecret controller", func() {
|
|||
Expect(syncCallsTotal.WithLabelValues(ExternalSecretName, ExternalSecretNamespace).Write(&metric)).To(Succeed())
|
||||
return metric.GetCounter().GetValue() == 1.0
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
|
||||
// check value
|
||||
Expect(string(secret.Data[targetProp])).To(Equal(secretVal))
|
||||
|
||||
// check labels & annotations
|
||||
Expect(secret.ObjectMeta.Labels).To(BeEquivalentTo(es.ObjectMeta.Labels))
|
||||
Expect(secret.ObjectMeta.Annotations).To(BeEquivalentTo(es.ObjectMeta.Annotations))
|
||||
// ownerRef must not not be set!
|
||||
Expect(hasOwnerRef(secret.ObjectMeta, "ExternalSecret", ExternalSecretName)).To(BeTrue())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -198,23 +283,22 @@ var _ = Describe("ExternalSecret controller", func() {
|
|||
|
||||
fakeProvider.WithGetSecret([]byte(secretVal), nil)
|
||||
tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) {
|
||||
Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionFalse, 0.0)).To(BeTrue())
|
||||
Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionTrue, 1.0)).To(BeTrue())
|
||||
Eventually(func() bool {
|
||||
Expect(syncCallsTotal.WithLabelValues(ExternalSecretName, ExternalSecretNamespace).Write(&metric)).To(Succeed())
|
||||
return metric.GetCounter().GetValue() == 1.0
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
|
||||
// check value
|
||||
Expect(string(secret.Data[existingKey])).To(Equal(existingVal))
|
||||
Expect(string(secret.Data[targetProp])).To(Equal(secretVal))
|
||||
|
||||
// check labels & annotations
|
||||
Expect(secret.ObjectMeta.Labels).To(BeEquivalentTo(es.ObjectMeta.Labels))
|
||||
Expect(secret.ObjectMeta.Annotations).To(BeEquivalentTo(es.ObjectMeta.Annotations))
|
||||
for k, v := range es.ObjectMeta.Annotations {
|
||||
Expect(secret.ObjectMeta.Annotations).To(HaveKeyWithValue(k, v))
|
||||
}
|
||||
Expect(hasOwnerRef(secret.ObjectMeta, "ExternalSecret", ExternalSecretName)).To(BeFalse())
|
||||
Expect(secret.ObjectMeta.ManagedFields).To(HaveLen(2))
|
||||
Expect(hasFieldOwnership(secret.ObjectMeta, "external-secrets", "{\"f:data\":{\"f:targetProperty\":{}}}")).To(BeTrue())
|
||||
Expect(hasFieldOwnership(
|
||||
secret.ObjectMeta,
|
||||
"external-secrets",
|
||||
fmt.Sprintf("{\"f:data\":{\"f:targetProperty\":{}},\"f:metadata\":{\"f:annotations\":{\"f:%s\":{}}}}", esv1alpha1.AnnotationDataHash)),
|
||||
).To(BeTrue())
|
||||
Expect(hasFieldOwnership(secret.ObjectMeta, "fake.manager", "{\"f:data\":{\".\":{},\"f:pre-existing-key\":{}},\"f:type\":{}}")).To(BeTrue())
|
||||
}
|
||||
}
|
||||
|
@ -313,20 +397,15 @@ var _ = Describe("ExternalSecret controller", func() {
|
|||
}
|
||||
fakeProvider.WithGetSecret([]byte(secretVal), nil)
|
||||
tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) {
|
||||
Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionFalse, 0.0)).To(BeTrue())
|
||||
Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionTrue, 1.0)).To(BeTrue())
|
||||
Eventually(func() bool {
|
||||
Expect(syncCallsTotal.WithLabelValues(ExternalSecretName, ExternalSecretNamespace).Write(&metric)).To(Succeed())
|
||||
return metric.GetCounter().GetValue() == 1.0
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
|
||||
// check values
|
||||
Expect(string(secret.Data[targetProp])).To(Equal(expectedSecretVal))
|
||||
Expect(string(secret.Data[tplStaticKey])).To(Equal(tplStaticVal))
|
||||
|
||||
// labels/annotations should be taken from the template
|
||||
Expect(secret.ObjectMeta.Labels).To(BeEquivalentTo(es.Spec.Target.Template.Metadata.Labels))
|
||||
Expect(secret.ObjectMeta.Annotations).To(BeEquivalentTo(es.Spec.Target.Template.Metadata.Annotations))
|
||||
for k, v := range es.Spec.Target.Template.Metadata.Annotations {
|
||||
Expect(secret.ObjectMeta.Annotations).To(HaveKeyWithValue(k, v))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -443,7 +522,12 @@ var _ = Describe("ExternalSecret controller", func() {
|
|||
|
||||
// labels/annotations should be taken from the template
|
||||
Expect(secret.ObjectMeta.Labels).To(BeEquivalentTo(es.Spec.Target.Template.Metadata.Labels))
|
||||
Expect(secret.ObjectMeta.Annotations).To(BeEquivalentTo(es.Spec.Target.Template.Metadata.Annotations))
|
||||
|
||||
// a secret will always have some extra annotations (i.e. hashmap check), so we only check for specific
|
||||
// source annotations
|
||||
for k, v := range es.Spec.Target.Template.Metadata.Annotations {
|
||||
Expect(secret.ObjectMeta.Annotations).To(HaveKeyWithValue(k, v))
|
||||
}
|
||||
|
||||
cleanEs := tc.externalSecret.DeepCopy()
|
||||
|
||||
|
@ -470,7 +554,9 @@ var _ = Describe("ExternalSecret controller", func() {
|
|||
|
||||
// also check labels/annotations have been updated
|
||||
Expect(secret.ObjectMeta.Labels).To(BeEquivalentTo(es.Spec.Target.Template.Metadata.Labels))
|
||||
Expect(secret.ObjectMeta.Annotations).To(BeEquivalentTo(es.Spec.Target.Template.Metadata.Annotations))
|
||||
for k, v := range es.Spec.Target.Template.Metadata.Annotations {
|
||||
Expect(secret.ObjectMeta.Annotations).To(HaveKeyWithValue(k, v))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -490,7 +576,9 @@ var _ = Describe("ExternalSecret controller", func() {
|
|||
|
||||
// labels/annotations should be taken from the template
|
||||
Expect(secret.ObjectMeta.Labels).To(BeEquivalentTo(es.Spec.Target.Template.Metadata.Labels))
|
||||
Expect(secret.ObjectMeta.Annotations).To(BeEquivalentTo(es.Spec.Target.Template.Metadata.Annotations))
|
||||
for k, v := range es.Spec.Target.Template.Metadata.Annotations {
|
||||
Expect(secret.ObjectMeta.Annotations).To(HaveKeyWithValue(k, v))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -502,13 +590,6 @@ var _ = Describe("ExternalSecret controller", func() {
|
|||
fakeProvider.WithGetSecret([]byte(secretVal), nil)
|
||||
tc.externalSecret.Spec.RefreshInterval = &metav1.Duration{Duration: time.Second}
|
||||
tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) {
|
||||
Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionFalse, 0.0)).To(BeTrue())
|
||||
Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionTrue, 1.0)).To(BeTrue())
|
||||
Eventually(func() bool {
|
||||
Expect(syncCallsTotal.WithLabelValues(ExternalSecretName, ExternalSecretNamespace).Write(&metric)).To(Succeed())
|
||||
return metric.GetCounter().GetValue() == 1.0
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
|
||||
// check values
|
||||
Expect(string(secret.Data[targetProp])).To(Equal(secretVal))
|
||||
|
||||
|
@ -537,13 +618,6 @@ var _ = Describe("ExternalSecret controller", func() {
|
|||
fakeProvider.WithGetSecret([]byte(secretVal), nil)
|
||||
tc.externalSecret.Spec.RefreshInterval = &metav1.Duration{Duration: 0}
|
||||
tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) {
|
||||
Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionFalse, 0.0)).To(BeTrue())
|
||||
Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionTrue, 1.0)).To(BeTrue())
|
||||
Eventually(func() bool {
|
||||
Expect(syncCallsTotal.WithLabelValues(ExternalSecretName, ExternalSecretNamespace).Write(&metric)).To(Succeed())
|
||||
return metric.GetCounter().GetValue() == 1.0
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
|
||||
// check values
|
||||
Expect(string(secret.Data[targetProp])).To(Equal(secretVal))
|
||||
|
||||
|
@ -580,13 +654,6 @@ var _ = Describe("ExternalSecret controller", func() {
|
|||
"bar": []byte("map-bar-value"),
|
||||
}, nil)
|
||||
tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) {
|
||||
Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionFalse, 0.0)).To(BeTrue())
|
||||
Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionTrue, 1.0)).To(BeTrue())
|
||||
Eventually(func() bool {
|
||||
Expect(syncCallsTotal.WithLabelValues(ExternalSecretName, ExternalSecretNamespace).Write(&metric)).To(Succeed())
|
||||
return metric.GetCounter().GetValue() == 1.0
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
|
||||
// check values
|
||||
Expect(string(secret.Data["foo"])).To(Equal("map-foo-value"))
|
||||
Expect(string(secret.Data["bar"])).To(Equal("map-bar-value"))
|
||||
|
@ -703,6 +770,80 @@ var _ = Describe("ExternalSecret controller", func() {
|
|||
}
|
||||
}
|
||||
|
||||
// When the ownership is set to owner, and we delete a dependent child kind=secret
|
||||
// it should be recreated without waiting for refresh interval
|
||||
checkDeletion := func(tc *testCase) {
|
||||
const secretVal = "someValue"
|
||||
fakeProvider.WithGetSecret([]byte(secretVal), nil)
|
||||
tc.externalSecret.Spec.RefreshInterval = &metav1.Duration{Duration: time.Minute * 10}
|
||||
tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) {
|
||||
|
||||
// check values
|
||||
oldUID := secret.UID
|
||||
Expect(oldUID).NotTo(BeEmpty())
|
||||
|
||||
// delete the related config
|
||||
Expect(k8sClient.Delete(context.TODO(), secret))
|
||||
|
||||
var newSecret v1.Secret
|
||||
secretLookupKey := types.NamespacedName{
|
||||
Name: ExternalSecretTargetSecretName,
|
||||
Namespace: ExternalSecretNamespace,
|
||||
}
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(context.Background(), secretLookupKey, &newSecret)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
// new secret should be a new, recreated object with a different UID
|
||||
return newSecret.UID != oldUID
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
}
|
||||
}
|
||||
|
||||
// Checks that secret annotation has been written based on the data
|
||||
checkSecretDataHashAnnotation := func(tc *testCase) {
|
||||
const secretVal = "someValue"
|
||||
fakeProvider.WithGetSecret([]byte(secretVal), nil)
|
||||
tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) {
|
||||
Expect(secret.Annotations[esv1alpha1.AnnotationDataHash]).To(Equal("9d30b95ca81e156f9454b5ef3bfcc6ee"))
|
||||
}
|
||||
}
|
||||
|
||||
// When we amend the created kind=secret, refresh operation should be run again regardless of refresh interval
|
||||
checkSecretDataHashAnnotationChange := func(tc *testCase) {
|
||||
fakeData := map[string][]byte{
|
||||
"targetProperty": []byte("map-foo-value"),
|
||||
}
|
||||
fakeProvider.WithGetSecretMap(fakeData, nil)
|
||||
tc.externalSecret.Spec.RefreshInterval = &metav1.Duration{Duration: time.Minute * 10}
|
||||
tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) {
|
||||
oldHash := secret.Annotations[esv1alpha1.AnnotationDataHash]
|
||||
oldResourceVersion := secret.ResourceVersion
|
||||
Expect(oldHash).NotTo(BeEmpty())
|
||||
|
||||
cleanSecret := secret.DeepCopy()
|
||||
secret.Data["new"] = []byte("value")
|
||||
secret.ObjectMeta.Annotations[esv1alpha1.AnnotationDataHash] = "thisiswronghash"
|
||||
Expect(k8sClient.Patch(context.Background(), secret, client.MergeFrom(cleanSecret))).To(Succeed())
|
||||
|
||||
var refreshedSecret v1.Secret
|
||||
secretLookupKey := types.NamespacedName{
|
||||
Name: ExternalSecretTargetSecretName,
|
||||
Namespace: ExternalSecretNamespace,
|
||||
}
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(context.Background(), secretLookupKey, &refreshedSecret)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
// refreshed secret should have a different generation (sign that it was updated), but since
|
||||
// the secret source is the same (not changed), the hash should be reverted to an old value
|
||||
return refreshedSecret.ResourceVersion != oldResourceVersion && refreshedSecret.Annotations[esv1alpha1.AnnotationDataHash] == oldHash
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
}
|
||||
}
|
||||
|
||||
DescribeTable("When reconciling an ExternalSecret",
|
||||
func(tweaks ...testTweaks) {
|
||||
tc := makeDefaultTestcase()
|
||||
|
@ -739,9 +880,13 @@ var _ = Describe("ExternalSecret controller", func() {
|
|||
tc.checkSecret(createdES, syncedSecret)
|
||||
}
|
||||
},
|
||||
Entry("should recreate deleted secret", checkDeletion),
|
||||
Entry("should create proper hash annotation for the external secret", checkSecretDataHashAnnotation),
|
||||
Entry("should refresh when the hash annotation doesn't correspond to secret data", checkSecretDataHashAnnotationChange),
|
||||
Entry("should set the condition eventually", syncLabelsAnnotations),
|
||||
Entry("should set prometheus counters", checkPrometheusCounters),
|
||||
Entry("should merge with existing secret using creationPolicy=Merge", mergeWithSecret),
|
||||
Entry("should error if sceret 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 sync with template", syncWithTemplate),
|
||||
Entry("should sync template with correct value precedence", syncWithTemplatePrecedence),
|
||||
|
@ -766,7 +911,6 @@ var _ = Describe("ExternalSecret refresh logic", func() {
|
|||
},
|
||||
})).To(BeTrue())
|
||||
})
|
||||
|
||||
It("should refresh when labels change", func() {
|
||||
es := esv1alpha1.ExternalSecret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
|
|
|
@ -14,7 +14,13 @@ limitations under the License.
|
|||
|
||||
package utils
|
||||
|
||||
import "reflect"
|
||||
import (
|
||||
|
||||
// nolint:gosec
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// MergeByteMap merges map of byte slices.
|
||||
func MergeByteMap(dst, src map[string][]byte) map[string][]byte {
|
||||
|
@ -35,3 +41,10 @@ func MergeStringMap(dest, src map[string]string) {
|
|||
func IsNil(i interface{}) bool {
|
||||
return i == nil || reflect.ValueOf(i).IsNil()
|
||||
}
|
||||
|
||||
// ObjectHash calculates md5 sum of the data contained in the secret.
|
||||
// nolint:gosec
|
||||
func ObjectHash(object interface{}) string {
|
||||
textualVersion := fmt.Sprintf("%+v", object)
|
||||
return fmt.Sprintf("%x", md5.Sum([]byte(textualVersion)))
|
||||
}
|
||||
|
|
62
pkg/utils/utils_test.go
Normal file
62
pkg/utils/utils_test.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
func TestObjectHash(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input interface{}
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "A nil should be still working",
|
||||
input: nil,
|
||||
want: "60046f14c917c18a9a0f923e191ba0dc",
|
||||
},
|
||||
{
|
||||
name: "We accept a simple scalar value, i.e. string",
|
||||
input: "hello there",
|
||||
want: "161bc25962da8fed6d2f59922fb642aa",
|
||||
},
|
||||
{
|
||||
name: "A complex object like a secret is not an issue",
|
||||
input: v1.Secret{Data: map[string][]byte{
|
||||
"xx": []byte("yyy"),
|
||||
}},
|
||||
want: "a9fe13fd43b20829b45f0a93372413dd",
|
||||
},
|
||||
{
|
||||
name: "map also works",
|
||||
input: map[string][]byte{
|
||||
"foo": []byte("value1"),
|
||||
"bar": []byte("value2"),
|
||||
},
|
||||
want: "caa0155759a6a9b3b6ada5a6883ee2bb",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := ObjectHash(tt.input); got != tt.want {
|
||||
t.Errorf("ObjectHash() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
1
tools.go
1
tools.go
|
@ -1,3 +1,4 @@
|
|||
//go:build tools
|
||||
// +build tools
|
||||
|
||||
package tools
|
||||
|
|
Loading…
Reference in a new issue