547 lines
14 KiB
Go
547 lines
14 KiB
Go
/*
|
|
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
|
|
|
|
htcp://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 reference
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
|
|
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
|
|
"github.com/crossplane/crossplane-runtime/pkg/errors"
|
|
"github.com/crossplane/crossplane-runtime/pkg/meta"
|
|
"github.com/crossplane/crossplane-runtime/pkg/resource"
|
|
"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
|
|
"github.com/crossplane/crossplane-runtime/pkg/test"
|
|
)
|
|
|
|
// TODO(negz): Find a better home for this. It can't currently live alongside
|
|
// its contemporaries in pkg/resource/fake because it would cause an import
|
|
// cycle.
|
|
type FakeManagedList struct {
|
|
client.ObjectList
|
|
|
|
Items []resource.Managed
|
|
}
|
|
|
|
func (fml *FakeManagedList) GetItems() []resource.Managed {
|
|
return fml.Items
|
|
}
|
|
|
|
func TestToAndFromPtr(t *testing.T) {
|
|
cases := map[string]struct {
|
|
want string
|
|
}{
|
|
"Zero": {want: ""},
|
|
"NonZero": {want: "pointy"},
|
|
}
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
got := FromPtrValue(ToPtrValue(tc.want))
|
|
if diff := cmp.Diff(tc.want, got); diff != "" {
|
|
t.Errorf("FromPtrValue(ToPtrValue(%s): -want, +got: %s", tc.want, diff)
|
|
|
|
}
|
|
})
|
|
|
|
}
|
|
}
|
|
|
|
func TestToAndFromPtrValues(t *testing.T) {
|
|
cases := map[string]struct {
|
|
want []string
|
|
}{
|
|
"Nil": {want: []string{}},
|
|
"Zero": {want: []string{""}},
|
|
"NonZero": {want: []string{"pointy"}},
|
|
"Multiple": {want: []string{"pointy", "pointers"}},
|
|
}
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
got := FromPtrValues(ToPtrValues(tc.want))
|
|
if diff := cmp.Diff(tc.want, got); diff != "" {
|
|
t.Errorf("FromPtrValues(ToPtrValues(%s): -want, +got: %s", tc.want, diff)
|
|
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolve(t *testing.T) {
|
|
errBoom := errors.New("boom")
|
|
now := metav1.Now()
|
|
value := "coolv"
|
|
ref := &xpv1.Reference{Name: "cool"}
|
|
optionalRef := &xpv1.Reference{Name: "cool", Policy: xpv1.ReferencePolicyOptional}
|
|
|
|
controlled := &fake.Managed{}
|
|
controlled.SetName(value)
|
|
meta.SetExternalName(controlled, value)
|
|
meta.AddControllerReference(controlled, meta.AsController(&xpv1.TypedReference{UID: types.UID("very-unique")}))
|
|
|
|
type args struct {
|
|
ctx context.Context
|
|
req ResolutionRequest
|
|
}
|
|
type want struct {
|
|
rsp ResolutionResponse
|
|
err error
|
|
}
|
|
cases := map[string]struct {
|
|
reason string
|
|
c client.Reader
|
|
from resource.Managed
|
|
args args
|
|
want want
|
|
}{
|
|
"FromDeleted": {
|
|
reason: "Should return early if the referencing managed resource was deleted",
|
|
from: &fake.Managed{ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &now}},
|
|
args: args{
|
|
req: ResolutionRequest{},
|
|
},
|
|
want: want{
|
|
rsp: ResolutionResponse{},
|
|
err: nil,
|
|
},
|
|
},
|
|
"AlreadyResolved": {
|
|
reason: "Should return early if the current value is non-zero",
|
|
from: &fake.Managed{},
|
|
args: args{
|
|
req: ResolutionRequest{CurrentValue: value},
|
|
},
|
|
want: want{
|
|
rsp: ResolutionResponse{ResolvedValue: value},
|
|
err: nil,
|
|
},
|
|
},
|
|
"Unresolvable": {
|
|
reason: "Should return early if neither a reference or selector were provided",
|
|
from: &fake.Managed{},
|
|
args: args{
|
|
req: ResolutionRequest{},
|
|
},
|
|
want: want{
|
|
err: nil,
|
|
},
|
|
},
|
|
"GetError": {
|
|
reason: "Should return errors encountered while getting the referenced resource",
|
|
c: &test.MockClient{
|
|
MockGet: test.NewMockGetFn(errBoom),
|
|
},
|
|
from: &fake.Managed{},
|
|
args: args{
|
|
req: ResolutionRequest{
|
|
Reference: ref,
|
|
To: To{Managed: &fake.Managed{}},
|
|
Extract: ExternalName(),
|
|
},
|
|
},
|
|
want: want{
|
|
err: errors.Wrap(errBoom, errGetManaged),
|
|
},
|
|
},
|
|
"ResolvedNoValue": {
|
|
reason: "Should return an error if the extract function returns the empty string",
|
|
c: &test.MockClient{
|
|
MockGet: test.NewMockGetFn(nil),
|
|
},
|
|
from: &fake.Managed{},
|
|
args: args{
|
|
req: ResolutionRequest{
|
|
Reference: ref,
|
|
To: To{Managed: &fake.Managed{}},
|
|
Extract: func(resource.Managed) string { return "" },
|
|
},
|
|
},
|
|
want: want{
|
|
rsp: ResolutionResponse{
|
|
ResolvedReference: ref,
|
|
},
|
|
err: errors.New(errNoValue),
|
|
},
|
|
},
|
|
"SuccessfulResolve": {
|
|
reason: "No error should be returned when the value is successfully extracted",
|
|
c: &test.MockClient{
|
|
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
|
|
meta.SetExternalName(obj.(metav1.Object), value)
|
|
return nil
|
|
}),
|
|
},
|
|
from: &fake.Managed{},
|
|
args: args{
|
|
req: ResolutionRequest{
|
|
Reference: ref,
|
|
To: To{Managed: &fake.Managed{}},
|
|
Extract: ExternalName(),
|
|
},
|
|
},
|
|
want: want{
|
|
rsp: ResolutionResponse{
|
|
ResolvedValue: value,
|
|
ResolvedReference: ref,
|
|
},
|
|
},
|
|
},
|
|
"OptionalPolicy": {
|
|
reason: "No error should be returned when the resolution policy is Optional",
|
|
c: &test.MockClient{
|
|
MockGet: test.NewMockGetFn(nil),
|
|
},
|
|
from: &fake.Managed{},
|
|
args: args{
|
|
req: ResolutionRequest{
|
|
Reference: optionalRef,
|
|
To: To{Managed: &fake.Managed{}},
|
|
Extract: func(resource.Managed) string { return "" },
|
|
},
|
|
},
|
|
want: want{
|
|
rsp: ResolutionResponse{
|
|
ResolvedReference: optionalRef,
|
|
},
|
|
err: nil,
|
|
},
|
|
},
|
|
"ListError": {
|
|
reason: "Should return errors encountered while listing potential referenced resources",
|
|
c: &test.MockClient{
|
|
MockList: test.NewMockListFn(errBoom),
|
|
},
|
|
from: &fake.Managed{},
|
|
args: args{
|
|
req: ResolutionRequest{
|
|
Selector: &xpv1.Selector{},
|
|
},
|
|
},
|
|
want: want{
|
|
rsp: ResolutionResponse{},
|
|
err: errors.Wrap(errBoom, errListManaged),
|
|
},
|
|
},
|
|
"NoMatches": {
|
|
reason: "Should return an error when no managed resources match the selector",
|
|
c: &test.MockClient{
|
|
MockList: test.NewMockListFn(nil),
|
|
},
|
|
from: &fake.Managed{},
|
|
args: args{
|
|
req: ResolutionRequest{
|
|
Selector: &xpv1.Selector{},
|
|
To: To{List: &FakeManagedList{}},
|
|
},
|
|
},
|
|
want: want{
|
|
rsp: ResolutionResponse{},
|
|
err: errors.New(errNoMatches),
|
|
},
|
|
},
|
|
"SuccessfulSelect": {
|
|
reason: "A managed resource with a matching controller reference should be selected and returned",
|
|
c: &test.MockClient{
|
|
MockList: test.NewMockListFn(nil),
|
|
},
|
|
from: controlled,
|
|
args: args{
|
|
req: ResolutionRequest{
|
|
Selector: &xpv1.Selector{
|
|
MatchControllerRef: func() *bool { t := true; return &t }(),
|
|
},
|
|
To: To{List: &FakeManagedList{Items: []resource.Managed{
|
|
&fake.Managed{}, // A resource that does not match.
|
|
controlled, // A resource with a matching controller reference.
|
|
}}},
|
|
Extract: ExternalName(),
|
|
},
|
|
},
|
|
want: want{
|
|
rsp: ResolutionResponse{
|
|
ResolvedValue: value,
|
|
ResolvedReference: &xpv1.Reference{Name: value},
|
|
},
|
|
err: nil,
|
|
},
|
|
},
|
|
}
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
r := NewAPIResolver(tc.c, tc.from)
|
|
got, err := r.Resolve(tc.args.ctx, tc.args.req)
|
|
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
|
|
t.Errorf("\n%s\nControllersMustMatch(...): -want error, +got error:\n%s", tc.reason, diff)
|
|
}
|
|
if diff := cmp.Diff(tc.want.rsp, got); diff != "" {
|
|
t.Errorf("\n%s\nControllersMustMatch(...): -want, +got:\n%s", tc.reason, diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
func TestResolveMultiple(t *testing.T) {
|
|
errBoom := errors.New("boom")
|
|
now := metav1.Now()
|
|
value := "coolv"
|
|
ref := xpv1.Reference{Name: "cool"}
|
|
optionalRef := xpv1.Reference{Name: "cool", Policy: xpv1.ReferencePolicyOptional}
|
|
|
|
controlled := &fake.Managed{}
|
|
controlled.SetName(value)
|
|
meta.SetExternalName(controlled, value)
|
|
meta.AddControllerReference(controlled, meta.AsController(&xpv1.TypedReference{UID: types.UID("very-unique")}))
|
|
|
|
type args struct {
|
|
ctx context.Context
|
|
req MultiResolutionRequest
|
|
}
|
|
type want struct {
|
|
rsp MultiResolutionResponse
|
|
err error
|
|
}
|
|
cases := map[string]struct {
|
|
reason string
|
|
c client.Reader
|
|
from resource.Managed
|
|
args args
|
|
want want
|
|
}{
|
|
"FromDeleted": {
|
|
reason: "Should return early if the referencing managed resource was deleted",
|
|
from: &fake.Managed{ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &now}},
|
|
args: args{
|
|
req: MultiResolutionRequest{},
|
|
},
|
|
want: want{
|
|
rsp: MultiResolutionResponse{},
|
|
err: nil,
|
|
},
|
|
},
|
|
"AlreadyResolved": {
|
|
reason: "Should return early if the current value is non-zero",
|
|
from: &fake.Managed{},
|
|
args: args{
|
|
req: MultiResolutionRequest{CurrentValues: []string{value}},
|
|
},
|
|
want: want{
|
|
rsp: MultiResolutionResponse{ResolvedValues: []string{value}},
|
|
err: nil,
|
|
},
|
|
},
|
|
"Unresolvable": {
|
|
reason: "Should return early if neither a reference or selector were provided",
|
|
from: &fake.Managed{},
|
|
args: args{
|
|
req: MultiResolutionRequest{},
|
|
},
|
|
want: want{
|
|
err: nil,
|
|
},
|
|
},
|
|
"GetError": {
|
|
reason: "Should return errors encountered while getting the referenced resource",
|
|
c: &test.MockClient{
|
|
MockGet: test.NewMockGetFn(errBoom),
|
|
},
|
|
from: &fake.Managed{},
|
|
args: args{
|
|
req: MultiResolutionRequest{
|
|
References: []xpv1.Reference{ref},
|
|
To: To{Managed: &fake.Managed{}},
|
|
Extract: ExternalName(),
|
|
},
|
|
},
|
|
want: want{
|
|
err: errors.Wrap(errBoom, errGetManaged),
|
|
},
|
|
},
|
|
"ResolvedNoValue": {
|
|
reason: "Should return an error if the extract function returns the empty string",
|
|
c: &test.MockClient{
|
|
MockGet: test.NewMockGetFn(nil),
|
|
},
|
|
from: &fake.Managed{},
|
|
args: args{
|
|
req: MultiResolutionRequest{
|
|
References: []xpv1.Reference{ref},
|
|
To: To{Managed: &fake.Managed{}},
|
|
Extract: func(resource.Managed) string { return "" },
|
|
},
|
|
},
|
|
want: want{
|
|
rsp: MultiResolutionResponse{
|
|
ResolvedValues: []string{""},
|
|
ResolvedReferences: []xpv1.Reference{ref},
|
|
},
|
|
err: errors.New(errNoValue),
|
|
},
|
|
},
|
|
"SuccessfulResolve": {
|
|
reason: "No error should be returned when the value is successfully extracted",
|
|
c: &test.MockClient{
|
|
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
|
|
meta.SetExternalName(obj.(metav1.Object), value)
|
|
return nil
|
|
}),
|
|
},
|
|
from: &fake.Managed{},
|
|
args: args{
|
|
req: MultiResolutionRequest{
|
|
References: []xpv1.Reference{ref},
|
|
To: To{Managed: &fake.Managed{}},
|
|
Extract: ExternalName(),
|
|
},
|
|
},
|
|
want: want{
|
|
rsp: MultiResolutionResponse{
|
|
ResolvedValues: []string{value},
|
|
ResolvedReferences: []xpv1.Reference{ref},
|
|
},
|
|
},
|
|
},
|
|
"OptionalPolicy": {
|
|
reason: "No error should be returned when the resolution policy is Optional",
|
|
c: &test.MockClient{
|
|
MockGet: test.NewMockGetFn(nil),
|
|
},
|
|
from: &fake.Managed{},
|
|
args: args{
|
|
req: MultiResolutionRequest{
|
|
References: []xpv1.Reference{optionalRef},
|
|
To: To{Managed: &fake.Managed{}},
|
|
Extract: func(resource.Managed) string { return "" },
|
|
},
|
|
},
|
|
want: want{
|
|
rsp: MultiResolutionResponse{
|
|
ResolvedValues: []string{""},
|
|
ResolvedReferences: []xpv1.Reference{optionalRef},
|
|
},
|
|
err: nil,
|
|
},
|
|
},
|
|
"ListError": {
|
|
reason: "Should return errors encountered while listing potential referenced resources",
|
|
c: &test.MockClient{
|
|
MockList: test.NewMockListFn(errBoom),
|
|
},
|
|
from: &fake.Managed{},
|
|
args: args{
|
|
req: MultiResolutionRequest{
|
|
Selector: &xpv1.Selector{},
|
|
},
|
|
},
|
|
want: want{
|
|
rsp: MultiResolutionResponse{},
|
|
err: errors.Wrap(errBoom, errListManaged),
|
|
},
|
|
},
|
|
"NoMatches": {
|
|
reason: "Should return an error when no managed resources match the selector",
|
|
c: &test.MockClient{
|
|
MockList: test.NewMockListFn(nil),
|
|
},
|
|
from: &fake.Managed{},
|
|
args: args{
|
|
req: MultiResolutionRequest{
|
|
Selector: &xpv1.Selector{},
|
|
To: To{List: &FakeManagedList{}},
|
|
},
|
|
},
|
|
want: want{
|
|
rsp: MultiResolutionResponse{},
|
|
err: errors.New(errNoMatches),
|
|
},
|
|
},
|
|
"SuccessfulSelect": {
|
|
reason: "A managed resource with a matching controller reference should be selected and returned",
|
|
c: &test.MockClient{
|
|
MockList: test.NewMockListFn(nil),
|
|
},
|
|
from: controlled,
|
|
args: args{
|
|
req: MultiResolutionRequest{
|
|
Selector: &xpv1.Selector{
|
|
MatchControllerRef: func() *bool { t := true; return &t }(),
|
|
},
|
|
To: To{List: &FakeManagedList{Items: []resource.Managed{
|
|
&fake.Managed{}, // A resource that does not match.
|
|
controlled, // A resource with a matching controller reference.
|
|
}}},
|
|
Extract: ExternalName(),
|
|
},
|
|
},
|
|
want: want{
|
|
rsp: MultiResolutionResponse{
|
|
ResolvedValues: []string{value},
|
|
ResolvedReferences: []xpv1.Reference{{Name: value}},
|
|
},
|
|
err: nil,
|
|
},
|
|
},
|
|
}
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
r := NewAPIResolver(tc.c, tc.from)
|
|
got, err := r.ResolveMultiple(tc.args.ctx, tc.args.req)
|
|
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
|
|
t.Errorf("\n%s\nControllersMustMatch(...): -want error, +got error:\n%s", tc.reason, diff)
|
|
}
|
|
if diff := cmp.Diff(tc.want.rsp, got, cmpopts.EquateEmpty()); diff != "" {
|
|
t.Errorf("\n%s\nControllersMustMatch(...): -want, +got:\n%s", tc.reason, diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestControllersMustMatch(t *testing.T) {
|
|
cases := map[string]struct {
|
|
s *xpv1.Selector
|
|
want bool
|
|
}{
|
|
"NilSelector": {
|
|
s: nil,
|
|
want: false,
|
|
},
|
|
"NilMatchControllerRef": {
|
|
s: &xpv1.Selector{},
|
|
want: false,
|
|
},
|
|
"False": {
|
|
s: &xpv1.Selector{MatchControllerRef: func() *bool { f := false; return &f }()},
|
|
want: false,
|
|
},
|
|
"True": {
|
|
s: &xpv1.Selector{MatchControllerRef: func() *bool { t := true; return &t }()},
|
|
want: true,
|
|
},
|
|
}
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
got := ControllersMustMatch(tc.s)
|
|
if diff := cmp.Diff(tc.want, got); diff != "" {
|
|
t.Errorf("ControllersMustMatch(...): -want, +got:\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|