mirror of
https://github.com/kubevirt/containerized-data-importer.git
synced 2025-06-03 06:30:22 +00:00

* Fix PVC webhook rendering segfault When there is no default StorageClass and no virt default StorageClass, creating a PVC with no StorageClass specified will cause a segfault in the webhook: Error from server (InternalError): error when creating "pvc_render.yaml": Internal error occurred: failed calling webhook "pvc-mutate.cdi.kubevirt.io": failed to call webhook: Post "https://cdi-api.cdi.svc:443/pvc-mutate?timeout=10s": EOF In cdi-apiserver log: panic({0x1b49360?, 0x3295550?}) GOROOT/src/runtime/panic.go:770 +0x132 kubevirt.io/containerized-data-importer/pkg/controller/datavolume. renderPvcSpecVolumeModeAndAccessModesAndStorageClass({0x2164d00, 0xc000156090}, {0x0, 0x0}, 0x0?, 0x0, 0xc0002e86a8, {0x1e09ed8?, 0xc0002e8530?}) pkg/controller/datavolume/util.go:162 +0xb37 renderPvcSpecVolumeModeAndAccessModesAndStorageClass() is used from both DV controller and PVC webhook rendering. In the PVC mutating webhook (cdi-apiserver) we don't have logr.Logger (unlike in cdi-deployment), but klog which has different api, and no reason to log there as we return the failure anyway to the user when she creates the PVC. We also don't have a DV there, and no event recorder. Signed-off-by: Arnon Gilboa <agilboa@redhat.com> * Rephrase rendering errors Signed-off-by: Arnon Gilboa <agilboa@redhat.com> * Add some utests Signed-off-by: Arnon Gilboa <agilboa@redhat.com> --------- Signed-off-by: Arnon Gilboa <agilboa@redhat.com>
687 lines
24 KiB
Go
687 lines
24 KiB
Go
/*
|
|
Copyright 2020 The CDI Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package datavolume
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strconv"
|
|
|
|
"github.com/docker/go-units"
|
|
"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"
|
|
storagev1 "k8s.io/api/storage/v1"
|
|
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/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 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 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 {
|
|
return dv.Spec.PVC.DeepCopy(), nil
|
|
} else if dv.Spec.Storage != nil {
|
|
return pvcFromStorage(client, recorder, log, dv, pvc)
|
|
}
|
|
|
|
return nil, errors.Errorf("datavolume one of {pvc, storage} field is required")
|
|
}
|
|
|
|
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.PvcApplyStorageProfileLabel] != "true"
|
|
|
|
if pvc == nil {
|
|
pvcSpec = copyStorageAsPvc(dv.Spec.Storage)
|
|
if shouldRender {
|
|
if err := renderPvcSpecVolumeModeAndAccessModesAndStorageClass(client, recorder, &log, dv, pvcSpec, dv.Spec.ContentType); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
} else {
|
|
pvcSpec = pvc.Spec.DeepCopy()
|
|
}
|
|
|
|
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 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 && dv != nil {
|
|
keysAndValuesWithDv := append(keysAndValues, "namespace", dv.Namespace, "name", dv.Name)
|
|
log.V(1).Info(msg, keysAndValuesWithDv...)
|
|
}
|
|
}
|
|
|
|
recordEventf := func(eventtype, reason, messageFmt string, args ...interface{}) {
|
|
if recorder != nil && dv != nil {
|
|
recorder.Eventf(dv, eventtype, reason, messageFmt, args...)
|
|
}
|
|
}
|
|
|
|
if dvContentType == cdiv1.DataVolumeArchive {
|
|
if pvcSpec.VolumeMode != nil && *pvcSpec.VolumeMode == v1.PersistentVolumeBlock {
|
|
logInfo("ContentType Archive cannot have block volumeMode")
|
|
recordEventf(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
|
|
}
|
|
if storageClass == nil {
|
|
if err := renderPvcSpecFromAvailablePv(client, pvcSpec); err != nil {
|
|
return err
|
|
}
|
|
// Not even default storageClass on the cluster, cannot apply the defaults, verify spec is ok
|
|
if len(pvcSpec.AccessModes) == 0 {
|
|
logInfo("Cannot set accessMode for new pvc")
|
|
recordEventf(v1.EventTypeWarning, cc.ErrClaimNotValid, MessageErrStorageClassNotFound)
|
|
return ErrStorageClassNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
pvcSpec.StorageClassName = &storageClass.Name
|
|
// given storageClass we can apply defaults if needed
|
|
if (pvcSpec.VolumeMode == nil || *pvcSpec.VolumeMode == "") && len(pvcSpec.AccessModes) == 0 {
|
|
accessModes, volumeMode, err := getDefaultVolumeAndAccessMode(client, storageClass)
|
|
if err != nil {
|
|
logInfo("Cannot set accessMode and volumeMode for new pvc", "Error", err)
|
|
recordEventf(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...)
|
|
pvcSpec.VolumeMode = volumeMode
|
|
} else if len(pvcSpec.AccessModes) == 0 {
|
|
accessModes, err := getDefaultAccessModes(client, storageClass, pvcSpec.VolumeMode)
|
|
if err != nil {
|
|
logInfo("Cannot set accessMode for new pvc", "Error", err)
|
|
recordEventf(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...)
|
|
} else if pvcSpec.VolumeMode == nil || *pvcSpec.VolumeMode == "" {
|
|
volumeMode, err := getDefaultVolumeMode(client, storageClass, pvcSpec.AccessModes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pvcSpec.VolumeMode = volumeMode
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Kubevirt doesn't allow disks smaller than 1MiB. Rejecting for consistency.
|
|
if requestedSize.Value() < units.MiB {
|
|
return errors.Errorf("PVC Spec is not valid - storage size should be at least 1MiB")
|
|
}
|
|
|
|
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] = volumeSize
|
|
}
|
|
|
|
func getName(storageClass *storagev1.StorageClass) string {
|
|
if storageClass != nil {
|
|
return storageClass.Name
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func copyStorageAsPvc(storage *cdiv1.StorageSpec) *v1.PersistentVolumeClaimSpec {
|
|
input := storage.DeepCopy()
|
|
pvcSpec := &v1.PersistentVolumeClaimSpec{
|
|
AccessModes: input.AccessModes,
|
|
Selector: input.Selector,
|
|
Resources: input.Resources,
|
|
VolumeName: input.VolumeName,
|
|
StorageClassName: input.StorageClassName,
|
|
VolumeMode: input.VolumeMode,
|
|
DataSource: input.DataSource,
|
|
DataSourceRef: input.DataSourceRef,
|
|
}
|
|
|
|
return pvcSpec
|
|
}
|
|
|
|
// Renders the PVC spec VolumeMode and AccessModes from an available satisfying PV
|
|
func renderPvcSpecFromAvailablePv(c client.Client, pvcSpec *v1.PersistentVolumeClaimSpec) error {
|
|
if pvcSpec.StorageClassName == nil {
|
|
return nil
|
|
}
|
|
|
|
pvList := &v1.PersistentVolumeList{}
|
|
fields := client.MatchingFields{claimStorageClassNameField: *pvcSpec.StorageClassName}
|
|
if err := c.List(context.TODO(), pvList, fields); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, pv := range pvList.Items {
|
|
pvc := &v1.PersistentVolumeClaim{Spec: *pvcSpec}
|
|
if err := CheckVolumeSatisfyClaim(&pv, pvc); err == nil {
|
|
pvcSpec.VolumeMode = pv.Spec.VolumeMode
|
|
if len(pvcSpec.AccessModes) == 0 {
|
|
pvcSpec.AccessModes = pv.Spec.AccessModes
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getDefaultVolumeAndAccessMode(c client.Client, storageClass *storagev1.StorageClass) ([]v1.PersistentVolumeAccessMode, *v1.PersistentVolumeMode, error) {
|
|
if storageClass == nil {
|
|
return nil, nil, errors.Errorf("no accessMode specified and StorageClass not found")
|
|
}
|
|
|
|
storageProfile := &cdiv1.StorageProfile{}
|
|
err := c.Get(context.TODO(), types.NamespacedName{Name: storageClass.Name}, storageProfile)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(err, "cannot get StorageProfile")
|
|
}
|
|
|
|
if len(storageProfile.Status.ClaimPropertySets) > 0 &&
|
|
len(storageProfile.Status.ClaimPropertySets[0].AccessModes) > 0 {
|
|
accessModes := storageProfile.Status.ClaimPropertySets[0].AccessModes
|
|
volumeMode := storageProfile.Status.ClaimPropertySets[0].VolumeMode
|
|
return accessModes, volumeMode, nil
|
|
}
|
|
|
|
// no accessMode configured on storageProfile
|
|
return nil, nil, errors.Errorf("no accessMode specified in StorageProfile %s", storageProfile.Name)
|
|
}
|
|
|
|
func getDefaultVolumeMode(c client.Client, storageClass *storagev1.StorageClass, pvcAccessModes []v1.PersistentVolumeAccessMode) (*v1.PersistentVolumeMode, error) {
|
|
if storageClass == nil {
|
|
// fallback to k8s defaults
|
|
return nil, nil
|
|
}
|
|
|
|
storageProfile := &cdiv1.StorageProfile{}
|
|
err := c.Get(context.TODO(), types.NamespacedName{Name: storageClass.Name}, storageProfile)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "cannot get StorageProfile")
|
|
}
|
|
if len(storageProfile.Status.ClaimPropertySets) > 0 {
|
|
volumeMode := storageProfile.Status.ClaimPropertySets[0].VolumeMode
|
|
if len(pvcAccessModes) == 0 {
|
|
return volumeMode, nil
|
|
}
|
|
// check for volume mode matching with given pvc access modes
|
|
for _, cps := range storageProfile.Status.ClaimPropertySets {
|
|
for _, accessMode := range cps.AccessModes {
|
|
for _, pvcAccessMode := range pvcAccessModes {
|
|
if accessMode == pvcAccessMode {
|
|
return cps.VolumeMode, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// if not found return default volume mode for the storage class
|
|
return volumeMode, nil
|
|
}
|
|
|
|
// since volumeMode is optional - > gracefully fallback to k8s defaults,
|
|
return nil, nil
|
|
}
|
|
|
|
func getDefaultAccessModes(c client.Client, storageClass *storagev1.StorageClass, pvcVolumeMode *v1.PersistentVolumeMode) ([]v1.PersistentVolumeAccessMode, error) {
|
|
if storageClass == nil {
|
|
return nil, errors.Errorf("no accessMode specified and no StorageProfile")
|
|
}
|
|
|
|
storageProfile := &cdiv1.StorageProfile{}
|
|
err := c.Get(context.TODO(), types.NamespacedName{Name: storageClass.Name}, storageProfile)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "no accessMode specified and cannot get StorageProfile %s", storageClass.Name)
|
|
}
|
|
|
|
if len(storageProfile.Status.ClaimPropertySets) > 0 {
|
|
// check for access modes matching with given pvc volume mode
|
|
defaultAccessModes := []v1.PersistentVolumeAccessMode{}
|
|
for _, cps := range storageProfile.Status.ClaimPropertySets {
|
|
if cps.VolumeMode != nil && pvcVolumeMode != nil && *cps.VolumeMode == *pvcVolumeMode {
|
|
if len(cps.AccessModes) > 0 {
|
|
return cps.AccessModes, nil
|
|
}
|
|
} else if len(cps.AccessModes) > 0 && len(defaultAccessModes) == 0 {
|
|
defaultAccessModes = cps.AccessModes
|
|
}
|
|
}
|
|
// if not found return default access modes for the storage profile
|
|
if len(defaultAccessModes) > 0 {
|
|
return defaultAccessModes, nil
|
|
}
|
|
}
|
|
|
|
// no accessMode configured on storageProfile
|
|
return nil, errors.Errorf("no accessMode specified in StorageProfile %s", storageProfile.Name)
|
|
}
|
|
|
|
// 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)
|
|
|
|
storageClass, err := cc.GetStorageClassByNameWithK8sFallback(context.TODO(), client, storageClassName)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if storageClass == nil {
|
|
log.Info("Target PVC's Storage Class not found")
|
|
return false, nil
|
|
}
|
|
|
|
csiDriver := &storagev1.CSIDriver{}
|
|
|
|
if err := client.Get(context.TODO(), types.NamespacedName{Name: storageClass.Provisioner}, csiDriver); err != nil {
|
|
if !k8serrors.IsNotFound(err) {
|
|
return false, err
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// CheckPVCUsingPopulators returns true if pvc has dataSourceRef and has
|
|
// the usePopulator annotation
|
|
func CheckPVCUsingPopulators(pvc *v1.PersistentVolumeClaim) (bool, error) {
|
|
if pvc.Spec.DataSourceRef == nil {
|
|
return false, nil
|
|
}
|
|
usePopulator, ok := pvc.Annotations[cc.AnnUsePopulator]
|
|
if !ok {
|
|
return false, nil
|
|
}
|
|
boolUsePopulator, err := strconv.ParseBool(usePopulator)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return boolUsePopulator, nil
|
|
}
|
|
|
|
func updateDataVolumeUseCDIPopulator(syncState *dvSyncState) {
|
|
cc.AddAnnotation(syncState.dvMutated, cc.AnnUsePopulator, strconv.FormatBool(syncState.usePopulator))
|
|
}
|
|
|
|
func updateDataVolumeDefaultInstancetypeLabels(client client.Client, syncState *dvSyncState) error {
|
|
// Skip looking anything up if any default instance type labels are already present
|
|
dv := syncState.dvMutated
|
|
for _, defaultInstanceTypeLabel := range cc.DefaultInstanceTypeLabels {
|
|
if _, ok := dv.Labels[defaultInstanceTypeLabel]; ok {
|
|
return nil
|
|
}
|
|
}
|
|
if dv.Spec.Source != nil && dv.Spec.Source.PVC != nil {
|
|
pvc := &v1.PersistentVolumeClaim{}
|
|
key := types.NamespacedName{
|
|
Name: dv.Spec.Source.PVC.Name,
|
|
Namespace: dv.Spec.Source.PVC.Namespace,
|
|
}
|
|
if err := client.Get(context.TODO(), key, pvc); err != nil {
|
|
if k8serrors.IsNotFound(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
copyDefaultInstancetypeLabels(dv, pvc.Labels)
|
|
return nil
|
|
}
|
|
if dv.Spec.Source != nil && dv.Spec.Source.Snapshot != nil {
|
|
snapshot := &snapshotv1.VolumeSnapshot{}
|
|
key := types.NamespacedName{
|
|
Name: dv.Spec.Source.Snapshot.Name,
|
|
Namespace: dv.Spec.Source.Snapshot.Namespace,
|
|
}
|
|
if err := client.Get(context.TODO(), key, snapshot); err != nil {
|
|
if k8serrors.IsNotFound(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
copyDefaultInstancetypeLabels(dv, snapshot.Labels)
|
|
return nil
|
|
}
|
|
if dv.Spec.Source != nil && dv.Spec.Source.Registry != nil {
|
|
pvc := &v1.PersistentVolumeClaim{}
|
|
key := types.NamespacedName{
|
|
Name: dv.Name,
|
|
Namespace: dv.Namespace,
|
|
}
|
|
if err := client.Get(context.TODO(), key, pvc); err != nil {
|
|
if k8serrors.IsNotFound(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
copyDefaultInstancetypeLabels(dv, pvc.Labels)
|
|
return nil
|
|
}
|
|
if dv.Spec.SourceRef != nil && dv.Spec.SourceRef.Namespace != nil && dv.Spec.SourceRef.Kind == cdiv1.DataVolumeDataSource {
|
|
ds := &cdiv1.DataSource{}
|
|
key := types.NamespacedName{
|
|
Name: dv.Spec.SourceRef.Name,
|
|
Namespace: *dv.Spec.SourceRef.Namespace,
|
|
}
|
|
if err := client.Get(context.TODO(), key, ds); err != nil {
|
|
if k8serrors.IsNotFound(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
copyDefaultInstancetypeLabels(dv, ds.Labels)
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func copyDefaultInstancetypeLabels(dataVolume *cdiv1.DataVolume, labels map[string]string) {
|
|
for _, defaultInstancetypeLabel := range cc.DefaultInstanceTypeLabels {
|
|
if v, ok := labels[defaultInstancetypeLabel]; ok {
|
|
cc.AddLabel(dataVolume, defaultInstancetypeLabel, v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func checkDVUsingPopulators(dv *cdiv1.DataVolume) (bool, error) {
|
|
usePopulator, ok := dv.Annotations[cc.AnnUsePopulator]
|
|
if !ok {
|
|
return false, nil
|
|
}
|
|
boolUsePopulator, err := strconv.ParseBool(usePopulator)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return boolUsePopulator, nil
|
|
}
|
|
|
|
func dvBoundOrPopulationInProgress(dataVolume *cdiv1.DataVolume, boundCond *cdiv1.DataVolumeCondition) bool {
|
|
usePopulator, err := checkDVUsingPopulators(dataVolume)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return boundCond.Status == v1.ConditionTrue ||
|
|
(usePopulator && dataVolume.Status.Phase != cdiv1.Pending && dataVolume.Status.Phase != cdiv1.PendingPopulation)
|
|
}
|
|
|
|
func createStorageProfile(name string,
|
|
accessModes []v1.PersistentVolumeAccessMode,
|
|
volumeMode v1.PersistentVolumeMode) *cdiv1.StorageProfile {
|
|
claimPropertySets := []cdiv1.ClaimPropertySet{{
|
|
AccessModes: accessModes,
|
|
VolumeMode: &volumeMode,
|
|
}}
|
|
return createStorageProfileWithClaimPropertySets(name, claimPropertySets)
|
|
}
|
|
|
|
func createStorageProfileWithClaimPropertySets(name string,
|
|
claimPropertySets []cdiv1.ClaimPropertySet) *cdiv1.StorageProfile {
|
|
return createStorageProfileWithCloneStrategy(name, claimPropertySets, nil)
|
|
}
|
|
|
|
func createStorageProfileWithCloneStrategy(name string,
|
|
claimPropertySets []cdiv1.ClaimPropertySet,
|
|
cloneStrategy *cdiv1.CDICloneStrategy) *cdiv1.StorageProfile {
|
|
return &cdiv1.StorageProfile{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
},
|
|
Status: cdiv1.StorageProfileStatus{
|
|
StorageClass: &name,
|
|
ClaimPropertySets: claimPropertySets,
|
|
CloneStrategy: cloneStrategy,
|
|
},
|
|
}
|
|
}
|
|
|
|
func hasAnnOwnedByDataVolume(obj metav1.Object) bool {
|
|
_, ok := obj.GetAnnotations()[AnnOwnedByDataVolume]
|
|
return ok
|
|
}
|
|
|
|
func getAnnOwnedByDataVolume(obj metav1.Object) (string, string, error) {
|
|
val := obj.GetAnnotations()[AnnOwnedByDataVolume]
|
|
return cache.SplitMetaNamespaceKey(val)
|
|
}
|
|
|
|
func setAnnOwnedByDataVolume(dest, obj metav1.Object) error {
|
|
key, err := cache.MetaNamespaceKeyFunc(obj)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if dest.GetAnnotations() == nil {
|
|
dest.SetAnnotations(make(map[string]string))
|
|
}
|
|
dest.GetAnnotations()[AnnOwnedByDataVolume] = key
|
|
|
|
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
|
|
func CheckVolumeSatisfyClaim(volume *v1.PersistentVolume, claim *v1.PersistentVolumeClaim) error {
|
|
requestedQty := claim.Spec.Resources.Requests[v1.ResourceStorage]
|
|
requestedSize := requestedQty.Value()
|
|
|
|
// check if PV's DeletionTimeStamp is set, if so, return error.
|
|
if volume.ObjectMeta.DeletionTimestamp != nil {
|
|
return fmt.Errorf("the volume is marked for deletion %q", volume.Name)
|
|
}
|
|
|
|
volumeQty := volume.Spec.Capacity[v1.ResourceStorage]
|
|
volumeSize := volumeQty.Value()
|
|
if volumeSize < requestedSize {
|
|
return fmt.Errorf("requested PV is too small")
|
|
}
|
|
|
|
// this differs from pv_controller, be loose on storage class if not specified
|
|
requestedClass := storagehelpers.GetPersistentVolumeClaimClass(claim)
|
|
if requestedClass != "" && storagehelpers.GetPersistentVolumeClass(volume) != requestedClass {
|
|
return fmt.Errorf("storageClassName does not match")
|
|
}
|
|
|
|
if storagehelpers.CheckVolumeModeMismatches(&claim.Spec, &volume.Spec) {
|
|
return fmt.Errorf("incompatible volumeMode")
|
|
}
|
|
|
|
if !storagehelpers.CheckAccessModes(claim, volume) {
|
|
return fmt.Errorf("incompatible accessMode")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getReconcileRequest(obj client.Object) reconcile.Request {
|
|
return reconcile.Request{NamespacedName: client.ObjectKeyFromObject(obj)}
|
|
}
|