Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion pkg/clioptions/clusterdiscovery/csi.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ func InitCSITests() error {
ocpDrivers := sets.New[string]()
upstreamDrivers := sets.New[string]()

upstreamManifestList := os.Getenv(CSIManifestEnvVar)

// Register OCP CSI suites that do not require an OCP-specific manifest whenever
// External Storage tests will be defined. Must run before AddDriverDefinition.
if upstreamManifestList != "" {
csi.RegisterAlwaysOnCSISuites()
}

// Load OCP specific tests first, because AddOpenShiftCSITests() modifies global list of
// testsuites.CSISuites used by AddDriverDefinition() below.
ocpManifestList := os.Getenv(OCPManifestEnvVar)
Expand All @@ -39,7 +47,6 @@ func InitCSITests() error {
}
}

upstreamManifestList := os.Getenv(CSIManifestEnvVar)
if upstreamManifestList != "" {
manifests := strings.Split(upstreamManifestList, ",")
for _, manifest := range manifests {
Expand Down
11 changes: 11 additions & 0 deletions test/extended/storage/csi/csi.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ package csi
import (
"fmt"
"os"
"sync"

"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/kubernetes/test/e2e/storage/testsuites"
)

var registerAlwaysOnCSISuites sync.Once

const (
// The defaul timeout for the LUN stress test.
DefaultLUNStressTestTimeout = "40m"
Expand Down Expand Up @@ -80,3 +83,11 @@ func AddOpenShiftCSITests(filename string) (string, error) {
testsuites.CSISuites = append(testsuites.CSISuites, initSCSILUNOverflowCSISuite(cfg.LUNStressTest))
return cfg.Driver, nil
}

// RegisterAlwaysOnCSISuites appends OpenShift CSI suites that do not need an OCP-specific
// driver manifest. Call before external.AddDriverDefinition. Safe to call once per process.
func RegisterAlwaysOnCSISuites() {
registerAlwaysOnCSISuites.Do(func() {
testsuites.CSISuites = append(testsuites.CSISuites, initPVCCloneLargerCSISuite)
})
}
180 changes: 180 additions & 0 deletions test/extended/storage/csi/pvc_clone_larger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package csi

import (
"context"
"fmt"

g "github.com/onsi/ginkgo/v2"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientset "k8s.io/client-go/kubernetes"
e2e "k8s.io/kubernetes/test/e2e/framework"
e2enode "k8s.io/kubernetes/test/e2e/framework/node"
e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
e2epv "k8s.io/kubernetes/test/e2e/framework/pv"
e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper"
e2evolume "k8s.io/kubernetes/test/e2e/framework/volume"
storageframework "k8s.io/kubernetes/test/e2e/storage/framework"
"k8s.io/kubernetes/test/e2e/storage/testsuites"
storageutils "k8s.io/kubernetes/test/e2e/storage/utils"
admissionapi "k8s.io/pod-security-admission/api"
)

func initPVCCloneLargerCSISuite() storageframework.TestSuite {
return &pvcCloneLargerCSISuite{
tsInfo: storageframework.TestSuiteInfo{
Name: "OpenShift CSI extended - CSI Clone",
TestPatterns: []storageframework.TestPattern{
storageframework.DefaultFsDynamicPV,
storageframework.BlockVolModeDynamicPV,
},
SupportedSizeRange: e2evolume.SizeRange{
Min: "1Mi",
},
},
}
}

// pvcCloneLargerCSISuite covers cloning a PVC into a larger destination volume
// for both filesystem and raw block volume modes.
type pvcCloneLargerCSISuite struct {
tsInfo storageframework.TestSuiteInfo
}

var _ storageframework.TestSuite = &pvcCloneLargerCSISuite{}

func (s *pvcCloneLargerCSISuite) GetTestSuiteInfo() storageframework.TestSuiteInfo {
return s.tsInfo
}

func (s *pvcCloneLargerCSISuite) SkipUnsupportedTests(driver storageframework.TestDriver, pattern storageframework.TestPattern) {
dInfo := driver.GetDriverInfo()
if !dInfo.Capabilities[storageframework.CapPVCDataSource] {
e2eskipper.Skipf("Driver %q does not support cloning - skipping", dInfo.Name)
}
if pattern.VolMode == v1.PersistentVolumeBlock && !dInfo.Capabilities[storageframework.CapBlock] {
e2eskipper.Skipf("Driver %s doesn't support %v -- skipping", dInfo.Name, pattern.VolMode)
}
// Cloning to a larger filesystem volume requires the driver to expand the
// filesystem when presenting the volume (same requirement as snapshot restore).
if pattern.VolMode != v1.PersistentVolumeBlock && dInfo.Capabilities[storageframework.CapFSResizeFromSourceNotSupported] {
e2eskipper.Skipf("Driver %q does not support filesystem resizing from source - skipping", dInfo.Name)
}
}

func (s *pvcCloneLargerCSISuite) DefineTests(driver storageframework.TestDriver, pattern storageframework.TestPattern) {
f := e2e.NewFrameworkWithCustomTimeouts("csi-clone-larger", storageframework.GetDriverTimeouts(driver))
f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged

dInfo := driver.GetDriverInfo()

g.It("should provision volume with pvc data source larger than original volume", func(ctx context.Context) {
dDriver, ok := driver.(storageframework.DynamicPVTestDriver)
if !ok {
e2eskipper.Skipf("Driver %q does not support dynamic provisioning - skipping", dInfo.Name)
}

config := driver.PrepareTest(ctx, f)
cs := config.Framework.ClientSet

// Some drivers cannot clone across topology segments; pin source and clone to one.
pinToDriverTopology(ctx, &config.ClientNodeSelection, cs, dInfo)

testConfig := storageframework.ConvertTestConfig(config)
expectedContent := fmt.Sprintf("Hello from namespace %s", f.Namespace.Name)
contentTest := func(pvcName string) e2evolume.Test {
return e2evolume.Test{
Volume: *storageutils.CreateVolumeSource(pvcName, false /* readOnly */),
Mode: pattern.VolMode,
File: "index.html",
ExpectedContent: expectedContent,
}
}

claimSize, err := storageutils.GetSizeRangesIntersection(s.GetTestSuiteInfo().SupportedSizeRange, dInfo.SupportedSizeRange)
e2e.ExpectNoError(err, "determine intersection of test and driver size ranges")

sc := dDriver.GetDynamicProvisionStorageClass(ctx, config, pattern.FsType)
if sc == nil {
e2eskipper.Skipf("Driver %q does not define Dynamic Provision StorageClass - skipping", dInfo.Name)
}

g.By("Creating StorageClass and source PVC with test data")
testsuites.SetupStorageClass(ctx, cs, sc)
sourcePVC, err := cs.CoreV1().PersistentVolumeClaims(f.Namespace.Name).Create(ctx,
e2epv.MakePersistentVolumeClaim(e2epv.PersistentVolumeClaimConfig{
ClaimSize: claimSize,
StorageClassName: &sc.Name,
VolumeMode: &pattern.VolMode,
}, f.Namespace.Name),
metav1.CreateOptions{})
e2e.ExpectNoError(err)
g.DeferCleanup(func(ctx context.Context) {
_ = cs.CoreV1().PersistentVolumeClaims(sourcePVC.Namespace).Delete(ctx, sourcePVC.Name, metav1.DeleteOptions{})
})
e2evolume.InjectContent(ctx, f, testConfig, nil, "", []e2evolume.Test{contentTest(sourcePVC.Name)})

g.By("Recording source PVC capacity and requesting a larger clone")
sourcePVC, err = cs.CoreV1().PersistentVolumeClaims(sourcePVC.Namespace).Get(ctx, sourcePVC.Name, metav1.GetOptions{})
e2e.ExpectNoError(err, "Failed to get source PVC: %v", err)
storageRequest := resource.NewQuantity(sourcePVC.Status.Capacity.Storage().Value(), resource.BinarySI)
storageRequest.Add(resource.MustParse("1Gi"))
largerSize := storageRequest.String()
Comment on lines +119 to +123

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the source capacity is non-zero before deriving the clone size.

If Status.Capacity is missing (unbound/late-status PVC), Storage() yields 0 and largerSize silently becomes exactly 1Gi, so the test no longer verifies "larger than the source" — it may even request less than the source. A cheap guard keeps the intent explicit.

🛡️ Proposed guard
 	e2e.ExpectNoError(err, "Failed to get source PVC: %v", err)
-	storageRequest := resource.NewQuantity(sourcePVC.Status.Capacity.Storage().Value(), resource.BinarySI)
+	sourceCapacity := sourcePVC.Status.Capacity.Storage().Value()
+	o.Expect(sourceCapacity).To(o.BeNumerically(">", 0), "source PVC has no reported capacity")
+	storageRequest := resource.NewQuantity(sourceCapacity, resource.BinarySI)
 	storageRequest.Add(resource.MustParse("1Gi"))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
sourcePVC, err = cs.CoreV1().PersistentVolumeClaims(sourcePVC.Namespace).Get(ctx, sourcePVC.Name, metav1.GetOptions{})
e2e.ExpectNoError(err, "Failed to get source PVC: %v", err)
storageRequest := resource.NewQuantity(sourcePVC.Status.Capacity.Storage().Value(), resource.BinarySI)
storageRequest.Add(resource.MustParse("1Gi"))
largerSize := storageRequest.String()
sourcePVC, err = cs.CoreV1().PersistentVolumeClaims(sourcePVC.Namespace).Get(ctx, sourcePVC.Name, metav1.GetOptions{})
e2e.ExpectNoError(err, "Failed to get source PVC: %v", err)
sourceCapacity := sourcePVC.Status.Capacity.Storage().Value()
o.Expect(sourceCapacity).To(o.BeNumerically(">", 0), "source PVC has no reported capacity")
storageRequest := resource.NewQuantity(sourceCapacity, resource.BinarySI)
storageRequest.Add(resource.MustParse("1Gi"))
largerSize := storageRequest.String()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/extended/storage/csi/pvc_clone_larger.go` around lines 119 - 123, In the
PVC clone sizing flow after retrieving sourcePVC, validate that
sourcePVC.Status.Capacity.Storage() is non-zero before creating storageRequest.
Fail the test with an explicit assertion if capacity is missing or zero, then
preserve the existing largerSize calculation for valid capacities.


clonePVC := sourcePVC.DeepCopy()
clonePVC.ObjectMeta = metav1.ObjectMeta{
GenerateName: "pvc-",
Namespace: sourcePVC.Namespace,
}
clonePVC.Status = v1.PersistentVolumeClaimStatus{}
clonePVC.Spec.VolumeName = ""
clonePVC.Spec.Resources.Requests = v1.ResourceList{v1.ResourceStorage: *storageRequest}
clonePVC.Spec.DataSourceRef = &v1.TypedObjectReference{
Kind: "PersistentVolumeClaim",
Name: sourcePVC.Name,
}

testCase := &testsuites.StorageClassTest{
Client: cs,
Timeouts: f.Timeouts,
Claim: clonePVC,
Class: sc,
Provisioner: sc.Provisioner,
ClaimSize: largerSize,
ExpectedSize: largerSize,
VolumeMode: pattern.VolMode,
NodeSelection: testConfig.ClientNodeSelection,
PvCheck: func(ctx context.Context, claim *v1.PersistentVolumeClaim) {
g.By("checking whether the cloned volume has the pre-populated data")
e2evolume.TestVolumeClientSlow(ctx, f, testConfig, nil, "", []e2evolume.Test{contentTest(claim.Name)})
},
}

// Cloning fails if the source disk is still detaching; wait for VolumeAttachment removal.
volumeAttachment := e2evolume.GetVolumeAttachmentName(ctx, cs, testConfig, testCase.Provisioner, sourcePVC.Name, sourcePVC.Namespace)
e2e.ExpectNoError(e2evolume.WaitForVolumeAttachmentTerminated(ctx, volumeAttachment, cs, f.Timeouts.DataSourceProvision))

g.By("Provisioning clone PVC larger than the source and verifying data")
testCase.TestDynamicProvisioning(ctx)
})
}

// pinToDriverTopology constrains pods to one topology segment advertised by the driver.
// Matches the intent of upstream ensureTopologyRequirements for PVC cloning.
func pinToDriverTopology(ctx context.Context, nodeSelection *e2epod.NodeSelection, cs clientset.Interface, dInfo *storageframework.DriverInfo) {
if nodeSelection.Name != "" || len(dInfo.TopologyKeys) == 0 {
return
}
node, err := e2enode.GetRandomReadySchedulableNode(ctx, cs)
e2e.ExpectNoError(err)
topo := make(map[string]string, len(dInfo.TopologyKeys))
for _, key := range dInfo.TopologyKeys {
if val, ok := node.Labels[key]; ok && val != "" {
topo[key] = val
}
}
if len(topo) > 0 {
e2epod.SetNodeAffinityTopologyRequirement(nodeSelection, topo)
}
}