From 5ece4af54b32b991b571b535670715f10d971b86 Mon Sep 17 00:00:00 2001 From: Daniel Mangum <31777345+hasheddan@users.noreply.github.com> Date: Tue, 14 Jan 2020 12:36:41 -0800 Subject: [PATCH] Implement Target interface and reconciler (#103) Signed-off-by: hasheddan --- apis/core/v1alpha1/condition.go | 39 +- apis/core/v1alpha1/resource.go | 23 + apis/core/v1alpha1/zz_generated.deepcopy.go | 41 ++ pkg/resource/api.go | 8 +- pkg/resource/claim_binding_reconciler.go | 6 +- pkg/resource/claim_binding_reconciler_test.go | 6 +- pkg/resource/fake/mocks.go | 30 +- pkg/resource/interfaces.go | 12 + pkg/resource/target_reconciler.go | 123 ++++ pkg/resource/target_reconciler_test.go | 540 ++++++++++++++++++ 10 files changed, 813 insertions(+), 15 deletions(-) create mode 100644 pkg/resource/target_reconciler.go create mode 100644 pkg/resource/target_reconciler_test.go diff --git a/apis/core/v1alpha1/condition.go b/apis/core/v1alpha1/condition.go index cb53ba5..5b4cfdb 100644 --- a/apis/core/v1alpha1/condition.go +++ b/apis/core/v1alpha1/condition.go @@ -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(), + } +} diff --git a/apis/core/v1alpha1/resource.go b/apis/core/v1alpha1/resource.go index ce5c7c6..07c7fb1 100644 --- a/apis/core/v1alpha1/resource.go +++ b/apis/core/v1alpha1/resource.go @@ -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"` +} diff --git a/apis/core/v1alpha1/zz_generated.deepcopy.go b/apis/core/v1alpha1/zz_generated.deepcopy.go index ff67b63..0e5620c 100644 --- a/apis/core/v1alpha1/zz_generated.deepcopy.go +++ b/apis/core/v1alpha1/zz_generated.deepcopy.go @@ -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 +} diff --git a/pkg/resource/api.go b/pkg/resource/api.go index 7827d0d..7555b05 100644 --- a/pkg/resource/api.go +++ b/pkg/resource/api.go @@ -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 diff --git a/pkg/resource/claim_binding_reconciler.go b/pkg/resource/claim_binding_reconciler.go index b02fd09..9f2a881 100644 --- a/pkg/resource/claim_binding_reconciler.go +++ b/pkg/resource/claim_binding_reconciler.go @@ -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) } diff --git a/pkg/resource/claim_binding_reconciler_test.go b/pkg/resource/claim_binding_reconciler_test.go index 7027f7d..f2d87bb 100644 --- a/pkg/resource/claim_binding_reconciler_test.go +++ b/pkg/resource/claim_binding_reconciler_test.go @@ -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 }}, diff --git a/pkg/resource/fake/mocks.go b/pkg/resource/fake/mocks.go index 91fb3f6..65b14be 100644 --- a/pkg/resource/fake/mocks.go +++ b/pkg/resource/fake/mocks.go @@ -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 diff --git a/pkg/resource/interfaces.go b/pkg/resource/interfaces.go index c3dde70..317fa28 100644 --- a/pkg/resource/interfaces.go +++ b/pkg/resource/interfaces.go @@ -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 +} diff --git a/pkg/resource/target_reconciler.go b/pkg/resource/target_reconciler.go new file mode 100644 index 0000000..8df46ac --- /dev/null +++ b/pkg/resource/target_reconciler.go @@ -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 +} diff --git a/pkg/resource/target_reconciler_test.go b/pkg/resource/target_reconciler_test.go new file mode 100644 index 0000000..5a10383 --- /dev/null +++ b/pkg/resource/target_reconciler_test.go @@ -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) + } + }) + } +}