Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create PVC if possible even if the StorageClass is missing #2683

Merged
merged 2 commits into from
May 2, 2023

Conversation

arnongilboa
Copy link
Collaborator

@arnongilboa arnongilboa commented Apr 4, 2023

What this PR does / why we need it:
When PVC is created with storageClassName and the SC is not found, k8s looks for PV with the storageClassName for satisfying this claim. In this case k8s supports also a blank (“”, not the nil default one) storageClassName. To support this behavior we added:

  • DV controller support for this flow (for both “” and non-existing SC)
  • Condition update and event when StorageSpec PVC rendering errors and PVC is not created (e.g. missing both AccessModes and SC/PV)
  • PVC is created even if a satisfying SC/PV doesn't exist if spec.pvc/storage AccessModes is set (otherwise k8s PVC validation fails). PVC/DV phase will be Pending until a satisfying SC/PV is found
  • PV watch to reconcile DVs waiting for the PV storageClassName
  • PV storageClassName indexer, so we can list the relevant PVs

Which issue(s) this PR fixes (optional, in fixes #<issue number>(, fixes #<issue_number>, ...) format, will close the issue(s) when PR gets merged):
Fixes #2186

Special notes for your reviewer:

Release note:

Create PVC if possible even if the StorageClass is missing; Update DV condition fire and event on PVC rendering error

@kubevirt-bot kubevirt-bot added dco-signoff: yes Indicates the PR's author has DCO signed all their commits. do-not-merge/release-note-label-needed Indicates that a PR should not merge because it's missing one of the release note labels. size/L labels Apr 4, 2023
@kubevirt-bot kubevirt-bot added release-note Denotes a PR that will be considered when it comes time to generate release notes. and removed do-not-merge/release-note-label-needed Indicates that a PR should not merge because it's missing one of the release note labels. labels Apr 4, 2023
@arnongilboa arnongilboa force-pushed the dv_sc_validation branch 2 times, most recently from 86241ca to d88d20f Compare April 7, 2023 14:56
Copy link
Member

@mhenriks mhenriks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

storageClass is optional on a PV

user may ask for storageClass "" on a pvc and they will get a PV with no storageclass (assuming other param match of course)

causes = validateNameLength(spec.ManagedDataSource, validation.DNS1035LabelMaxLength)
if len(causes) > 0 {
if cause := validateNameLength(spec.ManagedDataSource, validation.DNS1035LabelMaxLength); cause != nil {
causes = append(causes, *cause)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any functional difference in the diff of this file or just style?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we moved from returning a slice of causes to a single cause in validateNameLength. This just changes the way that is handled, but functionally does look the same.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, the previous one was a bit clumsy.

if *sc == "" {
return &metav1.StatusCause{
Type: metav1.CauseTypeFieldValueInvalid,
Message: fmt.Sprintf("storageClassName is empty"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"" storage class name is valid

apiVersion: v1
kind: PersistentVolume
metadata:
  name: host-volume
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/mnt/data"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-pvc
spec:
  volumeMode: Filesystem
  storageClassName: ""
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
✗ k get pvc
selecting docker as container runtime
NAME     STATUS   VOLUME        CAPACITY   ACCESS MODES   STORAGECLASS   AGE
my-pvc   Bound    host-volume   10Gi       RWO                           3s

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So had this discussion with Arnon, if you read this, it does say "" is equivalent to missing, due to historical reasons, but having a blank string is sort of gray at this point. This is fixing a reported issue where someone is passing a blank storage class by accident. I think we should disallow blank, and if someone wants to use the default, they can omit the field.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dunno, this pr provides no way to explicitly bind to "" storageclass . That combined with the fact that in 1.26 "" on PVC will bind to default storage class if exists, makes me believe that we should leave things as-is OR allow updating the storageclass on the DV as you can do with PVCs in 1.26

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finally tried this out on a 1.26 cluster. Apparently, I misread the doc. "" will not bind to the default storageclass. And pvc with "" storageClassName can't be updated.

Nevertheless because "" is a legit value I don't think we should disallow it

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that "" is legit value. Fixing accordingly. Keeping the style changes, as the previous code (my lines) is quit ugly.

@arnongilboa arnongilboa changed the title Validate DV SC; update conditions if SC is missing Allow creating PVC if there is a satisfying PV but SC is missing Apr 19, 2023
Copy link
Member

@mhenriks mhenriks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is all a little awkward and racy. Remind me why we can't go straight to "create the pvc even if the storage class doesn't exist"?

@@ -338,7 +338,7 @@ func GetStorageClassByName(client client.Client, name *string) (*storagev1.Stora
storageClass := &storagev1.StorageClass{}
if err := client.Get(context.TODO(), types.NamespacedName{Name: *name}, storageClass); err != nil {
klog.V(3).Info("Unable to retrieve storage class", "storage class name", *name)
return nil, errors.New("unable to retrieve storage class")
return nil, errors.New(fmt.Sprintf("unable to retrieve storage class %s", *name))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a big deal, no need to change, but errors.Errorf can be used when string formatting errors

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@@ -122,7 +122,7 @@ func UpdateReadyCondition(conditions []cdiv1.DataVolumeCondition, status corev1.
return updateCondition(conditions, cdiv1.DataVolumeReady, status, message, reason)
}

func updateBoundCondition(conditions []cdiv1.DataVolumeCondition, pvc *corev1.PersistentVolumeClaim, reason string) []cdiv1.DataVolumeCondition {
func updateBoundCondition(conditions []cdiv1.DataVolumeCondition, pvc *corev1.PersistentVolumeClaim, message, reason string) []cdiv1.DataVolumeCondition {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a unit test for this change where message != ""?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

@@ -76,6 +76,8 @@ const (
dvPhaseField = "status.phase"

claimRefField = "spec.claimRef"

claimStorageClassField = "spec.claimStorageClass"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spec.storageClassName is the json property and the standard naming scheme we use

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

return
}
for _, dv := range dvList.Items {
storage := dv.Spec.Storage
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about dv.spec.pvc?

Copy link
Collaborator Author

@arnongilboa arnongilboa Apr 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not needed as we create the pvc even if the storage class doesn't exist if AccessModes is set (otherwise k8s PVC validation fails). PVC/DV phase will be Pending until a satisfying SC/PV is found.

@arnongilboa
Copy link
Collaborator Author

This is all a little awkward and racy. Remind me why we can't go straight to "create the pvc even if the storage class doesn't exist"?

We will create the pvc even if the storage class doesn't exist if spec.pvc/storage AccessModes is set (otherwise k8s PVC validation fails). PVC/DV phase will be Pending until a satisfying SC/PV is found.

@arnongilboa arnongilboa changed the title Allow creating PVC if there is a satisfying PV but SC is missing Create PVC if possible even if the StorageClass is missing Apr 24, 2023
@arnongilboa arnongilboa force-pushed the dv_sc_validation branch 3 times, most recently from 0967366 to 58c8536 Compare April 24, 2023 13:34
@mhenriks
Copy link
Member

This is all a little awkward and racy. Remind me why we can't go straight to "create the pvc even if the storage class doesn't exist"?

We will create the pvc even if the storage class doesn't exist if spec.pvc/storage AccessModes is set (otherwise k8s PVC validation fails). PVC/DV phase will be Pending until a satisfying SC/PV is found.

Okay, that makes sense, so what about this in the description, what is the follow up?

TODO in a follow-up PR: use k8s PVC SC/PV pending mechanism and get rid of it in CDI

// Gets either the bound PV or an available satisfying PV
func getPV(c client.Client, pvcSpec *v1.PersistentVolumeClaimSpec, pvcName, pvcNamespace string) (*corev1.PersistentVolume, error) {
if pvcSpec.StorageClassName == nil {
return nil, fmt.Errorf("PVC spec is missing StorageClassName")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really an error? Can't storageclass name be nil for either spec.pvc or spec.storage?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that tested?

Copy link
Collaborator Author

@arnongilboa arnongilboa Apr 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not an error. Fixed. Covered in [test_id:8383]Import fails when no default storage class, and recovers when default is set.

@arnongilboa
Copy link
Collaborator Author

Okay, that makes sense, so what about this in the description, what is the follow up?

TODO in a follow-up PR: use k8s PVC SC/PV pending mechanism and get rid of it in CDI

just forgot to remove it :)

},
table.Entry("[test_id:XXXX]the storage class is created", testScName, createStorageClass),
table.Entry("[test_id:XXXX]PV with the SC name is created", testScName, createPV),
table.Entry("[test_id:XXXX]PV with the SC name (\"\" blank) is created", "", createPV),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_ids: CNV-9922
test_ids: CNV-9924
test_ids: CNV-9925

@mhenriks
Copy link
Member

/test pull-containerized-data-importer-fossa

if pv.Status.Phase == corev1.VolumeBound &&
pv.Spec.ClaimRef != nil &&
pv.Spec.ClaimRef.Name == pvcName &&
pv.Spec.ClaimRef.Namespace == pvcNamespace {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this loop. If a PV exists that meets this criteria, I'm pretty sure it would mean that the target PVC already exists.

I think just the following loop is sufficient

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@arnongilboa arnongilboa force-pushed the dv_sc_validation branch 2 times, most recently from 64d1109 to 57ef88a Compare April 27, 2023 14:44
@mhenriks
Copy link
Member

/approve

@kubevirt-bot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: mhenriks

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@kubevirt-bot kubevirt-bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Apr 28, 2023
Copy link
Member

@awels awels left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good, just a few minor nitpicky things.

causes = validateNameLength(spec.ManagedDataSource, validation.DNS1035LabelMaxLength)
if len(causes) > 0 {
if cause := validateNameLength(spec.ManagedDataSource, validation.DNS1035LabelMaxLength); cause != nil {
causes = append(causes, *cause)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we moved from returning a slice of causes to a single cause in validateNameLength. This just changes the way that is handled, but functionally does look the same.

@@ -1702,7 +1705,7 @@ var _ = Describe("[vendor:cnv-qe@redhat.com][level:component]DataVolume tests",

createDataVolumeForImport := func(f *framework.Framework, storageSpec cdiv1.StorageSpec) *cdiv1.DataVolume {
dataVolume := utils.NewDataVolumeWithHTTPImportAndStorageSpec(
dataVolumeName, "1Gi", fmt.Sprintf(utils.TinyCoreQcow2URLRateLimit, f.CdiInstallNs))
dataVolumeName, "1Gi", fmt.Sprintf(utils.TinyCoreQcow2URL, f.CdiInstallNs))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, there seems to be no reason this was the rate limited url.

tests/datavolume_test.go Show resolved Hide resolved
tests/datavolume_test.go Show resolved Hide resolved
Copy link
Collaborator

@alromeros alromeros left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice job! I have some comments and questions about spec rendering, which I think is getting confusing. Other than that looks good to me.

@@ -151,6 +152,41 @@ func copyStorageAsPvc(log logr.Logger, storage *cdiv1.StorageSpec) *v1.Persisten
return pvcSpec
}

// Update the PVC spec from either the created PVC or an available satisfying PV
func updatePvcSpecFromPvcOrPv(c client.Client, pvcSpec *v1.PersistentVolumeClaimSpec, pvc *v1.PersistentVolumeClaim) error {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see we update the pvcSpec here but I'm struggling to see the cases where this is relevant if the PVC already exists. Why do we need to set VolumeMode and AccessMode if the PVC already has them?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! fixed.

if storageClass == nil {
if err := updatePvcSpecFromPvcOrPv(client, pvcSpec, pvc); err != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this flow is getting confusing, it'd be good to have some comments explaining why are we doing this. I'm also not a fan of the updatePvcSpecFromPvcOrPv name, it's not a must but I think this whole storageClass handling would benefit from being in a separate function.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplified and renamed.

When PVC is created with storageClassName and the SC is not found,
k8s looks for PV with the storageClassName for satisfying this claim.
In this case k8s supports also a blank (“”, not the nil default one)
storageClassName. To support this behavior we added:
-DV controller support for this flow (for both “” and non-existing SC)
-Condition update and event when StorageSpec PVC rendering errors and
 PVC is not created (e.g. missing both AccessModes and SC/PV)
-PVC is created even if a satisfying SC/PV doesn't exist if pvc/storage
 AccessModes is set (otherwise k8s PVC validation fails). PVC/DV phase
 will be Pending until a satisfying SC/PV is found
-PV watch to reconcile DVs waiting for the PV storageClassName
-PV storageClassName indexer, so we can list the relevant PVs

Signed-off-by: Arnon Gilboa <agilboa@redhat.com>
Signed-off-by: Arnon Gilboa <agilboa@redhat.com>
@arnongilboa
Copy link
Collaborator Author

/test pull-cdi-linter

Copy link
Collaborator

@alromeros alromeros left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! just a couple of nitpicks that I think could improve the spec rendering code

@@ -44,85 +44,105 @@ const (
)

// 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) (*v1.PersistentVolumeClaimSpec, error) {
func renderPvcSpec(client client.Client, recorder record.EventRecorder, log logr.Logger, dv *cdiv1.DataVolume, pvc *v1.PersistentVolumeClaim) (*v1.PersistentVolumeClaimSpec, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would look cleaner if we do something like:

	// If the PVC already exists, just return its spec
	if pvc != nil {
		return pvc.Spec.DeepCopy(), nil
	}
	if dv.Spec.PVC != nil {
		return dv.Spec.PVC.DeepCopy(), nil
	}
	if dv.Spec.Storage != nil {
		return pvcFromStorage(client, recorder, log, dv)
	}

pvcFromStorage would require fewer arguments too, wdyt?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It won't work, see the following comment about renderPvcSpecVolumeSize.

pvcSpec = pvc.Spec.DeepCopy()
}

if err := renderPvcSpecVolumeSize(client, dv.Spec, pvcSpec); err != nil {
Copy link
Collaborator

@alromeros alromeros May 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to my previous comment, we are still rendering the volume size for already existing PVCs, which I believe is not necessary. I think this function would be better without any PVC handling.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or do we really want to update the volume size? In that case, are we updating the PVC later to reflect the new pvcSpec?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is necessary to render the volume size for smart clones, as the DV may require volume size greater than the source PVC, so the traget PVC should be expanded.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh nice, wasn't sure about that. Thanks for explaining!


func renderPvcSpecVolumeModeAndAccessModes(client client.Client, recorder record.EventRecorder, log logr.Logger, dv *cdiv1.DataVolume, pvcSpec *v1.PersistentVolumeClaimSpec) error {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe renderVolumeAndAccessModes? I think we could remove the pvcSpec from the name, but renderPvcSpecVolumeAndAccessModes would also work

@alromeros
Copy link
Collaborator

/lgtm

@kubevirt-bot kubevirt-bot added the lgtm Indicates that a PR is ready to be merged. label May 2, 2023
@kubevirt-bot kubevirt-bot merged commit 5d78da3 into kubevirt:main May 2, 2023
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
approved Indicates a PR has been approved by an approver from all required OWNERS files. dco-signoff: yes Indicates the PR's author has DCO signed all their commits. lgtm Indicates that a PR is ready to be merged. release-note Denotes a PR that will be considered when it comes time to generate release notes. size/XL
Projects
None yet
Development

Successfully merging this pull request may close these issues.

No information if storage class can't be detected
6 participants