package cli import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" storagev1 "k8s.io/api/storage/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kfake "k8s.io/client-go/kubernetes/fake" ) func makeStorageClass(name string, allowExpansion bool) *storagev1.StorageClass { return &storagev1.StorageClass{ ObjectMeta: metav1.ObjectMeta{Name: name}, Provisioner: "kubernetes.io/no-provisioner", AllowVolumeExpansion: &allowExpansion, } } func makePVC(namespace, name, scName string) *corev1.PersistentVolumeClaim { storageRequest := resource.MustParse("1Gi") pvc := &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, Spec: corev1.PersistentVolumeClaimSpec{ Resources: corev1.VolumeResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceStorage: storageRequest, }, }, }, } if scName != "" { pvc.Spec.StorageClassName = &scName } return pvc } func TestResizePersistentVolumeClaim(t *testing.T) { t.Parallel() t.Run("returns error when storage class does not allow expansion", func(t *testing.T) { t.Parallel() k := NewTestKubeClient(kfake.NewClientset()) sc := makeStorageClass("no-expand", false) _, err := k.cli.StorageV1().StorageClasses().Create(t.Context(), sc, metav1.CreateOptions{}) require.NoError(t, err) pvc := makePVC("default", "pvc-no-expand", "no-expand") _, err = k.cli.CoreV1().PersistentVolumeClaims("default").Create(t.Context(), pvc, metav1.CreateOptions{}) require.NoError(t, err) err = k.ResizePersistentVolumeClaim("default", "pvc-no-expand", "2Gi") require.Error(t, err) assert.Contains(t, err.Error(), "does not allow volume expansion") }) t.Run("returns error for invalid size format", func(t *testing.T) { t.Parallel() k := NewTestKubeClient(kfake.NewClientset()) sc := makeStorageClass("expandable", true) _, err := k.cli.StorageV1().StorageClasses().Create(t.Context(), sc, metav1.CreateOptions{}) require.NoError(t, err) pvc := makePVC("default", "pvc-badsize", "expandable") _, err = k.cli.CoreV1().PersistentVolumeClaims("default").Create(t.Context(), pvc, metav1.CreateOptions{}) require.NoError(t, err) err = k.ResizePersistentVolumeClaim("default", "pvc-badsize", "notasize") require.Error(t, err) assert.Contains(t, err.Error(), "invalid size format") }) t.Run("success when storage class allows expansion with valid size", func(t *testing.T) { t.Parallel() k := NewTestKubeClient(kfake.NewClientset()) sc := makeStorageClass("expandable", true) _, err := k.cli.StorageV1().StorageClasses().Create(t.Context(), sc, metav1.CreateOptions{}) require.NoError(t, err) pvc := makePVC("default", "pvc-resize", "expandable") _, err = k.cli.CoreV1().PersistentVolumeClaims("default").Create(t.Context(), pvc, metav1.CreateOptions{}) require.NoError(t, err) err = k.ResizePersistentVolumeClaim("default", "pvc-resize", "5Gi") require.NoError(t, err) }) t.Run("success when PVC has no storage class", func(t *testing.T) { t.Parallel() k := NewTestKubeClient(kfake.NewClientset()) pvc := makePVC("default", "pvc-no-sc", "") _, err := k.cli.CoreV1().PersistentVolumeClaims("default").Create(t.Context(), pvc, metav1.CreateOptions{}) require.NoError(t, err) err = k.ResizePersistentVolumeClaim("default", "pvc-no-sc", "10Gi") require.NoError(t, err) }) t.Run("returns error for non-existent PVC", func(t *testing.T) { t.Parallel() k := NewTestKubeClient(kfake.NewClientset()) err := k.ResizePersistentVolumeClaim("default", "does-not-exist", "2Gi") require.Error(t, err) }) } func TestGetPersistentVolumeClaims_EnrichesExpansionFlag(t *testing.T) { t.Parallel() t.Run("PVC with AllowVolumeExpansion=true SC gets flag true", func(t *testing.T) { t.Parallel() k := NewTestKubeClient(kfake.NewClientset()) sc := makeStorageClass("expandable", true) _, err := k.cli.StorageV1().StorageClasses().Create(t.Context(), sc, metav1.CreateOptions{}) require.NoError(t, err) pvc := makePVC("default", "pvc-expand", "expandable") _, err = k.cli.CoreV1().PersistentVolumeClaims("default").Create(t.Context(), pvc, metav1.CreateOptions{}) require.NoError(t, err) pvcs, err := k.GetPersistentVolumeClaims("default") require.NoError(t, err) require.Len(t, pvcs, 1) assert.True(t, pvcs[0].AllowVolumeExpansion) }) t.Run("PVC with AllowVolumeExpansion=false SC gets flag false", func(t *testing.T) { t.Parallel() k := NewTestKubeClient(kfake.NewClientset()) sc := makeStorageClass("not-expandable", false) _, err := k.cli.StorageV1().StorageClasses().Create(t.Context(), sc, metav1.CreateOptions{}) require.NoError(t, err) pvc := makePVC("default", "pvc-no-expand", "not-expandable") _, err = k.cli.CoreV1().PersistentVolumeClaims("default").Create(t.Context(), pvc, metav1.CreateOptions{}) require.NoError(t, err) pvcs, err := k.GetPersistentVolumeClaims("default") require.NoError(t, err) require.Len(t, pvcs, 1) assert.False(t, pvcs[0].AllowVolumeExpansion) }) } func makePod(namespace, name, pvcName string) *corev1.Pod { return &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}, Spec: corev1.PodSpec{ Volumes: []corev1.Volume{ { Name: "data", VolumeSource: corev1.VolumeSource{ PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ ClaimName: pvcName, }, }, }, }, Containers: []corev1.Container{{Name: "app", Image: "nginx"}}, }, } } func makePodWithDeploymentOwner(namespace, name, pvcName, rsName string) *corev1.Pod { pod := makePod(namespace, name, pvcName) pod.OwnerReferences = []metav1.OwnerReference{ {Kind: "ReplicaSet", Name: rsName, APIVersion: "apps/v1"}, } return pod } func makeReplicaSet(namespace, name, deploymentName string) *appsv1.ReplicaSet { return &appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, OwnerReferences: []metav1.OwnerReference{ {Kind: "Deployment", Name: deploymentName, APIVersion: "apps/v1"}, }, }, } } func makeDeployment(namespace, name string) *appsv1.Deployment { return &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}, Spec: appsv1.DeploymentSpec{ Selector: &metav1.LabelSelector{}, }, } } func TestCombineClaimsWithApplications(t *testing.T) { t.Parallel() t.Run("no pods leaves owningApplications empty", func(t *testing.T) { t.Parallel() k := NewTestKubeClient(kfake.NewClientset()) pvc := makePVC("default", "my-pvc", "") _, err := k.cli.CoreV1().PersistentVolumeClaims("default").Create(t.Context(), pvc, metav1.CreateOptions{}) require.NoError(t, err) pvcs, err := k.GetPersistentVolumeClaims("default") require.NoError(t, err) result, err := k.CombineClaimsWithApplications(pvcs) require.NoError(t, err) require.Len(t, result, 1) assert.Empty(t, result[0].OwningApplications) }) t.Run("pod with no PVC volumes leaves owningApplications empty", func(t *testing.T) { t.Parallel() k := NewTestKubeClient(kfake.NewClientset()) pvc := makePVC("default", "my-pvc", "") _, err := k.cli.CoreV1().PersistentVolumeClaims("default").Create(t.Context(), pvc, metav1.CreateOptions{}) require.NoError(t, err) pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "app", Namespace: "default"}, Spec: corev1.PodSpec{ Containers: []corev1.Container{{Name: "app", Image: "nginx"}}, }, } _, err = k.cli.CoreV1().Pods("default").Create(t.Context(), pod, metav1.CreateOptions{}) require.NoError(t, err) pvcs, err := k.GetPersistentVolumeClaims("default") require.NoError(t, err) result, err := k.CombineClaimsWithApplications(pvcs) require.NoError(t, err) assert.Empty(t, result[0].OwningApplications) }) t.Run("pod mounting PVC populates owningApplications", func(t *testing.T) { t.Parallel() k := NewTestKubeClient(kfake.NewClientset()) pvc := makePVC("default", "my-pvc", "") _, err := k.cli.CoreV1().PersistentVolumeClaims("default").Create(t.Context(), pvc, metav1.CreateOptions{}) require.NoError(t, err) pod := makePod("default", "my-pod", "my-pvc") _, err = k.cli.CoreV1().Pods("default").Create(t.Context(), pod, metav1.CreateOptions{}) require.NoError(t, err) pvcs, err := k.GetPersistentVolumeClaims("default") require.NoError(t, err) result, err := k.CombineClaimsWithApplications(pvcs) require.NoError(t, err) require.Len(t, result[0].OwningApplications, 1) assert.Equal(t, "my-pod", result[0].OwningApplications[0].Name) assert.Equal(t, "Pod", result[0].OwningApplications[0].ApplicationType) assert.Equal(t, "default", result[0].OwningApplications[0].ResourcePool) }) t.Run("pod in different namespace does not match PVC", func(t *testing.T) { t.Parallel() k := NewTestKubeClient(kfake.NewClientset()) pvc := makePVC("default", "my-pvc", "") _, err := k.cli.CoreV1().PersistentVolumeClaims("default").Create(t.Context(), pvc, metav1.CreateOptions{}) require.NoError(t, err) pod := makePod("other-ns", "other-pod", "my-pvc") _, err = k.cli.CoreV1().Pods("other-ns").Create(t.Context(), pod, metav1.CreateOptions{}) require.NoError(t, err) pvcs, err := k.GetPersistentVolumeClaims("default") require.NoError(t, err) result, err := k.CombineClaimsWithApplications(pvcs) require.NoError(t, err) assert.Empty(t, result[0].OwningApplications) }) t.Run("two pods mounting the same PVC from the same Deployment are deduplicated", func(t *testing.T) { t.Parallel() k := NewTestKubeClient(kfake.NewClientset()) pvc := makePVC("default", "shared-pvc", "") _, err := k.cli.CoreV1().PersistentVolumeClaims("default").Create(t.Context(), pvc, metav1.CreateOptions{}) require.NoError(t, err) deploy := makeDeployment("default", "my-deploy") _, err = k.cli.AppsV1().Deployments("default").Create(t.Context(), deploy, metav1.CreateOptions{}) require.NoError(t, err) rs := makeReplicaSet("default", "my-deploy-rs", "my-deploy") _, err = k.cli.AppsV1().ReplicaSets("default").Create(t.Context(), rs, metav1.CreateOptions{}) require.NoError(t, err) pod1 := makePodWithDeploymentOwner("default", "my-deploy-pod-1", "shared-pvc", "my-deploy-rs") pod2 := makePodWithDeploymentOwner("default", "my-deploy-pod-2", "shared-pvc", "my-deploy-rs") _, err = k.cli.CoreV1().Pods("default").Create(t.Context(), pod1, metav1.CreateOptions{}) require.NoError(t, err) _, err = k.cli.CoreV1().Pods("default").Create(t.Context(), pod2, metav1.CreateOptions{}) require.NoError(t, err) pvcs, err := k.GetPersistentVolumeClaims("default") require.NoError(t, err) result, err := k.CombineClaimsWithApplications(pvcs) require.NoError(t, err) require.Len(t, result[0].OwningApplications, 1) assert.Equal(t, "my-deploy", result[0].OwningApplications[0].Name) assert.Equal(t, "Deployment", result[0].OwningApplications[0].ApplicationType) }) t.Run("pod owned by Deployment resolves application name to Deployment", func(t *testing.T) { t.Parallel() k := NewTestKubeClient(kfake.NewClientset()) pvc := makePVC("default", "app-pvc", "") _, err := k.cli.CoreV1().PersistentVolumeClaims("default").Create(t.Context(), pvc, metav1.CreateOptions{}) require.NoError(t, err) deploy := makeDeployment("default", "web-app") _, err = k.cli.AppsV1().Deployments("default").Create(t.Context(), deploy, metav1.CreateOptions{}) require.NoError(t, err) rs := makeReplicaSet("default", "web-app-rs-abc", "web-app") _, err = k.cli.AppsV1().ReplicaSets("default").Create(t.Context(), rs, metav1.CreateOptions{}) require.NoError(t, err) pod := makePodWithDeploymentOwner("default", "web-app-pod-xyz", "app-pvc", "web-app-rs-abc") _, err = k.cli.CoreV1().Pods("default").Create(t.Context(), pod, metav1.CreateOptions{}) require.NoError(t, err) pvcs, err := k.GetPersistentVolumeClaims("default") require.NoError(t, err) result, err := k.CombineClaimsWithApplications(pvcs) require.NoError(t, err) require.Len(t, result[0].OwningApplications, 1) assert.Equal(t, "web-app", result[0].OwningApplications[0].Name) assert.Equal(t, "Deployment", result[0].OwningApplications[0].ApplicationType) assert.Equal(t, "default", result[0].OwningApplications[0].ResourcePool) }) t.Run("unrelated pod mounting a different PVC does not affect result", func(t *testing.T) { t.Parallel() k := NewTestKubeClient(kfake.NewClientset()) pvc := makePVC("default", "target-pvc", "") _, err := k.cli.CoreV1().PersistentVolumeClaims("default").Create(t.Context(), pvc, metav1.CreateOptions{}) require.NoError(t, err) pod := makePod("default", "unrelated-pod", "other-pvc") _, err = k.cli.CoreV1().Pods("default").Create(t.Context(), pod, metav1.CreateOptions{}) require.NoError(t, err) pvcs, err := k.GetPersistentVolumeClaims("default") require.NoError(t, err) result, err := k.CombineClaimsWithApplications(pvcs) require.NoError(t, err) assert.Empty(t, result[0].OwningApplications) }) }