
After K8s 1.10 is upgraded to K8s 1.11 finalizer [kubernetes.io/pvc-protection] is added to PVCs because StorageObjectInUseProtection feature will be GA in K8s 1.11. However, when K8s 1.11 is downgraded to K8s 1.10 and the StorageObjectInUseProtection feature is disabled the finalizers remain in the PVCs and as pvc-protection-controller is not started in K8s 1.10 finalizers are not removed automatically from deleted PVCs and that's why deleted PVC are not removed from the system but remain in Terminating phase. The same applies to pv-protection-controller and [kubernetes.io/pvc-protection] finalizer in PVs. That's why pvc-protection-controller is always started because the pvc-protection-controller removes finalizers from PVCs automatically when a PVC is not in active use by a pod. Also the pv-protection-controller is always started to remove finalizers from PVs automatically when a PV is not Bound to a PVC. Related issue: https://github.com/kubernetes/kubernetes/issues/60764
279 lines
8.5 KiB
Go
279 lines
8.5 KiB
Go
/*
|
|
Copyright 2018 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package pvprotection
|
|
|
|
import (
|
|
"errors"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/davecgh/go-spew/spew"
|
|
"github.com/golang/glog"
|
|
|
|
"k8s.io/api/core/v1"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/client-go/informers"
|
|
"k8s.io/client-go/kubernetes/fake"
|
|
clienttesting "k8s.io/client-go/testing"
|
|
"k8s.io/kubernetes/pkg/controller"
|
|
volumeutil "k8s.io/kubernetes/pkg/volume/util"
|
|
)
|
|
|
|
const defaultPVName = "default-pv"
|
|
|
|
type reaction struct {
|
|
verb string
|
|
resource string
|
|
reactorfn clienttesting.ReactionFunc
|
|
}
|
|
|
|
func pv() *v1.PersistentVolume {
|
|
return &v1.PersistentVolume{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: defaultPVName,
|
|
},
|
|
}
|
|
}
|
|
|
|
func boundPV() *v1.PersistentVolume {
|
|
return &v1.PersistentVolume{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: defaultPVName,
|
|
},
|
|
Status: v1.PersistentVolumeStatus{
|
|
Phase: v1.VolumeBound,
|
|
},
|
|
}
|
|
}
|
|
|
|
func withProtectionFinalizer(pv *v1.PersistentVolume) *v1.PersistentVolume {
|
|
pv.Finalizers = append(pv.Finalizers, volumeutil.PVProtectionFinalizer)
|
|
return pv
|
|
}
|
|
|
|
func generateUpdateErrorFunc(t *testing.T, failures int) clienttesting.ReactionFunc {
|
|
i := 0
|
|
return func(action clienttesting.Action) (bool, runtime.Object, error) {
|
|
i++
|
|
if i <= failures {
|
|
// Update fails
|
|
update, ok := action.(clienttesting.UpdateAction)
|
|
|
|
if !ok {
|
|
t.Fatalf("Reactor got non-update action: %+v", action)
|
|
}
|
|
acc, _ := meta.Accessor(update.GetObject())
|
|
return true, nil, apierrors.NewForbidden(update.GetResource().GroupResource(), acc.GetName(), errors.New("Mock error"))
|
|
}
|
|
// Update succeeds
|
|
return false, nil, nil
|
|
}
|
|
}
|
|
|
|
func deleted(pv *v1.PersistentVolume) *v1.PersistentVolume {
|
|
pv.DeletionTimestamp = &metav1.Time{}
|
|
return pv
|
|
}
|
|
|
|
func TestPVProtectionController(t *testing.T) {
|
|
pvVer := schema.GroupVersionResource{
|
|
Group: v1.GroupName,
|
|
Version: "v1",
|
|
Resource: "persistentvolumes",
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
// Object to insert into fake kubeclient before the test starts.
|
|
initialObjects []runtime.Object
|
|
// Optional client reactors.
|
|
reactors []reaction
|
|
// PV event to simulate. This PV will be automatically added to
|
|
// initalObjects.
|
|
updatedPV *v1.PersistentVolume
|
|
// List of expected kubeclient actions that should happen during the
|
|
// test.
|
|
expectedActions []clienttesting.Action
|
|
storageObjectInUseProtectionEnabled bool
|
|
}{
|
|
// PV events
|
|
//
|
|
{
|
|
name: "StorageObjectInUseProtection Enabled, PV without finalizer -> finalizer is added",
|
|
updatedPV: pv(),
|
|
expectedActions: []clienttesting.Action{
|
|
clienttesting.NewUpdateAction(pvVer, "", withProtectionFinalizer(pv())),
|
|
},
|
|
storageObjectInUseProtectionEnabled: true,
|
|
},
|
|
{
|
|
name: "StorageObjectInUseProtection Disabled, PV without finalizer -> finalizer is added",
|
|
updatedPV: pv(),
|
|
expectedActions: []clienttesting.Action{},
|
|
storageObjectInUseProtectionEnabled: false,
|
|
},
|
|
{
|
|
name: "PVC with finalizer -> no action",
|
|
updatedPV: withProtectionFinalizer(pv()),
|
|
expectedActions: []clienttesting.Action{},
|
|
storageObjectInUseProtectionEnabled: true,
|
|
},
|
|
{
|
|
name: "saving PVC finalizer fails -> controller retries",
|
|
updatedPV: pv(),
|
|
reactors: []reaction{
|
|
{
|
|
verb: "update",
|
|
resource: "persistentvolumes",
|
|
reactorfn: generateUpdateErrorFunc(t, 2 /* update fails twice*/),
|
|
},
|
|
},
|
|
expectedActions: []clienttesting.Action{
|
|
// This fails
|
|
clienttesting.NewUpdateAction(pvVer, "", withProtectionFinalizer(pv())),
|
|
// This fails too
|
|
clienttesting.NewUpdateAction(pvVer, "", withProtectionFinalizer(pv())),
|
|
// This succeeds
|
|
clienttesting.NewUpdateAction(pvVer, "", withProtectionFinalizer(pv())),
|
|
},
|
|
storageObjectInUseProtectionEnabled: true,
|
|
},
|
|
{
|
|
name: "StorageObjectInUseProtection Enabled, deleted PV with finalizer -> finalizer is removed",
|
|
updatedPV: deleted(withProtectionFinalizer(pv())),
|
|
expectedActions: []clienttesting.Action{
|
|
clienttesting.NewUpdateAction(pvVer, "", deleted(pv())),
|
|
},
|
|
storageObjectInUseProtectionEnabled: true,
|
|
},
|
|
{
|
|
name: "StorageObjectInUseProtection Disabled, deleted PV with finalizer -> finalizer is removed",
|
|
updatedPV: deleted(withProtectionFinalizer(pv())),
|
|
expectedActions: []clienttesting.Action{
|
|
clienttesting.NewUpdateAction(pvVer, "", deleted(pv())),
|
|
},
|
|
storageObjectInUseProtectionEnabled: false,
|
|
},
|
|
{
|
|
name: "finalizer removal fails -> controller retries",
|
|
updatedPV: deleted(withProtectionFinalizer(pv())),
|
|
reactors: []reaction{
|
|
{
|
|
verb: "update",
|
|
resource: "persistentvolumes",
|
|
reactorfn: generateUpdateErrorFunc(t, 2 /* update fails twice*/),
|
|
},
|
|
},
|
|
expectedActions: []clienttesting.Action{
|
|
// Fails
|
|
clienttesting.NewUpdateAction(pvVer, "", deleted(pv())),
|
|
// Fails too
|
|
clienttesting.NewUpdateAction(pvVer, "", deleted(pv())),
|
|
// Succeeds
|
|
clienttesting.NewUpdateAction(pvVer, "", deleted(pv())),
|
|
},
|
|
storageObjectInUseProtectionEnabled: true,
|
|
},
|
|
{
|
|
name: "deleted PVC with finalizer + PV is bound -> finalizer is not removed",
|
|
updatedPV: deleted(withProtectionFinalizer(boundPV())),
|
|
expectedActions: []clienttesting.Action{},
|
|
storageObjectInUseProtectionEnabled: true,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
// Create client with initial data
|
|
objs := test.initialObjects
|
|
if test.updatedPV != nil {
|
|
objs = append(objs, test.updatedPV)
|
|
}
|
|
|
|
client := fake.NewSimpleClientset(objs...)
|
|
|
|
// Create informers
|
|
informers := informers.NewSharedInformerFactory(client, controller.NoResyncPeriodFunc())
|
|
pvInformer := informers.Core().V1().PersistentVolumes()
|
|
|
|
// Populate the informers with initial objects so the controller can
|
|
// Get() it.
|
|
for _, obj := range objs {
|
|
switch obj.(type) {
|
|
case *v1.PersistentVolume:
|
|
pvInformer.Informer().GetStore().Add(obj)
|
|
default:
|
|
t.Fatalf("Unknown initalObject type: %+v", obj)
|
|
}
|
|
}
|
|
|
|
// Add reactor to inject test errors.
|
|
for _, reactor := range test.reactors {
|
|
client.Fake.PrependReactor(reactor.verb, reactor.resource, reactor.reactorfn)
|
|
}
|
|
|
|
// Create the controller
|
|
ctrl := NewPVProtectionController(pvInformer, client, test.storageObjectInUseProtectionEnabled)
|
|
|
|
// Start the test by simulating an event
|
|
if test.updatedPV != nil {
|
|
ctrl.pvAddedUpdated(test.updatedPV)
|
|
}
|
|
|
|
// Process the controller queue until we get expected results
|
|
timeout := time.Now().Add(10 * time.Second)
|
|
lastReportedActionCount := 0
|
|
for {
|
|
if time.Now().After(timeout) {
|
|
t.Errorf("Test %q: timed out", test.name)
|
|
break
|
|
}
|
|
if ctrl.queue.Len() > 0 {
|
|
glog.V(5).Infof("Test %q: %d events queue, processing one", test.name, ctrl.queue.Len())
|
|
ctrl.processNextWorkItem()
|
|
}
|
|
if ctrl.queue.Len() > 0 {
|
|
// There is still some work in the queue, process it now
|
|
continue
|
|
}
|
|
currentActionCount := len(client.Actions())
|
|
if currentActionCount < len(test.expectedActions) {
|
|
// Do not log evey wait, only when the action count changes.
|
|
if lastReportedActionCount < currentActionCount {
|
|
glog.V(5).Infof("Test %q: got %d actions out of %d, waiting for the rest", test.name, currentActionCount, len(test.expectedActions))
|
|
lastReportedActionCount = currentActionCount
|
|
}
|
|
// The test expected more to happen, wait for the actions.
|
|
// Most probably it's exponential backoff
|
|
time.Sleep(10 * time.Millisecond)
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
actions := client.Actions()
|
|
|
|
if !reflect.DeepEqual(actions, test.expectedActions) {
|
|
t.Errorf("Test %q: action not expected\nExpected:\n%s\ngot:\n%s", test.name, spew.Sdump(test.expectedActions), spew.Sdump(actions))
|
|
}
|
|
|
|
}
|
|
|
|
}
|