mirror of
https://github.com/kubevirt/containerized-data-importer.git
synced 2025-06-03 06:30:22 +00:00
Add PVC spec mutating webhook rendering based on StorageProfiles (#2813)
* Add PVC mutating webhook using StorageProfiles The webhook mutates the PVC Spec based on the available StorageProfiles, so for example you can create PVC without accessModes and it will be auto-completed. To use this feature, enable the `WebhookPvcRendering` feature gate. For any PVC you want to use StorageProfile, label it with: cdi.kubevirt.io/useStorageProfile: "true" If you want to use volumeMode preferred by CDI according to StorageProfiles, set it to FromStorageProfile. Otherwise if not explicitly set to Block, it will be Filesystem by k8s default. E.g.: apiVersion: v1 kind: PersistentVolumeClaim metadata: name: pvc-test labels: cdi.kubevirt.io/useStorageProfile: "true" spec: storageClassName: rook-ceph-block volumeMode: FromStorageProfile resources: requests: storage: 1Mi Signed-off-by: Arnon Gilboa <agilboa@redhat.com> * Move webhook create/delete to callback plus some CR fixes and cleanups Signed-off-by: Arnon Gilboa <agilboa@redhat.com> * Move webhook CR creation to sit with callbacks Signed-off-by: Arnon Gilboa <agilboa@redhat.com> * Update existing webhook if modified Signed-off-by: Arnon Gilboa <agilboa@redhat.com> * Eliminate unnecessary CR update Signed-off-by: Arnon Gilboa <agilboa@redhat.com> --------- Signed-off-by: Arnon Gilboa <agilboa@redhat.com>
This commit is contained in:
parent
cdc266d3ac
commit
221469d062
@ -3356,11 +3356,12 @@
|
||||
"type": "string"
|
||||
},
|
||||
"volumeMode": {
|
||||
"description": "volumeMode defines what type of volume is required by the claim. Value of Filesystem is implied when not included in claim spec.\n\nPossible enum values:\n - `\"Block\"` means the volume will not be formatted with a filesystem and will remain a raw block device.\n - `\"Filesystem\"` means the volume will be or is formatted with a filesystem.",
|
||||
"description": "volumeMode defines what type of volume is required by the claim. Value of Filesystem is implied when not included in claim spec.\n\nPossible enum values:\n - `\"Block\"` means the volume will not be formatted with a filesystem and will remain a raw block device.\n - `\"Filesystem\"` means the volume will be or is formatted with a filesystem.\n - `\"FromStorageProfile\"` means the volume mode will be auto selected by CDI according to a matching StorageProfile",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Block",
|
||||
"Filesystem"
|
||||
"Filesystem",
|
||||
"FromStorageProfile"
|
||||
]
|
||||
},
|
||||
"volumeName": {
|
||||
@ -5123,11 +5124,12 @@
|
||||
"type": "string"
|
||||
},
|
||||
"volumeMode": {
|
||||
"description": "volumeMode defines what type of volume is required by the claim. Value of Filesystem is implied when not included in claim spec.\n\nPossible enum values:\n - `\"Block\"` means the volume will not be formatted with a filesystem and will remain a raw block device.\n - `\"Filesystem\"` means the volume will be or is formatted with a filesystem.",
|
||||
"description": "volumeMode defines what type of volume is required by the claim. Value of Filesystem is implied when not included in claim spec.\n\nPossible enum values:\n - `\"Block\"` means the volume will not be formatted with a filesystem and will remain a raw block device.\n - `\"Filesystem\"` means the volume will be or is formatted with a filesystem.\n - `\"FromStorageProfile\"` means the volume mode will be auto selected by CDI according to a matching StorageProfile",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Block",
|
||||
"Filesystem"
|
||||
"Filesystem",
|
||||
"FromStorageProfile"
|
||||
]
|
||||
},
|
||||
"volumeName": {
|
||||
|
@ -9,14 +9,19 @@ go_library(
|
||||
"//pkg/apiserver:go_default_library",
|
||||
"//pkg/client/clientset/versioned:go_default_library",
|
||||
"//pkg/common:go_default_library",
|
||||
"//pkg/controller/datavolume:go_default_library",
|
||||
"//pkg/util/cert/watcher:go_default_library",
|
||||
"//pkg/util/tls-crypto-watch:go_default_library",
|
||||
"//pkg/version/verflag:go_default_library",
|
||||
"//staging/src/kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1:go_default_library",
|
||||
"//vendor/github.com/kelseyhightower/envconfig:go_default_library",
|
||||
"//vendor/github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1:go_default_library",
|
||||
"//vendor/github.com/kubernetes-csi/external-snapshotter/client/v6/clientset/versioned:go_default_library",
|
||||
"//vendor/github.com/pkg/errors:go_default_library",
|
||||
"//vendor/k8s.io/api/core/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
|
||||
"//vendor/k8s.io/client-go/kubernetes:go_default_library",
|
||||
"//vendor/k8s.io/client-go/kubernetes/scheme:go_default_library",
|
||||
"//vendor/k8s.io/client-go/tools/clientcmd:go_default_library",
|
||||
"//vendor/k8s.io/klog/v2:go_default_library",
|
||||
"//vendor/k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset:go_default_library",
|
||||
|
@ -25,13 +25,18 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
|
||||
snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1"
|
||||
snapclient "github.com/kubernetes-csi/external-snapshotter/client/v6/clientset/versioned"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/klog/v2"
|
||||
aggregatorclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/cluster"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager/signals"
|
||||
|
||||
@ -39,6 +44,7 @@ import (
|
||||
"kubevirt.io/containerized-data-importer/pkg/apiserver"
|
||||
cdiclient "kubevirt.io/containerized-data-importer/pkg/client/clientset/versioned"
|
||||
"kubevirt.io/containerized-data-importer/pkg/common"
|
||||
dvc "kubevirt.io/containerized-data-importer/pkg/controller/datavolume"
|
||||
certwatcher "kubevirt.io/containerized-data-importer/pkg/util/cert/watcher"
|
||||
cryptowatch "kubevirt.io/containerized-data-importer/pkg/util/tls-crypto-watch"
|
||||
"kubevirt.io/containerized-data-importer/pkg/version/verflag"
|
||||
@ -105,6 +111,10 @@ func main() {
|
||||
klog.Fatalf("Unable to get environment variables: %v\n", errors.WithStack(err))
|
||||
}
|
||||
|
||||
utilruntime.Must(corev1.AddToScheme(scheme.Scheme))
|
||||
utilruntime.Must(cdiv1.AddToScheme(scheme.Scheme))
|
||||
utilruntime.Must(snapshotv1.AddToScheme(scheme.Scheme))
|
||||
|
||||
cfg, err := clientcmd.BuildConfigFromFlags(kubeURL, configPath)
|
||||
if err != nil {
|
||||
klog.Fatalf("Unable to get kube config: %v\n", errors.WithStack(err))
|
||||
@ -130,6 +140,10 @@ func main() {
|
||||
klog.Fatalf("Unable to add to scheme: %v\n", errors.WithStack(err))
|
||||
}
|
||||
|
||||
if err := dvc.CreateAvailablePersistentVolumeIndex(cluster.GetFieldIndexer()); err != nil {
|
||||
klog.Fatalf("Unable to create field index: %v\n", errors.WithStack(err))
|
||||
}
|
||||
|
||||
ctx := signals.SetupSignalHandler()
|
||||
|
||||
authConfigWatcher, err := apiserver.NewAuthConfigWatcher(ctx, client)
|
||||
@ -165,7 +179,7 @@ func main() {
|
||||
certWatcher,
|
||||
installerLabels)
|
||||
if err != nil {
|
||||
klog.Fatalf("Upload api failed to initialize: %v\n", errors.WithStack(err))
|
||||
klog.Fatalf("CDI API server failed to initialize: %v\n", errors.WithStack(err))
|
||||
}
|
||||
|
||||
go func() {
|
||||
|
@ -37,6 +37,8 @@ spec:
|
||||
### Using populators with PVCs
|
||||
User can create a CR and PVCs specifying the CR in the `DataSourceRef` field and those will be handled by the matching populator controller.
|
||||
|
||||
PVC created with missing fields (`accessModes`, `volumeMode`, clone `storage` size), can be auto-completed with defaults from the [StorageProfiles](./storageprofile.md). This is achieved using [CDI PVC mutating webhook rendering](./pvc-mutating-webhook-rendering.md).
|
||||
|
||||
#### Import
|
||||
Example for PVC which use the VolumeImportSource above that will be handled by the import populator:
|
||||
```yaml
|
||||
|
85
doc/pvc-mutating-webhook-rendering.md
Normal file
85
doc/pvc-mutating-webhook-rendering.md
Normal file
@ -0,0 +1,85 @@
|
||||
# PVC Mutating Webhook Rendering
|
||||
|
||||
## Introduction
|
||||
|
||||
PVC Mutating Webhook Rendering is an optional CDI feature, allowing users to get CDI PVC rendering functionality without using a `DataVolume`. Traditionally, when the CDI DV controller creates a PVC, it renders the PVC spec (`volumeMode`, `accessMode`, `storage`) according to the DV storage spec (or default) `storageClass`, CDI `StorageProfiles`, `CDIConfig` `filesystemOverhead` etc.
|
||||
|
||||
The PVC mutating webhook eliminates the need for DVs for `StorageProfile` based rendering, providing auto-completion of PVC missing spec fields, based on optimal values per `StorageClass`. The webhook intercepts only explicitly CDI-labeled PVCs, so it won't affect cluster stability if the CDI api server is down. For labeled PVCs, `objectSelectors` decide when to call out over HTTP to the webhook, so if the CDI api server is down the request and PVC creation will fail. Unlabeled PVC will not be affected at all.
|
||||
|
||||
CDI volume populators already cover almost all DV import/clone/upload functionality, but miss the PVC rendering functionality, so this feature complements CDI volume populators, as together they get most DV pros, but without its cons (e.g. limitations in backup and restore, disaster recovery).
|
||||
|
||||
## Configuration
|
||||
|
||||
To be fully compatible with any external tools that may already use CDI, this new feature has to be enabled by the feature gate: `WebhookPvcRendering`. In the released `cdi-cr` it is disabled by default. To enable it, add the feature gate in the `CDI` custom resource, under spec.config (see [cdi-config doc](./cdi-config.md)).
|
||||
|
||||
A Snippet below shows CDI resource with `WebhookPvcRendering` enabled.
|
||||
```yaml
|
||||
apiVersion: cdi.kubevirt.io/v1beta1
|
||||
kind: CDI
|
||||
[...]
|
||||
spec:
|
||||
config:
|
||||
featureGates:
|
||||
- WebhookPvcRendering
|
||||
[...]
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
For any PVC you want to use `StorageProfile` mutating webhook rendering, label it with `cdi.kubevirt.io/useStorageProfile: "true"`
|
||||
|
||||
If you want to use `volumeMode` preferred by CDI according to `StorageProfiles`, set it to `FromStorageProfile`. Otherwise if not explicitly set to `Block`, it will be `Filesystem` by k8s default.
|
||||
|
||||
## Examples
|
||||
|
||||
Blank PVC (missing `accessMode` and using CDI preferred `volumeMode`):
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: my-blank-pvc
|
||||
labels:
|
||||
cdi.kubevirt.io/useStorageProfile: "true"
|
||||
spec:
|
||||
storageClassName: rook-ceph-block
|
||||
volumeMode: FromStorageProfile
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Mi
|
||||
```
|
||||
|
||||
PVC imported using the import populator (missing `accessMode` and using the k8s default `Filesystem` `volumeMode`):
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: my-imported-pvc
|
||||
labels:
|
||||
cdi.kubevirt.io/useStorageProfile: "true"
|
||||
spec:
|
||||
dataSourceRef:
|
||||
apiGroup: cdi.kubevirt.io
|
||||
kind: VolumeImportSource
|
||||
name: my-import-source
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
```
|
||||
|
||||
PVC cloned using the clone populator (missing `accessModes`, and `storage` which is detected from the source PVC if bound):
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: my-cloned-pvc
|
||||
labels:
|
||||
cdi.kubevirt.io/useStorageProfile: "true"
|
||||
spec:
|
||||
dataSourceRef:
|
||||
apiGroup: cdi.kubevirt.io
|
||||
kind: VolumeCloneSource
|
||||
name: my-clone-source
|
||||
```
|
@ -7,6 +7,8 @@ Storage Profile is the resource that serves the information about recommended pa
|
||||
This can be used by CDI controllers when creating a PVC for DV. That way the DataVolume can be simplified and if the properties are missing,
|
||||
defaults can be applied from the StorageProfile.
|
||||
|
||||
PVC created independently (without DV) with missing fields, can also be auto-completed with defaults from the StorageProfiles. This is achieved using [CDI PVC mutating webhook rendering](./pvc-mutating-webhook-rendering.md).
|
||||
|
||||
CDI provides a collection of Storage Profiles with default recommended values for some well known backends.
|
||||
If the storage provisioner defined in storage class does not have defaults configured in CDI the resulting StorageProfile
|
||||
has empty `claimPropertySets`.
|
||||
|
@ -17923,10 +17923,10 @@ func schema_k8sio_api_core_v1_PersistentVolumeClaimSpec(ref common.ReferenceCall
|
||||
},
|
||||
"volumeMode": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "volumeMode defines what type of volume is required by the claim. Value of Filesystem is implied when not included in claim spec.\n\nPossible enum values:\n - `\"Block\"` means the volume will not be formatted with a filesystem and will remain a raw block device.\n - `\"Filesystem\"` means the volume will be or is formatted with a filesystem.",
|
||||
Description: "volumeMode defines what type of volume is required by the claim. Value of Filesystem is implied when not included in claim spec.\n\nPossible enum values:\n - `\"Block\"` means the volume will not be formatted with a filesystem and will remain a raw block device.\n - `\"Filesystem\"` means the volume will be or is formatted with a filesystem.\n - `\"FromStorageProfile\"` means the volume mode will be auto selected by CDI according to a matching StorageProfile",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
Enum: []interface{}{"Block", "Filesystem"},
|
||||
Enum: []interface{}{"Block", "Filesystem", "FromStorageProfile"},
|
||||
},
|
||||
},
|
||||
"dataSource": {
|
||||
@ -18527,10 +18527,10 @@ func schema_k8sio_api_core_v1_PersistentVolumeSpec(ref common.ReferenceCallback)
|
||||
},
|
||||
"volumeMode": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "volumeMode defines if a volume is intended to be used with a formatted filesystem or to remain in raw block state. Value of Filesystem is implied when not included in spec.\n\nPossible enum values:\n - `\"Block\"` means the volume will not be formatted with a filesystem and will remain a raw block device.\n - `\"Filesystem\"` means the volume will be or is formatted with a filesystem.",
|
||||
Description: "volumeMode defines if a volume is intended to be used with a formatted filesystem or to remain in raw block state. Value of Filesystem is implied when not included in spec.\n\nPossible enum values:\n - `\"Block\"` means the volume will not be formatted with a filesystem and will remain a raw block device.\n - `\"Filesystem\"` means the volume will be or is formatted with a filesystem.\n - `\"FromStorageProfile\"` means the volume mode will be auto selected by CDI according to a matching StorageProfile",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
Enum: []interface{}{"Block", "Filesystem"},
|
||||
Enum: []interface{}{"Block", "Filesystem", "FromStorageProfile"},
|
||||
},
|
||||
},
|
||||
"nodeAffinity": {
|
||||
@ -27164,10 +27164,10 @@ func schema_pkg_apis_core_v1beta1_ClaimPropertySet(ref common.ReferenceCallback)
|
||||
},
|
||||
"volumeMode": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "VolumeMode defines what type of volume is required by the claim. Value of Filesystem is implied when not included in claim spec.\n\nPossible enum values:\n - `\"Block\"` means the volume will not be formatted with a filesystem and will remain a raw block device.\n - `\"Filesystem\"` means the volume will be or is formatted with a filesystem.",
|
||||
Description: "VolumeMode defines what type of volume is required by the claim. Value of Filesystem is implied when not included in claim spec.\n\nPossible enum values:\n - `\"Block\"` means the volume will not be formatted with a filesystem and will remain a raw block device.\n - `\"Filesystem\"` means the volume will be or is formatted with a filesystem.\n - `\"FromStorageProfile\"` means the volume mode will be auto selected by CDI according to a matching StorageProfile",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
Enum: []interface{}{"Block", "Filesystem"},
|
||||
Enum: []interface{}{"Block", "Filesystem", "FromStorageProfile"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -29386,10 +29386,10 @@ func schema_pkg_apis_core_v1beta1_StorageSpec(ref common.ReferenceCallback) comm
|
||||
},
|
||||
"volumeMode": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "volumeMode defines what type of volume is required by the claim. Value of Filesystem is implied when not included in claim spec.\n\nPossible enum values:\n - `\"Block\"` means the volume will not be formatted with a filesystem and will remain a raw block device.\n - `\"Filesystem\"` means the volume will be or is formatted with a filesystem.",
|
||||
Description: "volumeMode defines what type of volume is required by the claim. Value of Filesystem is implied when not included in claim spec.\n\nPossible enum values:\n - `\"Block\"` means the volume will not be formatted with a filesystem and will remain a raw block device.\n - `\"Filesystem\"` means the volume will be or is formatted with a filesystem.\n - `\"FromStorageProfile\"` means the volume mode will be auto selected by CDI according to a matching StorageProfile",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
Enum: []interface{}{"Block", "Filesystem"},
|
||||
Enum: []interface{}{"Block", "Filesystem", "FromStorageProfile"},
|
||||
},
|
||||
},
|
||||
"dataSource": {
|
||||
|
@ -67,6 +67,8 @@ const (
|
||||
|
||||
dvMutatePath = "/datavolume-mutate"
|
||||
|
||||
pvcMutatePath = "/pvc-mutate"
|
||||
|
||||
cdiValidatePath = "/cdi-validate"
|
||||
|
||||
objectTransferValidatePath = "/objecttransfer-validate"
|
||||
@ -189,6 +191,11 @@ func NewCdiAPIServer(bindAddress string,
|
||||
return nil, errors.Errorf("failed to create DataVolume mutating webhook: %s", err)
|
||||
}
|
||||
|
||||
err = app.createPvcMutatingWebhook()
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("failed to create PVC mutating webhook: %s", err)
|
||||
}
|
||||
|
||||
err = app.createCDIValidatingWebhook()
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("failed to create CDI validating webhook: %s", err)
|
||||
@ -527,6 +534,11 @@ func (app *cdiAPIApp) createDataVolumeMutatingWebhook() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *cdiAPIApp) createPvcMutatingWebhook() error {
|
||||
app.container.ServeMux.Handle(pvcMutatePath, webhooks.NewPvcMutatingWebhook(app.controllerRuntimeClient))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *cdiAPIApp) createCDIValidatingWebhook() error {
|
||||
app.container.ServeMux.Handle(cdiValidatePath, webhooks.NewCDIValidatingWebhook(app.cdiClient))
|
||||
return nil
|
||||
|
@ -9,6 +9,7 @@ go_library(
|
||||
"datavolume-validate.go",
|
||||
"handler.go",
|
||||
"populators-validate.go",
|
||||
"pvc-mutate.go",
|
||||
"scheme.go",
|
||||
"transfer-validate.go",
|
||||
"util.go",
|
||||
@ -19,6 +20,7 @@ go_library(
|
||||
"//pkg/client/clientset/versioned:go_default_library",
|
||||
"//pkg/common:go_default_library",
|
||||
"//pkg/controller/common:go_default_library",
|
||||
"//pkg/controller/datavolume:go_default_library",
|
||||
"//pkg/token:go_default_library",
|
||||
"//staging/src/kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1:go_default_library",
|
||||
"//vendor/github.com/appscode/jsonpatch:go_default_library",
|
||||
|
@ -30,6 +30,7 @@ import (
|
||||
|
||||
"github.com/appscode/jsonpatch"
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/klog/v2"
|
||||
@ -68,6 +69,11 @@ func NewDataVolumeMutatingWebhook(k8sClient kubernetes.Interface, cdiClient cdic
|
||||
return newAdmissionHandler(&dataVolumeMutatingWebhook{k8sClient: k8sClient, cdiClient: cdiClient, tokenGenerator: generator})
|
||||
}
|
||||
|
||||
// NewPvcMutatingWebhook creates a new PvcMutation webhook
|
||||
func NewPvcMutatingWebhook(cachedClient client.Client) http.Handler {
|
||||
return newAdmissionHandler(&pvcMutatingWebhook{cachedClient: cachedClient})
|
||||
}
|
||||
|
||||
// NewCDIValidatingWebhook creates a new CDI validating webhook
|
||||
func NewCDIValidatingWebhook(client cdiclient.Interface) http.Handler {
|
||||
return newAdmissionHandler(&cdiValidatingWebhook{client: client})
|
||||
@ -191,21 +197,31 @@ func allowedAdmissionResponse() *admissionv1.AdmissionResponse {
|
||||
}
|
||||
|
||||
func validateDataVolumeResource(ar admissionv1.AdmissionReview) error {
|
||||
resources := []metav1.GroupVersionResource{
|
||||
{
|
||||
dvResource := metav1.GroupVersionResource{
|
||||
Group: cdiv1.SchemeGroupVersion.Group,
|
||||
Version: cdiv1.SchemeGroupVersion.Version,
|
||||
Resource: "datavolumes",
|
||||
},
|
||||
}
|
||||
for _, resource := range resources {
|
||||
if ar.Request.Resource == resource {
|
||||
if ar.Request.Resource == dvResource {
|
||||
return nil
|
||||
}
|
||||
|
||||
klog.Errorf("resource is %s but request is: %s", dvResource, ar.Request.Resource)
|
||||
return fmt.Errorf("expect resource to be '%s'", dvResource.Resource)
|
||||
}
|
||||
|
||||
func validatePvcResource(ar admissionv1.AdmissionReview) error {
|
||||
pvcResource := metav1.GroupVersionResource{
|
||||
Group: corev1.SchemeGroupVersion.Group,
|
||||
Version: corev1.SchemeGroupVersion.Version,
|
||||
Resource: "persistentvolumeclaims",
|
||||
}
|
||||
if ar.Request.Resource == pvcResource {
|
||||
return nil
|
||||
}
|
||||
|
||||
klog.Errorf("resource is %s but request is: %s", resources[0], ar.Request.Resource)
|
||||
return fmt.Errorf("expect resource to be '%s'", resources[0].Resource)
|
||||
klog.Errorf("resource is %s but request is: %s", pvcResource, ar.Request.Resource)
|
||||
return fmt.Errorf("expect resource to be '%s'", pvcResource.Resource)
|
||||
}
|
||||
|
||||
func toPatchResponse(original, current interface{}) *admissionv1.AdmissionResponse {
|
||||
|
66
pkg/apiserver/webhooks/pvc-mutate.go
Normal file
66
pkg/apiserver/webhooks/pvc-mutate.go
Normal file
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* This file is part of the CDI project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* Copyright 2023 Red Hat, Inc.
|
||||
*
|
||||
*/
|
||||
|
||||
package webhooks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"kubevirt.io/containerized-data-importer/pkg/common"
|
||||
dvc "kubevirt.io/containerized-data-importer/pkg/controller/datavolume"
|
||||
)
|
||||
|
||||
type pvcMutatingWebhook struct {
|
||||
cachedClient client.Client
|
||||
}
|
||||
|
||||
func (wh *pvcMutatingWebhook) Admit(ar admissionv1.AdmissionReview) *admissionv1.AdmissionResponse {
|
||||
if ar.Request.Operation != admissionv1.Create {
|
||||
return allowedAdmissionResponse()
|
||||
}
|
||||
|
||||
if err := validatePvcResource(ar); err != nil {
|
||||
return toAdmissionResponseError(err)
|
||||
}
|
||||
|
||||
pvc := &v1.PersistentVolumeClaim{}
|
||||
if err := json.Unmarshal(ar.Request.Object.Raw, &pvc); err != nil {
|
||||
return toAdmissionResponseError(err)
|
||||
}
|
||||
|
||||
// Note the webhook LabelSelector should not pass us such pvcs
|
||||
if pvc.Labels[common.PvcUseStorageProfileLabel] != "true" {
|
||||
klog.Warningf("Got PVC %s/%s which was not labeled for rendering", pvc.Namespace, pvc.Name)
|
||||
return allowedAdmissionResponse()
|
||||
}
|
||||
|
||||
pvcCpy := pvc.DeepCopy()
|
||||
if err := dvc.RenderPvc(context.TODO(), wh.cachedClient, pvcCpy); err != nil {
|
||||
return toAdmissionResponseError(err)
|
||||
|
||||
}
|
||||
|
||||
return toPatchResponse(pvc, pvcCpy)
|
||||
}
|
@ -61,6 +61,9 @@ const (
|
||||
// DataImportCronCleanupLabel tells whether to delete the resource when its DataImportCron is deleted
|
||||
DataImportCronCleanupLabel = DataImportCronLabel + ".cleanup"
|
||||
|
||||
// PvcUseStorageProfileLabel tells whether the PVC should be rendered by the mutating webhook based on StorageProfiles
|
||||
PvcUseStorageProfileLabel = CDIComponentLabel + "/useStorageProfile"
|
||||
|
||||
// ImporterVolumePath provides a constant for the directory where the PV is mounted.
|
||||
ImporterVolumePath = "/data"
|
||||
// DiskImageName provides a constant for our importer/datastream_ginkgo_test and to build ImporterWritePath
|
||||
|
@ -561,13 +561,17 @@ func GetPlatformDefaultStorageClass(storageClasses *storagev1.StorageClassList,
|
||||
|
||||
// GetFilesystemOverheadForStorageClass determines the filesystem overhead defined in CDIConfig for the storageClass.
|
||||
func GetFilesystemOverheadForStorageClass(ctx context.Context, client client.Client, storageClassName *string) (cdiv1.Percent, error) {
|
||||
if storageClassName != nil && *storageClassName == "" {
|
||||
klog.V(3).Info("No storage class name passed")
|
||||
return "0", nil
|
||||
}
|
||||
|
||||
cdiConfig := &cdiv1.CDIConfig{}
|
||||
if err := client.Get(ctx, types.NamespacedName{Name: common.ConfigName}, cdiConfig); err != nil {
|
||||
if k8serrors.IsNotFound(err) {
|
||||
klog.V(1).Info("CDIConfig does not exist, pod will not start until it does")
|
||||
return "0", nil
|
||||
}
|
||||
|
||||
return "0", err
|
||||
}
|
||||
|
||||
|
@ -38,6 +38,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/utils/ptr"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
@ -202,12 +203,7 @@ func getIndexArgs() []indexArgs {
|
||||
{
|
||||
obj: &corev1.PersistentVolume{},
|
||||
field: claimStorageClassNameField,
|
||||
extractValue: func(obj client.Object) []string {
|
||||
if pv, ok := obj.(*corev1.PersistentVolume); ok && pv.Status.Phase == corev1.VolumeAvailable {
|
||||
return []string{pv.Spec.StorageClassName}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
extractValue: extractAvailablePersistentVolumeStorageClassName,
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -226,6 +222,19 @@ func CreateCommonIndexes(mgr manager.Manager) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateAvailablePersistentVolumeIndex adds storage class name index for available PersistentVolumes
|
||||
func CreateAvailablePersistentVolumeIndex(fieldIndexer client.FieldIndexer) error {
|
||||
return fieldIndexer.IndexField(context.TODO(), &corev1.PersistentVolume{},
|
||||
claimStorageClassNameField, extractAvailablePersistentVolumeStorageClassName)
|
||||
}
|
||||
|
||||
func extractAvailablePersistentVolumeStorageClassName(obj client.Object) []string {
|
||||
if pv, ok := obj.(*corev1.PersistentVolume); ok && pv.Status.Phase == corev1.VolumeAvailable {
|
||||
return []string{pv.Spec.StorageClassName}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addDataVolumeControllerCommonWatches(mgr manager.Manager, dataVolumeController controller.Controller, op dataVolumeOp) error {
|
||||
appendMatchingDataVolumeRequest := func(ctx context.Context, reqs []reconcile.Request, mgr manager.Manager, namespace, name string) []reconcile.Request {
|
||||
dvKey := types.NamespacedName{Namespace: namespace, Name: name}
|
||||
@ -670,7 +679,7 @@ func (r *ReconcilerBase) getAvailableVolumesForDV(syncState *dvSyncState, log lo
|
||||
pvc := &corev1.PersistentVolumeClaim{
|
||||
Spec: *syncState.pvcSpec,
|
||||
}
|
||||
if err := checkVolumeSatisfyClaim(&pv, pvc); err != nil {
|
||||
if err := CheckVolumeSatisfyClaim(&pv, pvc); err != nil {
|
||||
continue
|
||||
}
|
||||
log.Info("Found matching volume for DV", "pv", pv.Name)
|
||||
@ -1120,6 +1129,20 @@ func (r *ReconcilerBase) newPersistentVolumeClaim(dataVolume *cdiv1.DataVolume,
|
||||
annotations[cc.AnnPriorityClassName] = dataVolume.Spec.PriorityClassName
|
||||
}
|
||||
annotations[cc.AnnPreallocationRequested] = strconv.FormatBool(cc.GetPreallocation(context.TODO(), r.client, dataVolume.Spec.Preallocation))
|
||||
|
||||
if dataVolume.Spec.Storage != nil && labels[common.PvcUseStorageProfileLabel] == "true" {
|
||||
isWebhookPvcRenderingEnabled, err := featuregates.IsWebhookPvcRenderingEnabled(r.client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isWebhookPvcRenderingEnabled {
|
||||
labels[common.PvcUseStorageProfileLabel] = "true"
|
||||
if targetPvcSpec.VolumeMode == nil {
|
||||
targetPvcSpec.VolumeMode = ptr.To[corev1.PersistentVolumeMode](cdiv1.PersistentVolumeFromStorageProfile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pvc := &corev1.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: namespace,
|
||||
|
@ -478,10 +478,10 @@ var _ = Describe("All DataVolume Tests", func() {
|
||||
|
||||
_, err := reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: "test-dv", Namespace: metav1.NamespaceDefault}})
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("DataVolume with ContentType Archive cannot have block volumeMode"))
|
||||
Expect(err.Error()).To(ContainSubstring("ContentType Archive cannot have block volumeMode"))
|
||||
By("Checking error event recorded")
|
||||
event := <-reconciler.recorder.(*record.FakeRecorder).Events
|
||||
Expect(event).To(ContainSubstring("DataVolume with ContentType Archive cannot have block volumeMode"))
|
||||
Expect(event).To(ContainSubstring("ContentType Archive cannot have block volumeMode"))
|
||||
})
|
||||
|
||||
It("Should set on a PVC matching access mode from storageProfile to the DV given volume mode", func() {
|
||||
@ -1990,7 +1990,6 @@ func readyStatusByPhase(phase cdiv1.DataVolumePhase) corev1.ConditionStatus {
|
||||
func createImportReconcilerWFFCDisabled(objects ...client.Object) *ImportReconciler {
|
||||
return createImportReconcilerWithFeatureGates(nil, objects...)
|
||||
}
|
||||
|
||||
func createImportReconciler(objects ...client.Object) *ImportReconciler {
|
||||
return createImportReconcilerWithFeatureGates([]string{featuregates.HonorWaitForFirstConsumer}, objects...)
|
||||
}
|
||||
|
@ -520,7 +520,11 @@ func (r *PvcCloneReconciler) detectCloneSize(syncState *dvSyncState) (bool, erro
|
||||
return false, err
|
||||
}
|
||||
|
||||
if syncState.pvcSpec.Resources.Requests == nil {
|
||||
syncState.pvcSpec.Resources.Requests = corev1.ResourceList{}
|
||||
}
|
||||
syncState.pvcSpec.Resources.Requests[corev1.ResourceStorage] = targetCapacity
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
|
@ -604,6 +604,7 @@ var _ = Describe("All DataVolume Tests", func() {
|
||||
var _ = Describe("Clone with empty storage size", func() {
|
||||
scName := "testsc"
|
||||
accessMode := []corev1.PersistentVolumeAccessMode{corev1.ReadOnlyMany}
|
||||
annKubevirt := map[string]string{AnnContentType: "kubevirt"}
|
||||
sc := CreateStorageClassWithProvisioner(scName, map[string]string{
|
||||
AnnDefaultStorageClass: "true",
|
||||
}, map[string]string{}, "csi-plugin")
|
||||
@ -612,18 +613,27 @@ var _ = Describe("All DataVolume Tests", func() {
|
||||
return &dvSyncState{dv: dv, dvMutated: dv.DeepCopy(), pvc: pvc, pvcSpec: pvcSpec}
|
||||
}
|
||||
|
||||
createTargetPvc := func(pvcSpec *corev1.PersistentVolumeClaimSpec) *corev1.PersistentVolumeClaim {
|
||||
return &corev1.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{AnnCloneType: string(cdiv1.CloneStrategyHostAssisted)},
|
||||
},
|
||||
Spec: *pvcSpec,
|
||||
}
|
||||
}
|
||||
// detectCloneSize tests
|
||||
It("Size-detection fails when source PVC is not attainable", func() {
|
||||
dv := newCloneDataVolumeWithEmptyStorage("test-dv", "default")
|
||||
cloneStrategy := cdiv1.CloneStrategyHostAssisted
|
||||
targetPvc := &corev1.PersistentVolumeClaim{}
|
||||
storageProfile := createStorageProfileWithCloneStrategy(scName, []cdiv1.ClaimPropertySet{
|
||||
{AccessModes: accessMode, VolumeMode: &BlockMode}}, &cloneStrategy)
|
||||
|
||||
reconciler := createCloneReconciler(dv, storageProfile, sc)
|
||||
pvcSpec, err := renderPvcSpec(reconciler.client, reconciler.recorder, reconciler.log, dv, nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
done, err := reconciler.detectCloneSize(syncState(dv, targetPvc, pvcSpec))
|
||||
|
||||
targetPvc := createTargetPvc(pvcSpec)
|
||||
done, err := reconciler.detectCloneSize(syncState(dv, targetPvc, &targetPvc.Spec))
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(done).To(BeFalse())
|
||||
Expect(k8serrors.IsNotFound(err)).To(BeTrue())
|
||||
@ -644,7 +654,7 @@ var _ = Describe("All DataVolume Tests", func() {
|
||||
Phase: cdiv1.ImportInProgress,
|
||||
},
|
||||
}
|
||||
pvc := CreatePvcInStorageClass("test", metav1.NamespaceDefault, &scName, nil, nil, corev1.ClaimBound)
|
||||
pvc := CreatePvcInStorageClass("test", metav1.NamespaceDefault, &scName, annKubevirt, nil, corev1.ClaimBound)
|
||||
pvc.OwnerReferences = []metav1.OwnerReference{
|
||||
{
|
||||
Kind: "DataVolume",
|
||||
@ -652,12 +662,13 @@ var _ = Describe("All DataVolume Tests", func() {
|
||||
Controller: ptr.To[bool](true),
|
||||
},
|
||||
}
|
||||
AddAnnotation(pvc, AnnContentType, "kubevirt")
|
||||
reconciler := createCloneReconciler(dv, sourceDV, pvc, storageProfile, sc)
|
||||
|
||||
pvcSpec, err := renderPvcSpec(reconciler.client, reconciler.recorder, reconciler.log, dv, pvc)
|
||||
pvcSpec, err := renderPvcSpec(reconciler.client, reconciler.recorder, reconciler.log, dv, nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
done, err := reconciler.detectCloneSize(syncState(dv, pvc, pvcSpec))
|
||||
|
||||
targetPvc := createTargetPvc(pvcSpec)
|
||||
done, err := reconciler.detectCloneSize(syncState(dv, targetPvc, &targetPvc.Spec))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(done).To(BeFalse())
|
||||
By("Checking events recorded")
|
||||
@ -678,14 +689,14 @@ var _ = Describe("All DataVolume Tests", func() {
|
||||
storageProfile := createStorageProfileWithCloneStrategy(scName, []cdiv1.ClaimPropertySet{
|
||||
{AccessModes: accessMode, VolumeMode: &BlockMode}}, &cloneStrategy)
|
||||
|
||||
pvc := CreatePvcInStorageClass("test", metav1.NamespaceDefault, &scName, nil, nil, corev1.ClaimBound)
|
||||
pvc.SetAnnotations(make(map[string]string))
|
||||
pvc.Annotations[AnnContentType] = "kubevirt"
|
||||
pvc := CreatePvcInStorageClass("test", metav1.NamespaceDefault, &scName, annKubevirt, nil, corev1.ClaimBound)
|
||||
reconciler := createCloneReconciler(dv, pvc, storageProfile, sc)
|
||||
|
||||
pvcSpec, err := renderPvcSpec(reconciler.client, reconciler.recorder, reconciler.log, dv, pvc)
|
||||
pvcSpec, err := renderPvcSpec(reconciler.client, reconciler.recorder, reconciler.log, dv, nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
done, err := reconciler.detectCloneSize(syncState(dv, pvc, pvcSpec))
|
||||
|
||||
targetPvc := createTargetPvc(pvcSpec)
|
||||
done, err := reconciler.detectCloneSize(syncState(dv, targetPvc, &targetPvc.Spec))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(done).To(BeFalse())
|
||||
By("Checking events recorded")
|
||||
@ -707,9 +718,7 @@ var _ = Describe("All DataVolume Tests", func() {
|
||||
storageProfile := createStorageProfileWithCloneStrategy(scName, []cdiv1.ClaimPropertySet{
|
||||
{AccessModes: accessMode, VolumeMode: &BlockMode}}, &cloneStrategy)
|
||||
|
||||
pvc := CreatePvcInStorageClass("test", metav1.NamespaceDefault, &scName, nil, nil, corev1.ClaimBound)
|
||||
pvc.SetAnnotations(make(map[string]string))
|
||||
pvc.Annotations[AnnContentType] = "kubevirt"
|
||||
pvc := CreatePvcInStorageClass("test", metav1.NamespaceDefault, &scName, annKubevirt, nil, corev1.ClaimBound)
|
||||
reconciler := createCloneReconciler(dv, pvc, storageProfile, sc)
|
||||
|
||||
// Prepare the size-detection Pod with the required information
|
||||
@ -719,9 +728,11 @@ var _ = Describe("All DataVolume Tests", func() {
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Checks
|
||||
pvcSpec, err := renderPvcSpec(reconciler.client, reconciler.recorder, reconciler.log, dv, pvc)
|
||||
pvcSpec, err := renderPvcSpec(reconciler.client, reconciler.recorder, reconciler.log, dv, nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
done, err := reconciler.detectCloneSize(syncState(dv, pvc, pvcSpec))
|
||||
|
||||
targetPvc := createTargetPvc(pvcSpec)
|
||||
done, err := reconciler.detectCloneSize(syncState(dv, targetPvc, &targetPvc.Spec))
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(Equal(ErrInvalidTermMsg))
|
||||
Expect(done).To(BeFalse())
|
||||
@ -737,10 +748,7 @@ var _ = Describe("All DataVolume Tests", func() {
|
||||
cloneStrategy := cdiv1.CloneStrategyHostAssisted
|
||||
storageProfile := createStorageProfileWithCloneStrategy(scName, []cdiv1.ClaimPropertySet{
|
||||
{AccessModes: accessMode, VolumeMode: &BlockMode}}, &cloneStrategy)
|
||||
|
||||
pvc := CreatePvcInStorageClass("test", metav1.NamespaceDefault, &scName, nil, nil, corev1.ClaimBound)
|
||||
pvc.SetAnnotations(make(map[string]string))
|
||||
pvc.Annotations[AnnContentType] = "kubevirt"
|
||||
pvc := CreatePvcInStorageClass("test", metav1.NamespaceDefault, &scName, annKubevirt, nil, corev1.ClaimBound)
|
||||
reconciler := createCloneReconciler(dv, pvc, storageProfile, sc)
|
||||
|
||||
// Prepare the size-detection Pod with the required information
|
||||
@ -760,19 +768,22 @@ var _ = Describe("All DataVolume Tests", func() {
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Get the expected value
|
||||
pvcSpec, err := renderPvcSpec(reconciler.client, reconciler.recorder, reconciler.log, dv, pvc)
|
||||
pvcSpec, err := renderPvcSpec(reconciler.client, reconciler.recorder, reconciler.log, dv, nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
expectedSize, err := InflateSizeWithOverhead(context.TODO(), reconciler.client, int64(100), pvcSpec)
|
||||
|
||||
targetPvc := createTargetPvc(pvcSpec)
|
||||
expectedSize, err := InflateSizeWithOverhead(context.TODO(), reconciler.client, int64(100), &targetPvc.Spec)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
expectedSizeInt64, _ := expectedSize.AsInt64()
|
||||
|
||||
// Checks
|
||||
syncState := syncState(dv, pvc, pvcSpec)
|
||||
syncState := syncState(dv, targetPvc, &targetPvc.Spec)
|
||||
done, err := reconciler.detectCloneSize(syncState)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(done).To(BeTrue())
|
||||
Expect(syncState.dvMutated.Annotations[AnnPermissiveClone]).To(Equal("true"))
|
||||
targetSize := pvcSpec.Resources.Requests[corev1.ResourceStorage]
|
||||
targetSize := targetPvc.Spec.Resources.Requests.Storage()
|
||||
targetSizeInt64, _ := targetSize.AsInt64()
|
||||
Expect(targetSizeInt64).To(Equal(expectedSizeInt64))
|
||||
})
|
||||
@ -784,27 +795,27 @@ var _ = Describe("All DataVolume Tests", func() {
|
||||
{AccessModes: accessMode, VolumeMode: &BlockMode}}, &cloneStrategy)
|
||||
|
||||
// Prepare the source PVC with the required annotations
|
||||
pvc := CreatePvcInStorageClass("test", metav1.NamespaceDefault, &scName, nil, nil, corev1.ClaimBound)
|
||||
pvc.SetAnnotations(make(map[string]string))
|
||||
pvc.GetAnnotations()[AnnVirtualImageSize] = "100" // Mock value
|
||||
pvc.GetAnnotations()[AnnSourceCapacity] = string(pvc.Status.Capacity.Storage().String())
|
||||
pvc.GetAnnotations()[AnnContentType] = "kubevirt"
|
||||
pvc := CreatePvcInStorageClass("test", metav1.NamespaceDefault, &scName, annKubevirt, nil, corev1.ClaimBound)
|
||||
pvc.Annotations[AnnVirtualImageSize] = "100" // Mock value
|
||||
pvc.Annotations[AnnSourceCapacity] = string(pvc.Status.Capacity.Storage().String())
|
||||
reconciler := createCloneReconciler(dv, pvc, storageProfile, sc)
|
||||
|
||||
// Get the expected value
|
||||
pvcSpec, err := renderPvcSpec(reconciler.client, reconciler.recorder, reconciler.log, dv, pvc)
|
||||
pvcSpec, err := renderPvcSpec(reconciler.client, reconciler.recorder, reconciler.log, dv, nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
expectedSize, err := InflateSizeWithOverhead(context.TODO(), reconciler.client, int64(100), pvcSpec)
|
||||
|
||||
targetPvc := createTargetPvc(pvcSpec)
|
||||
expectedSize, err := InflateSizeWithOverhead(context.TODO(), reconciler.client, int64(100), &targetPvc.Spec)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
expectedSizeInt64, _ := expectedSize.AsInt64()
|
||||
|
||||
// Checks
|
||||
syncState := syncState(dv, pvc, pvcSpec)
|
||||
syncState := syncState(dv, targetPvc, &targetPvc.Spec)
|
||||
done, err := reconciler.detectCloneSize(syncState)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(done).To(BeTrue())
|
||||
Expect(syncState.dvMutated.Annotations[AnnPermissiveClone]).To(Equal("true"))
|
||||
targetSize := pvcSpec.Resources.Requests[corev1.ResourceStorage]
|
||||
targetSize := targetPvc.Spec.Resources.Requests.Storage()
|
||||
targetSizeInt64, _ := targetSize.AsInt64()
|
||||
Expect(targetSizeInt64).To(Equal(expectedSizeInt64))
|
||||
})
|
||||
@ -819,13 +830,21 @@ var _ = Describe("All DataVolume Tests", func() {
|
||||
pvc.Spec.VolumeMode = &volumeMode
|
||||
reconciler := createCloneReconciler(dv, pvc, storageProfile, sc)
|
||||
|
||||
pvcSpec, err := renderPvcSpec(reconciler.client, reconciler.recorder, reconciler.log, dv, pvc)
|
||||
pvcSpec, err := renderPvcSpec(reconciler.client, reconciler.recorder, reconciler.log, dv, nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
targetPvc := createTargetPvc(pvcSpec)
|
||||
expectedSize := *pvc.Status.Capacity.Storage()
|
||||
done, err := reconciler.detectCloneSize(syncState(dv, pvc, pvcSpec))
|
||||
expectedSizeInt64, _ := expectedSize.AsInt64()
|
||||
|
||||
syncState := syncState(dv, targetPvc, &targetPvc.Spec)
|
||||
done, err := reconciler.detectCloneSize(syncState)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(done).To(BeTrue())
|
||||
Expect(pvc.Spec.Resources.Requests.Storage().Cmp(expectedSize)).To(Equal(0))
|
||||
|
||||
targetSize := targetPvc.Spec.Resources.Requests.Storage()
|
||||
targetSizeInt64, _ := targetSize.AsInt64()
|
||||
Expect(targetSizeInt64).To(Equal(expectedSizeInt64))
|
||||
},
|
||||
Entry("hostAssited with empty size and 'Block' volume mode", cdiv1.CloneStrategyHostAssisted, BlockMode),
|
||||
)
|
||||
|
@ -302,6 +302,9 @@ func (r *SnapshotCloneReconciler) detectCloneSize(log logr.Logger, syncState *dv
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if pvcSpec.Resources.Requests == nil {
|
||||
pvcSpec.Resources.Requests = corev1.ResourceList{}
|
||||
}
|
||||
pvcSpec.Resources.Requests[corev1.ResourceStorage] = *snapshot.Status.RestoreSize
|
||||
|
||||
log.V(3).Info("set pvc request size", "size", pvcSpec.Resources.Requests[corev1.ResourceStorage])
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
@ -29,30 +30,56 @@ import (
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/tools/record"
|
||||
storagehelpers "k8s.io/component-helpers/storage/volume"
|
||||
"k8s.io/utils/ptr"
|
||||
"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/common"
|
||||
cc "kubevirt.io/containerized-data-importer/pkg/controller/common"
|
||||
"kubevirt.io/containerized-data-importer/pkg/controller/populators"
|
||||
featuregates "kubevirt.io/containerized-data-importer/pkg/feature-gates"
|
||||
"kubevirt.io/containerized-data-importer/pkg/util"
|
||||
)
|
||||
|
||||
const (
|
||||
// AnnOwnedByDataVolume annotation has the owner DataVolume name
|
||||
AnnOwnedByDataVolume = "cdi.kubevirt.io/ownedByDataVolume"
|
||||
|
||||
// MessageErrStorageClassNotFound provides a const to indicate the DV storage spec is missing accessMode and no storageClass to choose profile
|
||||
MessageErrStorageClassNotFound = "DataVolume.storage spec is missing accessMode and no storageClass to choose profile"
|
||||
// MessageErrStorageClassNotFound provides a const to indicate the PVC spec is missing accessMode and no storageClass to choose profile
|
||||
MessageErrStorageClassNotFound = "PVC spec is missing accessMode and no storageClass to choose profile"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrStorageClassNotFound indicates the DV storage spec is missing accessMode and no storageClass to choose profile
|
||||
// ErrStorageClassNotFound indicates the PVC spec is missing accessMode and no storageClass to choose profile
|
||||
ErrStorageClassNotFound = errors.New(MessageErrStorageClassNotFound)
|
||||
)
|
||||
|
||||
// RenderPvc renders the PVC according to StorageProfiles
|
||||
func RenderPvc(ctx context.Context, client client.Client, pvc *v1.PersistentVolumeClaim) error {
|
||||
if pvc.Spec.VolumeMode != nil &&
|
||||
*pvc.Spec.VolumeMode == cdiv1.PersistentVolumeFromStorageProfile {
|
||||
pvc.Spec.VolumeMode = nil
|
||||
}
|
||||
|
||||
dvContentType := cc.GetPVCContentType(pvc)
|
||||
if err := renderPvcSpecVolumeModeAndAccessModesAndStorageClass(client, nil, nil, nil, &pvc.Spec, dvContentType); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if hasCloneSourceRef(pvc) {
|
||||
return renderClonePvcVolumeSizeFromSource(ctx, client, pvc)
|
||||
}
|
||||
|
||||
isClone := pvc.Annotations[cc.AnnCloneType] != ""
|
||||
return renderPvcSpecVolumeSize(client, &pvc.Spec, isClone)
|
||||
}
|
||||
|
||||
// renderPvcSpec creates a new PVC Spec based on either the dv.spec.pvc or dv.spec.storage section
|
||||
func renderPvcSpec(client client.Client, recorder record.EventRecorder, log logr.Logger, dv *cdiv1.DataVolume, pvc *v1.PersistentVolumeClaim) (*v1.PersistentVolumeClaimSpec, error) {
|
||||
if dv.Spec.PVC != nil {
|
||||
@ -67,34 +94,61 @@ func renderPvcSpec(client client.Client, recorder record.EventRecorder, log logr
|
||||
func pvcFromStorage(client client.Client, recorder record.EventRecorder, log logr.Logger, dv *cdiv1.DataVolume, pvc *v1.PersistentVolumeClaim) (*v1.PersistentVolumeClaimSpec, error) {
|
||||
var pvcSpec *v1.PersistentVolumeClaimSpec
|
||||
|
||||
isWebhookRenderingEnabled, err := featuregates.IsWebhookPvcRenderingEnabled(client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
shouldRender := !isWebhookRenderingEnabled || dv.Labels[common.PvcUseStorageProfileLabel] != "true"
|
||||
|
||||
if pvc == nil {
|
||||
pvcSpec = copyStorageAsPvc(log, dv.Spec.Storage)
|
||||
if err := renderPvcSpecVolumeModeAndAccessModes(client, recorder, log, dv, pvcSpec); err != nil {
|
||||
if shouldRender {
|
||||
if err := renderPvcSpecVolumeModeAndAccessModesAndStorageClass(client, recorder, &log, dv, pvcSpec, dv.Spec.ContentType); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pvcSpec = pvc.Spec.DeepCopy()
|
||||
}
|
||||
|
||||
if err := renderPvcSpecVolumeSize(client, dv.Spec, pvcSpec); err != nil {
|
||||
if shouldRender {
|
||||
isClone := dv.Spec.Source.PVC != nil || dv.Spec.Source.Snapshot != nil
|
||||
if err := renderPvcSpecVolumeSize(client, pvcSpec, isClone); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return pvcSpec, nil
|
||||
}
|
||||
|
||||
func renderPvcSpecVolumeModeAndAccessModes(client client.Client, recorder record.EventRecorder, log logr.Logger, dv *cdiv1.DataVolume, pvcSpec *v1.PersistentVolumeClaimSpec) error {
|
||||
if dv.Spec.ContentType == cdiv1.DataVolumeArchive {
|
||||
if pvcSpec.VolumeMode != nil && *pvcSpec.VolumeMode == v1.PersistentVolumeBlock {
|
||||
log.V(1).Info("DataVolume with ContentType Archive cannot have block volumeMode", "namespace", dv.Namespace, "name", dv.Name)
|
||||
recorder.Eventf(dv, v1.EventTypeWarning, cc.ErrClaimNotValid, "DataVolume with ContentType Archive cannot have block volumeMode")
|
||||
return errors.Errorf("DataVolume with ContentType Archive cannot have block volumeMode")
|
||||
// func is called from both DV controller (with recorder and log) and PVC mutating webhook (without recorder and log)
|
||||
// therefore we use wrappers for log and recorder calls
|
||||
func renderPvcSpecVolumeModeAndAccessModesAndStorageClass(client client.Client, recorder record.EventRecorder, log *logr.Logger,
|
||||
dv *cdiv1.DataVolume, pvcSpec *v1.PersistentVolumeClaimSpec, dvContentType cdiv1.DataVolumeContentType) error {
|
||||
|
||||
logInfo := func(msg string, keysAndValues ...interface{}) {
|
||||
if log != nil {
|
||||
log.V(1).Info(msg, keysAndValues...)
|
||||
}
|
||||
volumeMode := v1.PersistentVolumeFilesystem
|
||||
pvcSpec.VolumeMode = &volumeMode
|
||||
}
|
||||
|
||||
storageClass, err := cc.GetStorageClassByNameWithVirtFallback(context.TODO(), client, dv.Spec.Storage.StorageClassName, dv.Spec.ContentType)
|
||||
recordEventf := func(object runtime.Object, eventtype, reason, messageFmt string, args ...interface{}) {
|
||||
if recorder != nil {
|
||||
recorder.Eventf(object, eventtype, reason, messageFmt, args...)
|
||||
}
|
||||
}
|
||||
|
||||
if dvContentType == cdiv1.DataVolumeArchive {
|
||||
if pvcSpec.VolumeMode != nil && *pvcSpec.VolumeMode == v1.PersistentVolumeBlock {
|
||||
logInfo("ContentType Archive cannot have block volumeMode", "namespace", dv.Namespace, "name", dv.Name)
|
||||
recordEventf(dv, v1.EventTypeWarning, cc.ErrClaimNotValid, "ContentType Archive cannot have block volumeMode")
|
||||
return errors.Errorf("ContentType Archive cannot have block volumeMode")
|
||||
}
|
||||
pvcSpec.VolumeMode = ptr.To[v1.PersistentVolumeMode](v1.PersistentVolumeFilesystem)
|
||||
}
|
||||
|
||||
storageClass, err := cc.GetStorageClassByNameWithVirtFallback(context.TODO(), client, pvcSpec.StorageClassName, dvContentType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -104,8 +158,8 @@ func renderPvcSpecVolumeModeAndAccessModes(client client.Client, recorder record
|
||||
}
|
||||
// Not even default storageClass on the cluster, cannot apply the defaults, verify spec is ok
|
||||
if len(pvcSpec.AccessModes) == 0 {
|
||||
log.V(1).Info("Cannot set accessMode for new pvc", "namespace", dv.Namespace, "name", dv.Name)
|
||||
recorder.Eventf(dv, v1.EventTypeWarning, cc.ErrClaimNotValid, MessageErrStorageClassNotFound)
|
||||
logInfo("Cannot set accessMode for new pvc", "namespace", dv.Namespace, "name", dv.Name)
|
||||
recordEventf(dv, v1.EventTypeWarning, cc.ErrClaimNotValid, MessageErrStorageClassNotFound)
|
||||
return ErrStorageClassNotFound
|
||||
}
|
||||
return nil
|
||||
@ -113,12 +167,12 @@ func renderPvcSpecVolumeModeAndAccessModes(client client.Client, recorder record
|
||||
|
||||
pvcSpec.StorageClassName = &storageClass.Name
|
||||
// given storageClass we can apply defaults if needed
|
||||
if (pvcSpec.VolumeMode == nil || *pvcSpec.VolumeMode == "") && (len(pvcSpec.AccessModes) == 0) {
|
||||
if (pvcSpec.VolumeMode == nil || *pvcSpec.VolumeMode == "") && len(pvcSpec.AccessModes) == 0 {
|
||||
accessModes, volumeMode, err := getDefaultVolumeAndAccessMode(client, storageClass)
|
||||
if err != nil {
|
||||
log.V(1).Info("Cannot set accessMode and volumeMode for new pvc", "namespace", dv.Namespace, "name", dv.Name, "Error", err)
|
||||
recorder.Eventf(dv, v1.EventTypeWarning, cc.ErrClaimNotValid,
|
||||
fmt.Sprintf("DataVolume.storage spec is missing accessMode and volumeMode, cannot get access mode from StorageProfile %s", getName(storageClass)))
|
||||
logInfo("Cannot set accessMode and volumeMode for new pvc", "namespace", dv.Namespace, "name", dv.Name, "Error", err)
|
||||
recordEventf(dv, v1.EventTypeWarning, cc.ErrClaimNotValid,
|
||||
fmt.Sprintf("Spec is missing accessMode and volumeMode, cannot get access mode from StorageProfile %s", getName(storageClass)))
|
||||
return err
|
||||
}
|
||||
pvcSpec.AccessModes = append(pvcSpec.AccessModes, accessModes...)
|
||||
@ -126,9 +180,9 @@ func renderPvcSpecVolumeModeAndAccessModes(client client.Client, recorder record
|
||||
} else if len(pvcSpec.AccessModes) == 0 {
|
||||
accessModes, err := getDefaultAccessModes(client, storageClass, pvcSpec.VolumeMode)
|
||||
if err != nil {
|
||||
log.V(1).Info("Cannot set accessMode for new pvc", "namespace", dv.Namespace, "name", dv.Name, "Error", err)
|
||||
recorder.Eventf(dv, v1.EventTypeWarning, cc.ErrClaimNotValid,
|
||||
fmt.Sprintf("DataVolume.storage spec is missing accessMode and cannot get access mode from StorageProfile %s", getName(storageClass)))
|
||||
logInfo("Cannot set accessMode for new pvc", "namespace", dv.Namespace, "name", dv.Name, "Error", err)
|
||||
recordEventf(dv, v1.EventTypeWarning, cc.ErrClaimNotValid,
|
||||
fmt.Sprintf("Spec is missing accessMode and cannot get access mode from StorageProfile %s", getName(storageClass)))
|
||||
return err
|
||||
}
|
||||
pvcSpec.AccessModes = append(pvcSpec.AccessModes, accessModes...)
|
||||
@ -143,16 +197,110 @@ func renderPvcSpecVolumeModeAndAccessModes(client client.Client, recorder record
|
||||
return nil
|
||||
}
|
||||
|
||||
func renderPvcSpecVolumeSize(client client.Client, dvSpec cdiv1.DataVolumeSpec, pvcSpec *v1.PersistentVolumeClaimSpec) error {
|
||||
requestedVolumeSize, err := resolveVolumeSize(client, dvSpec, pvcSpec)
|
||||
func renderClonePvcVolumeSizeFromSource(ctx context.Context, client client.Client, pvc *v1.PersistentVolumeClaim) error {
|
||||
if size, exists := pvc.Spec.Resources.Requests[v1.ResourceStorage]; exists && !size.IsZero() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !hasCloneSourceRef(pvc) {
|
||||
return nil
|
||||
}
|
||||
|
||||
sourceNamespace, exists := pvc.Annotations[populators.AnnDataSourceNamespace]
|
||||
if !exists {
|
||||
sourceNamespace = pvc.Namespace
|
||||
}
|
||||
|
||||
volumeCloneSource := &cdiv1.VolumeCloneSource{}
|
||||
if exists, err := cc.GetResource(ctx, client, sourceNamespace, pvc.Spec.DataSourceRef.Name, volumeCloneSource); err != nil || !exists {
|
||||
return err
|
||||
}
|
||||
|
||||
source := volumeCloneSource.Spec.Source
|
||||
|
||||
if source.Kind == "VolumeSnapshot" && source.Name != "" {
|
||||
sourceSnapshot := &snapshotv1.VolumeSnapshot{}
|
||||
if exists, err := cc.GetResource(ctx, client, sourceNamespace, source.Name, sourceSnapshot); err != nil || !exists {
|
||||
return err
|
||||
}
|
||||
if sourceSnapshot.Status != nil && sourceSnapshot.Status.RestoreSize != nil {
|
||||
setRequestedVolumeSize(&pvc.Spec, *sourceSnapshot.Status.RestoreSize)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if source.Kind != "PersistentVolumeClaim" || source.Name == "" {
|
||||
return nil
|
||||
}
|
||||
sourcePvc := &v1.PersistentVolumeClaim{}
|
||||
if exists, err := cc.GetResource(ctx, client, sourceNamespace, source.Name, sourcePvc); err != nil || !exists {
|
||||
return err
|
||||
}
|
||||
|
||||
// We cannot fill in the PVC size when these conditions are met, where the size detection pod is used when PVC size is rendered by the controllor
|
||||
sourceVolumeMode := util.ResolveVolumeMode(sourcePvc.Spec.VolumeMode)
|
||||
targetVolumeMode := util.ResolveVolumeMode(pvc.Spec.VolumeMode)
|
||||
isKubevirtContent := cc.GetPVCContentType(sourcePvc) == cdiv1.DataVolumeKubeVirt
|
||||
isHostAssistedClone := pvc.Annotations[cc.AnnCloneType] == string(cdiv1.CloneStrategyHostAssisted)
|
||||
if sourceVolumeMode == v1.PersistentVolumeFilesystem &&
|
||||
targetVolumeMode == v1.PersistentVolumeBlock &&
|
||||
isKubevirtContent && isHostAssistedClone {
|
||||
return nil
|
||||
}
|
||||
|
||||
sourceSC, err := cc.GetStorageClassByNameWithK8sFallback(ctx, client, sourcePvc.Spec.StorageClassName)
|
||||
if err != nil || sourceSC == nil {
|
||||
return err
|
||||
}
|
||||
targetSC, err := cc.GetStorageClassByNameWithK8sFallback(ctx, client, pvc.Spec.StorageClassName)
|
||||
if err != nil || targetSC == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If target has the source volume mode and storage class, it can request the source requested volume size.
|
||||
// Otherwise try using the source capacity.
|
||||
volSize := sourcePvc.Spec.Resources.Requests[v1.ResourceStorage]
|
||||
if targetVolumeMode != sourceVolumeMode || targetSC.Name != sourceSC.Name {
|
||||
if capacity, exists := sourcePvc.Status.Capacity[v1.ResourceStorage]; exists {
|
||||
volSize = capacity
|
||||
}
|
||||
}
|
||||
setRequestedVolumeSize(&pvc.Spec, volSize)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasCloneSourceRef(pvc *v1.PersistentVolumeClaim) bool {
|
||||
dsRef := pvc.Spec.DataSourceRef
|
||||
return dsRef != nil && dsRef.APIGroup != nil && *dsRef.APIGroup == cc.AnnAPIGroup && dsRef.Kind == cdiv1.VolumeCloneSourceRef && dsRef.Name != ""
|
||||
}
|
||||
|
||||
func renderPvcSpecVolumeSize(client client.Client, pvcSpec *v1.PersistentVolumeClaimSpec, isClone bool) error {
|
||||
requestedSize, found := pvcSpec.Resources.Requests[v1.ResourceStorage]
|
||||
|
||||
// Storage size can be empty when cloning
|
||||
if !found {
|
||||
if !isClone {
|
||||
return errors.Errorf("PVC Spec is not valid - missing storage size")
|
||||
}
|
||||
setRequestedVolumeSize(pvcSpec, resource.Quantity{})
|
||||
return nil
|
||||
}
|
||||
|
||||
requestedSize, err := cc.InflateSizeWithOverhead(context.TODO(), client, requestedSize.Value(), pvcSpec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setRequestedVolumeSize(pvcSpec, requestedSize)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setRequestedVolumeSize(pvcSpec *v1.PersistentVolumeClaimSpec, volumeSize resource.Quantity) {
|
||||
if pvcSpec.Resources.Requests == nil {
|
||||
pvcSpec.Resources.Requests = v1.ResourceList{}
|
||||
}
|
||||
pvcSpec.Resources.Requests[v1.ResourceStorage] = *requestedVolumeSize
|
||||
return nil
|
||||
pvcSpec.Resources.Requests[v1.ResourceStorage] = volumeSize
|
||||
}
|
||||
|
||||
func getName(storageClass *storagev1.StorageClass) string {
|
||||
@ -189,10 +337,10 @@ func renderPvcSpecFromAvailablePv(c client.Client, pvcSpec *v1.PersistentVolumeC
|
||||
if err := c.List(context.TODO(), pvList, fields); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, pv := range pvList.Items {
|
||||
if pv.Status.Phase == v1.VolumeAvailable {
|
||||
pvc := &v1.PersistentVolumeClaim{Spec: *pvcSpec}
|
||||
if err := checkVolumeSatisfyClaim(&pv, pvc); err == nil {
|
||||
if err := CheckVolumeSatisfyClaim(&pv, pvc); err == nil {
|
||||
pvcSpec.VolumeMode = pv.Spec.VolumeMode
|
||||
if len(pvcSpec.AccessModes) == 0 {
|
||||
pvcSpec.AccessModes = pv.Spec.AccessModes
|
||||
@ -200,7 +348,6 @@ func renderPvcSpecFromAvailablePv(c client.Client, pvcSpec *v1.PersistentVolumeC
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -294,25 +441,6 @@ func getDefaultAccessModes(c client.Client, storageClass *storagev1.StorageClass
|
||||
return nil, errors.Errorf("no accessMode defined on StorageProfile for %s StorageClass", storageClass.Name)
|
||||
}
|
||||
|
||||
func resolveVolumeSize(c client.Client, dvSpec cdiv1.DataVolumeSpec, pvcSpec *v1.PersistentVolumeClaimSpec) (*resource.Quantity, error) {
|
||||
// resources.requests[storage] - just copy it to pvc,
|
||||
requestedSize, found := dvSpec.Storage.Resources.Requests[v1.ResourceStorage]
|
||||
|
||||
if !found {
|
||||
// Storage size can be empty when cloning
|
||||
isClone := dvSpec.Source.PVC != nil || dvSpec.Source.Snapshot != nil
|
||||
if isClone {
|
||||
return &requestedSize, nil
|
||||
}
|
||||
return nil, errors.Errorf("Datavolume Spec is not valid - missing storage size")
|
||||
}
|
||||
|
||||
// disk or image size, inflate it with overhead
|
||||
requestedSize, err := cc.InflateSizeWithOverhead(context.TODO(), c, requestedSize.Value(), pvcSpec)
|
||||
|
||||
return &requestedSize, err
|
||||
}
|
||||
|
||||
// storageClassCSIDriverExists returns true if the passed storage class has CSI drivers available
|
||||
func storageClassCSIDriverExists(client client.Client, log logr.Logger, storageClassName *string) (bool, error) {
|
||||
log = log.WithName("storageClassCSIDriverExists").V(3)
|
||||
@ -484,9 +612,9 @@ func setAnnOwnedByDataVolume(dest, obj metav1.Object) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckVolumeSatisfyClaim checks if the volume requested by the claim satisfies the requirements of the claim
|
||||
// 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 {
|
||||
func CheckVolumeSatisfyClaim(volume *v1.PersistentVolume, claim *v1.PersistentVolumeClaim) error {
|
||||
requestedQty := claim.Spec.Resources.Requests[v1.ResourceName(v1.ResourceStorage)]
|
||||
requestedSize := requestedQty.Value()
|
||||
|
||||
|
@ -23,78 +23,74 @@ import (
|
||||
ocpconfigv1 "github.com/openshift/api/config/v1"
|
||||
)
|
||||
|
||||
var _ = Describe("resolveVolumeSize", func() {
|
||||
var _ = Describe("renderPvcSpecVolumeSize", func() {
|
||||
client := createClient()
|
||||
volumeSize := resource.MustParse("1G")
|
||||
scName := "test"
|
||||
pvcSpec := &corev1.PersistentVolumeClaimSpec{
|
||||
AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadOnlyMany, corev1.ReadWriteOnce},
|
||||
Resources: corev1.ResourceRequirements{
|
||||
Requests: corev1.ResourceList{
|
||||
corev1.ResourceName(corev1.ResourceStorage): resource.MustParse("1G"),
|
||||
},
|
||||
},
|
||||
StorageClassName: &scName,
|
||||
}
|
||||
|
||||
It("Should return empty volume size", func() {
|
||||
pvcSource := &cdiv1.DataVolumeSource{
|
||||
PVC: &cdiv1.DataVolumeSourcePVC{},
|
||||
}
|
||||
storageSpec := &cdiv1.StorageSpec{}
|
||||
dv := createDataVolumeWithStorageAPI("testDV", "testNamespace", pvcSource, storageSpec)
|
||||
requestedVolumeSize, err := resolveVolumeSize(client, dv.Spec, pvcSpec)
|
||||
It("Should return empty volume size on clone PVC with empty storage size", func() {
|
||||
pvcSpec := &corev1.PersistentVolumeClaimSpec{}
|
||||
err := renderPvcSpecVolumeSize(client, pvcSpec, true)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
requestedVolumeSize, found := pvcSpec.Resources.Requests[corev1.ResourceStorage]
|
||||
Expect(found).To(BeTrue())
|
||||
Expect(requestedVolumeSize.IsZero()).To(BeTrue())
|
||||
})
|
||||
|
||||
It("Should return error after trying to create a DataVolume with empty storage size and http source", func() {
|
||||
httpSource := &cdiv1.DataVolumeSource{
|
||||
HTTP: &cdiv1.DataVolumeSourceHTTP{},
|
||||
}
|
||||
storageSpec := &cdiv1.StorageSpec{}
|
||||
dv := createDataVolumeWithStorageAPI("testDV", "testNamespace", httpSource, storageSpec)
|
||||
requestedVolumeSize, err := resolveVolumeSize(client, dv.Spec, pvcSpec)
|
||||
It("Should return error on non-clone PVC with empty storage size", func() {
|
||||
pvcSpec := &corev1.PersistentVolumeClaimSpec{}
|
||||
err := renderPvcSpecVolumeSize(client, pvcSpec, false)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("Datavolume Spec is not valid - missing storage size"))
|
||||
Expect(requestedVolumeSize).To(BeNil())
|
||||
Expect(err.Error()).To(ContainSubstring("PVC Spec is not valid - missing storage size"))
|
||||
_, found := pvcSpec.Resources.Requests[corev1.ResourceStorage]
|
||||
Expect(found).To(BeFalse())
|
||||
})
|
||||
|
||||
It("Should return the expected volume size (block volume mode)", func() {
|
||||
storageSpec := &cdiv1.StorageSpec{
|
||||
Resources: corev1.ResourceRequirements{
|
||||
Requests: corev1.ResourceList{
|
||||
corev1.ResourceStorage: resource.MustParse("1G"),
|
||||
},
|
||||
},
|
||||
}
|
||||
It("Should return the same volume size (block volume mode)", func() {
|
||||
volumeMode := corev1.PersistentVolumeBlock
|
||||
pvcSpec.VolumeMode = &volumeMode
|
||||
dv := createDataVolumeWithStorageAPI("testDV", "testNamespace", nil, storageSpec)
|
||||
requestedVolumeSize, err := resolveVolumeSize(client, dv.Spec, pvcSpec)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(storageSpec.Resources.Requests.Storage().Value()).To(Equal(requestedVolumeSize.Value()))
|
||||
})
|
||||
|
||||
It("Should return the expected volume size (filesystem volume mode)", func() {
|
||||
storageSpec := &cdiv1.StorageSpec{
|
||||
pvcSpec := &corev1.PersistentVolumeClaimSpec{
|
||||
StorageClassName: &scName,
|
||||
VolumeMode: &volumeMode,
|
||||
Resources: corev1.ResourceRequirements{
|
||||
Requests: corev1.ResourceList{
|
||||
corev1.ResourceStorage: resource.MustParse("1Gi"),
|
||||
corev1.ResourceStorage: volumeSize,
|
||||
},
|
||||
},
|
||||
}
|
||||
volumeMode := corev1.PersistentVolumeFilesystem
|
||||
pvcSpec.VolumeMode = &volumeMode
|
||||
dv := createDataVolumeWithStorageAPI("testDV", "testNamespace", nil, storageSpec)
|
||||
requestedVolumeSize, err := resolveVolumeSize(client, dv.Spec, pvcSpec)
|
||||
err := renderPvcSpecVolumeSize(client, pvcSpec, false)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
requestedVolumeSize, found := pvcSpec.Resources.Requests[corev1.ResourceStorage]
|
||||
Expect(found).To(BeTrue())
|
||||
Expect(requestedVolumeSize.Value()).To(Equal(volumeSize.Value()))
|
||||
})
|
||||
|
||||
It("Should return the inflated volume size (filesystem volume mode)", func() {
|
||||
volumeMode := corev1.PersistentVolumeFilesystem
|
||||
pvcSpec := &corev1.PersistentVolumeClaimSpec{
|
||||
StorageClassName: &scName,
|
||||
VolumeMode: &volumeMode,
|
||||
Resources: corev1.ResourceRequirements{
|
||||
Requests: corev1.ResourceList{
|
||||
corev1.ResourceStorage: volumeSize,
|
||||
},
|
||||
},
|
||||
}
|
||||
err := renderPvcSpecVolumeSize(client, pvcSpec, false)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
requestedVolumeSize, found := pvcSpec.Resources.Requests[corev1.ResourceStorage]
|
||||
Expect(found).To(BeTrue())
|
||||
|
||||
// Inflate expected size with overhead
|
||||
fsOverhead, err2 := GetFilesystemOverheadForStorageClass(context.TODO(), client, dv.Spec.Storage.StorageClassName)
|
||||
Expect(err2).ToNot(HaveOccurred())
|
||||
fsOverhead, err := GetFilesystemOverheadForStorageClass(context.TODO(), client, pvcSpec.StorageClassName)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
fsOverheadFloat, _ := strconv.ParseFloat(string(fsOverhead), 64)
|
||||
requiredSpace := GetRequiredSpace(fsOverheadFloat, requestedVolumeSize.Value())
|
||||
requiredSpace := GetRequiredSpace(fsOverheadFloat, volumeSize.Value())
|
||||
expectedResult := resource.NewScaledQuantity(requiredSpace, 0)
|
||||
Expect(expectedResult.Value()).To(Equal(requestedVolumeSize.Value()))
|
||||
|
||||
Expect(requestedVolumeSize.Value()).To(BeNumerically(">", volumeSize.Value()))
|
||||
Expect(requestedVolumeSize.Value()).To(Equal(expectedResult.Value()))
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -1142,6 +1142,7 @@ func createImportTestEnv(podEnvVar *importPodEnvVar, uid string) []corev1.EnvVar
|
||||
type FakeFeatureGates struct {
|
||||
honorWaitForFirstConsumerEnabled bool
|
||||
claimAdoptionEnabled bool
|
||||
webhookPvcRenderingEnabled bool
|
||||
}
|
||||
|
||||
func (f *FakeFeatureGates) HonorWaitForFirstConsumerEnabled() (bool, error) {
|
||||
@ -1152,6 +1153,10 @@ func (f *FakeFeatureGates) ClaimAdoptionEnabled() (bool, error) {
|
||||
return f.claimAdoptionEnabled, nil
|
||||
}
|
||||
|
||||
func (f *FakeFeatureGates) WebhookPvcRenderingEnabled() (bool, error) {
|
||||
return f.webhookPvcRenderingEnabled, nil
|
||||
}
|
||||
|
||||
func createPendingPvc(name, ns string, annotations, labels map[string]string) *v1.PersistentVolumeClaim {
|
||||
return cc.CreatePvcInStorageClass(name, ns, nil, annotations, labels, v1.ClaimPending)
|
||||
}
|
||||
|
@ -18,6 +18,9 @@ const (
|
||||
// DataVolumeClaimAdoption - if enabled will allow PVC to be adopted by a DataVolume
|
||||
// it is not an error if PVC of sam name exists before DataVolume is created
|
||||
DataVolumeClaimAdoption = "DataVolumeClaimAdoption"
|
||||
|
||||
// WebhookPvcRendering - if enabled will deploy PVC mutating webhook for PVC rendering instead of the DV controller
|
||||
WebhookPvcRendering = "WebhookPvcRendering"
|
||||
)
|
||||
|
||||
// FeatureGates is a util for determining whether an optional feature is enabled or not.
|
||||
@ -27,6 +30,9 @@ type FeatureGates interface {
|
||||
|
||||
// ClaimAdoptionEnabled - see the DataVolumeClaimAdoption const
|
||||
ClaimAdoptionEnabled() (bool, error)
|
||||
|
||||
// WebhookPvcRenderingEnabled - see the WebhookPvcRendering const
|
||||
WebhookPvcRenderingEnabled() (bool, error)
|
||||
}
|
||||
|
||||
// CDIConfigFeatureGates is a util for determining whether an optional feature is enabled or not.
|
||||
@ -71,3 +77,14 @@ func (f *CDIConfigFeatureGates) HonorWaitForFirstConsumerEnabled() (bool, error)
|
||||
func (f *CDIConfigFeatureGates) ClaimAdoptionEnabled() (bool, error) {
|
||||
return f.isFeatureGateEnabled(DataVolumeClaimAdoption)
|
||||
}
|
||||
|
||||
// WebhookPvcRenderingEnabled tells if webhook PVC rendering is enabled
|
||||
func (f *CDIConfigFeatureGates) WebhookPvcRenderingEnabled() (bool, error) {
|
||||
return f.isFeatureGateEnabled(WebhookPvcRendering)
|
||||
}
|
||||
|
||||
// IsWebhookPvcRenderingEnabled tells if webhook PVC rendering is enabled
|
||||
func IsWebhookPvcRenderingEnabled(c client.Client) (bool, error) {
|
||||
gates := NewFeatureGates(c)
|
||||
return gates.WebhookPvcRenderingEnabled()
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ go_library(
|
||||
"//pkg/common:go_default_library",
|
||||
"//pkg/controller:go_default_library",
|
||||
"//pkg/controller/common:go_default_library",
|
||||
"//pkg/feature-gates:go_default_library",
|
||||
"//pkg/monitoring/metrics/operator-controller:go_default_library",
|
||||
"//pkg/monitoring/rules/recordingrules:go_default_library",
|
||||
"//pkg/operator:go_default_library",
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
"reflect"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
@ -33,10 +34,15 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
|
||||
sdk "kubevirt.io/controller-lifecycle-operator-sdk/pkg/sdk"
|
||||
|
||||
cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1"
|
||||
featuregates "kubevirt.io/containerized-data-importer/pkg/feature-gates"
|
||||
"kubevirt.io/containerized-data-importer/pkg/operator/resources/cluster"
|
||||
"kubevirt.io/containerized-data-importer/pkg/operator/resources/utils"
|
||||
|
||||
"kubevirt.io/containerized-data-importer/pkg/common"
|
||||
cdicontroller "kubevirt.io/containerized-data-importer/pkg/controller"
|
||||
cc "kubevirt.io/containerized-data-importer/pkg/controller/common"
|
||||
@ -51,6 +57,7 @@ func addReconcileCallbacks(r *ReconcileCDI) {
|
||||
r.reconciler.AddCallback(&appsv1.Deployment{}, reconcileRemainingRelationshipLabels)
|
||||
r.reconciler.AddCallback(&appsv1.Deployment{}, reconcileDeleteDeprecatedResources)
|
||||
r.reconciler.AddCallback(&appsv1.Deployment{}, reconcileCDICRD)
|
||||
r.reconciler.AddCallback(&appsv1.Deployment{}, reconcilePvcMutatingWebhook)
|
||||
r.reconciler.AddCallback(&extv1.CustomResourceDefinition{}, reconcileSetConfigAuthority)
|
||||
r.reconciler.AddCallback(&extv1.CustomResourceDefinition{}, reconcileHandleOldVersion)
|
||||
}
|
||||
@ -355,3 +362,113 @@ func restoreOlderVersions(currentCrd, desiredCrd *extv1.CustomResourceDefinition
|
||||
}
|
||||
return desiredCrd
|
||||
}
|
||||
|
||||
func reconcilePvcMutatingWebhook(args *callbacks.ReconcileCallbackArgs) error {
|
||||
if args.State != callbacks.ReconcileStatePostRead {
|
||||
return nil
|
||||
}
|
||||
|
||||
deployment, ok := args.DesiredObject.(*appsv1.Deployment)
|
||||
if !ok || deployment.Name != common.CDIApiServerResourceName {
|
||||
return nil
|
||||
}
|
||||
|
||||
enabled, err := featuregates.IsWebhookPvcRenderingEnabled(args.Client)
|
||||
if err != nil {
|
||||
return cc.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
whc := &admissionregistrationv1.MutatingWebhookConfiguration{}
|
||||
key := client.ObjectKey{Name: "cdi-api-pvc-mutate"}
|
||||
err = args.Client.Get(context.TODO(), key, whc)
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
exists := err == nil
|
||||
if !enabled {
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
err = args.Client.Delete(context.TODO(), whc)
|
||||
return client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
if err := initPvcMutatingWebhook(whc, args); err != nil {
|
||||
return err
|
||||
}
|
||||
return args.Client.Create(context.TODO(), whc)
|
||||
}
|
||||
|
||||
whcCopy := whc.DeepCopy()
|
||||
if err := initPvcMutatingWebhook(whc, args); err != nil {
|
||||
return err
|
||||
}
|
||||
if !reflect.DeepEqual(whc, whcCopy) {
|
||||
return args.Client.Update(context.TODO(), whc)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initPvcMutatingWebhook(whc *admissionregistrationv1.MutatingWebhookConfiguration, args *callbacks.ReconcileCallbackArgs) error {
|
||||
path := "/pvc-mutate"
|
||||
defaultServicePort := int32(443)
|
||||
allScopes := admissionregistrationv1.AllScopes
|
||||
exactPolicy := admissionregistrationv1.Exact
|
||||
failurePolicy := admissionregistrationv1.Fail
|
||||
defaultTimeoutSeconds := int32(10)
|
||||
reinvocationNever := admissionregistrationv1.NeverReinvocationPolicy
|
||||
sideEffect := admissionregistrationv1.SideEffectClassNone
|
||||
bundle := cluster.GetAPIServerCABundle(args.Namespace, args.Client, args.Logger)
|
||||
|
||||
whc.Name = "cdi-api-pvc-mutate"
|
||||
whc.Labels = map[string]string{utils.CDILabel: cluster.APIServerServiceName}
|
||||
whc.Webhooks = []admissionregistrationv1.MutatingWebhook{
|
||||
{
|
||||
Name: "pvc-mutate.cdi.kubevirt.io",
|
||||
Rules: []admissionregistrationv1.RuleWithOperations{{
|
||||
Operations: []admissionregistrationv1.OperationType{
|
||||
admissionregistrationv1.Create,
|
||||
},
|
||||
Rule: admissionregistrationv1.Rule{
|
||||
APIGroups: []string{corev1.SchemeGroupVersion.Group},
|
||||
APIVersions: []string{corev1.SchemeGroupVersion.Version},
|
||||
Resources: []string{"persistentvolumeclaims"},
|
||||
Scope: &allScopes,
|
||||
},
|
||||
}},
|
||||
ClientConfig: admissionregistrationv1.WebhookClientConfig{
|
||||
Service: &admissionregistrationv1.ServiceReference{
|
||||
Namespace: args.Namespace,
|
||||
Name: cluster.APIServerServiceName,
|
||||
Path: &path,
|
||||
Port: &defaultServicePort,
|
||||
},
|
||||
CABundle: bundle,
|
||||
},
|
||||
FailurePolicy: &failurePolicy,
|
||||
SideEffects: &sideEffect,
|
||||
MatchPolicy: &exactPolicy,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
TimeoutSeconds: &defaultTimeoutSeconds,
|
||||
AdmissionReviewVersions: []string{
|
||||
"v1",
|
||||
},
|
||||
ObjectSelector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
common.PvcUseStorageProfileLabel: "true",
|
||||
},
|
||||
},
|
||||
ReinvocationPolicy: &reinvocationNever,
|
||||
},
|
||||
}
|
||||
|
||||
cdi, err := cc.GetActiveCDI(context.TODO(), args.Client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return controllerutil.SetControllerReference(cdi, whc, args.Scheme)
|
||||
}
|
||||
|
@ -19,22 +19,24 @@ package cluster
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
|
||||
"kubevirt.io/containerized-data-importer/pkg/common"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
cdicorev1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1"
|
||||
cdiuploadv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/upload/v1beta1"
|
||||
"kubevirt.io/containerized-data-importer/pkg/common"
|
||||
"kubevirt.io/containerized-data-importer/pkg/operator/resources/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
apiServerServiceName = "cdi-api"
|
||||
// APIServerServiceName is the API server service name
|
||||
APIServerServiceName = "cdi-api"
|
||||
)
|
||||
|
||||
func createStaticAPIServerResources(args *FactoryArgs) []client.Object {
|
||||
@ -91,8 +93,39 @@ func getAPIServerClusterPolicyRules() []rbacv1.PolicyRule {
|
||||
},
|
||||
Verbs: []string{
|
||||
"get",
|
||||
"list",
|
||||
"watch",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
APIGroups: []string{
|
||||
"",
|
||||
},
|
||||
Resources: []string{
|
||||
"persistentvolumes",
|
||||
},
|
||||
Verbs: []string{
|
||||
"get",
|
||||
"list",
|
||||
"watch",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
APIGroups: []string{
|
||||
"storage.k8s.io",
|
||||
},
|
||||
Resources: []string{
|
||||
"storageclasses",
|
||||
},
|
||||
Verbs: []string{
|
||||
"get",
|
||||
"list",
|
||||
"watch",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
APIGroups: []string{
|
||||
"",
|
||||
@ -113,6 +146,8 @@ func getAPIServerClusterPolicyRules() []rbacv1.PolicyRule {
|
||||
},
|
||||
Verbs: []string{
|
||||
"get",
|
||||
"list",
|
||||
"watch",
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -138,6 +173,35 @@ func getAPIServerClusterPolicyRules() []rbacv1.PolicyRule {
|
||||
"get",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
APIGroups: []string{
|
||||
"cdi.kubevirt.io",
|
||||
},
|
||||
Resources: []string{
|
||||
"volumeclonesources",
|
||||
},
|
||||
Verbs: []string{
|
||||
"get",
|
||||
"list",
|
||||
"watch",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
APIGroups: []string{
|
||||
"cdi.kubevirt.io",
|
||||
},
|
||||
Resources: []string{
|
||||
"storageprofiles",
|
||||
},
|
||||
Verbs: []string{
|
||||
"get",
|
||||
"list",
|
||||
"watch",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
APIGroups: []string{
|
||||
"cdi.kubevirt.io",
|
||||
@ -147,6 +211,8 @@ func getAPIServerClusterPolicyRules() []rbacv1.PolicyRule {
|
||||
},
|
||||
Verbs: []string{
|
||||
"get",
|
||||
"list",
|
||||
"watch",
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -185,13 +251,13 @@ func createAPIService(version, namespace string, c client.Client, l logr.Logger)
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("%s.%s", version, cdiuploadv1.SchemeGroupVersion.Group),
|
||||
Labels: map[string]string{
|
||||
utils.CDILabel: apiServerServiceName,
|
||||
utils.CDILabel: APIServerServiceName,
|
||||
},
|
||||
},
|
||||
Spec: apiregistrationv1.APIServiceSpec{
|
||||
Service: &apiregistrationv1.ServiceReference{
|
||||
Namespace: namespace,
|
||||
Name: apiServerServiceName,
|
||||
Name: APIServerServiceName,
|
||||
},
|
||||
Group: cdiuploadv1.SchemeGroupVersion.Group,
|
||||
Version: version,
|
||||
@ -204,7 +270,7 @@ func createAPIService(version, namespace string, c client.Client, l logr.Logger)
|
||||
return apiService
|
||||
}
|
||||
|
||||
bundle := getAPIServerCABundle(namespace, c, l)
|
||||
bundle := GetAPIServerCABundle(namespace, c, l)
|
||||
if bundle != nil {
|
||||
apiService.Spec.CABundle = bundle
|
||||
}
|
||||
@ -228,7 +294,7 @@ func createDataImportCronValidatingWebhook(namespace string, c client.Client, l
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "cdi-api-dataimportcron-validate",
|
||||
Labels: map[string]string{
|
||||
utils.CDILabel: apiServerServiceName,
|
||||
utils.CDILabel: APIServerServiceName,
|
||||
},
|
||||
},
|
||||
Webhooks: []admissionregistrationv1.ValidatingWebhook{
|
||||
@ -249,7 +315,7 @@ func createDataImportCronValidatingWebhook(namespace string, c client.Client, l
|
||||
ClientConfig: admissionregistrationv1.WebhookClientConfig{
|
||||
Service: &admissionregistrationv1.ServiceReference{
|
||||
Namespace: namespace,
|
||||
Name: apiServerServiceName,
|
||||
Name: APIServerServiceName,
|
||||
Path: &path,
|
||||
Port: &defaultServicePort,
|
||||
},
|
||||
@ -271,7 +337,7 @@ func createDataImportCronValidatingWebhook(namespace string, c client.Client, l
|
||||
return whc
|
||||
}
|
||||
|
||||
bundle := getAPIServerCABundle(namespace, c, l)
|
||||
bundle := GetAPIServerCABundle(namespace, c, l)
|
||||
if bundle != nil {
|
||||
whc.Webhooks[0].ClientConfig.CABundle = bundle
|
||||
}
|
||||
@ -295,7 +361,7 @@ func createPopulatorsValidatingWebhook(namespace string, c client.Client, l logr
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "cdi-api-populator-validate",
|
||||
Labels: map[string]string{
|
||||
utils.CDILabel: apiServerServiceName,
|
||||
utils.CDILabel: APIServerServiceName,
|
||||
},
|
||||
},
|
||||
Webhooks: []admissionregistrationv1.ValidatingWebhook{
|
||||
@ -316,7 +382,7 @@ func createPopulatorsValidatingWebhook(namespace string, c client.Client, l logr
|
||||
ClientConfig: admissionregistrationv1.WebhookClientConfig{
|
||||
Service: &admissionregistrationv1.ServiceReference{
|
||||
Namespace: namespace,
|
||||
Name: apiServerServiceName,
|
||||
Name: APIServerServiceName,
|
||||
Path: &path,
|
||||
Port: &defaultServicePort,
|
||||
},
|
||||
@ -338,7 +404,7 @@ func createPopulatorsValidatingWebhook(namespace string, c client.Client, l logr
|
||||
return whc
|
||||
}
|
||||
|
||||
bundle := getAPIServerCABundle(namespace, c, l)
|
||||
bundle := GetAPIServerCABundle(namespace, c, l)
|
||||
if bundle != nil {
|
||||
whc.Webhooks[0].ClientConfig.CABundle = bundle
|
||||
}
|
||||
@ -362,7 +428,7 @@ func createDataVolumeValidatingWebhook(namespace string, c client.Client, l logr
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "cdi-api-datavolume-validate",
|
||||
Labels: map[string]string{
|
||||
utils.CDILabel: apiServerServiceName,
|
||||
utils.CDILabel: APIServerServiceName,
|
||||
},
|
||||
},
|
||||
Webhooks: []admissionregistrationv1.ValidatingWebhook{
|
||||
@ -383,7 +449,7 @@ func createDataVolumeValidatingWebhook(namespace string, c client.Client, l logr
|
||||
ClientConfig: admissionregistrationv1.WebhookClientConfig{
|
||||
Service: &admissionregistrationv1.ServiceReference{
|
||||
Namespace: namespace,
|
||||
Name: apiServerServiceName,
|
||||
Name: APIServerServiceName,
|
||||
Path: &path,
|
||||
Port: &defaultServicePort,
|
||||
},
|
||||
@ -405,7 +471,7 @@ func createDataVolumeValidatingWebhook(namespace string, c client.Client, l logr
|
||||
return whc
|
||||
}
|
||||
|
||||
bundle := getAPIServerCABundle(namespace, c, l)
|
||||
bundle := GetAPIServerCABundle(namespace, c, l)
|
||||
if bundle != nil {
|
||||
whc.Webhooks[0].ClientConfig.CABundle = bundle
|
||||
}
|
||||
@ -429,7 +495,7 @@ func createCDIValidatingWebhook(namespace string, c client.Client, l logr.Logger
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "cdi-api-validate",
|
||||
Labels: map[string]string{
|
||||
utils.CDILabel: apiServerServiceName,
|
||||
utils.CDILabel: APIServerServiceName,
|
||||
},
|
||||
},
|
||||
Webhooks: []admissionregistrationv1.ValidatingWebhook{
|
||||
@ -449,7 +515,7 @@ func createCDIValidatingWebhook(namespace string, c client.Client, l logr.Logger
|
||||
ClientConfig: admissionregistrationv1.WebhookClientConfig{
|
||||
Service: &admissionregistrationv1.ServiceReference{
|
||||
Namespace: namespace,
|
||||
Name: apiServerServiceName,
|
||||
Name: APIServerServiceName,
|
||||
Path: &path,
|
||||
Port: &defaultServicePort,
|
||||
},
|
||||
@ -471,7 +537,7 @@ func createCDIValidatingWebhook(namespace string, c client.Client, l logr.Logger
|
||||
return whc
|
||||
}
|
||||
|
||||
bundle := getAPIServerCABundle(namespace, c, l)
|
||||
bundle := GetAPIServerCABundle(namespace, c, l)
|
||||
if bundle != nil {
|
||||
for i := range whc.Webhooks {
|
||||
whc.Webhooks[i].ClientConfig.CABundle = bundle
|
||||
@ -498,7 +564,7 @@ func createObjectTransferValidatingWebhook(namespace string, c client.Client, l
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "objecttransfer-api-validate",
|
||||
Labels: map[string]string{
|
||||
utils.CDILabel: apiServerServiceName,
|
||||
utils.CDILabel: APIServerServiceName,
|
||||
},
|
||||
},
|
||||
Webhooks: []admissionregistrationv1.ValidatingWebhook{
|
||||
@ -521,7 +587,7 @@ func createObjectTransferValidatingWebhook(namespace string, c client.Client, l
|
||||
ClientConfig: admissionregistrationv1.WebhookClientConfig{
|
||||
Service: &admissionregistrationv1.ServiceReference{
|
||||
Namespace: namespace,
|
||||
Name: apiServerServiceName,
|
||||
Name: APIServerServiceName,
|
||||
Path: &path,
|
||||
Port: &defaultServicePort,
|
||||
},
|
||||
@ -543,7 +609,7 @@ func createObjectTransferValidatingWebhook(namespace string, c client.Client, l
|
||||
return whc
|
||||
}
|
||||
|
||||
bundle := getAPIServerCABundle(namespace, c, l)
|
||||
bundle := GetAPIServerCABundle(namespace, c, l)
|
||||
if bundle != nil {
|
||||
for i := range whc.Webhooks {
|
||||
whc.Webhooks[i].ClientConfig.CABundle = bundle
|
||||
@ -571,7 +637,7 @@ func createDataVolumeMutatingWebhook(namespace string, c client.Client, l logr.L
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "cdi-api-datavolume-mutate",
|
||||
Labels: map[string]string{
|
||||
utils.CDILabel: apiServerServiceName,
|
||||
utils.CDILabel: APIServerServiceName,
|
||||
},
|
||||
},
|
||||
Webhooks: []admissionregistrationv1.MutatingWebhook{
|
||||
@ -592,7 +658,7 @@ func createDataVolumeMutatingWebhook(namespace string, c client.Client, l logr.L
|
||||
ClientConfig: admissionregistrationv1.WebhookClientConfig{
|
||||
Service: &admissionregistrationv1.ServiceReference{
|
||||
Namespace: namespace,
|
||||
Name: apiServerServiceName,
|
||||
Name: APIServerServiceName,
|
||||
Path: &path,
|
||||
Port: &defaultServicePort,
|
||||
},
|
||||
@ -615,7 +681,7 @@ func createDataVolumeMutatingWebhook(namespace string, c client.Client, l logr.L
|
||||
return whc
|
||||
}
|
||||
|
||||
bundle := getAPIServerCABundle(namespace, c, l)
|
||||
bundle := GetAPIServerCABundle(namespace, c, l)
|
||||
if bundle != nil {
|
||||
whc.Webhooks[0].ClientConfig.CABundle = bundle
|
||||
}
|
||||
@ -623,7 +689,8 @@ func createDataVolumeMutatingWebhook(namespace string, c client.Client, l logr.L
|
||||
return whc
|
||||
}
|
||||
|
||||
func getAPIServerCABundle(namespace string, c client.Client, l logr.Logger) []byte {
|
||||
// GetAPIServerCABundle returns the API server CA bundle
|
||||
func GetAPIServerCABundle(namespace string, c client.Client, l logr.Logger) []byte {
|
||||
cm := &corev1.ConfigMap{}
|
||||
key := client.ObjectKey{Namespace: namespace, Name: "cdi-apiserver-signer-bundle"}
|
||||
if err := c.Get(context.TODO(), key, cm); err != nil {
|
||||
|
@ -150,6 +150,7 @@ func getClusterPolicyRules() []rbacv1.PolicyRule {
|
||||
},
|
||||
ResourceNames: []string{
|
||||
"cdi-api-datavolume-mutate",
|
||||
"cdi-api-pvc-mutate",
|
||||
},
|
||||
Verbs: []string{
|
||||
"get",
|
||||
|
@ -106,6 +106,9 @@ type StorageSpec struct {
|
||||
DataSourceRef *corev1.TypedObjectReference `json:"dataSourceRef,omitempty"`
|
||||
}
|
||||
|
||||
// PersistentVolumeFromStorageProfile means the volume mode will be auto selected by CDI according to a matching StorageProfile
|
||||
const PersistentVolumeFromStorageProfile corev1.PersistentVolumeMode = "FromStorageProfile"
|
||||
|
||||
// DataVolumeCheckpoint defines a stage in a warm migration.
|
||||
type DataVolumeCheckpoint struct {
|
||||
// Previous is the identifier of the snapshot from the previous checkpoint.
|
||||
|
@ -83,6 +83,7 @@ go_test(
|
||||
"//vendor/github.com/openshift/client-go/security/clientset/versioned:go_default_library",
|
||||
"//vendor/github.com/openshift/custom-resource-status/conditions/v1:go_default_library",
|
||||
"//vendor/github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1:go_default_library",
|
||||
"//vendor/k8s.io/api/admissionregistration/v1:go_default_library",
|
||||
"//vendor/k8s.io/api/apps/v1:go_default_library",
|
||||
"//vendor/k8s.io/api/authorization/v1:go_default_library",
|
||||
"//vendor/k8s.io/api/batch/v1:go_default_library",
|
||||
|
@ -18,8 +18,10 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1"
|
||||
"kubevirt.io/containerized-data-importer/pkg/common"
|
||||
"kubevirt.io/containerized-data-importer/pkg/controller/clone"
|
||||
cc "kubevirt.io/containerized-data-importer/pkg/controller/common"
|
||||
"kubevirt.io/containerized-data-importer/tests"
|
||||
"kubevirt.io/containerized-data-importer/tests/framework"
|
||||
"kubevirt.io/containerized-data-importer/tests/utils"
|
||||
)
|
||||
@ -37,6 +39,7 @@ var _ = Describe("Clone Populator tests", func() {
|
||||
var (
|
||||
defaultSize = resource.MustParse("1Gi")
|
||||
biggerSize = resource.MustParse("2Gi")
|
||||
target *corev1.PersistentVolumeClaim
|
||||
)
|
||||
|
||||
f := framework.NewFramework("clone-populator-test")
|
||||
@ -47,6 +50,10 @@ var _ = Describe("Clone Populator tests", func() {
|
||||
}
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
tests.DisableWebhookPvcRendering(f.CrClient)
|
||||
})
|
||||
|
||||
createSource := func(sz resource.Quantity, vm corev1.PersistentVolumeMode) *corev1.PersistentVolumeClaim {
|
||||
dataVolume := utils.NewDataVolumeWithHTTPImport(sourceName, sz.String(), fmt.Sprintf(utils.TinyCoreIsoURL, f.CdiInstallNs))
|
||||
dataVolume.Spec.PVC.VolumeMode = &vm
|
||||
@ -177,6 +184,25 @@ var _ = Describe("Clone Populator tests", func() {
|
||||
return result
|
||||
}
|
||||
|
||||
// Create target PVC without AccessModes and Resources.Requests, both to be auto-completed by the webhook rendering
|
||||
createIncompleteTarget := func(sz *resource.Quantity, vm corev1.PersistentVolumeMode, strategy, scName string) *corev1.PersistentVolumeClaim {
|
||||
tests.EnableWebhookPvcRendering(f.CrClient)
|
||||
size := resource.Quantity{}
|
||||
if sz != nil {
|
||||
size = *sz
|
||||
}
|
||||
pvc := generateTargetPVCWithStrategy(size, vm, strategy, scName)
|
||||
pvc.Spec.AccessModes = nil
|
||||
cc.AddLabel(pvc, common.PvcUseStorageProfileLabel, "true")
|
||||
err := f.CrClient.Create(context.Background(), pvc)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
f.ForceSchedulingIfWaitForFirstConsumerPopulationPVC(pvc)
|
||||
result := &corev1.PersistentVolumeClaim{}
|
||||
err = f.CrClient.Get(context.Background(), client.ObjectKeyFromObject(pvc), result)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
return result
|
||||
}
|
||||
|
||||
createTarget := func(sz resource.Quantity, vm corev1.PersistentVolumeMode) *corev1.PersistentVolumeClaim {
|
||||
return createTargetWithStrategy(sz, vm, "", utils.DefaultStorageClass.GetName())
|
||||
}
|
||||
@ -209,15 +235,22 @@ var _ = Describe("Clone Populator tests", func() {
|
||||
}
|
||||
|
||||
Context("Clone from PVC", func() {
|
||||
It("should do filesystem to filesystem clone, with immediateBinding annotation", func() {
|
||||
DescribeTable("should do filesystem to filesystem clone", func(webhookRendering bool) {
|
||||
source := createSource(defaultSize, corev1.PersistentVolumeFilesystem)
|
||||
createDataSource()
|
||||
target := createTargetWithImmediateBinding(defaultSize, corev1.PersistentVolumeFilesystem)
|
||||
if webhookRendering {
|
||||
target = createIncompleteTarget(nil, corev1.PersistentVolumeFilesystem, "", utils.DefaultStorageClass.GetName())
|
||||
} else {
|
||||
target = createTargetWithImmediateBinding(defaultSize, corev1.PersistentVolumeFilesystem)
|
||||
}
|
||||
target = waitSucceeded(target)
|
||||
sourceHash := getHash(source, 0)
|
||||
targetHash := getHash(target, 0)
|
||||
Expect(targetHash).To(Equal(sourceHash))
|
||||
})
|
||||
},
|
||||
Entry("with immediateBinding annotation", false),
|
||||
Entry("with incomplete target PVC webhook rendering", Serial, true),
|
||||
)
|
||||
|
||||
It("should do filesystem to filesystem clone, source created after target", func() {
|
||||
createDataSource()
|
||||
@ -251,27 +284,38 @@ var _ = Describe("Clone Populator tests", func() {
|
||||
Expect(targetHash).To(Equal(sourceHash))
|
||||
})
|
||||
|
||||
It("should do block to filesystem clone", func() {
|
||||
DescribeTable("should do block to filesystem clone", func(webhookRendering bool) {
|
||||
if !f.IsBlockVolumeStorageClassAvailable() {
|
||||
Skip("Storage Class for block volume is not available")
|
||||
}
|
||||
source := createSource(defaultSize, corev1.PersistentVolumeBlock)
|
||||
createDataSource()
|
||||
target := createTarget(biggerSize, corev1.PersistentVolumeFilesystem)
|
||||
if webhookRendering {
|
||||
target = createIncompleteTarget(nil, corev1.PersistentVolumeFilesystem, "", utils.DefaultStorageClass.GetName())
|
||||
} else {
|
||||
target = createTarget(biggerSize, corev1.PersistentVolumeFilesystem)
|
||||
}
|
||||
target = waitSucceeded(target)
|
||||
sourceHash := getHash(source, 100000)
|
||||
targetHash := getHash(target, 100000)
|
||||
Expect(targetHash).To(Equal(sourceHash))
|
||||
f.ExpectCloneFallback(target, clone.IncompatibleVolumeModes, clone.MessageIncompatibleVolumeModes)
|
||||
})
|
||||
},
|
||||
Entry("with valid target PVC", false),
|
||||
Entry("with incomplete target PVC webhook rendering", Serial, true),
|
||||
)
|
||||
|
||||
It("should do filesystem to block clone", func() {
|
||||
DescribeTable("should do filesystem to block clone", func(webhookRendering bool) {
|
||||
if !f.IsBlockVolumeStorageClassAvailable() {
|
||||
Skip("Storage Class for block volume is not available")
|
||||
}
|
||||
source := createSource(defaultSize, corev1.PersistentVolumeFilesystem)
|
||||
createDataSource()
|
||||
target := createTarget(defaultSize, corev1.PersistentVolumeBlock)
|
||||
if webhookRendering {
|
||||
target = createIncompleteTarget(nil, corev1.PersistentVolumeBlock, "", utils.DefaultStorageClass.GetName())
|
||||
} else {
|
||||
target = createTarget(defaultSize, corev1.PersistentVolumeBlock)
|
||||
}
|
||||
target = waitSucceeded(target)
|
||||
targetSize := target.Status.Capacity[corev1.ResourceStorage]
|
||||
Expect(targetSize.Cmp(defaultSize)).To(BeNumerically(">=", 0))
|
||||
@ -279,31 +323,39 @@ var _ = Describe("Clone Populator tests", func() {
|
||||
targetHash := getHash(target, 100000)
|
||||
Expect(targetHash).To(Equal(sourceHash))
|
||||
f.ExpectCloneFallback(target, clone.IncompatibleVolumeModes, clone.MessageIncompatibleVolumeModes)
|
||||
})
|
||||
},
|
||||
Entry("with valid target PVC", false),
|
||||
Entry("with incomplete target PVC webhook rendering", Serial, true),
|
||||
)
|
||||
|
||||
DescribeTable("should clone explicit types requested by user", func(cloneType string, canDo func() bool) {
|
||||
DescribeTable("should clone explicit types requested by user", func(cloneType string, webhookRendering bool, canDo func() bool) {
|
||||
if canDo != nil && !canDo() {
|
||||
Skip(fmt.Sprintf("Clone type %s does not work without a capable storage class", cloneType))
|
||||
}
|
||||
source := createSource(defaultSize, corev1.PersistentVolumeFilesystem)
|
||||
createDataSource()
|
||||
target := createTargetWithStrategy(defaultSize, corev1.PersistentVolumeFilesystem, cloneType, utils.DefaultStorageClass.GetName())
|
||||
if webhookRendering {
|
||||
target = createIncompleteTarget(nil, corev1.PersistentVolumeFilesystem, cloneType, utils.DefaultStorageClass.GetName())
|
||||
} else {
|
||||
target = createTargetWithStrategy(defaultSize, corev1.PersistentVolumeFilesystem, cloneType, utils.DefaultStorageClass.GetName())
|
||||
}
|
||||
target = waitSucceeded(target)
|
||||
Expect(target.Annotations["cdi.kubevirt.io/cloneType"]).To(Equal(cloneType))
|
||||
sourceHash := getHash(source, 0)
|
||||
targetHash := getHash(target, 0)
|
||||
Expect(targetHash).To(Equal(sourceHash))
|
||||
},
|
||||
Entry("should do csi clone if possible", "csi-clone", f.IsCSIVolumeCloneStorageClassAvailable),
|
||||
Entry("should do snapshot clone if possible", "snapshot", f.IsSnapshotStorageClassAvailable),
|
||||
Entry("should do host assisted clone", "copy", nil),
|
||||
Entry("should do csi clone if possible", "csi-clone", false, f.IsCSIVolumeCloneStorageClassAvailable),
|
||||
Entry("should do csi clone if possible, with pvc webhook rendering", Serial, "csi-clone", true, f.IsCSIVolumeCloneStorageClassAvailable),
|
||||
Entry("should do snapshot clone if possible", "snapshot", false, f.IsSnapshotStorageClassAvailable),
|
||||
Entry("should do snapshot clone if possible, with pvc webhook rendering", Serial, "snapshot", true, f.IsSnapshotStorageClassAvailable),
|
||||
Entry("should do host assisted clone", "copy", false, nil),
|
||||
Entry("should do host assisted clone, with pvc webhook rendering", Serial, "copy", true, nil),
|
||||
)
|
||||
})
|
||||
|
||||
Context("Clone from Snapshot", func() {
|
||||
var (
|
||||
snapshot = &snapshotv1.VolumeSnapshot{}
|
||||
)
|
||||
var snapshot *snapshotv1.VolumeSnapshot
|
||||
|
||||
BeforeEach(func() {
|
||||
if !f.IsSnapshotStorageClassAvailable() {
|
||||
@ -312,25 +364,36 @@ var _ = Describe("Clone Populator tests", func() {
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
if snapshot != nil {
|
||||
return
|
||||
}
|
||||
By(fmt.Sprintf("[AfterEach] Removing snapshot %s/%s", snapshot.Namespace, snapshot.Name))
|
||||
Eventually(func() bool {
|
||||
err := f.CrClient.Delete(context.TODO(), snapshot)
|
||||
return err != nil && k8serrors.IsNotFound(err)
|
||||
}, time.Minute, time.Second).Should(BeTrue())
|
||||
snapshot = nil
|
||||
})
|
||||
|
||||
It("should do smart clone", func() {
|
||||
DescribeTable("should do smart clone", func(webhookRendering bool) {
|
||||
createSnapshotDataSource()
|
||||
snapshot = createVolumeSnapshotSource("1Gi", nil, corev1.PersistentVolumeFilesystem)
|
||||
By("Creating target PVC")
|
||||
target := createTarget(defaultSize, corev1.PersistentVolumeFilesystem)
|
||||
if webhookRendering {
|
||||
target = createIncompleteTarget(nil, corev1.PersistentVolumeFilesystem, "", utils.DefaultStorageClass.GetName())
|
||||
} else {
|
||||
target = createTarget(defaultSize, corev1.PersistentVolumeFilesystem)
|
||||
}
|
||||
By("Waiting for population to be succeeded")
|
||||
target = waitSucceeded(target)
|
||||
path := utils.DefaultImagePath
|
||||
same, err := f.VerifyTargetPVCContentMD5(f.Namespace, target, path, utils.UploadFileMD5, utils.UploadFileSize)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(same).To(BeTrue())
|
||||
})
|
||||
},
|
||||
Entry("with valid target PVC", false),
|
||||
Entry("with incomplete target PVC webhook rendering", Serial, true),
|
||||
)
|
||||
|
||||
Context("Fallback to host assisted", func() {
|
||||
var noExpansionStorageClass *storagev1.StorageClass
|
||||
@ -346,13 +409,18 @@ var _ = Describe("Clone Populator tests", func() {
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("should do regular host assisted clone", func() {
|
||||
DescribeTable("should do regular host assisted clone", func(webhookRendering bool) {
|
||||
createSnapshotDataSource()
|
||||
snapshot = createVolumeSnapshotSource("1Gi", &noExpansionStorageClass.Name, corev1.PersistentVolumeFilesystem)
|
||||
By("Creating target PVC")
|
||||
target := createTargetWithStrategy(biggerSize, corev1.PersistentVolumeFilesystem, "", noExpansionStorageClass.Name)
|
||||
if webhookRendering {
|
||||
target = createIncompleteTarget(&biggerSize, corev1.PersistentVolumeFilesystem, "", noExpansionStorageClass.Name)
|
||||
} else {
|
||||
target = createTargetWithStrategy(biggerSize, corev1.PersistentVolumeFilesystem, "", noExpansionStorageClass.Name)
|
||||
}
|
||||
By("Waiting for population to be succeeded")
|
||||
target = waitSucceeded(target)
|
||||
|
||||
path := utils.DefaultImagePath
|
||||
same, err := f.VerifyTargetPVCContentMD5(f.Namespace, target, path, utils.UploadFileMD5, utils.UploadFileSize)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
@ -361,7 +429,10 @@ var _ = Describe("Clone Populator tests", func() {
|
||||
_, err = f.K8sClient.CoreV1().PersistentVolumeClaims(snapshot.Namespace).Get(context.TODO(), tmpSourcePVCforSnapshot, metav1.GetOptions{})
|
||||
Expect(k8serrors.IsNotFound(err)).To(BeTrue())
|
||||
f.ExpectCloneFallback(target, clone.NoVolumeExpansion, clone.MessageNoVolumeExpansion)
|
||||
})
|
||||
},
|
||||
Entry("with valid target PVC", false),
|
||||
Entry("with incomplete target PVC webhook rendering", Serial, true),
|
||||
)
|
||||
|
||||
It("should finish the clone after creating the source snapshot", func() {
|
||||
By("Create the clone before the source snapshot")
|
||||
|
@ -9,10 +9,11 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/google/uuid"
|
||||
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
storagev1 "k8s.io/api/storage/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
@ -211,6 +212,12 @@ var _ = Describe("[vendor:cnv-qe@redhat.com][level:component]DataVolume tests",
|
||||
return utils.NewDataVolumeWithImageioWarmImport(dataVolumeName, size, url, s.Name, cm, diskID, checkpoints, true)
|
||||
}
|
||||
|
||||
updateWebhookPvcRendering := func(webhookRenderingLabel string) {
|
||||
if webhookRenderingLabel == "true" {
|
||||
EnableWebhookPvcRendering(f.CrClient)
|
||||
}
|
||||
}
|
||||
|
||||
AfterEach(func() {
|
||||
if sourcePvc != nil {
|
||||
By("[AfterEach] Clean up sourcePvc PVC")
|
||||
@ -1851,17 +1858,22 @@ var _ = Describe("[vendor:cnv-qe@redhat.com][level:component]DataVolume tests",
|
||||
testFile := utils.DefaultPvcMountPath + "/source.txt"
|
||||
fillCommand := "echo \"" + fillData + "\" >> " + testFile
|
||||
|
||||
createDataVolumeForImport := func(f *framework.Framework, storageSpec cdiv1.StorageSpec) *cdiv1.DataVolume {
|
||||
createLabeledDataVolumeForImport := func(f *framework.Framework, storageSpec cdiv1.StorageSpec, labels map[string]string) *cdiv1.DataVolume {
|
||||
dataVolume := utils.NewDataVolumeWithHTTPImportAndStorageSpec(
|
||||
dataVolumeName, "1Gi", fmt.Sprintf(utils.TinyCoreQcow2URL, f.CdiInstallNs))
|
||||
|
||||
dataVolume.Spec.Storage = &storageSpec
|
||||
dataVolume.Labels = labels
|
||||
|
||||
dv, err := utils.CreateDataVolumeFromDefinition(f.CdiClient, f.Namespace.Name, dataVolume)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
return dv
|
||||
}
|
||||
|
||||
createDataVolumeForImport := func(f *framework.Framework, storageSpec cdiv1.StorageSpec) *cdiv1.DataVolume {
|
||||
return createLabeledDataVolumeForImport(f, storageSpec, nil)
|
||||
}
|
||||
|
||||
createDataVolumeForUpload := func(f *framework.Framework, storageSpec cdiv1.StorageSpec) *cdiv1.DataVolume {
|
||||
dataVolume := utils.NewDataVolumeForUpload(dataVolumeName, "1Mi")
|
||||
dataVolume.Spec.PVC = nil
|
||||
@ -1977,6 +1989,8 @@ var _ = Describe("[vendor:cnv-qe@redhat.com][level:component]DataVolume tests",
|
||||
origSpec.DeepCopyInto(config)
|
||||
})
|
||||
|
||||
DisableWebhookPvcRendering(f.CrClient)
|
||||
|
||||
Eventually(func() bool {
|
||||
config, err = f.CdiClient.CdiV1beta1().CDIConfigs().Get(context.TODO(), common.ConfigName, metav1.GetOptions{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
@ -2071,7 +2085,21 @@ var _ = Describe("[vendor:cnv-qe@redhat.com][level:component]DataVolume tests",
|
||||
Expect(*pvc.Spec.StorageClassName).To(SatisfyAll(Not(BeNil()), Equal(defaultScName)))
|
||||
})
|
||||
|
||||
It("[test_id:5912]Import fails creating a PVC from DV without accessModes and volume mode, no profile", func() {
|
||||
verifyControllerRenderingEvent := func(events string) bool {
|
||||
return strings.Contains(events, controller.ErrClaimNotValid) && strings.Contains(events, "no accessMode defined DV nor on StorageProfile")
|
||||
}
|
||||
|
||||
verifyControllerRenderingNoDefaultScEvent := func(events string) bool {
|
||||
return strings.Contains(events, controller.ErrClaimNotValid) && strings.Contains(events, "PVC spec is missing accessMode and no storageClass to choose profile")
|
||||
}
|
||||
|
||||
verifyWebhookRenderingEvent := func(events string) bool {
|
||||
return strings.Contains(events, controller.NotFound) && strings.Contains(events, "No PVC found")
|
||||
}
|
||||
|
||||
DescribeTable("Import fails creating a PVC from DV without accessModes and volume mode, no profile", func(webhookRenderingLabel string, verifyEvent func(string) bool) {
|
||||
updateWebhookPvcRendering(webhookRenderingLabel)
|
||||
|
||||
// assumes local is available and has no volumeMode
|
||||
storageProfileName := findStorageProfileWithoutAccessModes(f.CrClient)
|
||||
By(fmt.Sprintf("creating new datavolume %s without accessModes", dataVolumeName))
|
||||
@ -2086,7 +2114,8 @@ var _ = Describe("[vendor:cnv-qe@redhat.com][level:component]DataVolume tests",
|
||||
},
|
||||
},
|
||||
}
|
||||
dataVolume := createDataVolumeForImport(f, spec)
|
||||
dataVolume := createLabeledDataVolumeForImport(f, spec,
|
||||
map[string]string{common.PvcUseStorageProfileLabel: webhookRenderingLabel})
|
||||
|
||||
By("verifying pvc not created")
|
||||
_, err := utils.FindPVC(f.K8sClient, dataVolume.Namespace, dataVolume.Name)
|
||||
@ -2098,14 +2127,19 @@ var _ = Describe("[vendor:cnv-qe@redhat.com][level:component]DataVolume tests",
|
||||
events, err := f.RunKubectlCommand("get", "events", "-n", dataVolume.Namespace, "--field-selector=involvedObject.kind=DataVolume")
|
||||
if err == nil {
|
||||
fmt.Fprintf(GinkgoWriter, "%s", events)
|
||||
return strings.Contains(events, controller.ErrClaimNotValid) && strings.Contains(events, "DataVolume.storage spec is missing accessMode and volumeMode, cannot get access mode from StorageProfile")
|
||||
return verifyEvent(events)
|
||||
}
|
||||
fmt.Fprintf(GinkgoWriter, "ERROR: %s\n", err.Error())
|
||||
return false
|
||||
}, timeout, pollingInterval).Should(BeTrue())
|
||||
})
|
||||
},
|
||||
Entry("[test_id:5912] (controller rendering)", "false", verifyControllerRenderingEvent),
|
||||
Entry("[test_id:XXXX] (webhook rendering)", Serial, "true", verifyWebhookRenderingEvent),
|
||||
)
|
||||
|
||||
DescribeTable("Import fails when no default storage class, and recovers when default is set", func(webhookRenderingLabel string, verifyEvent func(string) bool) {
|
||||
updateWebhookPvcRendering(webhookRenderingLabel)
|
||||
|
||||
It("[test_id:8383]Import fails when no default storage class, and recovers when default is set", func() {
|
||||
By("updating to no default storage class")
|
||||
defaultSc.Annotations[controller.AnnDefaultStorageClass] = "false"
|
||||
defaultSc, err = f.K8sClient.StorageV1().StorageClasses().Update(context.TODO(), defaultSc, metav1.UpdateOptions{})
|
||||
@ -2120,7 +2154,8 @@ var _ = Describe("[vendor:cnv-qe@redhat.com][level:component]DataVolume tests",
|
||||
},
|
||||
},
|
||||
}
|
||||
dataVolume := createDataVolumeForImport(f, spec)
|
||||
dataVolume := createLabeledDataVolumeForImport(f, spec,
|
||||
map[string]string{common.PvcUseStorageProfileLabel: webhookRenderingLabel})
|
||||
|
||||
By("verifying event occurred")
|
||||
Eventually(func() bool {
|
||||
@ -2128,7 +2163,7 @@ var _ = Describe("[vendor:cnv-qe@redhat.com][level:component]DataVolume tests",
|
||||
events, err := f.RunKubectlCommand("get", "events", "-n", dataVolume.Namespace, "--field-selector=involvedObject.kind=DataVolume")
|
||||
if err == nil {
|
||||
fmt.Fprintf(GinkgoWriter, "%s", events)
|
||||
return strings.Contains(events, controller.ErrClaimNotValid) && strings.Contains(events, dvc.MessageErrStorageClassNotFound)
|
||||
return verifyEvent(events)
|
||||
}
|
||||
fmt.Fprintf(GinkgoWriter, "ERROR: %s\n", err.Error())
|
||||
return false
|
||||
@ -2146,9 +2181,14 @@ var _ = Describe("[vendor:cnv-qe@redhat.com][level:component]DataVolume tests",
|
||||
By("verifying pvc created")
|
||||
_, err = utils.WaitForPVC(f.K8sClient, dataVolume.Namespace, dataVolume.Name)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
},
|
||||
Entry("[test_id:8383] (controller rendering)", "false", verifyControllerRenderingNoDefaultScEvent),
|
||||
Entry("[test_id:XXXX] (webhook rendering)", Serial, "true", verifyWebhookRenderingEvent),
|
||||
)
|
||||
|
||||
DescribeTable("Import recovers when user adds accessModes to profile", func(webhookRenderingLabel string, verifyEvent func(string) bool) {
|
||||
updateWebhookPvcRendering(webhookRenderingLabel)
|
||||
|
||||
It("[test_id:5913]Import recovers when user adds accessModes to profile", func() {
|
||||
// assumes local is available and has no volumeMode
|
||||
storageProfileName := findStorageProfileWithoutAccessModes(f.CrClient)
|
||||
By(fmt.Sprintf("creating new datavolume %s without accessModes", dataVolumeName))
|
||||
@ -2163,7 +2203,8 @@ var _ = Describe("[vendor:cnv-qe@redhat.com][level:component]DataVolume tests",
|
||||
},
|
||||
},
|
||||
}
|
||||
dataVolume := createDataVolumeForImport(f, spec)
|
||||
dataVolume := createLabeledDataVolumeForImport(f, spec,
|
||||
map[string]string{common.PvcUseStorageProfileLabel: webhookRenderingLabel})
|
||||
|
||||
By("verifying pvc not created")
|
||||
_, err := utils.FindPVC(f.K8sClient, dataVolume.Namespace, dataVolume.Name)
|
||||
@ -2175,7 +2216,7 @@ var _ = Describe("[vendor:cnv-qe@redhat.com][level:component]DataVolume tests",
|
||||
events, err := f.RunKubectlCommand("get", "events", "-n", dataVolume.Namespace, "--field-selector=involvedObject.kind=DataVolume")
|
||||
if err == nil {
|
||||
fmt.Fprintf(GinkgoWriter, "%s", events)
|
||||
return strings.Contains(events, controller.ErrClaimNotValid) && strings.Contains(events, "DataVolume.storage spec is missing accessMode and volumeMode, cannot get access mode from StorageProfile")
|
||||
return verifyEvent(events)
|
||||
}
|
||||
fmt.Fprintf(GinkgoWriter, "ERROR: %s\n", err.Error())
|
||||
return false
|
||||
@ -2195,7 +2236,10 @@ var _ = Describe("[vendor:cnv-qe@redhat.com][level:component]DataVolume tests",
|
||||
|
||||
By("Restore the profile")
|
||||
updateStorageProfileSpec(f.CrClient, storageProfileName, *originalProfileSpec)
|
||||
})
|
||||
},
|
||||
Entry("[test_id:5913] (controller rendering)", "false", verifyControllerRenderingEvent),
|
||||
Entry("[test_id:XXXX] (webhook rendering)", Serial, "true", verifyWebhookRenderingEvent),
|
||||
)
|
||||
|
||||
It("[test_id:6483]Import pod should not have size corrected on block", func() {
|
||||
SetFilesystemOverhead(f, "0.50", "0.50")
|
||||
@ -2522,28 +2566,13 @@ var _ = Describe("[vendor:cnv-qe@redhat.com][level:component]DataVolume tests",
|
||||
})
|
||||
pvName = ""
|
||||
}
|
||||
|
||||
DisableWebhookPvcRendering(f.CrClient)
|
||||
})
|
||||
|
||||
DescribeTable("import DV using StorageSpec without AccessModes, PVC is created only when", func(scName string, scFunc func(string)) {
|
||||
if utils.IsDefaultSCNoProvisioner() {
|
||||
Skip("Default storage class has no provisioner. The new storage class won't work")
|
||||
}
|
||||
|
||||
By(fmt.Sprintf("verifying no storage class %s", testScName))
|
||||
_, err := f.K8sClient.StorageV1().StorageClasses().Get(context.TODO(), scName, metav1.GetOptions{})
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
By(fmt.Sprintf("creating new datavolume %s with StorageClassName %s", dataVolumeName, scName))
|
||||
dataVolume := utils.NewDataVolumeWithHTTPImportAndStorageSpec(
|
||||
dataVolumeName, "100Mi", fmt.Sprintf(utils.TinyCoreQcow2URL, f.CdiInstallNs))
|
||||
dataVolume.Spec.Storage.StorageClassName = ptr.To[string](scName)
|
||||
dataVolume.Spec.Storage.AccessModes = nil
|
||||
|
||||
dataVolume, err = utils.CreateDataVolumeFromDefinition(f.CdiClient, f.Namespace.Name, dataVolume)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
verifyControllerRenderingEventAndConditions := func(dv *cdiv1.DataVolume) {
|
||||
By("verifying event occurred")
|
||||
f.ExpectEvent(dataVolume.Namespace).Should(And(ContainSubstring(controller.ErrClaimNotValid), ContainSubstring(dvc.MessageErrStorageClassNotFound)))
|
||||
f.ExpectEvent(dv.Namespace).Should(And(ContainSubstring(controller.ErrClaimNotValid), ContainSubstring(dvc.MessageErrStorageClassNotFound)))
|
||||
|
||||
By("verifying conditions")
|
||||
boundCondition := &cdiv1.DataVolumeCondition{
|
||||
@ -2558,7 +2587,49 @@ var _ = Describe("[vendor:cnv-qe@redhat.com][level:component]DataVolume tests",
|
||||
Message: dvc.MessageErrStorageClassNotFound,
|
||||
Reason: controller.ErrClaimNotValid,
|
||||
}
|
||||
utils.WaitForConditions(f, dataVolume.Name, f.Namespace.Name, timeout, pollingInterval, boundCondition, readyCondition)
|
||||
utils.WaitForConditions(f, dv.Name, f.Namespace.Name, timeout, pollingInterval, boundCondition, readyCondition)
|
||||
}
|
||||
|
||||
verifyWebhookRenderingEventAndConditions := func(dv *cdiv1.DataVolume) {
|
||||
By("verifying event occurred")
|
||||
f.ExpectEvent(dv.Namespace).Should(And(ContainSubstring(controller.NotFound), ContainSubstring("No PVC found")))
|
||||
|
||||
By("verifying conditions")
|
||||
boundCondition := &cdiv1.DataVolumeCondition{
|
||||
Type: cdiv1.DataVolumeBound,
|
||||
Status: v1.ConditionUnknown,
|
||||
Message: "No PVC found",
|
||||
Reason: controller.NotFound,
|
||||
}
|
||||
readyCondition := &cdiv1.DataVolumeCondition{
|
||||
Type: cdiv1.DataVolumeReady,
|
||||
Status: v1.ConditionFalse,
|
||||
}
|
||||
utils.WaitForConditions(f, dv.Name, f.Namespace.Name, timeout, pollingInterval, boundCondition, readyCondition)
|
||||
}
|
||||
|
||||
DescribeTable("import DV using StorageSpec without AccessModes, PVC is created only when", func(webhookRenderingLabel, scName string, dvFunc func(*cdiv1.DataVolume), scFunc func(string)) {
|
||||
if utils.IsDefaultSCNoProvisioner() {
|
||||
Skip("Default storage class has no provisioner. The new storage class won't work")
|
||||
}
|
||||
|
||||
updateWebhookPvcRendering(webhookRenderingLabel)
|
||||
|
||||
By(fmt.Sprintf("verifying no storage class %s", testScName))
|
||||
_, err := f.K8sClient.StorageV1().StorageClasses().Get(context.TODO(), scName, metav1.GetOptions{})
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
By(fmt.Sprintf("creating new datavolume %s with StorageClassName %s", dataVolumeName, scName))
|
||||
dataVolume := utils.NewDataVolumeWithHTTPImportAndStorageSpec(
|
||||
dataVolumeName, "100Mi", fmt.Sprintf(utils.TinyCoreQcow2URL, f.CdiInstallNs))
|
||||
dataVolume.Labels = map[string]string{common.PvcUseStorageProfileLabel: webhookRenderingLabel}
|
||||
dataVolume.Spec.Storage.StorageClassName = ptr.To[string](scName)
|
||||
dataVolume.Spec.Storage.AccessModes = nil
|
||||
|
||||
dataVolume, err = utils.CreateDataVolumeFromDefinition(f.CdiClient, f.Namespace.Name, dataVolume)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
dvFunc(dataVolume)
|
||||
|
||||
By("verifying pvc not created")
|
||||
_, err = utils.FindPVC(f.K8sClient, dataVolume.Namespace, dataVolume.Name)
|
||||
@ -2576,9 +2647,12 @@ var _ = Describe("[vendor:cnv-qe@redhat.com][level:component]DataVolume tests",
|
||||
err = utils.WaitForDataVolumePhase(f, dataVolume.Namespace, cdiv1.Succeeded, dataVolume.Name)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
},
|
||||
Entry("[test_id:9922]the storage class is created", testScName, createStorageClass),
|
||||
Entry("[test_id:9924]PV with the SC name is created", testScName, createPV),
|
||||
Entry("[test_id:9925]PV with the SC name (\"\" blank) is created", "", createPV),
|
||||
Entry("[test_id:9922]the storage class is created (controller rendering)", "false", testScName, verifyControllerRenderingEventAndConditions, createStorageClass),
|
||||
Entry("[test_id:9924]PV with the SC name is created (controller rendering)", "false", testScName, verifyControllerRenderingEventAndConditions, createPV),
|
||||
Entry("[test_id:9925]PV with the SC name (\"\" blank) is created (controller rendering)", "false", "", verifyControllerRenderingEventAndConditions, createPV),
|
||||
Entry("[test_id:XXXX]the storage class is created (webhook rendering)", Serial, "true", testScName, verifyWebhookRenderingEventAndConditions, createStorageClass),
|
||||
Entry("[test_id:XXXX]PV with the SC name is created (webhook rendering)", Serial, "true", testScName, verifyWebhookRenderingEventAndConditions, createPV),
|
||||
Entry("[test_id:XXXX]PV with the SC name (\"\" blank) is created (webhook rendering)", Serial, "true", "", verifyWebhookRenderingEventAndConditions, createPV),
|
||||
)
|
||||
|
||||
newDataVolumeWithStorageSpec := func(scName string) *cdiv1.DataVolume {
|
||||
@ -3292,3 +3366,28 @@ func SetFilesystemOverhead(f *framework.Framework, globalOverhead, scOverhead st
|
||||
return config.Status.FilesystemOverhead.StorageClass[defaultSCName] == cdiv1.Percent(globalOverhead)
|
||||
}, timeout, pollingInterval).Should(BeTrue(), "CDIConfig filesystem overhead wasn't set")
|
||||
}
|
||||
|
||||
func EnableWebhookPvcRendering(c client.Client) {
|
||||
By("enabling WebhookPvcRendering feature gate")
|
||||
_, err := utils.EnableFeatureGate(c, featuregates.WebhookPvcRendering)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Eventually(func() error {
|
||||
whc := &admissionregistrationv1.MutatingWebhookConfiguration{}
|
||||
return c.Get(context.TODO(), types.NamespacedName{Name: "cdi-api-pvc-mutate"}, whc)
|
||||
}, timeout, pollingInterval).ShouldNot(HaveOccurred())
|
||||
}
|
||||
|
||||
func DisableWebhookPvcRendering(c client.Client) {
|
||||
enabled, err := featuregates.IsWebhookPvcRenderingEnabled(c)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
if enabled {
|
||||
By("disabling WebhookPvcRendering feature gate")
|
||||
_, err := utils.DisableFeatureGate(c, featuregates.WebhookPvcRendering)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
Eventually(func() bool {
|
||||
whc := &admissionregistrationv1.MutatingWebhookConfiguration{}
|
||||
err := c.Get(context.TODO(), types.NamespacedName{Name: "cdi-api-pvc-mutate"}, whc)
|
||||
return err != nil && k8serrors.IsNotFound(err)
|
||||
}, timeout, pollingInterval).Should(BeTrue())
|
||||
}
|
||||
|
@ -1619,10 +1619,20 @@ var _ = Describe("Import populator", func() {
|
||||
By("Delete import population PVC")
|
||||
err = f.DeletePVC(pvc)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
tests.DisableWebhookPvcRendering(f.CrClient)
|
||||
})
|
||||
|
||||
DescribeTable("should import fileSystem PVC", func(expectedMD5 string, volumeImportSourceFunc func(cdiv1.DataVolumeContentType, bool) error, preallocation bool) {
|
||||
DescribeTable("should import fileSystem PVC", func(expectedMD5 string, volumeImportSourceFunc func(cdiv1.DataVolumeContentType, bool) error, preallocation, webhookRendering bool) {
|
||||
pvc = importPopulationPVCDefinition()
|
||||
|
||||
if webhookRendering {
|
||||
tests.EnableWebhookPvcRendering(f.CrClient)
|
||||
controller.AddLabel(pvc, common.PvcUseStorageProfileLabel, "true")
|
||||
// Unset AccessModes which will be set by the webhook rendering
|
||||
pvc.Spec.AccessModes = nil
|
||||
}
|
||||
|
||||
pvc = f.CreateScheduledPVCFromDefinition(pvc)
|
||||
err = volumeImportSourceFunc(cdiv1.DataVolumeKubeVirt, preallocation)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
@ -1672,16 +1682,17 @@ var _ = Describe("Import populator", func() {
|
||||
return err != nil && k8serrors.IsNotFound(err)
|
||||
}, timeout, pollingInterval).Should(BeTrue())
|
||||
},
|
||||
Entry("with HTTP image and preallocation", utils.TinyCoreMD5, createHTTPImportPopulatorCR, true),
|
||||
Entry("with HTTP image without preallocation", utils.TinyCoreMD5, createHTTPImportPopulatorCR, false),
|
||||
Entry("with Registry image and preallocation", utils.TinyCoreMD5, createRegistryImportPopulatorCR, true),
|
||||
Entry("with Registry image without preallocation", utils.TinyCoreMD5, createRegistryImportPopulatorCR, false),
|
||||
Entry("with ImageIO image with preallocation", Serial, utils.ImageioMD5, createImageIOImportPopulatorCR, true),
|
||||
Entry("with ImageIO image without preallocation", Serial, utils.ImageioMD5, createImageIOImportPopulatorCR, false),
|
||||
Entry("with VDDK image with preallocation", utils.VcenterMD5, createVDDKImportPopulatorCR, true),
|
||||
Entry("with VDDK image without preallocation", utils.VcenterMD5, createVDDKImportPopulatorCR, false),
|
||||
Entry("with Blank image with preallocation", utils.BlankMD5, createBlankImportPopulatorCR, true),
|
||||
Entry("with Blank image without preallocation", utils.BlankMD5, createBlankImportPopulatorCR, false),
|
||||
Entry("with HTTP image and preallocation", utils.TinyCoreMD5, createHTTPImportPopulatorCR, true, false),
|
||||
Entry("with HTTP image without preallocation", utils.TinyCoreMD5, createHTTPImportPopulatorCR, false, false),
|
||||
Entry("with HTTP image and preallocation, with incomplete PVC webhook rendering", Serial, utils.TinyCoreMD5, createHTTPImportPopulatorCR, true, true),
|
||||
Entry("with Registry image and preallocation", utils.TinyCoreMD5, createRegistryImportPopulatorCR, true, false),
|
||||
Entry("with Registry image without preallocation", utils.TinyCoreMD5, createRegistryImportPopulatorCR, false, false),
|
||||
Entry("with ImageIO image with preallocation", Serial, utils.ImageioMD5, createImageIOImportPopulatorCR, true, false),
|
||||
Entry("with ImageIO image without preallocation", Serial, utils.ImageioMD5, createImageIOImportPopulatorCR, false, false),
|
||||
Entry("with VDDK image with preallocation", utils.VcenterMD5, createVDDKImportPopulatorCR, true, false),
|
||||
Entry("with VDDK image without preallocation", utils.VcenterMD5, createVDDKImportPopulatorCR, false, false),
|
||||
Entry("with Blank image with preallocation", utils.BlankMD5, createBlankImportPopulatorCR, true, false),
|
||||
Entry("with Blank image without preallocation", utils.BlankMD5, createBlankImportPopulatorCR, false, false),
|
||||
)
|
||||
|
||||
DescribeTable("should import Block PVC", func(expectedMD5 string, volumeImportSourceFunc func(cdiv1.DataVolumeContentType, bool) error) {
|
||||
|
Loading…
Reference in New Issue
Block a user