615 lines
15 KiB
Go
615 lines
15 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 resource
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/pkg/errors"
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
|
|
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
|
|
"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
|
|
"github.com/crossplane/crossplane-runtime/pkg/test"
|
|
)
|
|
|
|
const (
|
|
namespace = "coolns"
|
|
name = "cool"
|
|
uid = types.UID("definitely-a-uuid")
|
|
)
|
|
|
|
var MockOwnerGVK = schema.GroupVersionKind{
|
|
Group: "cool",
|
|
Version: "large",
|
|
Kind: "MockOwner",
|
|
}
|
|
|
|
func TestLocalConnectionSecretFor(t *testing.T) {
|
|
secretName := "coolsecret"
|
|
|
|
type args struct {
|
|
o LocalConnectionSecretOwner
|
|
kind schema.GroupVersionKind
|
|
}
|
|
|
|
controller := true
|
|
|
|
cases := map[string]struct {
|
|
args args
|
|
want *corev1.Secret
|
|
}{
|
|
"Success": {
|
|
args: args{
|
|
o: &fake.MockLocalConnectionSecretOwner{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: namespace,
|
|
Name: name,
|
|
UID: uid,
|
|
},
|
|
Ref: &v1alpha1.LocalSecretReference{Name: secretName},
|
|
},
|
|
kind: MockOwnerGVK,
|
|
},
|
|
want: &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: namespace,
|
|
Name: secretName,
|
|
OwnerReferences: []metav1.OwnerReference{{
|
|
APIVersion: MockOwnerGVK.GroupVersion().String(),
|
|
Kind: MockOwnerGVK.Kind,
|
|
Name: name,
|
|
UID: uid,
|
|
Controller: &controller,
|
|
}},
|
|
},
|
|
Type: SecretTypeConnection,
|
|
Data: map[string][]byte{},
|
|
},
|
|
},
|
|
}
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
got := LocalConnectionSecretFor(tc.args.o, tc.args.kind)
|
|
if diff := cmp.Diff(tc.want, got); diff != "" {
|
|
t.Errorf("LocalConnectionSecretFor(): -want, +got:\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConnectionSecretFor(t *testing.T) {
|
|
secretName := "coolsecret"
|
|
|
|
type args struct {
|
|
o ConnectionSecretOwner
|
|
kind schema.GroupVersionKind
|
|
}
|
|
|
|
controller := true
|
|
|
|
cases := map[string]struct {
|
|
args args
|
|
want *corev1.Secret
|
|
}{
|
|
"Success": {
|
|
args: args{
|
|
o: &fake.MockConnectionSecretOwner{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: namespace,
|
|
Name: name,
|
|
UID: uid,
|
|
},
|
|
Ref: &v1alpha1.SecretReference{Namespace: namespace, Name: secretName},
|
|
},
|
|
kind: MockOwnerGVK,
|
|
},
|
|
want: &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: namespace,
|
|
Name: secretName,
|
|
OwnerReferences: []metav1.OwnerReference{{
|
|
APIVersion: MockOwnerGVK.GroupVersion().String(),
|
|
Kind: MockOwnerGVK.Kind,
|
|
Name: name,
|
|
UID: uid,
|
|
Controller: &controller,
|
|
}},
|
|
},
|
|
Type: SecretTypeConnection,
|
|
Data: map[string][]byte{},
|
|
},
|
|
},
|
|
}
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
got := ConnectionSecretFor(tc.args.o, tc.args.kind)
|
|
if diff := cmp.Diff(tc.want, got); diff != "" {
|
|
t.Errorf("ConnectionSecretFor(): -want, +got:\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type MockTyper struct {
|
|
GVKs []schema.GroupVersionKind
|
|
Unversioned bool
|
|
Error error
|
|
}
|
|
|
|
func (t MockTyper) ObjectKinds(_ runtime.Object) ([]schema.GroupVersionKind, bool, error) {
|
|
return t.GVKs, t.Unversioned, t.Error
|
|
}
|
|
|
|
func (t MockTyper) Recognizes(_ schema.GroupVersionKind) bool { return true }
|
|
|
|
func TestGetKind(t *testing.T) {
|
|
type args struct {
|
|
obj runtime.Object
|
|
ot runtime.ObjectTyper
|
|
}
|
|
type want struct {
|
|
kind schema.GroupVersionKind
|
|
err error
|
|
}
|
|
|
|
errBoom := errors.New("boom")
|
|
|
|
cases := map[string]struct {
|
|
args args
|
|
want want
|
|
}{
|
|
"KindFound": {
|
|
args: args{
|
|
ot: MockTyper{GVKs: []schema.GroupVersionKind{fake.GVK(&fake.Managed{})}},
|
|
},
|
|
want: want{
|
|
kind: fake.GVK(&fake.Managed{}),
|
|
},
|
|
},
|
|
"KindError": {
|
|
args: args{
|
|
ot: MockTyper{Error: errBoom},
|
|
},
|
|
want: want{
|
|
err: errors.Wrap(errBoom, "cannot get kind of supplied object"),
|
|
},
|
|
},
|
|
"KindIsUnversioned": {
|
|
args: args{
|
|
ot: MockTyper{Unversioned: true},
|
|
},
|
|
want: want{
|
|
err: errors.New("supplied object is unversioned"),
|
|
},
|
|
},
|
|
"NotEnoughKinds": {
|
|
args: args{
|
|
ot: MockTyper{},
|
|
},
|
|
want: want{
|
|
err: errors.New("supplied object does not have exactly one kind"),
|
|
},
|
|
},
|
|
"TooManyKinds": {
|
|
args: args{
|
|
ot: MockTyper{GVKs: []schema.GroupVersionKind{
|
|
fake.GVK(&fake.Object{}),
|
|
fake.GVK(&fake.Managed{}),
|
|
}},
|
|
},
|
|
want: want{
|
|
err: errors.New("supplied object does not have exactly one kind"),
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
got, err := GetKind(tc.args.obj, tc.args.ot)
|
|
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
|
|
t.Errorf("GetKind(...): -want error, +got error:\n%s", diff)
|
|
}
|
|
if diff := cmp.Diff(tc.want.kind, got); diff != "" {
|
|
t.Errorf("GetKind(...): -want, +got:\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
func TestMustCreateObject(t *testing.T) {
|
|
type args struct {
|
|
kind schema.GroupVersionKind
|
|
oc runtime.ObjectCreater
|
|
}
|
|
cases := map[string]struct {
|
|
args args
|
|
want runtime.Object
|
|
}{
|
|
"KindRegistered": {
|
|
args: args{
|
|
kind: fake.GVK(&fake.Managed{}),
|
|
oc: fake.SchemeWith(&fake.Managed{}),
|
|
},
|
|
want: &fake.Managed{},
|
|
},
|
|
}
|
|
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
got := MustCreateObject(tc.args.kind, tc.args.oc)
|
|
if diff := cmp.Diff(tc.want, got); diff != "" {
|
|
t.Errorf("MustCreateObject(...): -want, +got:\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIgnore(t *testing.T) {
|
|
errBoom := errors.New("boom")
|
|
|
|
type args struct {
|
|
is ErrorIs
|
|
err error
|
|
}
|
|
cases := map[string]struct {
|
|
args args
|
|
want error
|
|
}{
|
|
"IgnoreError": {
|
|
args: args{
|
|
is: func(err error) bool { return true },
|
|
err: errBoom,
|
|
},
|
|
want: nil,
|
|
},
|
|
"PropagateError": {
|
|
args: args{
|
|
is: func(err error) bool { return false },
|
|
err: errBoom,
|
|
},
|
|
want: errBoom,
|
|
},
|
|
}
|
|
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
got := Ignore(tc.args.is, tc.args.err)
|
|
if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" {
|
|
t.Errorf("Ignore(...): -want error, +got error:\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type object struct {
|
|
runtime.Object
|
|
metav1.ObjectMeta
|
|
}
|
|
|
|
func (o *object) DeepCopyObject() runtime.Object {
|
|
return &object{ObjectMeta: *o.ObjectMeta.DeepCopy()}
|
|
}
|
|
|
|
type nopeject struct {
|
|
runtime.Object
|
|
}
|
|
|
|
func (o *nopeject) DeepCopyObject() runtime.Object {
|
|
return &nopeject{}
|
|
}
|
|
|
|
func TestControllersMustMatch(t *testing.T) {
|
|
uid := types.UID("very-unique-string")
|
|
controller := true
|
|
|
|
type args struct {
|
|
ctx context.Context
|
|
current runtime.Object
|
|
desired runtime.Object
|
|
}
|
|
|
|
cases := map[string]struct {
|
|
reason string
|
|
args args
|
|
want error
|
|
}{
|
|
"ControllersMatch": {
|
|
reason: "The current and desired objects have matching controller references",
|
|
args: args{
|
|
current: &object{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{
|
|
UID: uid,
|
|
Controller: &controller,
|
|
}}}},
|
|
desired: &object{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{
|
|
UID: uid,
|
|
Controller: &controller,
|
|
}}}},
|
|
},
|
|
},
|
|
"ControllersDoNotMatch": {
|
|
reason: "The current and desired objects do not have matching controller references",
|
|
args: args{
|
|
current: &object{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{
|
|
UID: uid,
|
|
Controller: &controller,
|
|
}}}},
|
|
desired: &object{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{
|
|
UID: types.UID("some-other-uid"),
|
|
Controller: &controller,
|
|
}}}},
|
|
},
|
|
want: errors.New("existing object has a different (or no) controller"),
|
|
},
|
|
}
|
|
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
ao := ControllersMustMatch()
|
|
err := ao(tc.args.ctx, tc.args.current, tc.args.desired)
|
|
|
|
if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" {
|
|
t.Errorf("\n%s\nControllersMustMatch(...)(...): -want error, +got error\n%s\n", tc.reason, diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsNotControllable(t *testing.T) {
|
|
cases := map[string]struct {
|
|
reason string
|
|
err error
|
|
want bool
|
|
}{
|
|
"NilError": {
|
|
reason: "A nil error does not indicate something is not controllable.",
|
|
err: nil,
|
|
want: false,
|
|
},
|
|
"UnknownError": {
|
|
reason: "An that doesn't have a 'NotControllable() bool' method does not indicate something is not controllable.",
|
|
err: errors.New("boom"),
|
|
want: false,
|
|
},
|
|
"NotControllableError": {
|
|
reason: "An that has a 'NotControllable() bool' method indicates something is not controllable.",
|
|
err: errNotControllable{errors.New("boom")},
|
|
want: true,
|
|
},
|
|
}
|
|
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
got := IsNotControllable(tc.err)
|
|
if diff := cmp.Diff(tc.want, got); diff != "" {
|
|
t.Errorf("\n%s\nIsNotControllable(...): -want, +got:\n%s\n", tc.reason, diff)
|
|
}
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
func TestMustBeControllableBy(t *testing.T) {
|
|
uid := types.UID("very-unique-string")
|
|
controller := true
|
|
|
|
type args struct {
|
|
ctx context.Context
|
|
current runtime.Object
|
|
desired runtime.Object
|
|
}
|
|
|
|
cases := map[string]struct {
|
|
reason string
|
|
u types.UID
|
|
args args
|
|
want error
|
|
}{
|
|
"Adoptable": {
|
|
reason: "A current object with no controller reference may be adopted and controlled",
|
|
u: uid,
|
|
args: args{
|
|
current: &object{},
|
|
},
|
|
},
|
|
"ControlledBySuppliedUID": {
|
|
reason: "A current object that is already controlled by the supplied UID is controllable",
|
|
u: uid,
|
|
args: args{
|
|
current: &object{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{
|
|
UID: uid,
|
|
Controller: &controller,
|
|
}}}},
|
|
},
|
|
},
|
|
"ControlledBySomeoneElse": {
|
|
reason: "A current object that is already controlled by a different UID is not controllable",
|
|
u: uid,
|
|
args: args{
|
|
current: &object{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{
|
|
UID: types.UID("some-other-uid"),
|
|
Controller: &controller,
|
|
}}}},
|
|
},
|
|
want: errNotControllable{errors.Errorf("existing object is not controlled by UID %q", uid)},
|
|
},
|
|
}
|
|
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
ao := MustBeControllableBy(tc.u)
|
|
err := ao(tc.args.ctx, tc.args.current, tc.args.desired)
|
|
|
|
if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" {
|
|
t.Errorf("\n%s\nMustBeControllableBy(...)(...): -want error, +got error\n%s\n", tc.reason, diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
func TestConnectionSecretMustBeControllableBy(t *testing.T) {
|
|
uid := types.UID("very-unique-string")
|
|
controller := true
|
|
|
|
type args struct {
|
|
ctx context.Context
|
|
current runtime.Object
|
|
desired runtime.Object
|
|
}
|
|
|
|
cases := map[string]struct {
|
|
reason string
|
|
u types.UID
|
|
args args
|
|
want error
|
|
}{
|
|
"Adoptable": {
|
|
reason: "A Secret of SecretTypeConnection with no controller reference may be adopted and controlled",
|
|
u: uid,
|
|
args: args{
|
|
current: &corev1.Secret{Type: SecretTypeConnection},
|
|
},
|
|
},
|
|
"ControlledBySuppliedUID": {
|
|
reason: "A Secret of any type that is already controlled by the supplied UID is controllable",
|
|
u: uid,
|
|
args: args{
|
|
current: &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{
|
|
UID: uid,
|
|
Controller: &controller,
|
|
}}},
|
|
Type: corev1.SecretTypeOpaque,
|
|
},
|
|
},
|
|
},
|
|
"ControlledBySomeoneElse": {
|
|
reason: "A Secret of any type that is already controlled by the another UID is not controllable",
|
|
u: uid,
|
|
args: args{
|
|
current: &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{
|
|
UID: types.UID("some-other-uid"),
|
|
Controller: &controller,
|
|
}}},
|
|
Type: SecretTypeConnection,
|
|
},
|
|
},
|
|
want: errNotControllable{errors.Errorf("existing secret is not controlled by UID %q", uid)},
|
|
},
|
|
"UncontrolledOpaqueSecret": {
|
|
reason: "A Secret of corev1.SecretTypeOpqaue with no controller is not controllable",
|
|
u: uid,
|
|
args: args{
|
|
current: &corev1.Secret{Type: corev1.SecretTypeOpaque},
|
|
},
|
|
want: errNotControllable{errors.Errorf("refusing to modify uncontrolled secret of type %q", corev1.SecretTypeOpaque)},
|
|
},
|
|
}
|
|
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
ao := ConnectionSecretMustBeControllableBy(tc.u)
|
|
err := ao(tc.args.ctx, tc.args.current, tc.args.desired)
|
|
|
|
if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" {
|
|
t.Errorf("\n%s\nConnectionSecretMustBeControllableBy(...)(...): -want error, +got error\n%s\n", tc.reason, diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetExternalTags(t *testing.T) {
|
|
provName := "prov"
|
|
cases := map[string]struct {
|
|
o Managed
|
|
want map[string]string
|
|
}{
|
|
"Successful": {
|
|
o: &fake.Managed{ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
},
|
|
ProviderReferencer: fake.ProviderReferencer{Ref: &v1alpha1.Reference{Name: provName}},
|
|
},
|
|
want: map[string]string{
|
|
ExternalResourceTagKeyKind: strings.ToLower((&fake.Managed{}).GetObjectKind().GroupVersionKind().GroupKind().String()),
|
|
ExternalResourceTagKeyName: name,
|
|
ExternalResourceTagKeyProvider: provName,
|
|
},
|
|
},
|
|
"SuccessfulWithProviderConfig": {
|
|
o: &fake.Managed{ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
},
|
|
ProviderConfigReferencer: fake.ProviderConfigReferencer{Ref: &v1alpha1.Reference{Name: provName}},
|
|
},
|
|
want: map[string]string{
|
|
ExternalResourceTagKeyKind: strings.ToLower((&fake.Managed{}).GetObjectKind().GroupVersionKind().GroupKind().String()),
|
|
ExternalResourceTagKeyName: name,
|
|
ExternalResourceTagKeyProvider: provName,
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
got := GetExternalTags(tc.o)
|
|
if diff := cmp.Diff(tc.want, got); diff != "" {
|
|
t.Errorf("GetExternalTags(...): -want, +got:\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|