Merge pull request #46 from soorena776/issue_887
Implementating ReferenceResolver in managed reconciler
This commit is contained in:
commit
a56c70ba62
|
@ -34,6 +34,9 @@ const (
|
||||||
// TypeSynced managed resources are believed to be in sync with the
|
// TypeSynced managed resources are believed to be in sync with the
|
||||||
// Kubernetes resources that manage their lifecycle.
|
// Kubernetes resources that manage their lifecycle.
|
||||||
TypeSynced ConditionType = "Synced"
|
TypeSynced ConditionType = "Synced"
|
||||||
|
|
||||||
|
// TypeReferencesResolved managed resources' references are resolved
|
||||||
|
TypeReferencesResolved = "ReferencesResolved"
|
||||||
)
|
)
|
||||||
|
|
||||||
// A ConditionReason represents the reason a resource is in a condition.
|
// A ConditionReason represents the reason a resource is in a condition.
|
||||||
|
@ -53,6 +56,12 @@ const (
|
||||||
ReasonReconcileError ConditionReason = "Encountered an error during managed resource reconciliation"
|
ReasonReconcileError ConditionReason = "Encountered an error during managed resource reconciliation"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Reason references for a resource are or are not resolved
|
||||||
|
const (
|
||||||
|
ReasonReferenceResolveSuccess ConditionReason = "Successfully resolved managed resource references to other resources"
|
||||||
|
ReasonResolveReferencesBlocked ConditionReason = "One or more of referenced resources do not exist, or are not yet Ready"
|
||||||
|
)
|
||||||
|
|
||||||
// A Condition that may apply to a managed resource.
|
// A Condition that may apply to a managed resource.
|
||||||
type Condition struct {
|
type Condition struct {
|
||||||
// Type of this condition. At most one of each condition type may apply to
|
// Type of this condition. At most one of each condition type may apply to
|
||||||
|
@ -113,6 +122,18 @@ func NewConditionedStatus(c ...Condition) *ConditionedStatus {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCondition returns the condition for the given ConditionType if exists,
|
||||||
|
// otherwise returns nil
|
||||||
|
func (s *ConditionedStatus) GetCondition(ct ConditionType) Condition {
|
||||||
|
for _, c := range s.Conditions {
|
||||||
|
if c.Type == ct {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Condition{Type: ct, Status: corev1.ConditionUnknown}
|
||||||
|
}
|
||||||
|
|
||||||
// SetConditions sets the supplied conditions, replacing any existing conditions
|
// SetConditions sets the supplied conditions, replacing any existing conditions
|
||||||
// of the same type. This is a no-op if all supplied conditions are identical,
|
// of the same type. This is a no-op if all supplied conditions are identical,
|
||||||
// ignoring the last transition time, to those already set.
|
// ignoring the last transition time, to those already set.
|
||||||
|
@ -239,3 +260,28 @@ func ReconcileError(err error) Condition {
|
||||||
Message: err.Error(),
|
Message: err.Error(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReferenceResolutionSuccess returns a condition indicating that Crossplane
|
||||||
|
// successfully resolved the references used in the managed resource
|
||||||
|
func ReferenceResolutionSuccess() Condition {
|
||||||
|
return Condition{
|
||||||
|
Type: TypeReferencesResolved,
|
||||||
|
Status: corev1.ConditionTrue,
|
||||||
|
LastTransitionTime: metav1.Now(),
|
||||||
|
Reason: ReasonReferenceResolveSuccess,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReferenceResolutionBlocked returns a condition indicating that Crossplane is
|
||||||
|
// unable to resolve the references used in the managed resource. This could
|
||||||
|
// mean that one or more of referred resources do not yet exist, or are not yet
|
||||||
|
// Ready
|
||||||
|
func ReferenceResolutionBlocked(err error) Condition {
|
||||||
|
return Condition{
|
||||||
|
Type: TypeReferencesResolved,
|
||||||
|
Status: corev1.ConditionFalse,
|
||||||
|
LastTransitionTime: metav1.Now(),
|
||||||
|
Reason: ReasonResolveReferencesBlocked,
|
||||||
|
Message: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -153,6 +153,37 @@ func TestSetConditions(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetCondition(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
cs *ConditionedStatus
|
||||||
|
t ConditionType
|
||||||
|
want Condition
|
||||||
|
}{
|
||||||
|
"ConditionExists": {
|
||||||
|
cs: NewConditionedStatus(Available()),
|
||||||
|
t: TypeReady,
|
||||||
|
want: Available(),
|
||||||
|
},
|
||||||
|
"ConditionDoesNotExist": {
|
||||||
|
cs: NewConditionedStatus(Available()),
|
||||||
|
t: TypeSynced,
|
||||||
|
want: Condition{
|
||||||
|
Type: TypeSynced,
|
||||||
|
Status: corev1.ConditionUnknown,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range cases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
got := tc.cs.GetCondition(tc.t)
|
||||||
|
if diff := cmp.Diff(tc.want, got); diff != "" {
|
||||||
|
t.Errorf("tc.cs.GetConditions(...): -want, +got:\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestConditionWithMessage(t *testing.T) {
|
func TestConditionWithMessage(t *testing.T) {
|
||||||
testMsg := "Something went wrong on cloud side"
|
testMsg := "Something went wrong on cloud side"
|
||||||
cases := map[string]struct {
|
cases := map[string]struct {
|
||||||
|
|
|
@ -31,11 +31,11 @@ type Bindable interface {
|
||||||
GetBindingPhase() v1alpha1.BindingPhase
|
GetBindingPhase() v1alpha1.BindingPhase
|
||||||
}
|
}
|
||||||
|
|
||||||
// A ConditionSetter may have conditions set. Conditions are informational, and
|
// A Conditioned may have conditions set or retrieved. Conditions are typically
|
||||||
// typically indicate the status of both a resource and its reconciliation
|
// indicate the status of both a resource and its reconciliation process.
|
||||||
// process.
|
type Conditioned interface {
|
||||||
type ConditionSetter interface {
|
|
||||||
SetConditions(c ...v1alpha1.Condition)
|
SetConditions(c ...v1alpha1.Condition)
|
||||||
|
GetCondition(v1alpha1.ConditionType) v1alpha1.Condition
|
||||||
}
|
}
|
||||||
|
|
||||||
// A ClaimReferencer may reference a resource claim.
|
// A ClaimReferencer may reference a resource claim.
|
||||||
|
@ -91,7 +91,7 @@ type Claim interface {
|
||||||
ManagedResourceReferencer
|
ManagedResourceReferencer
|
||||||
ConnectionSecretWriterTo
|
ConnectionSecretWriterTo
|
||||||
|
|
||||||
ConditionSetter
|
Conditioned
|
||||||
Bindable
|
Bindable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ type Managed interface {
|
||||||
ConnectionSecretWriterTo
|
ConnectionSecretWriterTo
|
||||||
Reclaimer
|
Reclaimer
|
||||||
|
|
||||||
ConditionSetter
|
Conditioned
|
||||||
Bindable
|
Bindable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,9 +29,12 @@ type MockBindable struct{ Phase v1alpha1.BindingPhase }
|
||||||
func (m *MockBindable) SetBindingPhase(p v1alpha1.BindingPhase) { m.Phase = p }
|
func (m *MockBindable) SetBindingPhase(p v1alpha1.BindingPhase) { m.Phase = p }
|
||||||
func (m *MockBindable) GetBindingPhase() v1alpha1.BindingPhase { return m.Phase }
|
func (m *MockBindable) GetBindingPhase() v1alpha1.BindingPhase { return m.Phase }
|
||||||
|
|
||||||
type MockConditionSetter struct{ Conditions []v1alpha1.Condition }
|
type MockConditioned struct{ Conditions []v1alpha1.Condition }
|
||||||
|
|
||||||
func (m *MockConditionSetter) SetConditions(c ...v1alpha1.Condition) { m.Conditions = c }
|
func (m *MockConditioned) SetConditions(c ...v1alpha1.Condition) { m.Conditions = c }
|
||||||
|
func (m *MockConditioned) GetCondition(ct v1alpha1.ConditionType) v1alpha1.Condition {
|
||||||
|
return v1alpha1.Condition{Type: ct, Status: corev1.ConditionUnknown}
|
||||||
|
}
|
||||||
|
|
||||||
type MockClaimReferencer struct{ Ref *corev1.ObjectReference }
|
type MockClaimReferencer struct{ Ref *corev1.ObjectReference }
|
||||||
|
|
||||||
|
@ -89,7 +92,7 @@ type MockClaim struct {
|
||||||
MockPortableClassReferencer
|
MockPortableClassReferencer
|
||||||
MockManagedResourceReferencer
|
MockManagedResourceReferencer
|
||||||
MockConnectionSecretWriterTo
|
MockConnectionSecretWriterTo
|
||||||
MockConditionSetter
|
MockConditioned
|
||||||
MockBindable
|
MockBindable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,7 +115,7 @@ type MockManaged struct {
|
||||||
MockClaimReferencer
|
MockClaimReferencer
|
||||||
MockConnectionSecretWriterTo
|
MockConnectionSecretWriterTo
|
||||||
MockReclaimer
|
MockReclaimer
|
||||||
MockConditionSetter
|
MockConditioned
|
||||||
MockBindable
|
MockBindable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,8 +32,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
managedControllerName = "managedresource.crossplane.io"
|
managedControllerName = "managedresource.crossplane.io"
|
||||||
managedFinalizerName = "finalizer." + managedControllerName
|
managedFinalizerName = "finalizer." + managedControllerName
|
||||||
|
managedResourceStructTagPackageName = "resource"
|
||||||
|
|
||||||
managedReconcileTimeout = 1 * time.Minute
|
managedReconcileTimeout = 1 * time.Minute
|
||||||
|
|
||||||
defaultManagedShortWait = 30 * time.Second
|
defaultManagedShortWait = 30 * time.Second
|
||||||
|
@ -87,6 +89,14 @@ func (m ManagedInitializerFn) Initialize(ctx context.Context, mg Managed) error
|
||||||
return m(ctx, mg)
|
return m(ctx, mg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ManagedReferenceResolverFn is the pluggable struct to produce objects with ManagedReferenceResolver interface.
|
||||||
|
type ManagedReferenceResolverFn func(context.Context, CanReference) error
|
||||||
|
|
||||||
|
// ResolveReferences calls ManagedReferenceResolverFn function
|
||||||
|
func (m ManagedReferenceResolverFn) ResolveReferences(ctx context.Context, res CanReference) error {
|
||||||
|
return m(ctx, res)
|
||||||
|
}
|
||||||
|
|
||||||
// An ExternalConnecter produces a new ExternalClient given the supplied
|
// An ExternalConnecter produces a new ExternalClient given the supplied
|
||||||
// Managed resource.
|
// Managed resource.
|
||||||
type ExternalConnecter interface {
|
type ExternalConnecter interface {
|
||||||
|
@ -224,6 +234,7 @@ type mrManaged struct {
|
||||||
ManagedConnectionPublisher
|
ManagedConnectionPublisher
|
||||||
ManagedFinalizer
|
ManagedFinalizer
|
||||||
ManagedInitializer
|
ManagedInitializer
|
||||||
|
ManagedReferenceResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultMRManaged(m manager.Manager) mrManaged {
|
func defaultMRManaged(m manager.Manager) mrManaged {
|
||||||
|
@ -234,6 +245,7 @@ func defaultMRManaged(m manager.Manager) mrManaged {
|
||||||
NewManagedNameAsExternalName(m.GetClient()),
|
NewManagedNameAsExternalName(m.GetClient()),
|
||||||
NewAPIManagedFinalizerAdder(m.GetClient()),
|
NewAPIManagedFinalizerAdder(m.GetClient()),
|
||||||
},
|
},
|
||||||
|
ManagedReferenceResolver: NewAPIManagedReferenceResolver(m.GetClient()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -360,6 +372,21 @@ func (r *ManagedReconciler) Reconcile(req reconcile.Request) (reconcile.Result,
|
||||||
return reconcile.Result{RequeueAfter: r.shortWait}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
return reconcile.Result{RequeueAfter: r.shortWait}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !IsConditionTrue(managed.GetCondition(v1alpha1.TypeReferencesResolved)) {
|
||||||
|
if err := r.managed.ResolveReferences(ctx, managed); err != nil {
|
||||||
|
condition := v1alpha1.ReconcileError(err)
|
||||||
|
if IsReferencesAccessError(err) {
|
||||||
|
condition = v1alpha1.ReferenceResolutionBlocked(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
managed.SetConditions(condition)
|
||||||
|
return reconcile.Result{RequeueAfter: r.longWait}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add ReferenceResolutionSuccess to the conditions
|
||||||
|
managed.SetConditions(v1alpha1.ReferenceResolutionSuccess())
|
||||||
|
}
|
||||||
|
|
||||||
observation, err := external.Observe(ctx, managed)
|
observation, err := external.Observe(ctx, managed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// We'll usually hit this case if our Provider credentials are invalid
|
// We'll usually hit this case if our Provider credentials are invalid
|
||||||
|
|
|
@ -51,6 +51,7 @@ func TestManagedReconciler(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
errBoom := errors.New("boom")
|
errBoom := errors.New("boom")
|
||||||
|
errNotReady := &referencesAccessErr{[]ReferenceStatus{{Name: "cool-res", Status: ReferenceNotReady}}}
|
||||||
now := metav1.Now()
|
now := metav1.Now()
|
||||||
testFinalizers := []string{"finalizer.crossplane.io"}
|
testFinalizers := []string{"finalizer.crossplane.io"}
|
||||||
testConnectionDetails := ConnectionDetails{
|
testConnectionDetails := ConnectionDetails{
|
||||||
|
@ -517,6 +518,82 @@ func TestManagedReconciler(t *testing.T) {
|
||||||
},
|
},
|
||||||
want: want{result: reconcile.Result{RequeueAfter: defaultManagedShortWait}},
|
want: want{result: reconcile.Result{RequeueAfter: defaultManagedShortWait}},
|
||||||
},
|
},
|
||||||
|
"ResolveReferences_NotReadyErr_ReturnsExpected": {
|
||||||
|
args: args{
|
||||||
|
m: &MockManager{
|
||||||
|
c: &test.MockClient{
|
||||||
|
MockGet: test.NewMockGetFn(nil),
|
||||||
|
MockStatusUpdate: test.MockStatusUpdateFn(func(_ context.Context, obj runtime.Object, _ ...client.UpdateOption) error {
|
||||||
|
want := &MockManaged{}
|
||||||
|
want.SetConditions(v1alpha1.ReferenceResolutionBlocked(errNotReady))
|
||||||
|
want.SetFinalizers(testFinalizers)
|
||||||
|
if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" {
|
||||||
|
t.Errorf("-want, +got:\n%s", diff)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
s: MockSchemeWith(&MockManaged{}),
|
||||||
|
},
|
||||||
|
mg: ManagedKind(MockGVK(&MockManaged{})),
|
||||||
|
e: &ExternalClientFns{
|
||||||
|
ObserveFn: func(_ context.Context, _ Managed) (ExternalObservation, error) {
|
||||||
|
return ExternalObservation{
|
||||||
|
ResourceExists: false,
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
o: []ManagedReconcilerOption{
|
||||||
|
func(r *ManagedReconciler) {
|
||||||
|
r.managed.ManagedInitializer = ManagedInitializerFn(func(_ context.Context, mg Managed) error {
|
||||||
|
mg.SetFinalizers(testFinalizers)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
},
|
||||||
|
func(r *ManagedReconciler) {
|
||||||
|
r.managed.ManagedReferenceResolver = ManagedReferenceResolverFn(func(_ context.Context, res CanReference) error {
|
||||||
|
return errNotReady
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: want{result: reconcile.Result{RequeueAfter: defaultManagedLongWait}},
|
||||||
|
},
|
||||||
|
"ResolveReferences_OtherError_ReturnsExpected": {
|
||||||
|
args: args{
|
||||||
|
m: &MockManager{
|
||||||
|
c: &test.MockClient{
|
||||||
|
MockGet: test.NewMockGetFn(nil),
|
||||||
|
MockStatusUpdate: test.MockStatusUpdateFn(func(_ context.Context, obj runtime.Object, _ ...client.UpdateOption) error {
|
||||||
|
want := &MockManaged{}
|
||||||
|
want.SetConditions(v1alpha1.ReconcileError(errBoom))
|
||||||
|
want.SetFinalizers(testFinalizers)
|
||||||
|
if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" {
|
||||||
|
t.Errorf("-want, +got:\n%s", diff)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
s: MockSchemeWith(&MockManaged{}),
|
||||||
|
},
|
||||||
|
mg: ManagedKind(MockGVK(&MockManaged{})),
|
||||||
|
e: &ExternalClientFns{},
|
||||||
|
o: []ManagedReconcilerOption{
|
||||||
|
func(r *ManagedReconciler) {
|
||||||
|
r.managed.ManagedInitializer = ManagedInitializerFn(func(_ context.Context, mg Managed) error {
|
||||||
|
mg.SetFinalizers(testFinalizers)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
},
|
||||||
|
func(r *ManagedReconciler) {
|
||||||
|
r.managed.ManagedReferenceResolver = ManagedReferenceResolverFn(func(_ context.Context, res CanReference) error {
|
||||||
|
return errBoom
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: want{result: reconcile.Result{RequeueAfter: defaultManagedLongWait}},
|
||||||
|
},
|
||||||
"EstablishResourceDoesNotExistError": {
|
"EstablishResourceDoesNotExistError": {
|
||||||
args: args{
|
args: args{
|
||||||
m: &MockManager{
|
m: &MockManager{
|
||||||
|
@ -534,13 +611,7 @@ func TestManagedReconciler(t *testing.T) {
|
||||||
s: MockSchemeWith(&MockManaged{}),
|
s: MockSchemeWith(&MockManaged{}),
|
||||||
},
|
},
|
||||||
mg: ManagedKind(MockGVK(&MockManaged{})),
|
mg: ManagedKind(MockGVK(&MockManaged{})),
|
||||||
e: &ExternalClientFns{
|
e: &ExternalClientFns{},
|
||||||
ObserveFn: func(_ context.Context, _ Managed) (ExternalObservation, error) {
|
|
||||||
return ExternalObservation{
|
|
||||||
ResourceExists: false,
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
o: []ManagedReconcilerOption{
|
o: []ManagedReconcilerOption{
|
||||||
func(r *ManagedReconciler) {
|
func(r *ManagedReconciler) {
|
||||||
r.managed.ManagedConnectionPublisher = ManagedConnectionPublisherFns{
|
r.managed.ManagedConnectionPublisher = ManagedConnectionPublisherFns{
|
||||||
|
|
|
@ -0,0 +1,275 @@
|
||||||
|
/*
|
||||||
|
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"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
attributeReferencerTagName = "attributereferencer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error strings
|
||||||
|
const (
|
||||||
|
errTaggedFieldlNotImplemented = "ManagedReferenceResolver: the field has the %v tag, but has not implemented AttributeReferencer interface"
|
||||||
|
errBuildAttribute = "ManagedReferenceResolver: could not build the attribute"
|
||||||
|
errAssignAttribute = "ManagedReferenceResolver: could not assign the attribute"
|
||||||
|
errUpdateResourceAfterAssignment = "ManagedReferenceResolver: could not update the resource after resolving references"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fieldHasTagPair is used in findAttributeReferencerFields
|
||||||
|
type fieldHasTagPair struct {
|
||||||
|
fieldValue reflect.Value
|
||||||
|
hasTheTag bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReferenceStatusType is an enum type for the possible values for a Reference Status
|
||||||
|
type ReferenceStatusType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ReferenceStatusUnknown is the default value
|
||||||
|
ReferenceStatusUnknown ReferenceStatusType = iota
|
||||||
|
// ReferenceNotFound shows that the reference is not found
|
||||||
|
ReferenceNotFound
|
||||||
|
// ReferenceNotReady shows that the reference is not ready
|
||||||
|
ReferenceNotReady
|
||||||
|
// ReferenceReady shows that the reference is ready
|
||||||
|
ReferenceReady
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t ReferenceStatusType) String() string {
|
||||||
|
return []string{"Unknown", "NotFound", "NotReady", "Ready"}[t]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReferenceStatus has the name and status of a reference
|
||||||
|
type ReferenceStatus struct {
|
||||||
|
Name string
|
||||||
|
Status ReferenceStatusType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReferenceStatus) String() string {
|
||||||
|
return fmt.Sprintf("{reference:%s status:%s}", r.Name, r.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// referencesAccessErr is used to indicate that one or more references can not
|
||||||
|
// be accessed
|
||||||
|
type referencesAccessErr struct {
|
||||||
|
statuses []ReferenceStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
// newReferenceAccessErr returns a referencesAccessErr if any of the given
|
||||||
|
// references are not ready
|
||||||
|
func newReferenceAccessErr(statuses []ReferenceStatus) error {
|
||||||
|
for _, st := range statuses {
|
||||||
|
if st.Status != ReferenceReady {
|
||||||
|
return &referencesAccessErr{statuses}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *referencesAccessErr) Error() string {
|
||||||
|
return fmt.Sprintf("%s", r.statuses)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsReferencesAccessError returns true if the given error indicates that some
|
||||||
|
// of the `AttributeReferencer` fields are referring to objects that are not
|
||||||
|
// accessible, either they are not ready or they do not yet exist
|
||||||
|
func IsReferencesAccessError(err error) bool {
|
||||||
|
_, result := err.(*referencesAccessErr)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanReference is a type that is used as ReferenceResolver input
|
||||||
|
type CanReference interface {
|
||||||
|
runtime.Object
|
||||||
|
metav1.Object
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttributeReferencer is an interface for referencing and resolving
|
||||||
|
// cross-resource attribute references. See
|
||||||
|
// https://github.com/crossplaneio/crossplane/blob/master/design/one-pager-cross-resource-referencing.md
|
||||||
|
// for more information
|
||||||
|
type AttributeReferencer interface {
|
||||||
|
|
||||||
|
// GetStatus looks up the referenced objects in K8S api and returns a list
|
||||||
|
// of ReferenceStatus
|
||||||
|
GetStatus(context.Context, CanReference, client.Reader) ([]ReferenceStatus, error)
|
||||||
|
|
||||||
|
// Build retrieves referenced resource, as well as other non-managed
|
||||||
|
// resources (like a `Provider`), and builds the referenced attribute
|
||||||
|
Build(context.Context, CanReference, client.Reader) (string, error)
|
||||||
|
|
||||||
|
// Assign accepts a managed resource object, and assigns the given value to the
|
||||||
|
// corresponding property
|
||||||
|
Assign(CanReference, string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// A ManagedReferenceResolver resolves the references to other managed
|
||||||
|
// resources, by looking them up in the Kubernetes API server. The references
|
||||||
|
// are the fields in the managed resource that implement AttributeReferencer
|
||||||
|
// interface and have
|
||||||
|
// `attributeReferencerTagName:"managedResourceStructTagPackageName"` tag
|
||||||
|
type ManagedReferenceResolver interface {
|
||||||
|
ResolveReferences(context.Context, CanReference) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIManagedReferenceResolver resolves implements ManagedReferenceResolver interface
|
||||||
|
type APIManagedReferenceResolver struct {
|
||||||
|
client client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAPIManagedReferenceResolver returns a new APIManagedReferenceResolver
|
||||||
|
func NewAPIManagedReferenceResolver(c client.Client) *APIManagedReferenceResolver {
|
||||||
|
return &APIManagedReferenceResolver{c}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveReferences resolves references made to other managed resources
|
||||||
|
func (r *APIManagedReferenceResolver) ResolveReferences(ctx context.Context, res CanReference) (err error) {
|
||||||
|
// retrieve all the referencer fields from the managed resource
|
||||||
|
referencers, err := findAttributeReferencerFields(res, false)
|
||||||
|
if err != nil {
|
||||||
|
// if there is an error it should immediately panic, since this means an
|
||||||
|
// attribute is tagged but doesn't implement AttributeReferencer
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there are no referencers exit early
|
||||||
|
if len(referencers) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure that all the references are ready
|
||||||
|
allStatuses := []ReferenceStatus{}
|
||||||
|
for _, referencer := range referencers {
|
||||||
|
statuses, err := referencer.GetStatus(ctx, res, r.client)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
allStatuses = append(allStatuses, statuses...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := newReferenceAccessErr(allStatuses); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// build and assign the attributes
|
||||||
|
for _, referencer := range referencers {
|
||||||
|
val, err := referencer.Build(ctx, res, r.client)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, errBuildAttribute)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := referencer.Assign(res, val); err != nil {
|
||||||
|
return errors.Wrap(err, errAssignAttribute)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// persist the updated managed resource
|
||||||
|
return errors.Wrap(r.client.Update(ctx, res), errUpdateResourceAfterAssignment)
|
||||||
|
}
|
||||||
|
|
||||||
|
// findAttributeReferencerFields recursively finds all non-nil fields in a struct and its sub types
|
||||||
|
// that implement AttributeReferencer and have `attributeReferencerTagName:"managedResourceStructTagPackageName"` tag
|
||||||
|
func findAttributeReferencerFields(obj interface{}, objHasTheRightTag bool) ([]AttributeReferencer, error) {
|
||||||
|
pairs := []fieldHasTagPair{}
|
||||||
|
v := reflect.ValueOf(obj)
|
||||||
|
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.Ptr:
|
||||||
|
if v.IsNil() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pairs = append(pairs, fieldHasTagPair{v.Elem(), objHasTheRightTag})
|
||||||
|
|
||||||
|
case reflect.Struct:
|
||||||
|
for i := 0; i < v.NumField(); i++ {
|
||||||
|
pairs = append(pairs, fieldHasTagPair{v.Field(i), hasAttrRefTag(reflect.TypeOf(obj).Field(i))})
|
||||||
|
}
|
||||||
|
|
||||||
|
case reflect.Slice:
|
||||||
|
for i := 0; i < v.Len(); i++ {
|
||||||
|
pairs = append(pairs, fieldHasTagPair{v.Index(i), objHasTheRightTag})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inspectFields(pairs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// inspectFields along with findAttributeReferencerFields it recursively
|
||||||
|
// inspects the extracted struct fields, and returns the ones that are of type
|
||||||
|
// `AttributeReferencer`.
|
||||||
|
func inspectFields(pairs []fieldHasTagPair) ([]AttributeReferencer, error) {
|
||||||
|
result := []AttributeReferencer{}
|
||||||
|
for _, pair := range pairs {
|
||||||
|
if !pair.fieldValue.CanInterface() {
|
||||||
|
if pair.hasTheTag {
|
||||||
|
// if the field has the tag but its value cannot be converted to
|
||||||
|
// an `Interface{}` (like a struct with private fields) it can't
|
||||||
|
// possibly implement the method sets. returning error here
|
||||||
|
// since, there won't be a recursive call for this value
|
||||||
|
return nil, errors.Errorf(errTaggedFieldlNotImplemented, attributeReferencerTagName)
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if pair.hasTheTag {
|
||||||
|
if ar, implements := pair.fieldValue.Interface().(AttributeReferencer); implements {
|
||||||
|
if !pair.fieldValue.IsNil() {
|
||||||
|
result = append(result, ar)
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is for the case where a tag is assigned to a struct, but it
|
||||||
|
// doesn't implement the interface
|
||||||
|
if pair.fieldValue.Kind() == reflect.Struct {
|
||||||
|
return nil, errors.Errorf(errTaggedFieldlNotImplemented, attributeReferencerTagName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvers, err := findAttributeReferencerFields(pair.fieldValue.Interface(), pair.hasTheTag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, resolvers...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasAttrRefTag returns true if the given struct field has the
|
||||||
|
// AttributeReference tag
|
||||||
|
func hasAttrRefTag(field reflect.StructField) bool {
|
||||||
|
val, ok := field.Tag.Lookup(managedResourceStructTagPackageName)
|
||||||
|
return ok && (val == attributeReferencerTagName)
|
||||||
|
}
|
|
@ -0,0 +1,503 @@
|
||||||
|
/*
|
||||||
|
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"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
|
"github.com/crossplaneio/crossplane-runtime/pkg/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ItemAReferencer struct {
|
||||||
|
*corev1.LocalObjectReference
|
||||||
|
|
||||||
|
getStatusFn func(context.Context, CanReference, client.Reader) ([]ReferenceStatus, error)
|
||||||
|
getStatusCalled bool
|
||||||
|
buildFn func(context.Context, CanReference, client.Reader) (string, error)
|
||||||
|
buildCalled bool
|
||||||
|
assignFn func(CanReference, string) error
|
||||||
|
assignCalled bool
|
||||||
|
assignParam string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ItemAReferencer) GetStatus(ctx context.Context, res CanReference, reader client.Reader) ([]ReferenceStatus, error) {
|
||||||
|
r.getStatusCalled = true
|
||||||
|
return r.getStatusFn(ctx, res, reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ItemAReferencer) Build(ctx context.Context, res CanReference, reader client.Reader) (string, error) {
|
||||||
|
r.buildCalled = true
|
||||||
|
return r.buildFn(ctx, res, reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ItemAReferencer) Assign(res CanReference, val string) error {
|
||||||
|
r.assignCalled = true
|
||||||
|
r.assignParam = val
|
||||||
|
return r.assignFn(res, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ItemBReferencer struct {
|
||||||
|
*corev1.LocalObjectReference
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ItemBReferencer) GetStatus(context.Context, CanReference, client.Reader) ([]ReferenceStatus, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ItemBReferencer) Build(context.Context, CanReference, client.Reader) (string, error) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ItemBReferencer) Assign(CanReference, string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_findAttributeReferencerFields(t *testing.T) {
|
||||||
|
// some structs that are used in this test
|
||||||
|
type mockStruct struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockInnerStruct struct {
|
||||||
|
ItemARef *ItemAReferencer `resource:"attributereferencer"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
o interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type want struct {
|
||||||
|
arrLen int
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// test cases
|
||||||
|
cases := map[string]struct {
|
||||||
|
args args
|
||||||
|
want want
|
||||||
|
}{
|
||||||
|
"ValidResourceWithSingleReferencer_AsObject_ShouldReturnExpected": {
|
||||||
|
args: args{
|
||||||
|
o: struct {
|
||||||
|
NonReferencerField mockStruct
|
||||||
|
ItemARef *ItemAReferencer `resource:"attributereferencer"`
|
||||||
|
}{
|
||||||
|
NonReferencerField: mockStruct{},
|
||||||
|
ItemARef: &ItemAReferencer{LocalObjectReference: &corev1.LocalObjectReference{"item-name"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: want{
|
||||||
|
arrLen: 1,
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ValidResourceWithSingleReferencer_AsPointer_ShouldReturnExpected": {
|
||||||
|
args: args{
|
||||||
|
o: &struct {
|
||||||
|
NonReferencerField mockStruct
|
||||||
|
ItemARef *ItemAReferencer `resource:"attributereferencer"`
|
||||||
|
}{
|
||||||
|
NonReferencerField: mockStruct{},
|
||||||
|
ItemARef: &ItemAReferencer{LocalObjectReference: &corev1.LocalObjectReference{"item-name"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: want{
|
||||||
|
arrLen: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ValidResourceWithSingleReferencer_NilReferencer_ShouldReturnEmpty": {
|
||||||
|
args: args{
|
||||||
|
o: &struct {
|
||||||
|
NonReferencerField mockStruct
|
||||||
|
ItemARef *ItemAReferencer `resource:"attributereferencer"`
|
||||||
|
}{
|
||||||
|
NonReferencerField: mockStruct{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: want{},
|
||||||
|
},
|
||||||
|
"NilResource_ShouldReturnEmpty": {
|
||||||
|
args: args{
|
||||||
|
o: nil,
|
||||||
|
},
|
||||||
|
want: want{},
|
||||||
|
},
|
||||||
|
"ValidResourceWithMultipleReferencers_AllReferencersArePopulated_ShouldReturnExpected": {
|
||||||
|
args: args{
|
||||||
|
o: &struct {
|
||||||
|
ItemARef *ItemAReferencer `resource:"attributereferencer"`
|
||||||
|
ItemBRef *ItemBReferencer `resource:"attributereferencer"`
|
||||||
|
AStruct *MockInnerStruct
|
||||||
|
}{
|
||||||
|
ItemARef: &ItemAReferencer{LocalObjectReference: &corev1.LocalObjectReference{"itemA-name"}},
|
||||||
|
ItemBRef: &ItemBReferencer{LocalObjectReference: &corev1.LocalObjectReference{"itemB-name"}},
|
||||||
|
AStruct: &MockInnerStruct{
|
||||||
|
&ItemAReferencer{LocalObjectReference: &corev1.LocalObjectReference{"itemA-name"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: want{
|
||||||
|
arrLen: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ValidResourceWithMultipleReferencers_ReferencersArePartiallyPopulated_ShouldReturnExpected": {
|
||||||
|
args: args{
|
||||||
|
o: &struct {
|
||||||
|
ItemARef *ItemAReferencer `resource:"attributereferencer"`
|
||||||
|
ItemBRef *ItemBReferencer `resource:"attributereferencer"`
|
||||||
|
AStruct *MockInnerStruct
|
||||||
|
}{
|
||||||
|
ItemBRef: &ItemBReferencer{&corev1.LocalObjectReference{"itemB-name"}},
|
||||||
|
AStruct: &MockInnerStruct{
|
||||||
|
&ItemAReferencer{LocalObjectReference: &corev1.LocalObjectReference{"itemA-name"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: want{
|
||||||
|
arrLen: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ValidResourceWithListOfReferencers_ListIsPopulated_ShouldReturnExpected": {
|
||||||
|
args: args{
|
||||||
|
o: &struct {
|
||||||
|
ItemsRef []*ItemAReferencer `resource:"attributereferencer"`
|
||||||
|
}{
|
||||||
|
ItemsRef: []*ItemAReferencer{
|
||||||
|
{LocalObjectReference: &corev1.LocalObjectReference{"itemA1-name"}},
|
||||||
|
{LocalObjectReference: &corev1.LocalObjectReference{"itemA2-name"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: want{
|
||||||
|
arrLen: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ValidResourceWithListOfReferencers_ListIsEmpty_ShouldReturnEmpty": {
|
||||||
|
args: args{
|
||||||
|
o: &struct {
|
||||||
|
ItemsRef []*ItemAReferencer `resource:"attributereferencer"`
|
||||||
|
}{
|
||||||
|
ItemsRef: []*ItemAReferencer{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: want{},
|
||||||
|
},
|
||||||
|
"ResourceWithNotImplementingTaggedReferencers_ShouldReturnError": {
|
||||||
|
args: args{
|
||||||
|
o: struct {
|
||||||
|
// InvalidRef is tagged, but mockStruct doesn't implement
|
||||||
|
// the required interface
|
||||||
|
InvalidRef *mockStruct `resource:"attributereferencer"`
|
||||||
|
}{
|
||||||
|
InvalidRef: &mockStruct{"something"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: want{
|
||||||
|
err: errors.Errorf(errTaggedFieldlNotImplemented, attributeReferencerTagName),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ResourceWithNotInterfaceableTaggedReferencers_ShouldReturnError": {
|
||||||
|
args: args{
|
||||||
|
o: struct {
|
||||||
|
// since nonReferencerField is not exported, its value is
|
||||||
|
// not interfaceable
|
||||||
|
nonReferencerField mockStruct `resource:"attributereferencer"`
|
||||||
|
}{
|
||||||
|
nonReferencerField: mockStruct{"something else"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: want{
|
||||||
|
err: errors.Errorf(errTaggedFieldlNotImplemented, attributeReferencerTagName),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ResourceWithUntaggedReferencers_ShouldReturnEmpty": {
|
||||||
|
args: args{
|
||||||
|
o: struct {
|
||||||
|
// even though UntaggedRef has implemented the interface,
|
||||||
|
// but its not tagged
|
||||||
|
UntaggedRef *ItemAReferencer
|
||||||
|
}{
|
||||||
|
UntaggedRef: &ItemAReferencer{LocalObjectReference: &corev1.LocalObjectReference{"itemA-name"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: want{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range cases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
got, err := findAttributeReferencerFields(tc.args.o, false)
|
||||||
|
|
||||||
|
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
|
||||||
|
t.Errorf("findAttributeReferencerFields(...): -want error, +got error:\n%s", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(tc.want.arrLen, len(got)); diff != "" {
|
||||||
|
t.Errorf("findAttributeReferencerFields(...): -want len, +got len:\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_ResolveReferences(t *testing.T) {
|
||||||
|
|
||||||
|
validGetStatusFn := func(context.Context, CanReference, client.Reader) ([]ReferenceStatus, error) { return nil, nil }
|
||||||
|
validBuildFn := func(context.Context, CanReference, client.Reader) (string, error) { return "fakeValue", nil }
|
||||||
|
validAssignFn := func(CanReference, string) error { return nil }
|
||||||
|
|
||||||
|
errBoom := errors.New("boom")
|
||||||
|
|
||||||
|
type managed struct {
|
||||||
|
MockManaged
|
||||||
|
ItemARef *ItemAReferencer `resource:"attributereferencer"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
field *ItemAReferencer
|
||||||
|
clientUpdaterErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
type want struct {
|
||||||
|
getStatusCalled bool
|
||||||
|
buildCalled bool
|
||||||
|
assignCalled bool
|
||||||
|
assignParam string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range map[string]struct {
|
||||||
|
args args
|
||||||
|
want want
|
||||||
|
}{
|
||||||
|
"ValidAttribute_ReturnsNil": {
|
||||||
|
args: args{
|
||||||
|
field: &ItemAReferencer{
|
||||||
|
getStatusFn: validGetStatusFn,
|
||||||
|
buildFn: validBuildFn,
|
||||||
|
assignFn: validAssignFn,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: want{
|
||||||
|
getStatusCalled: true,
|
||||||
|
buildCalled: true,
|
||||||
|
assignCalled: true,
|
||||||
|
assignParam: "fakeValue",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ValidAttribute_GetStatusError_ReturnsErr": {
|
||||||
|
args: args{
|
||||||
|
field: &ItemAReferencer{
|
||||||
|
getStatusFn: func(context.Context, CanReference, client.Reader) ([]ReferenceStatus, error) {
|
||||||
|
return nil, errBoom
|
||||||
|
},
|
||||||
|
buildFn: validBuildFn,
|
||||||
|
assignFn: validAssignFn,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: want{
|
||||||
|
getStatusCalled: true,
|
||||||
|
err: errBoom,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ValidAttribute_GetStatusReturnsNotReadyStatus_ReturnsErr": {
|
||||||
|
args: args{
|
||||||
|
field: &ItemAReferencer{
|
||||||
|
getStatusFn: func(context.Context, CanReference, client.Reader) ([]ReferenceStatus, error) {
|
||||||
|
return []ReferenceStatus{{"cool-res", ReferenceNotReady}}, nil
|
||||||
|
},
|
||||||
|
buildFn: validBuildFn,
|
||||||
|
assignFn: validAssignFn,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: want{
|
||||||
|
getStatusCalled: true,
|
||||||
|
err: &referencesAccessErr{[]ReferenceStatus{{"cool-res", ReferenceNotReady}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ValidAttribute_GetStatusReturnsMixedReadyStatus_ReturnsErr": {
|
||||||
|
args: args{
|
||||||
|
field: &ItemAReferencer{
|
||||||
|
getStatusFn: func(context.Context, CanReference, client.Reader) ([]ReferenceStatus, error) {
|
||||||
|
return []ReferenceStatus{
|
||||||
|
{"cool1-res", ReferenceNotFound},
|
||||||
|
{"cool2-res", ReferenceReady},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
buildFn: validBuildFn,
|
||||||
|
assignFn: validAssignFn,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: want{
|
||||||
|
getStatusCalled: true,
|
||||||
|
err: &referencesAccessErr{[]ReferenceStatus{
|
||||||
|
{"cool1-res", ReferenceNotFound},
|
||||||
|
{"cool2-res", ReferenceReady},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ValidAttribute_GetStatusReturnsReadyStatus_ReturnsErr": {
|
||||||
|
args: args{
|
||||||
|
field: &ItemAReferencer{
|
||||||
|
getStatusFn: func(context.Context, CanReference, client.Reader) ([]ReferenceStatus, error) {
|
||||||
|
return []ReferenceStatus{{"cool-res", ReferenceReady}}, nil
|
||||||
|
},
|
||||||
|
buildFn: validBuildFn,
|
||||||
|
assignFn: validAssignFn,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: want{
|
||||||
|
getStatusCalled: true,
|
||||||
|
buildCalled: true,
|
||||||
|
assignCalled: true,
|
||||||
|
assignParam: "fakeValue",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ValidAttribute_BuildError_ReturnsErr": {
|
||||||
|
args: args{
|
||||||
|
field: &ItemAReferencer{
|
||||||
|
getStatusFn: validGetStatusFn,
|
||||||
|
buildFn: func(context.Context, CanReference, client.Reader) (string, error) { return "", errBoom },
|
||||||
|
assignFn: validAssignFn,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: want{
|
||||||
|
getStatusCalled: true,
|
||||||
|
buildCalled: true,
|
||||||
|
err: errors.Wrap(errBoom, errBuildAttribute),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ValidAttribute_AssignError_ReturnsErr": {
|
||||||
|
args: args{
|
||||||
|
field: &ItemAReferencer{
|
||||||
|
getStatusFn: validGetStatusFn,
|
||||||
|
buildFn: validBuildFn,
|
||||||
|
assignFn: func(CanReference, string) error { return errBoom },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: want{
|
||||||
|
getStatusCalled: true,
|
||||||
|
buildCalled: true,
|
||||||
|
assignCalled: true,
|
||||||
|
assignParam: "fakeValue",
|
||||||
|
err: errors.Wrap(errBoom, errAssignAttribute),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ValidAttribute_UpdateResourceError_ReturnsErr": {
|
||||||
|
args: args{
|
||||||
|
field: &ItemAReferencer{
|
||||||
|
getStatusFn: validGetStatusFn,
|
||||||
|
buildFn: validBuildFn,
|
||||||
|
assignFn: validAssignFn,
|
||||||
|
},
|
||||||
|
clientUpdaterErr: errBoom,
|
||||||
|
},
|
||||||
|
want: want{
|
||||||
|
getStatusCalled: true,
|
||||||
|
buildCalled: true,
|
||||||
|
assignCalled: true,
|
||||||
|
assignParam: "fakeValue",
|
||||||
|
err: errors.Wrap(errBoom, errUpdateResourceAfterAssignment),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
|
||||||
|
c := mockClient{updaterErr: tc.args.clientUpdaterErr}
|
||||||
|
rr := NewAPIManagedReferenceResolver(&c)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
res := managed{
|
||||||
|
ItemARef: tc.args.field,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := rr.ResolveReferences(ctx, &res)
|
||||||
|
|
||||||
|
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
|
||||||
|
t.Errorf("ResolveReferences(...): -want error, +got error:\n%s", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotCalls := []bool{tc.args.field.getStatusCalled, tc.args.field.buildCalled, tc.args.field.assignCalled}
|
||||||
|
wantCalls := []bool{tc.want.getStatusCalled, tc.want.buildCalled, tc.want.assignCalled}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(wantCalls, gotCalls); diff != "" {
|
||||||
|
t.Errorf("ResolveReferences(...) => []{getStatusCalled, buildCalled, assignCalled}, : -want, +got:\n%s", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(tc.want.assignParam, tc.args.field.assignParam); diff != "" {
|
||||||
|
t.Errorf("ResolveReferences(...) => []{getStatusCalled, buildCalled, assignCalled}, : -want, +got:\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockClient struct {
|
||||||
|
updaterErr error
|
||||||
|
client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *mockClient) Update(ctx context.Context, obj runtime.Object, opts ...client.UpdateOption) error {
|
||||||
|
return c.updaterErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_ResolveReferences_AttributeNotImplemented_Panics(t *testing.T) {
|
||||||
|
type mockStruct struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
res := struct {
|
||||||
|
MockManaged
|
||||||
|
ItemRef mockStruct `resource:"attributereferencer"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
paniced := false
|
||||||
|
|
||||||
|
func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
paniced = true
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
NewAPIManagedReferenceResolver(struct{ client.Client }{}).
|
||||||
|
ResolveReferences(context.Background(), &res)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if diff := cmp.Diff(paniced, true); diff != "" {
|
||||||
|
t.Errorf("ResolveReferences(...) should panic for invalid attributereferencer: -want , +got :\n%s", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_ResolveReferences_NoReferencersFound_ExitsEarly(t *testing.T) {
|
||||||
|
res := struct {
|
||||||
|
MockManaged
|
||||||
|
}{}
|
||||||
|
|
||||||
|
var wantErr error
|
||||||
|
gotErr := NewAPIManagedReferenceResolver(struct{ client.Client }{}).
|
||||||
|
ResolveReferences(context.Background(), &res)
|
||||||
|
|
||||||
|
if diff := cmp.Diff(wantErr, gotErr, test.EquateErrors()); diff != "" {
|
||||||
|
t.Errorf("ResolveReferences(...) with no referencers: -want error, +got error:\n%s", diff)
|
||||||
|
}
|
||||||
|
}
|
|
@ -138,3 +138,8 @@ func IsBindable(b Bindable) bool {
|
||||||
func IsBound(b Bindable) bool {
|
func IsBound(b Bindable) bool {
|
||||||
return b.GetBindingPhase() == v1alpha1.BindingPhaseBound
|
return b.GetBindingPhase() == v1alpha1.BindingPhaseBound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsConditionTrue returns if condition status is true
|
||||||
|
func IsConditionTrue(c v1alpha1.Condition) bool {
|
||||||
|
return c.Status == corev1.ConditionTrue
|
||||||
|
}
|
||||||
|
|
|
@ -326,3 +326,36 @@ func TestSetBindable(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsConditionTrue(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
c v1alpha1.Condition
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
"IsTrue": {
|
||||||
|
c: v1alpha1.Condition{Status: corev1.ConditionTrue},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
"IsFalse": {
|
||||||
|
c: v1alpha1.Condition{Status: corev1.ConditionFalse},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
"IsUnknown": {
|
||||||
|
c: v1alpha1.Condition{Status: corev1.ConditionUnknown},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
"IsUnset": {
|
||||||
|
c: v1alpha1.Condition{},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range cases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
got := IsConditionTrue(tc.c)
|
||||||
|
if diff := cmp.Diff(tc.want, got); diff != "" {
|
||||||
|
t.Errorf("IsConditionTrue(...): -want, +got:\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue