Merge pull request #46 from soorena776/issue_887

Implementating ReferenceResolver in managed reconciler
This commit is contained in:
Nic Cope 2019-10-21 17:17:40 -07:00 committed by GitHub
commit a56c70ba62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 1013 additions and 19 deletions

View File

@ -34,6 +34,9 @@ const (
// TypeSynced managed resources are believed to be in sync with the
// Kubernetes resources that manage their lifecycle.
TypeSynced ConditionType = "Synced"
// TypeReferencesResolved managed resources' references are resolved
TypeReferencesResolved = "ReferencesResolved"
)
// 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"
)
// 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.
type Condition struct {
// 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
}
// 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
// of the same type. This is a no-op if all supplied conditions are identical,
// ignoring the last transition time, to those already set.
@ -239,3 +260,28 @@ func ReconcileError(err error) Condition {
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(),
}
}

View File

@ -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) {
testMsg := "Something went wrong on cloud side"
cases := map[string]struct {

View File

@ -31,11 +31,11 @@ type Bindable interface {
GetBindingPhase() v1alpha1.BindingPhase
}
// A ConditionSetter may have conditions set. Conditions are informational, and
// typically indicate the status of both a resource and its reconciliation
// process.
type ConditionSetter interface {
// A Conditioned may have conditions set or retrieved. Conditions are typically
// indicate the status of both a resource and its reconciliation process.
type Conditioned interface {
SetConditions(c ...v1alpha1.Condition)
GetCondition(v1alpha1.ConditionType) v1alpha1.Condition
}
// A ClaimReferencer may reference a resource claim.
@ -91,7 +91,7 @@ type Claim interface {
ManagedResourceReferencer
ConnectionSecretWriterTo
ConditionSetter
Conditioned
Bindable
}
@ -115,7 +115,7 @@ type Managed interface {
ConnectionSecretWriterTo
Reclaimer
ConditionSetter
Conditioned
Bindable
}

View File

@ -29,9 +29,12 @@ type MockBindable struct{ Phase v1alpha1.BindingPhase }
func (m *MockBindable) SetBindingPhase(p v1alpha1.BindingPhase) { m.Phase = p }
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 }
@ -89,7 +92,7 @@ type MockClaim struct {
MockPortableClassReferencer
MockManagedResourceReferencer
MockConnectionSecretWriterTo
MockConditionSetter
MockConditioned
MockBindable
}
@ -112,7 +115,7 @@ type MockManaged struct {
MockClaimReferencer
MockConnectionSecretWriterTo
MockReclaimer
MockConditionSetter
MockConditioned
MockBindable
}

View File

@ -34,6 +34,8 @@ import (
const (
managedControllerName = "managedresource.crossplane.io"
managedFinalizerName = "finalizer." + managedControllerName
managedResourceStructTagPackageName = "resource"
managedReconcileTimeout = 1 * time.Minute
defaultManagedShortWait = 30 * time.Second
@ -87,6 +89,14 @@ func (m ManagedInitializerFn) Initialize(ctx context.Context, mg Managed) error
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
// Managed resource.
type ExternalConnecter interface {
@ -224,6 +234,7 @@ type mrManaged struct {
ManagedConnectionPublisher
ManagedFinalizer
ManagedInitializer
ManagedReferenceResolver
}
func defaultMRManaged(m manager.Manager) mrManaged {
@ -234,6 +245,7 @@ func defaultMRManaged(m manager.Manager) mrManaged {
NewManagedNameAsExternalName(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)
}
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)
if err != nil {
// We'll usually hit this case if our Provider credentials are invalid

View File

@ -51,6 +51,7 @@ func TestManagedReconciler(t *testing.T) {
}
errBoom := errors.New("boom")
errNotReady := &referencesAccessErr{[]ReferenceStatus{{Name: "cool-res", Status: ReferenceNotReady}}}
now := metav1.Now()
testFinalizers := []string{"finalizer.crossplane.io"}
testConnectionDetails := ConnectionDetails{
@ -517,6 +518,82 @@ func TestManagedReconciler(t *testing.T) {
},
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": {
args: args{
m: &MockManager{
@ -534,13 +611,7 @@ func TestManagedReconciler(t *testing.T) {
s: MockSchemeWith(&MockManaged{}),
},
mg: ManagedKind(MockGVK(&MockManaged{})),
e: &ExternalClientFns{
ObserveFn: func(_ context.Context, _ Managed) (ExternalObservation, error) {
return ExternalObservation{
ResourceExists: false,
}, nil
},
},
e: &ExternalClientFns{},
o: []ManagedReconcilerOption{
func(r *ManagedReconciler) {
r.managed.ManagedConnectionPublisher = ManagedConnectionPublisherFns{

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -138,3 +138,8 @@ func IsBindable(b Bindable) bool {
func IsBound(b Bindable) bool {
return b.GetBindingPhase() == v1alpha1.BindingPhaseBound
}
// IsConditionTrue returns if condition status is true
func IsConditionTrue(c v1alpha1.Condition) bool {
return c.Status == corev1.ConditionTrue
}

View File

@ -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)
}
})
}
}