crossplane-runtime/pkg/resource/api_test.go

609 lines
16 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
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"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/meta"
"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
"github.com/crossplane/crossplane-runtime/pkg/test"
)
var (
_ ManagedConnectionPropagator = &APIManagedConnectionPropagator{}
)
func TestPropagateConnection(t *testing.T) {
errBoom := errors.New("boom")
mgcsns := "coolnamespace"
mgcsname := "coolmanagedsecret"
mgcsdata := map[string][]byte{"cool": {1}}
cmcsns := "coolnamespace"
cmcsname := "coolclaimsecret"
mg := &fake.Managed{
ConnectionSecretWriterTo: fake.ConnectionSecretWriterTo{
Ref: &v1alpha1.SecretReference{Namespace: mgcsns, Name: mgcsname},
},
}
cm := &fake.Claim{
ObjectMeta: metav1.ObjectMeta{Namespace: cmcsns},
LocalConnectionSecretWriterTo: fake.LocalConnectionSecretWriterTo{
Ref: &v1alpha1.LocalSecretReference{Name: cmcsname},
},
}
type fields struct {
client ClientApplicator
typer runtime.ObjectTyper
}
type args struct {
ctx context.Context
o LocalConnectionSecretOwner
mg Managed
}
cases := map[string]struct {
reason string
fields fields
args args
want error
}{
"ClaimDoesNotWantConnectionSecret": {
reason: "The managed resource's secret should not be propagated if the claim does not want to write one",
args: args{
o: &fake.Claim{},
mg: mg,
},
want: nil,
},
"ManagedDoesNotExposeConnectionSecret": {
reason: "The managed resource's secret should not be propagated if it does not have one",
args: args{
o: cm,
mg: &fake.Managed{},
},
want: nil,
},
"GetManagedSecretError": {
reason: "Errors getting the managed resource's connection secret should be returned",
fields: fields{
client: ClientApplicator{
Client: &test.MockClient{MockGet: test.NewMockGetFn(errBoom)},
},
},
args: args{
o: cm,
mg: mg,
},
want: errors.Wrap(errBoom, errGetSecret),
},
"ManagedResourceDoesNotControlSecret": {
reason: "The managed resource must control its connection secret before it can be propagated",
fields: fields{
client: ClientApplicator{
// Simulate getting a secret that is not controlled by the
// managed resource by not modifying the secret passed to
// the client, and not returning an error. We thus proceed
// with our original empty secret, which has no controller
// reference.
Client: &test.MockClient{MockGet: test.NewMockGetFn(nil)},
},
},
args: args{
o: cm,
mg: mg,
},
want: errors.New(errSecretConflict),
},
"ApplyClaimSecretError": {
reason: "Errors applying the claim connection secret should be returned",
fields: fields{
client: ClientApplicator{
Client: &test.MockClient{MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
s := ConnectionSecretFor(mg, fake.GVK(mg))
*o.(*corev1.Secret) = *s
return nil
})},
Applicator: ApplyFn(func(_ context.Context, _ runtime.Object, _ ...ApplyOption) error { return errBoom }),
},
typer: fake.SchemeWith(mg, cm),
},
args: args{
o: cm,
mg: mg,
},
want: errors.Wrap(errBoom, errCreateOrUpdateSecret),
},
"UpdateManagedSecretError": {
reason: "Errors updating the managed resource connection secret should be returned",
fields: fields{
client: ClientApplicator{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
s := ConnectionSecretFor(mg, fake.GVK(mg))
*o.(*corev1.Secret) = *s
return nil
}),
MockUpdate: test.NewMockUpdateFn(errBoom),
},
Applicator: ApplyFn(func(_ context.Context, _ runtime.Object, _ ...ApplyOption) error { return nil }),
},
typer: fake.SchemeWith(mg, cm),
},
args: args{
o: cm,
mg: mg,
},
want: errors.Wrap(errBoom, errUpdateSecret),
},
"Successful": {
reason: "Successful propagation should update the claim and managed resource secrets with the appropriate values",
fields: fields{
client: ClientApplicator{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
// The managed secret has some data when we get it.
s := ConnectionSecretFor(mg, fake.GVK(mg))
s.Data = mgcsdata
*o.(*corev1.Secret) = *s
return nil
}),
MockUpdate: test.NewMockUpdateFn(nil, func(o runtime.Object) error {
// Ensure the managed secret is annotated to allow
// constant propagation to the claim secret.
want := ConnectionSecretFor(mg, fake.GVK(mg))
want.Data = mgcsdata
meta.AllowPropagation(want, LocalConnectionSecretFor(cm, fake.GVK(cm)))
if diff := cmp.Diff(want, o); diff != "" {
t.Errorf("-want, +got: %s", diff)
}
return nil
}),
},
Applicator: ApplyFn(func(_ context.Context, o runtime.Object, _ ...ApplyOption) error {
// Ensure the managed secret's data is copied to the
// claim secret, and that the claim secret is annotated
// to allow constant propagation from the managed
// secret.
want := LocalConnectionSecretFor(cm, fake.GVK(cm))
want.Data = mgcsdata
meta.AllowPropagation(ConnectionSecretFor(mg, fake.GVK(mg)), want)
if diff := cmp.Diff(want, o); diff != "" {
t.Errorf("-want, +got: %s", diff)
}
return nil
}),
},
typer: fake.SchemeWith(mg, cm),
},
args: args{
o: cm,
mg: mg,
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
api := &APIManagedConnectionPropagator{client: tc.fields.client, typer: tc.fields.typer}
err := api.PropagateConnection(tc.args.ctx, tc.args.o, tc.args.mg)
if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\napi.PropagateConnection(...): -want error, +got error:\n%s", tc.reason, diff)
}
})
}
}
func TestAPIPatchingApplicator(t *testing.T) {
errBoom := errors.New("boom")
named := &object{}
named.SetName("barry")
type args struct {
ctx context.Context
o runtime.Object
ao []ApplyOption
}
type want struct {
o runtime.Object
err error
}
cases := map[string]struct {
reason string
c client.Client
args args
want want
}{
"NotAMetadataObject": {
reason: "An error should be returned if we can't access the object's metadata",
c: &test.MockClient{MockGet: test.NewMockGetFn(errBoom)},
args: args{
o: &nopeject{},
},
want: want{
o: &nopeject{},
err: errors.New("cannot access object metadata"),
},
},
"GetError": {
reason: "An error should be returned if we can't get the object",
c: &test.MockClient{MockGet: test.NewMockGetFn(errBoom)},
args: args{
o: &object{},
},
want: want{
o: &object{},
err: errors.Wrap(errBoom, "cannot get object"),
},
},
"CreateError": {
reason: "No error should be returned if we successfully create a new object",
c: &test.MockClient{
MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, "")),
MockCreate: test.NewMockCreateFn(errBoom),
},
args: args{
o: &object{},
},
want: want{
o: &object{},
err: errors.Wrap(errBoom, "cannot create object"),
},
},
"ApplyOptionError": {
reason: "Any errors from an apply option should be returned",
c: &test.MockClient{MockGet: test.NewMockGetFn(nil)},
args: args{
o: &object{},
ao: []ApplyOption{func(_ context.Context, _, _ runtime.Object) error { return errBoom }},
},
want: want{
o: &object{},
err: errBoom,
},
},
"PatchError": {
reason: "An error should be returned if we can't patch the object",
c: &test.MockClient{
MockGet: test.NewMockGetFn(nil),
MockPatch: test.NewMockPatchFn(errBoom),
},
args: args{
o: &object{},
},
want: want{
o: &object{},
err: errors.Wrap(errBoom, "cannot patch object"),
},
},
"Created": {
reason: "No error should be returned if we successfully create a new object",
c: &test.MockClient{
MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, "")),
MockCreate: test.NewMockCreateFn(nil, func(o runtime.Object) error {
*o.(*object) = *named
return nil
}),
},
args: args{
o: &object{},
},
want: want{
o: named,
},
},
"Patched": {
reason: "No error should be returned if we successfully patch an existing object",
c: &test.MockClient{
MockGet: test.NewMockGetFn(nil),
MockPatch: test.NewMockPatchFn(nil, func(o runtime.Object) error {
*o.(*object) = *named
return nil
}),
},
args: args{
o: &object{},
},
want: want{
o: named,
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
a := NewAPIPatchingApplicator(tc.c)
err := a.Apply(tc.args.ctx, tc.args.o, tc.args.ao...)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nApply(...): -want error, +got error\n%s\n", tc.reason, diff)
}
if diff := cmp.Diff(tc.want.o, tc.args.o); diff != "" {
t.Errorf("\n%s\nApply(...): -want, +got\n%s\n", tc.reason, diff)
}
})
}
}
func TestAPIUpdatingApplicator(t *testing.T) {
errBoom := errors.New("boom")
named := &object{}
named.SetName("barry")
type args struct {
ctx context.Context
o runtime.Object
ao []ApplyOption
}
type want struct {
o runtime.Object
err error
}
cases := map[string]struct {
reason string
c client.Client
args args
want want
}{
"NotAMetadataObject": {
reason: "An error should be returned if we can't access the object's metadata",
c: &test.MockClient{MockGet: test.NewMockGetFn(errBoom)},
args: args{
o: &nopeject{},
},
want: want{
o: &nopeject{},
err: errors.New("cannot access object metadata"),
},
},
"GetError": {
reason: "An error should be returned if we can't get the object",
c: &test.MockClient{MockGet: test.NewMockGetFn(errBoom)},
args: args{
o: &object{},
},
want: want{
o: &object{},
err: errors.Wrap(errBoom, "cannot get object"),
},
},
"CreateError": {
reason: "No error should be returned if we successfully create a new object",
c: &test.MockClient{
MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, "")),
MockCreate: test.NewMockCreateFn(errBoom),
},
args: args{
o: &object{},
},
want: want{
o: &object{},
err: errors.Wrap(errBoom, "cannot create object"),
},
},
"ApplyOptionError": {
reason: "Any errors from an apply option should be returned",
c: &test.MockClient{MockGet: test.NewMockGetFn(nil)},
args: args{
o: &object{},
ao: []ApplyOption{func(_ context.Context, _, _ runtime.Object) error { return errBoom }},
},
want: want{
o: &object{},
err: errBoom,
},
},
"UpdateError": {
reason: "An error should be returned if we can't update the object",
c: &test.MockClient{
MockGet: test.NewMockGetFn(nil),
MockUpdate: test.NewMockUpdateFn(errBoom),
},
args: args{
o: &object{},
},
want: want{
o: &object{},
err: errors.Wrap(errBoom, "cannot update object"),
},
},
"Created": {
reason: "No error should be returned if we successfully create a new object",
c: &test.MockClient{
MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, "")),
MockCreate: test.NewMockCreateFn(nil, func(o runtime.Object) error {
*o.(*object) = *named
return nil
}),
},
args: args{
o: &object{},
},
want: want{
o: named,
},
},
"Updated": {
reason: "No error should be returned if we successfully update an existing object",
c: &test.MockClient{
MockGet: test.NewMockGetFn(nil),
MockUpdate: test.NewMockUpdateFn(nil, func(o runtime.Object) error {
*o.(*object) = *named
return nil
}),
},
args: args{
o: &object{},
},
want: want{
o: named,
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
a := NewAPIUpdatingApplicator(tc.c)
err := a.Apply(tc.args.ctx, tc.args.o, tc.args.ao...)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nApply(...): -want error, +got error\n%s\n", tc.reason, diff)
}
if diff := cmp.Diff(tc.want.o, tc.args.o); diff != "" {
t.Errorf("\n%s\nApply(...): -want, +got\n%s\n", tc.reason, diff)
}
})
}
}
func TestManagedRemoveFinalizer(t *testing.T) {
finalizer := "veryfinal"
type args struct {
ctx context.Context
obj Object
}
type want struct {
err error
obj Object
}
errBoom := errors.New("boom")
cases := map[string]struct {
client client.Client
args args
want want
}{
"UpdateError": {
client: &test.MockClient{MockUpdate: test.NewMockUpdateFn(errBoom)},
args: args{
ctx: context.Background(),
obj: &fake.Object{ObjectMeta: metav1.ObjectMeta{Finalizers: []string{finalizer}}},
},
want: want{
err: errors.Wrap(errBoom, errUpdateObject),
obj: &fake.Object{ObjectMeta: metav1.ObjectMeta{Finalizers: []string{}}},
},
},
"Successful": {
client: &test.MockClient{MockUpdate: test.NewMockUpdateFn(nil)},
args: args{
ctx: context.Background(),
obj: &fake.Object{ObjectMeta: metav1.ObjectMeta{Finalizers: []string{finalizer}}},
},
want: want{
err: nil,
obj: &fake.Object{ObjectMeta: metav1.ObjectMeta{Finalizers: []string{}}},
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
api := NewAPIFinalizer(tc.client, finalizer)
err := api.RemoveFinalizer(tc.args.ctx, tc.args.obj)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("api.RemoveFinalizer(...): -want error, +got error:\n%s", diff)
}
if diff := cmp.Diff(tc.want.obj, tc.args.obj, test.EquateConditions()); diff != "" {
t.Errorf("api.RemoveFinalizer(...) Managed: -want, +got:\n%s", diff)
}
})
}
}
func TestAPIFinalizerAdder(t *testing.T) {
finalizer := "veryfinal"
type args struct {
ctx context.Context
obj Object
}
type want struct {
err error
obj Object
}
errBoom := errors.New("boom")
cases := map[string]struct {
client client.Client
args args
want want
}{
"UpdateError": {
client: &test.MockClient{MockUpdate: test.NewMockUpdateFn(errBoom)},
args: args{
ctx: context.Background(),
obj: &fake.Object{ObjectMeta: metav1.ObjectMeta{Finalizers: []string{}}},
},
want: want{
err: errors.Wrap(errBoom, errUpdateObject),
obj: &fake.Object{ObjectMeta: metav1.ObjectMeta{Finalizers: []string{finalizer}}},
},
},
"Successful": {
client: &test.MockClient{MockUpdate: test.NewMockUpdateFn(nil)},
args: args{
ctx: context.Background(),
obj: &fake.Object{ObjectMeta: metav1.ObjectMeta{Finalizers: []string{}}},
},
want: want{
err: nil,
obj: &fake.Object{ObjectMeta: metav1.ObjectMeta{Finalizers: []string{finalizer}}},
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
api := NewAPIFinalizer(tc.client, finalizer)
err := api.AddFinalizer(tc.args.ctx, tc.args.obj)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("api.Initialize(...): -want error, +got error:\n%s", diff)
}
if diff := cmp.Diff(tc.want.obj, tc.args.obj, test.EquateConditions()); diff != "" {
t.Errorf("api.Initialize(...) Managed: -want, +got:\n%s", diff)
}
})
}
}