Skip to content

Commit a00181d

Browse files
authored
Merge pull request kubernetes#121902 from carlory/kep-3751-pv-controller
[kep-3751] pvc bind pv with vac
2 parents 77c3859 + 3a6a483 commit a00181d

File tree

12 files changed

+534
-37
lines changed

12 files changed

+534
-37
lines changed

pkg/apis/core/validation/validation.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2399,7 +2399,9 @@ func ValidatePersistentVolumeClaimUpdate(newPvc, oldPvc *core.PersistentVolumeCl
23992399
newPvcClone.Spec.Resources.Requests["storage"] = oldPvc.Spec.Resources.Requests["storage"] // +k8s:verify-mutation:reason=clone
24002400
}
24012401
// lets make sure volume attributes class name is same.
2402-
newPvcClone.Spec.VolumeAttributesClassName = oldPvcClone.Spec.VolumeAttributesClassName // +k8s:verify-mutation:reason=clone
2402+
if newPvc.Status.Phase == core.ClaimBound && newPvcClone.Spec.VolumeAttributesClassName != nil {
2403+
newPvcClone.Spec.VolumeAttributesClassName = oldPvcClone.Spec.VolumeAttributesClassName // +k8s:verify-mutation:reason=clone
2404+
}
24032405

24042406
oldSize := oldPvc.Spec.Resources.Requests["storage"]
24052407
newSize := newPvc.Spec.Resources.Requests["storage"]

pkg/apis/core/validation/validation_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3007,6 +3007,20 @@ func TestValidatePersistentVolumeClaimUpdate(t *testing.T) {
30073007
enableVolumeAttributesClass: true,
30083008
isExpectedFailure: true,
30093009
},
3010+
"invalid-update-volume-attributes-class-when-claim-not-bound": {
3011+
oldClaim: func() *core.PersistentVolumeClaim {
3012+
clone := validClaimVolumeAttributesClass1.DeepCopy()
3013+
clone.Status.Phase = core.ClaimPending
3014+
return clone
3015+
}(),
3016+
newClaim: func() *core.PersistentVolumeClaim {
3017+
clone := validClaimVolumeAttributesClass2.DeepCopy()
3018+
clone.Status.Phase = core.ClaimPending
3019+
return clone
3020+
}(),
3021+
enableVolumeAttributesClass: true,
3022+
isExpectedFailure: true,
3023+
},
30103024
"invalid-update-volume-attributes-class-to-nil-without-featuregate-enabled": {
30113025
oldClaim: validClaimVolumeAttributesClass1,
30123026
newClaim: validClaimNilVolumeAttributesClass,

pkg/controller/volume/persistentvolume/binder_test.go

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,17 @@ limitations under the License.
1717
package persistentvolume
1818

1919
import (
20+
"fmt"
2021
"testing"
2122

2223
v1 "k8s.io/api/core/v1"
2324
storage "k8s.io/api/storage/v1"
2425
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
utilfeature "k8s.io/apiserver/pkg/util/feature"
27+
featuregatetesting "k8s.io/component-base/featuregate/testing"
2528
"k8s.io/component-helpers/storage/volume"
2629
"k8s.io/klog/v2/ktesting"
30+
"k8s.io/kubernetes/pkg/features"
2731
)
2832

2933
// Test single call to syncClaim and syncVolume methods.
@@ -750,13 +754,73 @@ func TestSync(t *testing.T) {
750754
test: testSyncClaim,
751755
},
752756
}
753-
_, ctx := ktesting.NewTestContext(t)
754-
runSyncTests(t, ctx, tests, []*storage.StorageClass{
757+
758+
// Once the feature-gate VolumeAttributesClass is promoted to GA, merge it with the above tests
759+
whenFeatureGateEnabled := []controllerTest{
760+
{
761+
// syncClaim with claim pre-bound to a PV that exists and is
762+
// unbound, but its volume attributes class does not match. Check that the claim status is reset to Pending
763+
name: "2-11 - claim prebound to unbound volume that volume attributes class is different",
764+
initialVolumes: volumesWithVAC(classGold, newVolumeArray("volume2-11", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimRetain, classEmpty)),
765+
expectedVolumes: volumesWithVAC(classGold, newVolumeArray("volume2-11", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimRetain, classEmpty)),
766+
initialClaims: newClaimArray("claim2-11", "uid2-11", "1Gi", "volume2-11", v1.ClaimBound, nil),
767+
expectedClaims: newClaimArray("claim2-11", "uid2-11", "1Gi", "volume2-11", v1.ClaimPending, nil),
768+
expectedEvents: []string{"Warning VolumeMismatch"},
769+
errors: noerrors,
770+
test: testSyncClaim,
771+
},
755772
{
756-
ObjectMeta: metav1.ObjectMeta{Name: classWait},
757-
VolumeBindingMode: &modeWait,
773+
// syncClaim with claim pre-bound to a PV that exists and is
774+
// unbound, they have the same volume attributes class. Check it gets bound and no volume.AnnBoundByController is set.
775+
name: "2-12 - claim prebound to unbound volume that volume attributes class is same",
776+
initialVolumes: volumesWithVAC(classGold, newVolumeArray("volume2-12", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimRetain, classEmpty)),
777+
expectedVolumes: volumesWithVAC(classGold, newVolumeArray("volume2-12", "1Gi", "uid2-12", "claim2-12", v1.VolumeBound, v1.PersistentVolumeReclaimRetain, classEmpty, volume.AnnBoundByController)),
778+
initialClaims: claimWithVAC(&classGold, newClaimArray("claim2-12", "uid2-12", "1Gi", "volume2-12", v1.ClaimPending, nil)),
779+
expectedClaims: withExpectedVAC(&classGold, claimWithVAC(&classGold, newClaimArray("claim2-12", "uid2-12", "1Gi", "volume2-12", v1.ClaimBound, nil, volume.AnnBindCompleted))),
780+
expectedEvents: noevents,
781+
errors: noerrors,
782+
test: testSyncClaim,
758783
},
759-
}, []*v1.Pod{})
784+
}
785+
786+
// Once the feature-gate VolumeAttributesClass is promoted to GA, remove it
787+
whenFeatureGateDisabled := []controllerTest{
788+
{
789+
// syncClaim with claim pre-bound to a PV that exists and is
790+
// unbound, they have the same volume attributes class but the feature-gate is disabled. Check that the claim status is reset to Pending
791+
name: "2-13 - claim prebound to unbound volume that volume attributes class is same",
792+
initialVolumes: volumesWithVAC(classGold, newVolumeArray("volume2-13", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimRetain, classEmpty)),
793+
expectedVolumes: volumesWithVAC(classGold, newVolumeArray("volume2-13", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimRetain, classEmpty)),
794+
initialClaims: withExpectedVAC(&classGold, claimWithVAC(&classGold, newClaimArray("claim2-13", "uid2-13", "1Gi", "volume2-13", v1.ClaimBound, nil))),
795+
expectedClaims: claimWithVAC(&classGold, newClaimArray("claim2-13", "uid2-13", "1Gi", "volume2-13", v1.ClaimPending, nil)),
796+
expectedEvents: []string{"Warning VolumeMismatch"},
797+
errors: noerrors,
798+
test: testSyncClaim,
799+
},
800+
}
801+
802+
for _, isEnabled := range []bool{true, false} {
803+
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeAttributesClass, isEnabled)
804+
805+
allTests := tests
806+
if isEnabled {
807+
allTests = append(allTests, whenFeatureGateEnabled...)
808+
} else {
809+
allTests = append(allTests, whenFeatureGateDisabled...)
810+
}
811+
812+
for i := range allTests {
813+
allTests[i].name = fmt.Sprintf("features.VolumeAttributesClass=%v %s", isEnabled, allTests[i].name)
814+
}
815+
816+
_, ctx := ktesting.NewTestContext(t)
817+
runSyncTests(t, ctx, allTests, []*storage.StorageClass{
818+
{
819+
ObjectMeta: metav1.ObjectMeta{Name: classWait},
820+
VolumeBindingMode: &modeWait,
821+
},
822+
}, []*v1.Pod{})
823+
}
760824
}
761825

762826
func TestSyncBlockVolume(t *testing.T) {

pkg/controller/volume/persistentvolume/framework_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,14 @@ func withExpectedCapacity(capacity string, claims []*v1.PersistentVolumeClaim) [
409409
return claims
410410
}
411411

412+
// withExpectedVAC sets the claim.Status.CurrentVolumeAttributesClassName of the first claim in the
413+
// array to given value and returns the array. Meant to be used to compose
414+
// claims specified inline in a test.
415+
func withExpectedVAC(vacName *string, claims []*v1.PersistentVolumeClaim) []*v1.PersistentVolumeClaim {
416+
claims[0].Status.CurrentVolumeAttributesClassName = vacName
417+
return claims
418+
}
419+
412420
// withMessage saves given message into volume.Status.Message of the first
413421
// volume in the array and returns the array. Meant to be used to compose
414422
// volumes specified inline in a test.
@@ -419,6 +427,7 @@ func withMessage(message string, volumes []*v1.PersistentVolume) []*v1.Persisten
419427

420428
// newVolumeArray returns array with a single volume that would be returned by
421429
// newVolume() with the same parameters.
430+
// TODO: make the newVolumeArray function accept volume attributes class name as an input parameter
422431
func newVolumeArray(name, capacity, boundToClaimUID, boundToClaimName string, phase v1.PersistentVolumePhase, reclaimPolicy v1.PersistentVolumeReclaimPolicy, class string, annotations ...string) []*v1.PersistentVolume {
423432
return []*v1.PersistentVolume{
424433
newVolume(name, capacity, boundToClaimUID, boundToClaimName, phase, reclaimPolicy, class, annotations...),
@@ -489,19 +498,28 @@ func newClaim(name, claimUID, capacity, boundToVolume string, phase v1.Persisten
489498
// For most of the tests it's enough to copy claim's requested capacity,
490499
// individual tests can adjust it using withExpectedCapacity()
491500
claim.Status.Capacity = claim.Spec.Resources.Requests
501+
// For most of the tests it's enough to copy claim's requested vac,
502+
// individual tests can adjust it using withExpectedVAC()
503+
claim.Status.CurrentVolumeAttributesClassName = claim.Spec.VolumeAttributesClassName
492504
}
493505

494506
return &claim
495507
}
496508

497509
// newClaimArray returns array with a single claim that would be returned by
498510
// newClaim() with the same parameters.
511+
// TODO: make the newClaimArray function accept volume attributes class name as an input parameter
499512
func newClaimArray(name, claimUID, capacity, boundToVolume string, phase v1.PersistentVolumeClaimPhase, class *string, annotations ...string) []*v1.PersistentVolumeClaim {
500513
return []*v1.PersistentVolumeClaim{
501514
newClaim(name, claimUID, capacity, boundToVolume, phase, class, annotations...),
502515
}
503516
}
504517

518+
func claimWithVAC(vacName *string, claims []*v1.PersistentVolumeClaim) []*v1.PersistentVolumeClaim {
519+
claims[0].Spec.VolumeAttributesClassName = vacName
520+
return claims
521+
}
522+
505523
// claimWithAnnotation saves given annotation into given claims. Meant to be
506524
// used to compose claims specified inline in a test.
507525
// TODO(refactor): This helper function (and other helpers related to claim
@@ -536,6 +554,22 @@ func annotateClaim(claim *v1.PersistentVolumeClaim, ann map[string]string) *v1.P
536554
return claim
537555
}
538556

557+
// volumeWithVAC saves given vac into given volume.
558+
// Meant to be used to compose volume specified inline in a test.
559+
func volumeWithVAC(vacName string, volume *v1.PersistentVolume) *v1.PersistentVolume {
560+
volume.Spec.VolumeAttributesClassName = &vacName
561+
return volume
562+
}
563+
564+
// volumesWithVAC saves given vac into given volumes.
565+
// Meant to be used to compose volumes specified inline in a test.
566+
func volumesWithVAC(vacName string, volumes []*v1.PersistentVolume) []*v1.PersistentVolume {
567+
for _, volume := range volumes {
568+
volumeWithVAC(vacName, volume)
569+
}
570+
return volumes
571+
}
572+
539573
// volumeWithAnnotation saves given annotation into given volume.
540574
// Meant to be used to compose volume specified inline in a test.
541575
func volumeWithAnnotation(name, value string, volume *v1.PersistentVolume) *v1.PersistentVolume {

pkg/controller/volume/persistentvolume/index.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ import (
2121
"sort"
2222

2323
v1 "k8s.io/api/core/v1"
24+
utilfeature "k8s.io/apiserver/pkg/util/feature"
2425
"k8s.io/client-go/tools/cache"
2526
"k8s.io/component-helpers/storage/volume"
2627
v1helper "k8s.io/kubernetes/pkg/apis/core/v1/helper"
28+
"k8s.io/kubernetes/pkg/features"
2729
"k8s.io/kubernetes/pkg/volume/util"
2830
)
2931

@@ -92,7 +94,7 @@ func (pvIndex *persistentVolumeOrderedIndex) findByClaim(claim *v1.PersistentVol
9294
return nil, err
9395
}
9496

95-
bestVol, err := volume.FindMatchingVolume(claim, volumes, nil /* node for topology binding*/, nil /* exclusion map */, delayBinding)
97+
bestVol, err := volume.FindMatchingVolume(claim, volumes, nil /* node for topology binding*/, nil /* exclusion map */, delayBinding, utilfeature.DefaultFeatureGate.Enabled(features.VolumeAttributesClass))
9698
if err != nil {
9799
return nil, err
98100
}

pkg/controller/volume/persistentvolume/index_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@ import (
2323
v1 "k8s.io/api/core/v1"
2424
"k8s.io/apimachinery/pkg/api/resource"
2525
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
utilfeature "k8s.io/apiserver/pkg/util/feature"
2627
"k8s.io/client-go/kubernetes/scheme"
2728
ref "k8s.io/client-go/tools/reference"
29+
featuregatetesting "k8s.io/component-base/featuregate/testing"
2830
"k8s.io/component-helpers/storage/volume"
31+
"k8s.io/kubernetes/pkg/features"
2932
"k8s.io/kubernetes/pkg/volume/util"
3033
)
3134

@@ -75,6 +78,9 @@ func makeVolumeModePVC(size string, mode *v1.PersistentVolumeMode, modfn func(*v
7578
}
7679

7780
func TestMatchVolume(t *testing.T) {
81+
// Default enable the VolumeAttributesClass feature gate.
82+
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeAttributesClass, true)
83+
7884
volList := newPersistentVolumeOrderedIndex()
7985
for _, pv := range createTestVolumes() {
8086
volList.store.Add(pv)
@@ -134,6 +140,24 @@ func TestMatchVolume(t *testing.T) {
134140
pvc.Spec.AccessModes = []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}
135141
}),
136142
},
143+
"successful-match-with-empty-vac": {
144+
expectedMatch: "gce-pd-10",
145+
claim: makePVC("8G", func(pvc *v1.PersistentVolumeClaim) {
146+
pvc.Spec.VolumeAttributesClassName = &classEmpty
147+
}),
148+
},
149+
"successful-match-with-vac": {
150+
expectedMatch: "gce-pd-vac-silver1",
151+
claim: makePVC("1G", func(pvc *v1.PersistentVolumeClaim) {
152+
pvc.Spec.VolumeAttributesClassName = &classSilver
153+
}),
154+
},
155+
"successful-no-match-vac-nonexisting": {
156+
expectedMatch: "",
157+
claim: makePVC("1G", func(pvc *v1.PersistentVolumeClaim) {
158+
pvc.Spec.VolumeAttributesClassName = &classNonExisting
159+
}),
160+
},
137161
"successful-match-with-class": {
138162
expectedMatch: "gce-pd-silver1",
139163
claim: makePVC("1G", func(pvc *v1.PersistentVolumeClaim) {
@@ -962,6 +986,29 @@ func createTestVolumes() []*v1.PersistentVolume {
962986
VolumeMode: &fs,
963987
},
964988
},
989+
{
990+
ObjectMeta: metav1.ObjectMeta{
991+
UID: "gce-pd-vac-silver1",
992+
Name: "gce-pd-vac-silver1",
993+
},
994+
Spec: v1.PersistentVolumeSpec{
995+
Capacity: v1.ResourceList{
996+
v1.ResourceName(v1.ResourceStorage): resource.MustParse("100G"),
997+
},
998+
PersistentVolumeSource: v1.PersistentVolumeSource{
999+
GCEPersistentDisk: &v1.GCEPersistentDiskVolumeSource{},
1000+
},
1001+
AccessModes: []v1.PersistentVolumeAccessMode{
1002+
v1.ReadWriteOnce,
1003+
v1.ReadOnlyMany,
1004+
},
1005+
VolumeAttributesClassName: &classSilver,
1006+
VolumeMode: &fs,
1007+
},
1008+
Status: v1.PersistentVolumeStatus{
1009+
Phase: v1.VolumeAvailable,
1010+
},
1011+
},
9651012
}
9661013
}
9671014

pkg/controller/volume/persistentvolume/pv_controller.go

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
utilfeature "k8s.io/apiserver/pkg/util/feature"
2727
"k8s.io/kubernetes/pkg/features"
2828
"k8s.io/kubernetes/pkg/util/slice"
29+
"k8s.io/utils/ptr"
2930

3031
v1 "k8s.io/api/core/v1"
3132
storage "k8s.io/api/storage/v1"
@@ -274,6 +275,20 @@ func checkVolumeSatisfyClaim(volume *v1.PersistentVolume, claim *v1.PersistentVo
274275
return fmt.Errorf("storageClassName does not match")
275276
}
276277

278+
if utilfeature.DefaultFeatureGate.Enabled(features.VolumeAttributesClass) {
279+
requestedVAC := ptr.Deref(claim.Spec.VolumeAttributesClassName, "")
280+
volumeVAC := ptr.Deref(volume.Spec.VolumeAttributesClassName, "")
281+
if requestedVAC != volumeVAC {
282+
return fmt.Errorf("volumeAttributesClassName does not match")
283+
}
284+
} else {
285+
requestedVAC := ptr.Deref(claim.Spec.VolumeAttributesClassName, "")
286+
volumeVAC := ptr.Deref(volume.Spec.VolumeAttributesClassName, "")
287+
if requestedVAC != "" || volumeVAC != "" {
288+
return fmt.Errorf("volumeAttributesClassName is not supported when the feature-gate VolumeAttributesClass is disabled")
289+
}
290+
}
291+
277292
if storagehelpers.CheckVolumeModeMismatches(&claim.Spec, &volume.Spec) {
278293
return fmt.Errorf("incompatible volumeMode")
279294
}
@@ -764,7 +779,7 @@ func (ctrl *PersistentVolumeController) syncVolume(ctx context.Context, volume *
764779
//
765780
// claim - claim to update
766781
// phase - phase to set
767-
// volume - volume which Capacity is set into claim.Status.Capacity
782+
// volume - volume which Capacity is set into claim.Status.Capacity and VolumeAttributesClassName is set into claim.Status.CurrentVolumeAttributesClassName
768783
func (ctrl *PersistentVolumeController) updateClaimStatus(ctx context.Context, claim *v1.PersistentVolumeClaim, phase v1.PersistentVolumeClaimPhase, volume *v1.PersistentVolume) (*v1.PersistentVolumeClaim, error) {
769784
logger := klog.FromContext(ctx)
770785
logger.V(4).Info("Updating PersistentVolumeClaim status", "PVC", klog.KObj(claim), "setPhase", phase)
@@ -778,7 +793,7 @@ func (ctrl *PersistentVolumeController) updateClaimStatus(ctx context.Context, c
778793
}
779794

780795
if volume == nil {
781-
// Need to reset AccessModes and Capacity
796+
// Need to reset AccessModes, Capacity and CurrentVolumeAttributesClassName
782797
if claim.Status.AccessModes != nil {
783798
claimClone.Status.AccessModes = nil
784799
dirty = true
@@ -787,8 +802,12 @@ func (ctrl *PersistentVolumeController) updateClaimStatus(ctx context.Context, c
787802
claimClone.Status.Capacity = nil
788803
dirty = true
789804
}
805+
if claim.Status.CurrentVolumeAttributesClassName != nil {
806+
claimClone.Status.CurrentVolumeAttributesClassName = nil
807+
dirty = true
808+
}
790809
} else {
791-
// Need to update AccessModes and Capacity
810+
// Need to update AccessModes, Capacity and CurrentVolumeAttributesClassName
792811
if !reflect.DeepEqual(claim.Status.AccessModes, volume.Spec.AccessModes) {
793812
claimClone.Status.AccessModes = volume.Spec.AccessModes
794813
dirty = true
@@ -821,6 +840,20 @@ func (ctrl *PersistentVolumeController) updateClaimStatus(ctx context.Context, c
821840
dirty = true
822841
}
823842
}
843+
844+
if utilfeature.DefaultFeatureGate.Enabled(features.VolumeAttributesClass) {
845+
// There are two components to updating the current vac name, this controller and external-resizer.
846+
// The controller ensures that the field is set properly when the volume is statically provisioned.
847+
// It is safer for the controller to only set this field during binding, but not after. Without this
848+
// constraint, there is a potential race condition where the resizer sets the field after the controller
849+
// has set it, or vice versa. Afterwards, it should be handled only by the resizer, or if an admin wants
850+
// to override.
851+
if claim.Status.Phase == v1.ClaimPending && phase == v1.ClaimBound &&
852+
!reflect.DeepEqual(claim.Status.CurrentVolumeAttributesClassName, volume.Spec.VolumeAttributesClassName) {
853+
claimClone.Status.CurrentVolumeAttributesClassName = volume.Spec.VolumeAttributesClassName
854+
dirty = true
855+
}
856+
}
824857
}
825858

826859
if !dirty {
@@ -850,7 +883,7 @@ func (ctrl *PersistentVolumeController) updateClaimStatus(ctx context.Context, c
850883
//
851884
// claim - claim to update
852885
// phase - phase to set
853-
// volume - volume which Capacity is set into claim.Status.Capacity
886+
// volume - volume which Capacity is set into claim.Status.Capacity and VolumeAttributesClassName is set into claim.Status.CurrentVolumeAttributesClassName
854887
// eventtype, reason, message - event to send, see EventRecorder.Event()
855888
func (ctrl *PersistentVolumeController) updateClaimStatusWithEvent(ctx context.Context, claim *v1.PersistentVolumeClaim, phase v1.PersistentVolumeClaimPhase, volume *v1.PersistentVolume, eventtype, reason, message string) (*v1.PersistentVolumeClaim, error) {
856889
logger := klog.FromContext(ctx)

0 commit comments

Comments
 (0)