diff --git a/pkg/controller/persistentvolume/persistentvolume_framework_test.go b/pkg/controller/persistentvolume/persistentvolume_framework_test.go index daca2f085c8..8558d89fcc0 100644 --- a/pkg/controller/persistentvolume/persistentvolume_framework_test.go +++ b/pkg/controller/persistentvolume/persistentvolume_framework_test.go @@ -41,7 +41,6 @@ import ( "k8s.io/kubernetes/pkg/conversion" "k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/types" - "k8s.io/kubernetes/pkg/util" "k8s.io/kubernetes/pkg/util/diff" vol "k8s.io/kubernetes/pkg/volume" ) @@ -91,6 +90,7 @@ type controllerTest struct { type testCall func(ctrl *PersistentVolumeController, reactor *volumeReactor, test controllerTest) error const testNamespace = "default" +const mockPluginName = "MockVolumePlugin" var versionConflictError = errors.New("VersionError") var novolumes []*api.PersistentVolume @@ -135,6 +135,26 @@ func (r *volumeReactor) React(action core.Action) (handled bool, ret runtime.Obj glog.V(4).Infof("reactor got operation %q on %q", action.GetVerb(), action.GetResource()) switch { + case action.Matches("create", "persistentvolumes"): + obj := action.(core.UpdateAction).GetObject() + volume := obj.(*api.PersistentVolume) + + // check the volume does not exist + _, found := r.volumes[volume.Name] + if found { + return true, nil, fmt.Errorf("Cannot create volume %s: volume already exists", volume.Name) + } + + // Store the updated object to appropriate places. + if r.volumeSource != nil { + r.volumeSource.Add(volume) + } + r.volumes[volume.Name] = volume + r.changedObjects = append(r.changedObjects, volume) + r.changedSinceLastSync++ + glog.V(4).Infof("created volume %s", volume.Name) + return true, volume, nil + case action.Matches("update", "persistentvolumes"): obj := action.(core.UpdateAction).GetObject() volume := obj.(*api.PersistentVolume) @@ -446,6 +466,13 @@ func addDeletePlugin(ctrl *PersistentVolumeController, expectedDeleteCalls []err 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{ @@ -481,7 +508,11 @@ func newVolume(name, capacity, boundToClaimUID, boundToClaimName string, phase a if len(annotations) > 0 { volume.Annotations = make(map[string]string) for _, a := range annotations { - volume.Annotations[a] = "yes" + if a != annDynamicallyProvisioned { + volume.Annotations[a] = "yes" + } else { + volume.Annotations[a] = mockPluginName + } } } @@ -559,10 +590,11 @@ type operationType string const operationDelete = "Delete" const operationRecycle = "Recycle" +const operationProvision = "Provision" // wrapTestWithControllerConfig returns a testCall that: -// - configures controller with recycler or deleter which will return provided -// errors when a volume is deleted or recycled. +// - configures controller with recycler, deleter or provisioner which will +// return provided errors when a volume is deleted, recycled or provisioned // - calls given testCall func wrapTestWithControllerConfig(operation operationType, expectedOperationCalls []error, toWrap testCall) testCall { expected := expectedOperationCalls @@ -573,6 +605,8 @@ func wrapTestWithControllerConfig(operation operationType, expectedOperationCall addDeletePlugin(ctrl, expected) case operationRecycle: addRecyclePlugin(ctrl, expected) + case operationProvision: + addProvisionPlugin(ctrl, expected) } return toWrap(ctrl, reactor, test) @@ -798,7 +832,7 @@ func (plugin *mockVolumePlugin) Init(host vol.VolumeHost) error { } func (plugin *mockVolumePlugin) Name() string { - return "mockVolumePlugin" + return mockPluginName } func (plugin *mockVolumePlugin) CanSupport(spec *vol.Spec) bool { @@ -834,26 +868,19 @@ func (plugin *mockVolumePlugin) Provision() (*api.PersistentVolume, error) { var pv *api.PersistentVolume err := plugin.provisionCalls[plugin.provisionCallCounter] if err == nil { - // Create a fake PV - fullpath := fmt.Sprintf("/tmp/hostpath_pv/%s", util.NewUUID()) - + // Create a fake PV with known GCE volume (to match expected volume) pv = &api.PersistentVolume{ ObjectMeta: api.ObjectMeta{ Name: plugin.provisionOptions.PVName, - Annotations: map[string]string{ - "kubernetes.io/createdby": "hostpath-dynamic-provisioner", - }, }, Spec: api.PersistentVolumeSpec{ - PersistentVolumeReclaimPolicy: plugin.provisionOptions.PersistentVolumeReclaimPolicy, - AccessModes: plugin.provisionOptions.AccessModes, Capacity: api.ResourceList{ api.ResourceName(api.ResourceStorage): plugin.provisionOptions.Capacity, }, + AccessModes: plugin.provisionOptions.AccessModes, + PersistentVolumeReclaimPolicy: plugin.provisionOptions.PersistentVolumeReclaimPolicy, PersistentVolumeSource: api.PersistentVolumeSource{ - HostPath: &api.HostPathVolumeSource{ - Path: fullpath, - }, + GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{}, }, }, } diff --git a/pkg/controller/persistentvolume/persistentvolume_provision_test.go b/pkg/controller/persistentvolume/persistentvolume_provision_test.go new file mode 100644 index 00000000000..5655c4e6294 --- /dev/null +++ b/pkg/controller/persistentvolume/persistentvolume_provision_test.go @@ -0,0 +1,137 @@ +/* +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" + "testing" + + "k8s.io/kubernetes/pkg/api" +) + +// Test single call to syncVolume, expecting provisioning to happen. +// 1. Fill in the controller with initial data +// 2. Call the syncVolume *once*. +// 3. Compare resulting volumes with expected volumes. +func TestProvisionSync(t *testing.T) { + tests := []controllerTest{ + { + // Provision a volume + "11-1 - successful provision", + novolumes, + newVolumeArray("pv-provisioned-for-uid11-1", "1Gi", "uid11-1", "claim11-1", api.VolumeBound, api.PersistentVolumeReclaimDelete, annBoundByController, annDynamicallyProvisioned), + newClaimArray("claim11-1", "uid11-1", "1Gi", "", api.ClaimPending, annClass), + // Binding will be completed in the next syncClaim + newClaimArray("claim11-1", "uid11-1", "1Gi", "", api.ClaimPending, annClass), + noevents, wrapTestWithControllerConfig(operationProvision, []error{nil}, testSyncClaim), + }, + { + // Provision failure - plugin not found + "11-2 - plugin not found", + novolumes, + novolumes, + newClaimArray("claim11-2", "uid11-2", "1Gi", "", api.ClaimPending, annClass), + newClaimArray("claim11-2", "uid11-2", "1Gi", "", api.ClaimPending, annClass), + []string{"Warning ProvisioningFailed"}, + testSyncClaim, + }, + { + // Provision failure - newProvisioner returns error + "11-3 - newProvisioner failure", + novolumes, + novolumes, + newClaimArray("claim11-3", "uid11-3", "1Gi", "", api.ClaimPending, annClass), + newClaimArray("claim11-3", "uid11-3", "1Gi", "", api.ClaimPending, annClass), + []string{"Warning ProvisioningFailed"}, + wrapTestWithControllerConfig(operationProvision, []error{}, testSyncClaim), + }, + { + // Provision failure - Provision returns error + "11-4 - provision failure", + novolumes, + novolumes, + newClaimArray("claim11-4", "uid11-4", "1Gi", "", api.ClaimPending, annClass), + newClaimArray("claim11-4", "uid11-4", "1Gi", "", api.ClaimPending, annClass), + []string{"Warning ProvisioningFailed"}, + wrapTestWithControllerConfig(operationProvision, []error{errors.New("Moc provisioner error")}, testSyncClaim), + }, + { + // Provision success - there is already a volume available, still + // we provision a new one when requested. + "11-6 - provisioning when there is a volume available", + newVolumeArray("volume11-6", "1Gi", "", "", api.VolumePending, api.PersistentVolumeReclaimRetain), + []*api.PersistentVolume{ + newVolume("volume11-6", "1Gi", "", "", api.VolumePending, api.PersistentVolumeReclaimRetain), + newVolume("pv-provisioned-for-uid11-6", "1Gi", "uid11-6", "claim11-6", api.VolumeBound, api.PersistentVolumeReclaimDelete, annBoundByController, annDynamicallyProvisioned), + }, + newClaimArray("claim11-6", "uid11-6", "1Gi", "", api.ClaimPending, annClass), + // Binding will be completed in the next syncClaim + newClaimArray("claim11-6", "uid11-6", "1Gi", "", api.ClaimPending, annClass), + noevents, + // No provisioning plugin confingure - makes the test fail when + // the controller errorneously tries to provision something + wrapTestWithControllerConfig(operationProvision, []error{nil}, testSyncClaim), + }, + /* { + // Provision success? - claim is bound before provisioner creates + // a volume. + "11-7 - claim is bound before provisioning", + novolumes, + novolumes, + []*api.PersistentVolumeClaim{ + newClaim("claim11-7", "uid11-7", "1Gi", "", api.ClaimPending, annClass), + }, + []*api.PersistentVolumeClaim{ + newClaim("claim11-7", "uid11-7", "1Gi", "volume11-7", api.ClaimBound, annClass, annBindCompleted), + }, + []string{"Warning ProvisioningFailed"}, + getSyncClaimWithOperation(operationProvision, []error{errors.New("Moc provisioner error")}), + }, */ + } + runSyncTests(t, tests) +} + +// Test multiple calls to syncClaim/syncVolume and periodic sync of all +// volume/claims. 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 TestProvisionMultiSync(t *testing.T) { + tests := []controllerTest{ + { + // Provision a volume with binding + "12-1 - successful provision", + novolumes, + newVolumeArray("pv-provisioned-for-uid12-1", "1Gi", "uid12-1", "claim12-1", api.VolumeBound, api.PersistentVolumeReclaimDelete, annBoundByController, annDynamicallyProvisioned), + newClaimArray("claim12-1", "uid12-1", "1Gi", "", api.ClaimPending, annClass), + // Binding will be completed in the next syncClaim + newClaimArray("claim12-1", "uid12-1", "1Gi", "pv-provisioned-for-uid12-1", api.ClaimBound, annClass, annBoundByController, annBindCompleted), + noevents, wrapTestWithControllerConfig(operationProvision, []error{nil}, testSyncClaim), + }, + } + + runMultisyncTests(t, tests) +}