mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-11-04 12:18:16 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			895 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			895 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
/*
 | 
						|
Copyright 2016 The Kubernetes Authors All rights reserved.
 | 
						|
 | 
						|
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 persistentvolume
 | 
						|
 | 
						|
import (
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"reflect"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
	"sync"
 | 
						|
	"sync/atomic"
 | 
						|
	"testing"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/golang/glog"
 | 
						|
 | 
						|
	"k8s.io/kubernetes/pkg/api"
 | 
						|
	"k8s.io/kubernetes/pkg/api/resource"
 | 
						|
	"k8s.io/kubernetes/pkg/api/testapi"
 | 
						|
	"k8s.io/kubernetes/pkg/client/cache"
 | 
						|
	clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
 | 
						|
	"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
 | 
						|
	"k8s.io/kubernetes/pkg/client/record"
 | 
						|
	"k8s.io/kubernetes/pkg/client/testing/core"
 | 
						|
	"k8s.io/kubernetes/pkg/controller/framework"
 | 
						|
	"k8s.io/kubernetes/pkg/conversion"
 | 
						|
	"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.
 | 
						|
// It fills the controller with test claims/volumes and can simulate these
 | 
						|
// scenarios:
 | 
						|
// 1) Call syncClaim/syncVolume once.
 | 
						|
// 2) Call syncClaim/syncVolume several times (both simulating "claim/volume
 | 
						|
//    modified" events and periodic sync), until the controller settles down and
 | 
						|
//    does not modify anything.
 | 
						|
// 3) Simulate almost real API server/etcd and call add/update/delete
 | 
						|
//    volume/claim.
 | 
						|
// In all these scenarios, when the test finishes, the framework can compare
 | 
						|
// resulting claims/volumes with list of expected claims/volumes and report
 | 
						|
// differences.
 | 
						|
 | 
						|
// controllerTest contains a single controller test input.
 | 
						|
// Each test has initial set of volumes and claims that are filled into the
 | 
						|
// controller before the test starts. The test then contains a reference to
 | 
						|
// function to call as the actual test. Available functions are:
 | 
						|
//   - testSyncClaim - calls syncClaim on the first claim in initialClaims.
 | 
						|
//   - testSyncClaimError - calls syncClaim on the first claim in initialClaims
 | 
						|
//                          and expects an error to be returned.
 | 
						|
//   - testSyncVolume - calls syncVolume on the first volume in initialVolumes.
 | 
						|
//   - any custom function for specialized tests.
 | 
						|
// The test then contains list of volumes/claims that are expected at the end
 | 
						|
// of the test and list of generated events.
 | 
						|
type controllerTest struct {
 | 
						|
	// Name of the test, for logging
 | 
						|
	name string
 | 
						|
	// Initial content of controller volume cache.
 | 
						|
	initialVolumes []*api.PersistentVolume
 | 
						|
	// Expected content of controller volume cache at the end of the test.
 | 
						|
	expectedVolumes []*api.PersistentVolume
 | 
						|
	// Initial content of controller claim cache.
 | 
						|
	initialClaims []*api.PersistentVolumeClaim
 | 
						|
	// Expected content of controller claim cache at the end of the test.
 | 
						|
	expectedClaims []*api.PersistentVolumeClaim
 | 
						|
	// Expected events - any event with prefix will pass, we don't check full
 | 
						|
	// event message.
 | 
						|
	expectedEvents []string
 | 
						|
	// Function to call as the test.
 | 
						|
	test testCall
 | 
						|
}
 | 
						|
 | 
						|
type testCall func(ctrl *PersistentVolumeController, reactor *volumeReactor, test controllerTest) error
 | 
						|
 | 
						|
const testNamespace = "default"
 | 
						|
 | 
						|
var versionConflictError = errors.New("VersionError")
 | 
						|
var novolumes []*api.PersistentVolume
 | 
						|
var noclaims []*api.PersistentVolumeClaim
 | 
						|
var noevents = []string{}
 | 
						|
 | 
						|
// volumeReactor is a core.Reactor that simulates etcd and API server. It
 | 
						|
// stores:
 | 
						|
// - Latest version of claims volumes saved by the controller.
 | 
						|
// - Queue of all saves (to simulate "volume/claim updated" events). This queue
 | 
						|
//   contains all intermediate state of an object - e.g. a claim.VolumeName
 | 
						|
//   is updated first and claim.Phase second. This queue will then contain both
 | 
						|
//   updates as separate entries.
 | 
						|
// - Number of changes since the last call to volumeReactor.syncAll().
 | 
						|
// - Optionally, volume and claim event sources. When set, all changed
 | 
						|
//   volumes/claims are sent as Modify event to these sources. These sources can
 | 
						|
//   be linked back to the controller watcher as "volume/claim updated" events.
 | 
						|
type volumeReactor struct {
 | 
						|
	volumes              map[string]*api.PersistentVolume
 | 
						|
	claims               map[string]*api.PersistentVolumeClaim
 | 
						|
	changedObjects       []interface{}
 | 
						|
	changedSinceLastSync int
 | 
						|
	ctrl                 *PersistentVolumeController
 | 
						|
	volumeSource         *framework.FakeControllerSource
 | 
						|
	claimSource          *framework.FakeControllerSource
 | 
						|
	lock                 sync.Mutex
 | 
						|
}
 | 
						|
 | 
						|
// React is a callback called by fake kubeClient from the controller.
 | 
						|
// In other words, every claim/volume change performed by the controller ends
 | 
						|
// here.
 | 
						|
// This callback checks versions of the updated objects and refuse those that
 | 
						|
// are too old (simulating real etcd).
 | 
						|
// All updated objects are stored locally to keep track of object versions and
 | 
						|
// to evaluate test results.
 | 
						|
// All updated objects are also inserted into changedObjects queue and
 | 
						|
// optionally sent back to the controller via its watchers.
 | 
						|
func (r *volumeReactor) React(action core.Action) (handled bool, ret runtime.Object, err error) {
 | 
						|
	r.lock.Lock()
 | 
						|
	defer r.lock.Unlock()
 | 
						|
 | 
						|
	glog.V(4).Infof("reactor got operation %q on %q", action.GetVerb(), action.GetResource())
 | 
						|
 | 
						|
	switch {
 | 
						|
	case action.Matches("update", "persistentvolumes"):
 | 
						|
		obj := action.(core.UpdateAction).GetObject()
 | 
						|
		volume := obj.(*api.PersistentVolume)
 | 
						|
 | 
						|
		// Check and bump object version
 | 
						|
		storedVolume, found := r.volumes[volume.Name]
 | 
						|
		if found {
 | 
						|
			storedVer, _ := strconv.Atoi(storedVolume.ResourceVersion)
 | 
						|
			requestedVer, _ := strconv.Atoi(volume.ResourceVersion)
 | 
						|
			if storedVer != requestedVer {
 | 
						|
				return true, obj, versionConflictError
 | 
						|
			}
 | 
						|
			volume.ResourceVersion = strconv.Itoa(storedVer + 1)
 | 
						|
		} else {
 | 
						|
			return true, nil, fmt.Errorf("Cannot update volume %s: volume not found", volume.Name)
 | 
						|
		}
 | 
						|
 | 
						|
		// Store the updated object to appropriate places.
 | 
						|
		if r.volumeSource != nil {
 | 
						|
			r.volumeSource.Modify(volume)
 | 
						|
		}
 | 
						|
		r.volumes[volume.Name] = volume
 | 
						|
		r.changedObjects = append(r.changedObjects, volume)
 | 
						|
		r.changedSinceLastSync++
 | 
						|
		glog.V(4).Infof("saved updated volume %s", volume.Name)
 | 
						|
		return true, volume, nil
 | 
						|
 | 
						|
	case action.Matches("update", "persistentvolumeclaims"):
 | 
						|
		obj := action.(core.UpdateAction).GetObject()
 | 
						|
		claim := obj.(*api.PersistentVolumeClaim)
 | 
						|
 | 
						|
		// Check and bump object version
 | 
						|
		storedClaim, found := r.claims[claim.Name]
 | 
						|
		if found {
 | 
						|
			storedVer, _ := strconv.Atoi(storedClaim.ResourceVersion)
 | 
						|
			requestedVer, _ := strconv.Atoi(claim.ResourceVersion)
 | 
						|
			if storedVer != requestedVer {
 | 
						|
				return true, obj, versionConflictError
 | 
						|
			}
 | 
						|
			claim.ResourceVersion = strconv.Itoa(storedVer + 1)
 | 
						|
		} else {
 | 
						|
			return true, nil, fmt.Errorf("Cannot update claim %s: claim not found", claim.Name)
 | 
						|
		}
 | 
						|
 | 
						|
		// Store the updated object to appropriate places.
 | 
						|
		r.claims[claim.Name] = claim
 | 
						|
		if r.claimSource != nil {
 | 
						|
			r.claimSource.Modify(claim)
 | 
						|
		}
 | 
						|
		r.changedObjects = append(r.changedObjects, claim)
 | 
						|
		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)
 | 
						|
		}
 | 
						|
 | 
						|
	case action.Matches("delete", "persistentvolumes"):
 | 
						|
		name := action.(core.DeleteAction).GetName()
 | 
						|
		glog.V(4).Infof("deleted volume %s", name)
 | 
						|
		_, found := r.volumes[name]
 | 
						|
		if found {
 | 
						|
			delete(r.volumes, name)
 | 
						|
			return true, nil, nil
 | 
						|
		} else {
 | 
						|
			return true, nil, fmt.Errorf("Cannot delete volume %s: not found", name)
 | 
						|
		}
 | 
						|
 | 
						|
	case action.Matches("delete", "persistentvolumeclaims"):
 | 
						|
		name := action.(core.DeleteAction).GetName()
 | 
						|
		glog.V(4).Infof("deleted claim %s", name)
 | 
						|
		_, found := r.volumes[name]
 | 
						|
		if found {
 | 
						|
			delete(r.claims, name)
 | 
						|
			return true, nil, nil
 | 
						|
		} else {
 | 
						|
			return true, nil, fmt.Errorf("Cannot delete claim %s: not found", name)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return false, nil, nil
 | 
						|
}
 | 
						|
 | 
						|
// checkVolumes compares all expectedVolumes with set of volumes at the end of
 | 
						|
// the test and reports differences.
 | 
						|
func (r *volumeReactor) checkVolumes(t *testing.T, expectedVolumes []*api.PersistentVolume) error {
 | 
						|
	r.lock.Lock()
 | 
						|
	defer r.lock.Unlock()
 | 
						|
 | 
						|
	expectedMap := make(map[string]*api.PersistentVolume)
 | 
						|
	gotMap := make(map[string]*api.PersistentVolume)
 | 
						|
	// Clear any ResourceVersion from both sets
 | 
						|
	for _, v := range expectedVolumes {
 | 
						|
		v.ResourceVersion = ""
 | 
						|
		expectedMap[v.Name] = v
 | 
						|
	}
 | 
						|
	for _, v := range r.volumes {
 | 
						|
		// We must clone the volume because of golang race check - it was
 | 
						|
		// written by the controller without any locks on it.
 | 
						|
		clone, _ := conversion.NewCloner().DeepCopy(v)
 | 
						|
		v = clone.(*api.PersistentVolume)
 | 
						|
		v.ResourceVersion = ""
 | 
						|
		if v.Spec.ClaimRef != nil {
 | 
						|
			v.Spec.ClaimRef.ResourceVersion = ""
 | 
						|
		}
 | 
						|
		gotMap[v.Name] = v
 | 
						|
	}
 | 
						|
	if !reflect.DeepEqual(expectedMap, gotMap) {
 | 
						|
		// Print ugly but useful diff of expected and received objects for
 | 
						|
		// easier debugging.
 | 
						|
		return fmt.Errorf("Volume check failed [A-expected, B-got]: %s", diff.ObjectDiff(expectedMap, gotMap))
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// checkClaims compares all expectedClaims with set of claims at the end of the
 | 
						|
// test and reports differences.
 | 
						|
func (r *volumeReactor) checkClaims(t *testing.T, expectedClaims []*api.PersistentVolumeClaim) error {
 | 
						|
	r.lock.Lock()
 | 
						|
	defer r.lock.Unlock()
 | 
						|
 | 
						|
	expectedMap := make(map[string]*api.PersistentVolumeClaim)
 | 
						|
	gotMap := make(map[string]*api.PersistentVolumeClaim)
 | 
						|
	for _, c := range expectedClaims {
 | 
						|
		c.ResourceVersion = ""
 | 
						|
		expectedMap[c.Name] = c
 | 
						|
	}
 | 
						|
	for _, c := range r.claims {
 | 
						|
		// We must clone the claim because of golang race check - it was
 | 
						|
		// written by the controller without any locks on it.
 | 
						|
		clone, _ := conversion.NewCloner().DeepCopy(c)
 | 
						|
		c = clone.(*api.PersistentVolumeClaim)
 | 
						|
		c.ResourceVersion = ""
 | 
						|
		gotMap[c.Name] = c
 | 
						|
	}
 | 
						|
	if !reflect.DeepEqual(expectedMap, gotMap) {
 | 
						|
		// Print ugly but useful diff of expected and received objects for
 | 
						|
		// easier debugging.
 | 
						|
		return fmt.Errorf("Claim check failed [A-expected, B-got result]: %s", diff.ObjectDiff(expectedMap, gotMap))
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// checkEvents compares all expectedEvents with events generated during the test
 | 
						|
// and reports differences.
 | 
						|
func checkEvents(t *testing.T, expectedEvents []string, ctrl *PersistentVolumeController) error {
 | 
						|
	var err error
 | 
						|
 | 
						|
	// Read recorded events
 | 
						|
	fakeRecorder := ctrl.eventRecorder.(*record.FakeRecorder)
 | 
						|
	gotEvents := []string{}
 | 
						|
	finished := false
 | 
						|
	for !finished {
 | 
						|
		select {
 | 
						|
		case event, ok := <-fakeRecorder.Events:
 | 
						|
			if ok {
 | 
						|
				glog.V(5).Infof("event recorder got event %s", event)
 | 
						|
				gotEvents = append(gotEvents, event)
 | 
						|
			} else {
 | 
						|
				glog.V(5).Infof("event recorder finished")
 | 
						|
				finished = true
 | 
						|
			}
 | 
						|
		default:
 | 
						|
			glog.V(5).Infof("event recorder finished")
 | 
						|
			finished = true
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Evaluate the events
 | 
						|
	for i, expected := range expectedEvents {
 | 
						|
		if len(gotEvents) <= i {
 | 
						|
			t.Errorf("Event %q not emitted", expected)
 | 
						|
			err = fmt.Errorf("Events do not match")
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		received := gotEvents[i]
 | 
						|
		if !strings.HasPrefix(received, expected) {
 | 
						|
			t.Errorf("Unexpected event received, expected %q, got %q", expected, received)
 | 
						|
			err = fmt.Errorf("Events do not match")
 | 
						|
		}
 | 
						|
	}
 | 
						|
	for i := len(expectedEvents); i < len(gotEvents); i++ {
 | 
						|
		t.Errorf("Unexpected event received: %q", gotEvents[i])
 | 
						|
		err = fmt.Errorf("Events do not match")
 | 
						|
	}
 | 
						|
	return err
 | 
						|
}
 | 
						|
 | 
						|
// popChange returns one recorded updated object, either *api.PersistentVolume
 | 
						|
// or *api.PersistentVolumeClaim. Returns nil when there are no changes.
 | 
						|
func (r *volumeReactor) popChange() interface{} {
 | 
						|
	r.lock.Lock()
 | 
						|
	defer r.lock.Unlock()
 | 
						|
 | 
						|
	if len(r.changedObjects) == 0 {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	// For debugging purposes, print the queue
 | 
						|
	for _, obj := range r.changedObjects {
 | 
						|
		switch obj.(type) {
 | 
						|
		case *api.PersistentVolume:
 | 
						|
			vol, _ := obj.(*api.PersistentVolume)
 | 
						|
			glog.V(4).Infof("reactor queue: %s", vol.Name)
 | 
						|
		case *api.PersistentVolumeClaim:
 | 
						|
			claim, _ := obj.(*api.PersistentVolumeClaim)
 | 
						|
			glog.V(4).Infof("reactor queue: %s", claim.Name)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Pop the first item from the queue and return it
 | 
						|
	obj := r.changedObjects[0]
 | 
						|
	r.changedObjects = r.changedObjects[1:]
 | 
						|
	return obj
 | 
						|
}
 | 
						|
 | 
						|
// syncAll simulates the controller periodic sync of volumes and claim. It
 | 
						|
// simply adds all these objects to the internal queue of updates. This method
 | 
						|
// should be used when the test manually calls syncClaim/syncVolume. Test that
 | 
						|
// use real controller loop (ctrl.Run()) will get periodic sync automatically.
 | 
						|
func (r *volumeReactor) syncAll() {
 | 
						|
	r.lock.Lock()
 | 
						|
	defer r.lock.Unlock()
 | 
						|
 | 
						|
	for _, c := range r.claims {
 | 
						|
		r.changedObjects = append(r.changedObjects, c)
 | 
						|
	}
 | 
						|
	for _, v := range r.volumes {
 | 
						|
		r.changedObjects = append(r.changedObjects, v)
 | 
						|
	}
 | 
						|
	r.changedSinceLastSync = 0
 | 
						|
}
 | 
						|
 | 
						|
func (r *volumeReactor) getChangeCount() int {
 | 
						|
	r.lock.Lock()
 | 
						|
	defer r.lock.Unlock()
 | 
						|
	return r.changedSinceLastSync
 | 
						|
}
 | 
						|
 | 
						|
func (r *volumeReactor) getOperationCount() int {
 | 
						|
	r.ctrl.runningOperationsMapLock.Lock()
 | 
						|
	defer r.ctrl.runningOperationsMapLock.Unlock()
 | 
						|
	return len(r.ctrl.runningOperations)
 | 
						|
}
 | 
						|
 | 
						|
// waitTest waits until all tests, controllers and other goroutines do their
 | 
						|
// job and no new actions are registered for 10 milliseconds.
 | 
						|
func (r *volumeReactor) waitTest() {
 | 
						|
	// Check every 10ms if the controller does something and stop if it's
 | 
						|
	// idle.
 | 
						|
	oldChanges := -1
 | 
						|
	for {
 | 
						|
		time.Sleep(10 * time.Millisecond)
 | 
						|
		changes := r.getChangeCount()
 | 
						|
		if changes == oldChanges && r.getOperationCount() == 0 {
 | 
						|
			// No changes for last 10ms -> controller must be idle.
 | 
						|
			break
 | 
						|
		}
 | 
						|
		oldChanges = changes
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func newVolumeReactor(client *fake.Clientset, ctrl *PersistentVolumeController, volumeSource, claimSource *framework.FakeControllerSource) *volumeReactor {
 | 
						|
	reactor := &volumeReactor{
 | 
						|
		volumes:      make(map[string]*api.PersistentVolume),
 | 
						|
		claims:       make(map[string]*api.PersistentVolumeClaim),
 | 
						|
		ctrl:         ctrl,
 | 
						|
		volumeSource: volumeSource,
 | 
						|
		claimSource:  claimSource,
 | 
						|
	}
 | 
						|
	client.AddReactor("*", "*", reactor.React)
 | 
						|
	return reactor
 | 
						|
}
 | 
						|
 | 
						|
func newPersistentVolumeController(kubeClient clientset.Interface) *PersistentVolumeController {
 | 
						|
	ctrl := &PersistentVolumeController{
 | 
						|
		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, reclaimPolicy api.PersistentVolumeReclaimPolicy, annotations ...string) *api.PersistentVolume {
 | 
						|
	volume := api.PersistentVolume{
 | 
						|
		ObjectMeta: api.ObjectMeta{
 | 
						|
			Name:            name,
 | 
						|
			ResourceVersion: "1",
 | 
						|
		},
 | 
						|
		Spec: api.PersistentVolumeSpec{
 | 
						|
			Capacity: api.ResourceList{
 | 
						|
				api.ResourceName(api.ResourceStorage): resource.MustParse(capacity),
 | 
						|
			},
 | 
						|
			PersistentVolumeSource: api.PersistentVolumeSource{
 | 
						|
				GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{},
 | 
						|
			},
 | 
						|
			AccessModes:                   []api.PersistentVolumeAccessMode{api.ReadWriteOnce, api.ReadOnlyMany},
 | 
						|
			PersistentVolumeReclaimPolicy: reclaimPolicy,
 | 
						|
		},
 | 
						|
		Status: api.PersistentVolumeStatus{
 | 
						|
			Phase: phase,
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	if boundToClaimName != "" {
 | 
						|
		volume.Spec.ClaimRef = &api.ObjectReference{
 | 
						|
			Kind:       "PersistentVolumeClaim",
 | 
						|
			APIVersion: "v1",
 | 
						|
			UID:        types.UID(boundToClaimUID),
 | 
						|
			Namespace:  testNamespace,
 | 
						|
			Name:       boundToClaimName,
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if len(annotations) > 0 {
 | 
						|
		volume.Annotations = make(map[string]string)
 | 
						|
		for _, a := range annotations {
 | 
						|
			volume.Annotations[a] = "yes"
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return &volume
 | 
						|
}
 | 
						|
 | 
						|
// 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, reclaimPolicy api.PersistentVolumeReclaimPolicy, annotations ...string) []*api.PersistentVolume {
 | 
						|
	return []*api.PersistentVolume{
 | 
						|
		newVolume(name, capacity, boundToClaimUID, boundToClaimName, phase, reclaimPolicy, annotations...),
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// newClaim returns a new claim with given attributes
 | 
						|
func newClaim(name, claimUID, capacity, boundToVolume string, phase api.PersistentVolumeClaimPhase, annotations ...string) *api.PersistentVolumeClaim {
 | 
						|
	claim := api.PersistentVolumeClaim{
 | 
						|
		ObjectMeta: api.ObjectMeta{
 | 
						|
			Name:            name,
 | 
						|
			Namespace:       testNamespace,
 | 
						|
			UID:             types.UID(claimUID),
 | 
						|
			ResourceVersion: "1",
 | 
						|
		},
 | 
						|
		Spec: api.PersistentVolumeClaimSpec{
 | 
						|
			AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce, api.ReadOnlyMany},
 | 
						|
			Resources: api.ResourceRequirements{
 | 
						|
				Requests: api.ResourceList{
 | 
						|
					api.ResourceName(api.ResourceStorage): resource.MustParse(capacity),
 | 
						|
				},
 | 
						|
			},
 | 
						|
			VolumeName: boundToVolume,
 | 
						|
		},
 | 
						|
		Status: api.PersistentVolumeClaimStatus{
 | 
						|
			Phase: phase,
 | 
						|
		},
 | 
						|
	}
 | 
						|
	// Make sure api.GetReference(claim) works
 | 
						|
	claim.ObjectMeta.SelfLink = testapi.Default.SelfLink("pvc", name)
 | 
						|
 | 
						|
	if len(annotations) > 0 {
 | 
						|
		claim.Annotations = make(map[string]string)
 | 
						|
		for _, a := range annotations {
 | 
						|
			claim.Annotations[a] = "yes"
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return &claim
 | 
						|
}
 | 
						|
 | 
						|
// newClaimArray returns array with a single claim that would be returned by
 | 
						|
// newClaim() with the same parameters.
 | 
						|
func newClaimArray(name, claimUID, capacity, boundToVolume string, phase api.PersistentVolumeClaimPhase, annotations ...string) []*api.PersistentVolumeClaim {
 | 
						|
	return []*api.PersistentVolumeClaim{
 | 
						|
		newClaim(name, claimUID, capacity, boundToVolume, phase, annotations...),
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func testSyncClaim(ctrl *PersistentVolumeController, reactor *volumeReactor, test controllerTest) error {
 | 
						|
	return ctrl.syncClaim(test.initialClaims[0])
 | 
						|
}
 | 
						|
 | 
						|
func testSyncClaimError(ctrl *PersistentVolumeController, reactor *volumeReactor, test controllerTest) error {
 | 
						|
	err := ctrl.syncClaim(test.initialClaims[0])
 | 
						|
 | 
						|
	if err != nil {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	return fmt.Errorf("syncClaim succeeded when failure was expected")
 | 
						|
}
 | 
						|
 | 
						|
func testSyncVolume(ctrl *PersistentVolumeController, reactor *volumeReactor, test controllerTest) error {
 | 
						|
	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 {
 | 
						|
		t.Errorf("Test %q: %v", test.name, err)
 | 
						|
 | 
						|
	}
 | 
						|
	if err := reactor.checkVolumes(t, test.expectedVolumes); err != nil {
 | 
						|
		t.Errorf("Test %q: %v", test.name, err)
 | 
						|
	}
 | 
						|
 | 
						|
	if err := checkEvents(t, test.expectedEvents, ctrl); err != nil {
 | 
						|
		t.Errorf("Test %q: %v", test.name, err)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// Test single call to syncClaim and syncVolume methods.
 | 
						|
// For all tests:
 | 
						|
// 1. Fill in the controller with initial data
 | 
						|
// 2. Call the tested function (syncClaim/syncVolume) via
 | 
						|
//    controllerTest.testCall *once*.
 | 
						|
// 3. Compare resulting volumes and claims with expected volumes and claims.
 | 
						|
func runSyncTests(t *testing.T, tests []controllerTest) {
 | 
						|
	for _, test := range tests {
 | 
						|
		glog.V(4).Infof("starting test %q", test.name)
 | 
						|
 | 
						|
		// Initialize the controller
 | 
						|
		client := &fake.Clientset{}
 | 
						|
		ctrl := newPersistentVolumeController(client)
 | 
						|
		reactor := newVolumeReactor(client, ctrl, nil, nil)
 | 
						|
		for _, claim := range test.initialClaims {
 | 
						|
			ctrl.claims.Add(claim)
 | 
						|
			reactor.claims[claim.Name] = claim
 | 
						|
		}
 | 
						|
		for _, volume := range test.initialVolumes {
 | 
						|
			ctrl.volumes.store.Add(volume)
 | 
						|
			reactor.volumes[volume.Name] = volume
 | 
						|
		}
 | 
						|
 | 
						|
		// Run the tested functions
 | 
						|
		err := test.test(ctrl, reactor, test)
 | 
						|
		if err != nil {
 | 
						|
			t.Errorf("Test %q failed: %v", test.name, err)
 | 
						|
		}
 | 
						|
 | 
						|
		// Wait for all goroutines to finish
 | 
						|
		reactor.waitTest()
 | 
						|
 | 
						|
		evaluateTestResults(ctrl, reactor, test, t)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// Test multiple calls to syncClaim/syncVolume and periodic sync of all
 | 
						|
// volume/claims. For all tests, the test follows this pattern:
 | 
						|
// 0. Load the controller with initial data.
 | 
						|
// 1. Call controllerTest.testCall() once as in TestSync()
 | 
						|
// 2. For all volumes/claims changed by previous syncVolume/syncClaim calls,
 | 
						|
//    call appropriate syncVolume/syncClaim (simulating "volume/claim changed"
 | 
						|
//    events). Go to 2. if these calls change anything.
 | 
						|
// 3. When all changes are processed and no new changes were made, call
 | 
						|
//    syncVolume/syncClaim on all volumes/claims (simulating "periodic sync").
 | 
						|
// 4. If some changes were done by step 3., go to 2. (simulation of
 | 
						|
//    "volume/claim updated" events, eventually performing step 3. again)
 | 
						|
// 5. When 3. does not do any changes, finish the tests and compare final set
 | 
						|
//    of volumes/claims with expected claims/volumes and report differences.
 | 
						|
// Some limit of calls in enforced to prevent endless loops.
 | 
						|
func runMultisyncTests(t *testing.T, tests []controllerTest) {
 | 
						|
	for _, test := range tests {
 | 
						|
		glog.V(4).Infof("starting multisync test %q", test.name)
 | 
						|
 | 
						|
		// Initialize the controller
 | 
						|
		client := &fake.Clientset{}
 | 
						|
		ctrl := newPersistentVolumeController(client)
 | 
						|
		reactor := newVolumeReactor(client, ctrl, nil, nil)
 | 
						|
		for _, claim := range test.initialClaims {
 | 
						|
			ctrl.claims.Add(claim)
 | 
						|
			reactor.claims[claim.Name] = claim
 | 
						|
		}
 | 
						|
		for _, volume := range test.initialVolumes {
 | 
						|
			ctrl.volumes.store.Add(volume)
 | 
						|
			reactor.volumes[volume.Name] = volume
 | 
						|
		}
 | 
						|
 | 
						|
		// Run the tested function
 | 
						|
		err := test.test(ctrl, reactor, test)
 | 
						|
		if err != nil {
 | 
						|
			t.Errorf("Test %q failed: %v", test.name, err)
 | 
						|
		}
 | 
						|
 | 
						|
		// Simulate any "changed" events and "periodical sync" until we reach a
 | 
						|
		// stable state.
 | 
						|
		firstSync := true
 | 
						|
		counter := 0
 | 
						|
		for {
 | 
						|
			counter++
 | 
						|
			glog.V(4).Infof("test %q: iteration %d", test.name, counter)
 | 
						|
 | 
						|
			if counter > 100 {
 | 
						|
				t.Errorf("Test %q failed: too many iterations", test.name)
 | 
						|
				break
 | 
						|
			}
 | 
						|
 | 
						|
			// Wait for all goroutines to finish
 | 
						|
			reactor.waitTest()
 | 
						|
 | 
						|
			obj := reactor.popChange()
 | 
						|
			if obj == nil {
 | 
						|
				// Nothing was changed, should we exit?
 | 
						|
				if firstSync || reactor.changedSinceLastSync > 0 {
 | 
						|
					// There were some changes after the last "periodic sync".
 | 
						|
					// Simulate "periodic sync" of everything (until it produces
 | 
						|
					// no changes).
 | 
						|
					firstSync = false
 | 
						|
					glog.V(4).Infof("test %q: simulating periodical sync of all claims and volumes", test.name)
 | 
						|
					reactor.syncAll()
 | 
						|
				} else {
 | 
						|
					// Last sync did not produce any updates, the test reached
 | 
						|
					// stable state -> finish.
 | 
						|
					break
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			// There were some changes, process them
 | 
						|
			switch obj.(type) {
 | 
						|
			case *api.PersistentVolumeClaim:
 | 
						|
				claim := obj.(*api.PersistentVolumeClaim)
 | 
						|
				// Simulate "claim updated" event
 | 
						|
				ctrl.claims.Update(claim)
 | 
						|
				err = ctrl.syncClaim(claim)
 | 
						|
				if err != nil {
 | 
						|
					if err == versionConflictError {
 | 
						|
						// Ignore version errors
 | 
						|
						glog.V(4).Infof("test intentionaly ignores version error.")
 | 
						|
					} else {
 | 
						|
						t.Errorf("Error calling syncClaim: %v", err)
 | 
						|
						// Finish the loop on the first error
 | 
						|
						break
 | 
						|
					}
 | 
						|
				}
 | 
						|
				// Process generated changes
 | 
						|
				continue
 | 
						|
			case *api.PersistentVolume:
 | 
						|
				volume := obj.(*api.PersistentVolume)
 | 
						|
				// Simulate "volume updated" event
 | 
						|
				ctrl.volumes.store.Update(volume)
 | 
						|
				err = ctrl.syncVolume(volume)
 | 
						|
				if err != nil {
 | 
						|
					if err == versionConflictError {
 | 
						|
						// Ignore version errors
 | 
						|
						glog.V(4).Infof("test intentionaly ignores version error.")
 | 
						|
					} else {
 | 
						|
						t.Errorf("Error calling syncVolume: %v", err)
 | 
						|
						// Finish the loop on the first error
 | 
						|
						break
 | 
						|
					}
 | 
						|
				}
 | 
						|
				// Process generated changes
 | 
						|
				continue
 | 
						|
			}
 | 
						|
		}
 | 
						|
		evaluateTestResults(ctrl, reactor, test, t)
 | 
						|
		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
 | 
						|
}
 |