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
|
||||
// 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(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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 {
|
||||
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