func TestRecyclingRetry(t *testing.T) { // Test that recycler controller retries to recycle a volume several times, which succeeds eventually pv := preparePV() mockClient := &mockBinderClient{ volume: pv, } plugMgr := volume.VolumePluginMgr{} // Use a fake NewRecycler function plugMgr.InitPlugins(host_path.ProbeRecyclableVolumePlugins(newFailingMockRecycler, volume.VolumeConfig{}), volume.NewFakeVolumeHost("/tmp/fake", nil, nil)) // Reset a global call counter failedCallCount = 0 recycler := &PersistentVolumeRecycler{ kubeClient: fake.NewSimpleClientset(), client: mockClient, pluginMgr: plugMgr, syncPeriod: mySyncPeriod, maximumRetry: myMaximumRetry, releasedVolumes: make(map[string]releasedVolumeStatus), } // All but the last attempt will fail testRecycleFailures(t, recycler, mockClient, pv, myMaximumRetry-1) // The last attempt should succeed err := recycler.reclaimVolume(pv) if err != nil { t.Errorf("Last step: Recycler failed: %v", err) } if mockClient.volume.Status.Phase != api.VolumePending { t.Errorf("Last step: The volume should be Pending, but is %s instead", mockClient.volume.Status.Phase) } // Check the cache, it should not have any entry status, found := recycler.releasedVolumes[pv.Name] if found { t.Errorf("Last step: Expected PV to be removed from cache, got %v", status) } }
func TestBindingWithExamples(t *testing.T) { api.ForTesting_ReferencesAllowBlankSelfLinks = true o := testclient.NewObjects(api.Scheme, api.Scheme) if err := testclient.AddObjectsFromPath("../../../docs/user-guide/persistent-volumes/claims/claim-01.yaml", o, api.Scheme); err != nil { t.Fatal(err) } if err := testclient.AddObjectsFromPath("../../../docs/user-guide/persistent-volumes/volumes/local-01.yaml", o, api.Scheme); err != nil { t.Fatal(err) } client := &testclient.Fake{ReactFn: testclient.ObjectReaction(o, api.RESTMapper)} pv, err := client.PersistentVolumes().Get("any") pv.Spec.PersistentVolumeReclaimPolicy = api.PersistentVolumeReclaimRecycle if err != nil { t.Errorf("Unexpected error getting PV from client: %v", err) } claim, error := client.PersistentVolumeClaims("ns").Get("any") if error != nil { t.Errorf("Unexpected error getting PVC from client: %v", err) } volumeIndex := NewPersistentVolumeOrderedIndex() mockClient := &mockBinderClient{ volume: pv, claim: claim, } plugMgr := volume.VolumePluginMgr{} plugMgr.InitPlugins(host_path.ProbeRecyclableVolumePlugins(newMockRecycler), volume.NewFakeVolumeHost("/tmp/fake", nil, nil)) recycler := &PersistentVolumeRecycler{ kubeClient: client, client: mockClient, pluginMgr: plugMgr, } // adds the volume to the index, making the volume available syncVolume(volumeIndex, mockClient, pv) if pv.Status.Phase != api.VolumeAvailable { t.Errorf("Expected phase %s but got %s", api.VolumeBound, pv.Status.Phase) } // an initial sync for a claim will bind it to an unbound volume, triggers state change syncClaim(volumeIndex, mockClient, claim) // state change causes another syncClaim to update statuses syncClaim(volumeIndex, mockClient, claim) // claim updated volume's status, causing an update and syncVolume call syncVolume(volumeIndex, mockClient, pv) if pv.Spec.ClaimRef == nil { t.Errorf("Expected ClaimRef but got nil for pv.Status.ClaimRef: %+v\n", pv) } if pv.Status.Phase != api.VolumeBound { t.Errorf("Expected phase %s but got %s", api.VolumeBound, pv.Status.Phase) } if claim.Status.Phase != api.ClaimBound { t.Errorf("Expected phase %s but got %s", api.ClaimBound, claim.Status.Phase) } if len(claim.Status.AccessModes) != len(pv.Spec.AccessModes) { t.Errorf("Expected phase %s but got %s", api.ClaimBound, claim.Status.Phase) } if claim.Status.AccessModes[0] != pv.Spec.AccessModes[0] { t.Errorf("Expected access mode %s but got %s", claim.Status.AccessModes[0], pv.Spec.AccessModes[0]) } // pretend the user deleted their claim mockClient.claim = nil syncVolume(volumeIndex, mockClient, pv) if pv.Status.Phase != api.VolumeReleased { t.Errorf("Expected phase %s but got %s", api.VolumeReleased, pv.Status.Phase) } if pv.Spec.ClaimRef == nil { t.Errorf("Expected non-nil ClaimRef: %+v", pv.Spec) } mockClient.volume = pv // released volumes with a PersistentVolumeReclaimPolicy (recycle/delete) can have further processing err = recycler.reclaimVolume(pv) if err != nil { t.Errorf("Unexpected error reclaiming volume: %+v", err) } if pv.Status.Phase != api.VolumePending { t.Errorf("Expected phase %s but got %s", api.VolumePending, pv.Status.Phase) } // after the recycling changes the phase to Pending, the binder picks up again // to remove any vestiges of binding and make the volume Available again syncVolume(volumeIndex, mockClient, pv) if pv.Status.Phase != api.VolumeAvailable { t.Errorf("Expected phase %s but got %s", api.VolumeAvailable, pv.Status.Phase) } if pv.Spec.ClaimRef != nil { t.Errorf("Expected nil ClaimRef: %+v", pv.Spec) } }
func TestClaimRace(t *testing.T) { c1 := &api.PersistentVolumeClaim{ ObjectMeta: api.ObjectMeta{ Name: "c1", }, Spec: api.PersistentVolumeClaimSpec{ AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce}, Resources: api.ResourceRequirements{ Requests: api.ResourceList{ api.ResourceName(api.ResourceStorage): resource.MustParse("3Gi"), }, }, }, Status: api.PersistentVolumeClaimStatus{ Phase: api.ClaimPending, }, } c1.ObjectMeta.SelfLink = testapi.Default.SelfLink("pvc", "") c2 := &api.PersistentVolumeClaim{ ObjectMeta: api.ObjectMeta{ Name: "c2", }, Spec: api.PersistentVolumeClaimSpec{ AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce}, Resources: api.ResourceRequirements{ Requests: api.ResourceList{ api.ResourceName(api.ResourceStorage): resource.MustParse("3Gi"), }, }, }, Status: api.PersistentVolumeClaimStatus{ Phase: api.ClaimPending, }, } c2.ObjectMeta.SelfLink = testapi.Default.SelfLink("pvc", "") v := &api.PersistentVolume{ ObjectMeta: api.ObjectMeta{ Name: "foo", }, Spec: api.PersistentVolumeSpec{ AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce}, Capacity: api.ResourceList{ api.ResourceName(api.ResourceStorage): resource.MustParse("10Gi"), }, PersistentVolumeSource: api.PersistentVolumeSource{ HostPath: &api.HostPathVolumeSource{ Path: "/tmp/data01", }, }, }, Status: api.PersistentVolumeStatus{ Phase: api.VolumePending, }, } volumeIndex := NewPersistentVolumeOrderedIndex() mockClient := &mockBinderClient{} plugMgr := volume.VolumePluginMgr{} plugMgr.InitPlugins(host_path.ProbeRecyclableVolumePlugins(newMockRecycler, volume.VolumeConfig{}), volume.NewFakeVolumeHost("/tmp/fake", nil, nil)) // adds the volume to the index, making the volume available syncVolume(volumeIndex, mockClient, v) if mockClient.volume.Status.Phase != api.VolumeAvailable { t.Errorf("Expected phase %s but got %s", api.VolumeAvailable, mockClient.volume.Status.Phase) } if _, exists, _ := volumeIndex.Get(v); !exists { t.Errorf("Expected to find volume in index but it did not exist") } // an initial sync for a claim matches the volume err := syncClaim(volumeIndex, mockClient, c1) if err != nil { t.Errorf("Unexpected error: %v", err) } if c1.Status.Phase != api.ClaimBound { t.Errorf("Expected phase %s but got %s", api.ClaimBound, c1.Status.Phase) } // before the volume gets updated w/ claimRef, a 2nd claim can attempt to bind and find the same volume err = syncClaim(volumeIndex, mockClient, c2) if err != nil { t.Errorf("unexpected error for unmatched claim: %v", err) } if c2.Status.Phase != api.ClaimPending { t.Errorf("Expected phase %s but got %s", api.ClaimPending, c2.Status.Phase) } }
func TestBindingWithExamples(t *testing.T) { o := testclient.NewObjects(api.Scheme, api.Scheme) if err := testclient.AddObjectsFromPath("../../../docs/user-guide/persistent-volumes/claims/claim-01.yaml", o, api.Scheme); err != nil { t.Fatal(err) } if err := testclient.AddObjectsFromPath("../../../docs/user-guide/persistent-volumes/volumes/local-01.yaml", o, api.Scheme); err != nil { t.Fatal(err) } client := &testclient.Fake{} client.AddReactor("*", "*", testclient.ObjectReaction(o, api.RESTMapper)) pv, err := client.PersistentVolumes().Get("any") pv.Spec.PersistentVolumeReclaimPolicy = api.PersistentVolumeReclaimRecycle if err != nil { t.Errorf("Unexpected error getting PV from client: %v", err) } pv.ObjectMeta.SelfLink = testapi.Default.SelfLink("pv", "") // the default value of the PV is Pending. if processed at least once, its status in etcd is Available. // There was a bug where only Pending volumes were being indexed and made ready for claims. // Test that !Pending gets correctly added pv.Status.Phase = api.VolumeAvailable claim, error := client.PersistentVolumeClaims("ns").Get("any") if error != nil { t.Errorf("Unexpected error getting PVC from client: %v", err) } claim.ObjectMeta.SelfLink = testapi.Default.SelfLink("pvc", "") volumeIndex := NewPersistentVolumeOrderedIndex() mockClient := &mockBinderClient{ volume: pv, claim: claim, } plugMgr := volume.VolumePluginMgr{} plugMgr.InitPlugins(host_path.ProbeRecyclableVolumePlugins(newMockRecycler, volume.VolumeConfig{}), volume.NewFakeVolumeHost("/tmp/fake", nil, nil)) recycler := &PersistentVolumeRecycler{ kubeClient: client, client: mockClient, pluginMgr: plugMgr, } // adds the volume to the index, making the volume available syncVolume(volumeIndex, mockClient, pv) if mockClient.volume.Status.Phase != api.VolumeAvailable { t.Errorf("Expected phase %s but got %s", api.VolumeAvailable, mockClient.volume.Status.Phase) } // an initial sync for a claim will bind it to an unbound volume syncClaim(volumeIndex, mockClient, claim) // bind expected on pv.Spec but status update hasn't happened yet if mockClient.volume.Spec.ClaimRef == nil { t.Errorf("Expected ClaimRef but got nil for pv.Status.ClaimRef\n") } if mockClient.volume.Status.Phase != api.VolumeAvailable { t.Errorf("Expected phase %s but got %s", api.VolumeAvailable, mockClient.volume.Status.Phase) } if mockClient.claim.Spec.VolumeName != pv.Name { t.Errorf("Expected claim.Spec.VolumeName %s but got %s", mockClient.claim.Spec.VolumeName, pv.Name) } if mockClient.claim.Status.Phase != api.ClaimBound { t.Errorf("Expected phase %s but got %s", api.ClaimBound, claim.Status.Phase) } // state changes in pvc triggers sync that sets pv attributes to pvc.Status syncClaim(volumeIndex, mockClient, claim) if len(mockClient.claim.Status.AccessModes) == 0 { t.Errorf("Expected %d access modes but got 0", len(pv.Spec.AccessModes)) } // persisting the bind to pv.Spec.ClaimRef triggers a sync syncVolume(volumeIndex, mockClient, mockClient.volume) if mockClient.volume.Status.Phase != api.VolumeBound { t.Errorf("Expected phase %s but got %s", api.VolumeBound, mockClient.volume.Status.Phase) } // pretend the user deleted their claim. periodic resync picks it up. mockClient.claim = nil syncVolume(volumeIndex, mockClient, mockClient.volume) if mockClient.volume.Status.Phase != api.VolumeReleased { t.Errorf("Expected phase %s but got %s", api.VolumeReleased, mockClient.volume.Status.Phase) } // released volumes with a PersistentVolumeReclaimPolicy (recycle/delete) can have further processing err = recycler.reclaimVolume(mockClient.volume) if err != nil { t.Errorf("Unexpected error reclaiming volume: %+v", err) } if mockClient.volume.Status.Phase != api.VolumePending { t.Errorf("Expected phase %s but got %s", api.VolumePending, mockClient.volume.Status.Phase) } // after the recycling changes the phase to Pending, the binder picks up again // to remove any vestiges of binding and make the volume Available again syncVolume(volumeIndex, mockClient, mockClient.volume) if mockClient.volume.Status.Phase != api.VolumeAvailable { t.Errorf("Expected phase %s but got %s", api.VolumeAvailable, mockClient.volume.Status.Phase) } if mockClient.volume.Spec.ClaimRef != nil { t.Errorf("Expected nil ClaimRef: %+v", mockClient.volume.Spec.ClaimRef) } }
func TestClaimSyncAfterVolumeProvisioning(t *testing.T) { tmpDir, err := utiltesting.MkTmpdir("claimbinder-test") if err != nil { t.Fatalf("error creating temp dir: %v", err) } defer os.RemoveAll(tmpDir) // Tests that binder.syncVolume will also syncClaim if the PV has completed // provisioning but the claim is still Pending. We want to advance to Bound // without having to wait until the binder's next sync period. claim := &api.PersistentVolumeClaim{ ObjectMeta: api.ObjectMeta{ Name: "foo", Namespace: "bar", Annotations: map[string]string{ qosProvisioningKey: "foo", }, }, Spec: api.PersistentVolumeClaimSpec{ AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce}, Resources: api.ResourceRequirements{ Requests: api.ResourceList{ api.ResourceName(api.ResourceStorage): resource.MustParse("3Gi"), }, }, }, Status: api.PersistentVolumeClaimStatus{ Phase: api.ClaimPending, }, } claim.ObjectMeta.SelfLink = testapi.Default.SelfLink("pvc", "") claimRef, _ := api.GetReference(claim) pv := &api.PersistentVolume{ ObjectMeta: api.ObjectMeta{ Name: "foo", Annotations: map[string]string{ pvProvisioningRequiredAnnotationKey: pvProvisioningCompletedAnnotationValue, }, }, Spec: api.PersistentVolumeSpec{ AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce}, Capacity: api.ResourceList{ api.ResourceName(api.ResourceStorage): resource.MustParse("10Gi"), }, PersistentVolumeSource: api.PersistentVolumeSource{ HostPath: &api.HostPathVolumeSource{ Path: fmt.Sprintf("%s/data01", tmpDir), }, }, ClaimRef: claimRef, }, Status: api.PersistentVolumeStatus{ Phase: api.VolumePending, }, } volumeIndex := NewPersistentVolumeOrderedIndex() mockClient := &mockBinderClient{ claim: claim, volume: pv, } plugMgr := volume.VolumePluginMgr{} plugMgr.InitPlugins(host_path.ProbeRecyclableVolumePlugins(newMockRecycler, volume.VolumeConfig{}), volumetest.NewFakeVolumeHost(tmpDir, nil, nil)) // adds the volume to the index, making the volume available. // pv also completed provisioning, so syncClaim should cause claim's phase to advance to Bound syncVolume(volumeIndex, mockClient, pv) if mockClient.volume.Status.Phase != api.VolumeAvailable { t.Errorf("Expected phase %s but got %s", api.VolumeAvailable, mockClient.volume.Status.Phase) } if mockClient.claim.Status.Phase != api.ClaimBound { t.Errorf("Expected phase %s but got %s", api.ClaimBound, claim.Status.Phase) } }
func TestNewClaimWithSameNameAsOldClaim(t *testing.T) { c1 := &api.PersistentVolumeClaim{ ObjectMeta: api.ObjectMeta{ Name: "c1", Namespace: "foo", UID: "12345", }, Spec: api.PersistentVolumeClaimSpec{ AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce}, Resources: api.ResourceRequirements{ Requests: api.ResourceList{ api.ResourceName(api.ResourceStorage): resource.MustParse("3Gi"), }, }, }, Status: api.PersistentVolumeClaimStatus{ Phase: api.ClaimBound, }, } c1.ObjectMeta.SelfLink = testapi.Default.SelfLink("pvc", "") v := &api.PersistentVolume{ ObjectMeta: api.ObjectMeta{ Name: "foo", }, Spec: api.PersistentVolumeSpec{ ClaimRef: &api.ObjectReference{ Name: c1.Name, Namespace: c1.Namespace, UID: "45678", }, AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce}, Capacity: api.ResourceList{ api.ResourceName(api.ResourceStorage): resource.MustParse("10Gi"), }, PersistentVolumeSource: api.PersistentVolumeSource{ HostPath: &api.HostPathVolumeSource{ Path: "/tmp/data01", }, }, }, Status: api.PersistentVolumeStatus{ Phase: api.VolumeBound, }, } volumeIndex := NewPersistentVolumeOrderedIndex() mockClient := &mockBinderClient{ claim: c1, volume: v, } plugMgr := volume.VolumePluginMgr{} plugMgr.InitPlugins(host_path.ProbeRecyclableVolumePlugins(newMockRecycler, volume.VolumeConfig{}), volumetest.NewFakeVolumeHost("/tmp/fake", nil, nil)) syncVolume(volumeIndex, mockClient, v) if mockClient.volume.Status.Phase != api.VolumeReleased { t.Errorf("Expected phase %s but got %s", api.VolumeReleased, mockClient.volume.Status.Phase) } }
func TestRecycledPersistentVolumeUID(t *testing.T) { tmpDir, err := utiltesting.MkTmpdir("claimbinder-test") if err != nil { t.Fatalf("error creating temp dir: %v", err) } defer os.RemoveAll(tmpDir) codec := api.Codecs.UniversalDecoder() o := core.NewObjects(api.Scheme, codec) if err := core.AddObjectsFromPath("../../../docs/user-guide/persistent-volumes/claims/claim-01.yaml", o, codec); err != nil { t.Fatal(err) } if err := core.AddObjectsFromPath("../../../docs/user-guide/persistent-volumes/volumes/local-01.yaml", o, codec); err != nil { t.Fatal(err) } clientset := &fake.Clientset{} clientset.AddReactor("*", "*", core.ObjectReaction(o, registered.RESTMapper())) pv, err := clientset.Core().PersistentVolumes().Get("any") if err != nil { t.Errorf("Unexpected error getting PV from client: %v", err) } pv.Spec.PersistentVolumeReclaimPolicy = api.PersistentVolumeReclaimRecycle if err != nil { t.Errorf("Unexpected error getting PV from client: %v", err) } pv.ObjectMeta.SelfLink = testapi.Default.SelfLink("pv", "") // the default value of the PV is Pending. if processed at least once, its status in etcd is Available. // There was a bug where only Pending volumes were being indexed and made ready for claims. // Test that !Pending gets correctly added pv.Status.Phase = api.VolumeAvailable claim, error := clientset.Core().PersistentVolumeClaims("ns").Get("any") if error != nil { t.Errorf("Unexpected error getting PVC from client: %v", err) } claim.ObjectMeta.SelfLink = testapi.Default.SelfLink("pvc", "") claim.ObjectMeta.UID = types.UID("uid1") volumeIndex := NewPersistentVolumeOrderedIndex() mockClient := &mockBinderClient{ volume: pv, claim: claim, } plugMgr := volume.VolumePluginMgr{} plugMgr.InitPlugins(host_path.ProbeRecyclableVolumePlugins(newMockRecycler, volume.VolumeConfig{}), volumetest.NewFakeVolumeHost(tmpDir, nil, nil)) recycler := &PersistentVolumeRecycler{ kubeClient: clientset, client: mockClient, pluginMgr: plugMgr, } // adds the volume to the index, making the volume available syncVolume(volumeIndex, mockClient, pv) if mockClient.volume.Status.Phase != api.VolumeAvailable { t.Errorf("Expected phase %s but got %s", api.VolumeAvailable, mockClient.volume.Status.Phase) } // add the claim to fake API server mockClient.UpdatePersistentVolumeClaim(claim) // an initial sync for a claim will bind it to an unbound volume syncClaim(volumeIndex, mockClient, claim) // pretend the user deleted their claim. periodic resync picks it up. mockClient.claim = nil syncVolume(volumeIndex, mockClient, mockClient.volume) if mockClient.volume.Status.Phase != api.VolumeReleased { t.Errorf("Expected phase %s but got %s", api.VolumeReleased, mockClient.volume.Status.Phase) } // released volumes with a PersistentVolumeReclaimPolicy (recycle/delete) can have further processing err = recycler.reclaimVolume(mockClient.volume) if err != nil { t.Errorf("Unexpected error reclaiming volume: %+v", err) } if mockClient.volume.Status.Phase != api.VolumePending { t.Errorf("Expected phase %s but got %s", api.VolumePending, mockClient.volume.Status.Phase) } // after the recycling changes the phase to Pending, the binder picks up again // to remove any vestiges of binding and make the volume Available again // // explicitly set the claim's UID to a different value to ensure that a new claim with the same // name as what the PV was previously bound still yields an available volume claim.ObjectMeta.UID = types.UID("uid2") mockClient.claim = claim syncVolume(volumeIndex, mockClient, mockClient.volume) if mockClient.volume.Status.Phase != api.VolumeAvailable { t.Errorf("Expected phase %s but got %s", api.VolumeAvailable, mockClient.volume.Status.Phase) } if mockClient.volume.Spec.ClaimRef != nil { t.Errorf("Expected nil ClaimRef: %+v", mockClient.volume.Spec.ClaimRef) } }