diff --git a/charts/README.md b/charts/README.md index 9298d57b2..a032ece32 100644 --- a/charts/README.md +++ b/charts/README.md @@ -79,6 +79,9 @@ The following table lists the configurable parameters of the latest NFS CSI Driv | `controller.resources.csiProvisioner.limits.memory` | csi-provisioner memory limits | 100Mi | | `controller.resources.csiProvisioner.requests.cpu` | csi-provisioner cpu requests limits | 10m | | `controller.resources.csiProvisioner.requests.memory` | csi-provisioner memory requests limits | 20Mi | +| `controller.resources.csiResizer.limits.memory` | csi-resizer memory limits | 400Mi | +| `controller.resources.csiResizer.requests.cpu` | csi-resizer cpu requests | 10m | +| `controller.resources.csiResizer.requests.memory` | csi-resizer memory requests | 20Mi | | `controller.resources.livenessProbe.limits.memory` | liveness-probe memory limits | 100Mi | | `controller.resources.livenessProbe.requests.cpu` | liveness-probe cpu requests limits | 10m | | `controller.resources.livenessProbe.requests.memory` | liveness-probe memory requests limits | 20Mi | diff --git a/charts/latest/csi-driver-nfs-v0.0.0.tgz b/charts/latest/csi-driver-nfs-v0.0.0.tgz index 6e5cfe418..7036ac3cd 100644 Binary files a/charts/latest/csi-driver-nfs-v0.0.0.tgz and b/charts/latest/csi-driver-nfs-v0.0.0.tgz differ diff --git a/charts/latest/csi-driver-nfs/templates/csi-nfs-controller.yaml b/charts/latest/csi-driver-nfs/templates/csi-nfs-controller.yaml index 82332a0a9..afea01ea7 100644 --- a/charts/latest/csi-driver-nfs/templates/csi-nfs-controller.yaml +++ b/charts/latest/csi-driver-nfs/templates/csi-nfs-controller.yaml @@ -75,6 +75,30 @@ spec: capabilities: drop: - ALL + - name: csi-resizer +{{- if hasPrefix "/" .Values.image.csiResizer.repository }} + image: "{{ .Values.image.baseRepo }}{{ .Values.image.csiResizer.repository }}:{{ .Values.image.csiResizer.tag }}" +{{- else }} + image: "{{ .Values.image.csiResizer.repository }}:{{ .Values.image.csiResizer.tag }}" +{{- end }} + args: + - "-csi-address=$(ADDRESS)" + - "-v=2" + - "-leader-election" + - "--leader-election-namespace={{ .Release.Namespace }}" + - '-handle-volume-inuse-error=false' + env: + - name: ADDRESS + value: /csi/csi.sock + imagePullPolicy: {{ .Values.image.csiResizer.pullPolicy }} + volumeMounts: + - name: socket-dir + mountPath: /csi + resources: {{- toYaml .Values.controller.resources.csiResizer | nindent 12 }} + securityContext: + capabilities: + drop: + - ALL - name: csi-snapshotter {{- if hasPrefix "/" .Values.image.csiSnapshotter.repository }} image: "{{ .Values.image.baseRepo }}{{ .Values.image.csiSnapshotter.repository }}:{{ .Values.image.csiSnapshotter.tag }}" diff --git a/charts/latest/csi-driver-nfs/templates/rbac-csi-nfs.yaml b/charts/latest/csi-driver-nfs/templates/rbac-csi-nfs.yaml index 0fc047cf0..9bff94bb7 100644 --- a/charts/latest/csi-driver-nfs/templates/rbac-csi-nfs.yaml +++ b/charts/latest/csi-driver-nfs/templates/rbac-csi-nfs.yaml @@ -57,6 +57,42 @@ rules: resources: ["secrets"] verbs: ["get"] --- +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: {{ .Values.rbac.name }}-external-resizer-role +{{ include "nfs.labels" . | indent 2 }} +rules: + - apiGroups: [""] + resources: ["persistentvolumes"] + verbs: ["get", "list", "watch", "update", "patch"] + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["persistentvolumeclaims/status"] + verbs: ["update", "patch"] + - apiGroups: [""] + resources: ["events"] + verbs: ["list", "watch", "create", "update", "patch"] + - apiGroups: ["coordination.k8s.io"] + resources: ["leases"] + verbs: ["get", "list", "watch", "create", "update", "patch"] +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: {{ .Values.rbac.name }}-csi-resizer-role +{{ include "nfs.labels" . | indent 2 }} +subjects: + - kind: ServiceAccount + name: {{ .Values.serviceAccount.controller }} + namespace: {{ .Release.Namespace }} +roleRef: + kind: ClusterRole + name: {{ .Values.rbac.name }}-external-resizer-role + apiGroup: rbac.authorization.k8s.io +--- kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: diff --git a/charts/latest/csi-driver-nfs/values.yaml b/charts/latest/csi-driver-nfs/values.yaml index f47b11f9b..518706d18 100755 --- a/charts/latest/csi-driver-nfs/values.yaml +++ b/charts/latest/csi-driver-nfs/values.yaml @@ -9,6 +9,10 @@ image: repository: registry.k8s.io/sig-storage/csi-provisioner tag: v5.1.0 pullPolicy: IfNotPresent + csiResizer: + repository: registry.k8s.io/sig-storage/csi-resizer + tag: v1.12.0 + pullPolicy: IfNotPresent csiSnapshotter: repository: registry.k8s.io/sig-storage/csi-snapshotter tag: v8.1.0 @@ -81,6 +85,12 @@ controller: requests: cpu: 10m memory: 20Mi + csiResizer: + limits: + memory: 400Mi + requests: + cpu: 10m + memory: 20Mi csiSnapshotter: limits: memory: 200Mi diff --git a/deploy/csi-nfs-controller.yaml b/deploy/csi-nfs-controller.yaml index 4ed471c80..c4f8833d9 100644 --- a/deploy/csi-nfs-controller.yaml +++ b/deploy/csi-nfs-controller.yaml @@ -63,6 +63,30 @@ spec: capabilities: drop: - ALL + - name: csi-resizer + image: registry.k8s.io/sig-storage/csi-resizer:v1.12.0 + args: + - "-csi-address=$(ADDRESS)" + - "-v=2" + - "-leader-election" + - "--leader-election-namespace=kube-system" + - '-handle-volume-inuse-error=false' + env: + - name: ADDRESS + value: /csi/csi.sock + volumeMounts: + - name: socket-dir + mountPath: /csi + resources: + limits: + memory: 400Mi + requests: + cpu: 10m + memory: 20Mi + securityContext: + capabilities: + drop: + - ALL - name: csi-snapshotter image: registry.k8s.io/sig-storage/csi-snapshotter:v8.1.0 args: diff --git a/deploy/rbac-csi-nfs.yaml b/deploy/rbac-csi-nfs.yaml index 6d21b7b20..21e36effe 100644 --- a/deploy/rbac-csi-nfs.yaml +++ b/deploy/rbac-csi-nfs.yaml @@ -64,3 +64,39 @@ roleRef: kind: ClusterRole name: nfs-external-provisioner-role apiGroup: rbac.authorization.k8s.io +--- + +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: nfs-external-resizer-role +rules: + - apiGroups: [""] + resources: ["persistentvolumes"] + verbs: ["get", "list", "watch", "update", "patch"] + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["persistentvolumeclaims/status"] + verbs: ["update", "patch"] + - apiGroups: [""] + resources: ["events"] + verbs: ["list", "watch", "create", "update", "patch"] + - apiGroups: ["coordination.k8s.io"] + resources: ["leases"] + verbs: ["get", "list", "watch", "create", "update", "patch"] +--- + +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: nfs-csi-resizer-role +subjects: + - kind: ServiceAccount + name: csi-nfs-controller-sa + namespace: kube-system +roleRef: + kind: ClusterRole + name: nfs-external-resizer-role + apiGroup: rbac.authorization.k8s.io diff --git a/hack/verify-helm-chart.sh b/hack/verify-helm-chart.sh index 642eec226..45504bf5e 100755 --- a/hack/verify-helm-chart.sh +++ b/hack/verify-helm-chart.sh @@ -62,9 +62,10 @@ pip install yq --ignore-installed PyYAML # Extract images from csi-nfs-controller.yaml expected_csi_provisioner_image="$(cat ${PKG_ROOT}/deploy/csi-nfs-controller.yaml | yq -r .spec.template.spec.containers[0].image | head -n 1)" -expected_csi_snapshotter_image="$(cat ${PKG_ROOT}/deploy/csi-nfs-controller.yaml | yq -r .spec.template.spec.containers[1].image | head -n 1)" -expected_liveness_probe_image="$(cat ${PKG_ROOT}/deploy/csi-nfs-controller.yaml | yq -r .spec.template.spec.containers[2].image | head -n 1)" -expected_nfs_image="$(cat ${PKG_ROOT}/deploy/csi-nfs-controller.yaml | yq -r .spec.template.spec.containers[3].image | head -n 1)" +expected_csi_resizer_image="$(cat ${PKG_ROOT}/deploy/csi-nfs-controller.yaml | yq -r .spec.template.spec.containers[1].image | head -n 1)" +expected_csi_snapshotter_image="$(cat ${PKG_ROOT}/deploy/csi-nfs-controller.yaml | yq -r .spec.template.spec.containers[2].image | head -n 1)" +expected_liveness_probe_image="$(cat ${PKG_ROOT}/deploy/csi-nfs-controller.yaml | yq -r .spec.template.spec.containers[3].image | head -n 1)" +expected_nfs_image="$(cat ${PKG_ROOT}/deploy/csi-nfs-controller.yaml | yq -r .spec.template.spec.containers[4].image | head -n 1)" csi_provisioner_image="$(get_image_from_helm_chart "csiProvisioner")" validate_image "${expected_csi_provisioner_image}" "${csi_provisioner_image}" diff --git a/pkg/nfs/controllerserver.go b/pkg/nfs/controllerserver.go index f160c6450..adb5049d4 100644 --- a/pkg/nfs/controllerserver.go +++ b/pkg/nfs/controllerserver.go @@ -476,8 +476,19 @@ func (cs *ControllerServer) ListSnapshots(_ context.Context, _ *csi.ListSnapshot return nil, status.Error(codes.Unimplemented, "") } -func (cs *ControllerServer) ControllerExpandVolume(_ context.Context, _ *csi.ControllerExpandVolumeRequest) (*csi.ControllerExpandVolumeResponse, error) { - return nil, status.Error(codes.Unimplemented, "") +func (cs *ControllerServer) ControllerExpandVolume(_ context.Context, req *csi.ControllerExpandVolumeRequest) (*csi.ControllerExpandVolumeResponse, error) { + if len(req.GetVolumeId()) == 0 { + return nil, status.Error(codes.InvalidArgument, "Volume ID missing in request") + } + + if req.GetCapacityRange() == nil { + return nil, status.Error(codes.InvalidArgument, "Capacity Range missing in request") + } + + volSizeBytes := int64(req.GetCapacityRange().GetRequiredBytes()) + klog.V(2).Infof("ControllerExpandVolume(%s) successfully, currentQuota: %d bytes", req.VolumeId, volSizeBytes) + + return &csi.ControllerExpandVolumeResponse{CapacityBytes: req.GetCapacityRange().GetRequiredBytes()}, nil } // Mount nfs server at base-dir diff --git a/pkg/nfs/controllerserver_test.go b/pkg/nfs/controllerserver_test.go index 57a817610..196a5aa5c 100644 --- a/pkg/nfs/controllerserver_test.go +++ b/pkg/nfs/controllerserver_test.go @@ -372,6 +372,13 @@ func TestControllerGetCapabilities(t *testing.T) { }, }, }, + { + Type: &csi.ControllerServiceCapability_Rpc{ + Rpc: &csi.ControllerServiceCapability_RPC{ + Type: csi.ControllerServiceCapability_RPC_EXPAND_VOLUME, + }, + }, + }, }, }, expectedErr: nil, @@ -1057,6 +1064,60 @@ func TestDeleteSnapshot(t *testing.T) { } } +func TestControllerExpandVolume(t *testing.T) { + testCases := []struct { + name string + testFunc func(t *testing.T) + }{ + { + name: "volume ID missing", + testFunc: func(t *testing.T) { + d := initTestController(t) + req := &csi.ControllerExpandVolumeRequest{} + _, err := d.ControllerExpandVolume(context.Background(), req) + expectedErr := status.Error(codes.InvalidArgument, "Volume ID missing in request") + if !reflect.DeepEqual(err, expectedErr) { + t.Errorf("actualErr: (%v), expectedErr: (%v)", err, expectedErr) + } + }, + }, + { + name: "Capacity Range missing", + testFunc: func(t *testing.T) { + d := initTestController(t) + req := &csi.ControllerExpandVolumeRequest{ + VolumeId: "unit-test", + } + _, err := d.ControllerExpandVolume(context.Background(), req) + expectedErr := status.Error(codes.InvalidArgument, "Capacity Range missing in request") + if !reflect.DeepEqual(err, expectedErr) { + t.Errorf("actualErr: (%v), expectedErr: (%v)", err, expectedErr) + } + }, + }, + { + name: "Error = nil", + testFunc: func(t *testing.T) { + d := initTestController(t) + req := &csi.ControllerExpandVolumeRequest{ + VolumeId: "unit-test", + CapacityRange: &csi.CapacityRange{ + RequiredBytes: 10000, + }, + } + _, err := d.ControllerExpandVolume(context.Background(), req) + if !reflect.DeepEqual(err, nil) { + t.Errorf("actualErr: (%v), expectedErr: (%v)", err, nil) + } + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, tc.testFunc) + } +} + func matchCreateSnapshotResponse(e, r *csi.CreateSnapshotResponse) error { if e == nil && r == nil { return nil diff --git a/pkg/nfs/nfs.go b/pkg/nfs/nfs.go index 5a0c2c2ec..5d36e5ecc 100644 --- a/pkg/nfs/nfs.go +++ b/pkg/nfs/nfs.go @@ -103,6 +103,7 @@ func NewDriver(options *DriverOptions) *Driver { csi.ControllerServiceCapability_RPC_SINGLE_NODE_MULTI_WRITER, csi.ControllerServiceCapability_RPC_CLONE_VOLUME, csi.ControllerServiceCapability_RPC_CREATE_DELETE_SNAPSHOT, + csi.ControllerServiceCapability_RPC_EXPAND_VOLUME, }) n.AddNodeServiceCapabilities([]csi.NodeServiceCapability_RPC_Type{ diff --git a/test/e2e/dynamic_provisioning_test.go b/test/e2e/dynamic_provisioning_test.go index 7f9df668a..542e449d1 100644 --- a/test/e2e/dynamic_provisioning_test.go +++ b/test/e2e/dynamic_provisioning_test.go @@ -52,7 +52,7 @@ var _ = ginkgo.Describe("Dynamic Provisioning", func() { }) testDriver = driver.InitNFSDriver() - ginkgo.It("should create a volume on demand with mount options [nfs.csi.k8s.io]", func(ctx ginkgo.SpecContext) { + ginkgo.It("should create a volume on demand with mount options", func(ctx ginkgo.SpecContext) { pods := []testsuites.PodDetails{ { Cmd: "echo 'hello world' > /mnt/test-1/data && grep 'hello world' /mnt/test-1/data", @@ -75,7 +75,7 @@ var _ = ginkgo.Describe("Dynamic Provisioning", func() { test.Run(ctx, cs, ns) }) - ginkgo.It("should create a volume on demand with zero mountPermissions [nfs.csi.k8s.io]", func(ctx ginkgo.SpecContext) { + ginkgo.It("should create a volume on demand with zero mountPermissions", func(ctx ginkgo.SpecContext) { pods := []testsuites.PodDetails{ { Cmd: "echo 'hello world' > /mnt/test-1/data && grep 'hello world' /mnt/test-1/data", @@ -98,7 +98,7 @@ var _ = ginkgo.Describe("Dynamic Provisioning", func() { test.Run(ctx, cs, ns) }) - ginkgo.It("should create multiple PV objects, bind to PVCs and attach all to different pods on the same node [nfs.csi.k8s.io]", func(ctx ginkgo.SpecContext) { + ginkgo.It("should create multiple PV objects, bind to PVCs and attach all to different pods on the same node", func(ctx ginkgo.SpecContext) { pods := []testsuites.PodDetails{ { Cmd: "while true; do echo $(date -u) >> /mnt/test-1/data; sleep 100; done", @@ -135,7 +135,7 @@ var _ = ginkgo.Describe("Dynamic Provisioning", func() { }) // Track issue https://github.com/kubernetes/kubernetes/issues/70505 - ginkgo.It("should create a volume on demand and mount it as readOnly in a pod [nfs.csi.k8s.io]", func(ctx ginkgo.SpecContext) { + ginkgo.It("should create a volume on demand and mount it as readOnly in a pod", func(ctx ginkgo.SpecContext) { pods := []testsuites.PodDetails{ { Cmd: "touch /mnt/test-1/data", @@ -159,7 +159,7 @@ var _ = ginkgo.Describe("Dynamic Provisioning", func() { test.Run(ctx, cs, ns) }) - ginkgo.It("should create a deployment object, write and read to it, delete the pod and write and read to it again [nfs.csi.k8s.io]", func(ctx ginkgo.SpecContext) { + ginkgo.It("should create a deployment object, write and read to it, delete the pod and write and read to it again", func(ctx ginkgo.SpecContext) { pod := testsuites.PodDetails{ Cmd: "echo 'hello world' >> /mnt/test-1/data && while true; do sleep 100; done", Volumes: []testsuites.VolumeDetails{ @@ -188,7 +188,7 @@ var _ = ginkgo.Describe("Dynamic Provisioning", func() { test.Run(ctx, cs, ns) }) - ginkgo.It("[subDir]should create a deployment object, write and read to it, delete the pod and write and read to it again [nfs.csi.k8s.io]", func(ctx ginkgo.SpecContext) { + ginkgo.It("[subDir]should create a deployment object, write and read to it, delete the pod and write and read to it again", func(ctx ginkgo.SpecContext) { pod := testsuites.PodDetails{ Cmd: "echo 'hello world' >> /mnt/test-1/data && while true; do sleep 100; done", Volumes: []testsuites.VolumeDetails{ @@ -217,7 +217,7 @@ var _ = ginkgo.Describe("Dynamic Provisioning", func() { test.Run(ctx, cs, ns) }) - ginkgo.It(fmt.Sprintf("should delete PV with reclaimPolicy %q [nfs.csi.k8s.io]", v1.PersistentVolumeReclaimDelete), func(ctx ginkgo.SpecContext) { + ginkgo.It(fmt.Sprintf("should delete PV with reclaimPolicy %q", v1.PersistentVolumeReclaimDelete), func(ctx ginkgo.SpecContext) { reclaimPolicy := v1.PersistentVolumeReclaimDelete volumes := []testsuites.VolumeDetails{ { @@ -234,7 +234,7 @@ var _ = ginkgo.Describe("Dynamic Provisioning", func() { test.Run(ctx, cs, ns) }) - ginkgo.It(fmt.Sprintf("should retain PV with reclaimPolicy %q [nfs.csi.k8s.io]", v1.PersistentVolumeReclaimRetain), func(ctx ginkgo.SpecContext) { + ginkgo.It(fmt.Sprintf("should retain PV with reclaimPolicy %q", v1.PersistentVolumeReclaimRetain), func(ctx ginkgo.SpecContext) { reclaimPolicy := v1.PersistentVolumeReclaimRetain volumes := []testsuites.VolumeDetails{ { @@ -251,7 +251,7 @@ var _ = ginkgo.Describe("Dynamic Provisioning", func() { test.Run(ctx, cs, ns) }) - ginkgo.It("should create a pod with multiple volumes [nfs.csi.k8s.io]", func(ctx ginkgo.SpecContext) { + ginkgo.It("should create a pod with multiple volumes", func(ctx ginkgo.SpecContext) { volumes := []testsuites.VolumeDetails{} for i := 1; i <= 6; i++ { volume := testsuites.VolumeDetails{ @@ -278,7 +278,7 @@ var _ = ginkgo.Describe("Dynamic Provisioning", func() { test.Run(ctx, cs, ns) }) - ginkgo.It("should create a pod with volume mount subpath [nfs.csi.k8s.io]", func(ctx ginkgo.SpecContext) { + ginkgo.It("should create a pod with volume mount subpath", func(ctx ginkgo.SpecContext) { pods := []testsuites.PodDetails{ { Cmd: convertToPowershellCommandIfNecessary("echo 'hello world' > /mnt/test-1/data && grep 'hello world' /mnt/test-1/data"), @@ -301,7 +301,7 @@ var _ = ginkgo.Describe("Dynamic Provisioning", func() { test.Run(ctx, cs, ns) }) - ginkgo.It("should create a CSI inline volume [nfs.csi.k8s.io]", func(ctx ginkgo.SpecContext) { + ginkgo.It("should create a CSI inline volume", func(ctx ginkgo.SpecContext) { pods := []testsuites.PodDetails{ { Cmd: convertToPowershellCommandIfNecessary("echo 'hello world' > /mnt/test-1/data && grep 'hello world' /mnt/test-1/data"), @@ -328,7 +328,7 @@ var _ = ginkgo.Describe("Dynamic Provisioning", func() { test.Run(ctx, cs, ns) }) - ginkgo.It("should create a volume on demand with retaining subdir on delete [nfs.csi.k8s.io]", func(ctx ginkgo.SpecContext) { + ginkgo.It("should create a volume on demand with retaining subdir on delete", func(ctx ginkgo.SpecContext) { pods := []testsuites.PodDetails{ { Cmd: "echo 'hello world' > /mnt/test-1/data && grep 'hello world' /mnt/test-1/data", @@ -351,7 +351,7 @@ var _ = ginkgo.Describe("Dynamic Provisioning", func() { test.Run(ctx, cs, ns) }) - ginkgo.It("should create a volume on demand with archive on delete [nfs.csi.k8s.io]", func(ctx ginkgo.SpecContext) { + ginkgo.It("should create a volume on demand with archive on delete", func(ctx ginkgo.SpecContext) { pods := []testsuites.PodDetails{ { Cmd: "echo 'hello world' > /mnt/test-1/data && grep 'hello world' /mnt/test-1/data", @@ -374,7 +374,7 @@ var _ = ginkgo.Describe("Dynamic Provisioning", func() { test.Run(ctx, cs, ns) }) - ginkgo.It("should create a volume on demand with archive subdir on delete [nfs.csi.k8s.io]", func(ctx ginkgo.SpecContext) { + ginkgo.It("should create a volume on demand with archive subdir on delete", func(ctx ginkgo.SpecContext) { pods := []testsuites.PodDetails{ { Cmd: "echo 'hello world' > /mnt/test-1/data && grep 'hello world' /mnt/test-1/data", @@ -396,4 +396,27 @@ var _ = ginkgo.Describe("Dynamic Provisioning", func() { } test.Run(ctx, cs, ns) }) + + ginkgo.It("should create a volume on demand and resize it", func(ctx ginkgo.SpecContext) { + pods := []testsuites.PodDetails{ + { + Cmd: "echo 'hello world' > /mnt/test-1/data && grep 'hello world' /mnt/test-1/data", + Volumes: []testsuites.VolumeDetails{ + { + ClaimSize: "10Gi", + VolumeMount: testsuites.VolumeMountDetails{ + NameGenerate: "test-volume-", + MountPathGenerate: "/mnt/test-", + }, + }, + }, + }, + } + test := testsuites.DynamicallyProvisionedResizeVolumeTest{ + CSIDriver: testDriver, + Pods: pods, + StorageClassParameters: archiveSubDirStorageClassParameters, + } + test.Run(ctx, cs, ns) + }) }) diff --git a/test/e2e/testsuites/dynamically_provisioned_resize_volume_tester.go b/test/e2e/testsuites/dynamically_provisioned_resize_volume_tester.go new file mode 100644 index 000000000..c311f0528 --- /dev/null +++ b/test/e2e/testsuites/dynamically_provisioned_resize_volume_tester.go @@ -0,0 +1,99 @@ +/* +Copyright 2024 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 testsuites + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/kubernetes-csi/csi-driver-nfs/test/e2e/driver" + "github.com/onsi/ginkgo/v2" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clientset "k8s.io/client-go/kubernetes" + "k8s.io/kubernetes/test/e2e/framework" +) + +// DynamicallyProvisionedResizeVolumeTest will provision required StorageClass(es), PVC(s) and Pod(s) +// Waiting for the PV provisioner to resize the PV +// Testing if the PV is resized successfully. +type DynamicallyProvisionedResizeVolumeTest struct { + CSIDriver driver.DynamicPVTestDriver + Pods []PodDetails + StorageClassParameters map[string]string +} + +func (t *DynamicallyProvisionedResizeVolumeTest) Run(ctx context.Context, client clientset.Interface, namespace *v1.Namespace) { + for _, pod := range t.Pods { + tpod, cleanup := pod.SetupWithDynamicVolumes(ctx, client, namespace, t.CSIDriver, t.StorageClassParameters) + // defer must be called here for resources not get removed before using them + for i := range cleanup { + defer cleanup[i](ctx) + } + + ginkgo.By("deploying the pod") + tpod.Create(ctx) + defer tpod.Cleanup(ctx) + ginkgo.By("checking that the pods command exits with no error") + tpod.WaitForSuccess(ctx) + + pvcName := tpod.pod.Spec.Volumes[0].VolumeSource.PersistentVolumeClaim.ClaimName + pvc, err := client.CoreV1().PersistentVolumeClaims(namespace.Name).Get(ctx, pvcName, metav1.GetOptions{}) + if err != nil { + framework.ExpectNoError(err, fmt.Sprintf("fail to get original pvc(%s): %v", pvcName, err)) + } + + originalSize := pvc.Spec.Resources.Requests["storage"] + delta := resource.Quantity{} + delta.Set(1024 * 1024 * 1024) + originalSize.Add(delta) + pvc.Spec.Resources.Requests["storage"] = originalSize + + ginkgo.By("resizing the pvc") + updatedPvc, err := client.CoreV1().PersistentVolumeClaims(namespace.Name).Update(ctx, pvc, metav1.UpdateOptions{}) + if err != nil { + framework.ExpectNoError(err, fmt.Sprintf("fail to resize pvc(%s): %v", pvcName, err)) + } + updatedSize := updatedPvc.Spec.Resources.Requests["storage"] + + ginkgo.By("sleep 30s waiting for resize complete") + time.Sleep(30 * time.Second) + + ginkgo.By("checking the resizing result") + newPvc, err := client.CoreV1().PersistentVolumeClaims(namespace.Name).Get(ctx, tpod.pod.Spec.Volumes[0].VolumeSource.PersistentVolumeClaim.ClaimName, metav1.GetOptions{}) + if err != nil { + framework.ExpectNoError(err, fmt.Sprintf("fail to get new pvc(%s): %v", pvcName, err)) + } + newSize := newPvc.Spec.Resources.Requests["storage"] + if !newSize.Equal(updatedSize) { + framework.Failf("newSize(%+v) is not equal to updatedSize(%+v)", newSize, updatedSize) + } + + ginkgo.By("checking the resizing PV result") + newPv, _ := client.CoreV1().PersistentVolumes().Get(ctx, updatedPvc.Spec.VolumeName, metav1.GetOptions{}) + newPvSize := newPv.Spec.Capacity["storage"] + newPvSizeStr := newPvSize.String() + "Gi" + + if !strings.Contains(newPvSizeStr, newSize.String()) { + framework.Failf("newPVCSize(%+v) is not equal to newPVSize(%+v)", newSize.String(), newPvSizeStr) + } + } +} diff --git a/test/external-e2e/testdriver.yaml b/test/external-e2e/testdriver.yaml index f31623cd8..4e907eb60 100644 --- a/test/external-e2e/testdriver.yaml +++ b/test/external-e2e/testdriver.yaml @@ -16,6 +16,8 @@ DriverInfo: fsGroup: true pvcDataSource: true snapshotDataSource: true + controllerExpansion: true + nodeExpansion: true InlineVolumes: - Attributes: server: nfs-server.default.svc.cluster.local