diff --git a/Makefile b/Makefile index 82ce002..33d354f 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ mocks: install-mockery ## Build mocks @bin/mockery --quiet --name=ObjectKind --case=underscore --output=mocks/apimachinery/pkg/runtime/schema --dir="$(K8S_APIMACHINERY_DIR)/pkg/runtime/schema" @echo "ok." @echo -n "building mocks for sigs.k8s.io/controller-runtime/pkg/client ... " - @bin/mockery --quiet --name="(Client|Status)" --case=underscore --output=mocks/controller-runtime/pkg/client --dir="$(CONTROLLER_RUNTIME_DIR)/pkg/client" + @bin/mockery --quiet --name="(Client|Status|Reader)" --case=underscore --output=mocks/controller-runtime/pkg/client --dir="$(CONTROLLER_RUNTIME_DIR)/pkg/client" @echo "ok." help: ## Show this help. diff --git a/mocks/controller-runtime/pkg/client/reader.go b/mocks/controller-runtime/pkg/client/reader.go new file mode 100644 index 0000000..4819e2b --- /dev/null +++ b/mocks/controller-runtime/pkg/client/reader.go @@ -0,0 +1,55 @@ +// Code generated by mockery v2.2.2. DO NOT EDIT. + +package mocks + +import ( + context "context" + + client "sigs.k8s.io/controller-runtime/pkg/client" + + mock "github.com/stretchr/testify/mock" + + runtime "k8s.io/apimachinery/pkg/runtime" + + types "k8s.io/apimachinery/pkg/types" +) + +// Reader is an autogenerated mock type for the Reader type +type Reader struct { + mock.Mock +} + +// Get provides a mock function with given fields: ctx, key, obj +func (_m *Reader) Get(ctx context.Context, key types.NamespacedName, obj runtime.Object) error { + ret := _m.Called(ctx, key, obj) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, types.NamespacedName, runtime.Object) error); ok { + r0 = rf(ctx, key, obj) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// List provides a mock function with given fields: ctx, list, opts +func (_m *Reader) List(ctx context.Context, list runtime.Object, opts ...client.ListOption) error { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, list) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, runtime.Object, ...client.ListOption) error); ok { + r0 = rf(ctx, list, opts...) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/mocks/pkg/types/adopted_resource_reconciler.go b/mocks/pkg/types/adopted_resource_reconciler.go new file mode 100644 index 0000000..1743cd9 --- /dev/null +++ b/mocks/pkg/types/adopted_resource_reconciler.go @@ -0,0 +1,91 @@ +// Code generated by mockery v2.2.2. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + manager "sigs.k8s.io/controller-runtime/pkg/manager" + + reconcile "sigs.k8s.io/controller-runtime/pkg/reconcile" + + types "github.com/aws-controllers-k8s/runtime/pkg/types" + + v1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" +) + +// AdoptedResourceReconciler is an autogenerated mock type for the AdoptedResourceReconciler type +type AdoptedResourceReconciler struct { + mock.Mock +} + +// BindControllerManager provides a mock function with given fields: _a0 +func (_m *AdoptedResourceReconciler) BindControllerManager(_a0 manager.Manager) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(manager.Manager) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Reconcile provides a mock function with given fields: _a0 +func (_m *AdoptedResourceReconciler) Reconcile(_a0 reconcile.Request) (reconcile.Result, error) { + ret := _m.Called(_a0) + + var r0 reconcile.Result + if rf, ok := ret.Get(0).(func(reconcile.Request) reconcile.Result); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(reconcile.Result) + } + + var r1 error + if rf, ok := ret.Get(1).(func(reconcile.Request) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SecretValueFromReference provides a mock function with given fields: _a0, _a1 +func (_m *AdoptedResourceReconciler) SecretValueFromReference(_a0 context.Context, _a1 *v1alpha1.SecretKeyReference) (string, error) { + ret := _m.Called(_a0, _a1) + + var r0 string + if rf, ok := ret.Get(0).(func(context.Context, *v1alpha1.SecretKeyReference) string); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Get(0).(string) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *v1alpha1.SecretKeyReference) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Sync provides a mock function with given fields: _a0, _a1, _a2, _a3 +func (_m *AdoptedResourceReconciler) Sync(_a0 context.Context, _a1 types.AWSResourceDescriptor, _a2 types.AWSResourceManager, _a3 *v1alpha1.AdoptedResource) error { + ret := _m.Called(_a0, _a1, _a2, _a3) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, types.AWSResourceDescriptor, types.AWSResourceManager, *v1alpha1.AdoptedResource) error); ok { + r0 = rf(_a0, _a1, _a2, _a3) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/pkg/runtime/adoption_reconciler.go b/pkg/runtime/adoption_reconciler.go index e3c4813..b0b21b4 100644 --- a/pkg/runtime/adoption_reconciler.go +++ b/pkg/runtime/adoption_reconciler.go @@ -140,10 +140,10 @@ func (r *adoptionReconciler) reconcile(req ctrlrt.Request) error { return nil } - return r.sync(ctx, targetDescriptor, rm, res) + return r.Sync(ctx, targetDescriptor, rm, res) } -func (r *adoptionReconciler) sync( +func (r *adoptionReconciler) Sync( ctx context.Context, targetDescriptor acktypes.AWSResourceDescriptor, rm acktypes.AWSResourceManager, @@ -171,7 +171,10 @@ func (r *adoptionReconciler) sync( GenerateName: rmo.GetGenerateName(), } - desiredMetadata := desired.Spec.Kubernetes.Metadata + var desiredMetadata *ackv1alpha1.PartialObjectMeta + if desired.Spec.Kubernetes != nil { + desiredMetadata = desired.Spec.Kubernetes.Metadata + } // Attempt to use metadata values from the adopted resource target metadata if desiredMetadata != nil { @@ -235,6 +238,7 @@ func (r *adoptionReconciler) sync( } } + // TODO(vijtrip2@): Should adopted resource be marked as managed earlier ? if err := r.markManaged(ctx, desired); err != nil { return r.onError(ctx, desired, err) } @@ -489,7 +493,7 @@ func (r *adoptionReconciler) getRegion( // patchMetadataAndSpec patches the Metadata and Spec for AdoptedResource into // k8s. The adopted resource 'res' also gets updated with content returned from // apiserver. -// TODO(vijat@): Refactor this and use single 'patchMetadataAndSpec' method +// TODO(vijtrip2@): Refactor this and use single 'patchMetadataAndSpec' method // for reconciler and adoptionReconciler func (r *adoptionReconciler) patchMetadataAndSpec( ctx context.Context, @@ -511,7 +515,7 @@ func (r *adoptionReconciler) patchMetadataAndSpec( // patchStatus patches the Status for AdoptedResource into k8s. The adopted // resource 'res' also gets updated with the content returned from apiserver. -// TODO(vijat@): Refactor this and use single 'patchStatus' method +// TODO(vijtrip2): Refactor this and use single 'patchStatus' method // for reconciler and adoptionReconciler func (r *adoptionReconciler) patchStatus( ctx context.Context, @@ -533,13 +537,31 @@ func NewAdoptionReconciler( metrics *ackmetrics.Metrics, cache ackrtcache.Caches, ) acktypes.Reconciler { + return NewAdoptionReconcilerWithClient(sc, log, cfg, metrics, cache, nil, nil) +} + +// NewAdoptionReconcilerWithClient returns a new adoptionReconciler object with +// specified k8s client and Reader. Currently this function is used for testing +// purpose only because "adoptionReconciler" struct is not available outside +// 'runtime' package for dependency injection. +func NewAdoptionReconcilerWithClient( + sc acktypes.ServiceController, + log logr.Logger, + cfg ackcfg.Config, + metrics *ackmetrics.Metrics, + cache ackrtcache.Caches, + kc client.Client, + apiReader client.Reader, +) acktypes.AdoptedResourceReconciler { return &adoptionReconciler{ reconciler: reconciler{ - sc: sc, - log: log.WithName("adopted-reconciler"), - cfg: cfg, - metrics: metrics, - cache: cache, + sc: sc, + log: log.WithName("adopted-reconciler"), + cfg: cfg, + metrics: metrics, + cache: cache, + kc: kc, + apiReader: apiReader, }, } } diff --git a/pkg/runtime/adoption_reconciler_test.go b/pkg/runtime/adoption_reconciler_test.go new file mode 100644 index 0000000..2058805 --- /dev/null +++ b/pkg/runtime/adoption_reconciler_test.go @@ -0,0 +1,466 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file 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 runtime_test + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8sobj "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + ctrlrtzap "sigs.k8s.io/controller-runtime/pkg/log/zap" + + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" + k8srtmocks "github.com/aws-controllers-k8s/runtime/mocks/apimachinery/pkg/runtime" + ctrlrtclientmock "github.com/aws-controllers-k8s/runtime/mocks/controller-runtime/pkg/client" + ackmocks "github.com/aws-controllers-k8s/runtime/mocks/pkg/types" + ackcfg "github.com/aws-controllers-k8s/runtime/pkg/config" + ackmetrics "github.com/aws-controllers-k8s/runtime/pkg/metrics" + ackrt "github.com/aws-controllers-k8s/runtime/pkg/runtime" + ackrtcache "github.com/aws-controllers-k8s/runtime/pkg/runtime/cache" + acktypes "github.com/aws-controllers-k8s/runtime/pkg/types" +) + +const ( + Namespace = "default" + Name = "adoptedRes" +) + +// Helper functions for tests + +func mockReconciler() (acktypes.AdoptedResourceReconciler, *ctrlrtclientmock.Client, *ctrlrtclientmock.Reader) { + zapOptions := ctrlrtzap.Options{ + Development: true, + Level: zapcore.InfoLevel, + } + fakeLogger := ctrlrtzap.New(ctrlrtzap.UseFlagOptions(&zapOptions)) + cfg := ackcfg.Config{} + metrics := ackmetrics.NewMetrics("bookstore") + + sc := &ackmocks.ServiceController{} + rmfactory := ackmocks.AWSResourceManagerFactory{} + rmFactoryMap := make(map[string]acktypes.AWSResourceManagerFactory) + rmFactoryMap["services.k8s.aws"] = &rmfactory + sc.On("GetResourceManagerFactories").Return(rmFactoryMap) + kc := &ctrlrtclientmock.Client{} + apiReader := &ctrlrtclientmock.Reader{} + return ackrt.NewAdoptionReconcilerWithClient( + sc, + fakeLogger, + cfg, + metrics, + ackrtcache.Caches{}, + kc, + apiReader, + ), kc, apiReader +} + +func mockDescriptorAndAWSResource() (*ackmocks.AWSResourceDescriptor, *ackmocks.AWSResource) { + des := &ackmocks.AWSResourceDescriptor{} + emptyRuntimeObject := &k8srtmocks.Object{} + res := &ackmocks.AWSResource{} + des.On("EmptyRuntimeObject").Return(emptyRuntimeObject) + des.On("ResourceFromRuntimeObject", emptyRuntimeObject).Return(res) + return des, res +} + +func mockManager() *ackmocks.AWSResourceManager { + return &ackmocks.AWSResourceManager{} +} + +func setupMockClient(kc *ctrlrtclientmock.Client, statusWriter *ctrlrtclientmock.StatusWriter, ctx context.Context, adoptedRes *ackv1alpha1.AdoptedResource) { + kc.On("Status").Return(statusWriter) + statusWriter.On("Patch", ctx, adoptedRes, mock.AnythingOfType("*client.mergeFromPatch")).Return(nil) + kc.On("Patch", ctx, adoptedRes, mock.AnythingOfType("*client.mergeFromPatch")).Return(nil) +} + +func setupMockAwsResource(res *ackmocks.AWSResource, adoptedRes *ackv1alpha1.AdoptedResource) { + res.On("SetIdentifiers", adoptedRes.Spec.AWS).Return(nil) + res.On("RuntimeObject").Return(&k8srtmocks.Object{}) + res.On("SetObjectMeta", mock.AnythingOfType("ObjectMeta")).Run(func(args mock.Arguments) {}) + + metaObj := &k8sobj.Unstructured{} + metaObj.SetNamespace(Namespace) + metaObj.SetName(Name) + res.On("MetaObject").Return(metaObj) + + rmo := &ackmocks.RuntimeMetaObject{} + res.On("RuntimeMetaObject").Return(rmo) + + rmo.On("GetLabels").Return(make(map[string]string)) + rmo.On("GetAnnotations").Return(make(map[string]string)) + rmo.On("GetFinalizers").Return(make([]string, 0)) + rmo.On("GetOwnerReferences").Return(make([]v1.OwnerReference, 0)) + rmo.On("GetGenerateName").Return("") +} + +func setupMockManager(manager *ackmocks.AWSResourceManager, ctx context.Context, res *ackmocks.AWSResource) { + manager.On("ReadOne", ctx, res).Return(res, nil) +} + +func setupMockDescriptor(descriptor *ackmocks.AWSResourceDescriptor, res *ackmocks.AWSResource) { + descriptor.On("MarkManaged", res).Run(func(args mock.Arguments) {}) + descriptor.On("MarkAdopted", res).Run(func(args mock.Arguments) {}) +} + +func setupMockApiReader(apiReader *ctrlrtclientmock.Reader, ctx context.Context, res *ackmocks.AWSResource) { + apiReader.On("Get", ctx, types.NamespacedName{ + Namespace: Namespace, + Name: Name, + }, res.RuntimeObject()).Return(k8serrors.NewNotFound(schema.GroupResource{}, "")) +} + +func adoptedResource(namespace, name string) *ackv1alpha1.AdoptedResource { + return &ackv1alpha1.AdoptedResource{ + TypeMeta: v1.TypeMeta{}, + ObjectMeta: v1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + Spec: ackv1alpha1.AdoptedResourceSpec{ + Kubernetes: nil, + AWS: &ackv1alpha1.AWSIdentifiers{NameOrID: "name"}, + }, + Status: ackv1alpha1.AdoptedResourceStatus{}, + } +} + +//Tests + +func TestSync_FailureInSettingIdentifiers(t *testing.T) { + // Setup + require := require.New(t) + // Mock resource creation + r, kc, apiReader := mockReconciler() + descriptor, res := mockDescriptorAndAWSResource() + manager := mockManager() + adoptedRes := adoptedResource(Namespace, Name) + res.On("SetIdentifiers", adoptedRes.Spec.AWS).Return(errors.New("unable to set Identifier")) + ctx := context.TODO() + statusWriter := &ctrlrtclientmock.StatusWriter{} + + //Mock behavior setup + setupMockAwsResource(res, adoptedRes) + setupMockClient(kc, statusWriter, ctx, adoptedRes) + + // Call + err := r.Sync(ctx, descriptor, manager, adoptedRes) + + // Assertions + // error occured + require.NotNil(err) + require.Equal("unable to set Identifier", err.Error()) + // Attempt to set Identifiers from AdoptedResource into AWSResource + res.AssertCalled(t, "SetIdentifiers", adoptedRes.Spec.AWS) + // ReadOne call is not made to find observed state of AWSResource because + // of SetIdentifiers failure + manager.AssertNotCalled(t, "ReadOne", ctx, res) + // No calls to findout if the AWSResource already exists + apiReader.AssertNotCalled(t, "Get", ctx, types.NamespacedName{ + Namespace: Namespace, + Name: Name, + }, res.RuntimeObject()) + assertAWSResourceCreation(false, t, ctx, kc, statusWriter, res) + assertManaged(false, t, ctx, kc, adoptedRes) + assertAdoptedCondition("False", require, t, ctx, kc, statusWriter, adoptedRes) +} + +func TestSync_FailureInReadOne(t *testing.T) { + // Setup + require := require.New(t) + // Mock resource creation + r, kc, apiReader := mockReconciler() + descriptor, res := mockDescriptorAndAWSResource() + manager := mockManager() + adoptedRes := adoptedResource(Namespace, Name) + ctx := context.TODO() + statusWriter := &ctrlrtclientmock.StatusWriter{} + + //Mock behavior setup + setupMockAwsResource(res, adoptedRes) + setupMockClient(kc, statusWriter, ctx, adoptedRes) + manager.On("ReadOne", ctx, res).Return(res, errors.New("failed to perform ReadOne")) + + // Call + err := r.Sync(ctx, descriptor, manager, adoptedRes) + + //Assertions + require.NotNil(err) + require.Equal("failed to perform ReadOne", err.Error()) + // Identifiers are set from AdoptedResource into AWSResource + res.AssertCalled(t, "SetIdentifiers", adoptedRes.Spec.AWS) + // ReadOne call is made to find observed state of AWSResource + manager.AssertCalled(t, "ReadOne", ctx, res) + // No calls to findout if the AWSResource already exists because of ReadOne + // failure + apiReader.AssertNotCalled(t, "Get", ctx, types.NamespacedName{ + Namespace: Namespace, + Name: Name, + }, res.RuntimeObject()) + assertAWSResourceCreation(false, t, ctx, kc, statusWriter, res) + assertManaged(false, t, ctx, kc, adoptedRes) + assertAdoptedCondition("False", require, t, ctx, kc, statusWriter, adoptedRes) +} + +func TestSync_AWSResourceAlreadyExists(t *testing.T) { + // Setup + require := require.New(t) + // Mock resource creation + r, kc, apiReader := mockReconciler() + descriptor, res := mockDescriptorAndAWSResource() + manager := mockManager() + adoptedRes := adoptedResource(Namespace, Name) + ctx := context.TODO() + statusWriter := &ctrlrtclientmock.StatusWriter{} + + //Mock behavior setup + setupMockAwsResource(res, adoptedRes) + setupMockClient(kc, statusWriter, ctx, adoptedRes) + setupMockManager(manager, ctx, res) + setupMockDescriptor(descriptor, res) + + apiReader.On("Get", ctx, types.NamespacedName{ + Namespace: Namespace, + Name: Name, + }, res.RuntimeObject()).Return(nil) + + // Call + err := r.Sync(ctx, descriptor, manager, adoptedRes) + + //Assertions + require.Nil(err) + assertAWSResourceRead(t, ctx, manager, apiReader, adoptedRes, res) + assertAWSResourceCreation(false, t, ctx, kc, statusWriter, res) + assertManaged(true, t, ctx, kc, adoptedRes) + assertAdoptedCondition("True", require, t, ctx, kc, statusWriter, adoptedRes) +} + +func TestSync_APIReaderUnknownError(t *testing.T) { + // Setup + require := require.New(t) + // Mock resource creation + r, kc, apiReader := mockReconciler() + descriptor, res := mockDescriptorAndAWSResource() + manager := mockManager() + adoptedRes := adoptedResource(Namespace, Name) + ctx := context.TODO() + statusWriter := &ctrlrtclientmock.StatusWriter{} + + //Mock behavior setup + setupMockAwsResource(res, adoptedRes) + setupMockClient(kc, statusWriter, ctx, adoptedRes) + setupMockManager(manager, ctx, res) + setupMockDescriptor(descriptor, res) + + apiReader.On("Get", ctx, types.NamespacedName{ + Namespace: Namespace, + Name: Name, + }, res.RuntimeObject()).Return(errors.New("unknown error")) + + // Call + err := r.Sync(ctx, descriptor, manager, adoptedRes) + + //Assertions + require.NotNil(err) + require.Equal("unknown error", err.Error()) + assertAWSResourceRead(t, ctx, manager, apiReader, adoptedRes, res) + assertAWSResourceCreation(false, t, ctx, kc, statusWriter, res) + assertManaged(false, t, ctx, kc, adoptedRes) + assertAdoptedCondition("False", require, t, ctx, kc, statusWriter, adoptedRes) +} + +func TestSync_ErrorInResourceCreation(t *testing.T) { + // Setup + require := require.New(t) + // Mock resource creation + r, kc, apiReader := mockReconciler() + descriptor, res := mockDescriptorAndAWSResource() + manager := mockManager() + adoptedRes := adoptedResource(Namespace, Name) + ctx := context.TODO() + statusWriter := &ctrlrtclientmock.StatusWriter{} + + //Mock behavior setup + setupMockAwsResource(res, adoptedRes) + setupMockClient(kc, statusWriter, ctx, adoptedRes) + setupMockManager(manager, ctx, res) + setupMockDescriptor(descriptor, res) + setupMockApiReader(apiReader, ctx, res) + kc.On("Create", ctx, res.RuntimeObject()).Return(errors.New("creation failure")) + + // Call + err := r.Sync(ctx, descriptor, manager, adoptedRes) + + //Assertions + require.NotNil(err) + require.Equal("creation failure", err.Error()) + assertAWSResourceRead(t, ctx, manager, apiReader, adoptedRes, res) + kc.AssertCalled(t, "Create", ctx, res.RuntimeObject()) + // Update status of AWSResource should not happen due to creation failure + statusWriter.AssertNotCalled(t, "Update", ctx, res.RuntimeObject()) + assertManaged(false, t, ctx, kc, adoptedRes) + assertAdoptedCondition("False", require, t, ctx, kc, statusWriter, adoptedRes) +} + +func TestSync_ErrorInStatusUpdate(t *testing.T) { + // Setup + require := require.New(t) + // Mock resource creation + r, kc, apiReader := mockReconciler() + descriptor, res := mockDescriptorAndAWSResource() + manager := mockManager() + adoptedRes := adoptedResource(Namespace, Name) + ctx := context.TODO() + statusWriter := &ctrlrtclientmock.StatusWriter{} + + //Mock behavior setup + setupMockAwsResource(res, adoptedRes) + setupMockClient(kc, statusWriter, ctx, adoptedRes) + setupMockManager(manager, ctx, res) + setupMockDescriptor(descriptor, res) + setupMockApiReader(apiReader, ctx, res) + kc.On("Create", ctx, res.RuntimeObject()).Return(nil) + statusWriter.On("Update", ctx, res.RuntimeObject()).Return(errors.New("status update failure")) + + // Call + err := r.Sync(ctx, descriptor, manager, adoptedRes) + + //Assertions + require.NotNil(err) + require.Equal("status update failure", err.Error()) + assertAWSResourceRead(t, ctx, manager, apiReader, adoptedRes, res) + assertAWSResourceCreation(true, t, ctx, kc, statusWriter, res) + assertManaged(false, t, ctx, kc, adoptedRes) + assertAdoptedCondition("False", require, t, ctx, kc, statusWriter, adoptedRes) +} + +func TestSync_HappyCase(t *testing.T) { + // Setup + require := require.New(t) + // Mock resource creation + r, kc, apiReader := mockReconciler() + descriptor, res := mockDescriptorAndAWSResource() + manager := mockManager() + adoptedRes := adoptedResource(Namespace, Name) + ctx := context.TODO() + statusWriter := &ctrlrtclientmock.StatusWriter{} + + //Mock behavior setup + setupMockAwsResource(res, adoptedRes) + setupMockClient(kc, statusWriter, ctx, adoptedRes) + setupMockManager(manager, ctx, res) + setupMockDescriptor(descriptor, res) + setupMockApiReader(apiReader, ctx, res) + kc.On("Create", ctx, res.RuntimeObject()).Return(nil) + statusWriter.On("Update", ctx, res.RuntimeObject()).Return(nil) + + // Call + err := r.Sync(ctx, descriptor, manager, adoptedRes) + + //Assertions + require.Nil(err) + assertAWSResourceRead(t, ctx, manager, apiReader, adoptedRes, res) + assertAWSResourceCreation(true, t, ctx, kc, statusWriter, res) + assertManaged(true, t, ctx, kc, adoptedRes) + assertAdoptedCondition("True", require, t, ctx, kc, statusWriter, adoptedRes) +} + +// Assertion Helpers + +// assertAdoptedCondition asserts that 'ConditionTypeAdopted' condition is +// present in AdoptedResource status and that it's value is equal to +// 'conditionStatus' parameter +func assertAdoptedCondition( + conditionStatus string, + require *require.Assertions, + t *testing.T, + ctx context.Context, + kc *ctrlrtclientmock.Client, + statusWriter *ctrlrtclientmock.StatusWriter, + adoptedRes *ackv1alpha1.AdoptedResource, +) { + kc.AssertCalled(t, "Status") + statusWriter.AssertCalled(t, "Patch", ctx, adoptedRes, mock.AnythingOfType("*client.mergeFromPatch")) + // Only one kind of condition present + require.Equal(1, len(adoptedRes.Status.Conditions)) + require.Equal(ackv1alpha1.ConditionTypeAdopted, adoptedRes.Status.Conditions[0].Type) + require.Equal(conditionStatus, string(adoptedRes.Status.Conditions[0].Status)) +} + +// assertManaged asserts that adoptedResource was patched when 'expectedManaged' +// parameter is true. +// If 'expectedManaged' parameter is false, this function asserts that +// adoptedResource was never patched. +func assertManaged( + expectedManaged bool, + t *testing.T, + ctx context.Context, + kc *ctrlrtclientmock.Client, + adoptedRes *ackv1alpha1.AdoptedResource, +) { + if expectedManaged { + kc.AssertCalled(t, "Patch", ctx, adoptedRes, mock.AnythingOfType("*client.mergeFromPatch")) + } else { + kc.AssertNotCalled(t, "Patch", ctx, adoptedRes, mock.AnythingOfType("*client.mergeFromPatch")) + } +} + +// assertAWSResourceCreation asserts that AWSResource was created and it's spec +// was updated when 'expectedCreation' is true +// If 'expectedCreation' is false, this function asserts that AWSResource was +// neither created nor was the status updated. +func assertAWSResourceCreation( + expectedCreation bool, + t *testing.T, + ctx context.Context, + kc *ctrlrtclientmock.Client, + statusWriter *ctrlrtclientmock.StatusWriter, + res *ackmocks.AWSResource, +) { + if expectedCreation { + kc.AssertCalled(t, "Create", ctx, res.RuntimeObject()) + statusWriter.AssertCalled(t, "Update", ctx, res.RuntimeObject()) + } else { + kc.AssertNotCalled(t, "Create", ctx, res.RuntimeObject()) + statusWriter.AssertNotCalled(t, "Update", ctx, res.RuntimeObject()) + } +} + +// assertAWSResourceRead asserts that +// a) Identifiers are set from AdoptedResource to AWSResource +// b) ReadOne call is made to find observed state of AWSResource +// c) APIReader.Get call is made to validate that AWSResource does not already +// exist in k8s cluster +func assertAWSResourceRead( + t *testing.T, + ctx context.Context, + manager *ackmocks.AWSResourceManager, + apiReader *ctrlrtclientmock.Reader, + adoptedRes *ackv1alpha1.AdoptedResource, + res *ackmocks.AWSResource, +) { + res.AssertCalled(t, "SetIdentifiers", adoptedRes.Spec.AWS) + manager.AssertCalled(t, "ReadOne", ctx, res) + apiReader.AssertCalled(t, "Get", ctx, types.NamespacedName{ + Namespace: Namespace, + Name: Name, + }, res.RuntimeObject()) +} diff --git a/pkg/types/adopted_resource_reconciler.go b/pkg/types/adopted_resource_reconciler.go new file mode 100644 index 0000000..1d48b25 --- /dev/null +++ b/pkg/types/adopted_resource_reconciler.go @@ -0,0 +1,40 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file 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 types + +import ( + "context" + + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" +) + +// AdoptedResourceReconciler is responsible for reconciling an adopted resource +// that represent AWS service API resource. +// It implements the upstream controller-runtime `Reconciler` +// interface. +type AdoptedResourceReconciler interface { + Reconciler + // Sync ensures that the supplied AdoptedResource creates the matching + // AWSResource based on observed state from ReadOne method + // + // + // NOTE(vijtrip2): This is really only here for dependency injection + // purposes in unit testing in order to simplify test setups. + Sync( + context.Context, + AWSResourceDescriptor, + AWSResourceManager, + *ackv1alpha1.AdoptedResource, + ) error +}