From 5ebc177a7c75875e2e63bc79b9054d50b5f2852b Mon Sep 17 00:00:00 2001 From: Milos Tomic <59831542+tmilos77@users.noreply.github.com> Date: Fri, 14 Feb 2025 12:45:58 +0100 Subject: [PATCH] feat: Cceenfsvolume (#1009) --- .../v1beta1/cceenfsvolume_types.go | 85 +++++- ...ources.kyma-project.io_cceenfsvolumes.yaml | 3 + .../cloud-resources/awsnfsvolume_test.go | 2 +- .../cceenfsvolume_controller_test.go | 277 ++++++++++++++++++ .../controller/cloud-resources/suite_test.go | 3 + pkg/composed/conditionFilter.go | 57 ++++ pkg/composed/conditions.go | 38 +++ pkg/composed/state.go | 9 + pkg/composed/updateStatus.go | 10 + pkg/migrateFinalizers/migration.go | 5 + pkg/skr/cceenfsvolume/idGenerate.go | 29 ++ pkg/skr/cceenfsvolume/kcpNfsInstanceCreate.go | 63 ++++ pkg/skr/cceenfsvolume/kcpNfsInstanceDelete.go | 32 ++ pkg/skr/cceenfsvolume/kcpNfsInstanceLoad.go | 33 +++ .../kcpNfsInstanceWaitDeleted.go | 24 ++ pkg/skr/cceenfsvolume/pv_create.go | 79 +++++ pkg/skr/cceenfsvolume/pv_delete.go | 31 ++ pkg/skr/cceenfsvolume/pv_load.go | 26 ++ pkg/skr/cceenfsvolume/pv_removeClaimRef.go | 33 +++ pkg/skr/cceenfsvolume/pv_removeFinalizer.go | 30 ++ pkg/skr/cceenfsvolume/pv_validate.go | 55 ++++ pkg/skr/cceenfsvolume/pv_waitDeleted.go | 24 ++ pkg/skr/cceenfsvolume/pvc_create.go | 62 ++++ pkg/skr/cceenfsvolume/pvc_delete.go | 31 ++ pkg/skr/cceenfsvolume/pvc_load.go | 27 ++ pkg/skr/cceenfsvolume/pvc_removeFinalizer.go | 30 ++ pkg/skr/cceenfsvolume/pvc_validate.go | 54 ++++ pkg/skr/cceenfsvolume/pvc_waitDeleted.go | 24 ++ pkg/skr/cceenfsvolume/reconciler.go | 42 ++- pkg/skr/cceenfsvolume/statusCopy.go | 58 ++++ .../cceenfsvolume/stopIfNotBeingDeleted.go | 14 + pkg/skr/cceenfsvolume/updateSize.go | 56 ++++ .../cceenfsvolume/waitKcpNfsInstanceStatus.go | 26 ++ pkg/testinfra/dsl/cceeNfsVolume.go | 127 ++++++++ pkg/testinfra/dsl/nfsInstance.go | 16 + 35 files changed, 1510 insertions(+), 5 deletions(-) create mode 100644 pkg/composed/conditionFilter.go create mode 100644 pkg/skr/cceenfsvolume/idGenerate.go create mode 100644 pkg/skr/cceenfsvolume/kcpNfsInstanceCreate.go create mode 100644 pkg/skr/cceenfsvolume/kcpNfsInstanceDelete.go create mode 100644 pkg/skr/cceenfsvolume/kcpNfsInstanceLoad.go create mode 100644 pkg/skr/cceenfsvolume/kcpNfsInstanceWaitDeleted.go create mode 100644 pkg/skr/cceenfsvolume/pv_create.go create mode 100644 pkg/skr/cceenfsvolume/pv_delete.go create mode 100644 pkg/skr/cceenfsvolume/pv_load.go create mode 100644 pkg/skr/cceenfsvolume/pv_removeClaimRef.go create mode 100644 pkg/skr/cceenfsvolume/pv_removeFinalizer.go create mode 100644 pkg/skr/cceenfsvolume/pv_validate.go create mode 100644 pkg/skr/cceenfsvolume/pv_waitDeleted.go create mode 100644 pkg/skr/cceenfsvolume/pvc_create.go create mode 100644 pkg/skr/cceenfsvolume/pvc_delete.go create mode 100644 pkg/skr/cceenfsvolume/pvc_load.go create mode 100644 pkg/skr/cceenfsvolume/pvc_removeFinalizer.go create mode 100644 pkg/skr/cceenfsvolume/pvc_validate.go create mode 100644 pkg/skr/cceenfsvolume/pvc_waitDeleted.go create mode 100644 pkg/skr/cceenfsvolume/statusCopy.go create mode 100644 pkg/skr/cceenfsvolume/stopIfNotBeingDeleted.go create mode 100644 pkg/skr/cceenfsvolume/updateSize.go create mode 100644 pkg/skr/cceenfsvolume/waitKcpNfsInstanceStatus.go create mode 100644 pkg/testinfra/dsl/cceeNfsVolume.go diff --git a/api/cloud-resources/v1beta1/cceenfsvolume_types.go b/api/cloud-resources/v1beta1/cceenfsvolume_types.go index 42bb0f764..f8b05b57f 100644 --- a/api/cloud-resources/v1beta1/cceenfsvolume_types.go +++ b/api/cloud-resources/v1beta1/cceenfsvolume_types.go @@ -18,6 +18,7 @@ package v1beta1 import ( featuretypes "github.com/kyma-project/cloud-manager/pkg/feature/types" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -28,6 +29,7 @@ type CceeNfsVolumeSpec struct { IpRange IpRangeRef `json:"ipRange"` // +kubebuilder:validation:Required + // +kubebuilder:validation:XValidation:rule=(self > 0), message="The field capacityGb must be greater than zero" CapacityGb int `json:"capacityGb"` // +optional @@ -55,7 +57,7 @@ type CceeNfsVolumeStatus struct { // +optional // +listType=map // +listMapKey=type - Conditions []metav1.Condition `json:"conditions,omitempty"` + Conditions []metav1.Condition `json:"conditions"` // +optional State string `json:"state,omitempty"` @@ -74,6 +76,70 @@ type CceeNfsVolume struct { Status CceeNfsVolumeStatus `json:"status,omitempty"` } +func (in *CceeNfsVolume) GetPVName() string { + if in.Spec.PersistentVolume != nil && in.Spec.PersistentVolume.Name != "" { + return in.Spec.PersistentVolume.Name + } + return in.Status.Id +} + +func (in *CceeNfsVolume) GetPVLabels() map[string]string { + result := make(map[string]string, 10) + if in.Spec.PersistentVolume != nil && in.Spec.PersistentVolume.Labels != nil { + for k, v := range in.Spec.PersistentVolume.Labels { + result[k] = v + } + } + result[LabelNfsVolName] = in.Name + result[LabelNfsVolNS] = in.Namespace + result[LabelCloudManaged] = "true" + + return result +} + +func (in *CceeNfsVolume) GetPVAnnotations() map[string]string { + if in.Spec.PersistentVolume == nil { + return nil + } + result := make(map[string]string, len(in.Spec.PersistentVolume.Annotations)) + for k, v := range in.Spec.PersistentVolume.Annotations { + result[k] = v + } + return result +} + +func (in *CceeNfsVolume) GetPVCName() string { + if in.Spec.PersistentVolumeClaim != nil && in.Spec.PersistentVolumeClaim.Name != "" { + return in.Spec.PersistentVolumeClaim.Name + } + return in.Name +} + +func (in *CceeNfsVolume) GetPVCLabels() map[string]string { + result := make(map[string]string, 10) + if in.Spec.PersistentVolumeClaim != nil && in.Spec.PersistentVolumeClaim.Labels != nil { + for k, v := range in.Spec.PersistentVolumeClaim.Labels { + result[k] = v + } + } + result[LabelNfsVolName] = in.Name + result[LabelNfsVolNS] = in.Namespace + result[LabelCloudManaged] = "true" + + return result +} + +func (in *CceeNfsVolume) GetPVCAnnotations() map[string]string { + if in.Spec.PersistentVolumeClaim == nil { + return nil + } + result := make(map[string]string, len(in.Spec.PersistentVolumeClaim.Annotations)) + for k, v := range in.Spec.PersistentVolumeClaim.Annotations { + result[k] = v + } + return result +} + func (in *CceeNfsVolume) Conditions() *[]metav1.Condition { return &in.Status.Conditions } @@ -103,7 +169,7 @@ func (in *CceeNfsVolume) SetState(v string) { } func (in *CceeNfsVolume) CloneForPatchStatus() client.Object { - return &CceeNfsVolume{ + result := &CceeNfsVolume{ TypeMeta: metav1.TypeMeta{ Kind: "CceeNfsVolume", APIVersion: GroupVersion.String(), @@ -114,6 +180,21 @@ func (in *CceeNfsVolume) CloneForPatchStatus() client.Object { }, Status: in.Status, } + if result.Status.Conditions == nil { + result.Status.Conditions = []metav1.Condition{} + } + return result +} + +func (in *CceeNfsVolume) DeriveStateFromConditions() (changed bool) { + oldState := in.Status.State + if meta.FindStatusCondition(in.Status.Conditions, ConditionTypeReady) != nil { + in.Status.State = StateReady + } + if meta.FindStatusCondition(in.Status.Conditions, ConditionTypeError) != nil { + in.Status.State = StateError + } + return in.Status.State != oldState } // +kubebuilder:object:root=true diff --git a/config/crd/bases/cloud-resources.kyma-project.io_cceenfsvolumes.yaml b/config/crd/bases/cloud-resources.kyma-project.io_cceenfsvolumes.yaml index 56035cc42..5c29fc6bd 100644 --- a/config/crd/bases/cloud-resources.kyma-project.io_cceenfsvolumes.yaml +++ b/config/crd/bases/cloud-resources.kyma-project.io_cceenfsvolumes.yaml @@ -43,6 +43,9 @@ spec: properties: capacityGb: type: integer + x-kubernetes-validations: + - message: The field capacityGb must be greater than zero + rule: (self > 0) ipRange: properties: name: diff --git a/internal/controller/cloud-resources/awsnfsvolume_test.go b/internal/controller/cloud-resources/awsnfsvolume_test.go index 191d1c93c..d061c26d4 100644 --- a/internal/controller/cloud-resources/awsnfsvolume_test.go +++ b/internal/controller/cloud-resources/awsnfsvolume_test.go @@ -373,7 +373,7 @@ var _ = Describe("Feature: SKR AwsNfsVolume", func() { By("When AwsNfsVolume is deleted", func() { Eventually(Delete). WithArguments(infra.Ctx(), infra.SKR().Client(), awsNfsVolume). - Should(Succeed(), "failed deleting PV") + Should(Succeed(), "failed deleting AwsNfsVolume") }) By("Then SKR AwsNfsVolume has Deleting state", func() { diff --git a/internal/controller/cloud-resources/cceenfsvolume_controller_test.go b/internal/controller/cloud-resources/cceenfsvolume_controller_test.go index c9940f338..b4b32e335 100644 --- a/internal/controller/cloud-resources/cceenfsvolume_controller_test.go +++ b/internal/controller/cloud-resources/cceenfsvolume_controller_test.go @@ -17,12 +17,289 @@ limitations under the License. package cloudresources import ( + "fmt" + "github.com/google/uuid" + "github.com/kyma-project/cloud-manager/api" + cloudcontrolv1beta1 "github.com/kyma-project/cloud-manager/api/cloud-control/v1beta1" + cloudresourcesv1beta1 "github.com/kyma-project/cloud-manager/api/cloud-resources/v1beta1" + skriprange "github.com/kyma-project/cloud-manager/pkg/skr/iprange" + . "github.com/kyma-project/cloud-manager/pkg/testinfra/dsl" + "github.com/kyma-project/cloud-manager/pkg/util" . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/api/resource" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) var _ = Describe("Feature: SKR CceeNfsVolume", func() { + newRandomMapStringString := func() map[string]string { + return map[string]string{ + uuid.NewString(): uuid.NewString()[0:8], + } + } + It("Scenario: SKR CceeNfsVolume is created with empty IpRange when default IpRange does not exist", func() { + cceeNfsVolumeName := "d2859451-39ed-4cc5-bf6d-d04aa8feeb5b" + skrIpRangeId := "cddae623-d665-4dae-9899-515d1c2e1418" + cceeNfsVolume := &cloudresourcesv1beta1.CceeNfsVolume{} + kcpNfsInstance := &cloudcontrolv1beta1.NfsInstance{} + skrIpRange := &cloudresourcesv1beta1.IpRange{} + capacityGb := 100 + + pv := &corev1.PersistentVolume{} + pvc := &corev1.PersistentVolumeClaim{} + + pvLabels := newRandomMapStringString() + pvAnnotations := newRandomMapStringString() + pvcLabels := newRandomMapStringString() + pvcAnnotations := newRandomMapStringString() + + skriprange.Ignore.AddName("default") + + By("Given default SKR IpRange does not exist", func() { + Consistently(LoadAndCheck). + WithArguments(infra.Ctx(), infra.SKR().Client(), skrIpRange, + NewObjActions(WithName("default"), WithNamespace("kyma-system"))). + ShouldNot(Succeed()) + }) + + By("When CceeNfsVolume is created with empty IpRange", func() { + Eventually(CreateCceeNfsVolume). + WithArguments( + infra.Ctx(), infra.SKR().Client(), cceeNfsVolume, + WithName(cceeNfsVolumeName), + WithCceeNfsVolumeCapacity(capacityGb), + WithCceeNfsVolumePvLabels(pvLabels), + WithCceeNfsVolumePvAnnotations(pvAnnotations), + WithCceeNfsVolumePvcLabels(pvcLabels), + WithCceeNfsVolumePvcAnnotations(pvcAnnotations), + ). + Should(Succeed()) + }) + + By("Then default SKR IpRange is created", func() { + Eventually(LoadAndCheck). + WithArguments(infra.Ctx(), infra.SKR().Client(), skrIpRange, + NewObjActions(WithName("default"), WithNamespace("kyma-system"))). + Should(Succeed()) + }) + + By("And Then CceeNfsVolume is not ready", func() { + Eventually(LoadAndCheck). + WithArguments(infra.Ctx(), infra.SKR().Client(), cceeNfsVolume, NewObjActions()). + Should(Succeed()) + Expect(meta.IsStatusConditionTrue(cceeNfsVolume.Status.Conditions, cloudresourcesv1beta1.ConditionTypeReady)). + To(BeFalse(), "expected CceeNfsVolume not to have Ready condition, but it has") + }) + + By("When default SKR IpRange has Ready condition", func() { + Eventually(UpdateStatus). + WithArguments( + infra.Ctx(), infra.SKR().Client(), skrIpRange, + WithSkrIpRangeStatusId(skrIpRangeId), + WithConditions(SkrReadyCondition()), + ). + Should(Succeed()) + }) + + By("Then KCP NfsInstance is created", func() { + // load SKR CceeNfsVolume to get ID + Eventually(LoadAndCheck). + WithArguments( + infra.Ctx(), + infra.SKR().Client(), + cceeNfsVolume, + NewObjActions(), + HavingCceeNfsVolumeStatusId(), + HavingCceeNfsVolumeStatusState(cloudresourcesv1beta1.StateCreating), + ). + Should(Succeed(), "expected SKR CceeNfsVolume to get status.id and status creating") + + Eventually(LoadAndCheck). + WithArguments( + infra.Ctx(), + infra.KCP().Client(), + kcpNfsInstance, + NewObjActions( + WithName(cceeNfsVolume.Status.Id), + ), + ). + Should(Succeed()) + + Eventually(Update). + WithArguments(infra.Ctx(), infra.KCP().Client(), kcpNfsInstance, AddFinalizer(api.CommonFinalizerDeletionHook)). + Should(Succeed(), "failed adding finalizer on KCP NfsInstance") + }) + + By("When KCP NfsInstance has Ready condition", func() { + Eventually(UpdateStatus). + WithArguments( + infra.Ctx(), + infra.KCP().Client(), + kcpNfsInstance, + WithNfsInstanceStatusHost(""), + WithNfsInstanceStatusPath(""), + WithConditions(KcpReadyCondition()), + ). + Should(Succeed()) + }) + + By("Then SKR PersistentVolume is created", func() { + Eventually(LoadAndCheck). + WithArguments( + infra.Ctx(), + infra.SKR().Client(), + pv, + NewObjActions(WithName(cceeNfsVolume.Status.Id)), + ). + Should(Succeed()) + }) + + By("And Then SKR PersistentVolumeClaim is created", func() { + Eventually(LoadAndCheck). + WithArguments( + infra.Ctx(), + infra.SKR().Client(), + pvc, + NewObjActions( + WithName(cceeNfsVolume.Name), + WithNamespace(cceeNfsVolume.Namespace), + ), + ). + Should(Succeed()) + }) + + By("And Then SKR CceeNfsVolume has Ready condition", func() { + Eventually(LoadAndCheck). + WithArguments( + infra.Ctx(), + infra.SKR().Client(), + cceeNfsVolume, + NewObjActions(), + HavingConditionTrue(cloudresourcesv1beta1.ConditionTypeReady), + HavingCceeNfsVolumeStatusState(cloudresourcesv1beta1.StateReady), + ). + Should(Succeed()) + }) + + // PV assertions =============================================================== + + By("And Then SKR PersistentVolume has storage capacity equal to CceeNfsVolume capacity", func() { + expected := resource.MustParse(fmt.Sprintf("%dG", cceeNfsVolume.Spec.CapacityGb)) + Expect(expected.Equal(pv.Spec.Capacity["storage"])).To(BeTrue()) + }) + + By("And Then SKR PersistentVolume ReadWriteMany access", func() { + Expect(pv.Spec.AccessModes).To(HaveLen(1)) + Expect(pv.Spec.AccessModes[0]).To(Equal(corev1.ReadWriteMany)) + }) + + By("And Then SKR PersistentVolume has well-known CloudManager labels", func() { + Expect(pv.Labels[util.WellKnownK8sLabelComponent]).To(Equal(util.DefaultCloudManagerComponentLabelValue)) + Expect(pv.Labels[util.WellKnownK8sLabelPartOf]).To(Equal(util.DefaultCloudManagerPartOfLabelValue)) + Expect(pv.Labels[util.WellKnownK8sLabelManagedBy]).To(Equal(util.DefaultCloudManagerManagedByLabelValue)) + }) + + By("And Then SKR PersistentVolume has parent NFS volume label", func() { + Expect(pv.Labels[cloudresourcesv1beta1.LabelNfsVolName]).To(Equal(cceeNfsVolume.Name)) + Expect(pv.Labels[cloudresourcesv1beta1.LabelNfsVolNS]).To(Equal(cceeNfsVolume.Namespace)) + }) + + By("And Then SKR PersistentVolume has user defined labels", func() { + for k, v := range pvLabels { + Expect(pv.Labels[k]).To(Equal(v)) + } + }) + + By("And Then SKR PersistentVolume has user defined annotations", func() { + for k, v := range pvAnnotations { + Expect(pv.Annotations[k]).To(Equal(v)) + } + }) + + By("And Then SKR PersistentVolume has finalizer", func() { + Expect(controllerutil.ContainsFinalizer(pv, api.CommonFinalizerDeletionHook)).To(BeTrue()) + }) + + By("And Then SKR PersistentVolume has NFS host and path equal to KCP NfsInstance values", func() { + Expect(pv.Spec.PersistentVolumeSource.NFS).NotTo(BeNil()) + Expect(pv.Spec.PersistentVolumeSource.NFS.Server).To(Equal(kcpNfsInstance.Status.Host)) + Expect(pv.Spec.PersistentVolumeSource.NFS.Path).To(Equal(kcpNfsInstance.Status.Path)) + }) + + // PVC assertions =============================================================== + + By("And Then SKR PersistentVolumeClaim has well-known CloudManager labels", func() { + Expect(pvc.Labels[util.WellKnownK8sLabelComponent]).To(Equal(util.DefaultCloudManagerComponentLabelValue)) + Expect(pvc.Labels[util.WellKnownK8sLabelPartOf]).To(Equal(util.DefaultCloudManagerPartOfLabelValue)) + Expect(pvc.Labels[util.WellKnownK8sLabelManagedBy]).To(Equal(util.DefaultCloudManagerManagedByLabelValue)) + }) + + By("And Then SKR PersistentVolumeClaim has user defined labels", func() { + for k, v := range pvcLabels { + Expect(pvc.Labels[k]).To(Equal(v)) + } + }) + + By("And Then SKR PersistentVolumeClaim has user defined annotations", func() { + for k, v := range pvcAnnotations { + Expect(pvc.Annotations[k]).To(Equal(v)) + } + }) + + By("And Then SKR PersistentVolumeClaim has finalizer", func() { + Expect(controllerutil.ContainsFinalizer(pvc, api.CommonFinalizerDeletionHook)).To(BeTrue()) + }) + + By("And Then SKR PersistentVolumeClaim references PersistentVolume", func() { + Expect(pvc.Spec.VolumeName).To(Equal(pv.Name)) + }) + + // DELETE =============================================================== + + By("When CceeNfsVolume is deleted", func() { + Eventually(Delete). + WithArguments(infra.Ctx(), infra.SKR().Client(), cceeNfsVolume). + Should(Succeed(), "failed deleting CceeNfsVolume") + }) + + By("Then SKR PersistentVolumeClaim is deleted", func() { + Eventually(IsDeleted). + WithArguments(infra.Ctx(), infra.SKR().Client(), pvc). + Should(Succeed(), "expected PVC not to exist") + }) + + By("And Then SKR PersistentVolume is deleted", func() { + Eventually(IsDeleted). + WithArguments(infra.Ctx(), infra.SKR().Client(), pv). + Should(Succeed(), "expected PV not to exist") + }) + + By("And Then KCP NfsInstance is marked for deletion", func() { + Eventually(LoadAndCheck). + WithArguments(infra.Ctx(), infra.KCP().Client(), kcpNfsInstance, NewObjActions(), HavingDeletionTimestamp()). + Should(Succeed(), "expected KCP NfsInstance to be marked for deletion") + }) + + By("When KCP NfsInstance finalizer is removed and it is deleted", func() { + Eventually(Update). + WithArguments(infra.Ctx(), infra.KCP().Client(), kcpNfsInstance, RemoveFinalizer(api.CommonFinalizerDeletionHook)). + Should(Succeed(), "failed removing finalizer on KCP NfsInstance") + }) + + By("Then SKR CceeNfsVolume is deleted", func() { + Eventually(IsDeleted). + WithArguments(infra.Ctx(), infra.SKR().Client(), cceeNfsVolume). + Should(Succeed(), "expected CceeNfsVolume not to exist") + }) + By("// cleanup SKR IpRange", func() { + Eventually(Delete). + WithArguments(infra.Ctx(), infra.SKR().Client(), skrIpRange). + Should(Succeed()) + }) }) }) diff --git a/internal/controller/cloud-resources/suite_test.go b/internal/controller/cloud-resources/suite_test.go index 7bc1f7c72..20cc2efaa 100644 --- a/internal/controller/cloud-resources/suite_test.go +++ b/internal/controller/cloud-resources/suite_test.go @@ -18,6 +18,7 @@ package cloudresources import ( "context" + "github.com/kyma-project/cloud-manager/pkg/migrateFinalizers" "os" "testing" @@ -135,6 +136,8 @@ var _ = BeforeSuite(func() { //GCP Vpc Peering Expect(SetupGcpVpcPeeringReconciler(infra.Registry())).NotTo(HaveOccurred()) + migrateFinalizers.RunMigration = false + // Start controllers infra.StartSkrControllers(context.Background()) }) diff --git a/pkg/composed/conditionFilter.go b/pkg/composed/conditionFilter.go new file mode 100644 index 000000000..c00909855 --- /dev/null +++ b/pkg/composed/conditionFilter.go @@ -0,0 +1,57 @@ +package composed + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ConditionPredicate func(condition metav1.Condition) bool + +func NewFilterAllowedConditionsPredicate(allowedTypes ...string) ConditionPredicate { + return func(condition metav1.Condition) bool { + for _, allowedType := range allowedTypes { + if condition.Type == allowedType { + return true + } + } + return false + } +} + +func NewFilterProhibitedConditionsPredicate(prohibitedTypes ...string) ConditionPredicate { + return func(condition metav1.Condition) bool { + for _, allowedType := range prohibitedTypes { + if condition.Type == allowedType { + return false + } + } + return true + } +} + +type ConditionFilterWrapper interface { + ObjWithConditions + Inner() ObjWithConditions +} + +type conditionFilter struct { + ObjWithConditions + filter ConditionPredicate +} + +func (f *conditionFilter) Inner() ObjWithConditions { + return f.ObjWithConditions +} + +func FilterAllowedConditions(inner ObjWithConditions, allowedConditionTypes ...string) ConditionFilterWrapper { + return &conditionFilter{ + ObjWithConditions: inner, + filter: NewFilterAllowedConditionsPredicate(allowedConditionTypes...), + } +} + +func FilterProhibitedConditions(inner ObjWithConditions, prohibitedConditionTypes ...string) ConditionFilterWrapper { + return &conditionFilter{ + ObjWithConditions: inner, + filter: NewFilterProhibitedConditionsPredicate(prohibitedConditionTypes...), + } +} diff --git a/pkg/composed/conditions.go b/pkg/composed/conditions.go index c813971fc..82e7068cf 100644 --- a/pkg/composed/conditions.go +++ b/pkg/composed/conditions.go @@ -21,3 +21,41 @@ func HasCondition(desired metav1.Condition, existingConditions []metav1.Conditio } return true } + +// StatusCopyConditionsAndState copies conditions from source to the destination. If destination +// has a condition that source doesn't have it will be removed. If both +// implement ObjWithConditionsAndState it will also copy status.state. +func StatusCopyConditionsAndState(source ObjWithConditions, destination ObjWithConditions) (changed bool, addedConditions []string, removedConditions []string, newState string) { + changed = false + for _, srcCond := range *source.Conditions() { + added := meta.SetStatusCondition(destination.Conditions(), metav1.Condition{ + Type: srcCond.Type, + Status: srcCond.Status, + Reason: srcCond.Reason, + Message: srcCond.Message, + }) + if added { + changed = true + addedConditions = append(addedConditions, srcCond.Type) + } + } + for _, dstCond := range *destination.Conditions() { + if !HasCondition(dstCond, *source.Conditions()) { + changed = true + meta.RemoveStatusCondition(destination.Conditions(), dstCond.Type) + removedConditions = append(removedConditions, dstCond.Type) + } + } + + if srcWStatus, srcOk := source.(ObjWithConditionsAndState); srcOk { + if dstWStatus, dstOk := destination.(ObjWithConditionsAndState); dstOk { + if srcWStatus.State() != "" && srcWStatus.State() != dstWStatus.State() { + changed = true + dstWStatus.SetState(srcWStatus.State()) + } + newState = srcWStatus.State() + } + } + + return +} diff --git a/pkg/composed/state.go b/pkg/composed/state.go index a0326c8db..8b1e8f34f 100644 --- a/pkg/composed/state.go +++ b/pkg/composed/state.go @@ -2,6 +2,7 @@ package composed import ( "context" + "encoding/json" "fmt" "github.com/kyma-project/cloud-manager/pkg/common" "k8s.io/apimachinery/pkg/runtime" @@ -183,6 +184,14 @@ func (s *baseState) PatchObjRemoveFinalizer(ctx context.Context, f string) (bool return PatchObjRemoveFinalizer(ctx, f, s.Obj(), s.Cluster().K8sClient()) } +func MergePatchObj(ctx context.Context, obj client.Object, patch map[string]interface{}, clnt client.Writer) error { + p, err := json.Marshal(patch) + if err != nil { + return fmt.Errorf("error json patching object when marshaling given patch: %w", err) + } + return clnt.Patch(ctx, obj, client.RawPatch(types.MergePatchType, p)) +} + func PatchObjStatus(ctx context.Context, obj client.Object, clnt client.StatusClient) error { objToPatch := obj if objClonable, ok := obj.(ObjWithCloneForPatchStatus); ok { diff --git a/pkg/composed/updateStatus.go b/pkg/composed/updateStatus.go index 76edf9f83..2e37efa7a 100644 --- a/pkg/composed/updateStatus.go +++ b/pkg/composed/updateStatus.go @@ -27,11 +27,21 @@ type ObjWithConditions interface { GetObjectMeta() *metav1.ObjectMeta } +type ObjWithCloneForPatch interface { + ObjWithConditions + CloneForPatch() client.Object +} + type ObjWithCloneForPatchStatus interface { ObjWithConditions CloneForPatchStatus() client.Object } +type ObjWithDeriveStateFromConditions interface { + ObjWithConditions + DeriveStateFromConditions() (changed bool) +} + func PatchStatus(obj ObjWithConditions) *UpdateStatusBuilder { return &UpdateStatusBuilder{ applyType: applyServerSide, diff --git a/pkg/migrateFinalizers/migration.go b/pkg/migrateFinalizers/migration.go index 692d920d7..76cf0411b 100644 --- a/pkg/migrateFinalizers/migration.go +++ b/pkg/migrateFinalizers/migration.go @@ -6,6 +6,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) +var RunMigration = true + type Migration interface { Run(ctx context.Context) (alreadyExecuted bool, err error) } @@ -20,6 +22,9 @@ type migration struct { } func (m *migration) Run(ctx context.Context) (alreadyExecuted bool, err error) { + if !RunMigration { + return false, nil + } isRecorded, err := m.successHandler.IsRecorded(ctx) if err != nil { return false, err diff --git a/pkg/skr/cceenfsvolume/idGenerate.go b/pkg/skr/cceenfsvolume/idGenerate.go new file mode 100644 index 000000000..a6a8e4d74 --- /dev/null +++ b/pkg/skr/cceenfsvolume/idGenerate.go @@ -0,0 +1,29 @@ +package cceenfsvolume + +import ( + "context" + "github.com/google/uuid" + cloudresourcesv1beta1 "github.com/kyma-project/cloud-manager/api/cloud-resources/v1beta1" + "github.com/kyma-project/cloud-manager/pkg/composed" +) + +func idGenerate(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + + if state.ObjAsCceeNfsVolume().Status.Id != "" { + return nil, ctx + } + + state.ObjAsCceeNfsVolume().Status.Id = uuid.NewString() + + if len(state.ObjAsCceeNfsVolume().Status.State) == 0 { + state.ObjAsCceeNfsVolume().Status.State = cloudresourcesv1beta1.StateCreating + } + + err := composed.PatchObjStatus(ctx, state.ObjAsCceeNfsVolume(), state.Cluster().K8sClient()) + if err != nil { + return composed.LogErrorAndReturn(err, "Error patching CceeNfsVolume with status.id", composed.StopWithRequeue, ctx) + } + + return nil, ctx +} diff --git a/pkg/skr/cceenfsvolume/kcpNfsInstanceCreate.go b/pkg/skr/cceenfsvolume/kcpNfsInstanceCreate.go new file mode 100644 index 000000000..b628b81a4 --- /dev/null +++ b/pkg/skr/cceenfsvolume/kcpNfsInstanceCreate.go @@ -0,0 +1,63 @@ +package cceenfsvolume + +import ( + "context" + cloudcontrolv1beta1 "github.com/kyma-project/cloud-manager/api/cloud-control/v1beta1" + cloudresourcesv1beta1 "github.com/kyma-project/cloud-manager/api/cloud-resources/v1beta1" + "github.com/kyma-project/cloud-manager/pkg/composed" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func kcpNfsInstanceCreate(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + logger := composed.LoggerFromCtx(ctx) + + if composed.IsMarkedForDeletion(state.Obj()) { + return nil, ctx + } + if state.KcpNfsInstance != nil { + return nil, ctx + } + + state.KcpNfsInstance = &cloudcontrolv1beta1.NfsInstance{ + ObjectMeta: metav1.ObjectMeta{ + Name: state.ObjAsCceeNfsVolume().Status.Id, + Namespace: state.KymaRef.Namespace, + Labels: map[string]string{ + cloudcontrolv1beta1.LabelKymaName: state.KymaRef.Name, + cloudcontrolv1beta1.LabelRemoteName: state.ObjAsCceeNfsVolume().Name, + cloudcontrolv1beta1.LabelRemoteNamespace: state.ObjAsCceeNfsVolume().Namespace, + }, + }, + Spec: cloudcontrolv1beta1.NfsInstanceSpec{ + RemoteRef: cloudcontrolv1beta1.RemoteRef{ + Namespace: state.ObjAsCceeNfsVolume().Name, + Name: state.ObjAsCceeNfsVolume().Namespace, + }, + // IpRange can not be set for CCEE + Scope: cloudcontrolv1beta1.ScopeRef{ + Name: state.KymaRef.Name, + }, + Instance: cloudcontrolv1beta1.NfsInstanceInfo{ + OpenStack: &cloudcontrolv1beta1.NfsInstanceOpenStack{ + SizeGb: state.ObjAsCceeNfsVolume().Spec.CapacityGb, + }, + }, + }, + } + + err := state.KcpCluster.K8sClient().Create(ctx, state.KcpNfsInstance) + if err != nil { + return composed.LogErrorAndReturn(err, "Error creating KCP NfsInstance for CceeNfsVolume", composed.StopWithRequeue, ctx) + } + + logger.Info("Created KCP NfsInstance for CceeNfsVolume") + + state.ObjAsCceeNfsVolume().Status.State = cloudresourcesv1beta1.StateCreating + + return composed.PatchStatus(state.ObjAsCceeNfsVolume()). + FailedError(composed.StopWithRequeue). + ErrorLogMessage("Error patching status for CceeNfsVolume with creating state"). + SuccessErrorNil(). + Run(ctx, state) +} diff --git a/pkg/skr/cceenfsvolume/kcpNfsInstanceDelete.go b/pkg/skr/cceenfsvolume/kcpNfsInstanceDelete.go new file mode 100644 index 000000000..555d558aa --- /dev/null +++ b/pkg/skr/cceenfsvolume/kcpNfsInstanceDelete.go @@ -0,0 +1,32 @@ +package cceenfsvolume + +import ( + "context" + "github.com/kyma-project/cloud-manager/pkg/composed" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func kcpNfsInstanceDelete(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + logger := composed.LoggerFromCtx(ctx) + + if !composed.IsMarkedForDeletion(state.ObjAsCceeNfsVolume()) { + return nil, ctx + } + + if state.KcpNfsInstance == nil { + return nil, ctx + } + if composed.IsMarkedForDeletion(state.KcpNfsInstance) { + return nil, ctx + } + + logger.Info("Deleting KCP NfsInstance for CceeNfsVolume") + + err := state.KcpCluster.K8sClient().Delete(ctx, state.KcpNfsInstance) + if client.IgnoreNotFound(err) != nil { + return composed.LogErrorAndReturn(err, "Error deleting KCP NfsInstance for CceeNfsVolume", composed.StopWithRequeue, ctx) + } + + return nil, ctx +} diff --git a/pkg/skr/cceenfsvolume/kcpNfsInstanceLoad.go b/pkg/skr/cceenfsvolume/kcpNfsInstanceLoad.go new file mode 100644 index 000000000..74db6a441 --- /dev/null +++ b/pkg/skr/cceenfsvolume/kcpNfsInstanceLoad.go @@ -0,0 +1,33 @@ +package cceenfsvolume + +import ( + "context" + "errors" + cloudcontrolv1beta1 "github.com/kyma-project/cloud-manager/api/cloud-control/v1beta1" + "github.com/kyma-project/cloud-manager/pkg/composed" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func kcpNfsInstanceLoad(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + + if state.ObjAsCceeNfsVolume().Status.Id == "" { + return composed.LogErrorAndReturn(errors.New("Missing CceeNfsVolume status.id"), "Logical error", composed.StopAndForget, ctx) + } + + kcpNfsInstnace := &cloudcontrolv1beta1.NfsInstance{} + + err := state.KcpCluster.K8sClient().Get(ctx, types.NamespacedName{ + Namespace: state.KymaRef.Namespace, + Name: state.ObjAsCceeNfsVolume().Status.Id, + }, kcpNfsInstnace) + if client.IgnoreNotFound(err) != nil { + return composed.LogErrorAndReturn(err, "Error loading KCP NfsInstance for CceeNfsVolume", composed.StopWithRequeue, ctx) + } + if err == nil { + state.KcpNfsInstance = kcpNfsInstnace + } + + return nil, ctx +} diff --git a/pkg/skr/cceenfsvolume/kcpNfsInstanceWaitDeleted.go b/pkg/skr/cceenfsvolume/kcpNfsInstanceWaitDeleted.go new file mode 100644 index 000000000..94d99285a --- /dev/null +++ b/pkg/skr/cceenfsvolume/kcpNfsInstanceWaitDeleted.go @@ -0,0 +1,24 @@ +package cceenfsvolume + +import ( + "context" + "github.com/kyma-project/cloud-manager/pkg/composed" + "github.com/kyma-project/cloud-manager/pkg/util" +) + +func kcpNfsInstanceWaitDeleted(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + logger := composed.LoggerFromCtx(ctx) + + if !composed.IsMarkedForDeletion(state.Obj()) { + return nil, ctx + } + + if state.KcpNfsInstance == nil { + return nil, ctx + } + + logger.Info("Waiting for KCP NfsInstance for CceeNfsVolume to get deleted") + + return composed.StopWithRequeueDelay(util.Timing.T10000ms()), ctx +} diff --git a/pkg/skr/cceenfsvolume/pv_create.go b/pkg/skr/cceenfsvolume/pv_create.go new file mode 100644 index 000000000..5326b6f5f --- /dev/null +++ b/pkg/skr/cceenfsvolume/pv_create.go @@ -0,0 +1,79 @@ +package cceenfsvolume + +import ( + "context" + "fmt" + "github.com/kyma-project/cloud-manager/api" + cloudresourcesv1beta1 "github.com/kyma-project/cloud-manager/api/cloud-resources/v1beta1" + "github.com/kyma-project/cloud-manager/pkg/composed" + "github.com/kyma-project/cloud-manager/pkg/util" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "strings" +) + +func pvCreate(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + + if composed.IsMarkedForDeletion(state.Obj()) { + return nil, ctx + } + + if state.PV != nil { + return nil, ctx + } + + storageSize, err := resource.ParseQuantity(fmt.Sprintf("%dG", state.ObjAsCceeNfsVolume().Spec.CapacityGb)) + if err != nil { + return composed.LogErrorAndReturn(err, "Error parsing CceeNfsVolume capacity as resource quantity", composed.StopAndForget, ctx) + } + + path := state.KcpNfsInstance.Status.Path + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + + pv := &corev1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: state.ObjAsCceeNfsVolume().GetPVName(), + Labels: util.NewLabelBuilder(). + WithCustomLabels(state.ObjAsCceeNfsVolume().GetPVLabels()). + WithCustomLabel(cloudresourcesv1beta1.LabelNfsVolName, state.ObjAsCceeNfsVolume().Name). + WithCustomLabel(cloudresourcesv1beta1.LabelNfsVolNS, state.ObjAsCceeNfsVolume().Namespace). + WithCloudManagerDefaults(). + Build(), + Annotations: state.ObjAsCceeNfsVolume().GetPVAnnotations(), + Finalizers: []string{ + api.CommonFinalizerDeletionHook, + }, + }, + Spec: corev1.PersistentVolumeSpec{ + Capacity: corev1.ResourceList{ + "storage": storageSize, + }, + PersistentVolumeSource: corev1.PersistentVolumeSource{ + NFS: &corev1.NFSVolumeSource{ + Server: state.KcpNfsInstance.Status.Host, + Path: path, + ReadOnly: false, + }, + }, + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteMany}, + }, + } + + err = state.Cluster().K8sClient().Create(ctx, pv) + if err != nil { + return composed.LogErrorAndReturn(err, "Error creating PV for CceeNfsVolume", composed.StopWithRequeue, ctx) + } + + logger := composed.LoggerFromCtx(ctx) + logger. + WithValues("pvName", pv.Name). + Info("Created PV for CceeNfsVolume") + + state.PV = pv + + return nil, ctx +} diff --git a/pkg/skr/cceenfsvolume/pv_delete.go b/pkg/skr/cceenfsvolume/pv_delete.go new file mode 100644 index 000000000..67546049e --- /dev/null +++ b/pkg/skr/cceenfsvolume/pv_delete.go @@ -0,0 +1,31 @@ +package cceenfsvolume + +import ( + "context" + "github.com/kyma-project/cloud-manager/pkg/composed" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func pvDelete(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + logger := composed.LoggerFromCtx(ctx) + + if !composed.IsMarkedForDeletion(state.Obj()) { + return nil, ctx + } + + if state.PV == nil { + return nil, ctx + } + + err := state.Cluster().K8sClient().Delete(ctx, state.PV) + if client.IgnoreNotFound(err) != nil { + return composed.LogErrorAndReturn(err, "Error deleting CceeNfsVolume PV", composed.StopWithRequeue, ctx) + } + + if err != nil { + logger.Info("Deleted CceeNfsVolume PV") + } + + return nil, ctx +} diff --git a/pkg/skr/cceenfsvolume/pv_load.go b/pkg/skr/cceenfsvolume/pv_load.go new file mode 100644 index 000000000..63f33a6e9 --- /dev/null +++ b/pkg/skr/cceenfsvolume/pv_load.go @@ -0,0 +1,26 @@ +package cceenfsvolume + +import ( + "context" + "github.com/kyma-project/cloud-manager/pkg/composed" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func pvLoad(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + + vol := &corev1.PersistentVolume{} + err := state.Cluster().K8sClient().Get(ctx, client.ObjectKey{ + Name: state.ObjAsCceeNfsVolume().GetPVName(), + }, vol) + if client.IgnoreNotFound(err) != nil { + return composed.LogErrorAndReturn(err, "Error loading PV for CceeNfsVolume", composed.StopWithRequeue, ctx) + } + + if err == nil { + state.PV = vol + } + + return nil, ctx +} diff --git a/pkg/skr/cceenfsvolume/pv_removeClaimRef.go b/pkg/skr/cceenfsvolume/pv_removeClaimRef.go new file mode 100644 index 000000000..b611ade72 --- /dev/null +++ b/pkg/skr/cceenfsvolume/pv_removeClaimRef.go @@ -0,0 +1,33 @@ +package cceenfsvolume + +import ( + "context" + "github.com/kyma-project/cloud-manager/pkg/composed" + "github.com/kyma-project/cloud-manager/pkg/util" + corev1 "k8s.io/api/core/v1" +) + +func pvRemoveClaimRef(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + + if composed.IsMarkedForDeletion(state.Obj()) { + return nil, ctx + } + if state.PV == nil { + return nil, ctx + } + if state.PV.Status.Phase != corev1.VolumeReleased { + return nil, nil + } + if state.PV.Spec.ClaimRef == nil { + return nil, nil + } + + state.PV.Spec.ClaimRef = nil + err := state.Cluster().K8sClient().Update(ctx, state.PV) + if err != nil { + return composed.LogErrorAndReturn(err, "Error updating PV to remove ClaimRef", composed.StopWithRequeue, ctx) + } + + return composed.StopWithRequeueDelay(util.Timing.T100ms()), ctx +} diff --git a/pkg/skr/cceenfsvolume/pv_removeFinalizer.go b/pkg/skr/cceenfsvolume/pv_removeFinalizer.go new file mode 100644 index 000000000..febe9c657 --- /dev/null +++ b/pkg/skr/cceenfsvolume/pv_removeFinalizer.go @@ -0,0 +1,30 @@ +package cceenfsvolume + +import ( + "context" + "github.com/kyma-project/cloud-manager/api" + "github.com/kyma-project/cloud-manager/pkg/composed" +) + +func pvRemoveFinalizer(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + logger := composed.LoggerFromCtx(ctx) + + if !composed.IsMarkedForDeletion(state.Obj()) { + return nil, ctx + } + + if state.PV == nil { + return nil, ctx + } + + removed, err := composed.PatchObjRemoveFinalizer(ctx, api.CommonFinalizerDeletionHook, state.PV, state.Cluster().K8sClient()) + if err != nil { + return composed.LogErrorAndReturn(err, "Error removing finalizer from CceeNfsVolume PV", composed.StopWithRequeue, ctx) + } + if removed { + logger.Info("Removed CceeNfsVolume PV finalizer") + } + + return nil, ctx +} diff --git a/pkg/skr/cceenfsvolume/pv_validate.go b/pkg/skr/cceenfsvolume/pv_validate.go new file mode 100644 index 000000000..6e4624c84 --- /dev/null +++ b/pkg/skr/cceenfsvolume/pv_validate.go @@ -0,0 +1,55 @@ +package cceenfsvolume + +import ( + "context" + "fmt" + cloudresourcesv1beta1 "github.com/kyma-project/cloud-manager/api/cloud-resources/v1beta1" + "github.com/kyma-project/cloud-manager/pkg/composed" + "github.com/kyma-project/cloud-manager/pkg/util" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func pvValidate(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + + if composed.IsMarkedForDeletion(state.Obj()) { + return nil, ctx + } + + desiredPvName := state.ObjAsCceeNfsVolume().GetPVName() + + pv := &corev1.PersistentVolume{} + err := state.Cluster().K8sClient().Get(ctx, client.ObjectKey{Name: desiredPvName}, pv) + if apierrors.IsNotFound(err) { + return nil, ctx + } + if err != nil { + return composed.LogErrorAndReturn(err, "Error getting PV to validate CceeNfsVolume PV", composed.StopWithRequeueDelay(util.Timing.T1000ms()), ctx) + } + + parentName, parentNameExists := pv.Labels[cloudresourcesv1beta1.LabelNfsVolName] + parentNamespace, parentNamespaceExists := pv.Labels[cloudresourcesv1beta1.LabelNfsVolNS] + if parentNameExists && + parentNamespaceExists && + parentName == state.ObjAsCceeNfsVolume().Name && + parentNamespace == state.ObjAsCceeNfsVolume().Namespace { + return nil, ctx + } + + state.ObjAsCceeNfsVolume().Status.State = cloudresourcesv1beta1.StateError + return composed.PatchStatus(state.ObjAsCceeNfsVolume()). + SetExclusiveConditions(metav1.Condition{ + Type: cloudresourcesv1beta1.ConditionTypeError, + Status: metav1.ConditionTrue, + Reason: cloudresourcesv1beta1.ConditionReasonPVNameInvalid, + Message: fmt.Sprintf("Desired PV name %s already exists with different owner", desiredPvName), + }). + FailedError(composed.StopWithRequeueDelay(util.Timing.T1000ms())). + ErrorLogMessage("Error patching CceeNfsVolume status with error condition when PV already exists with different owner"). + SuccessError(composed.StopAndForget). + SuccessLogMsg(fmt.Sprintf("CceeNfsVoliume desired PV name %s already exists with different owner", desiredPvName)). + Run(ctx, state) +} diff --git a/pkg/skr/cceenfsvolume/pv_waitDeleted.go b/pkg/skr/cceenfsvolume/pv_waitDeleted.go new file mode 100644 index 000000000..c2947262a --- /dev/null +++ b/pkg/skr/cceenfsvolume/pv_waitDeleted.go @@ -0,0 +1,24 @@ +package cceenfsvolume + +import ( + "context" + "github.com/kyma-project/cloud-manager/pkg/composed" + "github.com/kyma-project/cloud-manager/pkg/util" +) + +func pvWaitDeleted(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + logger := composed.LoggerFromCtx(ctx) + + if !composed.IsMarkedForDeletion(state.Obj()) { + return nil, ctx + } + + if state.PV == nil { + return nil, ctx + } + + logger.Info("Waiting for CceeNfsVolume PV to be deleted") + + return composed.StopWithRequeueDelay(util.Timing.T10000ms()), ctx +} diff --git a/pkg/skr/cceenfsvolume/pvc_create.go b/pkg/skr/cceenfsvolume/pvc_create.go new file mode 100644 index 000000000..a7d16770c --- /dev/null +++ b/pkg/skr/cceenfsvolume/pvc_create.go @@ -0,0 +1,62 @@ +package cceenfsvolume + +import ( + "context" + "github.com/kyma-project/cloud-manager/api" + "github.com/kyma-project/cloud-manager/pkg/composed" + "github.com/kyma-project/cloud-manager/pkg/util" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" +) + +func pvcCreate(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + logger := composed.LoggerFromCtx(ctx) + + if composed.IsMarkedForDeletion(state.Obj()) { + return nil, ctx + } + if state.PVC != nil { + return nil, ctx + } + + pvc := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: state.ObjAsCceeNfsVolume().GetNamespace(), + Name: state.ObjAsCceeNfsVolume().GetPVCName(), + Labels: util.NewLabelBuilder(). + WithCustomLabels(state.ObjAsCceeNfsVolume().GetPVCLabels()). + WithCloudManagerDefaults(). + Build(), + Annotations: state.ObjAsCceeNfsVolume().GetPVCAnnotations(), + Finalizers: []string{ + api.CommonFinalizerDeletionHook, + }, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + VolumeName: state.PV.GetName(), // connection to PV + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteMany}, + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + "storage": state.PV.Spec.Capacity["storage"], + }, + }, + StorageClassName: ptr.To(""), + VolumeMode: ptr.To(corev1.PersistentVolumeFilesystem), + }, + } + + err := state.Cluster().K8sClient().Create(ctx, pvc) + if err != nil { + return composed.LogErrorAndReturn(err, "Error creating PVC for CceeNfsVolume", composed.StopWithRequeue, ctx) + } + + logger. + WithValues("pvcName", pvc.Name). + Info("Created PVC for CceeNfsVolume") + + state.PVC = pvc + + return nil, ctx +} diff --git a/pkg/skr/cceenfsvolume/pvc_delete.go b/pkg/skr/cceenfsvolume/pvc_delete.go new file mode 100644 index 000000000..e34f6578d --- /dev/null +++ b/pkg/skr/cceenfsvolume/pvc_delete.go @@ -0,0 +1,31 @@ +package cceenfsvolume + +import ( + "context" + "github.com/kyma-project/cloud-manager/pkg/composed" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func pvcDelete(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + logger := composed.LoggerFromCtx(ctx) + + if !composed.IsMarkedForDeletion(state.Obj()) { + return nil, ctx + } + + if state.PVC == nil { + return nil, ctx + } + + err := state.Cluster().K8sClient().Delete(ctx, state.PVC) + if client.IgnoreNotFound(err) != nil { + return composed.LogErrorAndReturn(err, "Error deleting CceeNfsVolume PVC", composed.StopWithRequeue, ctx) + } + + if err != nil { + logger.Info("Deleted CceeNfsVolume PVC") + } + + return nil, ctx +} diff --git a/pkg/skr/cceenfsvolume/pvc_load.go b/pkg/skr/cceenfsvolume/pvc_load.go new file mode 100644 index 000000000..d52424ae1 --- /dev/null +++ b/pkg/skr/cceenfsvolume/pvc_load.go @@ -0,0 +1,27 @@ +package cceenfsvolume + +import ( + "context" + "github.com/kyma-project/cloud-manager/pkg/composed" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func pvcLoad(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + + pvc := &corev1.PersistentVolumeClaim{} + err := state.Cluster().K8sClient().Get(ctx, client.ObjectKey{ + Namespace: state.Obj().GetNamespace(), + Name: state.ObjAsCceeNfsVolume().GetPVCName(), + }, pvc) + if client.IgnoreNotFound(err) != nil { + return composed.LogErrorAndReturn(err, "Error loading PVC for CceeNfsVolume", composed.StopWithRequeue, ctx) + } + + if err == nil { + state.PVC = pvc + } + + return nil, ctx +} diff --git a/pkg/skr/cceenfsvolume/pvc_removeFinalizer.go b/pkg/skr/cceenfsvolume/pvc_removeFinalizer.go new file mode 100644 index 000000000..7849736be --- /dev/null +++ b/pkg/skr/cceenfsvolume/pvc_removeFinalizer.go @@ -0,0 +1,30 @@ +package cceenfsvolume + +import ( + "context" + "github.com/kyma-project/cloud-manager/api" + "github.com/kyma-project/cloud-manager/pkg/composed" +) + +func pvcRemoveFinalizer(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + logger := composed.LoggerFromCtx(ctx) + + if !composed.IsMarkedForDeletion(state.Obj()) { + return nil, ctx + } + + if state.PVC == nil { + return nil, ctx + } + + removed, err := composed.PatchObjRemoveFinalizer(ctx, api.CommonFinalizerDeletionHook, state.PVC, state.Cluster().K8sClient()) + if err != nil { + return composed.LogErrorAndReturn(err, "Error removing finalizer from CceeNfsVolume PVC", composed.StopWithRequeue, ctx) + } + if removed { + logger.Info("Removed CceeNfsVolume PVC finalizer") + } + + return nil, ctx +} diff --git a/pkg/skr/cceenfsvolume/pvc_validate.go b/pkg/skr/cceenfsvolume/pvc_validate.go new file mode 100644 index 000000000..c40c3885e --- /dev/null +++ b/pkg/skr/cceenfsvolume/pvc_validate.go @@ -0,0 +1,54 @@ +package cceenfsvolume + +import ( + "context" + "fmt" + cloudresourcesv1beta1 "github.com/kyma-project/cloud-manager/api/cloud-resources/v1beta1" + "github.com/kyma-project/cloud-manager/pkg/composed" + "github.com/kyma-project/cloud-manager/pkg/util" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func pvcValidate(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + + if composed.IsMarkedForDeletion(state.Obj()) { + return nil, ctx + } + + desiredPvcName := state.ObjAsCceeNfsVolume().GetPVCName() + pvc := &corev1.PersistentVolumeClaim{} + err := state.Cluster().K8sClient().Get(ctx, client.ObjectKey{Namespace: state.ObjAsCceeNfsVolume().Namespace, Name: desiredPvcName}, pvc) + + if apierrors.IsNotFound(err) { + return nil, ctx + } + if err != nil { + return composed.LogErrorAndReturn(err, "Error getting PVC to validated CceeNfsVolume PVC", composed.StopWithRequeueDelay(util.Timing.T10000ms()), ctx) + } + + parentName, nameLabelExists := pvc.Labels[cloudresourcesv1beta1.LabelNfsVolName] + parentNamespace, namespaceLabelExists := pvc.Labels[cloudresourcesv1beta1.LabelNfsVolNS] + if nameLabelExists && + namespaceLabelExists && + parentName == state.ObjAsCceeNfsVolume().Name && + parentNamespace == state.ObjAsCceeNfsVolume().Namespace { + return nil, ctx + } + + state.ObjAsCceeNfsVolume().Status.State = cloudresourcesv1beta1.StateError + return composed.PatchStatus(state.ObjAsCceeNfsVolume()). + SetExclusiveConditions(metav1.Condition{ + Type: cloudresourcesv1beta1.ConditionTypeError, + Status: metav1.ConditionTrue, + Reason: cloudresourcesv1beta1.ConditionReasonPVNameInvalid, + Message: fmt.Sprintf("Desired PVC name %s already exists with different owner", desiredPvcName), + }). + FailedError(composed.StopWithRequeueDelay(util.Timing.T1000ms())). + ErrorLogMessage("Error patching CceeNfsVolume status with error condition when PVC already exists with different owner"). + SuccessError(composed.StopAndForget). + Run(ctx, state) +} diff --git a/pkg/skr/cceenfsvolume/pvc_waitDeleted.go b/pkg/skr/cceenfsvolume/pvc_waitDeleted.go new file mode 100644 index 000000000..b3c7be856 --- /dev/null +++ b/pkg/skr/cceenfsvolume/pvc_waitDeleted.go @@ -0,0 +1,24 @@ +package cceenfsvolume + +import ( + "context" + "github.com/kyma-project/cloud-manager/pkg/composed" + "github.com/kyma-project/cloud-manager/pkg/util" +) + +func pvcWaitDeleted(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + logger := composed.LoggerFromCtx(ctx) + + if !composed.IsMarkedForDeletion(state.Obj()) { + return nil, ctx + } + + if state.PVC == nil { + return nil, ctx + } + + logger.Info("Waiting for CceeNfsVolume PVC to be deleted") + + return composed.StopWithRequeueDelay(util.Timing.T10000ms()), ctx +} diff --git a/pkg/skr/cceenfsvolume/reconciler.go b/pkg/skr/cceenfsvolume/reconciler.go index dd50d1292..38fc155fb 100644 --- a/pkg/skr/cceenfsvolume/reconciler.go +++ b/pkg/skr/cceenfsvolume/reconciler.go @@ -3,6 +3,7 @@ package cceenfsvolume import ( "context" cloudresourcesv1beta1 "github.com/kyma-project/cloud-manager/api/cloud-resources/v1beta1" + "github.com/kyma-project/cloud-manager/pkg/common/actions" "github.com/kyma-project/cloud-manager/pkg/composed" "github.com/kyma-project/cloud-manager/pkg/feature" "github.com/kyma-project/cloud-manager/pkg/skr/common/defaultiprange" @@ -39,10 +40,47 @@ func (r *reconciler) Reconcile(ctx context.Context, request reconcile.Request) ( func (r *reconciler) newAction() composed.Action { return composed.ComposeActions( - "crAwsNfsVolumeMain", + "crCceeNfsVolumeMain", feature.LoadFeatureContextFromObj(&cloudresourcesv1beta1.CceeNfsVolume{}), composed.LoadObj, + pvValidate, + pvcValidate, defaultiprange.New(), - // TODO add more actions here + + pvLoad, + pvRemoveClaimRef, + pvcLoad, + actions.PatchAddCommonFinalizer(), + idGenerate, + + kcpNfsInstanceLoad, + kcpNfsInstanceCreate, + waitKcpNfsInstanceStatus, + + updateSize, + + pvCreate, + pvcCreate, + + statusCopy, + + stopIfNotBeingDeleted, + + // below executes only when marked for deletion + + pvcRemoveFinalizer, + pvcDelete, + pvcWaitDeleted, + + pvRemoveFinalizer, + pvDelete, + pvWaitDeleted, + + kcpNfsInstanceDelete, + kcpNfsInstanceWaitDeleted, + + actions.PatchRemoveCommonFinalizer(), + + composed.StopAndForgetAction, ) } diff --git a/pkg/skr/cceenfsvolume/statusCopy.go b/pkg/skr/cceenfsvolume/statusCopy.go new file mode 100644 index 000000000..0836f2b96 --- /dev/null +++ b/pkg/skr/cceenfsvolume/statusCopy.go @@ -0,0 +1,58 @@ +package cceenfsvolume + +import ( + "context" + "fmt" + "github.com/elliotchance/pie/v2" + cloudresourcesv1beta1 "github.com/kyma-project/cloud-manager/api/cloud-resources/v1beta1" + "github.com/kyma-project/cloud-manager/pkg/composed" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func statusCopy(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + logger := composed.LoggerFromCtx(ctx) + + if state.KcpNfsInstance == nil { + return nil, ctx + } + + oldState := state.ObjAsCceeNfsVolume().Status.State + + changed, addedConditions, removedConditions, newState := composed.StatusCopyConditionsAndState(state.KcpNfsInstance, state.ObjAsCceeNfsVolume()) + changed = changed || state.ObjAsCceeNfsVolume().DeriveStateFromConditions() + + if !changed { + return nil, ctx + } + + logger. + WithValues( + "allConditions", pie.Map(state.ObjAsCceeNfsVolume().Status.Conditions, func(c metav1.Condition) string { + return fmt.Sprintf("%s/%s/%s", c.Type, c.Reason, c.Message) + }), + "addedConditions", addedConditions, + "removedConditions", removedConditions, + "newState", newState, + "oldState", oldState, + ). + Info("Updating CceeNfsVolume status with conditions and state") + + b := composed.PatchStatus(state.ObjAsCceeNfsVolume()). + ErrorLogMessage("Error patching CceeNfsVolume status with conditions and state"). + FailedError(composed.StopWithRequeue) + + if meta.FindStatusCondition(state.ObjAsCceeNfsVolume().Status.Conditions, cloudresourcesv1beta1.ConditionTypeError) != nil { + // KCP NfsInstance has error status + // Stop the reconciliation + b = b. + SuccessError(composed.StopAndForget). + SuccessLogMsg("Forgetting CceeNfsVolume status with error condition") + } else { + // KCP NfsInstance is not in the error status, keep running + b = b.SuccessErrorNil() + } + + return b.Run(ctx, state) +} diff --git a/pkg/skr/cceenfsvolume/stopIfNotBeingDeleted.go b/pkg/skr/cceenfsvolume/stopIfNotBeingDeleted.go new file mode 100644 index 000000000..3edb45afa --- /dev/null +++ b/pkg/skr/cceenfsvolume/stopIfNotBeingDeleted.go @@ -0,0 +1,14 @@ +package cceenfsvolume + +import ( + "context" + "github.com/kyma-project/cloud-manager/pkg/composed" +) + +func stopIfNotBeingDeleted(ctx context.Context, st composed.State) (error, context.Context) { + if !composed.IsMarkedForDeletion(st.Obj()) { + return composed.StopAndForget, ctx + } + + return nil, ctx +} diff --git a/pkg/skr/cceenfsvolume/updateSize.go b/pkg/skr/cceenfsvolume/updateSize.go new file mode 100644 index 000000000..8608c61d2 --- /dev/null +++ b/pkg/skr/cceenfsvolume/updateSize.go @@ -0,0 +1,56 @@ +package cceenfsvolume + +import ( + "context" + "github.com/kyma-project/cloud-manager/pkg/composed" + "github.com/kyma-project/cloud-manager/pkg/util" +) + +func updateSize(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + + if composed.IsMarkedForDeletion(state.ObjAsCceeNfsVolume()) { + return nil, ctx + } + if state.KcpNfsInstance == nil { + return nil, ctx + } + + if state.ObjAsCceeNfsVolume().Spec.CapacityGb == state.KcpNfsInstance.Spec.Instance.OpenStack.SizeGb { + return nil, ctx + } + + newState := "Shrinking" + if state.ObjAsCceeNfsVolume().Spec.CapacityGb > state.KcpNfsInstance.Spec.Instance.OpenStack.SizeGb { + newState = "Extending" + } + + logger := composed.LoggerFromCtx(ctx) + logger. + WithValues( + "oldCapacityGb", state.KcpNfsInstance.Spec.Instance.OpenStack.SizeGb, + "newCapacityGb", state.ObjAsCceeNfsVolume().Spec.CapacityGb, + ). + Info("Updating KCP NfsInstance capacity for CceeNfsInstance") + + p := map[string]interface{}{ + "spec": map[string]interface{}{ + "capacityGb": state.KcpNfsInstance.Spec.Instance.OpenStack.SizeGb, + }, + } + err := composed.MergePatchObj(ctx, state.ObjAsCceeNfsVolume(), p, state.KcpCluster.K8sClient()) + + if err != nil { + return composed.LogErrorAndReturn(err, "Error patching KCP NfsInstance for CceeNfsVolume resize", composed.StopWithRequeue, ctx) + } + + state.ObjAsCceeNfsVolume().Status.State = newState + + return composed.PatchStatus(state.ObjAsCceeNfsVolume()). + ErrorLogMessage("Error patching CceeNfsVolume status with new status state for resize"). + // same as for success since we already updated kcp nfsInstance + // in the new loop it will pick up the kcp state.state + FailedError(composed.StopWithRequeueDelay(util.Timing.T10000ms())). + SuccessError(composed.StopWithRequeueDelay(util.Timing.T10000ms())). + Run(ctx, state) +} diff --git a/pkg/skr/cceenfsvolume/waitKcpNfsInstanceStatus.go b/pkg/skr/cceenfsvolume/waitKcpNfsInstanceStatus.go new file mode 100644 index 000000000..3eac4a125 --- /dev/null +++ b/pkg/skr/cceenfsvolume/waitKcpNfsInstanceStatus.go @@ -0,0 +1,26 @@ +package cceenfsvolume + +import ( + "context" + cloudcontrol1beta1 "github.com/kyma-project/cloud-manager/api/cloud-control/v1beta1" + "github.com/kyma-project/cloud-manager/pkg/composed" + "github.com/kyma-project/cloud-manager/pkg/util" + "k8s.io/apimachinery/pkg/api/meta" +) + +func waitKcpNfsInstanceStatus(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + + if state.KcpNfsInstance == nil { + return nil, ctx + } + + if meta.FindStatusCondition(*state.KcpNfsInstance.Conditions(), cloudcontrol1beta1.ConditionTypeReady) != nil { + return nil, ctx + } + if meta.FindStatusCondition(*state.KcpNfsInstance.Conditions(), cloudcontrol1beta1.ConditionTypeError) != nil { + return nil, ctx + } + + return composed.StopWithRequeueDelay(util.Timing.T1000ms()), ctx +} diff --git a/pkg/testinfra/dsl/cceeNfsVolume.go b/pkg/testinfra/dsl/cceeNfsVolume.go new file mode 100644 index 000000000..1ac47f149 --- /dev/null +++ b/pkg/testinfra/dsl/cceeNfsVolume.go @@ -0,0 +1,127 @@ +package dsl + +import ( + "context" + "errors" + "fmt" + cloudresourcesv1beta1 "github.com/kyma-project/cloud-manager/api/cloud-resources/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func CreateCceeNfsVolume(ctx context.Context, clnt client.Client, obj *cloudresourcesv1beta1.CceeNfsVolume, opts ...ObjAction) error { + if obj == nil { + obj = &cloudresourcesv1beta1.CceeNfsVolume{} + } + NewObjActions(opts...). + Append( + WithNamespace(DefaultSkrNamespace), + ). + ApplyOnObject(obj) + + if obj.Name == "" { + return errors.New("the SKR CceeNfsVolume must have name set") + } + + err := clnt.Create(ctx, obj) + return err +} + +func WithCceeNfsVolumeCapacity(capacityGb int) ObjAction { + return &objAction{ + f: func(obj client.Object) { + if x, ok := obj.(*cloudresourcesv1beta1.CceeNfsVolume); ok { + if x.Spec.CapacityGb == 0 { + x.Spec.CapacityGb = capacityGb + } + return + } + panic(fmt.Errorf("unhandled type %T in WithCceeNfsVolumeCapacity", obj)) + }, + } +} + +func WithCceeNfsVolumePvLabels(pvLabels map[string]string) ObjAction { + return &objAction{ + f: func(obj client.Object) { + if x, ok := obj.(*cloudresourcesv1beta1.CceeNfsVolume); ok { + if x.Spec.PersistentVolume == nil { + x.Spec.PersistentVolume = &cloudresourcesv1beta1.NameLabelsAnnotationsSpec{} + } + x.Spec.PersistentVolume.Labels = pvLabels + return + } + panic(fmt.Errorf("unhandled type %T in WithCceeNfsVolumePvLabels", obj)) + }, + } +} + +func WithCceeNfsVolumePvAnnotations(pvAnnotations map[string]string) ObjAction { + return &objAction{ + f: func(obj client.Object) { + if x, ok := obj.(*cloudresourcesv1beta1.CceeNfsVolume); ok { + if x.Spec.PersistentVolume == nil { + x.Spec.PersistentVolume = &cloudresourcesv1beta1.NameLabelsAnnotationsSpec{} + } + x.Spec.PersistentVolume.Annotations = pvAnnotations + return + } + panic(fmt.Errorf("unhandled type %T in WithCceeNfsVolumePvLabels", obj)) + }, + } +} + +func WithCceeNfsVolumePvcLabels(pvcLabels map[string]string) ObjAction { + return &objAction{ + f: func(obj client.Object) { + if x, ok := obj.(*cloudresourcesv1beta1.CceeNfsVolume); ok { + if x.Spec.PersistentVolumeClaim == nil { + x.Spec.PersistentVolumeClaim = &cloudresourcesv1beta1.NameLabelsAnnotationsSpec{} + } + x.Spec.PersistentVolumeClaim.Labels = pvcLabels + return + } + panic(fmt.Errorf("unhandled type %T in WithCceeNfsVolumePvcLabels", obj)) + }, + } +} + +func WithCceeNfsVolumePvcAnnotations(pvcAnnotations map[string]string) ObjAction { + return &objAction{ + f: func(obj client.Object) { + if x, ok := obj.(*cloudresourcesv1beta1.CceeNfsVolume); ok { + if x.Spec.PersistentVolumeClaim == nil { + x.Spec.PersistentVolumeClaim = &cloudresourcesv1beta1.NameLabelsAnnotationsSpec{} + } + x.Spec.PersistentVolumeClaim.Annotations = pvcAnnotations + return + } + panic(fmt.Errorf("unhandled type %T in WithCceeNfsVolumePvcLabels", obj)) + }, + } +} + +func HavingCceeNfsVolumeStatusId() ObjAssertion { + return func(obj client.Object) error { + x, ok := obj.(*cloudresourcesv1beta1.CceeNfsVolume) + if !ok { + return fmt.Errorf("the object %T is not SKR CceeNfsVolume", obj) + } + if x.Status.Id == "" { + return errors.New("the SKR CceeNfsVolume ID not set") + } + return nil + } +} + +func HavingCceeNfsVolumeStatusState(state string) ObjAssertion { + return func(obj client.Object) error { + x, ok := obj.(*cloudresourcesv1beta1.CceeNfsVolume) + if !ok { + return fmt.Errorf("the object %T is not SKR CceeNfsVolume", obj) + } + if x.Status.State != state { + return fmt.Errorf("the SKR CceeNfsVolume State does not match. expected: %s, got: %s", state, x.Status.State) + } + return nil + } +} diff --git a/pkg/testinfra/dsl/nfsInstance.go b/pkg/testinfra/dsl/nfsInstance.go index feb8501e9..2ca8a0639 100644 --- a/pkg/testinfra/dsl/nfsInstance.go +++ b/pkg/testinfra/dsl/nfsInstance.go @@ -12,6 +12,7 @@ import ( const ( DefaultNfsInstanceHost = "nfs.instance.local" + DefaultNfsInstancePath = "/path" DefaultGcpNfsInstanceFileShareName = "vol1" DefaultGcpNfsInstanceCapacityGb = 1024 DefaultGcpNfsInstanceConnectMode = "PRIVATE_SERVICE_ACCESS" @@ -34,6 +35,21 @@ func WithNfsInstanceStatusHost(host string) ObjStatusAction { } } +func WithNfsInstanceStatusPath(path string) ObjStatusAction { + return &objStatusAction{ + f: func(obj client.Object) { + if path == "" { + path = DefaultNfsInstancePath + } + if x, ok := obj.(*cloudcontrolv1beta1.NfsInstance); ok { + if x.Status.Path == "" { + x.Status.Path = path + } + } + }, + } +} + func WithNfsInstanceStatusId(id string) ObjStatusAction { return &objStatusAction{ f: func(obj client.Object) {