Implement Target interface and reconciler (#103)

Signed-off-by: hasheddan <georgedanielmangum@gmail.com>
This commit is contained in:
Daniel Mangum 2020-01-14 12:36:41 -08:00 committed by Nic Cope
parent d5c9dedd2a
commit 5ece4af54b
10 changed files with 813 additions and 15 deletions

View File

@ -36,7 +36,11 @@ const (
TypeSynced ConditionType = "Synced"
// TypeReferencesResolved resources' references are resolved
TypeReferencesResolved = "ReferencesResolved"
TypeReferencesResolved ConditionType = "ReferencesResolved"
// TypeSecretPropagated resources have had connection information
// propagated to their secret reference.
TypeSecretPropagated ConditionType = "ConnectionSecretPropagated"
)
// A ConditionReason represents the reason a resource is in a condition.
@ -56,12 +60,18 @@ const (
ReasonReconcileError ConditionReason = "Encountered an error during resource reconciliation"
)
// Reason references for a resource are or are not resolved
// Reason references for a resource are or are not resolved.
const (
ReasonReferenceResolveSuccess ConditionReason = "Successfully resolved resource references to other resources"
ReasonResolveReferencesBlocked ConditionReason = "One or more referenced resources do not exist, or are not yet Ready"
)
// Reason a referenced secret has or has not been propagated to.
const (
ReasonSecretPropagationSuccess ConditionReason = "Successfully propagated connection data to referenced secret"
ReasonSecretPropagationError ConditionReason = "Unable to propagate connection data to referenced secret"
)
// A Condition that may apply to a resource.
type Condition struct {
// Type of this condition. At most one of each condition type may apply to
@ -284,3 +294,28 @@ func ReferenceResolutionBlocked(err error) Condition {
Message: err.Error(),
}
}
// SecretPropagationSuccess returns a condition indicating that Crossplane
// successfully propagated connection data to the referenced secret.
func SecretPropagationSuccess() Condition {
return Condition{
Type: TypeSecretPropagated,
Status: corev1.ConditionTrue,
LastTransitionTime: metav1.Now(),
Reason: ReasonSecretPropagationSuccess,
}
}
// SecretPropagationError returns a condition indicating that Crossplane was
// unable to propagate connection data to the referenced secret. This could be
// because it was unable to find the managed resource that owns the secret to be
// propagated.
func SecretPropagationError(err error) Condition {
return Condition{
Type: TypeSecretPropagated,
Status: corev1.ConditionFalse,
LastTransitionTime: metav1.Now(),
Reason: ReasonSecretPropagationError,
Message: err.Error(),
}
}

View File

@ -208,3 +208,26 @@ type ProviderSpec struct {
// the credentials that are used to connect to the provider.
CredentialsSecretRef SecretKeySelector `json:"credentialsSecretRef"`
}
// A TargetSpec defines the common fields of objects used for exposing
// infrastructure to workloads that can be scheduled to.
type TargetSpec struct {
// WriteConnectionSecretToReference specifies the name of a Secret, in the
// same namespace as this target, to which any connection details for this
// target should be written or already exist. Connection secrets referenced
// by a target should contain information for connecting to a resource that
// allows for scheduling of workloads.
// +optional
WriteConnectionSecretToReference *LocalSecretReference `json:"connectionSecretRef,omitempty"`
// A ResourceReference specifies an existing managed resource, in any
// namespace, which this target should attempt to propagate a connection
// secret from.
// +optional
ResourceReference *corev1.ObjectReference `json:"clusterRef,omitempty"`
}
// A TargetStatus defines the observed status a target.
type TargetStatus struct {
ConditionedStatus `json:",inline"`
}

View File

@ -263,3 +263,44 @@ func (in *SecretReference) DeepCopy() *SecretReference {
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TargetSpec) DeepCopyInto(out *TargetSpec) {
*out = *in
if in.WriteConnectionSecretToReference != nil {
in, out := &in.WriteConnectionSecretToReference, &out.WriteConnectionSecretToReference
*out = new(LocalSecretReference)
**out = **in
}
if in.ResourceReference != nil {
in, out := &in.ResourceReference, &out.ResourceReference
*out = new(corev1.ObjectReference)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetSpec.
func (in *TargetSpec) DeepCopy() *TargetSpec {
if in == nil {
return nil
}
out := new(TargetSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TargetStatus) DeepCopyInto(out *TargetStatus) {
*out = *in
in.ConditionedStatus.DeepCopyInto(&out.ConditionedStatus)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetStatus.
func (in *TargetStatus) DeepCopy() *TargetStatus {
if in == nil {
return nil
}
out := new(TargetStatus)
in.DeepCopyInto(out)
return out
}

View File

@ -91,10 +91,10 @@ func NewAPIManagedConnectionPropagator(c client.Client, t runtime.ObjectTyper) *
}
// PropagateConnection details from the supplied resource to the supplied claim.
func (a *APIManagedConnectionPropagator) PropagateConnection(ctx context.Context, cm Claim, mg Managed) error {
func (a *APIManagedConnectionPropagator) PropagateConnection(ctx context.Context, tr Target, mg Managed) error {
// Either this resource does not expose a connection secret, or this claim
// does not want one.
if mg.GetWriteConnectionSecretToReference() == nil || cm.GetWriteConnectionSecretToReference() == nil {
if mg.GetWriteConnectionSecretToReference() == nil || tr.GetWriteConnectionSecretToReference() == nil {
return nil
}
@ -115,12 +115,12 @@ func (a *APIManagedConnectionPropagator) PropagateConnection(ctx context.Context
return errors.New(errSecretConflict)
}
cmcs := LocalConnectionSecretFor(cm, MustGetKind(cm, a.typer))
cmcs := LocalConnectionSecretFor(tr, MustGetKind(tr, a.typer))
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.
if c := metav1.GetControllerOf(cmcs); c == nil || c.UID != cm.GetUID() {
if c := metav1.GetControllerOf(cmcs); c == nil || c.UID != tr.GetUID() {
return errors.New(errSecretConflict)
}
cmcs.Data = mgcs.Data

View File

@ -107,16 +107,16 @@ func (fn ManagedCreatorFn) Create(ctx context.Context, cm Claim, cs Class, mg Ma
// required to connect to a managed resource (for example the connection secret)
// from the managed resource to its resource claim.
type ManagedConnectionPropagator interface {
PropagateConnection(ctx context.Context, cm Claim, mg Managed) error
PropagateConnection(ctx context.Context, cm Target, mg Managed) error
}
// A ManagedConnectionPropagatorFn is a function that satisfies the
// ManagedConnectionPropagator interface.
type ManagedConnectionPropagatorFn func(ctx context.Context, cm Claim, mg Managed) error
type ManagedConnectionPropagatorFn func(ctx context.Context, cm Target, mg Managed) error
// PropagateConnection information from the supplied managed resource to the
// supplied resource claim.
func (fn ManagedConnectionPropagatorFn) PropagateConnection(ctx context.Context, cm Claim, mg Managed) error {
func (fn ManagedConnectionPropagatorFn) PropagateConnection(ctx context.Context, cm Target, mg Managed) error {
return fn(ctx, cm, mg)
}

View File

@ -550,7 +550,7 @@ func TestClaimReconciler(t *testing.T) {
with: ManagedKind(fake.GVK(&fake.Managed{})),
o: []ClaimReconcilerOption{
WithManagedConnectionPropagator(ManagedConnectionPropagatorFn(
func(_ context.Context, _ Claim, _ Managed) error { return errBoom },
func(_ context.Context, _ Target, _ Managed) error { return errBoom },
)),
},
},
@ -594,7 +594,7 @@ func TestClaimReconciler(t *testing.T) {
with: ManagedKind(fake.GVK(&fake.Managed{})),
o: []ClaimReconcilerOption{
WithManagedConnectionPropagator(ManagedConnectionPropagatorFn(
func(_ context.Context, _ Claim, _ Managed) error { return nil },
func(_ context.Context, _ Target, _ Managed) error { return nil },
)),
WithClaimFinalizer(ClaimFinalizerFns{
AddFinalizerFn: func(_ context.Context, _ Claim) error { return errBoom }},
@ -641,7 +641,7 @@ func TestClaimReconciler(t *testing.T) {
with: ManagedKind(fake.GVK(&fake.Managed{})),
o: []ClaimReconcilerOption{
WithManagedConnectionPropagator(ManagedConnectionPropagatorFn(
func(_ context.Context, _ Claim, _ Managed) error { return nil },
func(_ context.Context, _ Target, _ Managed) error { return nil },
)),
WithClaimFinalizer(ClaimFinalizerFns{
AddFinalizerFn: func(_ context.Context, _ Claim) error { return nil }},

View File

@ -133,15 +133,15 @@ func (m *Reclaimer) GetReclaimPolicy() v1alpha1.ReclaimPolicy { return m.Policy
// CredentialsSecretReferencer is a mock that satisfies CredentialsSecretReferencer
// interface.
type CredentialsSecretReferencer struct{ Ref *v1alpha1.SecretKeySelector }
type CredentialsSecretReferencer struct{ Ref v1alpha1.SecretKeySelector }
// SetCredentialsSecretReference sets CredentialsSecretReference.
func (m *CredentialsSecretReferencer) SetCredentialsSecretReference(r *v1alpha1.SecretKeySelector) {
func (m *CredentialsSecretReferencer) SetCredentialsSecretReference(r v1alpha1.SecretKeySelector) {
m.Ref = r
}
// GetCredentialsSecretReference gets CredentialsSecretReference.
func (m *CredentialsSecretReferencer) GetCredentialsSecretReference() *v1alpha1.SecretKeySelector {
func (m *CredentialsSecretReferencer) GetCredentialsSecretReference() v1alpha1.SecretKeySelector {
return m.Ref
}
@ -243,6 +243,30 @@ func (m *Provider) DeepCopyObject() runtime.Object {
return out
}
// Target is a mock that implements Target interface.
type Target struct {
metav1.ObjectMeta
ManagedResourceReferencer
LocalConnectionSecretWriterTo
v1alpha1.ConditionedStatus
}
// GetObjectKind returns schema.ObjectKind.
func (m *Target) GetObjectKind() schema.ObjectKind {
return schema.EmptyObjectKind
}
// DeepCopyObject returns a deep copy of Target as runtime.Object.
func (m *Target) DeepCopyObject() runtime.Object {
out := &Target{}
j, err := json.Marshal(m)
if err != nil {
panic(err)
}
_ = json.Unmarshal(j, out)
return out
}
// Manager is a mock object that satisfies manager.Manager interface.
type Manager struct {
manager.Manager

View File

@ -137,3 +137,15 @@ type Provider interface {
CredentialsSecretReferencer
}
// A Target is a Kubernetes object that refers to credentials to connect
// to a deployment target. Target is a subset of the Claim interface.
type Target interface {
runtime.Object
metav1.Object
LocalConnectionSecretWriterTo
ManagedResourceReferencer
Conditioned
}

View File

@ -0,0 +1,123 @@
/*
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"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/crossplaneio/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplaneio/crossplane-runtime/pkg/logging"
"github.com/crossplaneio/crossplane-runtime/pkg/meta"
)
const (
targetControllerName = "kubernetestarget.crossplane.io"
targetReconcileTimeout = 1 * time.Minute
errGetTarget = "unable to get Target"
errManagedResourceIsNotBound = "managed resource in Target clusterRef is unbound"
errUpdateTarget = "unable to update Target"
)
// A TargetKind contains the type metadata for a kind of target resource.
type TargetKind schema.GroupVersionKind
// A TargetReconciler reconciles targets by propagating the secret of the
// referenced managed resource.
type TargetReconciler struct {
client client.Client
newTarget func() Target
newManaged func() Managed
propagator ManagedConnectionPropagator
}
// NewTargetReconciler returns a Reconciler that reconciles KubernetesTargets by
// propagating the referenced Kubernetes cluster's connection Secret to the
// namespace of the KubernetesTarget.
func NewTargetReconciler(m manager.Manager, of TargetKind, with ManagedKind) *TargetReconciler {
nt := func() Target { return MustCreateObject(schema.GroupVersionKind(of), m.GetScheme()).(Target) }
nr := func() Managed { return MustCreateObject(schema.GroupVersionKind(with), m.GetScheme()).(Managed) }
// Panic early if we've been asked to reconcile a target or resource kind
// that has not been registered with our controller manager's scheme.
_, _ = nt(), nr()
r := &TargetReconciler{
client: m.GetClient(),
newTarget: nt,
newManaged: nr,
propagator: NewAPIManagedConnectionPropagator(m.GetClient(), m.GetScheme()),
}
return r
}
// Reconcile a target with a concrete managed resource.
func (r *TargetReconciler) Reconcile(req reconcile.Request) (reconcile.Result, error) {
log.V(logging.Debug).Info("Reconciling", "controller", targetControllerName, "request", req)
ctx, cancel := context.WithTimeout(context.Background(), targetReconcileTimeout)
defer cancel()
target := r.newTarget()
if err := r.client.Get(ctx, req.NamespacedName, target); err != nil {
// There's no need to requeue if we no longer exist. Otherwise we'll be
// requeued implicitly because we return an error.
return reconcile.Result{}, errors.Wrap(IgnoreNotFound(err), errGetTarget)
}
if target.GetWriteConnectionSecretToReference() == nil {
// If the ConnectionSecretRef is not set on this Target, we generate a
// Secret name that matches the UID of the Target. We are implicitly
// requeued because of the Target update.
target.SetWriteConnectionSecretToReference(&v1alpha1.LocalSecretReference{Name: string(target.GetUID())})
return reconcile.Result{}, errors.Wrap(r.client.Update(ctx, target), errUpdateTarget)
}
if meta.WasDeleted(target) {
// If the Target was deleted, there is nothing left for us to do.
return reconcile.Result{Requeue: false}, nil
}
managed := r.newManaged()
if err := r.client.Get(ctx, meta.NamespacedNameOf(target.GetResourceReference()), managed); err != nil {
target.SetConditions(v1alpha1.SecretPropagationError(err))
return reconcile.Result{RequeueAfter: aShortWait}, errors.Wrap(r.client.Status().Update(ctx, target), errUpdateTarget)
}
if !IsBound(managed) {
target.SetConditions(v1alpha1.SecretPropagationError(errors.New(errManagedResourceIsNotBound)))
return reconcile.Result{RequeueAfter: aShortWait}, errors.Wrap(r.client.Status().Update(ctx, target), errUpdateTarget)
}
if err := r.propagator.PropagateConnection(ctx, target, managed); err != nil {
// If we fail to propagate the connection secret of a bound managed resource, we try again after a short wait.
target.SetConditions(v1alpha1.SecretPropagationError(err))
return reconcile.Result{RequeueAfter: aShortWait}, errors.Wrap(r.client.Status().Update(ctx, target), errUpdateTarget)
}
// No need to requeue.
return reconcile.Result{Requeue: false}, nil
}

View File

@ -0,0 +1,540 @@
/*
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"
"strings"
"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/apis/core/v1alpha1"
"github.com/crossplaneio/crossplane-runtime/pkg/resource/fake"
"github.com/crossplaneio/crossplane-runtime/pkg/test"
)
func TestTargetReconciler(t *testing.T) {
type args struct {
m manager.Manager
of TargetKind
with ManagedKind
}
type want struct {
result reconcile.Result
err error
}
now := metav1.Now()
ns := "namespace"
tgname := "cooltarget"
mgname := "coolmanaged"
tguid := types.UID("tg-uuid")
mguid := types.UID("mg-uuid")
tgcsuid := types.UID("tgcs-uuid")
mgcsuid := types.UID("mgcs-uuid")
tgcsname := "cooltargetsecret"
mgcsname := "coolmanagedsecret"
mgcsnamespace := "coolns"
mgcsdata := map[string][]byte{"cool": []byte("data")}
controller := true
errBoom := errors.New("boom")
errUnexpectedSecret := errors.New("unexpected secret name")
errUnexpected := errors.New("unexpected object type")
cases := map[string]struct {
args args
want want
}{
"ErrorGetTarget": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
switch o := o.(type) {
case *fake.Target:
*o = fake.Target{}
return errBoom
default:
return errUnexpected
}
},
},
Scheme: fake.SchemeWith(&fake.Target{}, &fake.Managed{}),
},
of: TargetKind(fake.GVK(&fake.Target{})),
with: ManagedKind(fake.GVK(&fake.Managed{})),
},
want: want{
result: reconcile.Result{},
err: errors.Wrap(errBoom, errGetTarget),
},
},
"SuccessTargetHasNoSecretRef": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
switch o := o.(type) {
case *fake.Target:
tg := &fake.Target{ObjectMeta: metav1.ObjectMeta{
UID: tguid,
Name: tgname,
Namespace: ns,
}}
tg.SetResourceReference(&corev1.ObjectReference{
Name: mgname,
})
*o = *tg
return nil
default:
return errUnexpected
}
},
MockUpdate: test.NewMockUpdateFn(nil, func(got runtime.Object) error {
want := &fake.Target{}
want.SetName(tgname)
want.SetNamespace(ns)
want.SetUID(tguid)
want.SetResourceReference(&corev1.ObjectReference{
Name: mgname,
})
want.SetWriteConnectionSecretToReference(&v1alpha1.LocalSecretReference{
Name: string(tguid),
})
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Target{}, &fake.Managed{}),
},
of: TargetKind(fake.GVK(&fake.Target{})),
with: ManagedKind(fake.GVK(&fake.Managed{})),
},
want: want{
result: reconcile.Result{},
},
},
"TargetWasDeleted": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
switch o := o.(type) {
case *fake.Target:
tg := &fake.Target{ObjectMeta: metav1.ObjectMeta{
UID: tguid,
Name: tgname,
Namespace: ns,
}}
tg.SetResourceReference(&corev1.ObjectReference{
Name: mgname,
})
tg.SetWriteConnectionSecretToReference(&v1alpha1.LocalSecretReference{
Name: tgcsname,
})
tg.SetDeletionTimestamp(&now)
*o = *tg
return nil
default:
return errUnexpected
}
},
},
Scheme: fake.SchemeWith(&fake.Target{}, &fake.Managed{}),
},
of: TargetKind(fake.GVK(&fake.Target{})),
with: ManagedKind(fake.GVK(&fake.Managed{})),
},
want: want{
result: reconcile.Result{Requeue: false},
},
},
"TargetNotFound": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
switch o := o.(type) {
case *fake.Target:
*o = fake.Target{}
return kerrors.NewNotFound(schema.GroupResource{}, "")
default:
return errUnexpected
}
},
},
Scheme: fake.SchemeWith(&fake.Target{}, &fake.Managed{}),
},
of: TargetKind(fake.GVK(&fake.Target{})),
with: ManagedKind(fake.GVK(&fake.Managed{})),
},
want: want{
result: reconcile.Result{},
},
},
"ErrorGetManaged": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
switch o := o.(type) {
case *fake.Target:
tg := &fake.Target{ObjectMeta: metav1.ObjectMeta{
UID: tguid,
Name: tgname,
Namespace: ns,
}}
tg.SetResourceReference(&corev1.ObjectReference{
Name: mgname,
})
tg.SetWriteConnectionSecretToReference(&v1alpha1.LocalSecretReference{
Name: tgcsname,
})
*o = *tg
return nil
case *fake.Managed:
*o = fake.Managed{}
return errBoom
default:
return errUnexpected
}
},
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
want := &fake.Target{}
want.SetName(tgname)
want.SetNamespace(ns)
want.SetUID(tguid)
want.SetResourceReference(&corev1.ObjectReference{
Name: mgname,
})
want.SetWriteConnectionSecretToReference(&v1alpha1.LocalSecretReference{Name: tgcsname})
want.SetConditions(v1alpha1.SecretPropagationError(errBoom))
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Target{}, &fake.Managed{}),
},
of: TargetKind(fake.GVK(&fake.Target{})),
with: ManagedKind(fake.GVK(&fake.Managed{})),
},
want: want{
result: reconcile.Result{RequeueAfter: aShortWait},
},
},
"ErrorManagedNotBound": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
switch o := o.(type) {
case *fake.Target:
tg := &fake.Target{ObjectMeta: metav1.ObjectMeta{
UID: tguid,
Name: tgname,
Namespace: ns,
}}
tg.SetResourceReference(&corev1.ObjectReference{
Name: mgname,
})
tg.SetWriteConnectionSecretToReference(&v1alpha1.LocalSecretReference{
Name: tgcsname,
})
*o = *tg
return nil
case *fake.Managed:
mg := &fake.Managed{ObjectMeta: metav1.ObjectMeta{
UID: mguid,
Name: mgname,
}}
mg.SetWriteConnectionSecretToReference(&v1alpha1.SecretReference{
Name: mgcsname,
Namespace: mgcsnamespace,
})
mg.SetBindingPhase(v1alpha1.BindingPhaseUnbound)
*o = *mg
return nil
default:
return errUnexpected
}
},
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
want := &fake.Target{}
want.SetName(tgname)
want.SetNamespace(ns)
want.SetUID(tguid)
want.SetResourceReference(&corev1.ObjectReference{
Name: mgname,
})
want.SetWriteConnectionSecretToReference(&v1alpha1.LocalSecretReference{Name: tgcsname})
want.SetConditions(v1alpha1.SecretPropagationError(errors.New(errManagedResourceIsNotBound)))
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Target{}, &fake.Managed{}),
},
of: TargetKind(fake.GVK(&fake.Target{})),
with: ManagedKind(fake.GVK(&fake.Managed{})),
},
want: want{
result: reconcile.Result{RequeueAfter: aShortWait},
},
},
"ErrorSecretPropagationFailed": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
switch o := o.(type) {
case *fake.Target:
tg := &fake.Target{ObjectMeta: metav1.ObjectMeta{
UID: tguid,
Name: tgname,
Namespace: ns,
}}
tg.SetResourceReference(&corev1.ObjectReference{
Name: mgname,
})
tg.SetWriteConnectionSecretToReference(&v1alpha1.LocalSecretReference{
Name: tgcsname,
})
*o = *tg
return nil
case *fake.Managed:
mg := &fake.Managed{ObjectMeta: metav1.ObjectMeta{
UID: mguid,
Name: mgname,
}}
mg.SetWriteConnectionSecretToReference(&v1alpha1.SecretReference{
Name: mgcsname,
Namespace: mgcsnamespace,
})
mg.SetBindingPhase(v1alpha1.BindingPhaseBound)
*o = *mg
return nil
case *corev1.Secret:
switch n.Name {
case tgcsname:
sc := &corev1.Secret{}
sc.SetName(tgcsname)
sc.SetNamespace(ns)
sc.SetUID(tgcsuid)
sc.SetOwnerReferences([]metav1.OwnerReference{{
UID: tguid,
Controller: &controller,
}})
*o = *sc
return nil
case mgcsname:
sc := &corev1.Secret{}
sc.SetName(mgcsname)
sc.SetNamespace(mgcsnamespace)
sc.SetUID(mgcsuid)
sc.Data = mgcsdata
*o = *sc
return nil
default:
return errUnexpectedSecret
}
default:
return errUnexpected
}
},
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
want := &fake.Target{}
want.SetName(tgname)
want.SetNamespace(ns)
want.SetUID(tguid)
want.SetResourceReference(&corev1.ObjectReference{
Name: mgname,
})
want.SetWriteConnectionSecretToReference(&v1alpha1.LocalSecretReference{Name: tgcsname})
want.SetConditions(v1alpha1.SecretPropagationError(errors.New(errSecretConflict)))
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Target{}, &fake.Managed{}),
},
of: TargetKind(fake.GVK(&fake.Target{})),
with: ManagedKind(fake.GVK(&fake.Managed{})),
},
want: want{
result: reconcile.Result{RequeueAfter: aShortWait},
},
},
"Successful": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
switch o := o.(type) {
case *fake.Target:
tg := &fake.Target{ObjectMeta: metav1.ObjectMeta{
UID: tguid,
Name: tgname,
Namespace: ns,
}}
tg.SetResourceReference(&corev1.ObjectReference{
Name: mgname,
})
tg.SetWriteConnectionSecretToReference(&v1alpha1.LocalSecretReference{
Name: tgcsname,
})
*o = *tg
return nil
case *fake.Managed:
mg := &fake.Managed{ObjectMeta: metav1.ObjectMeta{
UID: mguid,
Name: mgname,
}}
mg.SetWriteConnectionSecretToReference(&v1alpha1.SecretReference{
Name: mgcsname,
Namespace: mgcsnamespace,
})
mg.SetBindingPhase(v1alpha1.BindingPhaseBound)
*o = *mg
return nil
case *corev1.Secret:
switch n.Name {
case tgcsname:
sc := &corev1.Secret{}
sc.SetName(tgcsname)
sc.SetNamespace(ns)
sc.SetUID(tgcsuid)
sc.SetOwnerReferences([]metav1.OwnerReference{{
UID: tguid,
Controller: &controller,
}})
*o = *sc
return nil
case mgcsname:
sc := &corev1.Secret{}
sc.SetName(mgcsname)
sc.SetNamespace(mgcsnamespace)
sc.SetUID(mgcsuid)
sc.SetOwnerReferences([]metav1.OwnerReference{{
UID: mguid,
Controller: &controller,
}})
sc.Data = mgcsdata
*o = *sc
return nil
default:
return errUnexpectedSecret
}
default:
return errUnexpected
}
},
MockUpdate: test.NewMockUpdateFn(nil, func(got runtime.Object) error {
want := &corev1.Secret{}
want.Data = mgcsdata
switch got.(metav1.Object).GetName() {
case tgcsname:
want.SetName(tgcsname)
want.SetNamespace(ns)
want.SetUID(tgcsuid)
want.SetOwnerReferences([]metav1.OwnerReference{{UID: tguid, Controller: &controller}})
want.SetAnnotations(map[string]string{
AnnotationKeyPropagateFromNamespace: mgcsnamespace,
AnnotationKeyPropagateFromName: mgcsname,
AnnotationKeyPropagateFromUID: string(mgcsuid),
})
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
case mgcsname:
want.SetName(mgcsname)
want.SetNamespace(mgcsnamespace)
want.SetUID(mgcsuid)
want.SetOwnerReferences([]metav1.OwnerReference{{UID: mguid, Controller: &controller}})
want.SetAnnotations(map[string]string{
strings.Join([]string{AnnotationKeyPropagateToPrefix, string(tgcsuid)}, SlashDelimeter): strings.Join([]string{ns, tgcsname}, SlashDelimeter),
})
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
default:
return errUnexpectedSecret
}
return nil
}),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
want := &fake.Target{}
want.SetName(tgname)
want.SetNamespace(ns)
want.SetUID(tguid)
want.SetResourceReference(&corev1.ObjectReference{
Name: mgname,
})
want.SetWriteConnectionSecretToReference(&v1alpha1.LocalSecretReference{Name: tgcsname})
want.SetConditions(v1alpha1.SecretPropagationSuccess())
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Target{}, &fake.Managed{}),
},
of: TargetKind(fake.GVK(&fake.Target{})),
with: ManagedKind(fake.GVK(&fake.Managed{})),
},
want: want{
result: reconcile.Result{Requeue: false},
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
r := NewTargetReconciler(tc.args.m, tc.args.of, tc.args.with)
got, err := r.Reconcile(reconcile.Request{})
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)
}
})
}
}