From 9611f39f504fc6d54f2b195200a614fd68701b42 Mon Sep 17 00:00:00 2001 From: kubevirt-bot Date: Thu, 23 Feb 2023 22:52:40 +0100 Subject: [PATCH] [release-v1.56] Annotation to check for statically provisioned PVs when creating DataVolumes (#2605) * function should return dataVolumeSyncResult, take *dataVolumeSyncResult as a parameter Signed-off-by: Michael Henriksen * checkStaticVolume implemetation for import DataVolume Signed-off-by: Michael Henriksen * upload support for checkStaticVolume Signed-off-by: Michael Henriksen * checkStaticVolume for clone datavolumes Signed-off-by: Michael Henriksen * checkStaticVolume for snapshot clone Signed-off-by: Michael Henriksen * checkStaticVolume for external populator source Signed-off-by: Michael Henriksen * tignten up static volume check Signed-off-by: Michael Henriksen * expand functional test to compare creation timestamps Signed-off-by: Michael Henriksen * updates from code review mostly add md5 verification to test and refacto common index creation Signed-off-by: Michael Henriksen * webhook changes, allow clone source DataVolumes (with special annotations) even if source does not exist or user has no permission BUT no token is added so this is really just for the static/prepopulate cases Signed-off-by: Michael Henriksen --------- Signed-off-by: Michael Henriksen Co-authored-by: Michael Henriksen --- cmd/cdi-controller/controller.go | 5 + go.mod | 1 + go.sum | 2 + pkg/apiserver/webhooks/datavolume-mutate.go | 74 +++-- .../webhooks/datavolume-mutate_test.go | 86 +++++ pkg/apiserver/webhooks/datavolume-validate.go | 2 + pkg/apiserver/webhooks/handler.go | 7 +- pkg/controller/common/util.go | 7 + pkg/controller/datavolume/BUILD.bazel | 2 + .../datavolume/clone-controller-base.go | 2 +- pkg/controller/datavolume/controller-base.go | 221 +++++++++++-- .../external-population-controller.go | 25 +- .../datavolume/import-controller.go | 35 +-- .../datavolume/import-controller_test.go | 3 +- .../datavolume/pvc-clone-controller.go | 23 +- .../datavolume/snapshot-clone-controller.go | 20 +- .../datavolume/static-volume_test.go | 296 ++++++++++++++++++ .../datavolume/upload-controller.go | 21 +- pkg/controller/datavolume/util.go | 69 ++++ tests/BUILD.bazel | 1 + tests/cloner_test.go | 8 +- tests/external_population_test.go | 10 +- tests/static-volume_test.go | 158 ++++++++++ tests/utils/datavolume.go | 61 +++- tests/webhook_test.go | 2 +- vendor/k8s.io/component-helpers/LICENSE | 202 ++++++++++++ .../storage/volume/BUILD.bazel | 10 + .../storage/volume/helpers.go | 44 +++ vendor/modules.txt | 3 + 29 files changed, 1262 insertions(+), 138 deletions(-) create mode 100644 pkg/controller/datavolume/static-volume_test.go create mode 100644 tests/static-volume_test.go create mode 100644 vendor/k8s.io/component-helpers/LICENSE create mode 100644 vendor/k8s.io/component-helpers/storage/volume/BUILD.bazel create mode 100644 vendor/k8s.io/component-helpers/storage/volume/helpers.go diff --git a/cmd/cdi-controller/controller.go b/cmd/cdi-controller/controller.go index 2dbdadf32f..a3ce5241ca 100644 --- a/cmd/cdi-controller/controller.go +++ b/cmd/cdi-controller/controller.go @@ -204,6 +204,11 @@ func start(ctx context.Context, cfg *rest.Config) { os.Exit(1) } + if err := dvc.CreateCommonIndexes(mgr); err != nil { + klog.Errorf("Unable to create shared indexes: %v", err) + os.Exit(1) + } + // TODO: Current DV controller had threadiness 3, should we do the same here, defaults to one thread. if _, err := dvc.NewImportController(ctx, mgr, log, installerLabels); err != nil { klog.Errorf("Unable to setup datavolume import controller: %v", err) diff --git a/go.mod b/go.mod index 3149664eda..5c73f75ff8 100644 --- a/go.mod +++ b/go.mod @@ -47,6 +47,7 @@ require ( k8s.io/client-go v12.0.0+incompatible k8s.io/cluster-bootstrap v0.0.0 k8s.io/code-generator v0.23.3 + k8s.io/component-helpers v0.23.0 k8s.io/klog/v2 v2.70.1 k8s.io/kube-aggregator v0.23.0 k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 diff --git a/go.sum b/go.sum index d0efd71dd1..4c8766a110 100644 --- a/go.sum +++ b/go.sum @@ -1980,6 +1980,8 @@ k8s.io/code-generator v0.23.0 h1:lhyd2KJVCEmpjaCpuoooGs+e3xhPwpYvupnNRidO0Ds= k8s.io/code-generator v0.23.0/go.mod h1:vQvOhDXhuzqiVfM/YHp+dmg10WDZCchJVObc9MvowsE= k8s.io/component-base v0.23.0 h1:UAnyzjvVZ2ZR1lF35YwtNY6VMN94WtOnArcXBu34es8= k8s.io/component-base v0.23.0/go.mod h1:DHH5uiFvLC1edCpvcTDV++NKULdYYU6pR9Tt3HIKMKI= +k8s.io/component-helpers v0.23.0 h1:qNbqN10QTefiWcCOPkHL/0nn81sdKVv6ZgEXcSyot/U= +k8s.io/component-helpers v0.23.0/go.mod h1:liXMh6FZS4qamKtMJQ7uLHnFe3tlC86RX5mJEk/aerg= k8s.io/cri-api v0.23.0/go.mod h1:2edENu3/mkyW3c6fVPPPaVGEFbLRacJizBbSp7ZOLOo= k8s.io/gengo v0.0.0-20181113154421-fd15ee9cc2f7/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= diff --git a/pkg/apiserver/webhooks/datavolume-mutate.go b/pkg/apiserver/webhooks/datavolume-mutate.go index b6fa4fd7f6..703dd3eb74 100644 --- a/pkg/apiserver/webhooks/datavolume-mutate.go +++ b/pkg/apiserver/webhooks/datavolume-mutate.go @@ -26,6 +26,7 @@ import ( snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" admissionv1 "k8s.io/api/admission/v1" authv1 "k8s.io/api/authorization/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" k8sfield "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/client-go/kubernetes" @@ -85,7 +86,7 @@ func (p *sarProxy) Create(sar *authv1.SubjectAccessReview) (*authv1.SubjectAcces } func (wh *dataVolumeMutatingWebhook) Admit(ar admissionv1.AdmissionReview) *admissionv1.AdmissionResponse { - var dataVolume, oldDataVolume cdiv1.DataVolume + dataVolume := &cdiv1.DataVolume{} klog.V(3).Infof("Got AdmissionReview %+v", ar) @@ -102,22 +103,7 @@ func (wh *dataVolumeMutatingWebhook) Admit(ar admissionv1.AdmissionReview) *admi return allowedAdmissionResponse() } - cloneSourceHandler, err := newCloneSourceHandler(&dataVolume, wh.cdiClient) - if err != nil { - return toAdmissionResponseError(err) - } - - targetNamespace, targetName := dataVolume.Namespace, dataVolume.Name - if targetNamespace == "" { - targetNamespace = ar.Request.Namespace - } - - if targetName == "" { - targetName = ar.Request.Name - } - modifiedDataVolume := dataVolume.DeepCopy() - modified := false if ar.Request.Operation == admissionv1.Create { config, err := wh.cdiClient.CdiV1beta1().CDIConfigs().Get(context.TODO(), common.ConfigName, metav1.GetOptions{}) @@ -130,17 +116,41 @@ func (wh *dataVolumeMutatingWebhook) Admit(ar admissionv1.AdmissionReview) *admi } if modifiedDataVolume.Annotations[cc.AnnDeleteAfterCompletion] != "false" { modifiedDataVolume.Annotations[cc.AnnDeleteAfterCompletion] = "true" - modified = true } } } - if cloneSourceHandler.cloneType == noClone { - klog.V(3).Infof("DataVolume %s/%s not cloning", targetNamespace, targetName) - if modified { + _, prePopulated := dataVolume.Annotations[cc.AnnPrePopulated] + _, checkStaticVolume := dataVolume.Annotations[cc.AnnCheckStaticVolume] + noTokenOkay := prePopulated || checkStaticVolume + + targetNamespace, targetName := dataVolume.Namespace, dataVolume.Name + if targetNamespace == "" { + targetNamespace = ar.Request.Namespace + } + + if targetName == "" { + targetName = ar.Request.Name + } + + cloneSourceHandler, err := newCloneSourceHandler(dataVolume, wh.cdiClient) + if err != nil { + if k8serrors.IsNotFound(err) && noTokenOkay { + // no token needed, likely since no datasource + klog.V(3).Infof("DataVolume %s/%s is pre/static populated, not adding token, no datasource", targetNamespace, targetName) return toPatchResponse(dataVolume, modifiedDataVolume) } - return allowedAdmissionResponse() + return toAdmissionResponseError(err) + } + + if cloneSourceHandler.cloneType == noClone { + klog.V(3).Infof("DataVolume %s/%s not cloning", targetNamespace, targetName) + return toPatchResponse(dataVolume, modifiedDataVolume) + } + + // only add token at create time + if ar.Request.Operation != admissionv1.Create { + return toPatchResponse(dataVolume, modifiedDataVolume) } sourceName, sourceNamespace := cloneSourceHandler.sourceName, cloneSourceHandler.sourceNamespace @@ -150,19 +160,12 @@ func (wh *dataVolumeMutatingWebhook) Admit(ar admissionv1.AdmissionReview) *admi _, err = wh.k8sClient.CoreV1().Namespaces().Get(context.TODO(), sourceNamespace, metav1.GetOptions{}) if err != nil { - return toAdmissionResponseError(err) - } - - if ar.Request.Operation == admissionv1.Update { - if err := json.Unmarshal(ar.Request.OldObject.Raw, &oldDataVolume); err != nil { - return toAdmissionResponseError(err) - } - - _, ok := oldDataVolume.Annotations[cc.AnnCloneToken] - if ok { - klog.V(3).Infof("DataVolume %s/%s already has clone token", targetNamespace, targetName) - return allowedAdmissionResponse() + if k8serrors.IsNotFound(err) && noTokenOkay { + // no token needed, likely since no source namespace + klog.V(3).Infof("DataVolume %s/%s is pre/static populated, not adding token, no source namespace", targetNamespace, targetName) + return toPatchResponse(dataVolume, modifiedDataVolume) } + return toAdmissionResponseError(err) } ok, reason, err := cloneSourceHandler.cloneAuthFunc(wh.proxy, sourceNamespace, sourceName, targetNamespace, ar.Request.UserInfo) @@ -171,6 +174,11 @@ func (wh *dataVolumeMutatingWebhook) Admit(ar admissionv1.AdmissionReview) *admi } if !ok { + if noTokenOkay { + klog.V(3).Infof("DataVolume %s/%s is pre/static populated, not adding token, auth failed", targetNamespace, targetName) + return toPatchResponse(dataVolume, modifiedDataVolume) + } + causes := []metav1.StatusCause{ { Type: metav1.CauseTypeFieldValueInvalid, diff --git a/pkg/apiserver/webhooks/datavolume-mutate_test.go b/pkg/apiserver/webhooks/datavolume-mutate_test.go index 8ef3662897..2e50ba0e42 100644 --- a/pkg/apiserver/webhooks/datavolume-mutate_test.go +++ b/pkg/apiserver/webhooks/datavolume-mutate_test.go @@ -125,6 +125,32 @@ var _ = Describe("Mutating DataVolume Webhook", func() { Expect(resp.Patch).To(BeNil()) }) + DescribeTable("should accept a DataVolume with sourceRef to non-existing DataSource", func(anno string) { + dataVolume := newDataSourceDataVolume("testDV", nil, "test") + Expect(dataVolume.Annotations).To(BeNil()) + dataVolume.Annotations = map[string]string{anno: "whatever"} + dvBytes, _ := json.Marshal(&dataVolume) + ar := &admissionv1.AdmissionReview{ + Request: &admissionv1.AdmissionRequest{ + Operation: admissionv1.Create, + Resource: metav1.GroupVersionResource{ + Group: cdicorev1.SchemeGroupVersion.Group, + Version: cdicorev1.SchemeGroupVersion.Version, + Resource: "datavolumes", + }, + Object: runtime.RawExtension{ + Raw: dvBytes, + }, + }, + } + resp := mutateDVs(key, ar, true) + Expect(resp.Allowed).To(BeTrue()) + Expect(resp.Patch).ToNot(BeNil()) + }, + Entry("with static volume annotation", cc.AnnCheckStaticVolume), + Entry("with prePopulated volume annotation", cc.AnnPrePopulated), + ) + It("should allow a DataVolume with sourceRef to existing DataSource", func() { dataVolume := newDataSourceDataVolume("testDV", nil, "test") Expect(dataVolume.Annotations).To(BeNil()) @@ -132,6 +158,7 @@ var _ = Describe("Mutating DataVolume Webhook", func() { ar := &admissionv1.AdmissionReview{ Request: &admissionv1.AdmissionRequest{ + Operation: admissionv1.Create, Resource: metav1.GroupVersionResource{ Group: cdicorev1.SchemeGroupVersion.Group, Version: cdicorev1.SchemeGroupVersion.Version, @@ -207,6 +234,7 @@ var _ = Describe("Mutating DataVolume Webhook", func() { ar := &admissionv1.AdmissionReview{ Request: &admissionv1.AdmissionRequest{ + Operation: admissionv1.Create, Resource: metav1.GroupVersionResource{ Group: cdicorev1.SchemeGroupVersion.Group, Version: cdicorev1.SchemeGroupVersion.Version, @@ -223,12 +251,41 @@ var _ = Describe("Mutating DataVolume Webhook", func() { Expect(resp.Patch).To(BeNil()) }) + DescribeTable("should accept a clone DataVolume", func(anno string) { + dataVolume := newPVCDataVolume("testDV", "testNamespace", "test") + Expect(dataVolume.Annotations).To(BeNil()) + dataVolume.Annotations = map[string]string{anno: "whatever"} + dvBytes, _ := json.Marshal(&dataVolume) + + ar := &admissionv1.AdmissionReview{ + Request: &admissionv1.AdmissionRequest{ + Operation: admissionv1.Create, + Resource: metav1.GroupVersionResource{ + Group: cdicorev1.SchemeGroupVersion.Group, + Version: cdicorev1.SchemeGroupVersion.Version, + Resource: "datavolumes", + }, + Object: runtime.RawExtension{ + Raw: dvBytes, + }, + }, + } + + resp := mutateDVs(key, ar, false) + Expect(resp.Allowed).To(BeTrue()) + Expect(resp.Patch).ToNot(BeNil()) + }, + Entry("with static volume annotation", cc.AnnCheckStaticVolume), + Entry("with prePopulated volume annotation", cc.AnnPrePopulated), + ) + It("should reject a clone if the source PVC's namespace doesn't exist", func() { dataVolume := newPVCDataVolume("testDV", "noNamespace", "test") dvBytes, _ := json.Marshal(&dataVolume) ar := &admissionv1.AdmissionReview{ Request: &admissionv1.AdmissionRequest{ + Operation: admissionv1.Create, Resource: metav1.GroupVersionResource{ Group: cdicorev1.SchemeGroupVersion.Group, Version: cdicorev1.SchemeGroupVersion.Version, @@ -245,12 +302,41 @@ var _ = Describe("Mutating DataVolume Webhook", func() { Expect(resp.Patch).To(BeNil()) }) + DescribeTable("should accept a clone if the source PVC's namespace doesn't exist", func(anno string) { + dataVolume := newPVCDataVolume("testDV", "noNamespace", "test") + Expect(dataVolume.Annotations).To(BeNil()) + dataVolume.Annotations = map[string]string{anno: "whatever"} + dvBytes, _ := json.Marshal(&dataVolume) + + ar := &admissionv1.AdmissionReview{ + Request: &admissionv1.AdmissionRequest{ + Operation: admissionv1.Create, + Resource: metav1.GroupVersionResource{ + Group: cdicorev1.SchemeGroupVersion.Group, + Version: cdicorev1.SchemeGroupVersion.Version, + Resource: "datavolumes", + }, + Object: runtime.RawExtension{ + Raw: dvBytes, + }, + }, + } + + resp := mutateDVs(key, ar, false) + Expect(resp.Allowed).To(BeTrue()) + Expect(resp.Patch).ToNot(BeNil()) + }, + Entry("with static volume annotation", cc.AnnCheckStaticVolume), + Entry("with prePopulated volume annotation", cc.AnnPrePopulated), + ) + DescribeTable("should", func(srcNamespace string) { dataVolume := newPVCDataVolume("testDV", srcNamespace, "test") dvBytes, _ := json.Marshal(&dataVolume) ar := &admissionv1.AdmissionReview{ Request: &admissionv1.AdmissionRequest{ + Operation: admissionv1.Create, Resource: metav1.GroupVersionResource{ Group: cdicorev1.SchemeGroupVersion.Group, Version: cdicorev1.SchemeGroupVersion.Version, diff --git a/pkg/apiserver/webhooks/datavolume-validate.go b/pkg/apiserver/webhooks/datavolume-validate.go index 85d879d913..3e4d2ecf5d 100644 --- a/pkg/apiserver/webhooks/datavolume-validate.go +++ b/pkg/apiserver/webhooks/datavolume-validate.go @@ -592,6 +592,8 @@ func validateExternalPopulation(spec *cdiv1.DataVolumeSpec, field *k8sfield.Path } func (wh *dataVolumeValidatingWebhook) Admit(ar admissionv1.AdmissionReview) *admissionv1.AdmissionResponse { + klog.V(3).Infof("Got AdmissionReview %+v", ar) + if err := validateDataVolumeResource(ar); err != nil { return toAdmissionResponseError(err) } diff --git a/pkg/apiserver/webhooks/handler.go b/pkg/apiserver/webhooks/handler.go index a31c542fcf..b2052ac24c 100644 --- a/pkg/apiserver/webhooks/handler.go +++ b/pkg/apiserver/webhooks/handler.go @@ -25,6 +25,7 @@ import ( "fmt" "io" "net/http" + "reflect" "time" "github.com/appscode/jsonpatch" @@ -197,6 +198,10 @@ func validateDataVolumeResource(ar admissionv1.AdmissionReview) error { } func toPatchResponse(original, current interface{}) *admissionv1.AdmissionResponse { + if reflect.DeepEqual(original, current) { + return allowedAdmissionResponse() + } + patchType := admissionv1.PatchTypeJSONPatch ob, err := json.Marshal(original) @@ -219,7 +224,7 @@ func toPatchResponse(original, current interface{}) *admissionv1.AdmissionRespon return toAdmissionResponseError(err) } - klog.V(3).Infof("sending patches\n%s", pb) + klog.V(1).Infof("sending patches\n%s", pb) return &admissionv1.AdmissionResponse{ Allowed: true, diff --git a/pkg/controller/common/util.go b/pkg/controller/common/util.go index 54da74dff2..18ac585bdd 100644 --- a/pkg/controller/common/util.go +++ b/pkg/controller/common/util.go @@ -177,6 +177,13 @@ const ( // AnnUploadRequest marks that a PVC should be made available for upload AnnUploadRequest = AnnAPIGroup + "/storage.upload.target" + // AnnCheckStaticVolume checks if a statically allocated PV exists before creating the target PVC. + // If so, PVC is still created but population is skipped + AnnCheckStaticVolume = AnnAPIGroup + "/storage.checkStaticVolume" + + // AnnPersistentVolumeList is an annotation storing a list of PV names + AnnPersistentVolumeList = AnnAPIGroup + "/storage.persistentVolumeList" + //AnnDefaultStorageClass is the annotation indicating that a storage class is the default one. AnnDefaultStorageClass = "storageclass.kubernetes.io/is-default-class" diff --git a/pkg/controller/datavolume/BUILD.bazel b/pkg/controller/datavolume/BUILD.bazel index 56c1e03f2f..18fdd572e4 100644 --- a/pkg/controller/datavolume/BUILD.bazel +++ b/pkg/controller/datavolume/BUILD.bazel @@ -42,6 +42,7 @@ go_library( "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", "//vendor/k8s.io/client-go/tools/cache:go_default_library", "//vendor/k8s.io/client-go/tools/record:go_default_library", + "//vendor/k8s.io/component-helpers/storage/volume:go_default_library", "//vendor/sigs.k8s.io/controller-runtime/pkg/client:go_default_library", "//vendor/sigs.k8s.io/controller-runtime/pkg/controller:go_default_library", "//vendor/sigs.k8s.io/controller-runtime/pkg/controller/controllerutil:go_default_library", @@ -64,6 +65,7 @@ go_test( "pvc-clone-controller_test.go", "smart-clone-controller_test.go", "snapshot-clone-controller_test.go", + "static-volume_test.go", "upload-controller_test.go", "util_test.go", ], diff --git a/pkg/controller/datavolume/clone-controller-base.go b/pkg/controller/datavolume/clone-controller-base.go index fd6490bef3..e4ec724eb8 100644 --- a/pkg/controller/datavolume/clone-controller-base.go +++ b/pkg/controller/datavolume/clone-controller-base.go @@ -545,7 +545,7 @@ func (r *CloneReconcilerBase) syncCloneStatusPhase(syncRes *dataVolumeCloneSyncR return r.syncDataVolumeStatusPhaseWithEvent(syncRes, phase, pvc, event) } -func (r CloneReconcilerBase) updateStatusPhase(pvc *corev1.PersistentVolumeClaim, dataVolumeCopy *cdiv1.DataVolume, event *Event) error { +func (r *CloneReconcilerBase) updateStatusPhase(pvc *corev1.PersistentVolumeClaim, dataVolumeCopy *cdiv1.DataVolume, event *Event) error { phase, ok := pvc.Annotations[cc.AnnPodPhase] if phase != string(corev1.PodSucceeded) { _, ok = pvc.Annotations[cc.AnnCloneRequest] diff --git a/pkg/controller/datavolume/controller-base.go b/pkg/controller/datavolume/controller-base.go index b70d72fa4e..955eef4c6f 100644 --- a/pkg/controller/datavolume/controller-base.go +++ b/pkg/controller/datavolume/controller-base.go @@ -20,6 +20,7 @@ import ( "context" "crypto/rsa" "crypto/tls" + "encoding/json" "fmt" "io/ioutil" "net/http" @@ -33,7 +34,6 @@ import ( "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" - v1 "k8s.io/api/core/v1" storagev1 "k8s.io/api/storage/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -74,6 +74,8 @@ const ( MessageErrClaimLost = "PVC %s lost" dvPhaseField = "status.phase" + + claimRefField = "spec.claimRef" ) var httpClient *http.Client @@ -89,7 +91,7 @@ type dataVolumeSyncResult struct { dv *cdiv1.DataVolume dvMutated *cdiv1.DataVolume pvc *corev1.PersistentVolumeClaim - pvcSpec *v1.PersistentVolumeClaimSpec + pvcSpec *corev1.PersistentVolumeClaimSpec result *reconcile.Result } @@ -112,6 +114,34 @@ func pvcIsPopulated(pvc *corev1.PersistentVolumeClaim, dv *cdiv1.DataVolume) boo return ok && dvName == dv.Name } +func dvIsPrePopulated(dv *cdiv1.DataVolume) bool { + _, ok := dv.Annotations[cc.AnnPrePopulated] + return ok +} + +func checkStaticProvisionPending(pvc *corev1.PersistentVolumeClaim, dv *cdiv1.DataVolume) bool { + if pvc == nil || dv == nil { + return false + } + if _, ok := dv.Annotations[cc.AnnCheckStaticVolume]; !ok { + return false + } + _, ok := pvc.Annotations[cc.AnnPersistentVolumeList] + return ok +} + +func shouldSetDataVolumePending(pvc *corev1.PersistentVolumeClaim, dv *cdiv1.DataVolume) bool { + if checkStaticProvisionPending(pvc, dv) { + return true + } + + if pvc != nil { + return false + } + + return dvIsPrePopulated(dv) || (dv.Status.Phase == cdiv1.PhaseUnset) +} + type dataVolumeOp int const ( @@ -123,6 +153,30 @@ const ( dataVolumePopulator ) +func claimRefIndexKeyFunc(namespace, name string) string { + return namespace + "/" + name +} + +// CreateCommonIndexes creates indexes used by all controllers +func CreateCommonIndexes(mgr manager.Manager) error { + if err := mgr.GetFieldIndexer().IndexField(context.TODO(), &cdiv1.DataVolume{}, dvPhaseField, func(obj client.Object) []string { + return []string{string(obj.(*cdiv1.DataVolume).Status.Phase)} + }); err != nil { + return err + } + if err := mgr.GetFieldIndexer().IndexField(context.TODO(), &corev1.PersistentVolume{}, claimRefField, func(obj client.Object) []string { + if pv, ok := obj.(*corev1.PersistentVolume); ok { + if pv.Spec.ClaimRef != nil && pv.Spec.ClaimRef.Namespace != "" && pv.Spec.ClaimRef.Name != "" { + return []string{claimRefIndexKeyFunc(pv.Spec.ClaimRef.Namespace, pv.Spec.ClaimRef.Name)} + } + } + return nil + }); err != nil { + return err + } + return nil +} + func addDataVolumeControllerCommonWatches(mgr manager.Manager, dataVolumeController controller.Controller, op dataVolumeOp) error { appendMatchingDataVolumeRequest := func(reqs []reconcile.Request, mgr manager.Manager, namespace, name string) []reconcile.Request { dvKey := types.NamespacedName{Namespace: namespace, Name: name} @@ -246,16 +300,16 @@ func getDataVolumeOp(dv *cdiv1.DataVolume) dataVolumeOp { type dataVolumeSyncResultFunc func(*dataVolumeSyncResult) error -func (r ReconcilerBase) syncCommon(log logr.Logger, req reconcile.Request, cleanup, prepare dataVolumeSyncResultFunc) (*dataVolumeSyncResult, error) { +func (r *ReconcilerBase) syncCommon(log logr.Logger, req reconcile.Request, cleanup, prepare dataVolumeSyncResultFunc) (dataVolumeSyncResult, error) { syncRes, syncErr := r.sync(log, req, cleanup, prepare) - if err := r.syncUpdate(log, syncRes); err != nil { - syncErr = err + if syncErr != nil { + return syncRes, syncErr } - return syncRes, syncErr + return syncRes, r.syncUpdate(log, &syncRes) } -func (r ReconcilerBase) sync(log logr.Logger, req reconcile.Request, cleanup, prepare dataVolumeSyncResultFunc) (*dataVolumeSyncResult, error) { - syncRes := &dataVolumeSyncResult{} +func (r *ReconcilerBase) sync(log logr.Logger, req reconcile.Request, cleanup, prepare dataVolumeSyncResultFunc) (dataVolumeSyncResult, error) { + syncRes := dataVolumeSyncResult{} dv, err := r.getDataVolume(req.NamespacedName) if dv == nil || err != nil { syncRes.result = &reconcile.Result{} @@ -271,7 +325,7 @@ func (r ReconcilerBase) sync(log logr.Logger, req reconcile.Request, cleanup, pr if dv.DeletionTimestamp != nil { log.Info("DataVolume marked for deletion, cleaning up") if cleanup != nil { - if err := cleanup(syncRes); err != nil { + if err := cleanup(&syncRes); err != nil { return syncRes, err } } @@ -280,13 +334,22 @@ func (r ReconcilerBase) sync(log logr.Logger, req reconcile.Request, cleanup, pr } if prepare != nil { - if err := prepare(syncRes); err != nil { + if err := prepare(&syncRes); err != nil { return syncRes, err } } + syncRes.pvcSpec, err = renderPvcSpec(r.client, r.recorder, log, dv) + if err != nil { + return syncRes, err + } + + if err := r.handleStaticVolume(&syncRes, log); err != nil || syncRes.result != nil { + return syncRes, err + } + if syncRes.pvc != nil { - if err := r.garbageCollect(syncRes, log); err != nil { + if err := r.garbageCollect(&syncRes, log); err != nil { return syncRes, err } if syncRes.result != nil || syncRes.dv == nil { @@ -298,14 +361,10 @@ func (r ReconcilerBase) sync(log logr.Logger, req reconcile.Request, cleanup, pr r.handlePrePopulation(syncRes.dvMutated, syncRes.pvc) } - syncRes.pvcSpec, err = renderPvcSpec(r.client, r.recorder, log, dv) - if err != nil { - return syncRes, err - } return syncRes, nil } -func (r ReconcilerBase) syncUpdate(log logr.Logger, syncRes *dataVolumeSyncResult) error { +func (r *ReconcilerBase) syncUpdate(log logr.Logger, syncRes *dataVolumeSyncResult) error { if syncRes.dv == nil || syncRes.dvMutated == nil { return nil } @@ -323,18 +382,123 @@ func (r ReconcilerBase) syncUpdate(log logr.Logger, syncRes *dataVolumeSyncResul return nil } -func (r ReconcilerBase) handlePrePopulation(dv *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) { +func (r *ReconcilerBase) handleStaticVolume(syncRes *dataVolumeSyncResult, log logr.Logger) error { + if _, ok := syncRes.dvMutated.Annotations[cc.AnnCheckStaticVolume]; !ok { + return nil + } + + if syncRes.pvc == nil { + volumes, err := r.getAvailableVolumesForDV(syncRes, log) + if err != nil { + return err + } + + if len(volumes) == 0 { + log.Info("No PVs for DV") + return nil + } + + if err := r.handlePvcCreation(log, syncRes, func(_ *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error { + bs, err := json.Marshal(volumes) + if err != nil { + return err + } + cc.AddAnnotation(pvc, cc.AnnPersistentVolumeList, string(bs)) + return nil + }); err != nil { + return err + } + + // set result to make sure callers don't do anything else in sync + syncRes.result = &reconcile.Result{} + return nil + } + + volumeAnno, ok := syncRes.pvc.Annotations[cc.AnnPersistentVolumeList] + if !ok { + // etiher did not create the PVC here OR bind to expected PV succeeded + return nil + } + + if syncRes.pvc.Spec.VolumeName == "" { + // set result to make sure callers don't do anything else in sync + syncRes.result = &reconcile.Result{} + return nil + } + + var volumes []string + if err := json.Unmarshal([]byte(volumeAnno), &volumes); err != nil { + return err + } + + for _, v := range volumes { + if v == syncRes.pvc.Spec.VolumeName { + pvcCpy := syncRes.pvc.DeepCopy() + // handle as "populatedFor" going foreward + cc.AddAnnotation(pvcCpy, cc.AnnPopulatedFor, syncRes.dvMutated.Name) + delete(pvcCpy.Annotations, cc.AnnPersistentVolumeList) + if err := r.updatePVC(pvcCpy); err != nil { + return err + } + syncRes.pvc = pvcCpy + return nil + } + } + + // delete the pvc and hope for better luck... + pvcCpy := syncRes.pvc.DeepCopy() + if err := r.client.Delete(context.TODO(), pvcCpy, &client.DeleteOptions{}); err != nil { + return err + } + + syncRes.pvc = pvcCpy + + return fmt.Errorf("DataVolume bound to unexpected PV %s", syncRes.pvc.Spec.VolumeName) +} + +func (r *ReconcilerBase) getAvailableVolumesForDV(syncRes *dataVolumeSyncResult, log logr.Logger) ([]string, error) { + pvList := &corev1.PersistentVolumeList{} + fields := client.MatchingFields{claimRefField: claimRefIndexKeyFunc(syncRes.dv.Namespace, syncRes.dv.Name)} + if err := r.client.List(context.TODO(), pvList, fields); err != nil { + return nil, err + } + if syncRes.pvcSpec == nil { + return nil, fmt.Errorf("missing pvc spec") + } + var pvNames []string + for _, pv := range pvList.Items { + if pv.Status.Phase == corev1.VolumeAvailable { + pvc := &corev1.PersistentVolumeClaim{ + Spec: *syncRes.pvcSpec, + } + if err := checkVolumeSatisfyClaim(&pv, pvc); err != nil { + continue + } + log.Info("Found matching volume for DV", "pv", pv.Name) + pvNames = append(pvNames, pv.Name) + } + } + return pvNames, nil +} + +func (r *ReconcilerBase) handlePrePopulation(dv *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) { if pvc.Status.Phase == corev1.ClaimBound && pvcIsPopulated(pvc, dv) { cc.AddAnnotation(dv, cc.AnnPrePopulated, pvc.Name) } } func (r *ReconcilerBase) validatePVC(dv *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error { + // If the PVC is being deleted, we should log a warning to the event recorder and return to wait the deletion complete + // don't bother with owner refs is the pvc is deleted + if pvc.DeletionTimestamp != nil { + msg := fmt.Sprintf(MessageResourceMarkedForDeletion, pvc.Name) + r.recorder.Event(dv, corev1.EventTypeWarning, ErrResourceMarkedForDeletion, msg) + return errors.Errorf(msg) + } // If the PVC is not controlled by this DataVolume resource, we should log // a warning to the event recorder and return - pvcPopulated := pvcIsPopulated(pvc, dv) if !metav1.IsControlledBy(pvc, dv) { - if pvcPopulated { + if pvcIsPopulated(pvc, dv) { if err := r.addOwnerRef(pvc, dv); err != nil { return err } @@ -344,12 +508,6 @@ func (r *ReconcilerBase) validatePVC(dv *cdiv1.DataVolume, pvc *corev1.Persisten return errors.Errorf(msg) } } - // If the PVC is being deleted, we should log a warning to the event recorder and return to wait the deletion complete - if pvc.DeletionTimestamp != nil { - msg := fmt.Sprintf(MessageResourceMarkedForDeletion, pvc.Name) - r.recorder.Event(dv, corev1.EventTypeWarning, ErrResourceMarkedForDeletion, msg) - return errors.Errorf(msg) - } return nil } @@ -463,7 +621,7 @@ func (r *ReconcilerBase) updateDataVolumeStatusPhaseWithEvent( type updateStatusPhaseFunc func(pvc *corev1.PersistentVolumeClaim, dataVolumeCopy *cdiv1.DataVolume, event *Event) error -func (r ReconcilerBase) updateStatusCommon(syncRes dataVolumeSyncResult, updateStatusPhase updateStatusPhaseFunc) (reconcile.Result, error) { +func (r *ReconcilerBase) updateStatusCommon(syncRes dataVolumeSyncResult, updateStatusPhase updateStatusPhaseFunc) (reconcile.Result, error) { if syncRes.dv == nil { return reconcile.Result{}, nil } @@ -474,7 +632,9 @@ func (r ReconcilerBase) updateStatusCommon(syncRes dataVolumeSyncResult, updateS pvc := syncRes.pvc var event Event - if pvc != nil { + if shouldSetDataVolumePending(pvc, dataVolumeCopy) { + dataVolumeCopy.Status.Phase = cdiv1.Pending + } else if pvc != nil { dataVolumeCopy.Status.ClaimName = pvc.Name // the following check is for a case where the request is to create a blank disk for a block device. @@ -532,11 +692,6 @@ func (r ReconcilerBase) updateStatusCommon(syncRes dataVolumeSyncResult, updateS if err := r.reconcileProgressUpdate(dataVolumeCopy, pvc, &result); err != nil { return result, err } - } else { - _, ok := dataVolumeCopy.Annotations[cc.AnnPrePopulated] - if ok { - dataVolumeCopy.Status.Phase = cdiv1.Pending - } } currentCond := make([]cdiv1.DataVolumeCondition, len(dataVolumeCopy.Status.Conditions)) @@ -872,7 +1027,7 @@ func (r *ReconcilerBase) handlePvcCreation(log logr.Logger, syncRes *dataVolumeS if syncRes.pvc != nil { return nil } - if _, dvPrePopulated := syncRes.dvMutated.Annotations[cc.AnnPrePopulated]; dvPrePopulated { + if dvIsPrePopulated(syncRes.dvMutated) { return nil } // Creating the PVC diff --git a/pkg/controller/datavolume/external-population-controller.go b/pkg/controller/datavolume/external-population-controller.go index ce8c7e221b..e7f12bb481 100644 --- a/pkg/controller/datavolume/external-population-controller.go +++ b/pkg/controller/datavolume/external-population-controller.go @@ -84,16 +84,18 @@ func NewPopulatorController(ctx context.Context, mgr manager.Manager, log logr.L } // Reconcile loop for externally populated DataVolumes -func (r PopulatorReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { +func (r *PopulatorReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { log := r.log.WithValues("DataVolume", req.NamespacedName) return r.updateStatus(r.sync(log, req)) } // If dataSourceRef is set (external populator), use an empty spec.Source field -func (r PopulatorReconciler) prepare(syncRes *dataVolumeSyncResult) error { +func (r *PopulatorReconciler) prepare(syncRes *dataVolumeSyncResult) error { if !dvUsesVolumePopulator(syncRes.dv) { return errors.Errorf("undefined population source") } + // TODO - let's revisit this + syncRes.dv.Spec.Source = &cdiv1.DataVolumeSource{} syncRes.dvMutated.Spec.Source = &cdiv1.DataVolumeSource{} return nil } @@ -184,7 +186,7 @@ func isSnapshotPopulation(pvc *corev1.PersistentVolumeClaim) bool { // Generic controller functions -func (r PopulatorReconciler) updateAnnotations(dataVolume *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error { +func (r *PopulatorReconciler) updateAnnotations(dataVolume *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error { if !dvUsesVolumePopulator(dataVolume) { return errors.Errorf("undefined population source") } @@ -192,27 +194,30 @@ func (r PopulatorReconciler) updateAnnotations(dataVolume *cdiv1.DataVolume, pvc return nil } -func (r PopulatorReconciler) sync(log logr.Logger, req reconcile.Request) (dataVolumeSyncResult, error) { +func (r *PopulatorReconciler) sync(log logr.Logger, req reconcile.Request) (dataVolumeSyncResult, error) { syncRes, syncErr := r.syncExternalPopulation(log, req) + // TODO _ I think it is bad form that the datavolume is updated even in the case of error if err := r.syncUpdate(log, &syncRes); err != nil { syncErr = err } return syncRes, syncErr } -func (r PopulatorReconciler) syncExternalPopulation(log logr.Logger, req reconcile.Request) (dataVolumeSyncResult, error) { +func (r *PopulatorReconciler) syncExternalPopulation(log logr.Logger, req reconcile.Request) (dataVolumeSyncResult, error) { syncRes, syncErr := r.syncCommon(log, req, nil, r.prepare) if syncErr != nil || syncRes.result != nil { - return *syncRes, syncErr + return syncRes, syncErr } - if err := r.handlePvcCreation(log, syncRes, r.updateAnnotations); err != nil { + if err := r.handlePvcCreation(log, &syncRes, r.updateAnnotations); err != nil { syncErr = err } - return *syncRes, syncErr + return syncRes, syncErr } -func (r PopulatorReconciler) updateStatus(syncRes dataVolumeSyncResult, syncErr error) (reconcile.Result, error) { +func (r *PopulatorReconciler) updateStatus(syncRes dataVolumeSyncResult, syncErr error) (reconcile.Result, error) { + // TODO FIXME - WE SHOULD ALWAYS UPDATE STATUS if syncErr != nil { + r.log.Info("FIXME should not return because of this", "err", syncErr) return getReconcileResult(syncRes.result), syncErr } res, err := r.updateStatusCommon(syncRes, r.updateStatusPhase) @@ -222,7 +227,7 @@ func (r PopulatorReconciler) updateStatus(syncRes dataVolumeSyncResult, syncErr return res, syncErr } -func (r PopulatorReconciler) updateStatusPhase(pvc *corev1.PersistentVolumeClaim, dataVolumeCopy *cdiv1.DataVolume, event *Event) error { +func (r *PopulatorReconciler) updateStatusPhase(pvc *corev1.PersistentVolumeClaim, dataVolumeCopy *cdiv1.DataVolume, event *Event) error { // * Population by Snapshots doesn't have additional requirements. // * Population by PVC requires CSI drivers. // * Population by external populators requires both CSI drivers and the AnyVolumeDataSource feature gate. diff --git a/pkg/controller/datavolume/import-controller.go b/pkg/controller/datavolume/import-controller.go index 310224d084..d935505c01 100644 --- a/pkg/controller/datavolume/import-controller.go +++ b/pkg/controller/datavolume/import-controller.go @@ -31,7 +31,6 @@ import ( cc "kubevirt.io/containerized-data-importer/pkg/controller/common" featuregates "kubevirt.io/containerized-data-importer/pkg/feature-gates" - "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -102,18 +101,13 @@ func NewImportController( } func addDataVolumeImportControllerWatches(mgr manager.Manager, datavolumeController controller.Controller) error { - if err := mgr.GetFieldIndexer().IndexField(context.TODO(), &cdiv1.DataVolume{}, dvPhaseField, func(obj client.Object) []string { - return []string{string(obj.(*cdiv1.DataVolume).Status.Phase)} - }); err != nil { - return err - } if err := addDataVolumeControllerCommonWatches(mgr, datavolumeController, dataVolumeImport); err != nil { return err } return nil } -func (r ImportReconciler) updateAnnotations(dataVolume *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error { +func (r *ImportReconciler) updateAnnotations(dataVolume *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error { annotations := pvc.Annotations if checkpoint := r.getNextCheckpoint(dataVolume, pvc); checkpoint != nil { @@ -205,36 +199,41 @@ func (r ImportReconciler) updateAnnotations(dataVolume *cdiv1.DataVolume, pvc *c } // Reconcile loop for the import data volumes -func (r ImportReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { +func (r *ImportReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { log := r.log.WithValues("DataVolume", req.NamespacedName) return r.updateStatus(r.sync(log, req)) } -func (r ImportReconciler) sync(log logr.Logger, req reconcile.Request) (dataVolumeSyncResult, error) { +func (r *ImportReconciler) sync(log logr.Logger, req reconcile.Request) (dataVolumeSyncResult, error) { syncRes, syncErr := r.syncImport(log, req) + // TODO _ I think it is bad form that the datavolume is updated even in the case of error if err := r.syncUpdate(log, &syncRes); err != nil { syncErr = err } return syncRes, syncErr } -func (r ImportReconciler) syncImport(log logr.Logger, req reconcile.Request) (dataVolumeSyncResult, error) { +func (r *ImportReconciler) syncImport(log logr.Logger, req reconcile.Request) (dataVolumeSyncResult, error) { syncRes, syncErr := r.syncCommon(log, req, nil, nil) if syncErr != nil || syncRes.result != nil { - return *syncRes, syncErr + return syncRes, syncErr } - if err := r.handlePvcCreation(log, syncRes, r.updateAnnotations); err != nil { + if err := r.handlePvcCreation(log, &syncRes, r.updateAnnotations); err != nil { syncErr = err } if syncRes.pvc != nil && syncErr == nil { - r.setVddkAnnotations(*syncRes) + r.setVddkAnnotations(&syncRes) syncErr = r.maybeSetPvcMultiStageAnnotation(syncRes.pvc, syncRes.dvMutated) } - return *syncRes, syncErr + return syncRes, syncErr } -func (r ImportReconciler) updateStatus(syncRes dataVolumeSyncResult, syncErr error) (reconcile.Result, error) { +func (r *ImportReconciler) updateStatus(syncRes dataVolumeSyncResult, syncErr error) (reconcile.Result, error) { + // TODO FIXME - WE SHOULD ALWAYS UPDATE STATUS + // BUT THIS TEST FAILS WITHOUT IT + // [test_id:7737]when creating import dv with given valid url if syncErr != nil { + r.log.Info("FIXME should not return because of this", "err", syncErr) return getReconcileResult(syncRes.result), syncErr } res, err := r.updateStatusCommon(syncRes, r.updateStatusPhase) @@ -244,7 +243,7 @@ func (r ImportReconciler) updateStatus(syncRes dataVolumeSyncResult, syncErr err return res, syncErr } -func (r ImportReconciler) updateStatusPhase(pvc *corev1.PersistentVolumeClaim, dataVolumeCopy *cdiv1.DataVolume, event *Event) error { +func (r *ImportReconciler) updateStatusPhase(pvc *corev1.PersistentVolumeClaim, dataVolumeCopy *cdiv1.DataVolume, event *Event) error { phase, ok := pvc.Annotations[cc.AnnPodPhase] if phase != string(corev1.PodSucceeded) { _, ok := pvc.Annotations[cc.AnnImportPod] @@ -295,7 +294,7 @@ func (r ImportReconciler) updateStatusPhase(pvc *corev1.PersistentVolumeClaim, d return nil } -func (r ImportReconciler) setVddkAnnotations(syncRes dataVolumeSyncResult) { +func (r *ImportReconciler) setVddkAnnotations(syncRes *dataVolumeSyncResult) { if cc.GetSource(syncRes.pvc) != cc.SourceVDDK { return } @@ -307,7 +306,7 @@ func (r ImportReconciler) setVddkAnnotations(syncRes dataVolumeSyncResult) { } } -func (r ImportReconciler) updatesMultistageImportSucceeded(pvc *corev1.PersistentVolumeClaim, dataVolumeCopy *cdiv1.DataVolume) error { +func (r *ImportReconciler) updatesMultistageImportSucceeded(pvc *corev1.PersistentVolumeClaim, dataVolumeCopy *cdiv1.DataVolume) error { if multiStageImport := metav1.HasAnnotation(pvc.ObjectMeta, cc.AnnCurrentCheckpoint); !multiStageImport { return nil } diff --git a/pkg/controller/datavolume/import-controller_test.go b/pkg/controller/datavolume/import-controller_test.go index 31deb35d37..ea54fc3a3e 100644 --- a/pkg/controller/datavolume/import-controller_test.go +++ b/pkg/controller/datavolume/import-controller_test.go @@ -833,9 +833,8 @@ var _ = Describe("All DataVolume Tests", func() { } Expect(found).To(BeTrue()) }, - Entry("should remain unset", cdiv1.PhaseUnset, cdiv1.PhaseUnset), + Entry("should become pending", cdiv1.PhaseUnset, cdiv1.Pending), Entry("should remain pending", cdiv1.Pending, cdiv1.Pending), - Entry("should remain snapshotforsmartcloninginprogress", cdiv1.SnapshotForSmartCloneInProgress, cdiv1.SnapshotForSmartCloneInProgress), Entry("should remain inprogress", cdiv1.ImportInProgress, cdiv1.ImportInProgress), ) diff --git a/pkg/controller/datavolume/pvc-clone-controller.go b/pkg/controller/datavolume/pvc-clone-controller.go index a1ef31489b..99154a83bf 100644 --- a/pkg/controller/datavolume/pvc-clone-controller.go +++ b/pkg/controller/datavolume/pvc-clone-controller.go @@ -222,12 +222,12 @@ func addDataSourceWatch(mgr manager.Manager, c controller.Controller) error { } // Reconcile loop for the clone data volumes -func (r PvcCloneReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { +func (r *PvcCloneReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { log := r.log.WithValues("DataVolume", req.NamespacedName) return r.updateStatus(r.sync(log, req)) } -func (r PvcCloneReconciler) prepare(syncRes *dataVolumeSyncResult) error { +func (r *PvcCloneReconciler) prepare(syncRes *dataVolumeSyncResult) error { dv := syncRes.dvMutated if err := r.populateSourceIfSourceRef(dv); err != nil { return err @@ -240,7 +240,7 @@ func (r PvcCloneReconciler) prepare(syncRes *dataVolumeSyncResult) error { return nil } -func (r PvcCloneReconciler) updateAnnotations(dataVolume *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error { +func (r *PvcCloneReconciler) updateAnnotations(dataVolume *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error { if dataVolume.Spec.Source.PVC == nil { return errors.Errorf("no source set for clone datavolume") } @@ -257,17 +257,19 @@ func (r PvcCloneReconciler) updateAnnotations(dataVolume *cdiv1.DataVolume, pvc return nil } -func (r PvcCloneReconciler) sync(log logr.Logger, req reconcile.Request) (dataVolumeCloneSyncResult, error) { +func (r *PvcCloneReconciler) sync(log logr.Logger, req reconcile.Request) (dataVolumeCloneSyncResult, error) { syncRes, syncErr := r.syncClone(log, req) + // TODO _ I think it is bad form that the datavolume is updated even in the case of error + // however if this function exits without calling syncUpdate() test_id:7736 and test_id:7739 fail if err := r.syncUpdate(log, &syncRes.dataVolumeSyncResult); err != nil { syncErr = err } return syncRes, syncErr } -func (r PvcCloneReconciler) syncClone(log logr.Logger, req reconcile.Request) (dataVolumeCloneSyncResult, error) { +func (r *PvcCloneReconciler) syncClone(log logr.Logger, req reconcile.Request) (dataVolumeCloneSyncResult, error) { res, syncErr := r.syncCommon(log, req, r.cleanup, r.prepare) - syncRes := dataVolumeCloneSyncResult{dataVolumeSyncResult: *res} + syncRes := dataVolumeCloneSyncResult{dataVolumeSyncResult: res} if syncErr != nil || syncRes.result != nil { return syncRes, syncErr } @@ -287,9 +289,10 @@ func (r PvcCloneReconciler) syncClone(log logr.Logger, req reconcile.Request) (d } pvcPopulated := pvcIsPopulated(pvc, datavolume) - _, prePopulated := datavolume.Annotations[cc.AnnPrePopulated] + staticProvisionPending := checkStaticProvisionPending(pvc, datavolume) + prePopulated := dvIsPrePopulated(datavolume) - if pvcPopulated || prePopulated { + if pvcPopulated || prePopulated || staticProvisionPending { return syncRes, nil } @@ -423,7 +426,7 @@ func (r PvcCloneReconciler) syncClone(log logr.Logger, req reconcile.Request) (d return syncRes, syncErr } -func (r PvcCloneReconciler) updateStatus(syncRes dataVolumeCloneSyncResult, syncErr error) (reconcile.Result, error) { +func (r *PvcCloneReconciler) updateStatus(syncRes dataVolumeCloneSyncResult, syncErr error) (reconcile.Result, error) { if ps := syncRes.phaseSync; ps != nil { if err := r.updateDataVolumeStatusPhaseWithEvent(ps.phase, syncRes.dv, syncRes.dvMutated, ps.pvc, ps.event); err != nil { syncErr = err @@ -708,7 +711,7 @@ func (r *PvcCloneReconciler) sourceInUse(dv *cdiv1.DataVolume, eventReason strin return len(pods) > 0, nil } -func (r PvcCloneReconciler) cleanup(syncRes *dataVolumeSyncResult) error { +func (r *PvcCloneReconciler) cleanup(syncRes *dataVolumeSyncResult) error { dv := syncRes.dvMutated r.log.V(3).Info("Cleanup initiated in dv PVC clone controller") diff --git a/pkg/controller/datavolume/snapshot-clone-controller.go b/pkg/controller/datavolume/snapshot-clone-controller.go index 42fd03896b..f4d9349a4e 100644 --- a/pkg/controller/datavolume/snapshot-clone-controller.go +++ b/pkg/controller/datavolume/snapshot-clone-controller.go @@ -122,12 +122,12 @@ func addDataVolumeSnapshotCloneControllerWatches(mgr manager.Manager, datavolume } // Reconcile loop for the clone data volumes -func (r SnapshotCloneReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { +func (r *SnapshotCloneReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { log := r.log.WithValues("DataVolume", req.NamespacedName) return r.updateStatus(r.sync(log, req)) } -func (r SnapshotCloneReconciler) prepare(syncRes *dataVolumeSyncResult) error { +func (r *SnapshotCloneReconciler) prepare(syncRes *dataVolumeSyncResult) error { dv := syncRes.dvMutated if err := r.populateSourceIfSourceRef(dv); err != nil { return err @@ -141,7 +141,7 @@ func (r SnapshotCloneReconciler) prepare(syncRes *dataVolumeSyncResult) error { return nil } -func (r SnapshotCloneReconciler) updateAnnotations(dataVolume *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error { +func (r *SnapshotCloneReconciler) updateAnnotations(dataVolume *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error { if dataVolume.Spec.Source.Snapshot == nil { return errors.Errorf("no source set for clone datavolume") } @@ -159,17 +159,18 @@ func (r SnapshotCloneReconciler) updateAnnotations(dataVolume *cdiv1.DataVolume, return nil } -func (r SnapshotCloneReconciler) sync(log logr.Logger, req reconcile.Request) (dataVolumeCloneSyncResult, error) { +func (r *SnapshotCloneReconciler) sync(log logr.Logger, req reconcile.Request) (dataVolumeCloneSyncResult, error) { syncRes, syncErr := r.syncSnapshotClone(log, req) + // TODO _ I think it is bad form that the datavolume is updated even in the case of error if err := r.syncUpdate(log, &syncRes.dataVolumeSyncResult); err != nil { syncErr = err } return syncRes, syncErr } -func (r SnapshotCloneReconciler) syncSnapshotClone(log logr.Logger, req reconcile.Request) (dataVolumeCloneSyncResult, error) { +func (r *SnapshotCloneReconciler) syncSnapshotClone(log logr.Logger, req reconcile.Request) (dataVolumeCloneSyncResult, error) { res, syncErr := r.syncCommon(log, req, r.cleanup, r.prepare) - syncRes := dataVolumeCloneSyncResult{dataVolumeSyncResult: *res} + syncRes := dataVolumeCloneSyncResult{dataVolumeSyncResult: res} if syncErr != nil || syncRes.result != nil { return syncRes, syncErr } @@ -180,9 +181,10 @@ func (r SnapshotCloneReconciler) syncSnapshotClone(log logr.Logger, req reconcil transferName := getTransferName(datavolume) pvcPopulated := pvcIsPopulated(pvc, datavolume) + staticProvisionPending := checkStaticProvisionPending(pvc, datavolume) _, prePopulated := datavolume.Annotations[cc.AnnPrePopulated] - if pvcPopulated || prePopulated { + if pvcPopulated || prePopulated || staticProvisionPending { return syncRes, nil } @@ -513,7 +515,7 @@ func getTempHostAssistedSourcePvcName(dv *cdiv1.DataVolume) string { return fmt.Sprintf("%s-host-assisted-source-pvc", dv.GetUID()) } -func (r SnapshotCloneReconciler) updateStatus(syncRes dataVolumeCloneSyncResult, syncErr error) (reconcile.Result, error) { +func (r *SnapshotCloneReconciler) updateStatus(syncRes dataVolumeCloneSyncResult, syncErr error) (reconcile.Result, error) { if ps := syncRes.phaseSync; ps != nil { if err := r.updateDataVolumeStatusPhaseWithEvent(ps.phase, syncRes.dv, syncRes.dvMutated, ps.pvc, ps.event); err != nil { syncErr = err @@ -527,7 +529,7 @@ func (r SnapshotCloneReconciler) updateStatus(syncRes dataVolumeCloneSyncResult, return res, syncErr } -func (r SnapshotCloneReconciler) cleanup(syncRes *dataVolumeSyncResult) error { +func (r *SnapshotCloneReconciler) cleanup(syncRes *dataVolumeSyncResult) error { dv := syncRes.dvMutated r.log.V(3).Info("Cleanup initiated in dv snapshot clone controller") diff --git a/pkg/controller/datavolume/static-volume_test.go b/pkg/controller/datavolume/static-volume_test.go new file mode 100644 index 0000000000..6af13fc018 --- /dev/null +++ b/pkg/controller/datavolume/static-volume_test.go @@ -0,0 +1,296 @@ +/* +Copyright 2020 The CDI 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 datavolume + +import ( + "context" + "fmt" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" + . "kubevirt.io/containerized-data-importer/pkg/controller/common" +) + +var _ = Describe("checkStaticVolume Tests", func() { + const dvName = "static-check-dv" + + type testArgs struct { + newDV func() *cdiv1.DataVolume + newReconciler func(...runtime.Object) (reconcile.Reconciler, client.Client) + } + + addAnno := func(obj metav1.Object) { + if obj.GetAnnotations() == nil { + obj.SetAnnotations(make(map[string]string)) + } + obj.GetAnnotations()[AnnCheckStaticVolume] = "" + } + + newImportDV := func() *cdiv1.DataVolume { + dv := NewImportDataVolume(dvName) + addAnno(dv) + return dv + } + + importReconciler := func(objects ...runtime.Object) (reconcile.Reconciler, client.Client) { + r := createImportReconciler(objects...) + return r, r.client + } + + newImportArgs := func() *testArgs { + return &testArgs{ + newDV: newImportDV, + newReconciler: importReconciler, + } + } + + newUploadDV := func() *cdiv1.DataVolume { + dv := newUploadDataVolume(dvName) + addAnno(dv) + return dv + } + + uploadReconciler := func(objects ...runtime.Object) (reconcile.Reconciler, client.Client) { + r := createUploadReconciler(objects...) + return r, r.client + } + + newUploadArgs := func() *testArgs { + return &testArgs{ + newDV: newUploadDV, + newReconciler: uploadReconciler, + } + } + + newCloneDV := func() *cdiv1.DataVolume { + dv := newCloneDataVolume("test-dv") + addAnno(dv) + return dv + } + + cloneReconciler := func(objects ...runtime.Object) (reconcile.Reconciler, client.Client) { + r := createCloneReconciler(objects...) + return r, r.client + } + + newCloneArgs := func() *testArgs { + return &testArgs{ + newDV: newCloneDV, + newReconciler: cloneReconciler, + } + } + + newSnapshotCloneDV := func() *cdiv1.DataVolume { + dv := newCloneFromSnapshotDataVolume("test-dv") + addAnno(dv) + return dv + } + + snapshotCloneReconciler := func(objects ...runtime.Object) (reconcile.Reconciler, client.Client) { + r := createSnapshotCloneReconciler(objects...) + return r, r.client + } + + newSnapshotCloneArgs := func() *testArgs { + return &testArgs{ + newDV: newSnapshotCloneDV, + newReconciler: snapshotCloneReconciler, + } + } + + newPopulatorDV := func() *cdiv1.DataVolume { + pvcDataSource := &corev1.TypedLocalObjectReference{ + Kind: "PersistentVolumeClaim", + Name: "test", + } + dv := newPopulatorDataVolume("test-dv", pvcDataSource, nil) + addAnno(dv) + return dv + } + + populatorReconciler := func(objects ...runtime.Object) (reconcile.Reconciler, client.Client) { + r := createPopulatorReconciler(objects...) + return r, r.client + } + + newPopulatorArgs := func() *testArgs { + return &testArgs{ + newDV: newPopulatorDV, + newReconciler: populatorReconciler, + } + } + + newAvailablePV := func(dv *cdiv1.DataVolume) *corev1.PersistentVolume { + return &corev1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pv1", + }, + Spec: corev1.PersistentVolumeSpec{ + ClaimRef: &corev1.ObjectReference{ + Name: dv.Name, + Namespace: dv.Namespace, + }, + AccessModes: dv.Spec.PVC.AccessModes, + Capacity: dv.Spec.PVC.Resources.Requests, + }, + Status: corev1.PersistentVolumeStatus{ + Phase: corev1.VolumeAvailable, + }, + } + } + + newPendingPVC := func(dv *cdiv1.DataVolume, pv *corev1.PersistentVolume) *corev1.PersistentVolumeClaim { + return &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: dv.Namespace, + Name: dv.Name, + Annotations: map[string]string{ + AnnPersistentVolumeList: fmt.Sprintf("[\"%s\"]", pv.Name), + }, + }, + } + } + + requestFunc := func(dv *cdiv1.DataVolume) reconcile.Request { + return reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: dv.Namespace, + Name: dv.Name, + }, + } + } + + pvcFunc := func(c client.Client, dv *cdiv1.DataVolume) (*corev1.PersistentVolumeClaim, error) { + pvc := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: dv.Namespace, + Name: dv.Name, + }, + } + err := c.Get(context.TODO(), client.ObjectKeyFromObject(pvc), pvc) + return pvc, err + } + + DescribeTable("should do nothing special if no PV exists", func(args *testArgs) { + dv := args.newDV() + reconciler, client := args.newReconciler(dv) + _, err := reconciler.Reconcile(context.TODO(), requestFunc(dv)) + Expect(err).ToNot(HaveOccurred()) + + pvc, err := pvcFunc(client, dv) + Expect(err).ToNot(HaveOccurred()) + Expect(pvc.Annotations).ToNot(HaveKey(AnnPersistentVolumeList)) + }, + Entry("with import DataVolume", newImportArgs()), + Entry("with upload DataVolume", newUploadArgs()), + Entry("with populator DataVolume", newPopulatorArgs()), + ) + + DescribeTable("should create PVC with persistentVolumeList annotation", func(args *testArgs) { + dv := args.newDV() + pv := newAvailablePV(dv) + reconciler, client := args.newReconciler(dv, pv) + _, err := reconciler.Reconcile(context.TODO(), requestFunc(dv)) + Expect(err).ToNot(HaveOccurred()) + + pvc, err := pvcFunc(client, dv) + Expect(err).ToNot(HaveOccurred()) + Expect(pvc.Annotations[AnnPersistentVolumeList]).To(Equal(fmt.Sprintf("[\"%s\"]", pv.Name))) + }, + Entry("with import DataVolume", newImportArgs()), + Entry("with upload DataVolume", newUploadArgs()), + Entry("with clone DataVolume", newCloneArgs()), + Entry("with snapshot clone DataVolume", newSnapshotCloneArgs()), + Entry("with populator DataVolume", newPopulatorArgs()), + ) + + DescribeTable("should do nothing if PVC not bound", func(args *testArgs) { + dv := args.newDV() + pv := newAvailablePV(dv) + pvc := newPendingPVC(dv, pv) + reconciler, client := args.newReconciler(dv, pv, pvc) + _, err := reconciler.Reconcile(context.TODO(), requestFunc(dv)) + Expect(err).ToNot(HaveOccurred()) + + pvc, err = pvcFunc(client, dv) + Expect(err).ToNot(HaveOccurred()) + Expect(pvc.Annotations[AnnPersistentVolumeList]).To(Equal(fmt.Sprintf("[\"%s\"]", pv.Name))) + Expect(pvc.Annotations).ToNot(HaveKey(AnnPopulatedFor)) + Expect(pvc.Spec.VolumeName).To(BeEmpty()) + }, + Entry("with import DataVolume", newImportArgs()), + Entry("with upload DataVolume", newUploadArgs()), + Entry("with clone DataVolume", newCloneArgs()), + Entry("with snapshot clone DataVolume", newSnapshotCloneArgs()), + Entry("with populator DataVolume", newPopulatorArgs()), + ) + + DescribeTable("should remove persistentVolumeList and add populatedForAnnotation", func(args *testArgs) { + dv := args.newDV() + pv := newAvailablePV(dv) + pvc := newPendingPVC(dv, pv) + pvc.Spec.VolumeName = pv.Name + pv.Status.Phase = corev1.VolumeBound + reconciler, client := args.newReconciler(dv, pv, pvc) + _, err := reconciler.Reconcile(context.TODO(), requestFunc(dv)) + Expect(err).ToNot(HaveOccurred()) + + pvc, err = pvcFunc(client, dv) + Expect(err).ToNot(HaveOccurred()) + Expect(pvc.Annotations).ToNot(HaveKey(AnnPersistentVolumeList)) + Expect(pvc.Annotations).To(HaveKey(AnnPopulatedFor)) + Expect(pvc.Spec.VolumeName).To(Equal(pv.Name)) + }, + Entry("with import DataVolume", newImportArgs()), + Entry("with upload DataVolume", newUploadArgs()), + Entry("with clone DataVolume", newCloneArgs()), + Entry("with snapshot clone DataVolume", newSnapshotCloneArgs()), + Entry("with populator DataVolume", newPopulatorArgs()), + ) + + DescribeTable("should delete PVC if it gets bound to unknown PV", func(args *testArgs) { + dv := args.newDV() + pv := newAvailablePV(dv) + pvc := newPendingPVC(dv, pv) + pvc.Spec.VolumeName = "foobar" + pv.Status.Phase = corev1.VolumeBound + reconciler, client := args.newReconciler(dv, pv, pvc) + _, err := reconciler.Reconcile(context.TODO(), requestFunc(dv)) + Expect(err).To(HaveOccurred()) + + _, err = pvcFunc(client, dv) + Expect(err).To(HaveOccurred()) + Expect(errors.IsNotFound(err)).To(BeTrue()) + }, + Entry("with import DataVolume", newImportArgs()), + Entry("with upload DataVolume", newUploadArgs()), + Entry("with clone DataVolume", newCloneArgs()), + Entry("with snapshot clone DataVolume", newSnapshotCloneArgs()), + Entry("with populator DataVolume", newPopulatorArgs()), + ) +}) diff --git a/pkg/controller/datavolume/upload-controller.go b/pkg/controller/datavolume/upload-controller.go index f9e271d02c..e22f15443b 100644 --- a/pkg/controller/datavolume/upload-controller.go +++ b/pkg/controller/datavolume/upload-controller.go @@ -92,7 +92,7 @@ func NewUploadController( return datavolumeController, nil } -func (r UploadReconciler) updateAnnotations(dataVolume *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error { +func (r *UploadReconciler) updateAnnotations(dataVolume *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error { if dataVolume.Spec.Source.Upload == nil { return errors.Errorf("no source set for upload datavolume") } @@ -101,32 +101,35 @@ func (r UploadReconciler) updateAnnotations(dataVolume *cdiv1.DataVolume, pvc *c } // Reconcile loop for the upload data volumes -func (r UploadReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { +func (r *UploadReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { log := r.log.WithValues("DataVolume", req.NamespacedName) return r.updateStatus(r.sync(log, req)) } -func (r UploadReconciler) sync(log logr.Logger, req reconcile.Request) (dataVolumeSyncResult, error) { +func (r *UploadReconciler) sync(log logr.Logger, req reconcile.Request) (dataVolumeSyncResult, error) { syncRes, syncErr := r.syncUpload(log, req) + // TODO _ I think it is bad form that the datavolume is updated even in the case of error if err := r.syncUpdate(log, &syncRes); err != nil { syncErr = err } return syncRes, syncErr } -func (r UploadReconciler) syncUpload(log logr.Logger, req reconcile.Request) (dataVolumeSyncResult, error) { +func (r *UploadReconciler) syncUpload(log logr.Logger, req reconcile.Request) (dataVolumeSyncResult, error) { syncRes, syncErr := r.syncCommon(log, req, nil, nil) if syncErr != nil || syncRes.result != nil { - return *syncRes, syncErr + return syncRes, syncErr } - if err := r.handlePvcCreation(log, syncRes, r.updateAnnotations); err != nil { + if err := r.handlePvcCreation(log, &syncRes, r.updateAnnotations); err != nil { syncErr = err } - return *syncRes, syncErr + return syncRes, syncErr } -func (r UploadReconciler) updateStatus(syncRes dataVolumeSyncResult, syncErr error) (reconcile.Result, error) { +func (r *UploadReconciler) updateStatus(syncRes dataVolumeSyncResult, syncErr error) (reconcile.Result, error) { + // TODO FIXME - WE SHOULD ALWAYS UPDATE STATUS if syncErr != nil { + r.log.Info("FIXME should not return because of this", "err", syncErr) return getReconcileResult(syncRes.result), syncErr } res, err := r.updateStatusCommon(syncRes, r.updateStatusPhase) @@ -136,7 +139,7 @@ func (r UploadReconciler) updateStatus(syncRes dataVolumeSyncResult, syncErr err return res, syncErr } -func (r UploadReconciler) updateStatusPhase(pvc *corev1.PersistentVolumeClaim, dataVolumeCopy *cdiv1.DataVolume, event *Event) error { +func (r *UploadReconciler) updateStatusPhase(pvc *corev1.PersistentVolumeClaim, dataVolumeCopy *cdiv1.DataVolume, event *Event) error { phase, ok := pvc.Annotations[cc.AnnPodPhase] if phase != string(corev1.PodSucceeded) { _, ok = pvc.Annotations[cc.AnnUploadRequest] diff --git a/pkg/controller/datavolume/util.go b/pkg/controller/datavolume/util.go index 0f861f37fe..08b7be26e8 100644 --- a/pkg/controller/datavolume/util.go +++ b/pkg/controller/datavolume/util.go @@ -32,6 +32,7 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/record" + storagehelpers "k8s.io/component-helpers/storage/volume" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -354,3 +355,71 @@ func getReconcileResult(result *reconcile.Result) reconcile.Result { } return reconcile.Result{} } + +// adapted from k8s.io/kubernetes/pkg/controller/volume/persistentvolume/pv_controller.go +// checkVolumeSatisfyClaim checks if the volume requested by the claim satisfies the requirements of the claim +func checkVolumeSatisfyClaim(volume *v1.PersistentVolume, claim *v1.PersistentVolumeClaim) error { + requestedQty := claim.Spec.Resources.Requests[v1.ResourceName(v1.ResourceStorage)] + requestedSize := requestedQty.Value() + + // check if PV's DeletionTimeStamp is set, if so, return error. + if volume.ObjectMeta.DeletionTimestamp != nil { + return fmt.Errorf("the volume is marked for deletion %q", volume.Name) + } + + volumeQty := volume.Spec.Capacity[v1.ResourceStorage] + volumeSize := volumeQty.Value() + if volumeSize < requestedSize { + return fmt.Errorf("requested PV is too small") + } + + // this differs from pv_controller, be loose on storage class if not specified + requestedClass := storagehelpers.GetPersistentVolumeClaimClass(claim) + if requestedClass != "" && storagehelpers.GetPersistentVolumeClass(volume) != requestedClass { + return fmt.Errorf("storageClassName does not match") + } + + if CheckVolumeModeMismatches(&claim.Spec, &volume.Spec) { + return fmt.Errorf("incompatible volumeMode") + } + + if !CheckAccessModes(claim, volume) { + return fmt.Errorf("incompatible accessMode") + } + + return nil +} + +// CheckVolumeModeMismatches is a convenience method that checks volumeMode for PersistentVolume +// and PersistentVolumeClaims +// TODO - later versions of k8s.io/component-helpers have this function +func CheckVolumeModeMismatches(pvcSpec *v1.PersistentVolumeClaimSpec, pvSpec *v1.PersistentVolumeSpec) bool { + // In HA upgrades, we cannot guarantee that the apiserver is on a version >= controller-manager. + // So we default a nil volumeMode to filesystem + requestedVolumeMode := v1.PersistentVolumeFilesystem + if pvcSpec.VolumeMode != nil { + requestedVolumeMode = *pvcSpec.VolumeMode + } + pvVolumeMode := v1.PersistentVolumeFilesystem + if pvSpec.VolumeMode != nil { + pvVolumeMode = *pvSpec.VolumeMode + } + return requestedVolumeMode != pvVolumeMode +} + +// CheckAccessModes returns true if PV satisfies all the PVC's requested AccessModes +// TODO - later versions of k8s.io/component-helpers have this function +func CheckAccessModes(claim *v1.PersistentVolumeClaim, volume *v1.PersistentVolume) bool { + pvModesMap := map[v1.PersistentVolumeAccessMode]bool{} + for _, mode := range volume.Spec.AccessModes { + pvModesMap[mode] = true + } + + for _, mode := range claim.Spec.AccessModes { + _, ok := pvModesMap[mode] + if !ok { + return false + } + } + return true +} diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel index 57e60cbb18..69f52468eb 100644 --- a/tests/BUILD.bazel +++ b/tests/BUILD.bazel @@ -38,6 +38,7 @@ go_test( "operator_test.go", "rbac_test.go", "smartclone_test.go", + "static-volume_test.go", "tests_suite_test.go", "transfer_test.go", "transport_test.go", diff --git a/tests/cloner_test.go b/tests/cloner_test.go index 6e90140143..c443750272 100644 --- a/tests/cloner_test.go +++ b/tests/cloner_test.go @@ -2611,7 +2611,7 @@ var _ = Describe("all clone tests", func() { createSnapshot(size, nil, volumeMode) for i = 0; i < repeat; i++ { - dataVolume := utils.NewDataVolumeForCloningFromSnapshot(fmt.Sprintf("clone-from-snap-%d", i), size, snapshot.Namespace, snapshot.Name, nil, &volumeMode) + dataVolume := utils.NewDataVolumeForSnapshotCloningAndStorageSpec(fmt.Sprintf("clone-from-snap-%d", i), size, snapshot.Namespace, snapshot.Name, nil, &volumeMode) By(fmt.Sprintf("Create new datavolume %s which will clone from volumesnapshot", dataVolume.Name)) dataVolume, err = utils.CreateDataVolumeFromDefinition(f.CdiClient, targetNs.Name, dataVolume) Expect(err).ToNot(HaveOccurred()) @@ -2679,7 +2679,7 @@ var _ = Describe("all clone tests", func() { createSnapshot(snapSourceSize, &noExpansionStorageClass.Name, volumeMode) for i = 0; i < repeat; i++ { - dataVolume := utils.NewDataVolumeForCloningFromSnapshot(fmt.Sprintf("clone-from-snap-%d", i), targetDvSize, snapshot.Namespace, snapshot.Name, &noExpansionStorageClass.Name, &volumeMode) + dataVolume := utils.NewDataVolumeForSnapshotCloningAndStorageSpec(fmt.Sprintf("clone-from-snap-%d", i), targetDvSize, snapshot.Namespace, snapshot.Name, &noExpansionStorageClass.Name, &volumeMode) By(fmt.Sprintf("Create new datavolume %s which will clone from volumesnapshot", dataVolume.Name)) dataVolume, err = utils.CreateDataVolumeFromDefinition(f.CdiClient, targetNs.Name, dataVolume) Expect(err).ToNot(HaveOccurred()) @@ -2724,7 +2724,7 @@ var _ = Describe("all clone tests", func() { size := "1Gi" volumeMode := v1.PersistentVolumeMode(v1.PersistentVolumeFilesystem) By("Create the clone before the source snapshot") - cloneDV := utils.NewDataVolumeForCloningFromSnapshot("clone-from-snap", size, f.Namespace.Name, "snap-"+dataVolumeName, nil, &volumeMode) + cloneDV := utils.NewDataVolumeForSnapshotCloningAndStorageSpec("clone-from-snap", size, f.Namespace.Name, "snap-"+dataVolumeName, nil, &volumeMode) cloneDV, err := utils.CreateDataVolumeFromDefinition(f.CdiClient, f.Namespace.Name, cloneDV) Expect(err).ToNot(HaveOccurred()) // Check if the NoSourceClone annotation exists in target PVC @@ -2761,7 +2761,7 @@ var _ = Describe("all clone tests", func() { By("Create source snapshot") createSnapshot(recommendedSnapSize, nil, volumeMode) - cloneDV := utils.NewDataVolumeForCloningFromSnapshot("clone-from-snap", "500Mi", f.Namespace.Name, "snap-"+dataVolumeName, nil, &volumeMode) + cloneDV := utils.NewDataVolumeForSnapshotCloningAndStorageSpec("clone-from-snap", "500Mi", f.Namespace.Name, "snap-"+dataVolumeName, nil, &volumeMode) _, err := utils.CreateDataVolumeFromDefinition(f.CdiClient, f.Namespace.Name, cloneDV) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("target resources requests storage size is smaller than the source")) diff --git a/tests/external_population_test.go b/tests/external_population_test.go index 47686a05a0..e755ca25f5 100644 --- a/tests/external_population_test.go +++ b/tests/external_population_test.go @@ -137,7 +137,7 @@ var _ = Describe("Population tests", func() { } By(fmt.Sprintf("Creating new datavolume %s", dataVolumeName)) - dataVolume := utils.NewDataVolumeWithExternalPopulation(dataVolumeName, "100Mi", f.CsiCloneSCName, corev1.PersistentVolumeMode(corev1.PersistentVolumeBlock), nil, dataSourceRef) + dataVolume := utils.NewDataVolumeWithExternalPopulationAndStorageSpec(dataVolumeName, "100Mi", f.CsiCloneSCName, corev1.PersistentVolumeMode(corev1.PersistentVolumeBlock), nil, dataSourceRef) controller.AddAnnotation(dataVolume, controller.AnnDeleteAfterCompletion, "false") dataVolume, err := utils.CreateDataVolumeFromDefinition(f.CdiClient, f.Namespace.Name, dataVolume) Expect(err).ToNot(HaveOccurred()) @@ -178,7 +178,7 @@ var _ = Describe("Population tests", func() { } By(fmt.Sprintf("Creating new datavolume %s", dataVolumeName)) - dataVolume := utils.NewDataVolumeWithExternalPopulation(dataVolumeName, "100Mi", f.CsiCloneSCName, corev1.PersistentVolumeMode(corev1.PersistentVolumeBlock), nil, dataSourceRef) + dataVolume := utils.NewDataVolumeWithExternalPopulationAndStorageSpec(dataVolumeName, "100Mi", f.CsiCloneSCName, corev1.PersistentVolumeMode(corev1.PersistentVolumeBlock), nil, dataSourceRef) controller.AddAnnotation(dataVolume, controller.AnnDeleteAfterCompletion, "false") dataVolume, err := utils.CreateDataVolumeFromDefinition(f.CdiClient, f.Namespace.Name, dataVolume) Expect(err).ToNot(HaveOccurred()) @@ -207,7 +207,7 @@ var _ = Describe("Population tests", func() { } By(fmt.Sprintf("Creating new datavolume %s", dataVolumeName)) - dataVolume := utils.NewDataVolumeWithExternalPopulation(dataVolumeName, "100Mi", scName, corev1.PersistentVolumeMode(corev1.PersistentVolumeFilesystem), nil, dataSourceRef) + dataVolume := utils.NewDataVolumeWithExternalPopulationAndStorageSpec(dataVolumeName, "100Mi", scName, corev1.PersistentVolumeMode(corev1.PersistentVolumeFilesystem), nil, dataSourceRef) controller.AddAnnotation(dataVolume, controller.AnnDeleteAfterCompletion, "false") dataVolume, err := utils.CreateDataVolumeFromDefinition(f.CdiClient, f.Namespace.Name, dataVolume) Expect(err).ToNot(HaveOccurred()) @@ -245,7 +245,7 @@ var _ = Describe("Population tests", func() { } By(fmt.Sprintf("Creating target datavolume %s", dataVolumeName)) - dataVolume := utils.NewDataVolumeWithExternalPopulation(dataVolumeName, "100Mi", f.CsiCloneSCName, corev1.PersistentVolumeMode(corev1.PersistentVolumeFilesystem), dataSource, nil) + dataVolume := utils.NewDataVolumeWithExternalPopulationAndStorageSpec(dataVolumeName, "100Mi", f.CsiCloneSCName, corev1.PersistentVolumeMode(corev1.PersistentVolumeFilesystem), dataSource, nil) controller.AddAnnotation(dataVolume, controller.AnnDeleteAfterCompletion, "false") dataVolume, err := utils.CreateDataVolumeFromDefinition(f.CdiClient, f.Namespace.Name, dataVolume) Expect(err).ToNot(HaveOccurred()) @@ -320,7 +320,7 @@ var _ = Describe("Population tests", func() { }, timeout, pollingInterval).Should(BeTrue()) By(fmt.Sprintf("Creating target datavolume %s", dataVolumeName)) - dataVolume := utils.NewDataVolumeWithExternalPopulation(dataVolumeName, "100Mi", f.SnapshotSCName, corev1.PersistentVolumeMode(corev1.PersistentVolumeFilesystem), dataSource, nil) + dataVolume := utils.NewDataVolumeWithExternalPopulationAndStorageSpec(dataVolumeName, "100Mi", f.SnapshotSCName, corev1.PersistentVolumeMode(corev1.PersistentVolumeFilesystem), dataSource, nil) controller.AddAnnotation(dataVolume, controller.AnnDeleteAfterCompletion, "false") dataVolume, err = utils.CreateDataVolumeFromDefinition(f.CdiClient, f.Namespace.Name, dataVolume) Expect(err).ToNot(HaveOccurred()) diff --git a/tests/static-volume_test.go b/tests/static-volume_test.go new file mode 100644 index 0000000000..9cadf9bb7e --- /dev/null +++ b/tests/static-volume_test.go @@ -0,0 +1,158 @@ +package tests_test + +import ( + "context" + "fmt" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" + controller "kubevirt.io/containerized-data-importer/pkg/controller/common" + "kubevirt.io/containerized-data-importer/tests/framework" + "kubevirt.io/containerized-data-importer/tests/utils" +) + +var _ = Describe("checkStaticVolume tests", func() { + f := framework.NewFramework("transfer-test") + + Context("with available PV", func() { + const ( + dvName = "testdv" + dvSize = "1Gi" + ) + + var ( + pvName string + pvStorageClass *string + pvMode *corev1.PersistentVolumeMode + sourceMD5 string + ) + + importDef := func() *cdiv1.DataVolume { + return utils.NewDataVolumeWithHTTPImport(dvName, dvSize, fmt.Sprintf(utils.TinyCoreIsoURL, f.CdiInstallNs)) + } + + uploadDef := func() *cdiv1.DataVolume { + return utils.NewDataVolumeForUpload(dvName, dvSize) + } + + cloneDef := func() *cdiv1.DataVolume { + return utils.NewDataVolumeForImageCloning(dvName, dvSize, "baz", "foo", pvStorageClass, pvMode) + } + + snapshotCloneDef := func() *cdiv1.DataVolume { + return utils.NewDataVolumeForSnapshotCloning(dvName, dvSize, "baz", "foo", pvStorageClass, pvMode) + } + + populatorDef := func() *cdiv1.DataVolume { + dataSourceRef := &corev1.TypedLocalObjectReference{ + Kind: "PersistentVolumeClaim", + Name: "doesnotmatter", + } + return utils.NewDataVolumeWithExternalPopulation(dvName, dvSize, *pvStorageClass, *pvMode, nil, dataSourceRef) + } + + BeforeEach(func() { + By("Creating source DV") + // source here shouldn't matter + dvDef := importDef() + controller.AddAnnotation(dvDef, controller.AnnDeleteAfterCompletion, "false") + dv, err := utils.CreateDataVolumeFromDefinition(f.CdiClient, f.Namespace.Name, dvDef) + Expect(err).ToNot(HaveOccurred()) + + By("Waiting for source DV to succeed") + f.ForceBindPvcIfDvIsWaitForFirstConsumer(dv) + err = utils.WaitForDataVolumePhaseWithTimeout(f, f.Namespace.Name, cdiv1.Succeeded, dv.Name, 300*time.Second) + Expect(err).ToNot(HaveOccurred()) + + pvc, err := f.K8sClient.CoreV1().PersistentVolumeClaims(dv.Namespace).Get(context.TODO(), dv.Name, metav1.GetOptions{}) + Expect(err).ToNot(HaveOccurred()) + pvName = pvc.Spec.VolumeName + pvStorageClass = pvc.Spec.StorageClassName + pvMode = pvc.Spec.VolumeMode + + By("Getting source MD5") + md5, err := f.GetMD5(f.Namespace, pvc, utils.DefaultImagePath, 0) + Expect(err).ToNot(HaveOccurred()) + err = utils.DeleteVerifierPod(f.K8sClient, f.Namespace.Name) + Expect(err).ToNot(HaveOccurred()) + sourceMD5 = md5 + + By("Retaining PV") + pv, err := f.K8sClient.CoreV1().PersistentVolumes().Get(context.TODO(), pvName, metav1.GetOptions{}) + Expect(err).ToNot(HaveOccurred()) + pv.Spec.PersistentVolumeReclaimPolicy = corev1.PersistentVolumeReclaimRetain + _, err = f.K8sClient.CoreV1().PersistentVolumes().Update(context.TODO(), pv, metav1.UpdateOptions{}) + Expect(err).ToNot(HaveOccurred()) + + By("Deleting source DV") + err = utils.DeleteDataVolume(f.CdiClient, dv.Namespace, dv.Name) + Expect(err).ToNot(HaveOccurred()) + + By("Making PV available") + Eventually(func() bool { + pv, err := f.K8sClient.CoreV1().PersistentVolumes().Get(context.TODO(), pvName, metav1.GetOptions{}) + Expect(err).ToNot(HaveOccurred()) + Expect(pv.Spec.ClaimRef.Namespace).To(Equal(dv.Namespace)) + Expect(pv.Spec.ClaimRef.Name).To(Equal(dv.Name)) + if pv.Status.Phase == corev1.VolumeAvailable { + return true + } + pv.Spec.ClaimRef.ResourceVersion = "" + pv.Spec.ClaimRef.UID = "" + _, err = f.K8sClient.CoreV1().PersistentVolumes().Update(context.TODO(), pv, metav1.UpdateOptions{}) + Expect(err).ToNot(HaveOccurred()) + return false + }, timeout, pollingInterval).Should(BeTrue()) + }) + + AfterEach(func() { + if pvName == "" { + return + } + err := f.K8sClient.CoreV1().PersistentVolumes().Delete(context.TODO(), pvName, metav1.DeleteOptions{}) + if errors.IsNotFound(err) { + return + } + Expect(err).ToNot(HaveOccurred()) + }) + + DescribeTable("should handle static allocated DataVolume", func(defFunc func() *cdiv1.DataVolume) { + By("Creating target DV") + dvDef := defFunc() + controller.AddAnnotation(dvDef, controller.AnnCheckStaticVolume, "") + controller.AddAnnotation(dvDef, controller.AnnDeleteAfterCompletion, "false") + dv, err := utils.CreateDataVolumeFromDefinition(f.CdiClient, f.Namespace.Name, dvDef) + Expect(err).ToNot(HaveOccurred()) + + By("Waiting for target DV to succeed") + err = utils.WaitForDataVolumePhaseWithTimeout(f, f.Namespace.Name, cdiv1.Succeeded, dv.Name, 300*time.Second) + Expect(err).ToNot(HaveOccurred()) + + pvc, err := f.K8sClient.CoreV1().PersistentVolumeClaims(dv.Namespace).Get(context.TODO(), dv.Name, metav1.GetOptions{}) + Expect(err).ToNot(HaveOccurred()) + Expect(pvc.Spec.VolumeName).To(Equal(pvName)) + + pv, err := f.K8sClient.CoreV1().PersistentVolumes().Get(context.TODO(), pvName, metav1.GetOptions{}) + Expect(err).ToNot(HaveOccurred()) + Expect(pv.CreationTimestamp.Before(&pvc.CreationTimestamp)).To(BeTrue()) + + By("Verify content") + same, err := f.VerifyTargetPVCContentMD5(f.Namespace, pvc, utils.DefaultImagePath, sourceMD5, 0) + Expect(err).ToNot(HaveOccurred()) + Expect(same).To(BeTrue()) + }, + Entry("with import source", importDef), + Entry("with upload source", uploadDef), + Entry("with clone source", cloneDef), + Entry("with snapshot clone source", snapshotCloneDef), + Entry("with populator source", populatorDef), + ) + }) +}) diff --git a/tests/utils/datavolume.go b/tests/utils/datavolume.go index f65f8c06ed..84a32e539b 100644 --- a/tests/utils/datavolume.go +++ b/tests/utils/datavolume.go @@ -317,6 +317,30 @@ func NewDataVolumeWithHTTPImportToBlockPV(dataVolumeName string, size string, ht // NewDataVolumeWithExternalPopulation initializes a DataVolume struct meant to be externally populated func NewDataVolumeWithExternalPopulation(dataVolumeName, size, storageClassName string, volumeMode corev1.PersistentVolumeMode, dataSource, dataSourceRef *corev1.TypedLocalObjectReference) *cdiv1.DataVolume { + dataVolume := &cdiv1.DataVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: dataVolumeName, + }, + Spec: cdiv1.DataVolumeSpec{ + PVC: &corev1.PersistentVolumeClaimSpec{ + VolumeMode: &volumeMode, + StorageClassName: &storageClassName, + AccessModes: []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteOnce}, + DataSource: dataSource, + DataSourceRef: dataSourceRef, + Resources: k8sv1.ResourceRequirements{ + Requests: k8sv1.ResourceList{ + k8sv1.ResourceName(k8sv1.ResourceStorage): resource.MustParse(size), + }, + }, + }, + }, + } + return dataVolume +} + +// NewDataVolumeWithExternalPopulationAndStorageSpec initializes a DataVolume struct meant to be externally populated (with storage spec) +func NewDataVolumeWithExternalPopulationAndStorageSpec(dataVolumeName, size, storageClassName string, volumeMode corev1.PersistentVolumeMode, dataSource, dataSourceRef *corev1.TypedLocalObjectReference) *cdiv1.DataVolume { dataVolume := &cdiv1.DataVolume{ ObjectMeta: metav1.ObjectMeta{ Name: dataVolumeName, @@ -588,8 +612,41 @@ func NewDataVolumeForCloningWithEmptySize(dataVolumeName, namespace, pvcName str return dv } -// NewDataVolumeForCloningFromSnapshot initializes a DataVolume struct for cloning from a volume snapshot -func NewDataVolumeForCloningFromSnapshot(dataVolumeName, size, namespace, snapshot string, storageClassName *string, volumeMode *k8sv1.PersistentVolumeMode) *cdiv1.DataVolume { +// NewDataVolumeForSnapshotCloning initializes a DataVolume struct for cloning from a volume snapshot +func NewDataVolumeForSnapshotCloning(dataVolumeName, size, namespace, snapshot string, storageClassName *string, volumeMode *k8sv1.PersistentVolumeMode) *cdiv1.DataVolume { + dv := &cdiv1.DataVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: dataVolumeName, + Annotations: map[string]string{}, + }, + Spec: cdiv1.DataVolumeSpec{ + Source: &cdiv1.DataVolumeSource{ + Snapshot: &cdiv1.DataVolumeSourceSnapshot{ + Namespace: namespace, + Name: snapshot, + }, + }, + PVC: &corev1.PersistentVolumeClaimSpec{ + AccessModes: []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteOnce}, + Resources: k8sv1.ResourceRequirements{ + Requests: k8sv1.ResourceList{ + k8sv1.ResourceName(k8sv1.ResourceStorage): resource.MustParse(size), + }, + }, + }, + }, + } + if volumeMode != nil { + dv.Spec.PVC.VolumeMode = volumeMode + } + if storageClassName != nil { + dv.Spec.PVC.StorageClassName = storageClassName + } + return dv +} + +// NewDataVolumeForSnapshotCloningAndStorageSpec initializes a DataVolume struct for cloning from a volume snapshot +func NewDataVolumeForSnapshotCloningAndStorageSpec(dataVolumeName, size, namespace, snapshot string, storageClassName *string, volumeMode *k8sv1.PersistentVolumeMode) *cdiv1.DataVolume { dv := &cdiv1.DataVolume{ ObjectMeta: metav1.ObjectMeta{ Name: dataVolumeName, diff --git a/tests/webhook_test.go b/tests/webhook_test.go index 617fb259a2..3ed447987a 100644 --- a/tests/webhook_test.go +++ b/tests/webhook_test.go @@ -301,7 +301,7 @@ var _ = Describe("Clone Auth Webhook tests", func() { err = f.CrClient.Create(context.TODO(), snapshot) Expect(err).ToNot(HaveOccurred()) volumeMode := v1.PersistentVolumeMode(v1.PersistentVolumeFilesystem) - targetDV := utils.NewDataVolumeForCloningFromSnapshot("target-dv", "1G", snapshot.Namespace, snapshot.Name, nil, &volumeMode) + targetDV := utils.NewDataVolumeForSnapshotCloningAndStorageSpec("target-dv", "1G", snapshot.Namespace, snapshot.Name, nil, &volumeMode) client, err := f.GetCdiClientForServiceAccount(targetNamespace.Name, serviceAccountName) Expect(err).ToNot(HaveOccurred()) diff --git a/vendor/k8s.io/component-helpers/LICENSE b/vendor/k8s.io/component-helpers/LICENSE new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/vendor/k8s.io/component-helpers/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/vendor/k8s.io/component-helpers/storage/volume/BUILD.bazel b/vendor/k8s.io/component-helpers/storage/volume/BUILD.bazel new file mode 100644 index 0000000000..c5fc249260 --- /dev/null +++ b/vendor/k8s.io/component-helpers/storage/volume/BUILD.bazel @@ -0,0 +1,10 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["helpers.go"], + importmap = "kubevirt.io/containerized-data-importer/vendor/k8s.io/component-helpers/storage/volume", + importpath = "k8s.io/component-helpers/storage/volume", + visibility = ["//visibility:public"], + deps = ["//vendor/k8s.io/api/core/v1:go_default_library"], +) diff --git a/vendor/k8s.io/component-helpers/storage/volume/helpers.go b/vendor/k8s.io/component-helpers/storage/volume/helpers.go new file mode 100644 index 0000000000..a76e933448 --- /dev/null +++ b/vendor/k8s.io/component-helpers/storage/volume/helpers.go @@ -0,0 +1,44 @@ +/* +Copyright 2020 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 volume + +import v1 "k8s.io/api/core/v1" + +// GetPersistentVolumeClaimClass returns StorageClassName. If no storage class was +// requested, it returns "". +func GetPersistentVolumeClaimClass(claim *v1.PersistentVolumeClaim) string { + // Use beta annotation first + if class, found := claim.Annotations[v1.BetaStorageClassAnnotation]; found { + return class + } + + if claim.Spec.StorageClassName != nil { + return *claim.Spec.StorageClassName + } + + return "" +} + +// GetPersistentVolumeClass returns StorageClassName. +func GetPersistentVolumeClass(volume *v1.PersistentVolume) string { + // Use beta annotation first + if class, found := volume.Annotations[v1.BetaStorageClassAnnotation]; found { + return class + } + + return volume.Spec.StorageClassName +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 93e30a6af5..f74d56f700 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1143,6 +1143,9 @@ k8s.io/component-base/config/v1alpha1 k8s.io/component-base/metrics k8s.io/component-base/metrics/legacyregistry k8s.io/component-base/version +# k8s.io/component-helpers v0.23.0 => k8s.io/component-helpers v0.23.0 +## explicit; go 1.16 +k8s.io/component-helpers/storage/volume # k8s.io/gengo v0.0.0-20211129171323-c02415ce4185 ## explicit; go 1.13 k8s.io/gengo/args