Unverified Commit 5cecc6ec authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

Merge pull request #59350 from jsafrane/recycler-wait

Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>. Do not recycle volumes that are used by pods **What this PR does / why we need it**: Recycler should wait until all pods that use a volume are finished. Consider this scenario: 1. User creates a PVC that's bound to a NFS PV. 2. User creates a pod that uses the PVC 3. User deletes the PVC. Now the PV gets `Released` (the PVC does not exists) and recycled, however the PV is still mounted to a running pod. PVC protection won't help us, because it puts finalizers on PVC that is under user's control and user can remove it. This PR checks that there is no pod that uses a PV before it recycles it. **Release note**: ```release-note NONE ``` /sig storage
parents 99cfb388 53efb0ff
...@@ -175,6 +175,7 @@ func startPersistentVolumeBinderController(ctx ControllerContext) (bool, error) ...@@ -175,6 +175,7 @@ func startPersistentVolumeBinderController(ctx ControllerContext) (bool, error)
VolumeInformer: ctx.InformerFactory.Core().V1().PersistentVolumes(), VolumeInformer: ctx.InformerFactory.Core().V1().PersistentVolumes(),
ClaimInformer: ctx.InformerFactory.Core().V1().PersistentVolumeClaims(), ClaimInformer: ctx.InformerFactory.Core().V1().PersistentVolumeClaims(),
ClassInformer: ctx.InformerFactory.Storage().V1().StorageClasses(), ClassInformer: ctx.InformerFactory.Storage().V1().StorageClasses(),
PodInformer: ctx.InformerFactory.Core().V1().Pods(),
EnableDynamicProvisioning: ctx.Options.VolumeConfiguration.EnableDynamicProvisioning, EnableDynamicProvisioning: ctx.Options.VolumeConfiguration.EnableDynamicProvisioning,
} }
volumeController, volumeControllerErr := persistentvolumecontroller.NewController(params) volumeController, volumeControllerErr := persistentvolumecontroller.NewController(params)
......
...@@ -32,6 +32,7 @@ go_library( ...@@ -32,6 +32,7 @@ go_library(
"//pkg/util/mount:go_default_library", "//pkg/util/mount:go_default_library",
"//pkg/volume:go_default_library", "//pkg/volume:go_default_library",
"//pkg/volume/util:go_default_library", "//pkg/volume/util:go_default_library",
"//pkg/volume/util/volumehelper:go_default_library",
"//vendor/github.com/golang/glog:go_default_library", "//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/api/storage/v1:go_default_library", "//vendor/k8s.io/api/storage/v1:go_default_library",
...@@ -42,6 +43,7 @@ go_library( ...@@ -42,6 +43,7 @@ go_library(
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library", "//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library", "//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library", "//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
"//vendor/k8s.io/client-go/informers/core/v1:go_default_library", "//vendor/k8s.io/client-go/informers/core/v1:go_default_library",
...@@ -96,6 +98,7 @@ go_test( ...@@ -96,6 +98,7 @@ go_test(
"//vendor/k8s.io/client-go/kubernetes:go_default_library", "//vendor/k8s.io/client-go/kubernetes:go_default_library",
"//vendor/k8s.io/client-go/kubernetes/fake:go_default_library", "//vendor/k8s.io/client-go/kubernetes/fake:go_default_library",
"//vendor/k8s.io/client-go/kubernetes/scheme:go_default_library", "//vendor/k8s.io/client-go/kubernetes/scheme:go_default_library",
"//vendor/k8s.io/client-go/listers/core/v1:go_default_library",
"//vendor/k8s.io/client-go/listers/storage/v1:go_default_library", "//vendor/k8s.io/client-go/listers/storage/v1:go_default_library",
"//vendor/k8s.io/client-go/testing:go_default_library", "//vendor/k8s.io/client-go/testing:go_default_library",
"//vendor/k8s.io/client-go/tools/cache:go_default_library", "//vendor/k8s.io/client-go/tools/cache:go_default_library",
......
...@@ -624,7 +624,7 @@ func TestSync(t *testing.T) { ...@@ -624,7 +624,7 @@ func TestSync(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{Name: classWait}, ObjectMeta: metav1.ObjectMeta{Name: classWait},
VolumeBindingMode: &modeWait, VolumeBindingMode: &modeWait,
}, },
}) }, []*v1.Pod{})
} }
func TestSyncAlphaBlockVolume(t *testing.T) { func TestSyncAlphaBlockVolume(t *testing.T) {
...@@ -776,7 +776,7 @@ func TestSyncAlphaBlockVolume(t *testing.T) { ...@@ -776,7 +776,7 @@ func TestSyncAlphaBlockVolume(t *testing.T) {
} }
defer utilfeature.DefaultFeatureGate.Set("BlockVolume=false") defer utilfeature.DefaultFeatureGate.Set("BlockVolume=false")
runSyncTests(t, tests, []*storage.StorageClass{}) runSyncTests(t, tests, []*storage.StorageClass{}, []*v1.Pod{})
} }
// Test multiple calls to syncClaim/syncVolume and periodic sync of all // Test multiple calls to syncClaim/syncVolume and periodic sync of all
......
...@@ -192,7 +192,7 @@ func TestDeleteSync(t *testing.T) { ...@@ -192,7 +192,7 @@ func TestDeleteSync(t *testing.T) {
}, },
}, },
} }
runSyncTests(t, tests, []*storage.StorageClass{}) runSyncTests(t, tests, []*storage.StorageClass{}, []*v1.Pod{})
} }
// Test multiple calls to syncClaim/syncVolume and periodic sync of all // Test multiple calls to syncClaim/syncVolume and periodic sync of all
......
...@@ -41,6 +41,7 @@ import ( ...@@ -41,6 +41,7 @@ import (
"k8s.io/client-go/informers" "k8s.io/client-go/informers"
clientset "k8s.io/client-go/kubernetes" clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/fake"
corelisters "k8s.io/client-go/listers/core/v1"
storagelisters "k8s.io/client-go/listers/storage/v1" storagelisters "k8s.io/client-go/listers/storage/v1"
core "k8s.io/client-go/testing" core "k8s.io/client-go/testing"
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
...@@ -609,6 +610,7 @@ func newTestController(kubeClient clientset.Interface, informerFactory informers ...@@ -609,6 +610,7 @@ func newTestController(kubeClient clientset.Interface, informerFactory informers
VolumeInformer: informerFactory.Core().V1().PersistentVolumes(), VolumeInformer: informerFactory.Core().V1().PersistentVolumes(),
ClaimInformer: informerFactory.Core().V1().PersistentVolumeClaims(), ClaimInformer: informerFactory.Core().V1().PersistentVolumeClaims(),
ClassInformer: informerFactory.Storage().V1().StorageClasses(), ClassInformer: informerFactory.Storage().V1().StorageClasses(),
PodInformer: informerFactory.Core().V1().Pods(),
EventRecorder: record.NewFakeRecorder(1000), EventRecorder: record.NewFakeRecorder(1000),
EnableDynamicProvisioning: enableDynamicProvisioning, EnableDynamicProvisioning: enableDynamicProvisioning,
} }
...@@ -939,7 +941,7 @@ func evaluateTestResults(ctrl *PersistentVolumeController, reactor *volumeReacto ...@@ -939,7 +941,7 @@ func evaluateTestResults(ctrl *PersistentVolumeController, reactor *volumeReacto
// 2. Call the tested function (syncClaim/syncVolume) via // 2. Call the tested function (syncClaim/syncVolume) via
// controllerTest.testCall *once*. // controllerTest.testCall *once*.
// 3. Compare resulting volumes and claims with expected volumes and claims. // 3. Compare resulting volumes and claims with expected volumes and claims.
func runSyncTests(t *testing.T, tests []controllerTest, storageClasses []*storage.StorageClass) { func runSyncTests(t *testing.T, tests []controllerTest, storageClasses []*storage.StorageClass, pods []*v1.Pod) {
for _, test := range tests { for _, test := range tests {
glog.V(4).Infof("starting test %q", test.name) glog.V(4).Infof("starting test %q", test.name)
...@@ -966,6 +968,12 @@ func runSyncTests(t *testing.T, tests []controllerTest, storageClasses []*storag ...@@ -966,6 +968,12 @@ func runSyncTests(t *testing.T, tests []controllerTest, storageClasses []*storag
} }
ctrl.classLister = storagelisters.NewStorageClassLister(indexer) ctrl.classLister = storagelisters.NewStorageClassLister(indexer)
podIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{})
for _, pod := range pods {
podIndexer.Add(pod)
}
ctrl.podLister = corelisters.NewPodLister(podIndexer)
// Run the tested functions // Run the tested functions
err = test.test(ctrl, reactor, test) err = test.test(ctrl, reactor, test)
if err != nil { if err != nil {
......
...@@ -416,7 +416,7 @@ func TestProvisionSync(t *testing.T) { ...@@ -416,7 +416,7 @@ func TestProvisionSync(t *testing.T) {
noerrors, wrapTestWithProvisionCalls([]provisionCall{}, testSyncClaim), noerrors, wrapTestWithProvisionCalls([]provisionCall{}, testSyncClaim),
}, },
} }
runSyncTests(t, tests, storageClasses) runSyncTests(t, tests, storageClasses, []*v1.Pod{})
} }
// Test multiple calls to syncClaim/syncVolume and periodic sync of all // Test multiple calls to syncClaim/syncVolume and periodic sync of all
......
...@@ -26,6 +26,8 @@ import ( ...@@ -26,6 +26,8 @@ import (
storage "k8s.io/api/storage/v1" storage "k8s.io/api/storage/v1"
apierrs "k8s.io/apimachinery/pkg/api/errors" apierrs "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/sets"
utilfeature "k8s.io/apiserver/pkg/util/feature" utilfeature "k8s.io/apiserver/pkg/util/feature"
clientset "k8s.io/client-go/kubernetes" clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/kubernetes/scheme"
...@@ -43,6 +45,7 @@ import ( ...@@ -43,6 +45,7 @@ import (
"k8s.io/kubernetes/pkg/util/goroutinemap/exponentialbackoff" "k8s.io/kubernetes/pkg/util/goroutinemap/exponentialbackoff"
vol "k8s.io/kubernetes/pkg/volume" vol "k8s.io/kubernetes/pkg/volume"
"k8s.io/kubernetes/pkg/volume/util" "k8s.io/kubernetes/pkg/volume/util"
"k8s.io/kubernetes/pkg/volume/util/volumehelper"
"github.com/golang/glog" "github.com/golang/glog"
) )
...@@ -161,6 +164,8 @@ type PersistentVolumeController struct { ...@@ -161,6 +164,8 @@ type PersistentVolumeController struct {
claimListerSynced cache.InformerSynced claimListerSynced cache.InformerSynced
classLister storagelisters.StorageClassLister classLister storagelisters.StorageClassLister
classListerSynced cache.InformerSynced classListerSynced cache.InformerSynced
podLister corelisters.PodLister
podListerSynced cache.InformerSynced
kubeClient clientset.Interface kubeClient clientset.Interface
eventRecorder record.EventRecorder eventRecorder record.EventRecorder
...@@ -1065,6 +1070,17 @@ func (ctrl *PersistentVolumeController) recycleVolumeOperation(arg interface{}) ...@@ -1065,6 +1070,17 @@ func (ctrl *PersistentVolumeController) recycleVolumeOperation(arg interface{})
glog.V(3).Infof("volume %q no longer needs recycling, skipping", volume.Name) glog.V(3).Infof("volume %q no longer needs recycling, skipping", volume.Name)
return return
} }
pods, used, err := ctrl.isVolumeUsed(newVolume)
if err != nil {
glog.V(3).Infof("can't recycle volume %q: %v", volume.Name, err)
return
}
if used {
msg := fmt.Sprintf("Volume is used by pods: %s", strings.Join(pods, ","))
glog.V(3).Infof("can't recycle volume %q: %s", volume.Name, msg)
ctrl.eventRecorder.Event(volume, v1.EventTypeNormal, events.VolumeFailedRecycle, msg)
return
}
// Use the newest volume copy, this will save us from version conflicts on // Use the newest volume copy, this will save us from version conflicts on
// saving. // saving.
...@@ -1233,6 +1249,32 @@ func (ctrl *PersistentVolumeController) isVolumeReleased(volume *v1.PersistentVo ...@@ -1233,6 +1249,32 @@ func (ctrl *PersistentVolumeController) isVolumeReleased(volume *v1.PersistentVo
return true, nil return true, nil
} }
// isVolumeUsed returns list of pods that use given PV.
func (ctrl *PersistentVolumeController) isVolumeUsed(pv *v1.PersistentVolume) ([]string, bool, error) {
if pv.Spec.ClaimRef == nil {
return nil, false, nil
}
claimName := pv.Spec.ClaimRef.Name
podNames := sets.NewString()
pods, err := ctrl.podLister.Pods(pv.Spec.ClaimRef.Namespace).List(labels.Everything())
if err != nil {
return nil, false, fmt.Errorf("error listing pods: %s", err)
}
for _, pod := range pods {
if volumehelper.IsPodTerminated(pod, pod.Status) {
continue
}
for i := range pod.Spec.Volumes {
usedPV := &pod.Spec.Volumes[i]
if usedPV.PersistentVolumeClaim != nil && usedPV.PersistentVolumeClaim.ClaimName == claimName {
podNames.Insert(pod.Namespace + "/" + pod.Name)
}
}
}
return podNames.List(), podNames.Len() != 0, nil
}
// doDeleteVolume finds appropriate delete plugin and deletes given volume. It // doDeleteVolume finds appropriate delete plugin and deletes given volume. It
// returns 'true', when the volume was deleted and 'false' when the volume // returns 'true', when the volume was deleted and 'false' when the volume
// cannot be deleted because of the deleter is external. No error should be // cannot be deleted because of the deleter is external. No error should be
......
...@@ -62,6 +62,7 @@ type ControllerParameters struct { ...@@ -62,6 +62,7 @@ type ControllerParameters struct {
VolumeInformer coreinformers.PersistentVolumeInformer VolumeInformer coreinformers.PersistentVolumeInformer
ClaimInformer coreinformers.PersistentVolumeClaimInformer ClaimInformer coreinformers.PersistentVolumeClaimInformer
ClassInformer storageinformers.StorageClassInformer ClassInformer storageinformers.StorageClassInformer
PodInformer coreinformers.PodInformer
EventRecorder record.EventRecorder EventRecorder record.EventRecorder
EnableDynamicProvisioning bool EnableDynamicProvisioning bool
} }
...@@ -119,6 +120,8 @@ func NewController(p ControllerParameters) (*PersistentVolumeController, error) ...@@ -119,6 +120,8 @@ func NewController(p ControllerParameters) (*PersistentVolumeController, error)
controller.classLister = p.ClassInformer.Lister() controller.classLister = p.ClassInformer.Lister()
controller.classListerSynced = p.ClassInformer.Informer().HasSynced controller.classListerSynced = p.ClassInformer.Informer().HasSynced
controller.podLister = p.PodInformer.Lister()
controller.podListerSynced = p.PodInformer.Informer().HasSynced
return controller, nil return controller, nil
} }
...@@ -265,7 +268,7 @@ func (ctrl *PersistentVolumeController) Run(stopCh <-chan struct{}) { ...@@ -265,7 +268,7 @@ func (ctrl *PersistentVolumeController) Run(stopCh <-chan struct{}) {
glog.Infof("Starting persistent volume controller") glog.Infof("Starting persistent volume controller")
defer glog.Infof("Shutting down peristent volume controller") defer glog.Infof("Shutting down peristent volume controller")
if !controller.WaitForCacheSync("persistent volume", stopCh, ctrl.volumeListerSynced, ctrl.claimListerSynced, ctrl.classListerSynced) { if !controller.WaitForCacheSync("persistent volume", stopCh, ctrl.volumeListerSynced, ctrl.claimListerSynced, ctrl.classListerSynced, ctrl.podListerSynced) {
return return
} }
......
...@@ -22,6 +22,7 @@ import ( ...@@ -22,6 +22,7 @@ import (
"k8s.io/api/core/v1" "k8s.io/api/core/v1"
storage "k8s.io/api/storage/v1" storage "k8s.io/api/storage/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
// Test single call to syncVolume, expecting recycling to happen. // Test single call to syncVolume, expecting recycling to happen.
...@@ -29,6 +30,44 @@ import ( ...@@ -29,6 +30,44 @@ import (
// 2. Call the syncVolume *once*. // 2. Call the syncVolume *once*.
// 3. Compare resulting volumes with expected volumes. // 3. Compare resulting volumes with expected volumes.
func TestRecycleSync(t *testing.T) { func TestRecycleSync(t *testing.T) {
runningPod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "runningPod",
Namespace: testNamespace,
},
Spec: v1.PodSpec{
Volumes: []v1.Volume{
{
Name: "vol1",
VolumeSource: v1.VolumeSource{
PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
ClaimName: "runningClaim",
},
},
},
},
},
Status: v1.PodStatus{
Phase: v1.PodRunning,
},
}
pendingPod := runningPod.DeepCopy()
pendingPod.Name = "pendingPod"
pendingPod.Status.Phase = v1.PodPending
pendingPod.Spec.Volumes[0].PersistentVolumeClaim.ClaimName = "pendingClaim"
completedPod := runningPod.DeepCopy()
completedPod.Name = "completedPod"
completedPod.Status.Phase = v1.PodSucceeded
completedPod.Spec.Volumes[0].PersistentVolumeClaim.ClaimName = "completedClaim"
pods := []*v1.Pod{
runningPod,
pendingPod,
completedPod,
}
tests := []controllerTest{ tests := []controllerTest{
{ {
// recycle volume bound by controller // recycle volume bound by controller
...@@ -160,8 +199,38 @@ func TestRecycleSync(t *testing.T) { ...@@ -160,8 +199,38 @@ func TestRecycleSync(t *testing.T) {
noclaims, noclaims,
[]string{"Warning VolumeUnknownReclaimPolicy"}, noerrors, testSyncVolume, []string{"Warning VolumeUnknownReclaimPolicy"}, noerrors, testSyncVolume,
}, },
{
// volume is used by a running pod - failure expected
"6-11 - used by running pod",
newVolumeArray("volume6-11", "1Gi", "uid6-11", "runningClaim", v1.VolumeBound, v1.PersistentVolumeReclaimRecycle, classEmpty, annBoundByController),
newVolumeArray("volume6-11", "1Gi", "uid6-11", "runningClaim", v1.VolumeReleased, v1.PersistentVolumeReclaimRecycle, classEmpty, annBoundByController),
noclaims,
noclaims,
[]string{"Normal VolumeFailedRecycle"}, noerrors, testSyncVolume,
},
{
// volume is used by a pending pod - failure expected
"6-12 - used by pending pod",
newVolumeArray("volume6-12", "1Gi", "uid6-12", "pendingClaim", v1.VolumeBound, v1.PersistentVolumeReclaimRecycle, classEmpty, annBoundByController),
newVolumeArray("volume6-12", "1Gi", "uid6-12", "pendingClaim", v1.VolumeReleased, v1.PersistentVolumeReclaimRecycle, classEmpty, annBoundByController),
noclaims,
noclaims,
[]string{"Normal VolumeFailedRecycle"}, noerrors, testSyncVolume,
},
{
// volume is used by a completed pod - recycle succeeds
"6-13 - used by completed pod",
newVolumeArray("volume6-13", "1Gi", "uid6-13", "completedClaim", v1.VolumeBound, v1.PersistentVolumeReclaimRecycle, classEmpty, annBoundByController),
newVolumeArray("volume6-13", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimRecycle, classEmpty),
noclaims,
noclaims,
noevents, noerrors,
// Inject recycler into the controller and call syncVolume. The
// recycler simulates one recycle() call that succeeds.
wrapTestWithReclaimCalls(operationRecycle, []error{nil}, testSyncVolume),
},
} }
runSyncTests(t, tests, []*storage.StorageClass{}) runSyncTests(t, tests, []*storage.StorageClass{}, pods)
} }
// Test multiple calls to syncClaim/syncVolume and periodic sync of all // Test multiple calls to syncClaim/syncVolume and periodic sync of all
......
...@@ -136,6 +136,7 @@ func setupNodes(t *testing.T, nsName string, numberOfNodes int) *testConfig { ...@@ -136,6 +136,7 @@ func setupNodes(t *testing.T, nsName string, numberOfNodes int) *testConfig {
VolumeInformer: informers.Core().V1().PersistentVolumes(), VolumeInformer: informers.Core().V1().PersistentVolumes(),
ClaimInformer: informers.Core().V1().PersistentVolumeClaims(), ClaimInformer: informers.Core().V1().PersistentVolumeClaims(),
ClassInformer: informers.Storage().V1().StorageClasses(), ClassInformer: informers.Storage().V1().StorageClasses(),
PodInformer: informers.Core().V1().Pods(),
EventRecorder: nil, // TODO: add one so we can test PV events EventRecorder: nil, // TODO: add one so we can test PV events
EnableDynamicProvisioning: true, EnableDynamicProvisioning: true,
} }
......
...@@ -1135,6 +1135,7 @@ func createClients(ns *v1.Namespace, t *testing.T, s *httptest.Server, syncPerio ...@@ -1135,6 +1135,7 @@ func createClients(ns *v1.Namespace, t *testing.T, s *httptest.Server, syncPerio
VolumeInformer: informers.Core().V1().PersistentVolumes(), VolumeInformer: informers.Core().V1().PersistentVolumes(),
ClaimInformer: informers.Core().V1().PersistentVolumeClaims(), ClaimInformer: informers.Core().V1().PersistentVolumeClaims(),
ClassInformer: informers.Storage().V1().StorageClasses(), ClassInformer: informers.Storage().V1().StorageClasses(),
PodInformer: informers.Core().V1().Pods(),
EnableDynamicProvisioning: true, EnableDynamicProvisioning: true,
}) })
if err != nil { if err != nil {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment