Dynamic provisioning V2 controller, provisioners, docs and tests.

This commit is contained in:
Jan Safranek
2016-08-18 10:36:49 +02:00
parent 214c916045
commit 6e4d95f646
19 changed files with 659 additions and 229 deletions

View File

@@ -33,6 +33,7 @@ import (
"k8s.io/kubernetes/pkg/api/resource"
"k8s.io/kubernetes/pkg/api/testapi"
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/client/cache"
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
@@ -583,7 +584,7 @@ func newVolumeReactor(client *fake.Clientset, ctrl *PersistentVolumeController,
return reactor
}
func newTestController(kubeClient clientset.Interface, volumeSource, claimSource cache.ListerWatcher, enableDynamicProvisioning bool) *PersistentVolumeController {
func newTestController(kubeClient clientset.Interface, volumeSource, claimSource, classSource cache.ListerWatcher, enableDynamicProvisioning bool) *PersistentVolumeController {
if volumeSource == nil {
volumeSource = framework.NewFakePVControllerSource()
}
@@ -593,12 +594,12 @@ func newTestController(kubeClient clientset.Interface, volumeSource, claimSource
ctrl := NewPersistentVolumeController(
kubeClient,
5*time.Second, // sync period
nil, // provisioner
[]vol.VolumePlugin{}, // recyclers
nil, // cloud
"",
volumeSource,
claimSource,
classSource,
record.NewFakeRecorder(1000), // event recorder
enableDynamicProvisioning,
)
@@ -608,27 +609,6 @@ func newTestController(kubeClient clientset.Interface, volumeSource, claimSource
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)
}
func addProvisionPlugin(ctrl *PersistentVolumeController, expectedDeleteCalls []error) {
plugin := &mockVolumePlugin{
provisionCalls: expectedDeleteCalls,
}
ctrl.provisioner = plugin
}
// 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{
@@ -664,10 +644,13 @@ func newVolume(name, capacity, boundToClaimUID, boundToClaimName string, phase a
if len(annotations) > 0 {
volume.Annotations = make(map[string]string)
for _, a := range annotations {
if a != annDynamicallyProvisioned {
volume.Annotations[a] = "yes"
} else {
switch a {
case annDynamicallyProvisioned:
volume.Annotations[a] = mockPluginName
case annClass:
volume.Annotations[a] = "gold"
default:
volume.Annotations[a] = "yes"
}
}
}
@@ -713,6 +696,17 @@ func withMessage(message string, volumes []*api.PersistentVolume) []*api.Persist
return volumes
}
// volumeWithClass saves given class into annClass annotation.
// Meant to be used to compose claims specified inline in a test.
func volumeWithClass(className string, volumes []*api.PersistentVolume) []*api.PersistentVolume {
if volumes[0].Annotations == nil {
volumes[0].Annotations = map[string]string{annClass: className}
} else {
volumes[0].Annotations[annClass] = className
}
return volumes
}
// 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 {
@@ -749,7 +743,12 @@ func newClaim(name, claimUID, capacity, boundToVolume string, phase api.Persiste
if len(annotations) > 0 {
claim.Annotations = make(map[string]string)
for _, a := range annotations {
claim.Annotations[a] = "yes"
switch a {
case annClass:
claim.Annotations[a] = "gold"
default:
claim.Annotations[a] = "yes"
}
}
}
@@ -772,6 +771,17 @@ func newClaimArray(name, claimUID, capacity, boundToVolume string, phase api.Per
}
}
// claimWithClass saves given class into annClass annotation.
// Meant to be used to compose claims specified inline in a test.
func claimWithClass(className string, claims []*api.PersistentVolumeClaim) []*api.PersistentVolumeClaim {
if claims[0].Annotations == nil {
claims[0].Annotations = map[string]string{annClass: className}
} else {
claims[0].Annotations[annClass] = className
}
return claims
}
func testSyncClaim(ctrl *PersistentVolumeController, reactor *volumeReactor, test controllerTest) error {
return ctrl.syncClaim(test.initialClaims[0])
}
@@ -793,29 +803,45 @@ type operationType string
const operationDelete = "Delete"
const operationRecycle = "Recycle"
const operationProvision = "Provision"
// wrapTestWithControllerConfig returns a testCall that:
// - configures controller with recycler, deleter or provisioner which will
// return provided errors when a volume is deleted, recycled or provisioned
// wrapTestWithPluginCalls returns a testCall that:
// - configures controller with a volume plugin that implements recycler,
// deleter and provisioner. The plugin retunrs provided errors when a volume
// is deleted, recycled or provisioned.
// - calls given testCall
func wrapTestWithControllerConfig(operation operationType, expectedOperationCalls []error, toWrap testCall) testCall {
expected := expectedOperationCalls
func wrapTestWithPluginCalls(expectedRecycleCalls, expectedDeleteCalls []error, expectedProvisionCalls []provisionCall, toWrap testCall) testCall {
return func(ctrl *PersistentVolumeController, reactor *volumeReactor, test controllerTest) error {
switch operation {
case operationDelete:
addDeletePlugin(ctrl, expected)
case operationRecycle:
addRecyclePlugin(ctrl, expected)
case operationProvision:
addProvisionPlugin(ctrl, expected)
plugin := &mockVolumePlugin{
recycleCalls: expectedRecycleCalls,
deleteCalls: expectedDeleteCalls,
provisionCalls: expectedProvisionCalls,
}
ctrl.volumePluginMgr.InitPlugins([]vol.VolumePlugin{plugin}, ctrl)
return toWrap(ctrl, reactor, test)
}
}
// wrapTestWithReclaimCalls 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 wrapTestWithReclaimCalls(operation operationType, expectedOperationCalls []error, toWrap testCall) testCall {
if operation == operationDelete {
return wrapTestWithPluginCalls(nil, expectedOperationCalls, nil, toWrap)
} else {
return wrapTestWithPluginCalls(expectedOperationCalls, nil, nil, toWrap)
}
}
// wrapTestWithProvisionCalls returns a testCall that:
// - configures controller with a provisioner which will return provided errors
// when a claim is provisioned
// - calls given testCall
func wrapTestWithProvisionCalls(expectedProvisionCalls []provisionCall, toWrap testCall) testCall {
return wrapTestWithPluginCalls(nil, nil, expectedProvisionCalls, toWrap)
}
// 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
@@ -873,13 +899,13 @@ func evaluateTestResults(ctrl *PersistentVolumeController, reactor *volumeReacto
// 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) {
func runSyncTests(t *testing.T, tests []controllerTest, storageClasses []*extensions.StorageClass) {
for _, test := range tests {
glog.V(4).Infof("starting test %q", test.name)
// Initialize the controller
client := &fake.Clientset{}
ctrl := newTestController(client, nil, nil, true)
ctrl := newTestController(client, nil, nil, nil, true)
reactor := newVolumeReactor(client, ctrl, nil, nil, test.errors)
for _, claim := range test.initialClaims {
ctrl.claims.Add(claim)
@@ -890,6 +916,15 @@ func runSyncTests(t *testing.T, tests []controllerTest) {
reactor.volumes[volume.Name] = volume
}
// Convert classes to []interface{} and forcefully inject them into
// controller.
storageClassPtrs := make([]interface{}, len(storageClasses))
for i, s := range storageClasses {
storageClassPtrs[i] = s
}
// 1 is the resource version
ctrl.classes.Replace(storageClassPtrs, "1")
// Run the tested functions
err := test.test(ctrl, reactor, test)
if err != nil {
@@ -920,13 +955,22 @@ func runSyncTests(t *testing.T, tests []controllerTest) {
// 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) {
func runMultisyncTests(t *testing.T, tests []controllerTest, storageClasses []*extensions.StorageClass, defaultStorageClass string) {
for _, test := range tests {
glog.V(4).Infof("starting multisync test %q", test.name)
// Initialize the controller
client := &fake.Clientset{}
ctrl := newTestController(client, nil, nil, true)
ctrl := newTestController(client, nil, nil, nil, true)
// Convert classes to []interface{} and forcefully inject them into
// controller.
storageClassPtrs := make([]interface{}, len(storageClasses))
for i, s := range storageClasses {
storageClassPtrs[i] = s
}
ctrl.classes.Replace(storageClassPtrs, "1")
reactor := newVolumeReactor(client, ctrl, nil, nil, test.errors)
for _, claim := range test.initialClaims {
ctrl.claims.Add(claim)
@@ -1022,7 +1066,7 @@ func runMultisyncTests(t *testing.T, tests []controllerTest) {
// Dummy volume plugin for provisioning, deletion and recycling. It contains
// lists of expected return values to simulate errors.
type mockVolumePlugin struct {
provisionCalls []error
provisionCalls []provisionCall
provisionCallCounter int
deleteCalls []error
deleteCallCounter int
@@ -1031,6 +1075,11 @@ type mockVolumePlugin struct {
provisionOptions vol.VolumeOptions
}
type provisionCall struct {
expectedParameters map[string]string
ret error
}
var _ vol.VolumePlugin = &mockVolumePlugin{}
var _ vol.RecyclableVolumePlugin = &mockVolumePlugin{}
var _ vol.DeletableVolumePlugin = &mockVolumePlugin{}
@@ -1087,8 +1136,12 @@ func (plugin *mockVolumePlugin) Provision() (*api.PersistentVolume, error) {
}
var pv *api.PersistentVolume
err := plugin.provisionCalls[plugin.provisionCallCounter]
if err == nil {
call := plugin.provisionCalls[plugin.provisionCallCounter]
if !reflect.DeepEqual(call.expectedParameters, plugin.provisionOptions.Parameters) {
glog.Errorf("invalid provisioner call, expected options: %+v, got: %+v", call.expectedParameters, plugin.provisionOptions.Parameters)
return nil, fmt.Errorf("Mock plugin error: invalid provisioner call")
}
if call.ret == nil {
// Create a fake PV with known GCE volume (to match expected volume)
pv = &api.PersistentVolume{
ObjectMeta: api.ObjectMeta{
@@ -1108,8 +1161,8 @@ func (plugin *mockVolumePlugin) Provision() (*api.PersistentVolume, error) {
}
plugin.provisionCallCounter++
glog.V(4).Infof("mock plugin Provision call nr. %d, returning %v: %v", plugin.provisionCallCounter, pv, err)
return pv, err
glog.V(4).Infof("mock plugin Provision call nr. %d, returning %v: %v", plugin.provisionCallCounter, pv, call.ret)
return pv, call.ret
}
// Deleter interfaces