From 7150fb859d6083a2d6cdbb5c2105a7b955d20c22 Mon Sep 17 00:00:00 2001 From: Keerthana Arumugam Date: Fri, 9 Feb 2024 15:08:09 +0530 Subject: [PATCH] Scale from and to 0 support for autoscaler for VPC cluster (#1523) * Added support VPC autoscaler * Update controllers/ibmvpcmachinetemplate_controller_test.go Co-authored-by: Prajyot Parab * Removed duplicate and updated error message --------- Co-authored-by: Prajyot Parab --- api/v1beta1/ibmvpc_conversion.go | 4 + api/v1beta1/ibmvpcmachinetemplate_types.go | 9 +- api/v1beta1/zz_generated.conversion.go | 30 +++ api/v1beta1/zz_generated.deepcopy.go | 16 ++ api/v1beta2/ibmvpcmachinetemplate_types.go | 14 +- api/v1beta2/zz_generated.deepcopy.go | 23 ++ ...uster.x-k8s.io_ibmvpcmachinetemplates.yaml | 22 ++ config/rbac/role.yaml | 16 ++ .../ibmvpcmachinetemplate_controller.go | 118 +++++++++ .../ibmvpcmachinetemplate_controller_test.go | 233 ++++++++++++++++++ main.go | 9 + pkg/cloud/services/vpc/mock/vpc_generated.go | 16 ++ pkg/cloud/services/vpc/service.go | 5 + pkg/cloud/services/vpc/vpc.go | 1 + 14 files changed, 514 insertions(+), 2 deletions(-) create mode 100644 controllers/ibmvpcmachinetemplate_controller.go create mode 100644 controllers/ibmvpcmachinetemplate_controller_test.go diff --git a/api/v1beta1/ibmvpc_conversion.go b/api/v1beta1/ibmvpc_conversion.go index 98711ef85..3679f88b4 100644 --- a/api/v1beta1/ibmvpc_conversion.go +++ b/api/v1beta1/ibmvpc_conversion.go @@ -196,6 +196,10 @@ func Convert_v1beta2_IBMVPCMachineSpec_To_v1beta1_IBMVPCMachineSpec(in *infrav1b return autoConvert_v1beta2_IBMVPCMachineSpec_To_v1beta1_IBMVPCMachineSpec(in, out, s) } +func Convert_v1beta2_IBMVPCMachineTemplateStatus_To_v1beta1_IBMVPCMachineTemplateStatus(in *infrav1beta2.IBMVPCMachineTemplateStatus, out *IBMVPCMachineTemplateStatus, s apiconversion.Scope) error { + return autoConvert_v1beta2_IBMVPCMachineTemplateStatus_To_v1beta1_IBMVPCMachineTemplateStatus(in, out, s) +} + func Convert_Slice_Pointer_string_To_Slice_Pointer_v1beta2_IBMVPCResourceReference(in *[]*string, out *[]*infrav1beta2.IBMVPCResourceReference, _ apiconversion.Scope) error { for _, sshKey := range *in { *out = append(*out, &infrav1beta2.IBMVPCResourceReference{ diff --git a/api/v1beta1/ibmvpcmachinetemplate_types.go b/api/v1beta1/ibmvpcmachinetemplate_types.go index 659f1335f..25e42cfd9 100644 --- a/api/v1beta1/ibmvpcmachinetemplate_types.go +++ b/api/v1beta1/ibmvpcmachinetemplate_types.go @@ -31,6 +31,12 @@ type IBMVPCMachineTemplateResource struct { Spec IBMVPCMachineSpec `json:"spec"` } +// IBMVPCMachineTemplateStatus defines the observed state of IBMVPCMachineTemplate. +type IBMVPCMachineTemplateStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + // +kubebuilder:object:root=true // +kubebuilder:resource:path=ibmvpcmachinetemplates,scope=Namespaced,categories=cluster-api @@ -39,7 +45,8 @@ type IBMVPCMachineTemplate struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec IBMVPCMachineTemplateSpec `json:"spec,omitempty"` + Spec IBMVPCMachineTemplateSpec `json:"spec,omitempty"` + Status IBMVPCMachineTemplateStatus `json:"status,omitempty"` } //+kubebuilder:object:root=true diff --git a/api/v1beta1/zz_generated.conversion.go b/api/v1beta1/zz_generated.conversion.go index 308a7de44..764849ee9 100644 --- a/api/v1beta1/zz_generated.conversion.go +++ b/api/v1beta1/zz_generated.conversion.go @@ -344,6 +344,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*IBMVPCMachineTemplateStatus)(nil), (*v1beta2.IBMVPCMachineTemplateStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_IBMVPCMachineTemplateStatus_To_v1beta2_IBMVPCMachineTemplateStatus(a.(*IBMVPCMachineTemplateStatus), b.(*v1beta2.IBMVPCMachineTemplateStatus), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*NetworkInterface)(nil), (*v1beta2.NetworkInterface)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta1_NetworkInterface_To_v1beta2_NetworkInterface(a.(*NetworkInterface), b.(*v1beta2.NetworkInterface), scope) }); err != nil { @@ -444,6 +449,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*v1beta2.IBMVPCMachineTemplateStatus)(nil), (*IBMVPCMachineTemplateStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta2_IBMVPCMachineTemplateStatus_To_v1beta1_IBMVPCMachineTemplateStatus(a.(*v1beta2.IBMVPCMachineTemplateStatus), b.(*IBMVPCMachineTemplateStatus), scope) + }); err != nil { + return err + } if err := s.AddConversionFunc((*v1beta2.VPCLoadBalancerSpec)(nil), (*VPCLoadBalancerSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta2_VPCLoadBalancerSpec_To_v1beta1_VPCLoadBalancerSpec(a.(*v1beta2.VPCLoadBalancerSpec), b.(*VPCLoadBalancerSpec), scope) }); err != nil { @@ -1444,6 +1454,9 @@ func autoConvert_v1beta1_IBMVPCMachineTemplate_To_v1beta2_IBMVPCMachineTemplate( if err := Convert_v1beta1_IBMVPCMachineTemplateSpec_To_v1beta2_IBMVPCMachineTemplateSpec(&in.Spec, &out.Spec, s); err != nil { return err } + if err := Convert_v1beta1_IBMVPCMachineTemplateStatus_To_v1beta2_IBMVPCMachineTemplateStatus(&in.Status, &out.Status, s); err != nil { + return err + } return nil } @@ -1457,6 +1470,9 @@ func autoConvert_v1beta2_IBMVPCMachineTemplate_To_v1beta1_IBMVPCMachineTemplate( if err := Convert_v1beta2_IBMVPCMachineTemplateSpec_To_v1beta1_IBMVPCMachineTemplateSpec(&in.Spec, &out.Spec, s); err != nil { return err } + if err := Convert_v1beta2_IBMVPCMachineTemplateStatus_To_v1beta1_IBMVPCMachineTemplateStatus(&in.Status, &out.Status, s); err != nil { + return err + } return nil } @@ -1555,6 +1571,20 @@ func Convert_v1beta2_IBMVPCMachineTemplateSpec_To_v1beta1_IBMVPCMachineTemplateS return autoConvert_v1beta2_IBMVPCMachineTemplateSpec_To_v1beta1_IBMVPCMachineTemplateSpec(in, out, s) } +func autoConvert_v1beta1_IBMVPCMachineTemplateStatus_To_v1beta2_IBMVPCMachineTemplateStatus(in *IBMVPCMachineTemplateStatus, out *v1beta2.IBMVPCMachineTemplateStatus, s conversion.Scope) error { + return nil +} + +// Convert_v1beta1_IBMVPCMachineTemplateStatus_To_v1beta2_IBMVPCMachineTemplateStatus is an autogenerated conversion function. +func Convert_v1beta1_IBMVPCMachineTemplateStatus_To_v1beta2_IBMVPCMachineTemplateStatus(in *IBMVPCMachineTemplateStatus, out *v1beta2.IBMVPCMachineTemplateStatus, s conversion.Scope) error { + return autoConvert_v1beta1_IBMVPCMachineTemplateStatus_To_v1beta2_IBMVPCMachineTemplateStatus(in, out, s) +} + +func autoConvert_v1beta2_IBMVPCMachineTemplateStatus_To_v1beta1_IBMVPCMachineTemplateStatus(in *v1beta2.IBMVPCMachineTemplateStatus, out *IBMVPCMachineTemplateStatus, s conversion.Scope) error { + // WARNING: in.Capacity requires manual conversion: does not exist in peer-type + return nil +} + func autoConvert_v1beta1_NetworkInterface_To_v1beta2_NetworkInterface(in *NetworkInterface, out *v1beta2.NetworkInterface, s conversion.Scope) error { out.Subnet = in.Subnet return nil diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 1a595196e..3739e49af 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -838,6 +838,7 @@ func (in *IBMVPCMachineTemplate) DeepCopyInto(out *IBMVPCMachineTemplate) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IBMVPCMachineTemplate. @@ -922,6 +923,21 @@ func (in *IBMVPCMachineTemplateSpec) DeepCopy() *IBMVPCMachineTemplateSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IBMVPCMachineTemplateStatus) DeepCopyInto(out *IBMVPCMachineTemplateStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IBMVPCMachineTemplateStatus. +func (in *IBMVPCMachineTemplateStatus) DeepCopy() *IBMVPCMachineTemplateStatus { + if in == nil { + return nil + } + out := new(IBMVPCMachineTemplateStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NetworkInterface) DeepCopyInto(out *NetworkInterface) { *out = *in diff --git a/api/v1beta2/ibmvpcmachinetemplate_types.go b/api/v1beta2/ibmvpcmachinetemplate_types.go index 2d58eef78..3dd623f78 100644 --- a/api/v1beta2/ibmvpcmachinetemplate_types.go +++ b/api/v1beta2/ibmvpcmachinetemplate_types.go @@ -17,6 +17,7 @@ limitations under the License. package v1beta2 import ( + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -31,6 +32,16 @@ type IBMVPCMachineTemplateResource struct { Spec IBMVPCMachineSpec `json:"spec"` } +// IBMVPCMachineTemplateStatus defines the observed state of IBMVPCMachineTemplate. +type IBMVPCMachineTemplateStatus struct { + // Capacity defines the resource capacity for this machine. + // This value is used for autoscaling from zero operations as defined in: + // https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20210310-opt-in-autoscaling-from-zero.md + // +optional + Capacity corev1.ResourceList `json:"capacity,omitempty"` +} + +//+kubebuilder:subresource:status // +kubebuilder:object:root=true // +kubebuilder:resource:path=ibmvpcmachinetemplates,scope=Namespaced,categories=cluster-api // +kubebuilder:storageversion @@ -40,7 +51,8 @@ type IBMVPCMachineTemplate struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec IBMVPCMachineTemplateSpec `json:"spec,omitempty"` + Spec IBMVPCMachineTemplateSpec `json:"spec,omitempty"` + Status IBMVPCMachineTemplateStatus `json:"status,omitempty"` } //+kubebuilder:object:root=true diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index 50a6d4b85..3c8d4e3da 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -1070,6 +1070,7 @@ func (in *IBMVPCMachineTemplate) DeepCopyInto(out *IBMVPCMachineTemplate) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IBMVPCMachineTemplate. @@ -1154,6 +1155,28 @@ func (in *IBMVPCMachineTemplateSpec) DeepCopy() *IBMVPCMachineTemplateSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IBMVPCMachineTemplateStatus) DeepCopyInto(out *IBMVPCMachineTemplateStatus) { + *out = *in + if in.Capacity != nil { + in, out := &in.Capacity, &out.Capacity + *out = make(corev1.ResourceList, len(*in)) + for key, val := range *in { + (*out)[key] = val.DeepCopy() + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IBMVPCMachineTemplateStatus. +func (in *IBMVPCMachineTemplateStatus) DeepCopy() *IBMVPCMachineTemplateStatus { + if in == nil { + return nil + } + out := new(IBMVPCMachineTemplateStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IBMVPCResourceReference) DeepCopyInto(out *IBMVPCResourceReference) { *out = *in diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcmachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcmachinetemplates.yaml index d63c06fa0..d29ea9d4e 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcmachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcmachinetemplates.yaml @@ -147,6 +147,10 @@ spec: required: - template type: object + status: + description: IBMVPCMachineTemplateStatus defines the observed state of + IBMVPCMachineTemplate. + type: object type: object served: true storage: false @@ -296,6 +300,24 @@ spec: required: - template type: object + status: + description: IBMVPCMachineTemplateStatus defines the observed state of + IBMVPCMachineTemplate. + properties: + capacity: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Capacity defines the resource capacity for this machine. + This value is used for autoscaling from zero operations as defined + in: https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20210310-opt-in-autoscaling-from-zero.md' + type: object + type: object type: object served: true storage: true + subresources: + status: {} diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index fbd3a9830..e67f90dab 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -169,3 +169,19 @@ rules: - get - patch - update +- apiGroups: + - infrastructure.cluster.x-k8s.io + resources: + - ibmvpcmachinetemplates + verbs: + - get + - list + - watch +- apiGroups: + - infrastructure.cluster.x-k8s.io + resources: + - ibmvpcmachinetemplates/status + verbs: + - get + - patch + - update diff --git a/controllers/ibmvpcmachinetemplate_controller.go b/controllers/ibmvpcmachinetemplate_controller.go new file mode 100644 index 000000000..58ea594ab --- /dev/null +++ b/controllers/ibmvpcmachinetemplate_controller.go @@ -0,0 +1,118 @@ +/* +Copyright 2023 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 ( + "context" + "fmt" + "reflect" + + "github.com/IBM/vpc-go-sdk/vpcv1" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/runtime" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "sigs.k8s.io/cluster-api/util/patch" + + infrav1beta2 "sigs.k8s.io/cluster-api-provider-ibmcloud/api/v1beta2" + "sigs.k8s.io/cluster-api-provider-ibmcloud/pkg/cloud/services/vpc" + "sigs.k8s.io/cluster-api-provider-ibmcloud/pkg/endpoints" +) + +// IBMVPCMachineTemplateReconciler reconciles a IBMVPCMachineTemplate object. +type IBMVPCMachineTemplateReconciler struct { + client.Client + Scheme *runtime.Scheme + ServiceEndpoint []endpoints.ServiceEndpoint +} + +func (r *IBMVPCMachineTemplateReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&infrav1beta2.IBMVPCMachineTemplate{}). + Complete(r) +} + +// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=ibmvpcmachinetemplates,verbs=get;list;watch +// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=ibmvpcmachinetemplates/status,verbs=get;update;patch + +func (r *IBMVPCMachineTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := ctrl.LoggerFrom(ctx) + log.Info("Reconciling IBMVPCMachineTemplate") + + var machineTemplate infrav1beta2.IBMVPCMachineTemplate + if err := r.Get(ctx, req.NamespacedName, &machineTemplate); err != nil { + log.Error(err, "Unable to fetch ibmvpcmachinetemplate") + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + region := endpoints.CostructRegionFromZone(machineTemplate.Spec.Template.Spec.Zone) + + // Fetch the service endpoint. + svcEndpoint := endpoints.FetchVPCEndpoint(region, r.ServiceEndpoint) + + vpcClient, err := vpc.NewService(svcEndpoint) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to create IBM VPC client: %w", err) + } + + return r.reconcileNormal(ctx, vpcClient, machineTemplate) +} + +func (r *IBMVPCMachineTemplateReconciler) reconcileNormal(ctx context.Context, vpcClient vpc.Vpc, machineTemplate infrav1beta2.IBMVPCMachineTemplate) (ctrl.Result, error) { + log := ctrl.LoggerFrom(ctx) + helper, err := patch.NewHelper(&machineTemplate, r.Client) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to init patch helper: %w", err) + } + + options := &vpcv1.GetInstanceProfileOptions{} + options.SetName(machineTemplate.Spec.Template.Spec.Profile) + profileDetails, _, err := vpcClient.GetInstanceProfile(options) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to fetch profile Details: %w", err) + } + + if profileDetails == nil { + return ctrl.Result{}, fmt.Errorf("failed to find profileDetails") + } + + log.V(3).Info("Profile Details:", "profileDetails", profileDetails) + + capacity := make(corev1.ResourceList) + memory := fmt.Sprintf("%vG", *profileDetails.Memory.(*vpcv1.InstanceProfileMemory).Value) + cpu := fmt.Sprintf("%v", *profileDetails.VcpuCount.(*vpcv1.InstanceProfileVcpu).Value) + capacity[corev1.ResourceCPU] = resource.MustParse(cpu) + capacity[corev1.ResourceMemory] = resource.MustParse(memory) + + log.V(3).Info("Calculated capacity for machine template", "capacity", capacity) + if !reflect.DeepEqual(machineTemplate.Status.Capacity, capacity) { + machineTemplate.Status.Capacity = capacity + if err := helper.Patch(ctx, &machineTemplate); err != nil { + if !apierrors.IsNotFound(err) { + log.Error(err, "Failed to patch machineTemplate") + return ctrl.Result{}, err + } + } + } + log.V(3).Info("Machine template status", "status", machineTemplate.Status) + return ctrl.Result{}, nil +} diff --git a/controllers/ibmvpcmachinetemplate_controller_test.go b/controllers/ibmvpcmachinetemplate_controller_test.go new file mode 100644 index 000000000..dbdc26c99 --- /dev/null +++ b/controllers/ibmvpcmachinetemplate_controller_test.go @@ -0,0 +1,233 @@ +/* +Copyright 2023 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 ( + "fmt" + "reflect" + "testing" + "time" + + "go.uber.org/mock/gomock" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/pointer" + "sigs.k8s.io/cluster-api/util" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + infrav1beta2 "sigs.k8s.io/cluster-api-provider-ibmcloud/api/v1beta2" + "sigs.k8s.io/cluster-api-provider-ibmcloud/pkg/cloud/services/vpc/mock" + + "github.com/IBM/go-sdk-core/v5/core" + "github.com/IBM/vpc-go-sdk/vpcv1" + . "github.com/onsi/gomega" +) + +func TestIBMVPCMachineTemplateReconciler_Reconcile(t *testing.T) { + testCases := []struct { + name string + expectError bool + VPCMachineTemplate *infrav1beta2.IBMVPCMachineTemplate + }{ + { + name: "Should Reconcile successfully if no IBMVPCMachineTemplate found", + expectError: false, + }, + } + for _, tc := range testCases { + setup := func(t *testing.T) (*gomock.Controller, *mock.MockVpc, *IBMVPCMachineTemplateReconciler) { + t.Helper() + mockvpc := mock.NewMockVpc(gomock.NewController(t)) + reconciler := &IBMVPCMachineTemplateReconciler{ + Client: testEnv.Client, + } + return gomock.NewController(t), mockvpc, reconciler + } + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + mockController, _, reconciler := setup(t) + t.Cleanup(mockController.Finish) + 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.VPCMachineTemplate, ns.Name) + defer cleanupObject(g, tc.VPCMachineTemplate) + + if tc.VPCMachineTemplate != nil { + g.Eventually(func() bool { + machineTemplate := &infrav1beta2.IBMVPCMachineTemplate{} + key := client.ObjectKey{ + Name: tc.VPCMachineTemplate.Name, + Namespace: ns.Name, + } + err = testEnv.Get(ctx, key, machineTemplate) + return err == nil + }, 10*time.Second).Should(Equal(true)) + _, err := reconciler.Reconcile(ctx, ctrl.Request{ + NamespacedName: client.ObjectKey{ + Namespace: ns.Name, + Name: tc.VPCMachineTemplate.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 TestIBMVPCMachineTemplateReconciler_reconcileNormal(t *testing.T) { + setup := func(t *testing.T) (*gomock.Controller, *mock.MockVpc, *IBMVPCMachineTemplateReconciler) { + t.Helper() + mockvpc := mock.NewMockVpc(gomock.NewController(t)) + reconciler := &IBMVPCMachineTemplateReconciler{ + Client: testEnv.Client, + } + return gomock.NewController(t), mockvpc, reconciler + } + + t.Run("with valid profile ", func(tt *testing.T) { + g := NewWithT(tt) + var expectedCapacity corev1.ResourceList + profileDetails := vpcv1.InstanceProfile{ + Name: pointer.String("bx2-4x16"), + VcpuCount: &vpcv1.InstanceProfileVcpu{ + Type: pointer.String("fixed"), + Value: pointer.Int64(4), + }, + Memory: &vpcv1.InstanceProfileMemory{ + Type: pointer.String("fixed"), + Value: pointer.Int64(16), + }, + } + ns, err := testEnv.CreateNamespace(ctx, fmt.Sprintf("namespace-%s", util.RandomString(5))) + vPCMachineTemplate := stubVPCMachineTemplate("bx2-4x16") + + expectedCapacity = map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceCPU: resource.MustParse("4"), + corev1.ResourceMemory: resource.MustParse("16G"), + } + createObject(g, &vPCMachineTemplate, ns.Name) + defer cleanupObject(g, &vPCMachineTemplate) + + mockController, mockvpc, reconciler := setup(t) + t.Cleanup(mockController.Finish) + g.Expect(err).To(BeNil()) + defer func() { + g.Expect(testEnv.Cleanup(ctx, ns)).To(Succeed()) + }() + mockvpc.EXPECT().GetInstanceProfile(gomock.AssignableToTypeOf(&vpcv1.GetInstanceProfileOptions{})).Return(&profileDetails, &core.DetailedResponse{}, nil) + _, err = reconciler.reconcileNormal(ctx, mockvpc, vPCMachineTemplate) + if err != nil { + t.Fatalf("ReconcileNormal is not expected to return an error, error: %v", err) + } + g.Expect(err).To(BeNil()) + g.Eventually(func() bool { + machineTemplate := &infrav1beta2.IBMVPCMachineTemplate{} + key := client.ObjectKey{ + Name: vPCMachineTemplate.Name, + Namespace: ns.Name, + } + err = testEnv.Get(ctx, key, machineTemplate) + g.Expect(err).To(BeNil()) + return reflect.DeepEqual(machineTemplate.Status.Capacity, expectedCapacity) + }, 10*time.Second).Should(Equal(true)) + }, + ) + + t.Run("with invalid profile ", func(tt *testing.T) { + g := NewWithT(tt) + ns, err := testEnv.CreateNamespace(ctx, fmt.Sprintf("namespace-%s", util.RandomString(5))) + + vPCMachineTemplate := stubVPCMachineTemplate("") + createObject(g, &vPCMachineTemplate, ns.Name) + defer cleanupObject(g, &vPCMachineTemplate) + + mockController, mockvpc, reconciler := setup(t) + t.Cleanup(mockController.Finish) + g.Expect(err).To(BeNil()) + defer func() { + g.Expect(testEnv.Cleanup(ctx, ns)).To(Succeed()) + }() + mockvpc.EXPECT().GetInstanceProfile(gomock.AssignableToTypeOf(&vpcv1.GetInstanceProfileOptions{})).Return(nil, &core.DetailedResponse{}, nil) + _, err = reconciler.reconcileNormal(ctx, mockvpc, vPCMachineTemplate) + if err == nil { + t.Fatalf("ReconcileNormal is expected to return an error") + } else { + g.Expect(err).NotTo(BeNil()) + } + }, + ) + + t.Run("Error while fetching profile details ", func(tt *testing.T) { + g := NewWithT(tt) + ns, err := testEnv.CreateNamespace(ctx, fmt.Sprintf("namespace-%s", util.RandomString(5))) + + vPCMachineTemplate := stubVPCMachineTemplate("") + createObject(g, &vPCMachineTemplate, ns.Name) + defer cleanupObject(g, &vPCMachineTemplate) + + mockController, mockvpc, reconciler := setup(t) + t.Cleanup(mockController.Finish) + g.Expect(err).To(BeNil()) + defer func() { + g.Expect(testEnv.Cleanup(ctx, ns)).To(Succeed()) + }() + mockvpc.EXPECT().GetInstanceProfile(gomock.AssignableToTypeOf(&vpcv1.GetInstanceProfileOptions{})).Return(nil, nil, fmt.Errorf("intentional error")) + _, err = reconciler.reconcileNormal(ctx, mockvpc, vPCMachineTemplate) + if err == nil { + t.Fatalf("ReconcileNormal is expected to return an error") + } else { + g.Expect(err).NotTo(BeNil()) + } + }, + ) +} + +func stubVPCMachineTemplate(profile string) infrav1beta2.IBMVPCMachineTemplate { + return infrav1beta2.IBMVPCMachineTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vpc-test-1", + }, + Spec: infrav1beta2.IBMVPCMachineTemplateSpec{ + Template: infrav1beta2.IBMVPCMachineTemplateResource{ + Spec: infrav1beta2.IBMVPCMachineSpec{ + Image: &infrav1beta2.IBMVPCResourceReference{ + ID: pointer.String("capi-image"), + }, + Profile: profile, + }, + }, + }, + } +} diff --git a/main.go b/main.go index abae5d7db..bc41f9829 100644 --- a/main.go +++ b/main.go @@ -263,6 +263,15 @@ func setupReconcilers(mgr ctrl.Manager, serviceEndpoint []endpoints.ServiceEndpo setupLog.Error(err, "unable to create controller", "controller", "ibmpowervsmachinetemplate") os.Exit(1) } + + if err := (&controllers.IBMVPCMachineTemplateReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + ServiceEndpoint: serviceEndpoint, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "ibmvpcmachinetemplate") + os.Exit(1) + } } func setupWebhooks(mgr ctrl.Manager) { diff --git a/pkg/cloud/services/vpc/mock/vpc_generated.go b/pkg/cloud/services/vpc/mock/vpc_generated.go index 01e9ce3c5..cdb55ad56 100644 --- a/pkg/cloud/services/vpc/mock/vpc_generated.go +++ b/pkg/cloud/services/vpc/mock/vpc_generated.go @@ -273,6 +273,22 @@ func (mr *MockVpcMockRecorder) GetInstance(options any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInstance", reflect.TypeOf((*MockVpc)(nil).GetInstance), options) } +// GetInstanceProfile mocks base method. +func (m *MockVpc) GetInstanceProfile(options *vpcv1.GetInstanceProfileOptions) (*vpcv1.InstanceProfile, *core.DetailedResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetInstanceProfile", options) + ret0, _ := ret[0].(*vpcv1.InstanceProfile) + ret1, _ := ret[1].(*core.DetailedResponse) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetInstanceProfile indicates an expected call of GetInstanceProfile. +func (mr *MockVpcMockRecorder) GetInstanceProfile(options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInstanceProfile", reflect.TypeOf((*MockVpc)(nil).GetInstanceProfile), options) +} + // GetLoadBalancer mocks base method. func (m *MockVpc) GetLoadBalancer(options *vpcv1.GetLoadBalancerOptions) (*vpcv1.LoadBalancer, *core.DetailedResponse, error) { m.ctrl.T.Helper() diff --git a/pkg/cloud/services/vpc/service.go b/pkg/cloud/services/vpc/service.go index bbe919d84..320c12dde 100644 --- a/pkg/cloud/services/vpc/service.go +++ b/pkg/cloud/services/vpc/service.go @@ -158,6 +158,11 @@ func (s *Service) ListImages(options *vpcv1.ListImagesOptions) (*vpcv1.ImageColl return s.vpcService.ListImages(options) } +// GetInstanceProfile returns instance profile. +func (s *Service) GetInstanceProfile(options *vpcv1.GetInstanceProfileOptions) (*vpcv1.InstanceProfile, *core.DetailedResponse, error) { + return s.vpcService.GetInstanceProfile(options) +} + // NewService returns a new VPC Service. func NewService(svcEndpoint string) (Vpc, error) { service := &Service{} diff --git a/pkg/cloud/services/vpc/vpc.go b/pkg/cloud/services/vpc/vpc.go index 2f5d2893e..d6009bbb0 100644 --- a/pkg/cloud/services/vpc/vpc.go +++ b/pkg/cloud/services/vpc/vpc.go @@ -52,4 +52,5 @@ type Vpc interface { ListLoadBalancerPoolMembers(options *vpcv1.ListLoadBalancerPoolMembersOptions) (*vpcv1.LoadBalancerPoolMemberCollection, *core.DetailedResponse, error) ListKeys(options *vpcv1.ListKeysOptions) (*vpcv1.KeyCollection, *core.DetailedResponse, error) ListImages(options *vpcv1.ListImagesOptions) (*vpcv1.ImageCollection, *core.DetailedResponse, error) + GetInstanceProfile(options *vpcv1.GetInstanceProfileOptions) (*vpcv1.InstanceProfile, *core.DetailedResponse, error) }