Propagate connection secrets
This commit allows constant propagation of connection secrets from managed resources to their bound resource claims. It does this by updating the existing APIManagedConnectionPropagator to add 'propagation annotations', which can be used by a new 'secret propagating reconciler' to watch both secrets for constant propagation. The predicates and enqueue handler required to implement this are included. Signed-off-by: Nic Cope <negz@rk0n.org>
This commit is contained in:
parent
19891e990a
commit
894481e637
|
|
@ -37,6 +37,7 @@ const (
|
|||
errUpdateClaim = "cannot update resource claim"
|
||||
errGetSecret = "cannot get managed resource's connection secret"
|
||||
errSecretConflict = "cannot establish control of existing connection secret"
|
||||
errUpdateSecret = "cannot update connection secret"
|
||||
errCreateOrUpdateSecret = "cannot create or update connection secret"
|
||||
errUpdateManaged = "cannot update managed resource"
|
||||
errUpdateManagedStatus = "cannot update managed resource status"
|
||||
|
|
@ -100,8 +101,16 @@ func (a *APIManagedConnectionPropagator) PropagateConnection(ctx context.Context
|
|||
return errors.Wrap(err, errGetSecret)
|
||||
}
|
||||
|
||||
// Make sure the managed resource is the controller of the connection secret
|
||||
// it references before we propagate it. This ensures a managed resource
|
||||
// cannot use Crossplane to circumvent RBAC by propagating a secret it does
|
||||
// not own.
|
||||
if c := metav1.GetControllerOf(mgcs); c == nil || c.UID != mg.GetUID() {
|
||||
return errors.New(errSecretConflict)
|
||||
}
|
||||
|
||||
cmcs := ConnectionSecretFor(cm, MustGetKind(cm, a.typer))
|
||||
_, err := util.CreateOrUpdate(ctx, a.client, cmcs, func() error {
|
||||
if _, err := util.CreateOrUpdate(ctx, a.client, cmcs, func() error {
|
||||
// Inside this anonymous function cmcs could either be unchanged (if
|
||||
// it does not exist in the API server) or updated to reflect its
|
||||
// current state according to the API server.
|
||||
|
|
@ -109,10 +118,23 @@ func (a *APIManagedConnectionPropagator) PropagateConnection(ctx context.Context
|
|||
return errors.New(errSecretConflict)
|
||||
}
|
||||
cmcs.Data = mgcs.Data
|
||||
meta.AddAnnotations(cmcs, map[string]string{
|
||||
AnnotationKeyPropagateFromNamespace: mgcs.GetNamespace(),
|
||||
AnnotationKeyPropagateFromName: mgcs.GetName(),
|
||||
AnnotationKeyPropagateFromUID: string(mgcs.GetUID()),
|
||||
})
|
||||
return nil
|
||||
}); err != nil {
|
||||
return errors.Wrap(err, errCreateOrUpdateSecret)
|
||||
}
|
||||
|
||||
meta.AddAnnotations(mgcs, map[string]string{
|
||||
AnnotationKeyPropagateToNamespace: cmcs.GetNamespace(),
|
||||
AnnotationKeyPropagateToName: cmcs.GetName(),
|
||||
AnnotationKeyPropagateToUID: string(cmcs.GetUID()),
|
||||
})
|
||||
|
||||
return errors.Wrap(err, errCreateOrUpdateSecret)
|
||||
return errors.Wrap(a.client.Update(ctx, mgcs), errUpdateSecret)
|
||||
}
|
||||
|
||||
// An APIManagedBinder binds resources to claims by updating them in a
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@ func TestPropagateConnection(t *testing.T) {
|
|||
|
||||
cmname := "coolclaim"
|
||||
mgname := "coolmanaged"
|
||||
uid := types.UID("definitely-a-uuid")
|
||||
cmcsname := "coolclaimsecret"
|
||||
mgcsname := "coolmanagedsecret"
|
||||
mgcsdata := map[string][]byte{"cool": []byte("data")}
|
||||
|
|
@ -219,12 +220,24 @@ func TestPropagateConnection(t *testing.T) {
|
|||
fields: fields{
|
||||
client: &test.MockClient{
|
||||
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
|
||||
s := &corev1.Secret{}
|
||||
s.SetOwnerReferences([]metav1.OwnerReference{{
|
||||
UID: types.UID("some-other-uuid"),
|
||||
Controller: &controller,
|
||||
}})
|
||||
*o.(*corev1.Secret) = *s
|
||||
switch n.Name {
|
||||
case cmcsname:
|
||||
s := &corev1.Secret{}
|
||||
s.SetOwnerReferences([]metav1.OwnerReference{{
|
||||
UID: types.UID("some-other-uuid"),
|
||||
Controller: &controller,
|
||||
}})
|
||||
*o.(*corev1.Secret) = *s
|
||||
case mgcsname:
|
||||
s := &corev1.Secret{}
|
||||
s.SetOwnerReferences([]metav1.OwnerReference{{
|
||||
UID: uid,
|
||||
Controller: &controller,
|
||||
}})
|
||||
*o.(*corev1.Secret) = *s
|
||||
default:
|
||||
return errors.New("unexpected secret name")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
MockUpdate: test.NewMockUpdateFn(nil),
|
||||
|
|
@ -238,7 +251,7 @@ func TestPropagateConnection(t *testing.T) {
|
|||
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: corev1.LocalObjectReference{Name: cmcsname}},
|
||||
},
|
||||
mg: &MockManaged{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: mgname},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: mgname, UID: uid},
|
||||
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: corev1.LocalObjectReference{Name: mgcsname}},
|
||||
},
|
||||
},
|
||||
|
|
@ -248,8 +261,20 @@ func TestPropagateConnection(t *testing.T) {
|
|||
fields: fields{
|
||||
client: &test.MockClient{
|
||||
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
|
||||
s := &corev1.Secret{}
|
||||
*o.(*corev1.Secret) = *s
|
||||
switch n.Name {
|
||||
case mgcsname:
|
||||
s := &corev1.Secret{}
|
||||
s.SetOwnerReferences([]metav1.OwnerReference{{
|
||||
UID: uid,
|
||||
Controller: &controller,
|
||||
}})
|
||||
*o.(*corev1.Secret) = *s
|
||||
case cmcsname:
|
||||
// A secret without any owner references.
|
||||
*o.(*corev1.Secret) = corev1.Secret{}
|
||||
default:
|
||||
return errors.New("unexpected secret name")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
MockUpdate: test.NewMockUpdateFn(nil),
|
||||
|
|
@ -263,33 +288,39 @@ func TestPropagateConnection(t *testing.T) {
|
|||
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: corev1.LocalObjectReference{Name: cmcsname}},
|
||||
},
|
||||
mg: &MockManaged{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: mgname},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: mgname, UID: uid},
|
||||
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: corev1.LocalObjectReference{Name: mgcsname}},
|
||||
},
|
||||
},
|
||||
want: errors.Wrap(errors.New(errSecretConflict), errCreateOrUpdateSecret),
|
||||
},
|
||||
"Successful": {
|
||||
"ManagedSecretUpdateError": {
|
||||
fields: fields{
|
||||
client: &test.MockClient{
|
||||
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
|
||||
if n.Name == mgcsname {
|
||||
*o.(*corev1.Secret) = corev1.Secret{Data: mgcsdata}
|
||||
|
||||
switch n.Name {
|
||||
case mgcsname:
|
||||
s := corev1.Secret{}
|
||||
s.SetNamespace(namespace)
|
||||
s.SetUID(uid)
|
||||
s.SetOwnerReferences([]metav1.OwnerReference{{UID: uid, Controller: &controller}})
|
||||
s.SetName(mgcsname)
|
||||
s.Data = mgcsdata
|
||||
*o.(*corev1.Secret) = s
|
||||
case cmcsname:
|
||||
default:
|
||||
return errors.New("unexpected secret name")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
MockUpdate: test.NewMockUpdateFn(nil, func(got runtime.Object) error {
|
||||
want := &corev1.Secret{}
|
||||
want.SetName(cmcsname)
|
||||
want.SetOwnerReferences([]metav1.OwnerReference{{
|
||||
Name: cmname,
|
||||
APIVersion: MockGVK(&MockClaim{}).GroupVersion().String(),
|
||||
Kind: MockGVK(&MockClaim{}).Kind,
|
||||
Controller: &controller,
|
||||
}})
|
||||
want.Data = mgcsdata
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("-want, +got:\n%s", diff)
|
||||
switch got.(metav1.Object).GetName() {
|
||||
case cmcsname:
|
||||
case mgcsname:
|
||||
return errBoom
|
||||
default:
|
||||
return errors.New("unexpected secret name")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
|
|
@ -303,7 +334,78 @@ func TestPropagateConnection(t *testing.T) {
|
|||
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: corev1.LocalObjectReference{Name: cmcsname}},
|
||||
},
|
||||
mg: &MockManaged{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: mgname},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: mgname, UID: uid},
|
||||
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: corev1.LocalObjectReference{Name: mgcsname}},
|
||||
},
|
||||
},
|
||||
want: errors.Wrap(errBoom, errUpdateSecret),
|
||||
},
|
||||
"Successful": {
|
||||
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)
|
||||
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{
|
||||
AnnotationKeyPropagateToNamespace: namespace,
|
||||
AnnotationKeyPropagateToName: cmcsname,
|
||||
AnnotationKeyPropagateToUID: string(uid),
|
||||
})
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("-want, +got:\n%s", diff)
|
||||
}
|
||||
default:
|
||||
return errors.New("unexpected secret name")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
},
|
||||
typer: MockSchemeWith(&MockClaim{}, &MockManaged{}),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
cm: &MockClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: cmname, UID: uid},
|
||||
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: corev1.LocalObjectReference{Name: cmcsname}},
|
||||
},
|
||||
mg: &MockManaged{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: mgname, UID: uid},
|
||||
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: corev1.LocalObjectReference{Name: mgcsname}},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ type ManagedConfigurator interface {
|
|||
Configure(ctx context.Context, cm Claim, cs NonPortableClass, mg Managed) error
|
||||
}
|
||||
|
||||
// A ManagedConfiguratorFn is a function that sastisfies the
|
||||
// A ManagedConfiguratorFn is a function that satisfies the
|
||||
// ManagedConfigurator interface.
|
||||
type ManagedConfiguratorFn func(ctx context.Context, cm Claim, cs NonPortableClass, mg Managed) error
|
||||
|
||||
|
|
@ -93,7 +93,7 @@ type ManagedCreator interface {
|
|||
Create(ctx context.Context, cm Claim, cs NonPortableClass, mg Managed) error
|
||||
}
|
||||
|
||||
// A ManagedCreatorFn is a function that sastisfies the ManagedCreator interface.
|
||||
// A ManagedCreatorFn is a function that satisfies the ManagedCreator interface.
|
||||
type ManagedCreatorFn func(ctx context.Context, cm Claim, cs NonPortableClass, mg Managed) error
|
||||
|
||||
// Create the supplied resource.
|
||||
|
|
@ -108,7 +108,7 @@ type ManagedConnectionPropagator interface {
|
|||
PropagateConnection(ctx context.Context, cm Claim, mg Managed) error
|
||||
}
|
||||
|
||||
// A ManagedConnectionPropagatorFn is a function that sastisfies the
|
||||
// A ManagedConnectionPropagatorFn is a function that satisfies the
|
||||
// ManagedConnectionPropagator interface.
|
||||
type ManagedConnectionPropagatorFn func(ctx context.Context, cm Claim, mg Managed) error
|
||||
|
||||
|
|
@ -123,7 +123,7 @@ type ManagedBinder interface {
|
|||
Bind(ctx context.Context, cm Claim, mg Managed) error
|
||||
}
|
||||
|
||||
// A ManagedBinderFn is a function that sastisfies the ManagedBinder interface.
|
||||
// A ManagedBinderFn is a function that satisfies the ManagedBinder interface.
|
||||
type ManagedBinderFn func(ctx context.Context, cm Claim, mg Managed) error
|
||||
|
||||
// Bind the supplied resource claim to the supplied managed resource.
|
||||
|
|
@ -136,7 +136,7 @@ type ManagedFinalizer interface {
|
|||
Finalize(ctx context.Context, cm Managed) error
|
||||
}
|
||||
|
||||
// A ManagedFinalizerFn is a function that sastisfies the ManagedFinalizer interface.
|
||||
// A ManagedFinalizerFn is a function that satisfies the ManagedFinalizer interface.
|
||||
type ManagedFinalizerFn func(ctx context.Context, cm Managed) error
|
||||
|
||||
// Finalize the supplied managed resource.
|
||||
|
|
@ -149,7 +149,7 @@ type ClaimFinalizer interface {
|
|||
Finalize(ctx context.Context, cm Claim) error
|
||||
}
|
||||
|
||||
// A ClaimFinalizerFn is a function that sastisfies the ClaimFinalizer interface.
|
||||
// A ClaimFinalizerFn is a function that satisfies the ClaimFinalizer interface.
|
||||
type ClaimFinalizerFn func(ctx context.Context, cm Claim) error
|
||||
|
||||
// Finalize the supplied managed resource.
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package resource
|
|||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
|
@ -25,6 +26,10 @@ import (
|
|||
"github.com/crossplaneio/crossplane-runtime/pkg/meta"
|
||||
)
|
||||
|
||||
type adder interface {
|
||||
Add(item interface{})
|
||||
}
|
||||
|
||||
// EnqueueRequestForClaim enqueues a reconcile.Request for the NamespacedName
|
||||
// of a ClaimReferencer's ClaimReference.
|
||||
type EnqueueRequestForClaim struct{}
|
||||
|
|
@ -54,12 +59,58 @@ func (e *EnqueueRequestForClaim) Generic(evt event.GenericEvent, q workqueue.Rat
|
|||
addClaim(evt.Object, q)
|
||||
}
|
||||
|
||||
type adder interface {
|
||||
Add(item interface{})
|
||||
}
|
||||
|
||||
func addClaim(obj runtime.Object, queue adder) {
|
||||
if cr, ok := obj.(ClaimReferencer); ok && cr.GetClaimReference() != nil {
|
||||
queue.Add(reconcile.Request{NamespacedName: meta.NamespacedNameOf(cr.GetClaimReference())})
|
||||
}
|
||||
}
|
||||
|
||||
// EnqueueRequestForPropagator enqueues a reconcile.Request for the
|
||||
// NamespacedName of a propagated object, i.e. an object with propagation
|
||||
// metadata annotations.
|
||||
type EnqueueRequestForPropagator 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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 addPropagator(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],
|
||||
}})
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ import (
|
|||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
|
|
@ -46,7 +47,6 @@ func TestAddClaim(t *testing.T) {
|
|||
queue adder
|
||||
}{
|
||||
"ObjectIsNotAClaimReferencer": {
|
||||
obj: &corev1.Pod{},
|
||||
queue: addFn(func(_ interface{}) { t.Errorf("queue.Add() called unexpectedly") }),
|
||||
},
|
||||
"ObjectHasNilClaimReference": {
|
||||
|
|
@ -63,7 +63,6 @@ func TestAddClaim(t *testing.T) {
|
|||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("-want, +got:\n%s", diff)
|
||||
}
|
||||
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
|
@ -72,3 +71,45 @@ func TestAddClaim(t *testing.T) {
|
|||
addClaim(tc.obj, tc.queue)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddPropagator(t *testing.T) {
|
||||
ns := "coolns"
|
||||
name := "coolname"
|
||||
|
||||
cases := map[string]struct {
|
||||
obj runtime.Object
|
||||
queue adder
|
||||
}{
|
||||
"ObjectIsNotAnnotated": {
|
||||
queue: addFn(func(_ interface{}) { t.Errorf("queue.Add() called unexpectedly") }),
|
||||
},
|
||||
"ObjectMissing" + AnnotationKeyPropagateFromNamespace: {
|
||||
obj: &MockManaged{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{
|
||||
AnnotationKeyPropagateFromName: name,
|
||||
}}},
|
||||
queue: addFn(func(_ interface{}) { t.Errorf("queue.Add() called unexpectedly") }),
|
||||
},
|
||||
"ObjectMissing" + AnnotationKeyPropagateFromName: {
|
||||
obj: &MockManaged{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{
|
||||
AnnotationKeyPropagateFromNamespace: ns,
|
||||
}}},
|
||||
queue: addFn(func(_ interface{}) { t.Errorf("queue.Add() called unexpectedly") }),
|
||||
},
|
||||
"IsPropagatedObject": {
|
||||
obj: &MockManaged{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{
|
||||
AnnotationKeyPropagateFromNamespace: ns,
|
||||
AnnotationKeyPropagateFromName: name,
|
||||
}}},
|
||||
queue: addFn(func(got interface{}) {
|
||||
want := reconcile.Request{NamespacedName: types.NamespacedName{Namespace: ns, Name: name}}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("-want, +got:\n%s", diff)
|
||||
}
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
addPropagator(tc.obj, tc.queue)
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ package resource
|
|||
import (
|
||||
"context"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
|
@ -81,6 +82,70 @@ func IsManagedKind(k ManagedKind, ot runtime.ObjectTyper) PredicateFn {
|
|||
}
|
||||
}
|
||||
|
||||
// IsControlledByKind accepts objects that are controlled by a resource of the
|
||||
// supplied kind.
|
||||
func IsControlledByKind(k schema.GroupVersionKind) PredicateFn {
|
||||
return func(obj runtime.Object) bool {
|
||||
mo, ok := obj.(metav1.Object)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
ref := metav1.GetControllerOf(mo)
|
||||
if ref == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return ref.APIVersion == k.GroupVersion().String() && ref.Kind == k.Kind
|
||||
}
|
||||
}
|
||||
|
||||
// IsPropagator accepts objects that request to be partially or fully propagated
|
||||
// to another object of the same kind.
|
||||
func IsPropagator() PredicateFn {
|
||||
return func(obj runtime.Object) bool {
|
||||
ao, ok := obj.(annotated)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
a := ao.GetAnnotations()
|
||||
switch {
|
||||
case a[AnnotationKeyPropagateToNamespace] == "":
|
||||
return false
|
||||
case a[AnnotationKeyPropagateToName] == "":
|
||||
return false
|
||||
case a[AnnotationKeyPropagateToUID] == "":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IsPropagated accepts objects that consent to be partially or fully propagated
|
||||
// from another object of the same kind.
|
||||
func IsPropagated() PredicateFn {
|
||||
return func(obj runtime.Object) bool {
|
||||
ao, ok := obj.(annotated)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
a := ao.GetAnnotations()
|
||||
switch {
|
||||
case a[AnnotationKeyPropagateFromNamespace] == "":
|
||||
return false
|
||||
case a[AnnotationKeyPropagateFromName] == "":
|
||||
return false
|
||||
case a[AnnotationKeyPropagateFromUID] == "":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HasIndirectClassReferenceKind accepts namespaced objects that reference the
|
||||
// supplied non-portable class kind via the supplied portable class kind.
|
||||
func HasIndirectClassReferenceKind(c client.Client, oc runtime.ObjectCreater, k ClassKinds) PredicateFn {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import (
|
|||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
|
@ -140,6 +141,162 @@ func TestIsManagedKind(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestIsControlledByKind(t *testing.T) {
|
||||
controller := true
|
||||
|
||||
cases := map[string]struct {
|
||||
kind schema.GroupVersionKind
|
||||
obj runtime.Object
|
||||
want bool
|
||||
}{
|
||||
"NoObjectMeta": {
|
||||
want: false,
|
||||
},
|
||||
"NoControllerRef": {
|
||||
obj: &corev1.Secret{},
|
||||
want: false,
|
||||
},
|
||||
"WrongAPIVersion": {
|
||||
kind: MockGVK(&MockManaged{}),
|
||||
obj: &corev1.Secret{ObjectMeta: v1.ObjectMeta{OwnerReferences: []v1.OwnerReference{
|
||||
{
|
||||
Kind: MockGVK(&MockManaged{}).Kind,
|
||||
Controller: &controller,
|
||||
},
|
||||
}}},
|
||||
want: false,
|
||||
},
|
||||
"WrongKind": {
|
||||
kind: MockGVK(&MockManaged{}),
|
||||
obj: &corev1.Secret{ObjectMeta: v1.ObjectMeta{OwnerReferences: []v1.OwnerReference{
|
||||
{
|
||||
APIVersion: MockGVK(&MockManaged{}).GroupVersion().String(),
|
||||
Controller: &controller,
|
||||
},
|
||||
}}},
|
||||
want: false,
|
||||
},
|
||||
"IsControlledByKind": {
|
||||
kind: MockGVK(&MockManaged{}),
|
||||
obj: &corev1.Secret{ObjectMeta: v1.ObjectMeta{OwnerReferences: []v1.OwnerReference{
|
||||
{
|
||||
APIVersion: MockGVK(&MockManaged{}).GroupVersion().String(),
|
||||
Kind: MockGVK(&MockManaged{}).Kind,
|
||||
Controller: &controller,
|
||||
},
|
||||
}}},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := IsControlledByKind(tc.kind)(tc.obj)
|
||||
if diff := cmp.Diff(tc.want, got); diff != "" {
|
||||
t.Errorf("IsControlledByKind(...): -want, +got:\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPropagator(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
obj runtime.Object
|
||||
want bool
|
||||
}{
|
||||
"NotAnAnnotator": {
|
||||
want: false,
|
||||
},
|
||||
"Missing" + AnnotationKeyPropagateToNamespace: {
|
||||
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,
|
||||
}}},
|
||||
want: false,
|
||||
},
|
||||
"IsPropagator": {
|
||||
obj: &corev1.Secret{ObjectMeta: v1.ObjectMeta{Annotations: map[string]string{
|
||||
AnnotationKeyPropagateToNamespace: namespace,
|
||||
AnnotationKeyPropagateToName: name,
|
||||
AnnotationKeyPropagateToUID: string(uid),
|
||||
}}},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := IsPropagator()(tc.obj)
|
||||
if diff := cmp.Diff(tc.want, got); diff != "" {
|
||||
t.Errorf("IsPropagator(...): -want, +got:\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPropagated(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
obj runtime.Object
|
||||
want bool
|
||||
}{
|
||||
"NotAnAnnotator": {
|
||||
want: false,
|
||||
},
|
||||
"Missing" + AnnotationKeyPropagateFromNamespace: {
|
||||
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,
|
||||
}}},
|
||||
want: false,
|
||||
},
|
||||
"IsPropagated": {
|
||||
obj: &corev1.Secret{ObjectMeta: v1.ObjectMeta{Annotations: map[string]string{
|
||||
AnnotationKeyPropagateFromNamespace: namespace,
|
||||
AnnotationKeyPropagateFromName: name,
|
||||
AnnotationKeyPropagateFromUID: string(uid),
|
||||
}}},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := IsPropagated()(tc.obj)
|
||||
if diff := cmp.Diff(tc.want, got); diff != "" {
|
||||
t.Errorf("IsPropagated(...): -want, +got:\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasIndirectClassReferenceKind(t *testing.T) {
|
||||
errUnexpected := errors.New("unexpected object type")
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
Copyright 2019 The Crossplane Authors.
|
||||
|
||||
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 resource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
"github.com/crossplaneio/crossplane-runtime/pkg/logging"
|
||||
)
|
||||
|
||||
// Supported resources with all of these annotations will be fully or partially
|
||||
// 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"
|
||||
)
|
||||
|
||||
// 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"
|
||||
)
|
||||
|
||||
type annotated interface {
|
||||
GetAnnotations() map[string]string
|
||||
}
|
||||
|
||||
const (
|
||||
secretControllerName = "secretpropagator.crossplane.io"
|
||||
secretReconcileTimeout = 1 * time.Minute
|
||||
)
|
||||
|
||||
// NewSecretPropagatingReconciler returns a Reconciler that reconciles secrets
|
||||
// by propagating their data to another secret. Both secrets must consent to
|
||||
// this process by including propagation annotations. The Reconciler assumes it
|
||||
// has a watch on both propagating (from) and propagated (to) secrets.
|
||||
func NewSecretPropagatingReconciler(m manager.Manager) reconcile.Reconciler {
|
||||
client := m.GetClient()
|
||||
|
||||
return reconcile.Func(func(req reconcile.Request) (reconcile.Result, error) {
|
||||
log.V(logging.Debug).Info("Reconciling", "controller", secretControllerName, "request", req)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), secretReconcileTimeout)
|
||||
defer cancel()
|
||||
|
||||
// The 'from' secret is also know as the 'propagating' secret.
|
||||
from := &corev1.Secret{}
|
||||
if err := client.Get(ctx, req.NamespacedName, 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
|
||||
// implicitly because we return an error.
|
||||
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
|
||||
}
|
||||
|
||||
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.
|
||||
return reconcile.Result{Requeue: false}, errors.Wrap(client.Update(ctx, to), errUpdateSecret)
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,402 @@
|
|||
/*
|
||||
Copyright 2019 The Crossplane Authors.
|
||||
|
||||
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 resource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
"github.com/crossplaneio/crossplane-runtime/pkg/test"
|
||||
)
|
||||
|
||||
func TestSecretPropagatingReconciler(t *testing.T) {
|
||||
type args struct {
|
||||
m manager.Manager
|
||||
}
|
||||
|
||||
type want struct {
|
||||
result reconcile.Result
|
||||
err error
|
||||
}
|
||||
|
||||
ns := "namespace"
|
||||
|
||||
fromName := "from"
|
||||
fromUID := types.UID("from-uid")
|
||||
fromData := map[string][]byte{"cool": []byte("data")}
|
||||
|
||||
toName := "to"
|
||||
toUID := types.UID("to-uid")
|
||||
|
||||
errBoom := errors.New("boom")
|
||||
|
||||
cases := map[string]struct {
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
"FromNotFound": {
|
||||
args: args{
|
||||
m: &MockManager{
|
||||
c: &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: &MockManager{
|
||||
c: &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: &MockManager{
|
||||
c: &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
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
result: reconcile.Result{},
|
||||
},
|
||||
},
|
||||
"GetToError": {
|
||||
args: args{
|
||||
m: &MockManager{
|
||||
c: &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 errBoom
|
||||
default:
|
||||
return errors.New("unexpected secret name")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
err: errors.Wrap(errBoom, errGetSecret),
|
||||
},
|
||||
},
|
||||
"UnexpectedToUID": {
|
||||
args: args{
|
||||
m: &MockManager{
|
||||
c: &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: &MockManager{
|
||||
c: &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:
|
||||
*s = corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: ns,
|
||||
Name: toName,
|
||||
UID: toUID,
|
||||
Annotations: map[string]string{
|
||||
AnnotationKeyPropagateFromNamespace: ns,
|
||||
AnnotationKeyPropagateFromName: fromName,
|
||||
AnnotationKeyPropagateFromUID: "some-other-uuid",
|
||||
},
|
||||
},
|
||||
}
|
||||
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{},
|
||||
},
|
||||
},
|
||||
"UpdateToError": {
|
||||
args: args{
|
||||
m: &MockManager{
|
||||
c: &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:
|
||||
*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 errBoom
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
err: errors.Wrap(errBoom, errUpdateSecret),
|
||||
},
|
||||
},
|
||||
"Successful": {
|
||||
args: args{
|
||||
m: &MockManager{
|
||||
c: &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:
|
||||
*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 {
|
||||
want := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: ns,
|
||||
Name: toName,
|
||||
UID: toUID,
|
||||
Annotations: map[string]string{
|
||||
AnnotationKeyPropagateFromNamespace: ns,
|
||||
AnnotationKeyPropagateFromName: fromName,
|
||||
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{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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}})
|
||||
|
||||
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
|
||||
t.Errorf("r.Reconcile(...): -want error, +got error:\n%s", diff)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tc.want.result, got); diff != "" {
|
||||
t.Errorf("r.Reconcile(...): -want, +got:\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue