mirror of
				https://github.com/k3s-io/kubernetes.git
				synced 2025-11-04 07:49:35 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			259 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			259 lines
		
	
	
		
			7.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 (
 | 
						|
	"context"
 | 
						|
	"errors"
 | 
						|
	"reflect"
 | 
						|
	"testing"
 | 
						|
	"time"
 | 
						|
 | 
						|
	v1 "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/apimachinery/pkg/util/dump"
 | 
						|
	"k8s.io/client-go/informers"
 | 
						|
	"k8s.io/client-go/kubernetes/fake"
 | 
						|
	clienttesting "k8s.io/client-go/testing"
 | 
						|
	"k8s.io/klog/v2/ktesting"
 | 
						|
	"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
 | 
						|
		// initialObjects.
 | 
						|
		updatedPV *v1.PersistentVolume
 | 
						|
		// List of expected kubeclient actions that should happen during the
 | 
						|
		// test.
 | 
						|
		expectedActions []clienttesting.Action
 | 
						|
	}{
 | 
						|
		// PV events
 | 
						|
		//
 | 
						|
		{
 | 
						|
			name:      "PV without finalizer -> finalizer is added",
 | 
						|
			updatedPV: pv(),
 | 
						|
			expectedActions: []clienttesting.Action{
 | 
						|
				clienttesting.NewUpdateAction(pvVer, "", withProtectionFinalizer(pv())),
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:            "PVC with finalizer -> no action",
 | 
						|
			updatedPV:       withProtectionFinalizer(pv()),
 | 
						|
			expectedActions: []clienttesting.Action{},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			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())),
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "deleted PV with finalizer -> finalizer is removed",
 | 
						|
			updatedPV: deleted(withProtectionFinalizer(pv())),
 | 
						|
			expectedActions: []clienttesting.Action{
 | 
						|
				clienttesting.NewUpdateAction(pvVer, "", deleted(pv())),
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			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())),
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:            "deleted PVC with finalizer + PV is bound -> finalizer is not removed",
 | 
						|
			updatedPV:       deleted(withProtectionFinalizer(boundPV())),
 | 
						|
			expectedActions: []clienttesting.Action{},
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	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 initialObject 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
 | 
						|
		logger, _ := ktesting.NewTestContext(t)
 | 
						|
		ctrl := NewPVProtectionController(logger, pvInformer, client)
 | 
						|
 | 
						|
		// Start the test by simulating an event
 | 
						|
		if test.updatedPV != nil {
 | 
						|
			ctrl.pvAddedUpdated(logger, 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 {
 | 
						|
				logger.V(5).Info("Non-empty events queue, processing one", "test", test.name, "queueLength", ctrl.queue.Len())
 | 
						|
				ctrl.processNextWorkItem(context.TODO())
 | 
						|
			}
 | 
						|
			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 {
 | 
						|
					logger.V(5).Info("Waiting for the remaining actions", "test", test.name, "currentActionCount", currentActionCount, "expectedActionCount", 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, dump.Pretty(test.expectedActions), dump.Pretty(actions))
 | 
						|
		}
 | 
						|
 | 
						|
	}
 | 
						|
 | 
						|
}
 |