From a682b306fa12a6e0842dcef0794a5c39d77b1701 Mon Sep 17 00:00:00 2001 From: Amulyam24 Date: Fri, 24 Jun 2022 16:14:48 +0530 Subject: [PATCH] Improve UT coverage for IBMPowerVS controllers --- cloud/scope/powervs_cluster.go | 4 +- cloud/scope/powervs_machine.go | 8 +- cloud/scope/powervs_test.go | 2 +- .../ibmpowervscluster_controller_test.go | 69 ++- .../ibmpowervsmachine_controller_test.go | 582 ++++++++++++++++++ 5 files changed, 647 insertions(+), 18 deletions(-) create mode 100644 controllers/ibmpowervsmachine_controller_test.go diff --git a/cloud/scope/powervs_cluster.go b/cloud/scope/powervs_cluster.go index 1fa8af97a..af619fd0f 100644 --- a/cloud/scope/powervs_cluster.go +++ b/cloud/scope/powervs_cluster.go @@ -56,7 +56,7 @@ type PowerVSClusterScopeParams struct { // PowerVSClusterScope defines a scope defined around a Power VS Cluster. type PowerVSClusterScope struct { logr.Logger - client client.Client + Client client.Client patchHelper *patch.Helper IBMPowerVSClient powervs.PowerVS @@ -73,7 +73,7 @@ func NewPowerVSClusterScope(params PowerVSClusterScopeParams) (scope *PowerVSClu err = errors.New("failed to generate new scope from nil Client") return } - scope.client = params.Client + scope.Client = params.Client if params.Cluster == nil { err = errors.New("failed to generate new scope from nil Cluster") diff --git a/cloud/scope/powervs_machine.go b/cloud/scope/powervs_machine.go index 42017da25..e276fe69e 100644 --- a/cloud/scope/powervs_machine.go +++ b/cloud/scope/powervs_machine.go @@ -65,7 +65,7 @@ type PowerVSMachineScopeParams struct { // PowerVSMachineScope defines a scope defined around a Power VS Machine. type PowerVSMachineScope struct { logr.Logger - client client.Client + Client client.Client patchHelper *patch.Helper IBMPowerVSClient powervs.PowerVS @@ -85,7 +85,7 @@ func NewPowerVSMachineScope(params PowerVSMachineScopeParams) (scope *PowerVSMac err = errors.New("client is required when creating a MachineScope") return } - scope.client = params.Client + scope.Client = params.Client if params.Machine == nil { err = errors.New("machine is required when creating a MachineScope") @@ -299,8 +299,8 @@ func (m *PowerVSMachineScope) GetBootstrapData() (string, error) { secret := &corev1.Secret{} key := types.NamespacedName{Namespace: m.Machine.Namespace, Name: *m.Machine.Spec.Bootstrap.DataSecretName} - if err := m.client.Get(context.TODO(), key, secret); err != nil { - return "", errors.Wrapf(err, "failed to retrieve bootstrap data secret for IBMVPCMachine %s/%s", m.Machine.Namespace, m.Machine.Name) + if err := m.Client.Get(context.TODO(), key, secret); err != nil { + return "", errors.Wrapf(err, "failed to retrieve bootstrap data secret for IBMPowerVSMachine %s/%s", m.Machine.Namespace, m.Machine.Name) } value, ok := secret.Data["value"] diff --git a/cloud/scope/powervs_test.go b/cloud/scope/powervs_test.go index bc06de45c..57bd1655d 100644 --- a/cloud/scope/powervs_test.go +++ b/cloud/scope/powervs_test.go @@ -165,7 +165,7 @@ func setupPowerVSMachineScope(clusterName string, machineName string, imageID *s client := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(initObjects...).Build() return &PowerVSMachineScope{ - client: client, + Client: client, Logger: klogr.New(), IBMPowerVSClient: mockpowervs, Cluster: cluster, diff --git a/controllers/ibmpowervscluster_controller_test.go b/controllers/ibmpowervscluster_controller_test.go index e910860a6..04f211271 100644 --- a/controllers/ibmpowervscluster_controller_test.go +++ b/controllers/ibmpowervscluster_controller_test.go @@ -29,11 +29,12 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" infrav1beta1 "sigs.k8s.io/cluster-api-provider-ibmcloud/api/v1beta1" + "sigs.k8s.io/cluster-api-provider-ibmcloud/cloud/scope" capiv1beta1 "sigs.k8s.io/cluster-api/api/v1beta1" "sigs.k8s.io/cluster-api/util" ) -func TestIBMPowerVSClusterReconciler(t *testing.T) { +func TestIBMPowerVSClusterReconciler_Reconcile(t *testing.T) { testCases := []struct { name string powervsCluster *infrav1beta1.IBMPowerVSCluster @@ -42,20 +43,27 @@ func TestIBMPowerVSClusterReconciler(t *testing.T) { }{ { name: "Should fail Reconcile if owner cluster not found", - powervsCluster: &infrav1beta1.IBMPowerVSCluster{ObjectMeta: metav1.ObjectMeta{GenerateName: "powervs-test-", OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: capiv1beta1.GroupVersion.String(), - Kind: "Cluster", - Name: "capi-test", - UID: "1", - }}}, + powervsCluster: &infrav1beta1.IBMPowerVSCluster{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "powervs-test-", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: capiv1beta1.GroupVersion.String(), + Kind: "Cluster", + Name: "capi-test", + UID: "1", + }}}, Spec: infrav1beta1.IBMPowerVSClusterSpec{ServiceInstanceID: "foo"}}, expectError: true, }, { - name: "Should not reconcile if owner reference is not set", - powervsCluster: &infrav1beta1.IBMPowerVSCluster{ObjectMeta: metav1.ObjectMeta{GenerateName: "powervs-test-"}, Spec: infrav1beta1.IBMPowerVSClusterSpec{ServiceInstanceID: "foo"}}, - expectError: false, + name: "Should not reconcile if owner reference is not set", + powervsCluster: &infrav1beta1.IBMPowerVSCluster{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "powervs-test-"}, + Spec: infrav1beta1.IBMPowerVSClusterSpec{ + ServiceInstanceID: "foo"}}, + expectError: false, }, { name: "Should Reconcile successfully if no IBMPowerVSCluster found", @@ -117,6 +125,45 @@ func TestIBMPowerVSClusterReconciler(t *testing.T) { } } +func TestIBMPowerVSClusterReconciler_reconcile(t *testing.T) { + testCases := []struct { + name string + powervsClusterScope *scope.PowerVSClusterScope + clusterStatus bool + }{ + { + name: "Should add finalizer and reconcile IBMPowerVSCluster", + powervsClusterScope: &scope.PowerVSClusterScope{ + IBMPowerVSCluster: &infrav1beta1.IBMPowerVSCluster{}, + }, + clusterStatus: false, + }, + { + name: "Should reconcile IBMPowerVSCluster status as Ready", + powervsClusterScope: &scope.PowerVSClusterScope{ + IBMPowerVSCluster: &infrav1beta1.IBMPowerVSCluster{ + ObjectMeta: metav1.ObjectMeta{ + Finalizers: []string{infrav1beta1.IBMPowerVSClusterFinalizer}, + }, + }, + }, + clusterStatus: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + reconciler := &IBMPowerVSClusterReconciler{ + Client: testEnv.Client, + Log: klogr.New(), + } + _ = reconciler.reconcile(tc.powervsClusterScope) + g.Expect(tc.powervsClusterScope.IBMPowerVSCluster.Status.Ready).To(Equal(tc.clusterStatus)) + g.Expect(tc.powervsClusterScope.IBMPowerVSCluster.Finalizers).To(ContainElement(infrav1beta1.IBMPowerVSClusterFinalizer)) + }) + } +} + func createCluster(g *WithT, powervsCluster *infrav1beta1.IBMPowerVSCluster, namespace string) { if powervsCluster != nil { powervsCluster.Namespace = namespace diff --git a/controllers/ibmpowervsmachine_controller_test.go b/controllers/ibmpowervsmachine_controller_test.go new file mode 100644 index 000000000..bb020e9a8 --- /dev/null +++ b/controllers/ibmpowervsmachine_controller_test.go @@ -0,0 +1,582 @@ +/* +Copyright 2022 The Kubernetes 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 controllers + +import ( + "errors" + "fmt" + "testing" + "time" + + "github.com/IBM-Cloud/power-go-client/power/models" + "github.com/golang/mock/gomock" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/record" + "k8s.io/klog/v2/klogr" + "k8s.io/utils/pointer" + capiv1beta1 "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/cluster-api/util" + "sigs.k8s.io/cluster-api/util/conditions" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + infrav1beta1 "sigs.k8s.io/cluster-api-provider-ibmcloud/api/v1beta1" + "sigs.k8s.io/cluster-api-provider-ibmcloud/cloud/scope" + "sigs.k8s.io/cluster-api-provider-ibmcloud/pkg/cloud/services/powervs/mock" +) + +func TestIBMPowerVSMachineReconciler_Reconcile(t *testing.T) { + testCases := []struct { + name string + powervsMachine *infrav1beta1.IBMPowerVSMachine + ownerMachine *capiv1beta1.Machine + powervsCluster *infrav1beta1.IBMPowerVSCluster + ownerCluster *capiv1beta1.Cluster + expectError bool + }{ + { + name: "Should Reconcile successfully if no IBMPowerVSMachine found", + expectError: false, + }, + { + name: "Should Reconcile if Owner Reference is not set", + powervsMachine: &infrav1beta1.IBMPowerVSMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "powervs-test-1"}, + Spec: infrav1beta1.IBMPowerVSMachineSpec{ + ServiceInstanceID: "service-instance-1", + Image: &infrav1beta1.IBMPowerVSResourceReference{}}}, + expectError: false, + }, + { + name: "Should fail Reconcile if no OwnerMachine found", + powervsMachine: &infrav1beta1.IBMPowerVSMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "powervs-test-2", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: capiv1beta1.GroupVersion.String(), + Kind: "Machine", + Name: "capi-test-machine", + UID: "1", + }, + }, + }, + Spec: infrav1beta1.IBMPowerVSMachineSpec{ + ServiceInstanceID: "service-instance-1", + Image: &infrav1beta1.IBMPowerVSResourceReference{}}, + }, + expectError: true, + }, + { + name: "Should not Reconcile if machine does not contain cluster label", + powervsMachine: &infrav1beta1.IBMPowerVSMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "powervs-test-3", OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: capiv1beta1.GroupVersion.String(), + Kind: "Machine", + Name: "capi-test-machine", + UID: "1", + }, + }, + }, Spec: infrav1beta1.IBMPowerVSMachineSpec{ + ServiceInstanceID: "service-instance-1", + Image: &infrav1beta1.IBMPowerVSResourceReference{}}, + }, + ownerMachine: &capiv1beta1.Machine{ + ObjectMeta: metav1.ObjectMeta{Name: "capi-test-machine"}}, + ownerCluster: &capiv1beta1.Cluster{ + ObjectMeta: metav1.ObjectMeta{Name: "capi-test-1"}}, + expectError: false, + }, + { + name: "Should not Reconcile if IBMPowerVSCluster is not found", + powervsMachine: &infrav1beta1.IBMPowerVSMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "powervs-test-4", + Labels: map[string]string{capiv1beta1.ClusterNameAnnotation: "capi-test-2"}, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: capiv1beta1.GroupVersion.String(), + Kind: "Machine", + Name: "capi-test-machine", + UID: "1", + }, + { + APIVersion: capiv1beta1.GroupVersion.String(), + Kind: "Cluster", + Name: "capi-test-2", + UID: "1", + }, + }, + }, Spec: infrav1beta1.IBMPowerVSMachineSpec{ + ServiceInstanceID: "service-instance-1", + Image: &infrav1beta1.IBMPowerVSResourceReference{}}, + }, + ownerMachine: &capiv1beta1.Machine{ + ObjectMeta: metav1.ObjectMeta{Name: "capi-test-machine"}}, + ownerCluster: &capiv1beta1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "capi-test-2"}, + Spec: capiv1beta1.ClusterSpec{ + InfrastructureRef: &corev1.ObjectReference{ + Name: "powervs-cluster"}}}, + expectError: false, + }, + { + name: "Should not Reconcile if IBMPowerVSImage is not found", + powervsMachine: &infrav1beta1.IBMPowerVSMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "powervs-test-5", + Labels: map[string]string{capiv1beta1.ClusterNameAnnotation: "capi-test-3"}, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: capiv1beta1.GroupVersion.String(), + Kind: "Machine", + Name: "capi-test-machine", + UID: "1", + }, + { + APIVersion: capiv1beta1.GroupVersion.String(), + Kind: "Cluster", + Name: "capi-test-3", + UID: "1", + }, + }, + }, Spec: infrav1beta1.IBMPowerVSMachineSpec{ + ServiceInstanceID: "service-instance-1", + ImageRef: &corev1.LocalObjectReference{ + Name: "capi-image", + }}, + }, + ownerMachine: &capiv1beta1.Machine{ + ObjectMeta: metav1.ObjectMeta{Name: "capi-test-machine"}}, + ownerCluster: &capiv1beta1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "capi-test-3"}, + Spec: capiv1beta1.ClusterSpec{ + InfrastructureRef: &corev1.ObjectReference{Name: "powervs-cluster"}}}, + powervsCluster: &infrav1beta1.IBMPowerVSCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "powervs-cluster"}, + Spec: infrav1beta1.IBMPowerVSClusterSpec{ + ServiceInstanceID: "service-instance-1"}}, + expectError: false, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + reconciler := &IBMPowerVSMachineReconciler{ + Client: testEnv.Client, + Log: klogr.New(), + } + ns, err := testEnv.CreateNamespace(ctx, fmt.Sprintf("namespace-%s", util.RandomString(5))) + g.Expect(err).To(BeNil()) + defer func() { + g.Expect(testEnv.Cleanup(ctx, ns)).To(Succeed()) + }() + + createObject(g, tc.ownerCluster, ns.Name) + defer cleanupObject(g, tc.ownerCluster) + + createObject(g, tc.powervsCluster, ns.Name) + defer cleanupObject(g, tc.powervsCluster) + + createObject(g, tc.ownerMachine, ns.Name) + defer cleanupObject(g, tc.ownerMachine) + + createObject(g, tc.powervsMachine, ns.Name) + defer cleanupObject(g, tc.powervsMachine) + + if tc.powervsMachine != nil { + g.Eventually(func() bool { + machine := &infrav1beta1.IBMPowerVSMachine{} + key := client.ObjectKey{ + Name: tc.powervsMachine.Name, + Namespace: ns.Name, + } + err = testEnv.Get(ctx, key, machine) + return err == nil + }, 10*time.Second).Should(Equal(true)) + + _, err := reconciler.Reconcile(ctx, ctrl.Request{ + NamespacedName: client.ObjectKey{ + Namespace: tc.powervsMachine.Namespace, + Name: tc.powervsMachine.Name, + }, + }) + if tc.expectError { + g.Expect(err).ToNot(BeNil()) + } else { + g.Expect(err).To(BeNil()) + } + } else { + _, err = reconciler.Reconcile(ctx, ctrl.Request{ + NamespacedName: client.ObjectKey{ + Namespace: "default", + Name: "test", + }, + }) + g.Expect(err).To(BeNil()) + } + }) + } +} + +func TestIBMPowerVSMachineReconciler_Delete(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockpowervs := mock.NewMockPowerVS(mockCtrl) + reconciler := IBMPowerVSMachineReconciler{ + Client: testEnv.Client, + Log: klogr.New(), + } + g := NewWithT(t) + t.Run("Reconciling deleting IBMPowerVSMachine ", func(t *testing.T) { + t.Run("Should not delete IBMPowerVSMachine if instance ID not found", func(t *testing.T) { + machineScope := &scope.PowerVSMachineScope{ + Logger: klogr.New(), + IBMPowerVSClient: mockpowervs, + IBMPowerVSMachine: &infrav1beta1.IBMPowerVSMachine{ + ObjectMeta: metav1.ObjectMeta{ + Finalizers: []string{infrav1beta1.IBMPowerVSMachineFinalizer}, + }, + }, + } + _, err := reconciler.reconcileDelete(machineScope) + g.Expect(err).To(BeNil()) + g.Expect(len(machineScope.IBMPowerVSMachine.Finalizers)).To(BeZero()) + }) + t.Run("Should fail to delete PowerVS instance", func(t *testing.T) { + machineScope := &scope.PowerVSMachineScope{ + Logger: klogr.New(), + IBMPowerVSClient: mockpowervs, + IBMPowerVSMachine: &infrav1beta1.IBMPowerVSMachine{ + ObjectMeta: metav1.ObjectMeta{ + Finalizers: []string{infrav1beta1.IBMPowerVSMachineFinalizer}, + }, + Spec: infrav1beta1.IBMPowerVSMachineSpec{}, + Status: infrav1beta1.IBMPowerVSMachineStatus{ + InstanceID: "powervs-instance-id", + }, + }, + } + mockpowervs.EXPECT().DeleteInstance(machineScope.IBMPowerVSMachine.Status.InstanceID).Return(errors.New("Could not delete PowerVS instance")) + _, err := reconciler.reconcileDelete(machineScope) + g.Expect(err).To(Not(BeNil())) + g.Expect(machineScope.IBMPowerVSMachine.Finalizers).To(ContainElement(infrav1beta1.IBMPowerVSMachineFinalizer)) + }) + t.Run("Should successfully delete the PowerVS machine", func(t *testing.T) { + machineScope := &scope.PowerVSMachineScope{ + Logger: klogr.New(), + IBMPowerVSClient: mockpowervs, + IBMPowerVSMachine: &infrav1beta1.IBMPowerVSMachine{ + ObjectMeta: metav1.ObjectMeta{ + Finalizers: []string{infrav1beta1.IBMPowerVSMachineFinalizer}, + }, + Spec: infrav1beta1.IBMPowerVSMachineSpec{}, + Status: infrav1beta1.IBMPowerVSMachineStatus{ + InstanceID: "powervs-instance-id", + }, + }, + } + mockpowervs.EXPECT().DeleteInstance(machineScope.IBMPowerVSMachine.Status.InstanceID).Return(nil) + _, err := reconciler.reconcileDelete(machineScope) + g.Expect(err).To(BeNil()) + g.Expect(len(machineScope.IBMPowerVSMachine.Finalizers)).To(BeZero()) + }) + }) +} + +func TestIBMPowerVSMachineReconciler_ReconcileOperations(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockpowervs := mock.NewMockPowerVS(mockCtrl) + recorder := record.NewFakeRecorder(2) + reconciler := IBMPowerVSMachineReconciler{ + Client: testEnv.Client, + Log: klogr.New(), + Recorder: recorder, + } + g := NewWithT(t) + t.Run("Reconciling creating IBMPowerVSMachine ", func(t *testing.T) { + t.Run("Should requeue if Cluster infrastructure status is not ready", func(t *testing.T) { + machineScope := &scope.PowerVSMachineScope{ + Logger: klogr.New(), + Cluster: &capiv1beta1.Cluster{ + Status: capiv1beta1.ClusterStatus{ + InfrastructureReady: false, + }, + }, + IBMPowerVSMachine: &infrav1beta1.IBMPowerVSMachine{}, + } + result, err := reconciler.reconcileNormal(machineScope) + g.Expect(err).To(BeNil()) + g.Expect(result.RequeueAfter).To(Not(BeZero())) + expectConditions(g, machineScope.IBMPowerVSMachine, []conditionAssertion{{infrav1beta1.InstanceReadyCondition, corev1.ConditionFalse, capiv1beta1.ConditionSeverityInfo, infrav1beta1.WaitingForClusterInfrastructureReason}}) + }) + t.Run("Should requeue if IBMPowerVSImage status is not ready", func(t *testing.T) { + machineScope := &scope.PowerVSMachineScope{ + Logger: klogr.New(), + Cluster: &capiv1beta1.Cluster{ + Status: capiv1beta1.ClusterStatus{ + InfrastructureReady: true, + }, + }, + IBMPowerVSMachine: &infrav1beta1.IBMPowerVSMachine{}, + IBMPowerVSImage: &infrav1beta1.IBMPowerVSImage{ + Status: infrav1beta1.IBMPowerVSImageStatus{ + Ready: false, + }, + }, + } + result, err := reconciler.reconcileNormal(machineScope) + g.Expect(err).To(BeNil()) + g.Expect(result.RequeueAfter).To(Not(BeZero())) + expectConditions(g, machineScope.IBMPowerVSMachine, []conditionAssertion{{infrav1beta1.InstanceReadyCondition, corev1.ConditionFalse, capiv1beta1.ConditionSeverityInfo, infrav1beta1.WaitingForIBMPowerVSImageReason}}) + }) + t.Run("Should requeue if boostrap data secret reference is not found", func(t *testing.T) { + machineScope := &scope.PowerVSMachineScope{ + Logger: klogr.New(), + Cluster: &capiv1beta1.Cluster{ + Status: capiv1beta1.ClusterStatus{ + InfrastructureReady: true, + }, + }, + Machine: &capiv1beta1.Machine{}, + IBMPowerVSMachine: &infrav1beta1.IBMPowerVSMachine{}, + IBMPowerVSImage: &infrav1beta1.IBMPowerVSImage{ + Status: infrav1beta1.IBMPowerVSImageStatus{ + Ready: true, + }, + }, + } + result, err := reconciler.reconcileNormal(machineScope) + g.Expect(err).To(BeNil()) + g.Expect(result.RequeueAfter).To(Not(BeZero())) + expectConditions(g, machineScope.IBMPowerVSMachine, []conditionAssertion{{infrav1beta1.InstanceReadyCondition, corev1.ConditionFalse, capiv1beta1.ConditionSeverityInfo, infrav1beta1.WaitingForBootstrapDataReason}}) + }) + t.Run("Should fail reconcile with create instance failure due to error in retrieving bootstrap data secret", func(t *testing.T) { + var instances = &models.PVMInstances{} + mockclient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects().Build() + machineScope := &scope.PowerVSMachineScope{ + Logger: klogr.New(), + Client: mockclient, + Cluster: &capiv1beta1.Cluster{ + Status: capiv1beta1.ClusterStatus{ + InfrastructureReady: true, + }, + }, + Machine: &capiv1beta1.Machine{ + Spec: capiv1beta1.MachineSpec{ + Bootstrap: capiv1beta1.Bootstrap{ + DataSecretName: pointer.String("data-secret"), + }, + }, + }, + IBMPowerVSMachine: &infrav1beta1.IBMPowerVSMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: *pointer.String("capi-test-machine"), + }, + }, + IBMPowerVSImage: &infrav1beta1.IBMPowerVSImage{ + Status: infrav1beta1.IBMPowerVSImageStatus{ + Ready: true, + }, + }, + IBMPowerVSClient: mockpowervs, + } + mockpowervs.EXPECT().GetAllInstance().Return(instances, nil) + + result, err := reconciler.reconcileNormal(machineScope) + g.Expect(err).To(HaveOccurred()) + g.Expect(result.RequeueAfter).To(BeZero()) + g.Expect(machineScope.IBMPowerVSMachine.Finalizers).To(ContainElement(infrav1beta1.IBMPowerVSMachineFinalizer)) + expectConditions(g, machineScope.IBMPowerVSMachine, []conditionAssertion{{infrav1beta1.InstanceReadyCondition, corev1.ConditionFalse, capiv1beta1.ConditionSeverityError, infrav1beta1.InstanceProvisionFailedReason}}) + }) + t.Run("Should reconcile IBMPowerVSMachine instance creation in different states", func(t *testing.T) { + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + capiv1beta1.ClusterLabelName: "powervs-cluster", + }, + Name: "bootsecret", + Namespace: "default", + }, + Data: map[string][]byte{ + "value": []byte("user data"), + }, + } + createObject(g, secret, "default") + defer cleanupObject(g, secret) + pvsmachine := &infrav1beta1.IBMPowerVSMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: *pointer.String("capi-test-machine"), + }, + Spec: infrav1beta1.IBMPowerVSMachineSpec{ + Memory: "8", + Processors: "0.25", + Image: &infrav1beta1.IBMPowerVSResourceReference{ + ID: pointer.String("capi-image-id"), + }, + Network: infrav1beta1.IBMPowerVSResourceReference{ + ID: pointer.String("capi-net-id"), + }, + ServiceInstanceID: *pointer.String("service-instance-1"), + }, + } + createObject(g, pvsmachine, "default") + defer cleanupObject(g, pvsmachine) + + machine := &capiv1beta1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "owner-machine", + Namespace: "default"}, + Spec: capiv1beta1.MachineSpec{ + Bootstrap: capiv1beta1.Bootstrap{ + DataSecretName: pointer.String("bootsecret"), + }, + }, + } + createObject(g, machine, "default") + defer cleanupObject(g, machine) + + mockclient := fake.NewClientBuilder().WithObjects([]client.Object{secret, pvsmachine, machine}...).Build() + machineScope := &scope.PowerVSMachineScope{ + Logger: klogr.New(), + Client: mockclient, + Cluster: &capiv1beta1.Cluster{ + Status: capiv1beta1.ClusterStatus{ + InfrastructureReady: true, + }, + }, + Machine: machine, + IBMPowerVSMachine: pvsmachine, + IBMPowerVSImage: &infrav1beta1.IBMPowerVSImage{ + Status: infrav1beta1.IBMPowerVSImageStatus{ + Ready: true, + }, + }, + IBMPowerVSClient: mockpowervs, + } + + instanceReferences := &models.PVMInstances{ + PvmInstances: []*models.PVMInstanceReference{ + { + PvmInstanceID: pointer.String("capi-test-machine-id"), + ServerName: pointer.String("capi-test-machine"), + }, + }, + } + instance := &models.PVMInstance{ + PvmInstanceID: pointer.String("capi-test-machine-id"), + ServerName: pointer.String("capi-test-machine"), + Status: pointer.String("BUILD"), + } + + mockpowervs.EXPECT().GetAllInstance().Return(instanceReferences, nil) + mockpowervs.EXPECT().GetInstance(gomock.AssignableToTypeOf("capi-test-machine-id")).Return(instance, nil) + result, err := reconciler.reconcileNormal(machineScope) + g.Expect(err).To(BeNil()) + g.Expect(result.RequeueAfter).To(Not(BeZero())) + g.Expect(machineScope.IBMPowerVSMachine.Status.Ready).To(Equal(false)) + g.Expect(machineScope.IBMPowerVSMachine.Finalizers).To(ContainElement(infrav1beta1.IBMPowerVSMachineFinalizer)) + expectConditions(g, machineScope.IBMPowerVSMachine, []conditionAssertion{{infrav1beta1.InstanceReadyCondition, corev1.ConditionFalse, capiv1beta1.ConditionSeverityWarning, infrav1beta1.InstanceNotReadyReason}}) + + t.Run("When PVM instance is in SHUTOFF state", func(t *testing.T) { + instance.Status = pointer.String("SHUTOFF") + mockpowervs.EXPECT().GetAllInstance().Return(instanceReferences, nil) + mockpowervs.EXPECT().GetInstance(gomock.AssignableToTypeOf("capi-test-machine-id")).Return(instance, nil) + result, err = reconciler.reconcileNormal(machineScope) + g.Expect(err).To(BeNil()) + g.Expect(result.RequeueAfter).To(BeZero()) + g.Expect(machineScope.IBMPowerVSMachine.Status.Ready).To(Equal(false)) + g.Expect(machineScope.IBMPowerVSMachine.Finalizers).To(ContainElement(infrav1beta1.IBMPowerVSMachineFinalizer)) + expectConditions(g, machineScope.IBMPowerVSMachine, []conditionAssertion{{infrav1beta1.InstanceReadyCondition, corev1.ConditionFalse, capiv1beta1.ConditionSeverityError, infrav1beta1.InstanceStoppedReason}}) + }) + t.Run("When PVM instance is in ACTIVE state", func(t *testing.T) { + instance.Status = pointer.String("ACTIVE") + mockpowervs.EXPECT().GetAllInstance().Return(instanceReferences, nil) + mockpowervs.EXPECT().GetInstance(gomock.AssignableToTypeOf("capi-test-machine-id")).Return(instance, nil) + result, err = reconciler.reconcileNormal(machineScope) + g.Expect(err).To(BeNil()) + g.Expect(result.RequeueAfter).To(BeZero()) + g.Expect(machineScope.IBMPowerVSMachine.Status.Ready).To(Equal(true)) + g.Expect(machineScope.IBMPowerVSMachine.Finalizers).To(ContainElement(infrav1beta1.IBMPowerVSMachineFinalizer)) + expectConditions(g, machineScope.IBMPowerVSMachine, []conditionAssertion{{conditionType: infrav1beta1.InstanceReadyCondition, status: corev1.ConditionTrue}}) + }) + t.Run("When PVM instance is in ERROR state", func(t *testing.T) { + instance.Status = pointer.String("ERROR") + instance.Fault = &models.PVMInstanceFault{Details: "Timeout creating instance"} + mockpowervs.EXPECT().GetAllInstance().Return(instanceReferences, nil) + mockpowervs.EXPECT().GetInstance(gomock.AssignableToTypeOf("capi-test-machine-id")).Return(instance, nil) + result, err = reconciler.reconcileNormal(machineScope) + g.Expect(err).To(BeNil()) + g.Expect(result.RequeueAfter).To(BeZero()) + g.Expect(machineScope.IBMPowerVSMachine.Status.Ready).To(Equal(false)) + g.Expect(machineScope.IBMPowerVSMachine.Finalizers).To(ContainElement(infrav1beta1.IBMPowerVSMachineFinalizer)) + expectConditions(g, machineScope.IBMPowerVSMachine, []conditionAssertion{{infrav1beta1.InstanceReadyCondition, corev1.ConditionFalse, capiv1beta1.ConditionSeverityError, infrav1beta1.InstanceErroredReason}}) + }) + t.Run("When PVM instance is in unknown state", func(t *testing.T) { + instance.Status = pointer.String("UNKNOWN") + mockpowervs.EXPECT().GetAllInstance().Return(instanceReferences, nil) + mockpowervs.EXPECT().GetInstance(gomock.AssignableToTypeOf("capi-test-machine-id")).Return(instance, nil) + result, err = reconciler.reconcileNormal(machineScope) + g.Expect(err).To(BeNil()) + g.Expect(result.RequeueAfter).To(Not(BeZero())) + g.Expect(machineScope.IBMPowerVSMachine.Status.Ready).To(Equal(false)) + g.Expect(machineScope.IBMPowerVSMachine.Finalizers).To(ContainElement(infrav1beta1.IBMPowerVSMachineFinalizer)) + expectConditions(g, machineScope.IBMPowerVSMachine, []conditionAssertion{{conditionType: infrav1beta1.InstanceReadyCondition, status: corev1.ConditionUnknown}}) + }) + }) + }) +} + +type conditionAssertion struct { + conditionType capiv1beta1.ConditionType + status corev1.ConditionStatus + severity capiv1beta1.ConditionSeverity + reason string +} + +func expectConditions(g *WithT, m *infrav1beta1.IBMPowerVSMachine, expected []conditionAssertion) { + g.Expect(len(m.Status.Conditions)).To(BeNumerically(">=", len(expected))) + for _, c := range expected { + actual := conditions.Get(m, c.conditionType) + g.Expect(actual).To(Not(BeNil())) + g.Expect(actual.Type).To(Equal(c.conditionType)) + g.Expect(actual.Status).To(Equal(c.status)) + g.Expect(actual.Severity).To(Equal(c.severity)) + g.Expect(actual.Reason).To(Equal(c.reason)) + } +} + +func createObject(g *WithT, obj client.Object, namespace string) { + if obj.DeepCopyObject() != nil { + obj.SetNamespace(namespace) + g.Expect(testEnv.Create(ctx, obj)).To(Succeed()) + } +} + +func cleanupObject(g *WithT, obj client.Object) { + if obj.DeepCopyObject() != nil { + g.Expect(testEnv.Cleanup(ctx, obj)).To(Succeed()) + } +}