Files
portainer/api/kubernetes/cli/persistent_volume_claims_test.go

379 lines
13 KiB
Go

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)
})
}