containerized-data-importer/pkg/apiserver/webhooks/datavolume-validate_test.go
Michael Henriksen 8cb11f53d9
update k8s libs to 1.26. (#2687)
* update k8s libs to 1.26.

Signed-off-by: Michael Henriksen <mhenriks@redhat.com>

* remove some checks in log messages, they're redundant, and the format has changed

Signed-off-by: Michael Henriksen <mhenriks@redhat.com>

* use 1.26 lib function `CheckVolumeModeMismatches` and `CheckAccessModes`

Signed-off-by: Michael Henriksen <mhenriks@redhat.com>

---------

Signed-off-by: Michael Henriksen <mhenriks@redhat.com>
2023-04-18 19:30:40 +01:00

1134 lines
38 KiB
Go

/*
* This file is part of the CDI project
*
* 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.
*
* Copyright 2019 Red Hat, Inc.
*
*/
package webhooks
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega"
snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1"
admissionv1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
k8sv1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
fakeclient "k8s.io/client-go/kubernetes/fake"
snapclientfake "github.com/kubernetes-csi/external-snapshotter/client/v6/clientset/versioned/fake"
cdiclientfake "kubevirt.io/containerized-data-importer/pkg/client/clientset/versioned/fake"
cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1"
)
var (
testNamespace = "testNamespace"
emptyNamespace = ""
)
var _ = Describe("Validating Webhook", func() {
Context("with DataVolume admission review", func() {
It("should accept DataVolume with HTTP source on create", func() {
dataVolume := newHTTPDataVolume("testDV", "http://www.example.com")
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(true))
})
It("should accept DataVolume with GS source on create", func() {
dataVolume := newGCSDataVolume("testDV", "gs://www.example.com")
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(true))
})
It("should reject DataVolume with GCS source on create", func() {
dataVolume := newGCSDataVolume("testDV", "gcs://www.example.com")
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should reject DataVolume when target pvc exists", func() {
dataVolume := newPVCDataVolume("testDV", "testNamespace", "test")
pvc := &corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: dataVolume.Name,
Namespace: dataVolume.Namespace,
},
Spec: *dataVolume.Spec.PVC,
}
resp := validateDataVolumeCreate(dataVolume, pvc)
Expect(resp.Allowed).To(Equal(false))
})
It("should accept DataVolume with Registry source URL on create", func() {
dataVolume := newRegistryDataVolume("testDV", "docker://registry:5000/test")
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(true))
})
It("should accept DataVolume with Registry source ImageStream and node PullMethod on create", func() {
imageStream := "istream"
pullNode := cdiv1.RegistryPullNode
registrySource := cdiv1.DataVolumeSource{
Registry: &cdiv1.DataVolumeSourceRegistry{ImageStream: &imageStream, PullMethod: &pullNode},
}
pvc := newPVCSpec(pvcSizeDefault)
dataVolume := newDataVolume("testDV", registrySource, pvc)
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(true))
})
It("should reject DataVolume with Registry source ImageStream and pod PullMethod on create", func() {
imageStream := "istream"
registrySource := cdiv1.DataVolumeSource{
Registry: &cdiv1.DataVolumeSourceRegistry{ImageStream: &imageStream},
}
pvc := newPVCSpec(pvcSizeDefault)
dataVolume := newDataVolume("testDV", registrySource, pvc)
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should reject DataVolume with Registry source on create with no url or ImageStream", func() {
registrySource := cdiv1.DataVolumeSource{}
pvc := newPVCSpec(pvcSizeDefault)
dataVolume := newDataVolume("testDV", registrySource, pvc)
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should reject DataVolume with Registry source on create with both url and ImageStream", func() {
url := "docker://registry:5000/test"
imageStream := "istream"
registrySource := cdiv1.DataVolumeSource{
Registry: &cdiv1.DataVolumeSourceRegistry{URL: &url, ImageStream: &imageStream},
}
pvc := newPVCSpec(pvcSizeDefault)
dataVolume := newDataVolume("testDV", registrySource, pvc)
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should reject DataVolume with Registry source on create with non-kubevirt contentType", func() {
dataVolume := newRegistryDataVolume("testDV", "docker://registry:5000/test")
dataVolume.Spec.ContentType = cdiv1.DataVolumeArchive
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should reject DataVolume with Registry source on create with illegal source URL", func() {
dataVolume := newRegistryDataVolume("testDV", "docker/::registry:5000/test")
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should reject DataVolume with Registry source on create with illegal transport in source URL", func() {
dataVolume := newRegistryDataVolume("testDV", "joker://registry:5000/test")
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should reject DataVolume with Registry source on create with illegal importMethod", func() {
pullMethod := cdiv1.RegistryPullMethod("nosuch")
dataVolume := newRegistryDataVolume("testDV", "docker://registry:5000/test")
dataVolume.Spec.Source.Registry.PullMethod = &pullMethod
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should accept DataVolume with Registry source on create with supported importMethod", func() {
pullMethod := cdiv1.RegistryPullNode
dataVolume := newRegistryDataVolume("testDV", "docker://registry:5000/test")
dataVolume.Spec.Source.Registry.PullMethod = &pullMethod
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(true))
})
It("should accept DataVolume with PVC source on create", func() {
dataVolume := newPVCDataVolume("testDV", "testNamespace", "test")
pvc := &corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: dataVolume.Spec.Source.PVC.Name,
Namespace: dataVolume.Spec.Source.PVC.Namespace,
},
Spec: *dataVolume.Spec.PVC,
}
resp := validateDataVolumeCreate(dataVolume, pvc)
Expect(resp.Allowed).To(Equal(true))
})
It("should accept DataVolume with PVC initialized create", func() {
dataVolume := newHTTPDataVolume("testDV", "http://www.example.com")
pvc := &corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: dataVolume.Name,
Namespace: dataVolume.Namespace,
Annotations: map[string]string{
"cdi.kubevirt.io/storage.populatedFor": dataVolume.Name,
},
},
Spec: *dataVolume.Spec.PVC,
}
resp := validateDataVolumeCreate(dataVolume, pvc)
Expect(resp.Allowed).To(Equal(true))
})
It("should accept DataVolume with PVC source on create if PVC does not exist", func() {
dataVolume := newPVCDataVolume("testDV", "testNamespace", "test")
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(true))
})
It("should reject invalid DataVolume source PVC namespace on create", func() {
dataVolume := newPVCDataVolume("testDV", "", "test")
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should reject invalid DataVolume source PVC name on create", func() {
dataVolume := newPVCDataVolume("testDV", "testNamespace", "")
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should reject DataVolume with name length greater than 253 characters", func() {
longName := "the-name-length-of-this-datavolume-is-greater-then-253-characters" +
"123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-" +
"123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789"
dataVolume := newHTTPDataVolume(
longName,
"http://www.example.com")
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should reject DataVolume source with invalid URL on create", func() {
dataVolume := newHTTPDataVolume("testDV", "invalidurl")
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should reject DataVolume with multiple sources on create", func() {
dataVolume := newDataVolumeWithMultipleSources("testDV")
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should reject DataVolume with empty PVC create", func() {
dataVolume := newDataVolumeWithEmptyPVCSpec("testDV", "http://www.example.com")
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should reject DataVolume with PVC size 0", func() {
dataVolume := newDataVolumeWithPVCSizeZero("testDV", "http://www.example.com")
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should accept DataVolume with Blank source and no content type", func() {
dataVolume := newBlankDataVolume("blank")
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(true))
})
It("should accept DataVolume with Blank source and kubevirt contentType", func() {
dataVolume := newBlankDataVolume("blank")
dataVolume.Spec.ContentType = cdiv1.DataVolumeKubeVirt
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(true))
})
It("should reject DataVolume with Blank source and archive contentType", func() {
dataVolume := newBlankDataVolume("blank")
dataVolume.Spec.ContentType = cdiv1.DataVolumeArchive
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should reject DataVolume with invalid contentType", func() {
dataVolume := newHTTPDataVolume("testDV", "http://www.example.com")
dataVolume.Spec.ContentType = "invalid"
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should accept DataVolume with archive contentType", func() {
dataVolume := newHTTPDataVolume("testDV", "http://www.example.com")
dataVolume.Spec.ContentType = cdiv1.DataVolumeArchive
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(true))
})
It("should reject invalid DataVolume spec update", func() {
newDataVolume := newPVCDataVolume("testDV", "newNamespace", "testName")
newBytes, _ := json.Marshal(&newDataVolume)
oldDataVolume := newDataVolume.DeepCopy()
oldDataVolume.Spec.Source.PVC.Namespace = "oldNamespace"
oldBytes, _ := json.Marshal(oldDataVolume)
ar := &admissionv1.AdmissionReview{
Request: &admissionv1.AdmissionRequest{
Operation: admissionv1.Update,
Resource: metav1.GroupVersionResource{
Group: cdiv1.SchemeGroupVersion.Group,
Version: cdiv1.SchemeGroupVersion.Version,
Resource: "datavolumes",
},
Object: runtime.RawExtension{
Raw: newBytes,
},
OldObject: runtime.RawExtension{
Raw: oldBytes,
},
},
}
resp := validateAdmissionReview(ar)
Expect(resp.Allowed).To(Equal(false))
})
It("should accept object meta update", func() {
newDataVolume := newPVCDataVolume("testDV", "newNamespace", "testName")
newBytes, _ := json.Marshal(&newDataVolume)
oldDataVolume := newDataVolume.DeepCopy()
oldDataVolume.Annotations = map[string]string{"foo": "bar"}
oldBytes, _ := json.Marshal(oldDataVolume)
ar := &admissionv1.AdmissionReview{
Request: &admissionv1.AdmissionRequest{
Operation: admissionv1.Update,
Resource: metav1.GroupVersionResource{
Group: cdiv1.SchemeGroupVersion.Group,
Version: cdiv1.SchemeGroupVersion.Version,
Resource: "datavolumes",
},
Object: runtime.RawExtension{
Raw: newBytes,
},
OldObject: runtime.RawExtension{
Raw: oldBytes,
},
},
}
resp := validateAdmissionReview(ar)
Expect(resp.Allowed).To(Equal(true))
})
It("should reject DataVolume spec PVC size update", func() {
blankSource := cdiv1.DataVolumeSource{
Blank: &cdiv1.DataVolumeBlankImage{},
}
pvc := newPVCSpec(pvcSizeDefault)
newDataVolume := newDataVolume("testDv", blankSource, pvc)
newBytes, _ := json.Marshal(&newDataVolume)
oldDataVolume := newDataVolume.DeepCopy()
oldDataVolume.Spec.PVC.Resources.Requests["storage"] =
*resource.NewQuantity(pvcSizeDefault+1, resource.BinarySI)
oldBytes, _ := json.Marshal(oldDataVolume)
ar := &admissionv1.AdmissionReview{
Request: &admissionv1.AdmissionRequest{
Operation: admissionv1.Update,
Resource: metav1.GroupVersionResource{
Group: cdiv1.SchemeGroupVersion.Group,
Version: cdiv1.SchemeGroupVersion.Version,
Resource: "datavolumes",
},
Object: runtime.RawExtension{
Raw: newBytes,
},
OldObject: runtime.RawExtension{
Raw: oldBytes,
},
},
}
resp := validateAdmissionReview(ar)
Expect(resp.Allowed).To(Equal(false))
})
It("should accept DataVolume spec PVC size format update", func() {
blankSource := cdiv1.DataVolumeSource{
Blank: &cdiv1.DataVolumeBlankImage{},
}
pvc := newPVCSpec(pvcSizeDefault)
newDataVolume := newDataVolume("testDv", blankSource, pvc)
newBytes, _ := json.Marshal(&newDataVolume)
oldDataVolume := newDataVolume.DeepCopy()
oldDataVolume.Spec.PVC.Resources.Requests["storage"] =
*resource.NewQuantity(pvcSizeDefault, resource.DecimalSI)
oldBytes, _ := json.Marshal(oldDataVolume)
ar := &admissionv1.AdmissionReview{
Request: &admissionv1.AdmissionRequest{
Operation: admissionv1.Update,
Resource: metav1.GroupVersionResource{
Group: cdiv1.SchemeGroupVersion.Group,
Version: cdiv1.SchemeGroupVersion.Version,
Resource: "datavolumes",
},
Object: runtime.RawExtension{
Raw: newBytes,
},
OldObject: runtime.RawExtension{
Raw: oldBytes,
},
},
}
resp := validateAdmissionReview(ar)
Expect(resp.Allowed).To(Equal(true))
})
DescribeTable("should validate clones", func(storageSpec *cdiv1.StorageSpec, pvcSpec *corev1.PersistentVolumeClaimSpec, expected bool) {
dv, pvc := newDataVolumeClone(storageSpec, pvcSpec)
resp := validateDataVolumeCreate(dv, pvc)
Expect(resp.Allowed).To(Equal(expected))
},
Entry("should reject clones with both nil Storage and PVC spec", nil, nil, false),
Entry("should reject clones with both Storage and PVC spec", &cdiv1.StorageSpec{}, &corev1.PersistentVolumeClaimSpec{}, false),
Entry("should accept empty Storage spec when cloning PVC", &cdiv1.StorageSpec{}, nil, true),
Entry("should accept blank Resources when cloning using Storage API", &cdiv1.StorageSpec{
Resources: corev1.ResourceRequirements{},
}, nil, true),
Entry("should accept empty Requests when cloning using Storage API", &cdiv1.StorageSpec{
Resources: corev1.ResourceRequirements{
Requests: make(map[corev1.ResourceName]resource.Quantity),
},
}, nil, true),
Entry("should reject empty Requests when cloning using PVC API", nil, &corev1.PersistentVolumeClaimSpec{
AccessModes: []corev1.PersistentVolumeAccessMode{
corev1.ReadWriteOnce,
},
Resources: corev1.ResourceRequirements{
Requests: make(map[corev1.ResourceName]resource.Quantity),
},
}, false),
)
It("should reject empty Requests when using Storage API with DataVolumeSource but without DataVolumeSourcePVC", func() {
httpSource := &cdiv1.DataVolumeSource{
HTTP: &cdiv1.DataVolumeSourceHTTP{URL: "http://www.example.com"},
}
requests := make(map[corev1.ResourceName]resource.Quantity)
storage := &cdiv1.StorageSpec{
Resources: corev1.ResourceRequirements{
Requests: requests,
},
}
dv := newDataVolumeWithStorageSpec("testDV", httpSource, nil, storage)
resp := validateDataVolumeCreate(dv)
Expect(resp.Allowed).To(Equal(false))
})
It("should allow empty Requests when using Storage API with DataVolumeSourceRef", func() {
pvc := &corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: "testPVC",
Namespace: testNamespace,
},
Spec: *newPVCSpec(pvcSizeDefault),
}
dataSource := &cdiv1.DataSource{
ObjectMeta: metav1.ObjectMeta{
Name: "testDs",
Namespace: testNamespace,
},
Spec: cdiv1.DataSourceSpec{
Source: cdiv1.DataSourceSource{
PVC: &cdiv1.DataVolumeSourcePVC{
Name: pvc.Name,
Namespace: testNamespace,
},
},
},
}
storage := &cdiv1.StorageSpec{}
sourceRef := &cdiv1.DataVolumeSourceRef{
Kind: cdiv1.DataVolumeDataSource,
Namespace: &testNamespace,
Name: dataSource.Name,
}
dv := newDataVolumeWithStorageSpec("testDV", nil, sourceRef, storage)
resp := validateDataVolumeCreateEx(dv, []runtime.Object{pvc}, []runtime.Object{dataSource}, nil)
Expect(resp.Allowed).To(Equal(true))
})
It("should reject snapshot clone when input size is lower than recommended restore size", func() {
size := resource.MustParse("1G")
snapshot := &snapshotv1.VolumeSnapshot{
ObjectMeta: metav1.ObjectMeta{
Name: "testsnap",
Namespace: testNamespace,
},
Status: &snapshotv1.VolumeSnapshotStatus{
RestoreSize: &size,
},
}
storage := &cdiv1.StorageSpec{
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceStorage: resource.MustParse("500Mi"),
},
},
}
snapSource := &cdiv1.DataVolumeSource{
Snapshot: &cdiv1.DataVolumeSourceSnapshot{
Namespace: snapshot.Namespace,
Name: snapshot.Name,
},
}
dv := newDataVolumeWithStorageSpec("testDV", snapSource, nil, storage)
resp := validateDataVolumeCreateEx(dv, nil, nil, []runtime.Object{snapshot})
Expect(resp.Allowed).To(Equal(false))
})
It("should reject snapshot clone when no input size/recommended restore size", func() {
snapshot := &snapshotv1.VolumeSnapshot{
ObjectMeta: metav1.ObjectMeta{
Name: "testsnap",
Namespace: testNamespace,
},
Status: &snapshotv1.VolumeSnapshotStatus{},
}
storage := &cdiv1.StorageSpec{}
snapSource := &cdiv1.DataVolumeSource{
Snapshot: &cdiv1.DataVolumeSourceSnapshot{
Namespace: snapshot.Namespace,
Name: snapshot.Name,
},
}
dv := newDataVolumeWithStorageSpec("testDV", snapSource, nil, storage)
resp := validateDataVolumeCreateEx(dv, nil, nil, []runtime.Object{snapshot})
Expect(resp.Allowed).To(Equal(false))
})
DescribeTable("should", func(oldFinalCheckpoint bool, oldCheckpoints []string, newFinalCheckpoint bool, newCheckpoints []string, modifyDV func(*cdiv1.DataVolume), expectedSuccess bool, sourceFunc func() *cdiv1.DataVolumeSource) {
oldDV := newMultistageDataVolume("multi-stage", oldFinalCheckpoint, oldCheckpoints, sourceFunc)
oldBytes, _ := json.Marshal(&oldDV)
newDV := newMultistageDataVolume("multi-stage", newFinalCheckpoint, newCheckpoints, sourceFunc)
if modifyDV != nil {
modifyDV(newDV)
}
newBytes, _ := json.Marshal(&newDV)
ar := &admissionv1.AdmissionReview{
Request: &admissionv1.AdmissionRequest{
Operation: admissionv1.Update,
Resource: metav1.GroupVersionResource{
Group: cdiv1.SchemeGroupVersion.Group,
Version: cdiv1.SchemeGroupVersion.Version,
Resource: "datavolumes",
},
Object: runtime.RawExtension{
Raw: newBytes,
},
OldObject: runtime.RawExtension{
Raw: oldBytes,
},
},
}
resp := validateAdmissionReview(ar)
Expect(resp.Allowed).To(Equal(expectedSuccess))
},
Entry("accept a spec change on multi-stage VDDK import fields", false, []string{"stage-1"}, true, []string{"stage-1", "stage-2"}, nil, true, vddkSource),
Entry("reject a spec change on un-approved fields of a multi-stage VDDK import", false, []string{"stage-1"}, true, []string{"stage-1", "stage-2"}, func(newDV *cdiv1.DataVolume) { newDV.Spec.Source.VDDK.URL = "testing123" }, false, vddkSource),
Entry("accept identical multi-stage VDDK import field changes", false, []string{"stage-1"}, false, []string{"stage-1"}, nil, true, vddkSource),
Entry("reject a spec change on un-approved fields, even with identical non-empty multi-stage fields", false, []string{"stage-1"}, false, []string{"stage-1"}, func(newDV *cdiv1.DataVolume) { newDV.Spec.Source.VDDK.URL = "tesing123" }, false, vddkSource),
Entry("accept a spec change on multi-stage ImageIO import fields", false, []string{"snapshot-123"}, true, []string{"snapshot-123", "snapshot-234"}, nil, true, imageIOSource),
Entry("reject a spec change on source type that does not support multi-stage import", false, []string{}, true, []string{}, nil, false, blankSource),
)
It("should accept DataVolume without source if dataSource is correctly populated", func() {
pvc := newPVCSpec(pvcSizeDefault)
dataVolume := newDataVolumeWithSourceRef("test-dv", nil, nil, pvc)
dataVolume.Spec.PVC.DataSource = &corev1.TypedLocalObjectReference{
Kind: "PersistentVolumeClaim",
Name: dataVolume.Name,
}
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(true))
})
It("should accept DataVolume without source if dataSourceRef is correctly populated", func() {
pvc := newPVCSpec(pvcSizeDefault)
dataVolume := newDataVolumeWithSourceRef("test-dv", nil, nil, pvc)
dataVolume.Spec.PVC.DataSourceRef = &corev1.TypedObjectReference{
Kind: "PersistentVolumeClaim",
Name: dataVolume.Name,
}
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(true))
})
It("should reject DataVolume with populated source and dataSource", func() {
blankSource := cdiv1.DataVolumeSource{
Blank: &cdiv1.DataVolumeBlankImage{},
}
pvc := newPVCSpec(pvcSizeDefault)
dataVolume := newDataVolumeWithSourceRef("test-dv", &blankSource, nil, pvc)
dataVolume.Spec.PVC.DataSource = &corev1.TypedLocalObjectReference{
Kind: "PersistentVolumeClaim",
Name: dataVolume.Name,
}
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should reject DataVolume with populated source and dataSourceRef", func() {
blankSource := cdiv1.DataVolumeSource{
Blank: &cdiv1.DataVolumeBlankImage{},
}
pvc := newPVCSpec(pvcSizeDefault)
dataVolume := newDataVolumeWithSourceRef("test-dv", &blankSource, nil, pvc)
dataVolume.Spec.PVC.DataSourceRef = &corev1.TypedObjectReference{
Kind: "PersistentVolumeClaim",
Name: dataVolume.Name,
}
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should reject DataVolume with badly populated dataSource", func() {
pvc := newPVCSpec(pvcSizeDefault)
dataVolume := newDataVolumeWithSourceRef("test-dv", nil, nil, pvc)
// APIGroup can only be empty when kind is PersistentVolumeClaim
dataVolume.Spec.PVC.DataSource = &corev1.TypedLocalObjectReference{
Kind: "VolumeSnapshot",
Name: dataVolume.Name,
}
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should reject DataVolume with if dataSource and dataSourceRef are different", func() {
pvc := newPVCSpec(pvcSizeDefault)
dataVolume := newDataVolumeWithSourceRef("test-dv", nil, nil, pvc)
dataVolume.Spec.PVC.DataSource = &corev1.TypedLocalObjectReference{
Kind: "PersistentVolumeClaim",
Name: dataVolume.Name,
}
dataVolume.Spec.PVC.DataSourceRef = &corev1.TypedObjectReference{
Kind: "PersistentVolumeClaim",
Name: dataVolume.Name + "-test",
}
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should reject DataVolume with cross namespace dataSourceRef", func() {
namespace := "foo"
pvc := newPVCSpec(pvcSizeDefault)
dataVolume := newDataVolumeWithSourceRef("test-dv", nil, nil, pvc)
dataVolume.Spec.PVC.DataSourceRef = &corev1.TypedObjectReference{
Namespace: &namespace,
Kind: "PersistentVolumeClaim",
Name: dataVolume.Name,
}
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
})
Context("with DataVolume (using sourceRef) admission review", func() {
DescribeTable("should", func(dataSourceNamespace *string) {
pvcName := "testPVC"
dataVolume := newDataSourceDataVolume("testDV", dataSourceNamespace, "test")
dataVolume.Namespace = testNamespace
dataSource := &cdiv1.DataSource{
ObjectMeta: metav1.ObjectMeta{
Name: dataVolume.Spec.SourceRef.Name,
Namespace: testNamespace,
},
Spec: cdiv1.DataSourceSpec{
Source: cdiv1.DataSourceSource{
PVC: &cdiv1.DataVolumeSourcePVC{
Name: pvcName,
Namespace: testNamespace,
},
},
},
}
pvc := &corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: pvcName,
Namespace: testNamespace,
},
Spec: *newPVCSpec(pvcSizeDefault),
}
resp := validateDataVolumeCreateEx(dataVolume, []runtime.Object{pvc}, []runtime.Object{dataSource}, nil)
Expect(resp.Allowed).To(Equal(true))
},
Entry("accept DataVolume with PVC and sourceRef on create", &testNamespace),
Entry("accept DataVolume with PVC and sourceRef nil namespace on create", nil),
Entry("accept DataVolume with PVC and sourceRef missing namespace on create", &emptyNamespace),
)
It("should reject DataVolume with SourceRef on create if DataSource does not exist", func() {
ns := "testNamespace"
dataVolume := newDataSourceDataVolume("testDV", &ns, "test")
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should reject DataVolume with SourceRef on create if DataSource exists but its PVC field is not populated", func() {
dataVolume := newDataSourceDataVolume("testDV", &testNamespace, "test")
dataSource := &cdiv1.DataSource{
ObjectMeta: metav1.ObjectMeta{
Name: dataVolume.Spec.SourceRef.Name,
Namespace: testNamespace,
},
Spec: cdiv1.DataSourceSpec{
Source: cdiv1.DataSourceSource{
PVC: nil,
},
},
}
resp := validateDataVolumeCreateEx(dataVolume, nil, []runtime.Object{dataSource}, nil)
Expect(resp.Allowed).To(Equal(false))
})
It("should accept DataVolume with SourceRef on create if DataSource exists but PVC does not exist", func() {
dataVolume := newDataSourceDataVolume("testDV", &testNamespace, "test")
dataSource := &cdiv1.DataSource{
ObjectMeta: metav1.ObjectMeta{
Name: dataVolume.Spec.SourceRef.Name,
Namespace: testNamespace,
},
Spec: cdiv1.DataSourceSpec{
Source: cdiv1.DataSourceSource{
PVC: &cdiv1.DataVolumeSourcePVC{
Name: "testPVC",
Namespace: testNamespace,
},
},
},
}
resp := validateDataVolumeCreateEx(dataVolume, nil, []runtime.Object{dataSource}, nil)
Expect(resp.Allowed).To(Equal(true))
})
It("should accept DataVolume with SourceRef on create if DataSource exists but snapshot does not exist", func() {
dataVolume := newDataSourceDataVolume("testDV", &testNamespace, "test")
dataSource := &cdiv1.DataSource{
ObjectMeta: metav1.ObjectMeta{
Name: dataVolume.Spec.SourceRef.Name,
Namespace: testNamespace,
},
Spec: cdiv1.DataSourceSpec{
Source: cdiv1.DataSourceSource{
Snapshot: &cdiv1.DataVolumeSourceSnapshot{
Name: "testNonExistentSnap",
Namespace: testNamespace,
},
},
},
}
resp := validateDataVolumeCreateEx(dataVolume, nil, []runtime.Object{dataSource}, nil)
Expect(resp.Allowed).To(Equal(true))
})
It("should reject DataVolume with empty SourceRef name on create", func() {
dataVolume := newDataSourceDataVolume("testDV", &testNamespace, "")
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should reject DataVolume with both source and sourceRef on create", func() {
dataVolume := newDataVolumeWithBothSourceAndSourceRef("testDV", "testNamespace", "test")
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should reject DataVolume with no source or sourceRef on create", func() {
dataVolume := newDataVolumeWithNoSourceOrSourceRef("testDV")
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
})
})
func vddkSource() *cdiv1.DataVolumeSource {
return &cdiv1.DataVolumeSource{
VDDK: &cdiv1.DataVolumeSourceVDDK{
BackingFile: "disk.img",
URL: "http://example.com/data",
UUID: "12345",
Thumbprint: "aa:bb:cc",
SecretRef: "secret",
},
}
}
func imageIOSource() *cdiv1.DataVolumeSource {
return &cdiv1.DataVolumeSource{
Imageio: &cdiv1.DataVolumeSourceImageIO{
URL: "http://example.com/data",
DiskID: "disk-123",
SecretRef: "secret",
CertConfigMap: "certs",
},
}
}
func blankSource() *cdiv1.DataVolumeSource {
return &cdiv1.DataVolumeSource{
Blank: &cdiv1.DataVolumeBlankImage{},
}
}
func newMultistageDataVolume(name string, final bool, checkpoints []string, sourceFunc func() *cdiv1.DataVolumeSource) *cdiv1.DataVolume {
pvc := newPVCSpec(pvcSizeDefault)
previous := ""
dvCheckpoints := make([]cdiv1.DataVolumeCheckpoint, len(checkpoints))
for index, checkpoint := range checkpoints {
dvCheckpoints[index] = cdiv1.DataVolumeCheckpoint{
Current: checkpoint,
Previous: previous,
}
previous = checkpoint
}
namespace := metav1.NamespaceDefault
dv := &cdiv1.DataVolume{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
SelfLink: fmt.Sprintf("/apis/%s/namespaces/%s/datavolumes/%s", cdiv1.SchemeGroupVersion.String(), namespace, name),
},
TypeMeta: metav1.TypeMeta{
APIVersion: cdiv1.SchemeGroupVersion.String(),
Kind: "DataVolume",
},
Status: cdiv1.DataVolumeStatus{},
Spec: cdiv1.DataVolumeSpec{
Source: sourceFunc(),
FinalCheckpoint: final,
Checkpoints: dvCheckpoints,
PVC: pvc,
},
}
return dv
}
func newHTTPDataVolume(name, url string) *cdiv1.DataVolume {
httpSource := cdiv1.DataVolumeSource{
HTTP: &cdiv1.DataVolumeSourceHTTP{URL: url},
}
pvc := newPVCSpec(pvcSizeDefault)
return newDataVolume(name, httpSource, pvc)
}
func newGCSDataVolume(name, url string) *cdiv1.DataVolume {
gcsSource := cdiv1.DataVolumeSource{
GCS: &cdiv1.DataVolumeSourceGCS{URL: url},
}
pvc := newPVCSpec(pvcSizeDefault)
return newDataVolume(name, gcsSource, pvc)
}
func newRegistryDataVolume(name, url string) *cdiv1.DataVolume {
registrySource := cdiv1.DataVolumeSource{
Registry: &cdiv1.DataVolumeSourceRegistry{URL: &url},
}
pvc := newPVCSpec(pvcSizeDefault)
return newDataVolume(name, registrySource, pvc)
}
func newBlankDataVolume(name string) *cdiv1.DataVolume {
blankSource := cdiv1.DataVolumeSource{
Blank: &cdiv1.DataVolumeBlankImage{},
}
pvc := newPVCSpec(pvcSizeDefault)
return newDataVolume(name, blankSource, pvc)
}
func newPVCDataVolume(name, pvcNamespace, pvcName string) *cdiv1.DataVolume {
pvcSource := cdiv1.DataVolumeSource{
PVC: &cdiv1.DataVolumeSourcePVC{
Namespace: pvcNamespace,
Name: pvcName,
},
}
pvc := newPVCSpec(pvcSizeDefault)
return newDataVolume(name, pvcSource, pvc)
}
func newDataVolumeWithEmptyPVCSpec(name, url string) *cdiv1.DataVolume {
httpSource := cdiv1.DataVolumeSource{
HTTP: &cdiv1.DataVolumeSourceHTTP{URL: url},
}
return newDataVolume(name, httpSource, nil)
}
func newDataVolumeWithMultipleSources(name string) *cdiv1.DataVolume {
source := cdiv1.DataVolumeSource{
HTTP: &cdiv1.DataVolumeSourceHTTP{URL: "http://www.example.com"},
S3: &cdiv1.DataVolumeSourceS3{URL: "http://s3.examples3.com"},
}
pvc := newPVCSpec(pvcSizeDefault)
return newDataVolume(name, source, pvc)
}
func newDataVolumeWithPVCSizeZero(name, url string) *cdiv1.DataVolume {
httpSource := cdiv1.DataVolumeSource{
HTTP: &cdiv1.DataVolumeSourceHTTP{URL: url},
}
pvc := newPVCSpec(0)
return newDataVolume(name, httpSource, pvc)
}
func newDataSourceDataVolume(name string, sourceRefNamespace *string, sourceRefName string) *cdiv1.DataVolume {
sourceRef := cdiv1.DataVolumeSourceRef{
Kind: cdiv1.DataVolumeDataSource,
Namespace: sourceRefNamespace,
Name: sourceRefName,
}
pvc := newPVCSpec(pvcSizeDefault)
return newDataVolumeWithSourceRef(name, nil, &sourceRef, pvc)
}
func newDataVolumeWithBothSourceAndSourceRef(name string, pvcNamespace string, pvcName string) *cdiv1.DataVolume {
pvcSource := cdiv1.DataVolumeSource{
PVC: &cdiv1.DataVolumeSourcePVC{
Namespace: pvcNamespace,
Name: pvcName,
},
}
sourceRef := cdiv1.DataVolumeSourceRef{
Kind: cdiv1.DataVolumeDataSource,
Namespace: &pvcNamespace,
Name: pvcName,
}
pvc := newPVCSpec(pvcSizeDefault)
return newDataVolumeWithSourceRef(name, &pvcSource, &sourceRef, pvc)
}
func newDataVolumeWithNoSourceOrSourceRef(name string) *cdiv1.DataVolume {
pvc := newPVCSpec(pvcSizeDefault)
return newDataVolumeWithSourceRef(name, nil, nil, pvc)
}
func newDataVolume(name string, source cdiv1.DataVolumeSource, pvc *corev1.PersistentVolumeClaimSpec) *cdiv1.DataVolume {
return newDataVolumeWithSourceRef(name, &source, nil, pvc)
}
func newDataVolumeWithSourceRef(name string, source *cdiv1.DataVolumeSource, sourceRef *cdiv1.DataVolumeSourceRef, pvc *corev1.PersistentVolumeClaimSpec) *cdiv1.DataVolume {
namespace := k8sv1.NamespaceDefault
dv := &cdiv1.DataVolume{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
SelfLink: fmt.Sprintf("/apis/%s/namespaces/%s/datavolumes/%s", cdiv1.SchemeGroupVersion.String(), namespace, name),
},
TypeMeta: metav1.TypeMeta{
APIVersion: cdiv1.SchemeGroupVersion.String(),
Kind: "DataVolume",
},
Status: cdiv1.DataVolumeStatus{},
Spec: cdiv1.DataVolumeSpec{
Source: source,
SourceRef: sourceRef,
PVC: pvc,
},
}
return dv
}
func newDataVolumeWithStorageSpec(name string, source *cdiv1.DataVolumeSource, sourceRef *cdiv1.DataVolumeSourceRef, storage *cdiv1.StorageSpec) *cdiv1.DataVolume {
namespace := k8sv1.NamespaceDefault
dv := &cdiv1.DataVolume{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
TypeMeta: metav1.TypeMeta{
APIVersion: cdiv1.SchemeGroupVersion.String(),
Kind: "DataVolume",
},
Status: cdiv1.DataVolumeStatus{},
Spec: cdiv1.DataVolumeSpec{
Source: source,
SourceRef: sourceRef,
Storage: storage,
},
}
return dv
}
func newDataVolumeWithoutStorageAndPVC(name string, source *cdiv1.DataVolumeSource, sourceRef *cdiv1.DataVolumeSourceRef) *cdiv1.DataVolume {
namespace := k8sv1.NamespaceDefault
dv := &cdiv1.DataVolume{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
TypeMeta: metav1.TypeMeta{
APIVersion: cdiv1.SchemeGroupVersion.String(),
Kind: "DataVolume",
},
Status: cdiv1.DataVolumeStatus{},
Spec: cdiv1.DataVolumeSpec{
Source: source,
SourceRef: sourceRef,
},
}
return dv
}
// Returns both the DV clone and the original PVC
func newDataVolumeClone(storageSpec *cdiv1.StorageSpec, pvcSpec *corev1.PersistentVolumeClaimSpec) (*cdiv1.DataVolume, *corev1.PersistentVolumeClaim) {
var dv *cdiv1.DataVolume
pvcSource := &cdiv1.DataVolumeSource{
PVC: &cdiv1.DataVolumeSourcePVC{
Namespace: "testNamespace",
Name: "test",
},
}
if storageSpec != nil && pvcSpec != nil {
dv = newDataVolumeWithoutStorageAndPVC("testDV", pvcSource, nil)
dv.Spec.Storage = storageSpec
dv.Spec.PVC = pvcSpec
} else if storageSpec != nil {
dv = newDataVolumeWithStorageSpec("testDV", pvcSource, nil, storageSpec)
} else if pvcSpec != nil {
dv = newDataVolumeWithSourceRef("testDV", pvcSource, nil, pvcSpec)
} else {
dv = newDataVolumeWithoutStorageAndPVC("testDV", pvcSource, nil)
}
pvc := &corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: dv.Spec.Source.PVC.Name,
Namespace: dv.Spec.Source.PVC.Namespace,
},
Spec: *newPVCSpec(pvcSizeDefault),
}
return dv, pvc
}
const pvcSizeDefault = 5 << 20 // 5Mi
func newPVCSpec(sizeValue int64) *corev1.PersistentVolumeClaimSpec {
requests := make(map[corev1.ResourceName]resource.Quantity)
requests["storage"] = *resource.NewQuantity(sizeValue, resource.BinarySI)
pvc := &corev1.PersistentVolumeClaimSpec{
AccessModes: []corev1.PersistentVolumeAccessMode{
corev1.ReadWriteOnce,
},
Resources: corev1.ResourceRequirements{
Requests: requests,
},
}
return pvc
}
func validateDataVolumeCreate(dv *cdiv1.DataVolume, objects ...runtime.Object) *admissionv1.AdmissionResponse {
return validateDataVolumeCreateEx(dv, objects, nil, nil)
}
func validateDataVolumeCreateEx(dv *cdiv1.DataVolume, k8sObjects, cdiObjects, snapObjects []runtime.Object) *admissionv1.AdmissionResponse {
client := fakeclient.NewSimpleClientset(k8sObjects...)
cdiClient := cdiclientfake.NewSimpleClientset(cdiObjects...)
snapClient := snapclientfake.NewSimpleClientset(snapObjects...)
wh := NewDataVolumeValidatingWebhook(client, cdiClient, snapClient)
dvBytes, _ := json.Marshal(dv)
ar := &admissionv1.AdmissionReview{
Request: &admissionv1.AdmissionRequest{
Operation: admissionv1.Create,
Resource: metav1.GroupVersionResource{
Group: cdiv1.SchemeGroupVersion.Group,
Version: cdiv1.SchemeGroupVersion.Version,
Resource: "datavolumes",
},
Object: runtime.RawExtension{
Raw: dvBytes,
},
},
}
return serve(ar, wh)
}
func validateAdmissionReview(ar *admissionv1.AdmissionReview, objects ...runtime.Object) *admissionv1.AdmissionResponse {
client := fakeclient.NewSimpleClientset(objects...)
cdiClient := cdiclientfake.NewSimpleClientset()
snapClient := snapclientfake.NewSimpleClientset()
wh := NewDataVolumeValidatingWebhook(client, cdiClient, snapClient)
return serve(ar, wh)
}
func serve(ar *admissionv1.AdmissionReview, handler http.Handler) *admissionv1.AdmissionResponse {
reqBytes, _ := json.Marshal(ar)
req, err := http.NewRequest("POST", "/foobar", bytes.NewReader(reqBytes))
Expect(err).ToNot(HaveOccurred())
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
var response admissionv1.AdmissionReview
err = json.NewDecoder(rr.Body).Decode(&response)
Expect(err).ToNot(HaveOccurred())
return response.Response
}