secret reconciler: allow for propagation to multiple secrets (#92)

Signed-off-by: hasheddan <georgedanielmangum@gmail.com>
This commit is contained in:
Daniel Mangum 2020-01-06 13:51:27 -08:00 committed by Nic Cope
parent efd6c68c68
commit d5c9dedd2a
8 changed files with 412 additions and 297 deletions

View File

@ -18,6 +18,7 @@ package resource
import (
"context"
"strings"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
@ -134,9 +135,7 @@ func (a *APIManagedConnectionPropagator) PropagateConnection(ctx context.Context
}
meta.AddAnnotations(mgcs, map[string]string{
AnnotationKeyPropagateToNamespace: cmcs.GetNamespace(),
AnnotationKeyPropagateToName: cmcs.GetName(),
AnnotationKeyPropagateToUID: string(cmcs.GetUID()),
strings.Join([]string{AnnotationKeyPropagateToPrefix, string(cmcs.GetUID())}, SlashDelimeter): strings.Join([]string{cmcs.GetNamespace(), cmcs.GetName()}, SlashDelimeter),
})
return errors.Wrap(a.client.Update(ctx, mgcs), errUpdateSecret)

View File

@ -18,6 +18,7 @@ package resource
import (
"context"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
@ -404,9 +405,84 @@ func TestPropagateConnection(t *testing.T) {
case mgcsname:
want.SetName(mgcsname)
want.SetAnnotations(map[string]string{
AnnotationKeyPropagateToNamespace: namespace,
AnnotationKeyPropagateToName: cmcsname,
AnnotationKeyPropagateToUID: string(uid),
strings.Join([]string{AnnotationKeyPropagateToPrefix, string(uid)}, SlashDelimeter): strings.Join([]string{namespace, cmcsname}, SlashDelimeter),
})
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
default:
return errors.New("unexpected secret name")
}
return nil
}),
},
typer: fake.SchemeWith(&fake.Claim{}, &fake.Managed{}),
},
args: args{
ctx: context.Background(),
cm: &fake.Claim{
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: cmname, UID: uid},
LocalConnectionSecretWriterTo: fake.LocalConnectionSecretWriterTo{
Ref: &v1alpha1.LocalSecretReference{Name: cmcsname},
},
},
mg: &fake.Managed{
ObjectMeta: metav1.ObjectMeta{Name: mgname, UID: uid},
ConnectionSecretWriterTo: fake.ConnectionSecretWriterTo{
Ref: &v1alpha1.SecretReference{Namespace: mgcsnamespace, Name: mgcsname},
},
},
},
want: nil,
},
"SuccessfulWithExisting": {
fields: fields{
client: &test.MockClient{
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
s := corev1.Secret{}
s.SetNamespace(namespace)
s.SetUID(uid)
s.SetOwnerReferences([]metav1.OwnerReference{{UID: uid, Controller: &controller}})
switch n.Name {
case mgcsname:
s.SetName(mgcsname)
meta.AddAnnotations(&s, map[string]string{
strings.Join([]string{AnnotationKeyPropagateToPrefix, "existing-uid"}, SlashDelimeter): "existing-namespace/existing-name",
})
s.Data = mgcsdata
*o.(*corev1.Secret) = s
case cmcsname:
s.SetName(cmcsname)
*o.(*corev1.Secret) = s
default:
return errors.New("unexpected secret name")
}
return nil
},
MockUpdate: test.NewMockUpdateFn(nil, func(got runtime.Object) error {
want := &corev1.Secret{}
want.SetNamespace(namespace)
want.SetUID(uid)
want.SetOwnerReferences([]metav1.OwnerReference{{UID: uid, Controller: &controller}})
want.Data = mgcsdata
switch got.(metav1.Object).GetName() {
case cmcsname:
want.SetName(cmcsname)
want.SetAnnotations(map[string]string{
AnnotationKeyPropagateFromNamespace: namespace,
AnnotationKeyPropagateFromName: mgcsname,
AnnotationKeyPropagateFromUID: string(uid),
})
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
case mgcsname:
want.SetName(mgcsname)
want.SetAnnotations(map[string]string{
strings.Join([]string{AnnotationKeyPropagateToPrefix, "existing-uid"}, SlashDelimeter): "existing-namespace/existing-name",
strings.Join([]string{AnnotationKeyPropagateToPrefix, string(uid)}, SlashDelimeter): strings.Join([]string{namespace, cmcsname}, SlashDelimeter),
})
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
@ -490,7 +566,7 @@ func TestBind(t *testing.T) {
err: errors.Wrap(errBoom, errUpdateManaged),
cm: &fake.Claim{BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound}},
mg: &fake.Managed{
ClaimReferencer: fake.ClaimReferencer{meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
ClaimReferencer: fake.ClaimReferencer{Ref: meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
},
},
@ -522,7 +598,7 @@ func TestBind(t *testing.T) {
},
mg: &fake.Managed{
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{meta.ExternalNameAnnotationKey: externalName}},
ClaimReferencer: fake.ClaimReferencer{meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
ClaimReferencer: fake.ClaimReferencer{Ref: meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
},
},
@ -539,7 +615,7 @@ func TestBind(t *testing.T) {
err: nil,
cm: &fake.Claim{BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound}},
mg: &fake.Managed{
ClaimReferencer: fake.ClaimReferencer{meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
ClaimReferencer: fake.ClaimReferencer{Ref: meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
},
},
@ -561,7 +637,7 @@ func TestBind(t *testing.T) {
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound}},
mg: &fake.Managed{
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{meta.ExternalNameAnnotationKey: externalName}},
ClaimReferencer: fake.ClaimReferencer{meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
ClaimReferencer: fake.ClaimReferencer{Ref: meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
},
},
@ -626,7 +702,7 @@ func TestStatusBind(t *testing.T) {
err: errors.Wrap(errBoom, errUpdateManaged),
cm: &fake.Claim{BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound}},
mg: &fake.Managed{
ClaimReferencer: fake.ClaimReferencer{meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
ClaimReferencer: fake.ClaimReferencer{Ref: meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
},
},
},
@ -645,7 +721,7 @@ func TestStatusBind(t *testing.T) {
err: errors.Wrap(errBoom, errUpdateManagedStatus),
cm: &fake.Claim{BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound}},
mg: &fake.Managed{
ClaimReferencer: fake.ClaimReferencer{meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
ClaimReferencer: fake.ClaimReferencer{Ref: meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
},
},
@ -680,7 +756,7 @@ func TestStatusBind(t *testing.T) {
},
mg: &fake.Managed{
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{meta.ExternalNameAnnotationKey: externalName}},
ClaimReferencer: fake.ClaimReferencer{meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
ClaimReferencer: fake.ClaimReferencer{Ref: meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
},
},
@ -700,7 +776,7 @@ func TestStatusBind(t *testing.T) {
err: nil,
cm: &fake.Claim{BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound}},
mg: &fake.Managed{
ClaimReferencer: fake.ClaimReferencer{meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
ClaimReferencer: fake.ClaimReferencer{Ref: meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
},
},
@ -726,7 +802,7 @@ func TestStatusBind(t *testing.T) {
},
mg: &fake.Managed{
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{meta.ExternalNameAnnotationKey: externalName}},
ClaimReferencer: fake.ClaimReferencer{meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
ClaimReferencer: fake.ClaimReferencer{Ref: meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
},
},

View File

@ -17,6 +17,8 @@ limitations under the License.
package resource
import (
"strings"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/workqueue"
@ -65,52 +67,62 @@ func addClaim(obj runtime.Object, queue adder) {
}
}
// EnqueueRequestForPropagator enqueues a reconcile.Request for the
// EnqueueRequestForPropagated enqueues a reconcile.Request for the
// NamespacedName of a propagated object, i.e. an object with propagation
// metadata annotations.
type EnqueueRequestForPropagator struct{}
type EnqueueRequestForPropagated struct{}
// Create adds a NamespacedName for the supplied CreateEvent if its Object is
// propagated.
func (e *EnqueueRequestForPropagator) Create(evt event.CreateEvent, q workqueue.RateLimitingInterface) {
addPropagator(evt.Object, q)
func (e *EnqueueRequestForPropagated) Create(evt event.CreateEvent, q workqueue.RateLimitingInterface) {
addPropagated(evt.Object, q)
}
// Update adds a NamespacedName for the supplied UpdateEvent if its Objects are
// propagated.
func (e *EnqueueRequestForPropagator) Update(evt event.UpdateEvent, q workqueue.RateLimitingInterface) {
addPropagator(evt.ObjectOld, q)
addPropagator(evt.ObjectNew, q)
func (e *EnqueueRequestForPropagated) Update(evt event.UpdateEvent, q workqueue.RateLimitingInterface) {
addPropagated(evt.ObjectOld, q)
addPropagated(evt.ObjectNew, q)
}
// Delete adds a NamespacedName for the supplied DeleteEvent if its Object is
// propagated.
func (e *EnqueueRequestForPropagator) Delete(evt event.DeleteEvent, q workqueue.RateLimitingInterface) {
addPropagator(evt.Object, q)
func (e *EnqueueRequestForPropagated) Delete(evt event.DeleteEvent, q workqueue.RateLimitingInterface) {
addPropagated(evt.Object, q)
}
// Generic adds a NamespacedName for the supplied GenericEvent if its Object is
// propagated.
func (e *EnqueueRequestForPropagator) Generic(evt event.GenericEvent, q workqueue.RateLimitingInterface) {
addPropagator(evt.Object, q)
func (e *EnqueueRequestForPropagated) Generic(evt event.GenericEvent, q workqueue.RateLimitingInterface) {
addPropagated(evt.Object, q)
}
func addPropagator(obj runtime.Object, queue adder) {
func addPropagated(obj runtime.Object, queue adder) {
ao, ok := obj.(annotated)
if !ok {
return
}
a := ao.GetAnnotations()
switch {
case a[AnnotationKeyPropagateFromNamespace] == "":
return
case a[AnnotationKeyPropagateFromName] == "":
return
default:
queue.Add(reconcile.Request{NamespacedName: types.NamespacedName{
Namespace: a[AnnotationKeyPropagateFromNamespace],
Name: a[AnnotationKeyPropagateFromName],
}})
for key, val := range a {
if !strings.HasPrefix(key, AnnotationKeyPropagateToPrefix) {
continue
}
t := strings.Split(val, SlashDelimeter)
if len(t) != 2 {
continue
}
switch {
case t[0] == "":
continue
case t[1] == "":
continue
default:
queue.Add(reconcile.Request{NamespacedName: types.NamespacedName{
Namespace: t[0],
Name: t[1],
}})
}
}
}

View File

@ -17,6 +17,7 @@ limitations under the License.
package resource
import (
"strings"
"testing"
"github.com/google/go-cmp/cmp"
@ -74,9 +75,10 @@ func TestAddClaim(t *testing.T) {
}
}
func TestAddPropagator(t *testing.T) {
func TestAddPropagated(t *testing.T) {
ns := "coolns"
name := "coolname"
uid := "a-cool-uid"
cases := map[string]struct {
obj runtime.Object
@ -85,22 +87,15 @@ func TestAddPropagator(t *testing.T) {
"ObjectIsNotAnnotated": {
queue: addFn(func(_ interface{}) { t.Errorf("queue.Add() called unexpectedly") }),
},
"ObjectMissing" + AnnotationKeyPropagateFromNamespace: {
"ObjectMissing" + AnnotationKeyPropagateToPrefix: {
obj: &fake.Managed{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{
AnnotationKeyPropagateFromName: name,
"some.annotation": "some-value",
}}},
queue: addFn(func(_ interface{}) { t.Errorf("queue.Add() called unexpectedly") }),
},
"ObjectMissing" + AnnotationKeyPropagateFromName: {
"IsPropagatorObject": {
obj: &fake.Managed{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{
AnnotationKeyPropagateFromNamespace: ns,
}}},
queue: addFn(func(_ interface{}) { t.Errorf("queue.Add() called unexpectedly") }),
},
"IsPropagatedObject": {
obj: &fake.Managed{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{
AnnotationKeyPropagateFromNamespace: ns,
AnnotationKeyPropagateFromName: name,
strings.Join([]string{AnnotationKeyPropagateToPrefix, uid}, SlashDelimeter): strings.Join([]string{ns, name}, SlashDelimeter),
}}},
queue: addFn(func(got interface{}) {
want := reconcile.Request{NamespacedName: types.NamespacedName{Namespace: ns, Name: name}}
@ -112,6 +107,6 @@ func TestAddPropagator(t *testing.T) {
}
for _, tc := range cases {
addPropagator(tc.obj, tc.queue)
addPropagated(tc.obj, tc.queue)
}
}

View File

@ -17,6 +17,8 @@ limitations under the License.
package resource
import (
"strings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
@ -134,17 +136,13 @@ func IsPropagator() PredicateFn {
return false
}
a := ao.GetAnnotations()
switch {
case a[AnnotationKeyPropagateToNamespace] == "":
return false
case a[AnnotationKeyPropagateToName] == "":
return false
case a[AnnotationKeyPropagateToUID] == "":
return false
default:
return true
for key := range ao.GetAnnotations() {
if strings.HasPrefix(key, AnnotationKeyPropagateToPrefix) {
return true
}
}
return false
}
}

View File

@ -283,32 +283,22 @@ func TestIsPropagator(t *testing.T) {
"NotAnAnnotator": {
want: false,
},
"Missing" + AnnotationKeyPropagateToNamespace: {
"NotAPropagator": {
obj: &corev1.Secret{ObjectMeta: v1.ObjectMeta{Annotations: map[string]string{
AnnotationKeyPropagateToName: name,
AnnotationKeyPropagateToUID: string(uid),
}}},
want: false,
},
"Missing" + AnnotationKeyPropagateToName: {
obj: &corev1.Secret{ObjectMeta: v1.ObjectMeta{Annotations: map[string]string{
AnnotationKeyPropagateToNamespace: namespace,
AnnotationKeyPropagateToUID: string(uid),
}}},
want: false,
},
"Missing" + AnnotationKeyPropagateToUID: {
obj: &corev1.Secret{ObjectMeta: v1.ObjectMeta{Annotations: map[string]string{
AnnotationKeyPropagateToNamespace: namespace,
AnnotationKeyPropagateToName: name,
"some.annotation": "someValue",
}}},
want: false,
},
"IsPropagator": {
obj: &corev1.Secret{ObjectMeta: v1.ObjectMeta{Annotations: map[string]string{
AnnotationKeyPropagateToNamespace: namespace,
AnnotationKeyPropagateToName: name,
AnnotationKeyPropagateToUID: string(uid),
AnnotationKeyPropagateToPrefix + "cool-uid": "cool-namespace/cool-name",
}}},
want: true,
},
"IsMultiPropagator": {
obj: &corev1.Secret{ObjectMeta: v1.ObjectMeta{Annotations: map[string]string{
AnnotationKeyPropagateToPrefix + "cool-uid": "cool-namespace/cool-name",
AnnotationKeyPropagateToPrefix + "cool-uid-two": "cool-namespace/cool-name-2",
}}},
want: true,
},
@ -332,24 +322,9 @@ func TestIsPropagated(t *testing.T) {
"NotAnAnnotator": {
want: false,
},
"Missing" + AnnotationKeyPropagateFromNamespace: {
"NotPropagated": {
obj: &corev1.Secret{ObjectMeta: v1.ObjectMeta{Annotations: map[string]string{
AnnotationKeyPropagateFromName: name,
AnnotationKeyPropagateFromUID: string(uid),
}}},
want: false,
},
"Missing" + AnnotationKeyPropagateFromName: {
obj: &corev1.Secret{ObjectMeta: v1.ObjectMeta{Annotations: map[string]string{
AnnotationKeyPropagateFromNamespace: namespace,
AnnotationKeyPropagateFromUID: string(uid),
}}},
want: false,
},
"Missing" + AnnotationKeyPropagateFromUID: {
obj: &corev1.Secret{ObjectMeta: v1.ObjectMeta{Annotations: map[string]string{
AnnotationKeyPropagateFromNamespace: namespace,
AnnotationKeyPropagateFromName: name,
"some.annotation": "someValue",
}}},
want: false,
},

View File

@ -18,6 +18,7 @@ package resource
import (
"context"
"strings"
"time"
"github.com/pkg/errors"
@ -33,17 +34,12 @@ import (
// propagated to the named resource of the same kind, assuming it exists and
// consents to propagation.
const (
AnnotationKeyPropagateToNamespace = "crossplane.io/propagate-to-namespace"
AnnotationKeyPropagateToName = "crossplane.io/propagate-to-name"
AnnotationKeyPropagateToUID = "crossplane.io/propagate-to-uid"
)
AnnotationKeyPropagateToPrefix = "to.propagate.crossplane.io"
SlashDelimeter = "/"
// Supported resources with all of these annotations consent to be fully or
// partially propagated from the named resource of the same kind.
const (
AnnotationKeyPropagateFromNamespace = "crossplane.io/propagate-from-namespace"
AnnotationKeyPropagateFromName = "crossplane.io/propagate-from-name"
AnnotationKeyPropagateFromUID = "crossplane.io/propagate-from-uid"
AnnotationKeyPropagateFromNamespace = "from.propagate.crossplane.io/namespace"
AnnotationKeyPropagateFromName = "from.propagate.crossplane.io/name"
AnnotationKeyPropagateFromUID = "from.propagate.crossplane.io/uid"
)
type annotated interface {
@ -53,6 +49,8 @@ type annotated interface {
const (
secretControllerName = "secretpropagator.crossplane.io"
secretReconcileTimeout = 1 * time.Minute
errUnexpectedFromUID = "unexpected propagate from uid on propagated secret"
errUnexpectedToUID = "unexpected propagate to uid on propagator secret"
)
// NewSecretPropagatingReconciler returns a Reconciler that reconciles secrets
@ -68,9 +66,26 @@ func NewSecretPropagatingReconciler(m manager.Manager) reconcile.Reconciler {
ctx, cancel := context.WithTimeout(context.Background(), secretReconcileTimeout)
defer cancel()
// The 'to' secret is also known as the 'propagated' secret. We guard
// against abusers of the propagation process by requiring that both
// secrets consent to propagation by specifying each other's UID. We
// cannot know the UID of a secret that doesn't exist, so the propagated
// secret must be created outside of the propagation process.
to := &corev1.Secret{}
if err := client.Get(ctx, req.NamespacedName, to); err != nil {
// There's no propagation to be done if the secret we propagate to
// does not exist. We assume we have a watch on that secret and will
// be queued if/when it is created. Otherwise we'll be requeued
// implicitly because we return an error.
return reconcile.Result{}, errors.Wrap(IgnoreNotFound(err), errGetSecret)
}
// The 'from' secret is also know as the 'propagating' secret.
from := &corev1.Secret{}
if err := client.Get(ctx, req.NamespacedName, from); err != nil {
n := types.NamespacedName{
Namespace: to.GetAnnotations()[AnnotationKeyPropagateFromNamespace],
Name: to.GetAnnotations()[AnnotationKeyPropagateFromName],
}
if err := client.Get(ctx, n, from); err != nil {
// There's no propagation to be done if the secret we're propagating
// from does not exist. We assume we have a watch on that secret and
// will be queued if/when it is created. Otherwise we'll be requeued
@ -78,43 +93,24 @@ func NewSecretPropagatingReconciler(m manager.Manager) reconcile.Reconciler {
return reconcile.Result{}, errors.Wrap(IgnoreNotFound(err), errGetSecret)
}
// The 'to' secret is also known as the 'propagated' secret. We guard
// against abusers of the propagation process by requiring that both
// secrets consent to propagation by specifying each other's UID. We
// cannot know the UID of a secret that doesn't exist, so the propagated
// secret must be created outside of the propagation process.
to := &corev1.Secret{}
n := types.NamespacedName{
Namespace: from.GetAnnotations()[AnnotationKeyPropagateToNamespace],
Name: from.GetAnnotations()[AnnotationKeyPropagateToName],
}
if err := client.Get(ctx, n, to); err != nil {
// There's no propagation to be done if the secret we propagate to
// does not exist. We assume we have a watch on that secret and will
// be queued if/when it is created. Otherwise we'll be requeued
// implicitly because we return an error.
return reconcile.Result{}, errors.Wrap(IgnoreNotFound(err), errGetSecret)
}
if from.GetAnnotations()[AnnotationKeyPropagateToUID] != string(to.GetUID()) {
// The propagating secret expected a different propagated secret. We
// assume we have a watch on both secrets, and will be requeued if
// and when this situation is remedied.
return reconcile.Result{}, nil
}
if to.GetAnnotations()[AnnotationKeyPropagateFromUID] != string(from.GetUID()) {
// The propagated secret expected a different propagating secret. We
// assume we have a watch on both secrets, and will be requeued if
// and when this situation is remedied.
return reconcile.Result{}, nil
return reconcile.Result{}, errors.New(errUnexpectedFromUID)
}
if _, ok := from.GetAnnotations()[strings.Join([]string{AnnotationKeyPropagateToPrefix, string(to.GetUID())}, SlashDelimeter)]; !ok {
// The propagating secret expected a different propagated secret. We
// assume we have a watch on both secrets, and will be requeued if
// and when this situation is remedied.
return reconcile.Result{}, errors.New(errUnexpectedToUID)
}
to.Data = from.Data
// If our update was successful there's nothing else to do. We assume we
// have a watch on both secrets and will be queued if either changes.
// Otherwise we'll be requeued implicitly because we return an error.
// If our update was unsuccessful. Keep trying to update
// additional secrets but implicitly requeue when finished.
return reconcile.Result{Requeue: false}, errors.Wrap(client.Update(ctx, to), errUpdateSecret)
})
}

View File

@ -18,6 +18,7 @@ package resource
import (
"context"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
@ -60,71 +61,17 @@ func TestSecretPropagatingReconciler(t *testing.T) {
args args
want want
}{
"FromNotFound": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
switch n.Name {
case fromName:
return kerrors.NewNotFound(schema.GroupResource{}, "")
default:
return errors.New("unexpected secret name")
}
},
},
},
},
want: want{
result: reconcile.Result{},
},
},
"GetFromError": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
switch n.Name {
case fromName:
return errBoom
default:
return errors.New("unexpected secret name")
}
},
},
},
},
want: want{
err: errors.Wrap(errBoom, errGetSecret),
},
},
"ToNotFound": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
s := o.(*corev1.Secret)
switch n.Name {
case fromName:
*s = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: fromName,
UID: fromUID,
Annotations: map[string]string{
AnnotationKeyPropagateToNamespace: ns,
AnnotationKeyPropagateToName: toName,
AnnotationKeyPropagateToUID: string(toUID),
},
},
Data: fromData,
}
case toName:
return kerrors.NewNotFound(schema.GroupResource{}, "")
default:
return errors.New("unexpected secret name")
}
return nil
},
},
},
@ -134,27 +81,79 @@ func TestSecretPropagatingReconciler(t *testing.T) {
},
},
"GetToError": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
switch n.Name {
case toName:
return errBoom
default:
return errors.New("unexpected secret name")
}
},
},
},
},
want: want{
err: errors.Wrap(errBoom, errGetSecret),
},
},
"FromNotFound": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
s := o.(*corev1.Secret)
switch n.Name {
case fromName:
case toName:
*s = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: fromName,
UID: fromUID,
Name: toName,
UID: toUID,
Annotations: map[string]string{
AnnotationKeyPropagateToNamespace: ns,
AnnotationKeyPropagateToName: toName,
AnnotationKeyPropagateToUID: string(toUID),
AnnotationKeyPropagateFromName: fromName,
AnnotationKeyPropagateFromNamespace: ns,
AnnotationKeyPropagateFromUID: string(fromUID),
},
},
Data: fromData,
}
case fromName:
return kerrors.NewNotFound(schema.GroupResource{}, fromName)
default:
return errors.New("unexpected secret name")
}
return nil
},
},
},
},
want: want{
result: reconcile.Result{},
},
},
"GetFromError": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
s := o.(*corev1.Secret)
switch n.Name {
case toName:
*s = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: toName,
UID: toUID,
Annotations: map[string]string{
AnnotationKeyPropagateFromName: fromName,
AnnotationKeyPropagateFromNamespace: ns,
AnnotationKeyPropagateFromUID: string(fromUID),
},
},
}
case fromName:
return errBoom
default:
return errors.New("unexpected secret name")
@ -168,56 +167,6 @@ func TestSecretPropagatingReconciler(t *testing.T) {
err: errors.Wrap(errBoom, errGetSecret),
},
},
"UnexpectedToUID": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
s := o.(*corev1.Secret)
switch n.Name {
case fromName:
*s = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: fromName,
UID: fromUID,
Annotations: map[string]string{
AnnotationKeyPropagateToNamespace: ns,
AnnotationKeyPropagateToName: toName,
AnnotationKeyPropagateToUID: "some-other-uuid",
},
},
Data: fromData,
}
case toName:
*s = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: toName,
UID: toUID,
Annotations: map[string]string{
AnnotationKeyPropagateFromNamespace: ns,
AnnotationKeyPropagateFromName: fromName,
AnnotationKeyPropagateFromUID: string(fromUID),
},
},
}
default:
return errors.New("unexpected secret name")
}
return nil
},
MockUpdate: test.NewMockUpdateFn(nil, func(got runtime.Object) error {
return errors.New("called unexpectedly")
}),
},
},
},
want: want{
result: reconcile.Result{},
},
},
"UnexpectedFromUID": {
args: args{
m: &fake.Manager{
@ -226,20 +175,6 @@ func TestSecretPropagatingReconciler(t *testing.T) {
s := o.(*corev1.Secret)
switch n.Name {
case fromName:
*s = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: fromName,
UID: fromUID,
Annotations: map[string]string{
AnnotationKeyPropagateToNamespace: ns,
AnnotationKeyPropagateToName: toName,
AnnotationKeyPropagateToUID: string(toUID),
},
},
Data: fromData,
}
case toName:
*s = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
@ -247,11 +182,24 @@ func TestSecretPropagatingReconciler(t *testing.T) {
Name: toName,
UID: toUID,
Annotations: map[string]string{
AnnotationKeyPropagateFromNamespace: ns,
AnnotationKeyPropagateFromName: fromName,
AnnotationKeyPropagateFromUID: "some-other-uuid",
AnnotationKeyPropagateFromNamespace: ns,
AnnotationKeyPropagateFromUID: "some-other-UID",
},
},
Data: fromData,
}
case fromName:
*s = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: fromName,
UID: fromUID,
Annotations: map[string]string{
strings.Join([]string{AnnotationKeyPropagateToPrefix, string(toUID)}, SlashDelimeter): strings.Join([]string{ns, toName}, SlashDelimeter),
},
},
Data: fromData,
}
default:
return errors.New("unexpected secret name")
@ -266,6 +214,57 @@ func TestSecretPropagatingReconciler(t *testing.T) {
},
want: want{
result: reconcile.Result{},
err: errors.New(errUnexpectedFromUID),
},
},
"UnexpectedToUID": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
s := o.(*corev1.Secret)
switch n.Name {
case toName:
*s = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: toName,
UID: toUID,
Annotations: map[string]string{
AnnotationKeyPropagateFromName: fromName,
AnnotationKeyPropagateFromNamespace: ns,
AnnotationKeyPropagateFromUID: string(fromUID),
},
},
Data: fromData,
}
case fromName:
*s = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: fromName,
UID: fromUID,
Annotations: map[string]string{
strings.Join([]string{AnnotationKeyPropagateToPrefix, "some-other-uid"}, SlashDelimeter): strings.Join([]string{ns, toName}, SlashDelimeter),
},
},
Data: fromData,
}
default:
return errors.New("unexpected secret name")
}
return nil
},
MockUpdate: test.NewMockUpdateFn(nil, func(got runtime.Object) error {
return errors.New("called unexpectedly")
}),
},
},
},
want: want{
result: reconcile.Result{},
err: errors.New(errUnexpectedToUID),
},
},
"UpdateToError": {
@ -276,20 +275,6 @@ func TestSecretPropagatingReconciler(t *testing.T) {
s := o.(*corev1.Secret)
switch n.Name {
case fromName:
*s = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: fromName,
UID: fromUID,
Annotations: map[string]string{
AnnotationKeyPropagateToNamespace: ns,
AnnotationKeyPropagateToName: toName,
AnnotationKeyPropagateToUID: string(toUID),
},
},
Data: fromData,
}
case toName:
*s = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
@ -297,11 +282,24 @@ func TestSecretPropagatingReconciler(t *testing.T) {
Name: toName,
UID: toUID,
Annotations: map[string]string{
AnnotationKeyPropagateFromNamespace: ns,
AnnotationKeyPropagateFromName: fromName,
AnnotationKeyPropagateFromNamespace: ns,
AnnotationKeyPropagateFromUID: string(fromUID),
},
},
Data: fromData,
}
case fromName:
*s = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: fromName,
UID: fromUID,
Annotations: map[string]string{
strings.Join([]string{AnnotationKeyPropagateToPrefix, string(toUID)}, SlashDelimeter): strings.Join([]string{ns, toName}, SlashDelimeter),
},
},
Data: fromData,
}
default:
return errors.New("unexpected secret name")
@ -318,7 +316,7 @@ func TestSecretPropagatingReconciler(t *testing.T) {
err: errors.Wrap(errBoom, errUpdateSecret),
},
},
"Successful": {
"SuccessfulSingle": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
@ -326,20 +324,6 @@ func TestSecretPropagatingReconciler(t *testing.T) {
s := o.(*corev1.Secret)
switch n.Name {
case fromName:
*s = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: fromName,
UID: fromUID,
Annotations: map[string]string{
AnnotationKeyPropagateToNamespace: ns,
AnnotationKeyPropagateToName: toName,
AnnotationKeyPropagateToUID: string(toUID),
},
},
Data: fromData,
}
case toName:
*s = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
@ -347,11 +331,24 @@ func TestSecretPropagatingReconciler(t *testing.T) {
Name: toName,
UID: toUID,
Annotations: map[string]string{
AnnotationKeyPropagateFromNamespace: ns,
AnnotationKeyPropagateFromName: fromName,
AnnotationKeyPropagateFromNamespace: ns,
AnnotationKeyPropagateFromUID: string(fromUID),
},
},
Data: fromData,
}
case fromName:
*s = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: fromName,
UID: fromUID,
Annotations: map[string]string{
strings.Join([]string{AnnotationKeyPropagateToPrefix, string(toUID)}, SlashDelimeter): strings.Join([]string{ns, toName}, SlashDelimeter),
},
},
Data: fromData,
}
default:
return errors.New("unexpected secret name")
@ -365,8 +362,75 @@ func TestSecretPropagatingReconciler(t *testing.T) {
Name: toName,
UID: toUID,
Annotations: map[string]string{
AnnotationKeyPropagateFromNamespace: ns,
AnnotationKeyPropagateFromName: fromName,
AnnotationKeyPropagateFromNamespace: ns,
AnnotationKeyPropagateFromUID: string(fromUID),
},
},
Data: fromData,
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
},
},
want: want{
result: reconcile.Result{},
},
},
"SuccessfulMultiple": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
s := o.(*corev1.Secret)
switch n.Name {
case toName:
*s = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: toName,
UID: toUID,
Annotations: map[string]string{
AnnotationKeyPropagateFromName: fromName,
AnnotationKeyPropagateFromNamespace: ns,
AnnotationKeyPropagateFromUID: string(fromUID),
},
},
Data: fromData,
}
case fromName:
*s = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: fromName,
UID: fromUID,
Annotations: map[string]string{
strings.Join([]string{AnnotationKeyPropagateToPrefix, string(toUID)}, SlashDelimeter): strings.Join([]string{ns, toName}, SlashDelimeter),
strings.Join([]string{AnnotationKeyPropagateToPrefix, "some-uid"}, SlashDelimeter): strings.Join([]string{ns, toName}, SlashDelimeter),
strings.Join([]string{AnnotationKeyPropagateToPrefix, "some-other-uid"}, SlashDelimeter): strings.Join([]string{ns, toName}, SlashDelimeter),
},
},
Data: fromData,
}
default:
return errors.New("unexpected secret name")
}
return nil
},
MockUpdate: test.NewMockUpdateFn(nil, func(got runtime.Object) error {
want := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: toName,
UID: toUID,
Annotations: map[string]string{
AnnotationKeyPropagateFromName: fromName,
AnnotationKeyPropagateFromNamespace: ns,
AnnotationKeyPropagateFromUID: string(fromUID),
},
},
@ -389,7 +453,7 @@ func TestSecretPropagatingReconciler(t *testing.T) {
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
r := NewSecretPropagatingReconciler(tc.args.m)
got, err := r.Reconcile(reconcile.Request{NamespacedName: types.NamespacedName{Namespace: ns, Name: fromName}})
got, err := r.Reconcile(reconcile.Request{NamespacedName: types.NamespacedName{Namespace: ns, Name: toName}})
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("r.Reconcile(...): -want error, +got error:\n%s", diff)