crossplane/pkg/controller/workload/kubernetes/resource/resource_test.go

1594 lines
48 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"
"encoding/json"
"fmt"
"net/url"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
kmeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
runtimev1alpha1 "github.com/crossplaneio/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplaneio/crossplane-runtime/pkg/test"
"github.com/crossplaneio/crossplane/apis/workload/v1alpha1"
)
const (
namespace = "coolNamespace"
name = "coolAR"
uid = types.UID("definitely-a-uuid")
resourceVersion = "coolVersion"
)
var (
errorBoom = errors.New("boom")
objectMeta = metav1.ObjectMeta{
Namespace: namespace,
Name: name,
UID: uid,
Finalizers: []string{},
}
ctx = context.Background()
target = &v1alpha1.KubernetesTarget{
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "coolTarget"},
Spec: v1alpha1.KubernetesTargetSpec{
ConnectionSecretRef: &runtimev1alpha1.LocalSecretReference{Name: secret.GetName()},
},
}
targetRef = &v1alpha1.KubernetesTargetReference{Name: target.GetName()}
apiServerURL, _ = url.Parse("https://example.org")
malformedURL = ":wat:"
secret = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "coolSecret",
Namespace: namespace,
Annotations: map[string]string{
RemoteControllerNamespace: objectMeta.GetNamespace(),
RemoteControllerName: objectMeta.GetName(),
RemoteControllerUID: string(objectMeta.GetUID()),
},
},
Data: map[string][]byte{
runtimev1alpha1.ResourceCredentialsSecretEndpointKey: []byte(apiServerURL.String()),
runtimev1alpha1.ResourceCredentialsSecretUserKey: []byte("user"),
runtimev1alpha1.ResourceCredentialsSecretPasswordKey: []byte("password"),
runtimev1alpha1.ResourceCredentialsSecretCAKey: []byte("secretCA"),
runtimev1alpha1.ResourceCredentialsSecretClientCertKey: []byte("clientCert"),
runtimev1alpha1.ResourceCredentialsSecretClientKeyKey: []byte("clientKey"),
runtimev1alpha1.ResourceCredentialsSecretTokenKey: []byte("token"),
},
}
existingSecret = func() *corev1.Secret {
s := secret.DeepCopy()
s.Data["extrafield"] = []byte("somuchmore!")
return s
}()
secretLocalObjectRef = corev1.LocalObjectReference{Name: secret.GetName()}
secretWithExplicitType = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "basicAuthSecret",
Namespace: namespace,
Annotations: map[string]string{
RemoteControllerNamespace: objectMeta.GetNamespace(),
RemoteControllerName: objectMeta.GetName(),
RemoteControllerUID: string(objectMeta.GetUID()),
},
},
Data: map[string][]byte{
runtimev1alpha1.ResourceCredentialsSecretUserKey: []byte("user"),
runtimev1alpha1.ResourceCredentialsSecretPasswordKey: []byte("password"),
},
Type: corev1.SecretTypeBasicAuth,
}
serviceWithoutNamespace = &corev1.Service{
// Note we purposefully omit the namespace here in order to test our
// namespace defaulting logic.
ObjectMeta: metav1.ObjectMeta{
Name: "coolService",
Annotations: map[string]string{
RemoteControllerNamespace: objectMeta.GetNamespace(),
RemoteControllerName: objectMeta.GetName(),
RemoteControllerUID: string(objectMeta.GetUID()),
},
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeLoadBalancer,
},
Status: corev1.ServiceStatus{
LoadBalancer: corev1.LoadBalancerStatus{
Ingress: []corev1.LoadBalancerIngress{
{Hostname: "coolservice.crossplane.io"},
},
},
},
}
service = func() *corev1.Service {
s := serviceWithoutNamespace.DeepCopy()
s.SetNamespace(namespace)
return s
}()
existingService = func() *corev1.Service {
s := service.DeepCopy()
s.Spec.Type = corev1.ServiceTypeClusterIP
return s
}()
remoteStatus = func() *v1alpha1.RemoteStatus {
raw, _ := json.Marshal(serviceWithoutNamespace.Status)
return &v1alpha1.RemoteStatus{Raw: json.RawMessage(raw)}
}()
deleteTime = time.Now()
)
func template(s *corev1.Service) *unstructured.Unstructured {
u := &unstructured.Unstructured{}
scheme := runtime.NewScheme()
_ = corev1.AddToScheme(scheme)
_ = scheme.Convert(s, u, nil)
return u
}
type kubeARModifier func(*v1alpha1.KubernetesApplicationResource)
func withFinalizers(f ...string) kubeARModifier {
return func(r *v1alpha1.KubernetesApplicationResource) { r.ObjectMeta.Finalizers = f }
}
func withConditions(c ...runtimev1alpha1.Condition) kubeARModifier {
return func(r *v1alpha1.KubernetesApplicationResource) { r.Status.SetConditions(c...) }
}
func withState(s v1alpha1.KubernetesApplicationResourceState) kubeARModifier {
return func(r *v1alpha1.KubernetesApplicationResource) { r.Status.State = s }
}
func withRemoteStatus(s *v1alpha1.RemoteStatus) kubeARModifier {
return func(r *v1alpha1.KubernetesApplicationResource) { r.Status.Remote = s }
}
func withDeletionTimestamp(t time.Time) kubeARModifier {
return func(r *v1alpha1.KubernetesApplicationResource) {
r.ObjectMeta.DeletionTimestamp = &metav1.Time{Time: t}
}
}
func withTarget(name string) kubeARModifier {
return func(r *v1alpha1.KubernetesApplicationResource) {
r.Status.Target = &v1alpha1.KubernetesTargetReference{Name: name}
}
}
func withSecrets(s ...corev1.LocalObjectReference) kubeARModifier {
return func(r *v1alpha1.KubernetesApplicationResource) {
r.Spec.Secrets = s
}
}
func withTemplate(t *unstructured.Unstructured) kubeARModifier {
return func(r *v1alpha1.KubernetesApplicationResource) {
r.Spec.Template = t
}
}
func kubeAR(rm ...kubeARModifier) *v1alpha1.KubernetesApplicationResource {
r := &v1alpha1.KubernetesApplicationResource{ObjectMeta: objectMeta}
for _, m := range rm {
m(r)
}
return r
}
func TestCreatePredicate(t *testing.T) {
cases := []struct {
name string
event event.CreateEvent
want bool
}{
{
name: "ScheduledCluster",
event: event.CreateEvent{
Object: &v1alpha1.KubernetesApplicationResource{
Status: v1alpha1.KubernetesApplicationResourceStatus{
Target: targetRef,
},
},
},
want: true,
},
{
name: "UnscheduledCluster",
event: event.CreateEvent{
Object: &v1alpha1.KubernetesApplicationResource{},
},
want: false,
},
{
name: "NotAKubernetesApplicationResource",
event: event.CreateEvent{
Object: &v1alpha1.KubernetesApplication{},
},
want: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := CreatePredicate(tc.event)
if got != tc.want {
t.Errorf("CreatePredicate(...): got %v, want %v", got, tc.want)
}
})
}
}
func TestUpdatePredicate(t *testing.T) {
cases := []struct {
name string
event event.UpdateEvent
want bool
}{
{
name: "ScheduledCluster",
event: event.UpdateEvent{
ObjectNew: &v1alpha1.KubernetesApplicationResource{
Status: v1alpha1.KubernetesApplicationResourceStatus{
Target: targetRef,
},
},
},
want: true,
},
{
name: "UnscheduledCluster",
event: event.UpdateEvent{
ObjectNew: &v1alpha1.KubernetesApplicationResource{},
},
want: false,
},
{
name: "NotAKubernetesApplicationResource",
event: event.UpdateEvent{
ObjectNew: &v1alpha1.KubernetesApplication{},
},
want: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := UpdatePredicate(tc.event)
if got != tc.want {
t.Errorf("UpdatePredicate(...): got %v, want %v", got, tc.want)
}
})
}
}
type mockSyncUnstructuredFn func(ctx context.Context, template *unstructured.Unstructured) (*v1alpha1.RemoteStatus, error)
func newMockSyncUnstructuredFn(s *v1alpha1.RemoteStatus, err error) mockSyncUnstructuredFn {
return func(_ context.Context, _ *unstructured.Unstructured) (*v1alpha1.RemoteStatus, error) {
return s, err
}
}
type mockDeleteUnstructuredFn func(ctx context.Context, template *unstructured.Unstructured) error
func newMockDeleteUnstructuredFn(err error) mockDeleteUnstructuredFn {
return func(_ context.Context, _ *unstructured.Unstructured) error {
return err
}
}
type mockUnstructuredClient struct {
mockSync mockSyncUnstructuredFn
mockDelete mockDeleteUnstructuredFn
}
func (m *mockUnstructuredClient) sync(ctx context.Context, template *unstructured.Unstructured) (*v1alpha1.RemoteStatus, error) {
return m.mockSync(ctx, template)
}
func (m *mockUnstructuredClient) delete(ctx context.Context, template *unstructured.Unstructured) error {
return m.mockDelete(ctx, template)
}
type mockSyncSecretFn func(ctx context.Context, template *corev1.Secret) error
func newMockSyncSecretFn(err error) mockSyncSecretFn {
return func(ctx context.Context, template *corev1.Secret) error { return err }
}
type mockDeleteSecretFn func(ctx context.Context, template *corev1.Secret) error
func newMockDeleteSecretFn(err error) mockDeleteSecretFn {
return func(ctx context.Context, template *corev1.Secret) error { return err }
}
type mockSecretClient struct {
mockSync mockSyncSecretFn
mockDelete mockDeleteSecretFn
}
func (m *mockSecretClient) sync(ctx context.Context, template *corev1.Secret) error {
return m.mockSync(ctx, template)
}
func (m *mockSecretClient) delete(ctx context.Context, template *corev1.Secret) error {
return m.mockDelete(ctx, template)
}
func TestSync(t *testing.T) {
cases := []struct {
name string
syncer syncer
ar *v1alpha1.KubernetesApplicationResource
secrets []corev1.Secret
wantAR *v1alpha1.KubernetesApplicationResource
wantResult reconcile.Result
}{
{
name: "Successful",
syncer: &remoteCluster{
unstructured: &mockUnstructuredClient{
mockSync: func(_ context.Context, got *unstructured.Unstructured) (*v1alpha1.RemoteStatus, error) {
want := template(serviceWithoutNamespace)
want.SetNamespace(corev1.NamespaceDefault)
want.SetAnnotations(map[string]string{
RemoteControllerNamespace: objectMeta.GetNamespace(),
RemoteControllerName: objectMeta.GetName(),
RemoteControllerUID: string(objectMeta.GetUID()),
})
if diff := cmp.Diff(want, got); diff != "" {
return nil, errors.Errorf("mockSync: -want, +got: %s", diff)
}
return remoteStatus, nil
},
},
secret: &mockSecretClient{
mockSync: func(_ context.Context, got *corev1.Secret) error {
want := secret.DeepCopy()
want.SetName(fmt.Sprintf("%s-%s", objectMeta.GetName(), secret.GetName()))
want.SetNamespace(corev1.NamespaceDefault)
want.SetAnnotations(map[string]string{
RemoteControllerNamespace: objectMeta.GetNamespace(),
RemoteControllerName: objectMeta.GetName(),
RemoteControllerUID: string(objectMeta.GetUID()),
})
if diff := cmp.Diff(want, got); diff != "" {
return errors.Errorf("mockSync: -want, +got: %s", diff)
}
return nil
},
},
},
ar: kubeAR(withTemplate(template(serviceWithoutNamespace))),
secrets: []corev1.Secret{*secret},
wantAR: kubeAR(
withTemplate(template(serviceWithoutNamespace)),
withFinalizers(finalizerName),
withConditions(runtimev1alpha1.ReconcileSuccess()),
withState(v1alpha1.KubernetesApplicationResourceStateSubmitted),
withRemoteStatus(remoteStatus),
),
wantResult: reconcile.Result{RequeueAfter: aLongWait},
},
{
name: "MissingTemplate",
syncer: &remoteCluster{},
ar: kubeAR(),
wantAR: kubeAR(
withFinalizers(finalizerName),
withConditions(runtimev1alpha1.ReconcileError(errMissingTemplate)),
withState(v1alpha1.KubernetesApplicationResourceStateFailed),
),
wantResult: reconcile.Result{Requeue: true},
},
{
name: "SecretSyncPreservesType",
syncer: &remoteCluster{
unstructured: &mockUnstructuredClient{
mockSync: func(_ context.Context, got *unstructured.Unstructured) (*v1alpha1.RemoteStatus, error) {
return remoteStatus, nil
},
},
secret: &mockSecretClient{
mockSync: func(_ context.Context, got *corev1.Secret) error {
want := secretWithExplicitType.DeepCopy()
want.SetName(fmt.Sprintf("%s-%s", objectMeta.GetName(), secretWithExplicitType.GetName()))
want.SetNamespace(corev1.NamespaceDefault)
want.SetAnnotations(map[string]string{
RemoteControllerNamespace: objectMeta.GetNamespace(),
RemoteControllerName: objectMeta.GetName(),
RemoteControllerUID: string(objectMeta.GetUID()),
})
if diff := cmp.Diff(want, got); diff != "" {
return errors.Errorf("mockSync: -want, +got: %s", diff)
}
return nil
},
},
},
ar: kubeAR(withTemplate(template(serviceWithoutNamespace))),
secrets: []corev1.Secret{*secretWithExplicitType},
wantAR: kubeAR(
withTemplate(template(serviceWithoutNamespace)),
withFinalizers(finalizerName),
withConditions(runtimev1alpha1.ReconcileSuccess()),
withState(v1alpha1.KubernetesApplicationResourceStateSubmitted),
withRemoteStatus(remoteStatus),
),
wantResult: reconcile.Result{RequeueAfter: aLongWait},
},
{
name: "SecretSyncFailed",
syncer: &remoteCluster{
secret: &mockSecretClient{mockSync: newMockSyncSecretFn(errorBoom)},
},
ar: kubeAR(withTemplate(template(serviceWithoutNamespace))),
secrets: []corev1.Secret{*secret},
wantAR: kubeAR(
withTemplate(template(serviceWithoutNamespace)),
withFinalizers(finalizerName),
withConditions(runtimev1alpha1.ReconcileError(errorBoom)),
withState(v1alpha1.KubernetesApplicationResourceStateFailed),
),
wantResult: reconcile.Result{Requeue: true},
},
{
name: "ResourceSyncFailed",
syncer: &remoteCluster{
unstructured: &mockUnstructuredClient{mockSync: newMockSyncUnstructuredFn(nil, errorBoom)},
},
ar: kubeAR(
withTemplate(template(serviceWithoutNamespace)),
withFinalizers(finalizerName),
withRemoteStatus(remoteStatus),
),
wantAR: kubeAR(
withTemplate(template(serviceWithoutNamespace)),
withFinalizers(finalizerName),
withRemoteStatus(remoteStatus),
withConditions(runtimev1alpha1.ReconcileError(errorBoom)),
withState(v1alpha1.KubernetesApplicationResourceStateFailed),
),
wantResult: reconcile.Result{Requeue: true},
},
{
name: "ResourceSyncRefreshedStatusThenFailed",
syncer: &remoteCluster{
unstructured: &mockUnstructuredClient{mockSync: newMockSyncUnstructuredFn(remoteStatus, errorBoom)},
},
ar: kubeAR(withTemplate(template(serviceWithoutNamespace))),
wantAR: kubeAR(
withTemplate(template(serviceWithoutNamespace)),
withFinalizers(finalizerName),
withConditions(runtimev1alpha1.ReconcileError(errorBoom)),
withState(v1alpha1.KubernetesApplicationResourceStateFailed),
withRemoteStatus(remoteStatus),
),
wantResult: reconcile.Result{Requeue: true},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
gotResult := tc.syncer.sync(ctx, tc.ar, tc.secrets)
if diff := cmp.Diff(tc.wantResult, gotResult); diff != "" {
t.Errorf("tc.syncer.sync(...): -want, +got:\n%s", diff)
}
if diff := cmp.Diff(tc.wantAR, tc.ar); diff != "" {
t.Errorf("app: -want, +got:\n%s", diff)
}
})
}
}
func TestDelete(t *testing.T) {
cases := []struct {
name string
deleter deleter
ar *v1alpha1.KubernetesApplicationResource
secrets []corev1.Secret
wantAR *v1alpha1.KubernetesApplicationResource
wantResult reconcile.Result
}{
{
name: "Successful",
deleter: &remoteCluster{
unstructured: &mockUnstructuredClient{
mockDelete: func(_ context.Context, got *unstructured.Unstructured) error {
want := template(service)
want.SetAnnotations(map[string]string{
RemoteControllerNamespace: objectMeta.GetNamespace(),
RemoteControllerName: objectMeta.GetName(),
RemoteControllerUID: string(objectMeta.GetUID()),
})
if diff := cmp.Diff(want, got); diff != "" {
errors.Errorf("unstructured mockDelete: -want, +got: %s", diff)
}
return nil
},
},
secret: &mockSecretClient{
mockDelete: func(_ context.Context, got *corev1.Secret) error {
want := secret.DeepCopy()
want.SetName(fmt.Sprintf("%s-%s", objectMeta.GetName(), secret.GetName()))
want.SetNamespace(service.GetNamespace())
want.SetAnnotations(map[string]string{
RemoteControllerNamespace: objectMeta.GetNamespace(),
RemoteControllerName: objectMeta.GetName(),
RemoteControllerUID: string(objectMeta.GetUID()),
})
if diff := cmp.Diff(want, got); diff != "" {
return errors.Errorf("secret mockDelete: -want, +got: %s", diff)
}
return nil
},
},
},
ar: kubeAR(
withFinalizers(finalizerName),
withTemplate(template(service)),
),
secrets: []corev1.Secret{*secret},
wantAR: kubeAR(
withConditions(runtimev1alpha1.ReconcileSuccess()),
withTemplate(template(service)),
),
wantResult: reconcile.Result{Requeue: false},
},
{
name: "MissingTemplate",
deleter: &remoteCluster{},
ar: kubeAR(
withFinalizers(finalizerName),
),
wantAR: kubeAR(
withFinalizers(finalizerName),
withConditions(runtimev1alpha1.ReconcileError(errMissingTemplate)),
withState(v1alpha1.KubernetesApplicationResourceStateFailed),
),
wantResult: reconcile.Result{Requeue: true},
},
{
name: "SecretDeleteFailed",
deleter: &remoteCluster{
unstructured: &mockUnstructuredClient{mockDelete: newMockDeleteUnstructuredFn(nil)},
secret: &mockSecretClient{mockDelete: newMockDeleteSecretFn(errorBoom)},
},
ar: kubeAR(
withFinalizers(finalizerName),
withTemplate(template(serviceWithoutNamespace)),
),
secrets: []corev1.Secret{*secret},
wantAR: kubeAR(
withFinalizers(finalizerName),
withTemplate(template(serviceWithoutNamespace)),
withConditions(runtimev1alpha1.ReconcileError(errorBoom)),
withState(v1alpha1.KubernetesApplicationResourceStateFailed),
),
wantResult: reconcile.Result{Requeue: true},
},
{
name: "ResourceDeleteFailed",
deleter: &remoteCluster{
unstructured: &mockUnstructuredClient{mockDelete: newMockDeleteUnstructuredFn(errorBoom)},
},
ar: kubeAR(
withFinalizers(finalizerName),
withTemplate(template(serviceWithoutNamespace)),
),
wantAR: kubeAR(
withFinalizers(finalizerName),
withTemplate(template(serviceWithoutNamespace)),
withConditions(runtimev1alpha1.ReconcileError(errorBoom)),
withState(v1alpha1.KubernetesApplicationResourceStateFailed),
),
wantResult: reconcile.Result{Requeue: true},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
gotResult := tc.deleter.delete(ctx, tc.ar, tc.secrets)
if diff := cmp.Diff(tc.wantResult, gotResult); diff != "" {
t.Errorf("tc.deleter.delete(...): -want, +got:\n%s", diff)
}
if diff := cmp.Diff(tc.wantAR, tc.ar); diff != "" {
t.Errorf("AR: -want, +got:\n%s", diff)
}
})
}
}
func TestSyncUnstructured(t *testing.T) {
cases := []struct {
name string
unstructured unstructuredSyncer
template *unstructured.Unstructured
wantStatus *v1alpha1.RemoteStatus
wantErr error
}{
{
name: "Successful",
unstructured: &unstructuredClient{
kube: &test.MockClient{
MockGet: func(_ context.Context, _ client.ObjectKey, obj runtime.Object) error {
// The existing service is slightly different from the
// updated service because CreateOrUpdate does not call
// Update if the object did not change.
existing := template(existingService)
existing.SetResourceVersion(resourceVersion)
*obj.(*unstructured.Unstructured) = *existing
return nil
},
MockPatch: func(_ context.Context, obj runtime.Object, patch client.Patch, _ ...client.PatchOption) error {
// We compare resource versions to ensure we preserved
// the existing service's important object metadata.
want := resourceVersion
got := obj.(*unstructured.Unstructured).GetResourceVersion()
if got != want {
return errors.Errorf("MockUpdate: obj.GetResourceVersion(): want %s, got %s", want, got)
}
return nil
},
},
},
template: template(service),
wantStatus: remoteStatus,
wantErr: nil,
},
{
name: "ExistingResourceHasDifferentController",
unstructured: &unstructuredClient{
kube: &test.MockClient{
MockGet: func(_ context.Context, _ client.ObjectKey, obj runtime.Object) error {
existing := template(existingService)
existing.SetAnnotations(map[string]string{})
*obj.(*unstructured.Unstructured) = *existing
return nil
},
},
},
template: template(service),
wantStatus: nil,
wantErr: errors.WithStack(errors.Errorf("cannot sync resource: Service %s/%s exists and is not controlled by %s %s",
existingService.GetNamespace(),
existingService.GetName(),
v1alpha1.KubernetesApplicationResourceKind,
objectMeta.GetName(),
)),
},
{
name: "CreateSuccessful",
unstructured: &unstructuredClient{
kube: &test.MockClient{
MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, name)),
MockCreate: func(_ context.Context, obj runtime.Object, _ ...client.CreateOption) error {
if diff := cmp.Diff(template(service), obj); diff != "" {
t.Errorf("Create: -want, +got:\n%s", diff)
}
return nil
},
},
},
template: template(service),
wantStatus: nil,
wantErr: nil,
},
{
name: "CreateFailed",
unstructured: &unstructuredClient{
kube: &test.MockClient{
MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, name)),
MockCreate: test.NewMockCreateFn(errorBoom),
},
},
template: template(service),
wantStatus: nil,
wantErr: errors.Wrap(errorBoom, "cannot create resource"),
},
{
name: "GetFailed",
unstructured: &unstructuredClient{
kube: &test.MockClient{MockGet: test.NewMockGetFn(errorBoom)},
},
template: template(service),
wantStatus: nil,
wantErr: errors.Wrap(errorBoom, "cannot get resource"),
},
{
name: "PatchFailed",
unstructured: &unstructuredClient{
kube: &test.MockClient{
MockGet: test.NewMockGetFn(nil),
MockPatch: test.NewMockPatchFn(errorBoom),
},
},
template: template(service),
wantStatus: remoteStatus,
wantErr: errors.Wrap(errorBoom, "cannot patch resource"),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
gotStatus, gotErr := tc.unstructured.sync(ctx, tc.template)
if diff := cmp.Diff(tc.wantErr, gotErr, test.EquateErrors()); diff != "" {
t.Errorf("tc.unstructured.sync(...): want error != got error:\n%s", diff)
}
if diff := cmp.Diff(tc.wantStatus, gotStatus); diff != "" {
t.Errorf("tc.unstructured.sync(...): -want, +got:\n%s", diff)
}
})
}
}
func TestGetRemoteStatus(t *testing.T) {
cases := []struct {
name string
remote runtime.Unstructured
want *v1alpha1.RemoteStatus
}{
{
name: "Successful",
remote: template(service),
want: remoteStatus,
},
{
name: "MissingStatus",
remote: &unstructured.Unstructured{},
want: nil,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := getRemoteStatus(tc.remote)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("getRemoteStatus(...): -want, +got:\n%s", diff)
}
})
}
}
func TestDeleteUnstructured(t *testing.T) {
cases := []struct {
name string
unstructured unstructuredDeleter
template *unstructured.Unstructured
wantErr error
}{
{
name: "Successful",
unstructured: &unstructuredClient{
kube: &test.MockClient{
MockGet: func(_ context.Context, _ client.ObjectKey, obj runtime.Object) error {
*obj.(*unstructured.Unstructured) = *(template(existingService))
return nil
},
MockDelete: test.NewMockDeleteFn(nil),
},
},
template: template(service),
wantErr: nil,
},
{
name: "ExistingResourceNotFound",
unstructured: &unstructuredClient{
kube: &test.MockClient{
MockGet: func(_ context.Context, key client.ObjectKey, obj runtime.Object) error {
return kerrors.NewNotFound(schema.GroupResource{}, key.Name)
},
},
},
template: template(service),
},
{
name: "ExistingResourceHasNoRemoteController",
unstructured: &unstructuredClient{
kube: &test.MockClient{
MockGet: func(_ context.Context, _ client.ObjectKey, obj runtime.Object) error {
existing := template(existingService)
existing.SetAnnotations(map[string]string{})
*obj.(*unstructured.Unstructured) = *existing
return nil
},
},
},
template: template(service),
},
{
name: "GetExistingResourceFailed",
unstructured: &unstructuredClient{
kube: &test.MockClient{MockGet: test.NewMockGetFn(errorBoom)},
},
template: template(service),
wantErr: errors.Wrapf(errorBoom, "cannot get resource %s/%s", service.GetNamespace(), service.GetName()),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
gotErr := tc.unstructured.delete(ctx, tc.template)
if diff := cmp.Diff(tc.wantErr, gotErr, test.EquateErrors()); diff != "" {
t.Errorf("tc.unstructured.delete(...): want error != got error:\n%s", diff)
}
})
}
}
func TestSyncSecret(t *testing.T) {
cases := []struct {
name string
secret secretSyncer
template *corev1.Secret
wantErr error
}{
{
name: "Successful",
secret: &secretClient{
kube: &test.MockClient{
MockGet: func(_ context.Context, _ client.ObjectKey, obj runtime.Object) error {
// The existing service is slightly different from the
// updated service because CreateOrUpdate does not call
// Update if the object did not change.
existing := existingSecret.DeepCopy()
existing.SetResourceVersion(resourceVersion)
*obj.(*corev1.Secret) = *existing
return nil
},
MockUpdate: func(_ context.Context, obj runtime.Object, _ ...client.UpdateOption) error {
// We compare resource versions to ensure we preserved
// the existing service's important object metadata.
want := resourceVersion
got := obj.(*corev1.Secret).GetResourceVersion()
if got != want {
return errors.Errorf("MockUpdate: obj.GetResourceVersion(): want %s, got %s", want, got)
}
return nil
},
},
},
template: secret,
wantErr: nil,
},
{
name: "ExistingResourceHasDifferentController",
secret: &secretClient{
kube: &test.MockClient{
MockGet: func(_ context.Context, _ client.ObjectKey, obj runtime.Object) error {
existing := existingSecret.DeepCopy()
existing.SetAnnotations(map[string]string{})
*obj.(*corev1.Secret) = *existing
return nil
},
},
},
template: secret,
wantErr: errors.WithStack(errors.Errorf("cannot sync secret: secret %s/%s exists and is not controlled by %s %s",
existingSecret.GetNamespace(),
existingSecret.GetName(),
v1alpha1.KubernetesApplicationResourceKind,
objectMeta.GetName(),
)),
},
{
name: "CreateOrUpdateFailed",
secret: &secretClient{
kube: &test.MockClient{
MockGet: test.NewMockGetFn(errorBoom),
},
},
template: secret,
wantErr: errors.Wrap(errorBoom, "cannot sync secret"),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
gotErr := tc.secret.sync(ctx, tc.template)
if diff := cmp.Diff(tc.wantErr, gotErr, test.EquateErrors()); diff != "" {
t.Errorf("tc.unstructured.sync(...): want error != got error:\n%s", diff)
}
})
}
}
func TestDeleteSecret(t *testing.T) {
cases := []struct {
name string
secret secretDeleter
template *corev1.Secret
wantErr error
}{
{
name: "Successful",
secret: &secretClient{
kube: &test.MockClient{
MockGet: func(_ context.Context, _ client.ObjectKey, obj runtime.Object) error {
*obj.(*corev1.Secret) = *existingSecret
return nil
},
MockDelete: test.NewMockDeleteFn(nil),
},
},
template: secret,
wantErr: nil,
},
{
name: "ExistingResourceNotFound",
secret: &secretClient{
kube: &test.MockClient{
MockGet: func(_ context.Context, key client.ObjectKey, obj runtime.Object) error {
return kerrors.NewNotFound(schema.GroupResource{}, key.Name)
},
},
},
template: secret,
},
{
name: "ExistingResourceHasNoRemoteController",
secret: &secretClient{
kube: &test.MockClient{
MockGet: func(_ context.Context, _ client.ObjectKey, obj runtime.Object) error {
existing := existingSecret
existing.SetAnnotations(map[string]string{})
*obj.(*corev1.Secret) = *existing
return nil
},
},
},
template: secret,
},
{
name: "GetExistingResourceFailed",
secret: &secretClient{
kube: &test.MockClient{MockGet: test.NewMockGetFn(errorBoom)},
},
template: secret,
wantErr: errors.Wrapf(errorBoom, "cannot get secret %s/%s", secret.GetNamespace(), secret.GetName()),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
gotErr := tc.secret.delete(ctx, tc.template)
if diff := cmp.Diff(tc.wantErr, gotErr, test.EquateErrors()); diff != "" {
t.Errorf("tc.secret.delete(...): want error != got error:\n%s", diff)
}
})
}
}
// We pass in a mock RESTMapper when testing in order to prevent
// client.New() trying to create one itself, because creating a new
// RESTMapper involves connecting to the API server.
type mockRESTMapper struct {
kmeta.RESTMapper
}
func TestConnectConfig(t *testing.T) {
cases := []struct {
name string
connecter *clusterConnecter
ar *v1alpha1.KubernetesApplicationResource
wantConfig *rest.Config
wantErr error
}{
{
name: "Successful",
connecter: &clusterConnecter{
kube: &test.MockClient{
MockGet: func(_ context.Context, got client.ObjectKey, obj runtime.Object) error {
switch actual := obj.(type) {
case *v1alpha1.KubernetesTarget:
want := client.ObjectKey{
Namespace: target.GetNamespace(),
Name: target.GetName(),
}
if diff := cmp.Diff(want, got); diff != "" {
return errors.Errorf("MockGet(Secret): -want, +got: %s", diff)
}
*actual = *target
case *corev1.Secret:
want := client.ObjectKey{
Namespace: target.GetNamespace(),
Name: secret.GetName(),
}
if diff := cmp.Diff(want, got); diff != "" {
return errors.Errorf("MockGet(Secret): -want, +got: %s", diff)
}
*actual = *secret
}
return nil
},
},
options: client.Options{Mapper: mockRESTMapper{}},
},
ar: kubeAR(withTarget(target.GetName())),
wantConfig: &rest.Config{
Host: apiServerURL.String(),
Username: string(secret.Data[runtimev1alpha1.ResourceCredentialsSecretUserKey]),
Password: string(secret.Data[runtimev1alpha1.ResourceCredentialsSecretPasswordKey]),
TLSClientConfig: rest.TLSClientConfig{
ServerName: apiServerURL.Hostname(),
CAData: secret.Data[runtimev1alpha1.ResourceCredentialsSecretCAKey],
CertData: secret.Data[runtimev1alpha1.ResourceCredentialsSecretClientCertKey],
KeyData: secret.Data[runtimev1alpha1.ResourceCredentialsSecretClientKeyKey],
},
BearerToken: string(secret.Data[runtimev1alpha1.ResourceCredentialsSecretTokenKey]),
},
wantErr: nil,
},
{
name: "KubernetesApplicationResourceNotScheduled",
connecter: &clusterConnecter{},
ar: kubeAR(),
wantErr: errors.Errorf("%s %s/%s is not scheduled",
v1alpha1.KubernetesApplicationResourceKind, objectMeta.GetNamespace(), objectMeta.GetName()),
},
{
name: "GetKubernetesTargetFailed",
connecter: &clusterConnecter{
kube: &test.MockClient{MockGet: test.NewMockGetFn(errorBoom)},
options: client.Options{Mapper: mockRESTMapper{}},
},
ar: kubeAR(withTarget(target.GetName())),
wantErr: errors.Wrapf(errorBoom, "cannot get %s %s/%s",
v1alpha1.KubernetesTargetKind, target.GetNamespace(), target.GetName()),
},
{
name: "GetConnectionSecretFailed",
connecter: &clusterConnecter{
kube: &test.MockClient{
MockGet: func(_ context.Context, _ client.ObjectKey, obj runtime.Object) error {
switch actual := obj.(type) {
case *v1alpha1.KubernetesTarget:
*actual = *target
case *corev1.Secret:
return errorBoom
}
return nil
},
},
options: client.Options{Mapper: mockRESTMapper{}},
},
ar: kubeAR(withTarget(target.GetName())),
wantErr: errors.Wrapf(errorBoom, "cannot get secret %s/%s", secret.GetNamespace(), secret.GetName()),
},
{
name: "ParseEndpointFailed",
connecter: &clusterConnecter{
kube: &test.MockClient{
MockGet: func(_ context.Context, _ client.ObjectKey, obj runtime.Object) error {
switch actual := obj.(type) {
case *v1alpha1.KubernetesTarget:
*actual = *target
case *corev1.Secret:
s := secret.DeepCopy()
s.Data[runtimev1alpha1.ResourceCredentialsSecretEndpointKey] = []byte(malformedURL)
*actual = *s
}
return nil
},
},
options: client.Options{Mapper: mockRESTMapper{}},
},
ar: kubeAR(withTarget(target.GetName())),
wantErr: errors.WithStack(errors.Errorf("cannot parse Kubernetes endpoint as URL: parse %s: missing protocol scheme", malformedURL)),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
gotConfig, gotErr := tc.connecter.config(ctx, tc.ar)
if diff := cmp.Diff(tc.wantErr, gotErr, test.EquateErrors()); diff != "" {
t.Errorf("tc.connecter.config(...): want error != got error:\n%s", diff)
}
if diff := cmp.Diff(tc.wantConfig, gotConfig); diff != "" {
t.Errorf("tc.connecter.config(...): -want, +got:\n%s", diff)
}
})
}
}
func TestConnect(t *testing.T) {
cases := []struct {
name string
connecter connecter
ar *v1alpha1.KubernetesApplicationResource
wantSD syncdeleter
wantErr error
}{
{
name: "Successful",
connecter: &clusterConnecter{
kube: &test.MockClient{
MockGet: func(_ context.Context, _ client.ObjectKey, obj runtime.Object) error {
if actual, ok := obj.(*v1alpha1.KubernetesTarget); ok {
*actual = *target
}
return nil
},
},
options: client.Options{Mapper: mockRESTMapper{}},
},
ar: kubeAR(withTarget(target.GetName())),
// This empty struct is 'identical' to the actual, populated struct
// returned by tc.connecter.connect() because we do not compare
// unexported fields. We don't inspect these unexported fields
// because doing so would mostly be testing controller-runtime's
// client.New() code, not ours.
wantSD: &remoteCluster{},
wantErr: nil,
},
{
name: "MissingConnectionSecret",
connecter: &clusterConnecter{
kube: &test.MockClient{MockGet: test.NewMockGetFn(nil)},
options: client.Options{Mapper: mockRESTMapper{}},
},
ar: kubeAR(withTarget(target.GetName())),
// This empty struct is 'identical' to the actual, populated struct
// returned by tc.connecter.connect() because we do not compare
// unexported fields. We don't inspect these unexported fields
// because doing so would mostly be testing controller-runtime's
// client.New() code, not ours.
wantErr: errors.Wrap(
errors.Errorf("%s %s/%s has no connection secret", v1alpha1.KubernetesTargetKind, target.GetNamespace(), target.GetName()),
"cannot create Kubernetes client configuration"),
},
{
name: "ConfigFailure",
connecter: &clusterConnecter{
kube: &test.MockClient{MockGet: test.NewMockGetFn(errorBoom)},
},
ar: kubeAR(withTarget(target.GetName())),
wantErr: errors.Wrapf(errorBoom, "cannot create Kubernetes client configuration: cannot get %s %s/%s",
v1alpha1.KubernetesTargetKind, target.GetNamespace(), target.GetName()),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
gotSD, gotErr := tc.connecter.connect(ctx, tc.ar)
if diff := cmp.Diff(tc.wantErr, gotErr, test.EquateErrors()); diff != "" {
t.Errorf("tc.connecter.connect(...): want error != got error:\n%s", diff)
}
if diff := cmp.Diff(tc.wantSD, gotSD, cmpopts.IgnoreUnexported(remoteCluster{})); diff != "" {
t.Errorf("tc.connecter.connect(...): -want, +got:\n%s", diff)
}
})
}
}
type mockSyncFn func(ctx context.Context, ar *v1alpha1.KubernetesApplicationResource, secrets []corev1.Secret) reconcile.Result
func newMockSyncFn(r reconcile.Result) mockSyncFn {
return func(ctx context.Context, ar *v1alpha1.KubernetesApplicationResource, secrets []corev1.Secret) reconcile.Result {
return r
}
}
type mockDeleteFn func(ctx context.Context, ar *v1alpha1.KubernetesApplicationResource, secrets []corev1.Secret) reconcile.Result
func newMockDeleteFn(r reconcile.Result) mockDeleteFn {
return func(ctx context.Context, ar *v1alpha1.KubernetesApplicationResource, secrets []corev1.Secret) reconcile.Result {
return r
}
}
type mockSyncDeleter struct {
mockSync mockSyncFn
mockDelete mockDeleteFn
}
func (m *mockSyncDeleter) sync(ctx context.Context, ar *v1alpha1.KubernetesApplicationResource, secrets []corev1.Secret) reconcile.Result {
return m.mockSync(ctx, ar, secrets)
}
func (m *mockSyncDeleter) delete(ctx context.Context, ar *v1alpha1.KubernetesApplicationResource, secrets []corev1.Secret) reconcile.Result {
return m.mockDelete(ctx, ar, secrets)
}
var noopSyncDeleter = &mockSyncDeleter{
mockSync: newMockSyncFn(reconcile.Result{Requeue: false}),
mockDelete: newMockDeleteFn(reconcile.Result{Requeue: false}),
}
type mockConnectFn func(ctx context.Context, ar *v1alpha1.KubernetesApplicationResource) (syncdeleter, error)
func newMockConnectFn(sd syncdeleter, err error) mockConnectFn {
return func(ctx context.Context, ar *v1alpha1.KubernetesApplicationResource) (syncdeleter, error) {
return sd, err
}
}
type mockConnecter struct {
mockConnect mockConnectFn
}
func (m *mockConnecter) connect(ctx context.Context, ar *v1alpha1.KubernetesApplicationResource) (syncdeleter, error) {
return m.mockConnect(ctx, ar)
}
func TestReconcile(t *testing.T) {
cases := []struct {
name string
rec *Reconciler
req reconcile.Request
wantResult reconcile.Result
wantErr error
}{
{
name: "FailedToGetNonExistentKAR",
rec: &Reconciler{
kube: &test.MockClient{
MockGet: func(_ context.Context, key client.ObjectKey, _ runtime.Object) error {
return kerrors.NewNotFound(schema.GroupResource{}, key.Name)
},
},
},
req: reconcile.Request{NamespacedName: types.NamespacedName{Namespace: namespace, Name: name}},
wantResult: reconcile.Result{Requeue: false},
wantErr: nil,
},
{
name: "FailedToGetExtantKAR",
rec: &Reconciler{
kube: &test.MockClient{MockGet: test.NewMockGetFn(errorBoom)},
},
req: reconcile.Request{NamespacedName: types.NamespacedName{Namespace: namespace, Name: name}},
wantResult: reconcile.Result{Requeue: false},
wantErr: errors.Wrapf(errorBoom, "cannot get %s %s/%s", v1alpha1.KubernetesApplicationResourceKind, namespace, name),
},
{
name: "FailedToConnect",
rec: &Reconciler{
connecter: &mockConnecter{mockConnect: newMockConnectFn(nil, errorBoom)},
kube: &test.MockClient{
MockGet: func(_ context.Context, _ client.ObjectKey, obj runtime.Object) error {
*obj.(*v1alpha1.KubernetesApplicationResource) = *(kubeAR())
return nil
},
MockUpdate: func(_ context.Context, obj runtime.Object, _ ...client.UpdateOption) error {
got := obj.(*v1alpha1.KubernetesApplicationResource)
want := kubeAR(
withConditions(runtimev1alpha1.ReconcileError(errorBoom)),
)
if diff := cmp.Diff(want, got); diff != "" {
return errors.Errorf("MockUpdate: -want, +got: %s", diff)
}
return nil
},
},
},
req: reconcile.Request{NamespacedName: types.NamespacedName{Namespace: namespace, Name: name}},
wantResult: reconcile.Result{Requeue: true},
},
{
name: "KARDeletedButCannotConnect",
rec: &Reconciler{
connecter: &mockConnecter{mockConnect: newMockConnectFn(nil, errors.Wrap(kerrors.NewNotFound(schema.GroupResource{}, ""), ""))},
kube: &test.MockClient{
MockGet: func(_ context.Context, _ client.ObjectKey, obj runtime.Object) error {
*obj.(*v1alpha1.KubernetesApplicationResource) = *(kubeAR(
withFinalizers(finalizerName),
withDeletionTimestamp(deleteTime)))
return nil
},
MockUpdate: func(_ context.Context, obj runtime.Object, _ ...client.UpdateOption) error {
got := obj.(*v1alpha1.KubernetesApplicationResource)
want := kubeAR(withDeletionTimestamp(deleteTime))
if diff := cmp.Diff(want, got); diff != "" {
return errors.Errorf("MockUpdate: -want, +got: %s", diff)
}
return nil
},
},
},
req: reconcile.Request{NamespacedName: types.NamespacedName{Namespace: namespace, Name: name}},
wantResult: reconcile.Result{Requeue: false},
},
{
name: "KARDeletedSuccessfully",
rec: &Reconciler{
connecter: &mockConnecter{mockConnect: newMockConnectFn(noopSyncDeleter, nil)},
kube: &test.MockClient{
MockGet: func(_ context.Context, _ client.ObjectKey, obj runtime.Object) error {
*obj.(*v1alpha1.KubernetesApplicationResource) = *(kubeAR(withDeletionTimestamp(time.Now())))
return nil
},
MockUpdate: test.NewMockUpdateFn(nil),
},
},
req: reconcile.Request{NamespacedName: types.NamespacedName{Namespace: namespace, Name: name}},
wantResult: reconcile.Result{Requeue: false},
},
{
name: "KARDeleteFailure",
rec: &Reconciler{
connecter: &mockConnecter{mockConnect: newMockConnectFn(noopSyncDeleter, nil)},
kube: &test.MockClient{
MockGet: func(_ context.Context, _ client.ObjectKey, obj runtime.Object) error {
*obj.(*v1alpha1.KubernetesApplicationResource) = *(kubeAR(withDeletionTimestamp(time.Now())))
return nil
},
MockUpdate: test.NewMockUpdateFn(errorBoom),
},
},
req: reconcile.Request{NamespacedName: types.NamespacedName{Namespace: namespace, Name: name}},
wantResult: reconcile.Result{Requeue: false},
wantErr: errors.Wrapf(errorBoom, "cannot update %s %s/%s", v1alpha1.KubernetesApplicationResourceKind, namespace, name),
},
{
name: "KARSyncedSuccessfully",
rec: &Reconciler{
connecter: &mockConnecter{mockConnect: newMockConnectFn(noopSyncDeleter, nil)},
kube: &test.MockClient{MockGet: test.NewMockGetFn(nil), MockUpdate: test.NewMockUpdateFn(nil)},
},
req: reconcile.Request{NamespacedName: types.NamespacedName{Namespace: namespace, Name: name}},
wantResult: reconcile.Result{Requeue: false},
},
{
name: "KARSyncFailure",
rec: &Reconciler{
connecter: &mockConnecter{mockConnect: newMockConnectFn(noopSyncDeleter, nil)},
kube: &test.MockClient{MockGet: test.NewMockGetFn(nil), MockUpdate: test.NewMockUpdateFn(errorBoom)},
},
req: reconcile.Request{NamespacedName: types.NamespacedName{Namespace: namespace, Name: name}},
wantResult: reconcile.Result{Requeue: false},
wantErr: errors.Wrapf(errorBoom, "cannot update %s %s/%s", v1alpha1.KubernetesApplicationResourceKind, namespace, name),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
gotResult, gotErr := tc.rec.Reconcile(tc.req)
if diff := cmp.Diff(tc.wantErr, gotErr, test.EquateErrors()); diff != "" {
t.Errorf("tc.rec.Reconcile(...): want error != got error:\n%s", diff)
}
if diff := cmp.Diff(tc.wantResult, gotResult); diff != "" {
t.Errorf("tc.rec.Reconcile(...): -want, +got:\n%s", diff)
}
})
}
}
func TestGetConnectionSecrets(t *testing.T) {
cases := []struct {
name string
rec *Reconciler
ar *v1alpha1.KubernetesApplicationResource
wantAR *v1alpha1.KubernetesApplicationResource
wantSecrets []corev1.Secret
}{
{
name: "Successful",
rec: &Reconciler{
kube: &test.MockClient{
MockGet: func(_ context.Context, _ client.ObjectKey, obj runtime.Object) error {
*obj.(*corev1.Secret) = *secret
return nil
},
},
},
ar: kubeAR(
withSecrets(secretLocalObjectRef),
),
wantAR: kubeAR(
withSecrets(secretLocalObjectRef),
),
wantSecrets: []corev1.Secret{*secret},
},
{
name: "Failed",
rec: &Reconciler{
kube: &test.MockClient{MockGet: test.NewMockGetFn(errorBoom)},
},
ar: kubeAR(
withSecrets(secretLocalObjectRef),
),
wantAR: kubeAR(
withSecrets(secretLocalObjectRef),
withConditions(runtimev1alpha1.ReconcileError(errorBoom)),
),
wantSecrets: []corev1.Secret{},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
gotSecrets := tc.rec.getConnectionSecrets(ctx, tc.ar)
if diff := cmp.Diff(tc.wantSecrets, gotSecrets); diff != "" {
t.Errorf("tc.rec.getConnectionSecrets(...): -want, +got:\n%s", diff)
}
if diff := cmp.Diff(tc.wantAR, tc.ar); diff != "" {
t.Errorf("AR: -want, +got:\n%s", diff)
}
})
}
}
func TestHasController(t *testing.T) {
cases := []struct {
name string
obj metav1.Object
want bool
}{
{
name: "HasController",
obj: service,
want: true,
},
{
name: "MissingNamespace",
obj: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
RemoteControllerName: name,
RemoteControllerUID: string(uid),
},
},
},
want: false,
},
{
name: "MissingName",
obj: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
RemoteControllerNamespace: namespace,
RemoteControllerUID: string(uid),
},
},
},
want: false,
},
{
name: "MissingUID",
obj: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
RemoteControllerNamespace: namespace,
RemoteControllerName: name,
},
},
},
want: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := hasController(tc.obj)
if got != tc.want {
t.Errorf("hasController(...): want %t, got %t", tc.want, got)
}
})
}
}
func TestHaveSameController(t *testing.T) {
cases := []struct {
name string
a metav1.Object
b metav1.Object
want bool
}{
{
name: "HasSameController",
a: service,
b: existingService,
want: true,
},
{
name: "HasNoController",
a: &corev1.Service{},
b: existingService,
want: false,
},
{
name: "HasDifferentController",
a: service,
b: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
RemoteControllerNamespace: namespace,
RemoteControllerName: name,
RemoteControllerUID: "imdifferent!",
},
},
},
want: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := haveSameController(tc.a, tc.b)
if got != tc.want {
t.Errorf("hasController(...): want %t, got %t", tc.want, got)
}
})
}
}