containerized-data-importer/pkg/controller/datavolume/util_test.go
Felix Matouschek e1a553a052
feat(DataVolume): Add default instance type labels from VolumeSnapshots and from Registry imports (#3381)
* feat(DataVolume): Add default instance type labels from VolumeSnapshots

de5ba85 started updating DataVolumes when reconciled, adding any labels
found on PVC or DataSource sources. This commit is a follow up that
starts adding labels also from VolumeSnapshots.

Signed-off-by: Felix Matouschek <fmatouschek@redhat.com>

* feat(DataVolume): Add instance type labels from Registry imports

de5ba85 started updating DataVolumes when reconciled, adding any labels
found on PVC or DataSource sources. This commit is a follow up that
starts adding labels also from Registry imported PVCs.

Signed-off-by: Felix Matouschek <fmatouschek@redhat.com>

---------

Signed-off-by: Felix Matouschek <fmatouschek@redhat.com>
2024-08-14 23:02:14 +02:00

384 lines
12 KiB
Go

package datavolume
import (
"context"
"strconv"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1"
ocpconfigv1 "github.com/openshift/api/config/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1"
. "kubevirt.io/containerized-data-importer/pkg/controller/common"
)
var _ = Describe("renderPvcSpecVolumeSize", func() {
client := createClient()
volumeSize := resource.MustParse("1G")
scName := "test"
It("Should return empty volume size on clone PVC with empty storage size", func() {
pvcSpec := &corev1.PersistentVolumeClaimSpec{}
err := renderPvcSpecVolumeSize(client, pvcSpec, true)
Expect(err).ToNot(HaveOccurred())
requestedVolumeSize, found := pvcSpec.Resources.Requests[corev1.ResourceStorage]
Expect(found).To(BeTrue())
Expect(requestedVolumeSize.IsZero()).To(BeTrue())
})
It("Should return error on non-clone PVC with empty storage size", func() {
pvcSpec := &corev1.PersistentVolumeClaimSpec{}
err := renderPvcSpecVolumeSize(client, pvcSpec, false)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("PVC Spec is not valid - missing storage size"))
_, found := pvcSpec.Resources.Requests[corev1.ResourceStorage]
Expect(found).To(BeFalse())
})
It("Should return error on PVC with storage size smaller than 1MiB", func() {
volumeMode := corev1.PersistentVolumeBlock
pvcSpec := &corev1.PersistentVolumeClaimSpec{
StorageClassName: &scName,
VolumeMode: &volumeMode,
Resources: corev1.VolumeResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceStorage: resource.MustParse("9"),
},
},
}
err := renderPvcSpecVolumeSize(client, pvcSpec, false)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("PVC Spec is not valid - storage size should be at least 1MiB"))
})
It("Should return the same volume size (block volume mode)", func() {
volumeMode := corev1.PersistentVolumeBlock
pvcSpec := &corev1.PersistentVolumeClaimSpec{
StorageClassName: &scName,
VolumeMode: &volumeMode,
Resources: corev1.VolumeResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceStorage: volumeSize,
},
},
}
err := renderPvcSpecVolumeSize(client, pvcSpec, false)
Expect(err).ToNot(HaveOccurred())
requestedVolumeSize, found := pvcSpec.Resources.Requests[corev1.ResourceStorage]
Expect(found).To(BeTrue())
Expect(requestedVolumeSize.Value()).To(Equal(volumeSize.Value()))
})
It("Should return the inflated volume size (filesystem volume mode)", func() {
volumeMode := corev1.PersistentVolumeFilesystem
pvcSpec := &corev1.PersistentVolumeClaimSpec{
StorageClassName: &scName,
VolumeMode: &volumeMode,
Resources: corev1.VolumeResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceStorage: volumeSize,
},
},
}
err := renderPvcSpecVolumeSize(client, pvcSpec, false)
Expect(err).ToNot(HaveOccurred())
requestedVolumeSize, found := pvcSpec.Resources.Requests[corev1.ResourceStorage]
Expect(found).To(BeTrue())
// Inflate expected size with overhead
fsOverhead, err := GetFilesystemOverheadForStorageClass(context.TODO(), client, pvcSpec.StorageClassName)
Expect(err).ToNot(HaveOccurred())
fsOverheadFloat, _ := strconv.ParseFloat(string(fsOverhead), 64)
requiredSpace := GetRequiredSpace(fsOverheadFloat, volumeSize.Value())
expectedResult := resource.NewScaledQuantity(requiredSpace, 0)
Expect(requestedVolumeSize.Value()).To(BeNumerically(">", volumeSize.Value()))
Expect(requestedVolumeSize.Value()).To(Equal(expectedResult.Value()))
})
})
var _ = Describe("updateDataVolumeDefaultInstancetypeLabels", func() {
const (
namespace = "namespace"
sourcePVCName = "sourcePVC"
sourceSnapshotName = "sourceSnapshot"
sourceRegistryName = "sourceRegistry"
sourceDataSourceName = "sourceDataSource"
)
var (
fakeClient client.Client
dataVolumeWithSourcePVC cdiv1.DataVolume
dataVolumeWithSourceSnapshot cdiv1.DataVolume
dataVolumeWithSourceRegistry cdiv1.DataVolume
dataVolumeWithSourceDataSource cdiv1.DataVolume
)
defaultInstancetypeLabelMap := map[string]string{
LabelDefaultInstancetype: LabelDefaultInstancetype,
LabelDefaultInstancetypeKind: LabelDefaultInstancetypeKind,
LabelDefaultPreference: LabelDefaultPreference,
LabelDefaultPreferenceKind: LabelDefaultPreferenceKind,
}
BeforeEach(func() {
dataVolumeWithSourcePVC = cdiv1.DataVolume{
ObjectMeta: metav1.ObjectMeta{
Name: "pvc-datavolume",
Namespace: namespace,
},
Spec: cdiv1.DataVolumeSpec{
Source: &cdiv1.DataVolumeSource{
PVC: &cdiv1.DataVolumeSourcePVC{
Name: sourcePVCName,
Namespace: namespace,
},
},
},
}
dataVolumeWithSourceSnapshot = cdiv1.DataVolume{
ObjectMeta: metav1.ObjectMeta{
Name: "snapshot-datavolume",
Namespace: namespace,
},
Spec: cdiv1.DataVolumeSpec{
Source: &cdiv1.DataVolumeSource{
Snapshot: &cdiv1.DataVolumeSourceSnapshot{
Name: sourceSnapshotName,
Namespace: namespace,
},
},
},
}
dataVolumeWithSourceRegistry = cdiv1.DataVolume{
ObjectMeta: metav1.ObjectMeta{
Name: sourceRegistryName,
Namespace: namespace,
},
Spec: cdiv1.DataVolumeSpec{
Source: &cdiv1.DataVolumeSource{
Registry: &cdiv1.DataVolumeSourceRegistry{
URL: ptr.To("docker://testurl"),
},
},
},
}
dataVolumeWithSourceDataSource = cdiv1.DataVolume{
ObjectMeta: metav1.ObjectMeta{
Name: "datasource-datavolume",
Namespace: namespace,
},
Spec: cdiv1.DataVolumeSpec{
SourceRef: &cdiv1.DataVolumeSourceRef{
Kind: cdiv1.DataVolumeDataSource,
Name: sourceDataSourceName,
Namespace: ptr.To[string](namespace),
},
},
}
fakeClient = createClient(
&corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: sourcePVCName,
Namespace: namespace,
Labels: defaultInstancetypeLabelMap,
},
},
&snapshotv1.VolumeSnapshot{
ObjectMeta: metav1.ObjectMeta{
Name: sourceSnapshotName,
Namespace: namespace,
Labels: defaultInstancetypeLabelMap,
},
},
&corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: sourceRegistryName,
Namespace: namespace,
Labels: defaultInstancetypeLabelMap,
},
},
&cdiv1.DataSource{
ObjectMeta: metav1.ObjectMeta{
Name: sourceDataSourceName,
Namespace: namespace,
Labels: defaultInstancetypeLabelMap,
},
Spec: cdiv1.DataSourceSpec{
Source: cdiv1.DataSourceSource{
PVC: &cdiv1.DataVolumeSourcePVC{
Name: sourcePVCName,
},
},
},
},
)
})
DescribeTable("should update DataVolume with labels from source using", func(dataVolume *cdiv1.DataVolume) {
syncState := &dvSyncState{
dvMutated: dataVolume,
}
Expect(syncState.dvMutated.Labels).To(BeEmpty())
Expect(updateDataVolumeDefaultInstancetypeLabels(fakeClient, syncState)).To(Succeed())
Expect(syncState.dvMutated.Labels).ToNot(BeEmpty())
for k, v := range defaultInstancetypeLabelMap {
Expect(syncState.dvMutated.Labels).To(HaveKeyWithValue(k, v))
}
},
Entry("PVC", &dataVolumeWithSourcePVC),
Entry("Snapshot", &dataVolumeWithSourceSnapshot),
Entry("Registry", &dataVolumeWithSourceRegistry),
Entry("dataSource", &dataVolumeWithSourceDataSource),
)
DescribeTable("should not update DataVolume with labels from source if already present using", func(dataVolume *cdiv1.DataVolume) {
const customDefaultInstancetype = "customDefaultInstancetype"
dv := dataVolume
dv.Labels = map[string]string{
LabelDefaultInstancetype: customDefaultInstancetype,
}
syncState := &dvSyncState{
dvMutated: dv,
}
Expect(updateDataVolumeDefaultInstancetypeLabels(fakeClient, syncState)).To(Succeed())
Expect(syncState.dvMutated.Labels).To(HaveLen(1))
Expect(syncState.dvMutated.Labels).To(HaveKeyWithValue(LabelDefaultInstancetype, customDefaultInstancetype))
},
Entry("PVC", &dataVolumeWithSourcePVC),
Entry("Snapshot", &dataVolumeWithSourceSnapshot),
Entry("Registry", &dataVolumeWithSourceRegistry),
Entry("DataSource", &dataVolumeWithSourceDataSource),
)
DescribeTable("should ignore IsNotFound errors", func(dataVolume *cdiv1.DataVolume) {
syncState := &dvSyncState{
dvMutated: dataVolume,
}
Expect(updateDataVolumeDefaultInstancetypeLabels(fakeClient, syncState)).To(Succeed())
},
Entry("PVC", &cdiv1.DataVolume{
ObjectMeta: metav1.ObjectMeta{
Name: "pvc-datavolume",
Namespace: namespace,
},
Spec: cdiv1.DataVolumeSpec{
Source: &cdiv1.DataVolumeSource{
PVC: &cdiv1.DataVolumeSourcePVC{
Name: "unknown",
Namespace: namespace,
},
},
},
}),
Entry("Snapshot", &cdiv1.DataVolume{
ObjectMeta: metav1.ObjectMeta{
Name: "snapshot-datavolume",
Namespace: namespace,
},
Spec: cdiv1.DataVolumeSpec{
Source: &cdiv1.DataVolumeSource{
Snapshot: &cdiv1.DataVolumeSourceSnapshot{
Name: "unknown",
Namespace: namespace,
},
},
},
}),
Entry("Registry", &cdiv1.DataVolume{
ObjectMeta: metav1.ObjectMeta{
Name: "registry-datavolume-unknown",
Namespace: namespace,
},
Spec: cdiv1.DataVolumeSpec{
Source: &cdiv1.DataVolumeSource{
Registry: &cdiv1.DataVolumeSourceRegistry{
URL: ptr.To("docker://uknown"),
},
},
},
}),
Entry("DataSource", &cdiv1.DataVolume{
ObjectMeta: metav1.ObjectMeta{
Name: "datasource-datavolume",
Namespace: namespace,
},
Spec: cdiv1.DataVolumeSpec{
SourceRef: &cdiv1.DataVolumeSourceRef{
Kind: cdiv1.DataVolumeDataSource,
Name: "unknown",
Namespace: ptr.To[string](namespace),
},
},
}),
)
DescribeTable("should return all non IsNotFound errors", func(dataVolume *cdiv1.DataVolume) {
err := updateDataVolumeDefaultInstancetypeLabels(
fakeClientWithGetServiceUnavailableErr{
fakeClient,
},
&dvSyncState{
dvMutated: dataVolume,
},
)
Expect(errors.IsServiceUnavailable(err)).To(BeTrue())
},
Entry("PVC", &dataVolumeWithSourcePVC),
Entry("Snapshot", &dataVolumeWithSourceSnapshot),
Entry("Registry", &dataVolumeWithSourceRegistry),
Entry("DataSource", &dataVolumeWithSourceDataSource),
)
})
func createDataVolumeWithStorageAPI(name, ns string, source *cdiv1.DataVolumeSource, storageSpec *cdiv1.StorageSpec) *cdiv1.DataVolume {
return &cdiv1.DataVolume{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: ns,
},
Spec: cdiv1.DataVolumeSpec{
Source: source,
Storage: storageSpec,
},
}
}
func createClient(objs ...client.Object) client.Client {
// Register cdi types with the runtime scheme.
s := scheme.Scheme
_ = cdiv1.AddToScheme(s)
// Register other types with the runtime scheme.
_ = ocpconfigv1.Install(s)
_ = snapshotv1.AddToScheme(s)
// Create a fake client to mock API calls.
return fake.NewClientBuilder().WithScheme(s).WithObjects(objs...).Build()
}
type fakeClientWithGetServiceUnavailableErr struct {
client.Client
}
func (c fakeClientWithGetServiceUnavailableErr) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
return errors.NewServiceUnavailable("error")
}