123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420 |
- /*
- Copyright 2017 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 pvcprotection
- import (
- "errors"
- "reflect"
- "testing"
- "time"
- "github.com/davecgh/go-spew/spew"
- "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/klog"
- "k8s.io/kubernetes/pkg/controller"
- volumeutil "k8s.io/kubernetes/pkg/volume/util"
- )
- type reaction struct {
- verb string
- resource string
- reactorfn clienttesting.ReactionFunc
- }
- const (
- defaultNS = "default"
- defaultPVCName = "pvc1"
- defaultPodName = "pod1"
- defaultNodeName = "node1"
- )
- func pod() *v1.Pod {
- return &v1.Pod{
- ObjectMeta: metav1.ObjectMeta{
- Name: defaultPodName,
- Namespace: defaultNS,
- },
- Spec: v1.PodSpec{
- NodeName: defaultNodeName,
- },
- Status: v1.PodStatus{
- Phase: v1.PodPending,
- },
- }
- }
- func unscheduled(pod *v1.Pod) *v1.Pod {
- pod.Spec.NodeName = ""
- return pod
- }
- func withPVC(pvcName string, pod *v1.Pod) *v1.Pod {
- volume := v1.Volume{
- Name: pvcName,
- VolumeSource: v1.VolumeSource{
- PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
- ClaimName: pvcName,
- },
- },
- }
- pod.Spec.Volumes = append(pod.Spec.Volumes, volume)
- return pod
- }
- func withEmptyDir(pod *v1.Pod) *v1.Pod {
- volume := v1.Volume{
- Name: "emptyDir",
- VolumeSource: v1.VolumeSource{
- EmptyDir: &v1.EmptyDirVolumeSource{},
- },
- }
- pod.Spec.Volumes = append(pod.Spec.Volumes, volume)
- return pod
- }
- func withStatus(phase v1.PodPhase, pod *v1.Pod) *v1.Pod {
- pod.Status.Phase = phase
- return pod
- }
- func pvc() *v1.PersistentVolumeClaim {
- return &v1.PersistentVolumeClaim{
- ObjectMeta: metav1.ObjectMeta{
- Name: defaultPVCName,
- Namespace: defaultNS,
- },
- }
- }
- func withProtectionFinalizer(pvc *v1.PersistentVolumeClaim) *v1.PersistentVolumeClaim {
- pvc.Finalizers = append(pvc.Finalizers, volumeutil.PVCProtectionFinalizer)
- return pvc
- }
- func deleted(pvc *v1.PersistentVolumeClaim) *v1.PersistentVolumeClaim {
- pvc.DeletionTimestamp = &metav1.Time{}
- return pvc
- }
- 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 TestPVCProtectionController(t *testing.T) {
- pvcVer := schema.GroupVersionResource{
- Group: v1.GroupName,
- Version: "v1",
- Resource: "persistentvolumeclaims",
- }
- tests := []struct {
- name string
- // Object to insert into fake kubeclient before the test starts.
- initialObjects []runtime.Object
- // Optional client reactors.
- reactors []reaction
- // PVC event to simulate. This PVC will be automatically added to
- // initalObjects.
- updatedPVC *v1.PersistentVolumeClaim
- // Pod event to simulate. This Pod will be automatically added to
- // initalObjects.
- updatedPod *v1.Pod
- // Pod event to similate. This Pod is *not* added to
- // initalObjects.
- deletedPod *v1.Pod
- // List of expected kubeclient actions that should happen during the
- // test.
- expectedActions []clienttesting.Action
- storageObjectInUseProtectionEnabled bool
- }{
- //
- // PVC events
- //
- {
- name: "StorageObjectInUseProtection Enabled, PVC without finalizer -> finalizer is added",
- updatedPVC: pvc(),
- expectedActions: []clienttesting.Action{
- clienttesting.NewUpdateAction(pvcVer, defaultNS, withProtectionFinalizer(pvc())),
- },
- storageObjectInUseProtectionEnabled: true,
- },
- {
- name: "StorageObjectInUseProtection Disabled, PVC without finalizer -> finalizer is added",
- updatedPVC: pvc(),
- expectedActions: []clienttesting.Action{},
- storageObjectInUseProtectionEnabled: false,
- },
- {
- name: "PVC with finalizer -> no action",
- updatedPVC: withProtectionFinalizer(pvc()),
- expectedActions: []clienttesting.Action{},
- storageObjectInUseProtectionEnabled: true,
- },
- {
- name: "saving PVC finalizer fails -> controller retries",
- updatedPVC: pvc(),
- reactors: []reaction{
- {
- verb: "update",
- resource: "persistentvolumeclaims",
- reactorfn: generateUpdateErrorFunc(t, 2 /* update fails twice*/),
- },
- },
- expectedActions: []clienttesting.Action{
- // This fails
- clienttesting.NewUpdateAction(pvcVer, defaultNS, withProtectionFinalizer(pvc())),
- // This fails too
- clienttesting.NewUpdateAction(pvcVer, defaultNS, withProtectionFinalizer(pvc())),
- // This succeeds
- clienttesting.NewUpdateAction(pvcVer, defaultNS, withProtectionFinalizer(pvc())),
- },
- storageObjectInUseProtectionEnabled: true,
- },
- {
- name: "StorageObjectInUseProtection Enabled, deleted PVC with finalizer -> finalizer is removed",
- updatedPVC: deleted(withProtectionFinalizer(pvc())),
- expectedActions: []clienttesting.Action{
- clienttesting.NewUpdateAction(pvcVer, defaultNS, deleted(pvc())),
- },
- storageObjectInUseProtectionEnabled: true,
- },
- {
- name: "StorageObjectInUseProtection Disabled, deleted PVC with finalizer -> finalizer is removed",
- updatedPVC: deleted(withProtectionFinalizer(pvc())),
- expectedActions: []clienttesting.Action{
- clienttesting.NewUpdateAction(pvcVer, defaultNS, deleted(pvc())),
- },
- storageObjectInUseProtectionEnabled: false,
- },
- {
- name: "finalizer removal fails -> controller retries",
- updatedPVC: deleted(withProtectionFinalizer(pvc())),
- reactors: []reaction{
- {
- verb: "update",
- resource: "persistentvolumeclaims",
- reactorfn: generateUpdateErrorFunc(t, 2 /* update fails twice*/),
- },
- },
- expectedActions: []clienttesting.Action{
- // Fails
- clienttesting.NewUpdateAction(pvcVer, defaultNS, deleted(pvc())),
- // Fails too
- clienttesting.NewUpdateAction(pvcVer, defaultNS, deleted(pvc())),
- // Succeeds
- clienttesting.NewUpdateAction(pvcVer, defaultNS, deleted(pvc())),
- },
- storageObjectInUseProtectionEnabled: true,
- },
- {
- name: "deleted PVC with finalizer + pods with the PVC exists -> finalizer is not removed",
- initialObjects: []runtime.Object{
- withPVC(defaultPVCName, pod()),
- },
- updatedPVC: deleted(withProtectionFinalizer(pvc())),
- expectedActions: []clienttesting.Action{},
- },
- {
- name: "deleted PVC with finalizer + pods with unrelated PVC and EmptyDir exists -> finalizer is removed",
- initialObjects: []runtime.Object{
- withEmptyDir(withPVC("unrelatedPVC", pod())),
- },
- updatedPVC: deleted(withProtectionFinalizer(pvc())),
- expectedActions: []clienttesting.Action{
- clienttesting.NewUpdateAction(pvcVer, defaultNS, deleted(pvc())),
- },
- storageObjectInUseProtectionEnabled: true,
- },
- {
- name: "deleted PVC with finalizer + pods with the PVC finished but is not deleted -> finalizer is not removed",
- initialObjects: []runtime.Object{
- withStatus(v1.PodFailed, withPVC(defaultPVCName, pod())),
- },
- updatedPVC: deleted(withProtectionFinalizer(pvc())),
- expectedActions: []clienttesting.Action{},
- storageObjectInUseProtectionEnabled: true,
- },
- //
- // Pod events
- //
- {
- name: "updated running Pod -> no action",
- initialObjects: []runtime.Object{
- deleted(withProtectionFinalizer(pvc())),
- },
- updatedPod: withStatus(v1.PodRunning, withPVC(defaultPVCName, pod())),
- expectedActions: []clienttesting.Action{},
- storageObjectInUseProtectionEnabled: true,
- },
- {
- name: "updated finished Pod -> finalizer is not removed",
- initialObjects: []runtime.Object{
- deleted(withProtectionFinalizer(pvc())),
- },
- updatedPod: withStatus(v1.PodSucceeded, withPVC(defaultPVCName, pod())),
- expectedActions: []clienttesting.Action{},
- storageObjectInUseProtectionEnabled: true,
- },
- {
- name: "updated unscheduled Pod -> finalizer is removed",
- initialObjects: []runtime.Object{
- deleted(withProtectionFinalizer(pvc())),
- },
- updatedPod: unscheduled(withPVC(defaultPVCName, pod())),
- expectedActions: []clienttesting.Action{
- clienttesting.NewUpdateAction(pvcVer, defaultNS, deleted(pvc())),
- },
- storageObjectInUseProtectionEnabled: true,
- },
- {
- name: "deleted running Pod -> finalizer is removed",
- initialObjects: []runtime.Object{
- deleted(withProtectionFinalizer(pvc())),
- },
- deletedPod: withStatus(v1.PodRunning, withPVC(defaultPVCName, pod())),
- expectedActions: []clienttesting.Action{
- clienttesting.NewUpdateAction(pvcVer, defaultNS, deleted(pvc())),
- },
- storageObjectInUseProtectionEnabled: true,
- },
- }
- for _, test := range tests {
- // Create client with initial data
- objs := test.initialObjects
- if test.updatedPVC != nil {
- objs = append(objs, test.updatedPVC)
- }
- if test.updatedPod != nil {
- objs = append(objs, test.updatedPod)
- }
- client := fake.NewSimpleClientset(objs...)
- // Create informers
- informers := informers.NewSharedInformerFactory(client, controller.NoResyncPeriodFunc())
- pvcInformer := informers.Core().V1().PersistentVolumeClaims()
- podInformer := informers.Core().V1().Pods()
- // Populate the informers with initial objects so the controller can
- // Get() and List() it.
- for _, obj := range objs {
- switch obj.(type) {
- case *v1.PersistentVolumeClaim:
- pvcInformer.Informer().GetStore().Add(obj)
- case *v1.Pod:
- podInformer.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 := NewPVCProtectionController(pvcInformer, podInformer, client, test.storageObjectInUseProtectionEnabled)
- // Start the test by simulating an event
- if test.updatedPVC != nil {
- ctrl.pvcAddedUpdated(test.updatedPVC)
- }
- if test.updatedPod != nil {
- ctrl.podAddedDeletedUpdated(test.updatedPod, false)
- }
- if test.deletedPod != nil {
- ctrl.podAddedDeletedUpdated(test.deletedPod, true)
- }
- // 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 {
- klog.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 {
- klog.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()
- for i, action := range actions {
- if len(test.expectedActions) < i+1 {
- t.Errorf("Test %q: %d unexpected actions: %+v", test.name, len(actions)-len(test.expectedActions), spew.Sdump(actions[i:]))
- break
- }
- expectedAction := test.expectedActions[i]
- if !reflect.DeepEqual(expectedAction, action) {
- t.Errorf("Test %q: action %d\nExpected:\n%s\ngot:\n%s", test.name, i, spew.Sdump(expectedAction), spew.Sdump(action))
- }
- }
- if len(test.expectedActions) > len(actions) {
- t.Errorf("Test %q: %d additional expected actions", test.name, len(test.expectedActions)-len(actions))
- for _, a := range test.expectedActions[len(actions):] {
- t.Logf(" %+v", a)
- }
- }
- }
- }
|