recycler: unit tests

- Add reclaim policy to newVolume() call.
- Implement reactor Volumes().Get().
- Implement mock volume plugin.
- Add recycler tests.
- Add a synchronization condition to controller.scheduleOperation
  - we need to pause the controller here, let the test to do some bad things
    to the controller and test error cases in recycleVolumeOperation.

Test framework gets more and more complicated... But this is the last piece,
I promise.
This commit is contained in:
Jan Safranek
2016-05-17 14:55:22 +02:00
parent a08d826ca5
commit 22e68d4622
5 changed files with 502 additions and 83 deletions

View File

@@ -23,6 +23,7 @@ import (
"strconv"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
@@ -41,6 +42,7 @@ import (
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/types"
"k8s.io/kubernetes/pkg/util/diff"
vol "k8s.io/kubernetes/pkg/volume"
)
// This is a unit test framework for persistent volume controller.
@@ -185,6 +187,17 @@ func (r *volumeReactor) React(action core.Action) (handled bool, ret runtime.Obj
r.changedSinceLastSync++
glog.V(4).Infof("saved updated claim %s", claim.Name)
return true, claim, nil
case action.Matches("get", "persistentvolumes"):
name := action.(core.GetAction).GetName()
volume, found := r.volumes[name]
if found {
glog.V(4).Infof("GetVolume: found %s", volume.Name)
return true, volume, nil
} else {
glog.V(4).Infof("GetVolume: volume %s not found", name)
return true, nil, fmt.Errorf("Cannot find volume %s", name)
}
}
return false, nil, nil
}
@@ -382,16 +395,31 @@ func newVolumeReactor(client *fake.Clientset, ctrl *PersistentVolumeController,
func newPersistentVolumeController(kubeClient clientset.Interface) *PersistentVolumeController {
ctrl := &PersistentVolumeController{
volumes: newPersistentVolumeOrderedIndex(),
claims: cache.NewStore(cache.MetaNamespaceKeyFunc),
kubeClient: kubeClient,
eventRecorder: record.NewFakeRecorder(1000),
volumes: newPersistentVolumeOrderedIndex(),
claims: cache.NewStore(cache.MetaNamespaceKeyFunc),
kubeClient: kubeClient,
eventRecorder: record.NewFakeRecorder(1000),
runningOperations: make(map[string]bool),
}
return ctrl
}
func addRecyclePlugin(ctrl *PersistentVolumeController, expectedRecycleCalls []error) {
plugin := &mockVolumePlugin{
recycleCalls: expectedRecycleCalls,
}
ctrl.recyclePluginMgr.InitPlugins([]vol.VolumePlugin{plugin}, ctrl)
}
func addDeletePlugin(ctrl *PersistentVolumeController, expectedDeleteCalls []error) {
plugin := &mockVolumePlugin{
deleteCalls: expectedDeleteCalls,
}
ctrl.recyclePluginMgr.InitPlugins([]vol.VolumePlugin{plugin}, ctrl)
}
// newVolume returns a new volume with given attributes
func newVolume(name, capacity, boundToClaimUID, boundToClaimName string, phase api.PersistentVolumePhase, annotations ...string) *api.PersistentVolume {
func newVolume(name, capacity, boundToClaimUID, boundToClaimName string, phase api.PersistentVolumePhase, reclaimPolicy api.PersistentVolumeReclaimPolicy, annotations ...string) *api.PersistentVolume {
volume := api.PersistentVolume{
ObjectMeta: api.ObjectMeta{
Name: name,
@@ -404,7 +432,8 @@ func newVolume(name, capacity, boundToClaimUID, boundToClaimName string, phase a
PersistentVolumeSource: api.PersistentVolumeSource{
GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{},
},
AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce, api.ReadOnlyMany},
AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce, api.ReadOnlyMany},
PersistentVolumeReclaimPolicy: reclaimPolicy,
},
Status: api.PersistentVolumeStatus{
Phase: phase,
@@ -433,9 +462,9 @@ func newVolume(name, capacity, boundToClaimUID, boundToClaimName string, phase a
// newVolumeArray returns array with a single volume that would be returned by
// newVolume() with the same parameters.
func newVolumeArray(name, capacity, boundToClaimUID, boundToClaimName string, phase api.PersistentVolumePhase, annotations ...string) []*api.PersistentVolume {
func newVolumeArray(name, capacity, boundToClaimUID, boundToClaimName string, phase api.PersistentVolumePhase, reclaimPolicy api.PersistentVolumeReclaimPolicy, annotations ...string) []*api.PersistentVolume {
return []*api.PersistentVolume{
newVolume(name, capacity, boundToClaimUID, boundToClaimName, phase, annotations...),
newVolume(name, capacity, boundToClaimUID, boundToClaimName, phase, reclaimPolicy, annotations...),
}
}
@@ -498,6 +527,66 @@ func testSyncVolume(ctrl *PersistentVolumeController, reactor *volumeReactor, te
return ctrl.syncVolume(test.initialVolumes[0])
}
type operationType string
const operationDelete = "Delete"
const operationRecycle = "Recycle"
// wrapTestWithControllerConfig returns a testCall that:
// - configures controller with recycler or deleter which will return provided
// errors when a volume is deleted or recycled.
// - calls given testCall
func wrapTestWithControllerConfig(operation operationType, expectedOperationCalls []error, toWrap testCall) testCall {
expected := expectedOperationCalls
return func(ctrl *PersistentVolumeController, reactor *volumeReactor, test controllerTest) error {
switch operation {
case operationDelete:
addDeletePlugin(ctrl, expected)
case operationRecycle:
addRecyclePlugin(ctrl, expected)
}
return toWrap(ctrl, reactor, test)
}
}
// wrapTestWithInjectedOperation returns a testCall that:
// - starts the controller and lets it run original testCall until
// scheduleOperation() call. It blocks the controller there and calls the
// injected function to simulate that something is happenning when the
// controller waits for the operation lock. Controller is then resumed and we
// check how it behaves.
func wrapTestWithInjectedOperation(toWrap testCall, injectBeforeOperation func(ctrl *PersistentVolumeController, reactor *volumeReactor)) testCall {
return func(ctrl *PersistentVolumeController, reactor *volumeReactor, test controllerTest) error {
// Inject a hook before async operation starts
ctrl.preOperationHook = func(operationName string, arg interface{}) {
// Inside the hook, run the function to inject
glog.V(4).Infof("reactor: scheduleOperation reached, injecting call")
injectBeforeOperation(ctrl, reactor)
}
// Run the tested function (typically syncClaim/syncVolume) in a
// separate goroutine.
var testError error
var testFinished int32
go func() {
testError = toWrap(ctrl, reactor, test)
// Let the "main" test function know that syncVolume has finished.
atomic.StoreInt32(&testFinished, 1)
}()
// Wait for the controler to finish the test function.
for atomic.LoadInt32(&testFinished) == 0 {
time.Sleep(time.Millisecond * 10)
}
return testError
}
}
func evaluateTestResults(ctrl *PersistentVolumeController, reactor *volumeReactor, test controllerTest, t *testing.T) {
// Evaluate results
if err := reactor.checkClaims(t, test.expectedClaims); err != nil {
@@ -542,6 +631,9 @@ func runSyncTests(t *testing.T, tests []controllerTest) {
t.Errorf("Test %q failed: %v", test.name, err)
}
// Wait for all goroutines to finish
reactor.waitTest()
evaluateTestResults(ctrl, reactor, test, t)
}
}
@@ -596,6 +688,9 @@ func runMultisyncTests(t *testing.T, tests []controllerTest) {
break
}
// Wait for all goroutines to finish
reactor.waitTest()
obj := reactor.popChange()
if obj == nil {
// Nothing was changed, should we exit?
@@ -655,3 +750,122 @@ func runMultisyncTests(t *testing.T, tests []controllerTest) {
glog.V(4).Infof("test %q finished after %d iterations", test.name, counter)
}
}
// Dummy volume plugin for provisioning, deletion and recycling. It contains
// lists of expected return values to simulate errors.
type mockVolumePlugin struct {
provisionCalls []error
provisionCallCounter int
deleteCalls []error
deleteCallCounter int
recycleCalls []error
recycleCallCounter int
}
var _ vol.VolumePlugin = &mockVolumePlugin{}
func (plugin *mockVolumePlugin) Init(host vol.VolumeHost) error {
return nil
}
func (plugin *mockVolumePlugin) Name() string {
return "mockVolumePlugin"
}
func (plugin *mockVolumePlugin) CanSupport(spec *vol.Spec) bool {
return true
}
func (plugin *mockVolumePlugin) NewMounter(spec *vol.Spec, podRef *api.Pod, opts vol.VolumeOptions) (vol.Mounter, error) {
return nil, fmt.Errorf("Mounter is not supported by this plugin")
}
func (plugin *mockVolumePlugin) NewUnmounter(name string, podUID types.UID) (vol.Unmounter, error) {
return nil, fmt.Errorf("Unmounter is not supported by this plugin")
}
// Provisioner interfaces
func (plugin *mockVolumePlugin) NewProvisioner(options vol.VolumeOptions) (vol.Provisioner, error) {
if len(plugin.provisionCalls) > 0 {
// mockVolumePlugin directly implements Provisioner interface
glog.V(4).Infof("mock plugin NewProvisioner called, returning mock provisioner")
return plugin, nil
} else {
return nil, fmt.Errorf("Mock plugin error: no provisionCalls configured")
}
}
func (plugin *mockVolumePlugin) Provision(*api.PersistentVolume) error {
if len(plugin.provisionCalls) <= plugin.provisionCallCounter {
return fmt.Errorf("Mock plugin error: unexpected provisioner call %d", plugin.provisionCallCounter)
}
ret := plugin.provisionCalls[plugin.provisionCallCounter]
plugin.provisionCallCounter++
glog.V(4).Infof("mock plugin Provision call nr. %d, returning %v", plugin.provisionCallCounter, ret)
return ret
}
func (plugin *mockVolumePlugin) NewPersistentVolumeTemplate() (*api.PersistentVolume, error) {
if len(plugin.provisionCalls) <= plugin.provisionCallCounter {
return nil, fmt.Errorf("Mock plugin error: unexpected provisioner call %d", plugin.provisionCallCounter)
}
ret := plugin.provisionCalls[plugin.provisionCallCounter]
plugin.provisionCallCounter++
glog.V(4).Infof("mock plugin NewPersistentVolumeTemplate call nr. %d, returning %v", plugin.provisionCallCounter, ret)
return nil, ret
}
// Deleter interfaces
func (plugin *mockVolumePlugin) NewDeleter(spec *vol.Spec) (vol.Deleter, error) {
if len(plugin.deleteCalls) > 0 {
// mockVolumePlugin directly implements Deleter interface
glog.V(4).Infof("mock plugin NewDeleter called, returning mock deleter")
return plugin, nil
} else {
return nil, fmt.Errorf("Mock plugin error: no deleteCalls configured")
}
}
func (plugin *mockVolumePlugin) Delete() error {
if len(plugin.deleteCalls) <= plugin.deleteCallCounter {
return fmt.Errorf("Mock plugin error: unexpected deleter call %d", plugin.deleteCallCounter)
}
ret := plugin.deleteCalls[plugin.deleteCallCounter]
plugin.deleteCallCounter++
glog.V(4).Infof("mock plugin Delete call nr. %d, returning %v", plugin.deleteCallCounter, ret)
return ret
}
// Volume interfaces
func (plugin *mockVolumePlugin) GetPath() string {
return ""
}
func (plugin *mockVolumePlugin) GetMetrics() (*vol.Metrics, error) {
return nil, nil
}
// Recycler interfaces
func (plugin *mockVolumePlugin) NewRecycler(spec *vol.Spec) (vol.Recycler, error) {
if len(plugin.recycleCalls) > 0 {
// mockVolumePlugin directly implements Recycler interface
glog.V(4).Infof("mock plugin NewRecycler called, returning mock recycler")
return plugin, nil
} else {
return nil, fmt.Errorf("Mock plugin error: no recycleCalls configured")
}
}
func (plugin *mockVolumePlugin) Recycle() error {
if len(plugin.recycleCalls) <= plugin.recycleCallCounter {
return fmt.Errorf("Mock plugin error: unexpected recycle call %d", plugin.recycleCallCounter)
}
ret := plugin.recycleCalls[plugin.recycleCallCounter]
plugin.recycleCallCounter++
glog.V(4).Infof("mock plugin Recycle call nr. %d, returning %v", plugin.recycleCallCounter, ret)
return ret
}