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

Add modifycontroller SyncPVC unit tests #447

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
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
4 changes: 2 additions & 2 deletions pkg/modifycontroller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ func (ctrl *modifyController) sync() {
}
}

// syncPVC checks if a pvc requests resizing, and execute the resize operation if requested.
// syncPVC checks if a pvc requests modification, and execute the ModifyVolume operation if requested.
func (ctrl *modifyController) syncPVC(key string) error {
klog.V(4).InfoS("Started PVC processing for modify controller", "key", key)

Expand All @@ -277,7 +277,7 @@ func (ctrl *modifyController) syncPVC(key string) error {
}

// Only trigger modify volume if the following conditions are met
// 1. Non empty vac name
// 1. Non-empty vac name
// 2. PVC is in Bound state
// 3. PV CSI driver name matches local driver
vacName := pvc.Spec.VolumeAttributesClassName
Expand Down
239 changes: 135 additions & 104 deletions pkg/modifycontroller/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func TestController(t *testing.T) {
}{
{
name: "Modify called",
pvc: createTestPVC(pvcName, "target-vac" /*vacName*/, testVac /*curVacName*/, testVac /*targetVacName*/),
pvc: createTestPVC(pvcName, targetVac /*vacName*/, testVac /*curVacName*/, testVac /*targetVacName*/),
pv: basePV,
vacExists: true,
callCSIModify: true,
Expand All @@ -61,57 +61,13 @@ func TestController(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// Setup
client := csi.NewMockClient("foo", true, true, true, true, true, false)
driverName, _ := client.GetDriverName(context.TODO())

var initialObjects []runtime.Object
initialObjects = append(initialObjects, test.pvc)
initialObjects = append(initialObjects, test.pv)
// existing vac set in the pvc and pv
initialObjects = append(initialObjects, testVacObject)
if test.vacExists {
initialObjects = append(initialObjects, targetVacObject)
}

kubeClient, informerFactory := fakeK8s(initialObjects)
pvInformer := informerFactory.Core().V1().PersistentVolumes()
pvcInformer := informerFactory.Core().V1().PersistentVolumeClaims()
vacInformer := informerFactory.Storage().V1beta1().VolumeAttributesClasses()

csiModifier, err := modifier.NewModifierFromClient(client, 15*time.Second, kubeClient, informerFactory, false, driverName)
if err != nil {
t.Fatalf("Test %s: Unable to create modifier: %v", test.name, err)
}

featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeAttributesClass, true)
controller := NewModifyController(driverName,
csiModifier, kubeClient,
time.Second, false, informerFactory,
workqueue.DefaultControllerRateLimiter())
client := csi.NewMockClient(testDriverName, true, true, true, true, true, false)

ctrlInstance, _ := controller.(*modifyController)

stopCh := make(chan struct{})
informerFactory.Start(stopCh)

ctx := context.TODO()
initialObjects := []runtime.Object{test.pvc, test.pv, testVacObject, targetVacObject}
ctrlInstance, ctx := setupFakeK8sEnvironment(t, client, initialObjects)
defer ctx.Done()
go controller.Run(1, ctx)

for _, obj := range initialObjects {
switch obj.(type) {
case *v1.PersistentVolume:
pvInformer.Informer().GetStore().Add(obj)
case *v1.PersistentVolumeClaim:
pvcInformer.Informer().GetStore().Add(obj)
case *storagev1beta1.VolumeAttributesClass:
vacInformer.Informer().GetStore().Add(obj)
default:
t.Fatalf("Test %s: Unknown initalObject type: %+v", test.name, obj)
}
}
time.Sleep(time.Second * 2)
err = ctrlInstance.modifyPVC(test.pvc, test.pv)

_ = ctrlInstance.modifyPVC(test.pvc, test.pv)

modifyCallCount := client.GetModifyCount()
if test.callCSIModify && modifyCallCount == 0 {
Expand All @@ -138,14 +94,14 @@ func TestModifyPVC(t *testing.T) {
}{
{
name: "Modify succeeded",
pvc: createTestPVC(pvcName, "target-vac" /*vacName*/, testVac /*curVacName*/, testVac /*targetVacName*/),
pvc: createTestPVC(pvcName, targetVac /*vacName*/, testVac /*curVacName*/, testVac /*targetVacName*/),
pv: basePV,
modifyFailure: false,
expectFailure: false,
},
{
name: "Modify failed",
pvc: createTestPVC(pvcName, "target-vac" /*vacName*/, testVac /*curVacName*/, testVac /*targetVacName*/),
pvc: createTestPVC(pvcName, targetVac /*vacName*/, testVac /*curVacName*/, testVac /*targetVacName*/),
pv: basePV,
modifyFailure: true,
expectFailure: true,
Expand All @@ -154,77 +110,152 @@ func TestModifyPVC(t *testing.T) {

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
client := csi.NewMockClient("mock", true, true, true, true, true, false)
client := csi.NewMockClient(testDriverName, true, true, true, true, true, false)
if test.modifyFailure {
client.SetModifyFailed()
}
driverName, _ := client.GetDriverName(context.TODO())

initialObjects := []runtime.Object{}
if test.pvc != nil {
initialObjects = append(initialObjects, test.pvc)
initialObjects := []runtime.Object{test.pvc, test.pv, testVacObject, targetVacObject}
ctrlInstance, ctx := setupFakeK8sEnvironment(t, client, initialObjects)
defer ctx.Done()

_, _, err, _ := ctrlInstance.modify(test.pvc, test.pv)

if test.expectFailure && err == nil {
t.Errorf("for %s expected error got nothing", test.name)
}
if test.pv != nil {
test.pv.Spec.PersistentVolumeSource.CSI.Driver = driverName
initialObjects = append(initialObjects, test.pv)

if !test.expectFailure {
if err != nil {
t.Errorf("for %s, unexpected error: %v", test.name, err)
}
}
})
}
}

// existing vac set in the pvc and pv
initialObjects = append(initialObjects, testVacObject)
// new vac used in modify volume
initialObjects = append(initialObjects, targetVacObject)
func TestSyncPVC(t *testing.T) {
basePVC := createTestPVC(pvcName, targetVac /*vacName*/, testVac /*curVacName*/, testVac /*targetVacName*/)
basePV := createTestPV(1, pvcName, pvcNamespace, "foobaz" /*pvcUID*/, &fsVolumeMode, testVac)

kubeClient, informerFactory := fakeK8s(initialObjects)
pvInformer := informerFactory.Core().V1().PersistentVolumes()
pvcInformer := informerFactory.Core().V1().PersistentVolumeClaims()
vacInformer := informerFactory.Storage().V1beta1().VolumeAttributesClasses()
otherDriverPV := createTestPV(1, pvcName, pvcNamespace, "foobaz" /*pvcUID*/, &fsVolumeMode, testVac)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This seems like a code smell.

Happy to attempt to try to refactor resizer's testutils to implement Builder pattern to prevent by allowing developers to use MakePVC().WithXXX().WithYYY() style in the test-cases themselves, similar to what k/k has.

This work was started in #402, but was pushed back to once all-in-one sidecar project is here.

otherDriverPV.Spec.PersistentVolumeSource.CSI.Driver = "some-other-driver"

csiModifier, err := modifier.NewModifierFromClient(client, 15*time.Second, kubeClient, informerFactory, false, driverName)
if err != nil {
t.Fatalf("Test %s: Unable to create modifier: %v", test.name, err)
}
unboundPVC := createTestPVC(pvcName, targetVac /*vacName*/, testVac /*curVacName*/, testVac /*targetVacName*/)
unboundPVC.Status.Phase = v1.ClaimPending

featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeAttributesClass, true)
controller := NewModifyController(driverName,
csiModifier, kubeClient,
time.Second, false, informerFactory,
workqueue.DefaultControllerRateLimiter())
pvcWithUncreatedPV := createTestPVC(pvcName, targetVac /*vacName*/, testVac /*curVacName*/, testVac /*targetVacName*/)
pvcWithUncreatedPV.Spec.VolumeName = ""

ctrlInstance, _ := controller.(*modifyController)
tests := []struct {
name string
pvc *v1.PersistentVolumeClaim
pv *v1.PersistentVolume
callCSIModify bool
}{
{
name: "Should execute ModifyVolume operation when PVC's VAC changes",
pvc: basePVC,
pv: basePV,
callCSIModify: true,
},
{
name: "Should NOT modify if PVC managed by another CSI Driver",
pvc: basePVC,
pv: otherDriverPV,
callCSIModify: false,
},
{
name: "Should NOT modify if PVC has empty Spec.VACName",
pvc: createTestPVC(pvcName, "" /*vacName*/, testVac /*curVacName*/, testVac /*targetVacName*/),
pv: basePV,
callCSIModify: false,
},
{
name: "Should NOT modify if PVC not in bound state",
pvc: unboundPVC,
pv: basePV,
callCSIModify: false,
},
{
name: "Should NOT modify if PVC's PV not created yet",
pvc: pvcWithUncreatedPV,
pv: basePV,
callCSIModify: false,
},
}

stopCh := make(chan struct{})
informerFactory.Start(stopCh)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
client := csi.NewMockClient(testDriverName, true, true, true, true, true, false)

ctx := context.TODO()
initialObjects := []runtime.Object{test.pvc, test.pv, testVacObject, targetVacObject}
ctrlInstance, ctx := setupFakeK8sEnvironment(t, client, initialObjects)
defer ctx.Done()
go controller.Run(1, ctx)

for _, obj := range initialObjects {
switch obj.(type) {
case *v1.PersistentVolume:
pvInformer.Informer().GetStore().Add(obj)
case *v1.PersistentVolumeClaim:
pvcInformer.Informer().GetStore().Add(obj)
case *storagev1beta1.VolumeAttributesClass:
vacInformer.Informer().GetStore().Add(obj)
default:
t.Fatalf("Test %s: Unknown initalObject type: %+v", test.name, obj)
}
}

time.Sleep(time.Second * 2)

_, _, err, _ = ctrlInstance.modify(test.pvc, test.pv)
err := ctrlInstance.syncPVC(pvcNamespace + "/" + pvcName)
if err != nil {
t.Errorf("for %s, unexpected error: %v", test.name, err)
}

if test.expectFailure && err == nil {
t.Errorf("for %s expected error got nothing", test.name)
modifyCallCount := client.GetModifyCount()
if test.callCSIModify && modifyCallCount == 0 {
t.Fatalf("for %s: expected csi modify call, no csi modify call was made", test.name)
}

if !test.expectFailure {
if err != nil {
t.Errorf("for %s, unexpected error: %v", test.name, err)
}
if !test.callCSIModify && modifyCallCount > 0 {
t.Fatalf("for %s: expected no csi modify call, received csi modify request", test.name)
}
})
}
}

// setupFakeK8sEnvironment creates fake K8s environment and starts Informers and ModifyController
func setupFakeK8sEnvironment(t *testing.T, client *csi.MockClient, initialObjects []runtime.Object) (*modifyController, context.Context) {
t.Helper()

featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeAttributesClass, true)

/* Create fake kubeClient, Informers, and ModifyController */
kubeClient, informerFactory := fakeK8s(initialObjects)
pvInformer := informerFactory.Core().V1().PersistentVolumes()
pvcInformer := informerFactory.Core().V1().PersistentVolumeClaims()
vacInformer := informerFactory.Storage().V1beta1().VolumeAttributesClasses()

driverName, _ := client.GetDriverName(context.TODO())

csiModifier, err := modifier.NewModifierFromClient(client, 15*time.Second, kubeClient, informerFactory, false, driverName)
if err != nil {
t.Fatalf("Test %s: Unable to create modifier: %v", t.Name(), err)
}

controller := NewModifyController(driverName,
csiModifier, kubeClient,
0, false, informerFactory,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that I have set resync period to 0 because shared_informer does not support resyncing:

W1023 19:04:49.983836 13491 shared_informer.go:569] The specified resyncPeriod 1s is invalid because this shared informer doesn't support resyncing

This also let us cut out any sleeps in unit tests (instant tests!)

workqueue.DefaultControllerRateLimiter())

/* Start informers and ModifyController*/
stopCh := make(chan struct{})
informerFactory.Start(stopCh)

ctx := context.TODO()
go controller.Run(1, ctx)

/* Add initial objects to informer caches (TODO Q confirm this is true/needed?) */
for _, obj := range initialObjects {
switch obj.(type) {
case *v1.PersistentVolume:
pvInformer.Informer().GetStore().Add(obj)
case *v1.PersistentVolumeClaim:
pvcInformer.Informer().GetStore().Add(obj)
case *storagev1beta1.VolumeAttributesClass:
vacInformer.Informer().GetStore().Add(obj)
default:
t.Fatalf("Test %s: Unknown initalObject type: %+v", t.Name(), obj)
}
}

ctrlInstance, _ := controller.(*modifyController)

return ctrlInstance, ctx
}
4 changes: 3 additions & 1 deletion pkg/modifycontroller/modify_status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@ import (
const (
pvcName = "foo"
pvcNamespace = "modify"
pvName = "testPV"
)

var (
fsVolumeMode = v1.PersistentVolumeFilesystem
testVac = "test-vac"
targetVac = "target-vac"
testDriverName = "mock"
infeasibleErr = status.Errorf(codes.InvalidArgument, "Parameters in VolumeAttributesClass is invalid")
finalErr = status.Errorf(codes.Internal, "Final error")
pvcConditionInProgress = v1.PersistentVolumeClaimCondition{
Expand Down Expand Up @@ -390,7 +392,7 @@ func createTestPV(capacityGB int, pvcName, pvcNamespace string, pvcUID types.UID
},
PersistentVolumeSource: v1.PersistentVolumeSource{
CSI: &v1.CSIPersistentVolumeSource{
Driver: "foo",
Driver: testDriverName,
VolumeHandle: "foo",
},
},
Expand Down
15 changes: 8 additions & 7 deletions pkg/modifycontroller/modify_volume_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,18 @@ import (
var (
testVacObject = &storagev1beta1.VolumeAttributesClass{
ObjectMeta: metav1.ObjectMeta{Name: testVac},
DriverName: "test-driver",
DriverName: testDriverName,
Parameters: map[string]string{"iops": "3000"},
}

targetVacObject = &storagev1beta1.VolumeAttributesClass{
ObjectMeta: metav1.ObjectMeta{Name: targetVac},
DriverName: "test-driver",
DriverName: testDriverName,
Parameters: map[string]string{
"iops": "4567",
"csi.storage.k8s.io/pvc/name": "foo",
"csi.storage.k8s.io/pvc/namespace": "modify",
"csi.storage.k8s.io/pv/name": "testPV",
"csi.storage.k8s.io/pvc/name": pvcName,
"csi.storage.k8s.io/pvc/namespace": pvcNamespace,
"csi.storage.k8s.io/pv/name": pvName,
},
}
)
Expand Down Expand Up @@ -112,7 +112,7 @@ func TestModify(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
// Setup
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeAttributesClass, true)
client := csi.NewMockClient("foo", true, true, true, true, true, test.withExtraMetadata)
client := csi.NewMockClient(testDriverName, true, true, true, true, true, test.withExtraMetadata)
driverName, _ := client.GetDriverName(context.TODO())

var initialObjects []runtime.Object
Expand Down Expand Up @@ -199,7 +199,7 @@ func TestModify(t *testing.T) {

func createTestPVC(pvcName string, vacName string, curVacName string, targetVacName string) *v1.PersistentVolumeClaim {
pvc := &v1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{Name: pvcName, Namespace: "modify"},
ObjectMeta: metav1.ObjectMeta{Name: pvcName, Namespace: pvcNamespace},
Spec: v1.PersistentVolumeClaimSpec{
AccessModes: []v1.PersistentVolumeAccessMode{
v1.ReadWriteOnce,
Expand All @@ -211,6 +211,7 @@ func createTestPVC(pvcName string, vacName string, curVacName string, targetVacN
},
},
VolumeAttributesClassName: &vacName,
VolumeName: pvName,
},
Status: v1.PersistentVolumeClaimStatus{
Phase: v1.ClaimBound,
Expand Down