mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 19:56:01 +00:00
Remove all three PersistentVolume controllers.
We will add new ones gradually in smaller chunks.
This commit is contained in:
parent
dee24333ff
commit
6fa527a460
@ -1,530 +0,0 @@
|
||||
/*
|
||||
Copyright 2014 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 (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/errors"
|
||||
"k8s.io/kubernetes/pkg/client/cache"
|
||||
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
|
||||
"k8s.io/kubernetes/pkg/controller/framework"
|
||||
"k8s.io/kubernetes/pkg/conversion"
|
||||
"k8s.io/kubernetes/pkg/runtime"
|
||||
"k8s.io/kubernetes/pkg/util/metrics"
|
||||
"k8s.io/kubernetes/pkg/watch"
|
||||
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// PersistentVolumeClaimBinder is a controller that synchronizes PersistentVolumeClaims.
|
||||
type PersistentVolumeClaimBinder struct {
|
||||
volumeIndex *persistentVolumeOrderedIndex
|
||||
volumeController *framework.Controller
|
||||
claimController *framework.Controller
|
||||
client binderClient
|
||||
stopChannels map[string]chan struct{}
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// NewPersistentVolumeClaimBinder creates a new PersistentVolumeClaimBinder
|
||||
func NewPersistentVolumeClaimBinder(kubeClient clientset.Interface, syncPeriod time.Duration) *PersistentVolumeClaimBinder {
|
||||
if kubeClient != nil && kubeClient.Core().GetRESTClient().GetRateLimiter() != nil {
|
||||
metrics.RegisterMetricAndTrackRateLimiterUsage("pv_claim_binder_controller", kubeClient.Core().GetRESTClient().GetRateLimiter())
|
||||
}
|
||||
volumeIndex := NewPersistentVolumeOrderedIndex()
|
||||
binderClient := NewBinderClient(kubeClient)
|
||||
binder := &PersistentVolumeClaimBinder{
|
||||
volumeIndex: volumeIndex,
|
||||
client: binderClient,
|
||||
}
|
||||
|
||||
_, volumeController := framework.NewInformer(
|
||||
&cache.ListWatch{
|
||||
ListFunc: func(options api.ListOptions) (runtime.Object, error) {
|
||||
return kubeClient.Core().PersistentVolumes().List(options)
|
||||
},
|
||||
WatchFunc: func(options api.ListOptions) (watch.Interface, error) {
|
||||
return kubeClient.Core().PersistentVolumes().Watch(options)
|
||||
},
|
||||
},
|
||||
&api.PersistentVolume{},
|
||||
// TODO: Can we have much longer period here?
|
||||
syncPeriod,
|
||||
framework.ResourceEventHandlerFuncs{
|
||||
AddFunc: binder.addVolume,
|
||||
UpdateFunc: binder.updateVolume,
|
||||
DeleteFunc: binder.deleteVolume,
|
||||
},
|
||||
)
|
||||
_, claimController := framework.NewInformer(
|
||||
&cache.ListWatch{
|
||||
ListFunc: func(options api.ListOptions) (runtime.Object, error) {
|
||||
return kubeClient.Core().PersistentVolumeClaims(api.NamespaceAll).List(options)
|
||||
},
|
||||
WatchFunc: func(options api.ListOptions) (watch.Interface, error) {
|
||||
return kubeClient.Core().PersistentVolumeClaims(api.NamespaceAll).Watch(options)
|
||||
},
|
||||
},
|
||||
&api.PersistentVolumeClaim{},
|
||||
// TODO: Can we have much longer period here?
|
||||
syncPeriod,
|
||||
framework.ResourceEventHandlerFuncs{
|
||||
AddFunc: binder.addClaim,
|
||||
UpdateFunc: binder.updateClaim,
|
||||
DeleteFunc: binder.deleteClaim,
|
||||
},
|
||||
)
|
||||
|
||||
binder.claimController = claimController
|
||||
binder.volumeController = volumeController
|
||||
|
||||
return binder
|
||||
}
|
||||
func (binder *PersistentVolumeClaimBinder) addVolume(obj interface{}) {
|
||||
binder.lock.Lock()
|
||||
defer binder.lock.Unlock()
|
||||
pv, ok := obj.(*api.PersistentVolume)
|
||||
if !ok {
|
||||
glog.Errorf("Expected PersistentVolume but handler received %+v", obj)
|
||||
return
|
||||
}
|
||||
if err := syncVolume(binder.volumeIndex, binder.client, pv); err != nil {
|
||||
glog.Errorf("PVClaimBinder could not add volume %s: %+v", pv.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (binder *PersistentVolumeClaimBinder) updateVolume(oldObj, newObj interface{}) {
|
||||
binder.lock.Lock()
|
||||
defer binder.lock.Unlock()
|
||||
newVolume, ok := newObj.(*api.PersistentVolume)
|
||||
if !ok {
|
||||
glog.Errorf("Expected PersistentVolume but handler received %+v", newObj)
|
||||
return
|
||||
}
|
||||
if err := binder.volumeIndex.Update(newVolume); err != nil {
|
||||
glog.Errorf("Error updating volume %s in index: %v", newVolume.Name, err)
|
||||
return
|
||||
}
|
||||
if err := syncVolume(binder.volumeIndex, binder.client, newVolume); err != nil {
|
||||
glog.Errorf("PVClaimBinder could not update volume %s: %+v", newVolume.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (binder *PersistentVolumeClaimBinder) deleteVolume(obj interface{}) {
|
||||
binder.lock.Lock()
|
||||
defer binder.lock.Unlock()
|
||||
volume, ok := obj.(*api.PersistentVolume)
|
||||
if !ok {
|
||||
glog.Errorf("Expected PersistentVolume but handler received %+v", obj)
|
||||
return
|
||||
}
|
||||
if err := binder.volumeIndex.Delete(volume); err != nil {
|
||||
glog.Errorf("Error deleting volume %s from index: %v", volume.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (binder *PersistentVolumeClaimBinder) addClaim(obj interface{}) {
|
||||
binder.lock.Lock()
|
||||
defer binder.lock.Unlock()
|
||||
claim, ok := obj.(*api.PersistentVolumeClaim)
|
||||
if !ok {
|
||||
glog.Errorf("Expected PersistentVolumeClaim but handler received %+v", obj)
|
||||
return
|
||||
}
|
||||
if err := syncClaim(binder.volumeIndex, binder.client, claim); err != nil {
|
||||
glog.Errorf("PVClaimBinder could not add claim %s: %+v", claim.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (binder *PersistentVolumeClaimBinder) updateClaim(oldObj, newObj interface{}) {
|
||||
binder.lock.Lock()
|
||||
defer binder.lock.Unlock()
|
||||
newClaim, ok := newObj.(*api.PersistentVolumeClaim)
|
||||
if !ok {
|
||||
glog.Errorf("Expected PersistentVolumeClaim but handler received %+v", newObj)
|
||||
return
|
||||
}
|
||||
if err := syncClaim(binder.volumeIndex, binder.client, newClaim); err != nil {
|
||||
glog.Errorf("PVClaimBinder could not update claim %s: %+v", newClaim.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (binder *PersistentVolumeClaimBinder) deleteClaim(obj interface{}) {
|
||||
binder.lock.Lock()
|
||||
defer binder.lock.Unlock()
|
||||
var volume *api.PersistentVolume
|
||||
if pvc, ok := obj.(*api.PersistentVolumeClaim); ok {
|
||||
if pvObj, exists, _ := binder.volumeIndex.GetByKey(pvc.Spec.VolumeName); exists {
|
||||
if pv, ok := pvObj.(*api.PersistentVolume); ok {
|
||||
volume = pv
|
||||
}
|
||||
}
|
||||
}
|
||||
if unk, ok := obj.(cache.DeletedFinalStateUnknown); ok && unk.Obj != nil {
|
||||
if pv, ok := unk.Obj.(*api.PersistentVolume); ok {
|
||||
volume = pv
|
||||
}
|
||||
}
|
||||
|
||||
// sync the volume when its claim is deleted. Explicitly sync'ing the volume here in response to
|
||||
// claim deletion prevents the volume from waiting until the next sync period for its Release.
|
||||
if volume != nil {
|
||||
err := syncVolume(binder.volumeIndex, binder.client, volume)
|
||||
if err != nil {
|
||||
glog.Errorf("PVClaimBinder could not update volume %s from deleteClaim handler: %+v", volume.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func syncVolume(volumeIndex *persistentVolumeOrderedIndex, binderClient binderClient, volume *api.PersistentVolume) (err error) {
|
||||
glog.V(5).Infof("Synchronizing PersistentVolume[%s], current phase: %s\n", volume.Name, volume.Status.Phase)
|
||||
|
||||
// The PV may have been modified by parallel call to syncVolume, load
|
||||
// the current version.
|
||||
newPv, err := binderClient.GetPersistentVolume(volume.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Cannot reload volume %s: %v", volume.Name, err)
|
||||
}
|
||||
volume = newPv
|
||||
|
||||
// volumes can be in one of the following states:
|
||||
//
|
||||
// VolumePending -- default value -- not bound to a claim and not yet processed through this controller.
|
||||
// VolumeAvailable -- not bound to a claim, but processed at least once and found in this controller's volumeIndex.
|
||||
// VolumeBound -- bound to a claim because volume.Spec.ClaimRef != nil. Claim status may not be correct.
|
||||
// VolumeReleased -- volume.Spec.ClaimRef != nil but the claim has been deleted by the user.
|
||||
// VolumeFailed -- volume.Spec.ClaimRef != nil and the volume failed processing in the recycler
|
||||
currentPhase := volume.Status.Phase
|
||||
nextPhase := currentPhase
|
||||
|
||||
// Always store the newest volume state in local cache.
|
||||
_, exists, err := volumeIndex.Get(volume)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
volumeIndex.Add(volume)
|
||||
} else {
|
||||
volumeIndex.Update(volume)
|
||||
}
|
||||
|
||||
if isBeingProvisioned(volume) {
|
||||
glog.V(4).Infof("Skipping PersistentVolume[%s], waiting for provisioning to finish", volume.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
switch currentPhase {
|
||||
case api.VolumePending:
|
||||
|
||||
// 4 possible states:
|
||||
// 1. ClaimRef != nil, Claim exists, Claim UID == ClaimRef UID: Prebound to claim. Make volume available for binding (it will match PVC).
|
||||
// 2. ClaimRef != nil, Claim exists, Claim UID != ClaimRef UID: Recently recycled. Remove bind. Make volume available for new claim.
|
||||
// 3. ClaimRef != nil, Claim !exists: Recently recycled. Remove bind. Make volume available for new claim.
|
||||
// 4. ClaimRef == nil: Neither recycled nor prebound. Make volume available for binding.
|
||||
nextPhase = api.VolumeAvailable
|
||||
|
||||
if volume.Spec.ClaimRef != nil {
|
||||
claim, err := binderClient.GetPersistentVolumeClaim(volume.Spec.ClaimRef.Namespace, volume.Spec.ClaimRef.Name)
|
||||
switch {
|
||||
case err != nil && !errors.IsNotFound(err):
|
||||
return fmt.Errorf("Error getting PersistentVolumeClaim[%s/%s]: %v", volume.Spec.ClaimRef.Namespace, volume.Spec.ClaimRef.Name, err)
|
||||
case errors.IsNotFound(err) || (claim != nil && claim.UID != volume.Spec.ClaimRef.UID):
|
||||
glog.V(5).Infof("PersistentVolume[%s] has a claim ref to a claim which does not exist", volume.Name)
|
||||
if volume.Spec.PersistentVolumeReclaimPolicy == api.PersistentVolumeReclaimRecycle {
|
||||
// Pending volumes that have a ClaimRef where the claim is missing were recently recycled.
|
||||
// The Recycler set the phase to VolumePending to start the volume at the beginning of this lifecycle.
|
||||
// removing ClaimRef unbinds the volume
|
||||
clone, err := conversion.NewCloner().DeepCopy(volume)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error cloning pv: %v", err)
|
||||
}
|
||||
volumeClone, ok := clone.(*api.PersistentVolume)
|
||||
if !ok {
|
||||
return fmt.Errorf("Unexpected pv cast error : %v\n", volumeClone)
|
||||
}
|
||||
glog.V(5).Infof("PersistentVolume[%s] is recently recycled; remove claimRef.", volume.Name)
|
||||
volumeClone.Spec.ClaimRef = nil
|
||||
|
||||
if updatedVolume, err := binderClient.UpdatePersistentVolume(volumeClone); err != nil {
|
||||
return fmt.Errorf("Unexpected error saving PersistentVolume: %+v", err)
|
||||
} else {
|
||||
volume = updatedVolume
|
||||
volumeIndex.Update(volume)
|
||||
}
|
||||
} else {
|
||||
// Pending volumes that has a ClaimRef and the claim is missing and is was not recycled.
|
||||
// It must have been freshly provisioned and the claim was deleted during the provisioning.
|
||||
// Mark the volume as Released, it will be deleted.
|
||||
nextPhase = api.VolumeReleased
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamically provisioned claims remain Pending until its volume is completely provisioned.
|
||||
// The provisioner updates the PV and triggers this update for the volume. Explicitly sync'ing
|
||||
// the claim here prevents the need to wait until the next sync period when the claim would normally
|
||||
// advance to Bound phase. Otherwise, the maximum wait time for the claim to be Bound is the default sync period.
|
||||
if claim != nil && claim.Status.Phase == api.ClaimPending && keyExists(qosProvisioningKey, claim.Annotations) && isProvisioningComplete(volume) {
|
||||
syncClaim(volumeIndex, binderClient, claim)
|
||||
}
|
||||
}
|
||||
glog.V(5).Infof("PersistentVolume[%s] is available\n", volume.Name)
|
||||
|
||||
// available volumes await a claim
|
||||
case api.VolumeAvailable:
|
||||
if volume.Spec.ClaimRef != nil {
|
||||
_, err := binderClient.GetPersistentVolumeClaim(volume.Spec.ClaimRef.Namespace, volume.Spec.ClaimRef.Name)
|
||||
if err == nil {
|
||||
// change of phase will trigger an update event with the newly bound volume
|
||||
glog.V(5).Infof("PersistentVolume[%s] is now bound\n", volume.Name)
|
||||
nextPhase = api.VolumeBound
|
||||
} else {
|
||||
if errors.IsNotFound(err) {
|
||||
nextPhase = api.VolumeReleased
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//bound volumes require verification of their bound claims
|
||||
case api.VolumeBound:
|
||||
if volume.Spec.ClaimRef == nil {
|
||||
return fmt.Errorf("PersistentVolume[%s] expected to be bound but found nil claimRef: %+v", volume.Name, volume)
|
||||
} else {
|
||||
claim, err := binderClient.GetPersistentVolumeClaim(volume.Spec.ClaimRef.Namespace, volume.Spec.ClaimRef.Name)
|
||||
|
||||
// A volume is Released when its bound claim cannot be found in the API server.
|
||||
// A claim by the same name can be found if deleted and recreated before this controller can release
|
||||
// the volume from the original claim, so a UID check is necessary.
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
nextPhase = api.VolumeReleased
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
} else if claim != nil && claim.UID != volume.Spec.ClaimRef.UID {
|
||||
nextPhase = api.VolumeReleased
|
||||
}
|
||||
}
|
||||
|
||||
// released volumes require recycling
|
||||
case api.VolumeReleased:
|
||||
if volume.Spec.ClaimRef == nil {
|
||||
return fmt.Errorf("PersistentVolume[%s] expected to be bound but found nil claimRef: %+v", volume.Name, volume)
|
||||
} else {
|
||||
// another process is watching for released volumes.
|
||||
// PersistentVolumeReclaimPolicy is set per PersistentVolume
|
||||
// Recycle - sets the PV to Pending and back under this controller's management
|
||||
// Delete - delete events are handled by this controller's watch. PVs are removed from the index.
|
||||
}
|
||||
|
||||
// volumes are removed by processes external to this binder and must be removed from the cluster
|
||||
case api.VolumeFailed:
|
||||
if volume.Spec.ClaimRef == nil {
|
||||
return fmt.Errorf("PersistentVolume[%s] expected to be bound but found nil claimRef: %+v", volume.Name, volume)
|
||||
} else {
|
||||
glog.V(5).Infof("PersistentVolume[%s] previously failed recycling. Skipping.\n", volume.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if currentPhase != nextPhase {
|
||||
volume.Status.Phase = nextPhase
|
||||
|
||||
// a change in state will trigger another update through this controller.
|
||||
// each pass through this controller evaluates current phase and decides whether or not to change to the next phase
|
||||
glog.V(5).Infof("PersistentVolume[%s] changing phase from %s to %s\n", volume.Name, currentPhase, nextPhase)
|
||||
volume, err := binderClient.UpdatePersistentVolumeStatus(volume)
|
||||
if err != nil {
|
||||
// Rollback to previous phase
|
||||
volume.Status.Phase = currentPhase
|
||||
}
|
||||
volumeIndex.Update(volume)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func syncClaim(volumeIndex *persistentVolumeOrderedIndex, binderClient binderClient, claim *api.PersistentVolumeClaim) (err error) {
|
||||
glog.V(5).Infof("Synchronizing PersistentVolumeClaim[%s] for binding", claim.Name)
|
||||
|
||||
// The claim may have been modified by parallel call to syncClaim, load
|
||||
// the current version.
|
||||
newClaim, err := binderClient.GetPersistentVolumeClaim(claim.Namespace, claim.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Cannot reload claim %s/%s: %v", claim.Namespace, claim.Name, err)
|
||||
}
|
||||
claim = newClaim
|
||||
|
||||
switch claim.Status.Phase {
|
||||
case api.ClaimPending:
|
||||
// claims w/ a storage-class annotation for provisioning with *only* match volumes with a ClaimRef of the claim.
|
||||
volume, err := volumeIndex.findBestMatchForClaim(claim)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if volume == nil {
|
||||
glog.V(5).Infof("A volume match does not exist for persistent claim: %s", claim.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
if isBeingProvisioned(volume) {
|
||||
glog.V(5).Infof("PersistentVolume[%s] for PersistentVolumeClaim[%s/%s] is still being provisioned.", volume.Name, claim.Namespace, claim.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
claimRef, err := api.GetReference(claim)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unexpected error getting claim reference: %v\n", err)
|
||||
}
|
||||
|
||||
// Make a binding reference to the claim by persisting claimRef on the volume.
|
||||
// The local cache must be updated with the new bind to prevent subsequent
|
||||
// claims from binding to the volume.
|
||||
if volume.Spec.ClaimRef == nil {
|
||||
clone, err := conversion.NewCloner().DeepCopy(volume)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error cloning pv: %v", err)
|
||||
}
|
||||
volumeClone, ok := clone.(*api.PersistentVolume)
|
||||
if !ok {
|
||||
return fmt.Errorf("Unexpected pv cast error : %v\n", volumeClone)
|
||||
}
|
||||
volumeClone.Spec.ClaimRef = claimRef
|
||||
if updatedVolume, err := binderClient.UpdatePersistentVolume(volumeClone); err != nil {
|
||||
return fmt.Errorf("Unexpected error saving PersistentVolume.Status: %+v", err)
|
||||
} else {
|
||||
volume = updatedVolume
|
||||
volumeIndex.Update(updatedVolume)
|
||||
}
|
||||
}
|
||||
|
||||
// the bind is persisted on the volume above and will always match the claim in a search.
|
||||
// claim would remain Pending if the update fails, so processing this state is idempotent.
|
||||
// this only needs to be processed once.
|
||||
if claim.Spec.VolumeName != volume.Name {
|
||||
claim.Spec.VolumeName = volume.Name
|
||||
claim, err = binderClient.UpdatePersistentVolumeClaim(claim)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error updating claim with VolumeName %s: %+v\n", volume.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
claim.Status.Phase = api.ClaimBound
|
||||
claim.Status.AccessModes = volume.Spec.AccessModes
|
||||
claim.Status.Capacity = volume.Spec.Capacity
|
||||
_, err = binderClient.UpdatePersistentVolumeClaimStatus(claim)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unexpected error saving claim status: %+v", err)
|
||||
}
|
||||
|
||||
case api.ClaimBound:
|
||||
// no-op. Claim is bound, values from PV are set. PVCs are technically mutable in the API server
|
||||
// and we don't want to handle those changes at this time.
|
||||
|
||||
default:
|
||||
return fmt.Errorf("Unknown state for PVC: %#v", claim)
|
||||
|
||||
}
|
||||
|
||||
glog.V(5).Infof("PersistentVolumeClaim[%s] is bound\n", claim.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func isBeingProvisioned(volume *api.PersistentVolume) bool {
|
||||
value, found := volume.Annotations[pvProvisioningRequiredAnnotationKey]
|
||||
if found && value != pvProvisioningCompletedAnnotationValue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Run starts all of this binder's control loops
|
||||
func (controller *PersistentVolumeClaimBinder) Run() {
|
||||
glog.V(5).Infof("Starting PersistentVolumeClaimBinder\n")
|
||||
if controller.stopChannels == nil {
|
||||
controller.stopChannels = make(map[string]chan struct{})
|
||||
}
|
||||
|
||||
if _, exists := controller.stopChannels["volumes"]; !exists {
|
||||
controller.stopChannels["volumes"] = make(chan struct{})
|
||||
go controller.volumeController.Run(controller.stopChannels["volumes"])
|
||||
}
|
||||
|
||||
if _, exists := controller.stopChannels["claims"]; !exists {
|
||||
controller.stopChannels["claims"] = make(chan struct{})
|
||||
go controller.claimController.Run(controller.stopChannels["claims"])
|
||||
}
|
||||
}
|
||||
|
||||
// Stop gracefully shuts down this binder
|
||||
func (controller *PersistentVolumeClaimBinder) Stop() {
|
||||
glog.V(5).Infof("Stopping PersistentVolumeClaimBinder\n")
|
||||
for name, stopChan := range controller.stopChannels {
|
||||
close(stopChan)
|
||||
delete(controller.stopChannels, name)
|
||||
}
|
||||
}
|
||||
|
||||
// binderClient abstracts access to PVs and PVCs
|
||||
type binderClient interface {
|
||||
GetPersistentVolume(name string) (*api.PersistentVolume, error)
|
||||
UpdatePersistentVolume(volume *api.PersistentVolume) (*api.PersistentVolume, error)
|
||||
DeletePersistentVolume(volume *api.PersistentVolume) error
|
||||
UpdatePersistentVolumeStatus(volume *api.PersistentVolume) (*api.PersistentVolume, error)
|
||||
GetPersistentVolumeClaim(namespace, name string) (*api.PersistentVolumeClaim, error)
|
||||
UpdatePersistentVolumeClaim(claim *api.PersistentVolumeClaim) (*api.PersistentVolumeClaim, error)
|
||||
UpdatePersistentVolumeClaimStatus(claim *api.PersistentVolumeClaim) (*api.PersistentVolumeClaim, error)
|
||||
}
|
||||
|
||||
func NewBinderClient(c clientset.Interface) binderClient {
|
||||
return &realBinderClient{c}
|
||||
}
|
||||
|
||||
type realBinderClient struct {
|
||||
client clientset.Interface
|
||||
}
|
||||
|
||||
func (c *realBinderClient) GetPersistentVolume(name string) (*api.PersistentVolume, error) {
|
||||
return c.client.Core().PersistentVolumes().Get(name)
|
||||
}
|
||||
|
||||
func (c *realBinderClient) UpdatePersistentVolume(volume *api.PersistentVolume) (*api.PersistentVolume, error) {
|
||||
return c.client.Core().PersistentVolumes().Update(volume)
|
||||
}
|
||||
|
||||
func (c *realBinderClient) DeletePersistentVolume(volume *api.PersistentVolume) error {
|
||||
return c.client.Core().PersistentVolumes().Delete(volume.Name, nil)
|
||||
}
|
||||
|
||||
func (c *realBinderClient) UpdatePersistentVolumeStatus(volume *api.PersistentVolume) (*api.PersistentVolume, error) {
|
||||
return c.client.Core().PersistentVolumes().UpdateStatus(volume)
|
||||
}
|
||||
|
||||
func (c *realBinderClient) GetPersistentVolumeClaim(namespace, name string) (*api.PersistentVolumeClaim, error) {
|
||||
return c.client.Core().PersistentVolumeClaims(namespace).Get(name)
|
||||
}
|
||||
|
||||
func (c *realBinderClient) UpdatePersistentVolumeClaim(claim *api.PersistentVolumeClaim) (*api.PersistentVolumeClaim, error) {
|
||||
return c.client.Core().PersistentVolumeClaims(claim.Namespace).Update(claim)
|
||||
}
|
||||
|
||||
func (c *realBinderClient) UpdatePersistentVolumeClaimStatus(claim *api.PersistentVolumeClaim) (*api.PersistentVolumeClaim, error) {
|
||||
return c.client.Core().PersistentVolumeClaims(claim.Namespace).UpdateStatus(claim)
|
||||
}
|
@ -1,732 +0,0 @@
|
||||
/*
|
||||
Copyright 2014 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 (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/errors"
|
||||
"k8s.io/kubernetes/pkg/api/resource"
|
||||
"k8s.io/kubernetes/pkg/api/testapi"
|
||||
"k8s.io/kubernetes/pkg/apimachinery/registered"
|
||||
"k8s.io/kubernetes/pkg/client/cache"
|
||||
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
|
||||
"k8s.io/kubernetes/pkg/client/testing/core"
|
||||
"k8s.io/kubernetes/pkg/types"
|
||||
utiltesting "k8s.io/kubernetes/pkg/util/testing"
|
||||
"k8s.io/kubernetes/pkg/volume"
|
||||
"k8s.io/kubernetes/pkg/volume/host_path"
|
||||
volumetest "k8s.io/kubernetes/pkg/volume/testing"
|
||||
)
|
||||
|
||||
func TestRunStop(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset()
|
||||
binder := NewPersistentVolumeClaimBinder(clientset, 1*time.Second)
|
||||
|
||||
if len(binder.stopChannels) != 0 {
|
||||
t.Errorf("Non-running binder should not have any stopChannels. Got %v", len(binder.stopChannels))
|
||||
}
|
||||
|
||||
binder.Run()
|
||||
|
||||
if len(binder.stopChannels) != 2 {
|
||||
t.Errorf("Running binder should have exactly 2 stopChannels. Got %v", len(binder.stopChannels))
|
||||
}
|
||||
|
||||
binder.Stop()
|
||||
|
||||
if len(binder.stopChannels) != 0 {
|
||||
t.Errorf("Non-running binder should not have any stopChannels. Got %v", len(binder.stopChannels))
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaimRace(t *testing.T) {
|
||||
tmpDir, err := utiltesting.MkTmpdir("claimbinder-test")
|
||||
if err != nil {
|
||||
t.Fatalf("error creating temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
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: fmt.Sprintf("%s/data01", tmpDir),
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumePending,
|
||||
},
|
||||
}
|
||||
|
||||
volumeIndex := NewPersistentVolumeOrderedIndex()
|
||||
mockClient := &mockBinderClient{}
|
||||
mockClient.volume = v
|
||||
|
||||
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
|
||||
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")
|
||||
}
|
||||
|
||||
// add the claim to fake API server
|
||||
mockClient.UpdatePersistentVolumeClaim(c1)
|
||||
// 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
|
||||
// add the 2nd claim to fake API server
|
||||
mockClient.UpdatePersistentVolumeClaim(c2)
|
||||
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 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 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 TestExampleObjects(t *testing.T) {
|
||||
scenarios := map[string]struct {
|
||||
expected interface{}
|
||||
}{
|
||||
"claims/claim-01.yaml": {
|
||||
expected: &api.PersistentVolumeClaim{
|
||||
Spec: api.PersistentVolumeClaimSpec{
|
||||
AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce},
|
||||
Resources: api.ResourceRequirements{
|
||||
Requests: api.ResourceList{
|
||||
api.ResourceName(api.ResourceStorage): resource.MustParse("3Gi"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"claims/claim-02.yaml": {
|
||||
expected: &api.PersistentVolumeClaim{
|
||||
Spec: api.PersistentVolumeClaimSpec{
|
||||
AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce},
|
||||
Resources: api.ResourceRequirements{
|
||||
Requests: api.ResourceList{
|
||||
api.ResourceName(api.ResourceStorage): resource.MustParse("8Gi"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"volumes/local-01.yaml": {
|
||||
expected: &api.PersistentVolume{
|
||||
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: "/somepath/data01",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"volumes/local-02.yaml": {
|
||||
expected: &api.PersistentVolume{
|
||||
Spec: api.PersistentVolumeSpec{
|
||||
AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce},
|
||||
Capacity: api.ResourceList{
|
||||
api.ResourceName(api.ResourceStorage): resource.MustParse("8Gi"),
|
||||
},
|
||||
PersistentVolumeSource: api.PersistentVolumeSource{
|
||||
HostPath: &api.HostPathVolumeSource{
|
||||
Path: "/somepath/data02",
|
||||
},
|
||||
},
|
||||
PersistentVolumeReclaimPolicy: api.PersistentVolumeReclaimRecycle,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, scenario := range scenarios {
|
||||
codec := api.Codecs.UniversalDecoder()
|
||||
o := core.NewObjects(api.Scheme, codec)
|
||||
if err := core.AddObjectsFromPath("../../../docs/user-guide/persistent-volumes/"+name, o, codec); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
clientset := &fake.Clientset{}
|
||||
clientset.AddReactor("*", "*", core.ObjectReaction(o, registered.RESTMapper()))
|
||||
|
||||
if reflect.TypeOf(scenario.expected) == reflect.TypeOf(&api.PersistentVolumeClaim{}) {
|
||||
pvc, err := clientset.Core().PersistentVolumeClaims("ns").Get("doesntmatter")
|
||||
if err != nil {
|
||||
t.Fatalf("Error retrieving object: %v", err)
|
||||
}
|
||||
|
||||
expected := scenario.expected.(*api.PersistentVolumeClaim)
|
||||
if pvc.Spec.AccessModes[0] != expected.Spec.AccessModes[0] {
|
||||
t.Errorf("Unexpected mismatch. Got %v wanted %v", pvc.Spec.AccessModes[0], expected.Spec.AccessModes[0])
|
||||
}
|
||||
|
||||
aQty := pvc.Spec.Resources.Requests[api.ResourceStorage]
|
||||
bQty := expected.Spec.Resources.Requests[api.ResourceStorage]
|
||||
aSize := aQty.Value()
|
||||
bSize := bQty.Value()
|
||||
|
||||
if aSize != bSize {
|
||||
t.Errorf("Unexpected mismatch. Got %v wanted %v", aSize, bSize)
|
||||
}
|
||||
}
|
||||
|
||||
if reflect.TypeOf(scenario.expected) == reflect.TypeOf(&api.PersistentVolume{}) {
|
||||
pv, err := clientset.Core().PersistentVolumes().Get("doesntmatter")
|
||||
if err != nil {
|
||||
t.Fatalf("Error retrieving object: %v", err)
|
||||
}
|
||||
|
||||
expected := scenario.expected.(*api.PersistentVolume)
|
||||
if pv.Spec.AccessModes[0] != expected.Spec.AccessModes[0] {
|
||||
t.Errorf("Unexpected mismatch. Got %v wanted %v", pv.Spec.AccessModes[0], expected.Spec.AccessModes[0])
|
||||
}
|
||||
|
||||
aQty := pv.Spec.Capacity[api.ResourceStorage]
|
||||
bQty := expected.Spec.Capacity[api.ResourceStorage]
|
||||
aSize := aQty.Value()
|
||||
bSize := bQty.Value()
|
||||
|
||||
if aSize != bSize {
|
||||
t.Errorf("Unexpected mismatch. Got %v wanted %v", aSize, bSize)
|
||||
}
|
||||
|
||||
if pv.Spec.HostPath.Path != expected.Spec.HostPath.Path {
|
||||
t.Errorf("Unexpected mismatch. Got %v wanted %v", pv.Spec.HostPath.Path, expected.Spec.HostPath.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindingWithExamples(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", "")
|
||||
|
||||
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)
|
||||
|
||||
// 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 TestCasting(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset()
|
||||
binder := NewPersistentVolumeClaimBinder(clientset, 1*time.Second)
|
||||
|
||||
pv := &api.PersistentVolume{}
|
||||
unk := cache.DeletedFinalStateUnknown{}
|
||||
pvc := &api.PersistentVolumeClaim{
|
||||
ObjectMeta: api.ObjectMeta{Name: "foo"},
|
||||
Status: api.PersistentVolumeClaimStatus{Phase: api.ClaimBound},
|
||||
}
|
||||
|
||||
// Inject mockClient into the binder. This prevents weird errors on stderr
|
||||
// as the binder wants to load PV/PVC from API server.
|
||||
mockClient := &mockBinderClient{
|
||||
volume: pv,
|
||||
claim: pvc,
|
||||
}
|
||||
binder.client = mockClient
|
||||
|
||||
// none of these should fail casting.
|
||||
// the real test is not failing when passed DeletedFinalStateUnknown in the deleteHandler
|
||||
binder.addVolume(pv)
|
||||
binder.updateVolume(pv, pv)
|
||||
binder.deleteVolume(pv)
|
||||
binder.deleteVolume(unk)
|
||||
binder.addClaim(pvc)
|
||||
binder.updateClaim(pvc, pvc)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
type mockBinderClient struct {
|
||||
volume *api.PersistentVolume
|
||||
claim *api.PersistentVolumeClaim
|
||||
}
|
||||
|
||||
func (c *mockBinderClient) GetPersistentVolume(name string) (*api.PersistentVolume, error) {
|
||||
return c.volume, nil
|
||||
}
|
||||
|
||||
func (c *mockBinderClient) UpdatePersistentVolume(volume *api.PersistentVolume) (*api.PersistentVolume, error) {
|
||||
c.volume = volume
|
||||
return c.volume, nil
|
||||
}
|
||||
|
||||
func (c *mockBinderClient) DeletePersistentVolume(volume *api.PersistentVolume) error {
|
||||
c.volume = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockBinderClient) UpdatePersistentVolumeStatus(volume *api.PersistentVolume) (*api.PersistentVolume, error) {
|
||||
c.volume = volume
|
||||
return c.volume, nil
|
||||
}
|
||||
|
||||
func (c *mockBinderClient) GetPersistentVolumeClaim(namespace, name string) (*api.PersistentVolumeClaim, error) {
|
||||
if c.claim != nil {
|
||||
return c.claim, nil
|
||||
} else {
|
||||
return nil, errors.NewNotFound(api.Resource("persistentvolumes"), name)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *mockBinderClient) UpdatePersistentVolumeClaim(claim *api.PersistentVolumeClaim) (*api.PersistentVolumeClaim, error) {
|
||||
c.claim = claim
|
||||
return c.claim, nil
|
||||
}
|
||||
|
||||
func (c *mockBinderClient) UpdatePersistentVolumeClaimStatus(claim *api.PersistentVolumeClaim) (*api.PersistentVolumeClaim, error) {
|
||||
c.claim = claim
|
||||
return c.claim, nil
|
||||
}
|
||||
|
||||
func newMockRecycler(spec *volume.Spec, host volume.VolumeHost, config volume.VolumeConfig) (volume.Recycler, error) {
|
||||
return &mockRecycler{
|
||||
path: spec.PersistentVolume.Spec.HostPath.Path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type mockRecycler struct {
|
||||
path string
|
||||
host volume.VolumeHost
|
||||
volume.MetricsNil
|
||||
}
|
||||
|
||||
func (r *mockRecycler) GetPath() string {
|
||||
return r.path
|
||||
}
|
||||
|
||||
func (r *mockRecycler) Recycle() error {
|
||||
// return nil means recycle passed
|
||||
return nil
|
||||
}
|
@ -1,536 +0,0 @@
|
||||
/*
|
||||
Copyright 2015 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 (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/client/cache"
|
||||
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
|
||||
"k8s.io/kubernetes/pkg/cloudprovider"
|
||||
"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/io"
|
||||
"k8s.io/kubernetes/pkg/util/mount"
|
||||
"k8s.io/kubernetes/pkg/volume"
|
||||
"k8s.io/kubernetes/pkg/watch"
|
||||
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// PersistentVolumeProvisionerController reconciles the state of all PersistentVolumes and PersistentVolumeClaims.
|
||||
type PersistentVolumeProvisionerController struct {
|
||||
volumeController *framework.Controller
|
||||
volumeStore cache.Store
|
||||
claimController *framework.Controller
|
||||
claimStore cache.Store
|
||||
client controllerClient
|
||||
cloud cloudprovider.Interface
|
||||
provisioner volume.ProvisionableVolumePlugin
|
||||
pluginMgr volume.VolumePluginMgr
|
||||
stopChannels map[string]chan struct{}
|
||||
mutex sync.RWMutex
|
||||
clusterName string
|
||||
}
|
||||
|
||||
// constant name values for the controllers stopChannels map.
|
||||
// the controller uses these for graceful shutdown
|
||||
const volumesStopChannel = "volumes"
|
||||
const claimsStopChannel = "claims"
|
||||
|
||||
// NewPersistentVolumeProvisionerController creates a new PersistentVolumeProvisionerController
|
||||
func NewPersistentVolumeProvisionerController(client controllerClient, syncPeriod time.Duration, clusterName string, plugins []volume.VolumePlugin, provisioner volume.ProvisionableVolumePlugin, cloud cloudprovider.Interface) (*PersistentVolumeProvisionerController, error) {
|
||||
controller := &PersistentVolumeProvisionerController{
|
||||
client: client,
|
||||
cloud: cloud,
|
||||
provisioner: provisioner,
|
||||
clusterName: clusterName,
|
||||
}
|
||||
|
||||
if err := controller.pluginMgr.InitPlugins(plugins, controller); err != nil {
|
||||
return nil, fmt.Errorf("Could not initialize volume plugins for PersistentVolumeProvisionerController: %+v", err)
|
||||
}
|
||||
|
||||
glog.V(5).Infof("Initializing provisioner: %s", controller.provisioner.Name())
|
||||
controller.provisioner.Init(controller)
|
||||
|
||||
controller.volumeStore, controller.volumeController = framework.NewInformer(
|
||||
&cache.ListWatch{
|
||||
ListFunc: func(options api.ListOptions) (runtime.Object, error) {
|
||||
return client.ListPersistentVolumes(options)
|
||||
},
|
||||
WatchFunc: func(options api.ListOptions) (watch.Interface, error) {
|
||||
return client.WatchPersistentVolumes(options)
|
||||
},
|
||||
},
|
||||
&api.PersistentVolume{},
|
||||
syncPeriod,
|
||||
framework.ResourceEventHandlerFuncs{
|
||||
AddFunc: controller.handleAddVolume,
|
||||
UpdateFunc: controller.handleUpdateVolume,
|
||||
// delete handler not needed in this controller.
|
||||
// volume deletion is handled by the recycler controller
|
||||
},
|
||||
)
|
||||
controller.claimStore, controller.claimController = framework.NewInformer(
|
||||
&cache.ListWatch{
|
||||
ListFunc: func(options api.ListOptions) (runtime.Object, error) {
|
||||
return client.ListPersistentVolumeClaims(api.NamespaceAll, options)
|
||||
},
|
||||
WatchFunc: func(options api.ListOptions) (watch.Interface, error) {
|
||||
return client.WatchPersistentVolumeClaims(api.NamespaceAll, options)
|
||||
},
|
||||
},
|
||||
&api.PersistentVolumeClaim{},
|
||||
syncPeriod,
|
||||
framework.ResourceEventHandlerFuncs{
|
||||
AddFunc: controller.handleAddClaim,
|
||||
UpdateFunc: controller.handleUpdateClaim,
|
||||
// delete handler not needed.
|
||||
// normal recycling applies when a claim is deleted.
|
||||
// recycling is handled by the binding controller.
|
||||
},
|
||||
)
|
||||
|
||||
return controller, nil
|
||||
}
|
||||
|
||||
func (controller *PersistentVolumeProvisionerController) handleAddVolume(obj interface{}) {
|
||||
controller.mutex.Lock()
|
||||
defer controller.mutex.Unlock()
|
||||
cachedPv, _, _ := controller.volumeStore.Get(obj)
|
||||
if pv, ok := cachedPv.(*api.PersistentVolume); ok {
|
||||
err := controller.reconcileVolume(pv)
|
||||
if err != nil {
|
||||
glog.Errorf("Error reconciling volume %s: %+v", pv.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (controller *PersistentVolumeProvisionerController) handleUpdateVolume(oldObj, newObj interface{}) {
|
||||
// The flow for Update is the same as Add.
|
||||
// A volume is only provisioned if not done so already.
|
||||
controller.handleAddVolume(newObj)
|
||||
}
|
||||
|
||||
func (controller *PersistentVolumeProvisionerController) handleAddClaim(obj interface{}) {
|
||||
controller.mutex.Lock()
|
||||
defer controller.mutex.Unlock()
|
||||
cachedPvc, exists, _ := controller.claimStore.Get(obj)
|
||||
if !exists {
|
||||
glog.Errorf("PersistentVolumeClaim does not exist in the local cache: %+v", obj)
|
||||
return
|
||||
}
|
||||
if pvc, ok := cachedPvc.(*api.PersistentVolumeClaim); ok {
|
||||
err := controller.reconcileClaim(pvc)
|
||||
if err != nil {
|
||||
glog.Errorf("Error encoutered reconciling claim %s: %+v", pvc.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (controller *PersistentVolumeProvisionerController) handleUpdateClaim(oldObj, newObj interface{}) {
|
||||
// The flow for Update is the same as Add.
|
||||
// A volume is only provisioned for a claim if not done so already.
|
||||
controller.handleAddClaim(newObj)
|
||||
}
|
||||
|
||||
func (controller *PersistentVolumeProvisionerController) reconcileClaim(claim *api.PersistentVolumeClaim) error {
|
||||
glog.V(5).Infof("Synchronizing PersistentVolumeClaim[%s] for dynamic provisioning", claim.Name)
|
||||
|
||||
// The claim may have been modified by parallel call to reconcileClaim, load
|
||||
// the current version.
|
||||
newClaim, err := controller.client.GetPersistentVolumeClaim(claim.Namespace, claim.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Cannot reload claim %s/%s: %v", claim.Namespace, claim.Name, err)
|
||||
}
|
||||
claim = newClaim
|
||||
err = controller.claimStore.Update(claim)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Cannot update claim %s/%s: %v", claim.Namespace, claim.Name, err)
|
||||
}
|
||||
|
||||
if controller.provisioner == nil {
|
||||
return fmt.Errorf("No provisioner configured for controller")
|
||||
}
|
||||
|
||||
// no provisioning requested, return Pending. Claim may be pending indefinitely without a match.
|
||||
if !keyExists(qosProvisioningKey, claim.Annotations) {
|
||||
glog.V(5).Infof("PersistentVolumeClaim[%s] no provisioning required", claim.Name)
|
||||
return nil
|
||||
}
|
||||
if len(claim.Spec.VolumeName) != 0 {
|
||||
glog.V(5).Infof("PersistentVolumeClaim[%s] already bound. No provisioning required", claim.Name)
|
||||
return nil
|
||||
}
|
||||
if isAnnotationMatch(pvProvisioningRequiredAnnotationKey, pvProvisioningCompletedAnnotationValue, claim.Annotations) {
|
||||
glog.V(5).Infof("PersistentVolumeClaim[%s] is already provisioned.", claim.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
glog.V(5).Infof("PersistentVolumeClaim[%s] provisioning", claim.Name)
|
||||
provisioner, err := controller.newProvisioner(controller.provisioner, claim, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unexpected error getting new provisioner for claim %s: %v\n", claim.Name, err)
|
||||
}
|
||||
newVolume, err := provisioner.NewPersistentVolumeTemplate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unexpected error getting new volume template for claim %s: %v\n", claim.Name, err)
|
||||
}
|
||||
|
||||
claimRef, err := api.GetReference(claim)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unexpected error getting claim reference for %s: %v\n", claim.Name, err)
|
||||
}
|
||||
|
||||
storageClass, _ := claim.Annotations[qosProvisioningKey]
|
||||
|
||||
// the creation of this volume is the bind to the claim.
|
||||
// The claim will match the volume during the next sync period when the volume is in the local cache
|
||||
newVolume.Spec.ClaimRef = claimRef
|
||||
newVolume.Annotations[pvProvisioningRequiredAnnotationKey] = "true"
|
||||
newVolume.Annotations[qosProvisioningKey] = storageClass
|
||||
newVolume, err = controller.client.CreatePersistentVolume(newVolume)
|
||||
glog.V(5).Infof("Unprovisioned PersistentVolume[%s] created for PVC[%s], which will be fulfilled in the storage provider", newVolume.Name, claim.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("PersistentVolumeClaim[%s] failed provisioning: %+v", claim.Name, err)
|
||||
}
|
||||
|
||||
claim.Annotations[pvProvisioningRequiredAnnotationKey] = pvProvisioningCompletedAnnotationValue
|
||||
_, err = controller.client.UpdatePersistentVolumeClaim(claim)
|
||||
if err != nil {
|
||||
glog.Errorf("error updating persistent volume claim: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (controller *PersistentVolumeProvisionerController) reconcileVolume(pv *api.PersistentVolume) error {
|
||||
glog.V(5).Infof("PersistentVolume[%s] reconciling", pv.Name)
|
||||
|
||||
// The PV may have been modified by parallel call to reconcileVolume, load
|
||||
// the current version.
|
||||
newPv, err := controller.client.GetPersistentVolume(pv.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Cannot reload volume %s: %v", pv.Name, err)
|
||||
}
|
||||
pv = newPv
|
||||
|
||||
if pv.Spec.ClaimRef == nil {
|
||||
glog.V(5).Infof("PersistentVolume[%s] is not bound to a claim. No provisioning required", pv.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: fix this leaky abstraction. Had to make our own store key because ClaimRef fails the default keyfunc (no Meta on object).
|
||||
obj, exists, _ := controller.claimStore.GetByKey(fmt.Sprintf("%s/%s", pv.Spec.ClaimRef.Namespace, pv.Spec.ClaimRef.Name))
|
||||
if !exists {
|
||||
return fmt.Errorf("PersistentVolumeClaim[%s/%s] not found in local cache", pv.Spec.ClaimRef.Namespace, pv.Spec.ClaimRef.Name)
|
||||
}
|
||||
|
||||
claim, ok := obj.(*api.PersistentVolumeClaim)
|
||||
if !ok {
|
||||
return fmt.Errorf("PersistentVolumeClaim expected, but got %v", obj)
|
||||
}
|
||||
|
||||
// no provisioning required, volume is ready and Bound
|
||||
if !keyExists(pvProvisioningRequiredAnnotationKey, pv.Annotations) {
|
||||
glog.V(5).Infof("PersistentVolume[%s] does not require provisioning", pv.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// provisioning is completed, volume is ready.
|
||||
if isProvisioningComplete(pv) {
|
||||
glog.V(5).Infof("PersistentVolume[%s] is bound and provisioning is complete", pv.Name)
|
||||
if pv.Spec.ClaimRef.Namespace != claim.Namespace || pv.Spec.ClaimRef.Name != claim.Name {
|
||||
return fmt.Errorf("pre-bind mismatch - expected %s but found %s/%s", claimToClaimKey(claim), pv.Spec.ClaimRef.Namespace, pv.Spec.ClaimRef.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// provisioning is incomplete. Attempt to provision the volume.
|
||||
glog.V(5).Infof("PersistentVolume[%s] provisioning in progress", pv.Name)
|
||||
err = provisionVolume(pv, controller)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error provisioning PersistentVolume[%s]: %v", pv.Name, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// provisionVolume provisions a volume that has been created in the cluster but not yet fulfilled by
|
||||
// the storage provider.
|
||||
func provisionVolume(pv *api.PersistentVolume, controller *PersistentVolumeProvisionerController) error {
|
||||
if isProvisioningComplete(pv) {
|
||||
return fmt.Errorf("PersistentVolume[%s] is already provisioned", pv.Name)
|
||||
}
|
||||
|
||||
if _, exists := pv.Annotations[qosProvisioningKey]; !exists {
|
||||
return fmt.Errorf("PersistentVolume[%s] does not contain a provisioning request. Provisioning not required.", pv.Name)
|
||||
}
|
||||
|
||||
if controller.provisioner == nil {
|
||||
return fmt.Errorf("No provisioner found for volume: %s", pv.Name)
|
||||
}
|
||||
|
||||
// Find the claim in local cache
|
||||
obj, exists, _ := controller.claimStore.GetByKey(fmt.Sprintf("%s/%s", pv.Spec.ClaimRef.Namespace, pv.Spec.ClaimRef.Name))
|
||||
if !exists {
|
||||
return fmt.Errorf("Could not find PersistentVolumeClaim[%s/%s] in local cache", pv.Spec.ClaimRef.Name, pv.Name)
|
||||
}
|
||||
claim := obj.(*api.PersistentVolumeClaim)
|
||||
|
||||
provisioner, _ := controller.newProvisioner(controller.provisioner, claim, pv)
|
||||
err := provisioner.Provision(pv)
|
||||
if err != nil {
|
||||
glog.Errorf("Could not provision %s", pv.Name)
|
||||
pv.Status.Phase = api.VolumeFailed
|
||||
pv.Status.Message = err.Error()
|
||||
if pv, apiErr := controller.client.UpdatePersistentVolumeStatus(pv); apiErr != nil {
|
||||
return fmt.Errorf("PersistentVolume[%s] failed provisioning and also failed status update: %v - %v", pv.Name, err, apiErr)
|
||||
}
|
||||
return fmt.Errorf("PersistentVolume[%s] failed provisioning: %v", pv.Name, err)
|
||||
}
|
||||
|
||||
clone, err := conversion.NewCloner().DeepCopy(pv)
|
||||
volumeClone, ok := clone.(*api.PersistentVolume)
|
||||
if !ok {
|
||||
return fmt.Errorf("Unexpected pv cast error : %v\n", volumeClone)
|
||||
}
|
||||
volumeClone.Annotations[pvProvisioningRequiredAnnotationKey] = pvProvisioningCompletedAnnotationValue
|
||||
|
||||
pv, err = controller.client.UpdatePersistentVolume(volumeClone)
|
||||
if err != nil {
|
||||
// TODO: https://github.com/kubernetes/kubernetes/issues/14443
|
||||
// the volume was created in the infrastructure and likely has a PV name on it,
|
||||
// but we failed to save the annotation that marks the volume as provisioned.
|
||||
return fmt.Errorf("Error updating PersistentVolume[%s] with provisioning completed annotation. There is a potential for dupes and orphans.", volumeClone.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run starts all of this controller's control loops
|
||||
func (controller *PersistentVolumeProvisionerController) Run() {
|
||||
glog.V(5).Infof("Starting PersistentVolumeProvisionerController\n")
|
||||
if controller.stopChannels == nil {
|
||||
controller.stopChannels = make(map[string]chan struct{})
|
||||
}
|
||||
|
||||
if _, exists := controller.stopChannels[volumesStopChannel]; !exists {
|
||||
controller.stopChannels[volumesStopChannel] = make(chan struct{})
|
||||
go controller.volumeController.Run(controller.stopChannels[volumesStopChannel])
|
||||
}
|
||||
|
||||
if _, exists := controller.stopChannels[claimsStopChannel]; !exists {
|
||||
controller.stopChannels[claimsStopChannel] = make(chan struct{})
|
||||
go controller.claimController.Run(controller.stopChannels[claimsStopChannel])
|
||||
}
|
||||
}
|
||||
|
||||
// Stop gracefully shuts down this controller
|
||||
func (controller *PersistentVolumeProvisionerController) Stop() {
|
||||
glog.V(5).Infof("Stopping PersistentVolumeProvisionerController\n")
|
||||
for name, stopChan := range controller.stopChannels {
|
||||
close(stopChan)
|
||||
delete(controller.stopChannels, name)
|
||||
}
|
||||
}
|
||||
|
||||
func (controller *PersistentVolumeProvisionerController) newProvisioner(plugin volume.ProvisionableVolumePlugin, claim *api.PersistentVolumeClaim, pv *api.PersistentVolume) (volume.Provisioner, error) {
|
||||
tags := make(map[string]string)
|
||||
tags[cloudVolumeCreatedForClaimNamespaceTag] = claim.Namespace
|
||||
tags[cloudVolumeCreatedForClaimNameTag] = claim.Name
|
||||
|
||||
// pv can be nil when the provisioner has not created the PV yet
|
||||
if pv != nil {
|
||||
tags[cloudVolumeCreatedForVolumeNameTag] = pv.Name
|
||||
}
|
||||
|
||||
volumeOptions := volume.VolumeOptions{
|
||||
Capacity: claim.Spec.Resources.Requests[api.ResourceName(api.ResourceStorage)],
|
||||
AccessModes: claim.Spec.AccessModes,
|
||||
PersistentVolumeReclaimPolicy: api.PersistentVolumeReclaimDelete,
|
||||
CloudTags: &tags,
|
||||
ClusterName: controller.clusterName,
|
||||
}
|
||||
|
||||
if pv != nil {
|
||||
volumeOptions.PVName = pv.Name
|
||||
}
|
||||
|
||||
provisioner, err := plugin.NewProvisioner(volumeOptions)
|
||||
return provisioner, err
|
||||
}
|
||||
|
||||
// controllerClient abstracts access to PVs and PVCs. Easy to mock for testing and wrap for real client.
|
||||
type controllerClient interface {
|
||||
CreatePersistentVolume(pv *api.PersistentVolume) (*api.PersistentVolume, error)
|
||||
ListPersistentVolumes(options api.ListOptions) (*api.PersistentVolumeList, error)
|
||||
WatchPersistentVolumes(options api.ListOptions) (watch.Interface, error)
|
||||
GetPersistentVolume(name string) (*api.PersistentVolume, error)
|
||||
UpdatePersistentVolume(volume *api.PersistentVolume) (*api.PersistentVolume, error)
|
||||
DeletePersistentVolume(volume *api.PersistentVolume) error
|
||||
UpdatePersistentVolumeStatus(volume *api.PersistentVolume) (*api.PersistentVolume, error)
|
||||
|
||||
GetPersistentVolumeClaim(namespace, name string) (*api.PersistentVolumeClaim, error)
|
||||
ListPersistentVolumeClaims(namespace string, options api.ListOptions) (*api.PersistentVolumeClaimList, error)
|
||||
WatchPersistentVolumeClaims(namespace string, options api.ListOptions) (watch.Interface, error)
|
||||
UpdatePersistentVolumeClaim(claim *api.PersistentVolumeClaim) (*api.PersistentVolumeClaim, error)
|
||||
UpdatePersistentVolumeClaimStatus(claim *api.PersistentVolumeClaim) (*api.PersistentVolumeClaim, error)
|
||||
|
||||
// provided to give VolumeHost and plugins access to the kube client
|
||||
GetKubeClient() clientset.Interface
|
||||
}
|
||||
|
||||
func NewControllerClient(c clientset.Interface) controllerClient {
|
||||
return &realControllerClient{c}
|
||||
}
|
||||
|
||||
var _ controllerClient = &realControllerClient{}
|
||||
|
||||
type realControllerClient struct {
|
||||
client clientset.Interface
|
||||
}
|
||||
|
||||
func (c *realControllerClient) GetPersistentVolume(name string) (*api.PersistentVolume, error) {
|
||||
return c.client.Core().PersistentVolumes().Get(name)
|
||||
}
|
||||
|
||||
func (c *realControllerClient) ListPersistentVolumes(options api.ListOptions) (*api.PersistentVolumeList, error) {
|
||||
return c.client.Core().PersistentVolumes().List(options)
|
||||
}
|
||||
|
||||
func (c *realControllerClient) WatchPersistentVolumes(options api.ListOptions) (watch.Interface, error) {
|
||||
return c.client.Core().PersistentVolumes().Watch(options)
|
||||
}
|
||||
|
||||
func (c *realControllerClient) CreatePersistentVolume(pv *api.PersistentVolume) (*api.PersistentVolume, error) {
|
||||
return c.client.Core().PersistentVolumes().Create(pv)
|
||||
}
|
||||
|
||||
func (c *realControllerClient) UpdatePersistentVolume(volume *api.PersistentVolume) (*api.PersistentVolume, error) {
|
||||
return c.client.Core().PersistentVolumes().Update(volume)
|
||||
}
|
||||
|
||||
func (c *realControllerClient) DeletePersistentVolume(volume *api.PersistentVolume) error {
|
||||
return c.client.Core().PersistentVolumes().Delete(volume.Name, nil)
|
||||
}
|
||||
|
||||
func (c *realControllerClient) UpdatePersistentVolumeStatus(volume *api.PersistentVolume) (*api.PersistentVolume, error) {
|
||||
return c.client.Core().PersistentVolumes().UpdateStatus(volume)
|
||||
}
|
||||
|
||||
func (c *realControllerClient) GetPersistentVolumeClaim(namespace, name string) (*api.PersistentVolumeClaim, error) {
|
||||
return c.client.Core().PersistentVolumeClaims(namespace).Get(name)
|
||||
}
|
||||
|
||||
func (c *realControllerClient) ListPersistentVolumeClaims(namespace string, options api.ListOptions) (*api.PersistentVolumeClaimList, error) {
|
||||
return c.client.Core().PersistentVolumeClaims(namespace).List(options)
|
||||
}
|
||||
|
||||
func (c *realControllerClient) WatchPersistentVolumeClaims(namespace string, options api.ListOptions) (watch.Interface, error) {
|
||||
return c.client.Core().PersistentVolumeClaims(namespace).Watch(options)
|
||||
}
|
||||
|
||||
func (c *realControllerClient) UpdatePersistentVolumeClaim(claim *api.PersistentVolumeClaim) (*api.PersistentVolumeClaim, error) {
|
||||
return c.client.Core().PersistentVolumeClaims(claim.Namespace).Update(claim)
|
||||
}
|
||||
|
||||
func (c *realControllerClient) UpdatePersistentVolumeClaimStatus(claim *api.PersistentVolumeClaim) (*api.PersistentVolumeClaim, error) {
|
||||
return c.client.Core().PersistentVolumeClaims(claim.Namespace).UpdateStatus(claim)
|
||||
}
|
||||
|
||||
func (c *realControllerClient) GetKubeClient() clientset.Interface {
|
||||
return c.client
|
||||
}
|
||||
|
||||
func keyExists(key string, haystack map[string]string) bool {
|
||||
_, exists := haystack[key]
|
||||
return exists
|
||||
}
|
||||
|
||||
func isProvisioningComplete(pv *api.PersistentVolume) bool {
|
||||
return isAnnotationMatch(pvProvisioningRequiredAnnotationKey, pvProvisioningCompletedAnnotationValue, pv.Annotations)
|
||||
}
|
||||
|
||||
func isAnnotationMatch(key, needle string, haystack map[string]string) bool {
|
||||
value, exists := haystack[key]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
return value == needle
|
||||
}
|
||||
|
||||
func isRecyclable(policy api.PersistentVolumeReclaimPolicy) bool {
|
||||
return policy == api.PersistentVolumeReclaimDelete || policy == api.PersistentVolumeReclaimRecycle
|
||||
}
|
||||
|
||||
// VolumeHost implementation
|
||||
// PersistentVolumeRecycler is host to the volume plugins, but does not actually mount any volumes.
|
||||
// Because no mounting is performed, most of the VolumeHost methods are not implemented.
|
||||
func (c *PersistentVolumeProvisionerController) GetPluginDir(podUID string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *PersistentVolumeProvisionerController) GetPodVolumeDir(podUID types.UID, pluginName, volumeName string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *PersistentVolumeProvisionerController) GetPodPluginDir(podUID types.UID, pluginName string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *PersistentVolumeProvisionerController) GetKubeClient() clientset.Interface {
|
||||
return c.client.GetKubeClient()
|
||||
}
|
||||
|
||||
func (c *PersistentVolumeProvisionerController) NewWrapperMounter(volName string, spec volume.Spec, pod *api.Pod, opts volume.VolumeOptions) (volume.Mounter, error) {
|
||||
return nil, fmt.Errorf("NewWrapperMounter not supported by PVClaimBinder's VolumeHost implementation")
|
||||
}
|
||||
|
||||
func (c *PersistentVolumeProvisionerController) NewWrapperUnmounter(volName string, spec volume.Spec, podUID types.UID) (volume.Unmounter, error) {
|
||||
return nil, fmt.Errorf("NewWrapperUnmounter not supported by PVClaimBinder's VolumeHost implementation")
|
||||
}
|
||||
|
||||
func (c *PersistentVolumeProvisionerController) GetCloudProvider() cloudprovider.Interface {
|
||||
return c.cloud
|
||||
}
|
||||
|
||||
func (c *PersistentVolumeProvisionerController) GetMounter() mount.Interface {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *PersistentVolumeProvisionerController) GetWriter() io.Writer {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *PersistentVolumeProvisionerController) GetHostName() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
const (
|
||||
// these pair of constants are used by the provisioner.
|
||||
// The key is a kube namespaced key that denotes a volume requires provisioning.
|
||||
// The value is set only when provisioning is completed. Any other value will tell the provisioner
|
||||
// that provisioning has not yet occurred.
|
||||
pvProvisioningRequiredAnnotationKey = "volume.experimental.kubernetes.io/provisioning-required"
|
||||
pvProvisioningCompletedAnnotationValue = "volume.experimental.kubernetes.io/provisioning-completed"
|
||||
)
|
@ -1,295 +0,0 @@
|
||||
/*
|
||||
Copyright 2015 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 (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/errors"
|
||||
"k8s.io/kubernetes/pkg/api/resource"
|
||||
"k8s.io/kubernetes/pkg/api/testapi"
|
||||
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
|
||||
fake_cloud "k8s.io/kubernetes/pkg/cloudprovider/providers/fake"
|
||||
"k8s.io/kubernetes/pkg/util"
|
||||
volumetest "k8s.io/kubernetes/pkg/volume/testing"
|
||||
"k8s.io/kubernetes/pkg/watch"
|
||||
)
|
||||
|
||||
func TestProvisionerRunStop(t *testing.T) {
|
||||
controller, _, _ := makeTestController()
|
||||
|
||||
if len(controller.stopChannels) != 0 {
|
||||
t.Errorf("Non-running provisioner should not have any stopChannels. Got %v", len(controller.stopChannels))
|
||||
}
|
||||
|
||||
controller.Run()
|
||||
|
||||
if len(controller.stopChannels) != 2 {
|
||||
t.Errorf("Running provisioner should have exactly 2 stopChannels. Got %v", len(controller.stopChannels))
|
||||
}
|
||||
|
||||
controller.Stop()
|
||||
|
||||
if len(controller.stopChannels) != 0 {
|
||||
t.Errorf("Non-running provisioner should not have any stopChannels. Got %v", len(controller.stopChannels))
|
||||
}
|
||||
}
|
||||
|
||||
func makeTestVolume() *api.PersistentVolume {
|
||||
return &api.PersistentVolume{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Annotations: map[string]string{},
|
||||
Name: "pv01",
|
||||
},
|
||||
Spec: api.PersistentVolumeSpec{
|
||||
PersistentVolumeReclaimPolicy: api.PersistentVolumeReclaimDelete,
|
||||
AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce},
|
||||
Capacity: api.ResourceList{
|
||||
api.ResourceName(api.ResourceStorage): resource.MustParse("10Gi"),
|
||||
},
|
||||
PersistentVolumeSource: api.PersistentVolumeSource{
|
||||
HostPath: &api.HostPathVolumeSource{
|
||||
Path: "/somepath/data01",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func makeTestClaim() *api.PersistentVolumeClaim {
|
||||
return &api.PersistentVolumeClaim{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Annotations: map[string]string{},
|
||||
Name: "claim01",
|
||||
Namespace: "ns",
|
||||
SelfLink: testapi.Default.SelfLink("pvc", ""),
|
||||
},
|
||||
Spec: api.PersistentVolumeClaimSpec{
|
||||
AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce},
|
||||
Resources: api.ResourceRequirements{
|
||||
Requests: api.ResourceList{
|
||||
api.ResourceName(api.ResourceStorage): resource.MustParse("8G"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func makeTestController() (*PersistentVolumeProvisionerController, *mockControllerClient, *volumetest.FakeVolumePlugin) {
|
||||
mockClient := &mockControllerClient{}
|
||||
mockVolumePlugin := &volumetest.FakeVolumePlugin{}
|
||||
controller, _ := NewPersistentVolumeProvisionerController(mockClient, 1*time.Second, "fake-kubernetes", nil, mockVolumePlugin, &fake_cloud.FakeCloud{})
|
||||
return controller, mockClient, mockVolumePlugin
|
||||
}
|
||||
|
||||
func TestReconcileClaim(t *testing.T) {
|
||||
controller, mockClient, _ := makeTestController()
|
||||
pvc := makeTestClaim()
|
||||
|
||||
// watch would have added the claim to the store
|
||||
controller.claimStore.Add(pvc)
|
||||
// store it in fake API server
|
||||
mockClient.UpdatePersistentVolumeClaim(pvc)
|
||||
|
||||
err := controller.reconcileClaim(pvc)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// non-provisionable PVC should not have created a volume on reconciliation
|
||||
if mockClient.volume != nil {
|
||||
t.Error("Unexpected volume found in mock client. Expected nil")
|
||||
}
|
||||
|
||||
pvc.Annotations[qosProvisioningKey] = "foo"
|
||||
// store it in fake API server
|
||||
mockClient.UpdatePersistentVolumeClaim(pvc)
|
||||
|
||||
err = controller.reconcileClaim(pvc)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// PVC requesting provisioning should have a PV created for it
|
||||
if mockClient.volume == nil {
|
||||
t.Error("Expected to find bound volume but got nil")
|
||||
}
|
||||
|
||||
if mockClient.volume.Spec.ClaimRef.Name != pvc.Name {
|
||||
t.Errorf("Expected PV to be bound to %s but got %s", mockClient.volume.Spec.ClaimRef.Name, pvc.Name)
|
||||
}
|
||||
|
||||
// the PVC should have correct annotation
|
||||
if mockClient.claim.Annotations[pvProvisioningRequiredAnnotationKey] != pvProvisioningCompletedAnnotationValue {
|
||||
t.Errorf("Annotation %q not set", pvProvisioningRequiredAnnotationKey)
|
||||
}
|
||||
|
||||
// Run the syncClaim 2nd time to simulate periodic sweep running in parallel
|
||||
// to the previous syncClaim. There is a lock in handleUpdateVolume(), so
|
||||
// they will be called sequentially, but the second call will have old
|
||||
// version of the claim.
|
||||
oldPVName := mockClient.volume.Name
|
||||
|
||||
// Make the "old" claim
|
||||
pvc2 := makeTestClaim()
|
||||
pvc2.Annotations[qosProvisioningKey] = "foo"
|
||||
// Add a dummy annotation so we recognize the claim was updated (i.e.
|
||||
// stored in mockClient)
|
||||
pvc2.Annotations["test"] = "test"
|
||||
|
||||
err = controller.reconcileClaim(pvc2)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// The 2nd PVC should be ignored, no new PV was created
|
||||
if val, found := pvc2.Annotations[pvProvisioningRequiredAnnotationKey]; found {
|
||||
t.Errorf("2nd PVC got unexpected annotation %q: %q", pvProvisioningRequiredAnnotationKey, val)
|
||||
}
|
||||
if mockClient.volume.Name != oldPVName {
|
||||
t.Errorf("2nd PVC unexpectedly provisioned a new volume")
|
||||
}
|
||||
if _, found := mockClient.claim.Annotations["test"]; found {
|
||||
t.Errorf("2nd PVC was unexpectedly updated")
|
||||
}
|
||||
}
|
||||
|
||||
func checkTagValue(t *testing.T, tags map[string]string, tag string, expectedValue string) {
|
||||
value, found := tags[tag]
|
||||
if !found || value != expectedValue {
|
||||
t.Errorf("Expected tag value %s = %s but value %s found", tag, expectedValue, value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcileVolume(t *testing.T) {
|
||||
|
||||
controller, mockClient, mockVolumePlugin := makeTestController()
|
||||
pv := makeTestVolume()
|
||||
pvc := makeTestClaim()
|
||||
mockClient.volume = pv
|
||||
|
||||
err := controller.reconcileVolume(pv)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error %v", err)
|
||||
}
|
||||
|
||||
// watch adds claim to the store.
|
||||
// we need to add it to our mock client to mimic normal Get call
|
||||
controller.claimStore.Add(pvc)
|
||||
mockClient.claim = pvc
|
||||
|
||||
// pretend the claim and volume are bound, no provisioning required
|
||||
claimRef, _ := api.GetReference(pvc)
|
||||
pv.Spec.ClaimRef = claimRef
|
||||
mockClient.volume = pv
|
||||
err = controller.reconcileVolume(pv)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error %v", err)
|
||||
}
|
||||
|
||||
pv.Annotations[pvProvisioningRequiredAnnotationKey] = "!pvProvisioningCompleted"
|
||||
pv.Annotations[qosProvisioningKey] = "foo"
|
||||
mockClient.volume = pv
|
||||
err = controller.reconcileVolume(pv)
|
||||
|
||||
if !isAnnotationMatch(pvProvisioningRequiredAnnotationKey, pvProvisioningCompletedAnnotationValue, mockClient.volume.Annotations) {
|
||||
t.Errorf("Expected %s but got %s", pvProvisioningRequiredAnnotationKey, mockClient.volume.Annotations[pvProvisioningRequiredAnnotationKey])
|
||||
}
|
||||
|
||||
// Check that the volume plugin was called with correct tags
|
||||
tags := *mockVolumePlugin.LastProvisionerOptions.CloudTags
|
||||
checkTagValue(t, tags, cloudVolumeCreatedForClaimNamespaceTag, pvc.Namespace)
|
||||
checkTagValue(t, tags, cloudVolumeCreatedForClaimNameTag, pvc.Name)
|
||||
checkTagValue(t, tags, cloudVolumeCreatedForVolumeNameTag, pv.Name)
|
||||
|
||||
}
|
||||
|
||||
var _ controllerClient = &mockControllerClient{}
|
||||
|
||||
type mockControllerClient struct {
|
||||
volume *api.PersistentVolume
|
||||
claim *api.PersistentVolumeClaim
|
||||
}
|
||||
|
||||
func (c *mockControllerClient) GetPersistentVolume(name string) (*api.PersistentVolume, error) {
|
||||
return c.volume, nil
|
||||
}
|
||||
|
||||
func (c *mockControllerClient) CreatePersistentVolume(pv *api.PersistentVolume) (*api.PersistentVolume, error) {
|
||||
if pv.GenerateName != "" && pv.Name == "" {
|
||||
pv.Name = fmt.Sprintf(pv.GenerateName, util.NewUUID())
|
||||
}
|
||||
c.volume = pv
|
||||
return c.volume, nil
|
||||
}
|
||||
|
||||
func (c *mockControllerClient) ListPersistentVolumes(options api.ListOptions) (*api.PersistentVolumeList, error) {
|
||||
return &api.PersistentVolumeList{
|
||||
Items: []api.PersistentVolume{*c.volume},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *mockControllerClient) WatchPersistentVolumes(options api.ListOptions) (watch.Interface, error) {
|
||||
return watch.NewFake(), nil
|
||||
}
|
||||
|
||||
func (c *mockControllerClient) UpdatePersistentVolume(pv *api.PersistentVolume) (*api.PersistentVolume, error) {
|
||||
return c.CreatePersistentVolume(pv)
|
||||
}
|
||||
|
||||
func (c *mockControllerClient) DeletePersistentVolume(volume *api.PersistentVolume) error {
|
||||
c.volume = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockControllerClient) UpdatePersistentVolumeStatus(volume *api.PersistentVolume) (*api.PersistentVolume, error) {
|
||||
return volume, nil
|
||||
}
|
||||
|
||||
func (c *mockControllerClient) GetPersistentVolumeClaim(namespace, name string) (*api.PersistentVolumeClaim, error) {
|
||||
if c.claim != nil {
|
||||
return c.claim, nil
|
||||
} else {
|
||||
return nil, errors.NewNotFound(api.Resource("persistentvolumes"), name)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *mockControllerClient) ListPersistentVolumeClaims(namespace string, options api.ListOptions) (*api.PersistentVolumeClaimList, error) {
|
||||
return &api.PersistentVolumeClaimList{
|
||||
Items: []api.PersistentVolumeClaim{*c.claim},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *mockControllerClient) WatchPersistentVolumeClaims(namespace string, options api.ListOptions) (watch.Interface, error) {
|
||||
return watch.NewFake(), nil
|
||||
}
|
||||
|
||||
func (c *mockControllerClient) UpdatePersistentVolumeClaim(claim *api.PersistentVolumeClaim) (*api.PersistentVolumeClaim, error) {
|
||||
c.claim = claim
|
||||
return c.claim, nil
|
||||
}
|
||||
|
||||
func (c *mockControllerClient) UpdatePersistentVolumeClaimStatus(claim *api.PersistentVolumeClaim) (*api.PersistentVolumeClaim, error) {
|
||||
return claim, nil
|
||||
}
|
||||
|
||||
func (c *mockControllerClient) GetKubeClient() clientset.Interface {
|
||||
return nil
|
||||
}
|
@ -1,415 +0,0 @@
|
||||
/*
|
||||
Copyright 2014 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 (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/client/cache"
|
||||
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
|
||||
"k8s.io/kubernetes/pkg/cloudprovider"
|
||||
"k8s.io/kubernetes/pkg/controller/framework"
|
||||
"k8s.io/kubernetes/pkg/runtime"
|
||||
"k8s.io/kubernetes/pkg/types"
|
||||
ioutil "k8s.io/kubernetes/pkg/util/io"
|
||||
"k8s.io/kubernetes/pkg/util/metrics"
|
||||
"k8s.io/kubernetes/pkg/util/mount"
|
||||
"k8s.io/kubernetes/pkg/volume"
|
||||
"k8s.io/kubernetes/pkg/watch"
|
||||
)
|
||||
|
||||
var _ volume.VolumeHost = &PersistentVolumeRecycler{}
|
||||
|
||||
// PersistentVolumeRecycler is a controller that watches for PersistentVolumes that are released from their claims.
|
||||
// This controller will Recycle those volumes whose reclaim policy is set to PersistentVolumeReclaimRecycle and make them
|
||||
// available again for a new claim.
|
||||
type PersistentVolumeRecycler struct {
|
||||
volumeController *framework.Controller
|
||||
stopChannel chan struct{}
|
||||
client recyclerClient
|
||||
kubeClient clientset.Interface
|
||||
pluginMgr volume.VolumePluginMgr
|
||||
cloud cloudprovider.Interface
|
||||
maximumRetry int
|
||||
syncPeriod time.Duration
|
||||
// Local cache of failed recycle / delete operations. Map volume.Name -> status of the volume.
|
||||
// Only PVs in Released state have an entry here.
|
||||
releasedVolumes map[string]releasedVolumeStatus
|
||||
}
|
||||
|
||||
// releasedVolumeStatus holds state of failed delete/recycle operation on a
|
||||
// volume. The controller re-tries the operation several times and it stores
|
||||
// retry count + timestamp of the last attempt here.
|
||||
type releasedVolumeStatus struct {
|
||||
// How many recycle/delete operations failed.
|
||||
retryCount int
|
||||
// Timestamp of the last attempt.
|
||||
lastAttempt time.Time
|
||||
}
|
||||
|
||||
// NewPersistentVolumeRecycler creates a new PersistentVolumeRecycler
|
||||
func NewPersistentVolumeRecycler(kubeClient clientset.Interface, syncPeriod time.Duration, maximumRetry int, plugins []volume.VolumePlugin, cloud cloudprovider.Interface) (*PersistentVolumeRecycler, error) {
|
||||
recyclerClient := NewRecyclerClient(kubeClient)
|
||||
if kubeClient != nil && kubeClient.Core().GetRESTClient().GetRateLimiter() != nil {
|
||||
metrics.RegisterMetricAndTrackRateLimiterUsage("pv_recycler_controller", kubeClient.Core().GetRESTClient().GetRateLimiter())
|
||||
}
|
||||
recycler := &PersistentVolumeRecycler{
|
||||
client: recyclerClient,
|
||||
kubeClient: kubeClient,
|
||||
cloud: cloud,
|
||||
maximumRetry: maximumRetry,
|
||||
syncPeriod: syncPeriod,
|
||||
releasedVolumes: make(map[string]releasedVolumeStatus),
|
||||
}
|
||||
|
||||
if err := recycler.pluginMgr.InitPlugins(plugins, recycler); err != nil {
|
||||
return nil, fmt.Errorf("Could not initialize volume plugins for PVClaimBinder: %+v", err)
|
||||
}
|
||||
|
||||
_, volumeController := framework.NewInformer(
|
||||
&cache.ListWatch{
|
||||
ListFunc: func(options api.ListOptions) (runtime.Object, error) {
|
||||
return kubeClient.Core().PersistentVolumes().List(options)
|
||||
},
|
||||
WatchFunc: func(options api.ListOptions) (watch.Interface, error) {
|
||||
return kubeClient.Core().PersistentVolumes().Watch(options)
|
||||
},
|
||||
},
|
||||
&api.PersistentVolume{},
|
||||
syncPeriod,
|
||||
framework.ResourceEventHandlerFuncs{
|
||||
AddFunc: func(obj interface{}) {
|
||||
pv, ok := obj.(*api.PersistentVolume)
|
||||
if !ok {
|
||||
glog.Errorf("Error casting object to PersistentVolume: %v", obj)
|
||||
return
|
||||
}
|
||||
recycler.reclaimVolume(pv)
|
||||
},
|
||||
UpdateFunc: func(oldObj, newObj interface{}) {
|
||||
pv, ok := newObj.(*api.PersistentVolume)
|
||||
if !ok {
|
||||
glog.Errorf("Error casting object to PersistentVolume: %v", newObj)
|
||||
return
|
||||
}
|
||||
recycler.reclaimVolume(pv)
|
||||
},
|
||||
DeleteFunc: func(obj interface{}) {
|
||||
pv, ok := obj.(*api.PersistentVolume)
|
||||
if !ok {
|
||||
glog.Errorf("Error casting object to PersistentVolume: %v", obj)
|
||||
return
|
||||
}
|
||||
recycler.reclaimVolume(pv)
|
||||
recycler.removeReleasedVolume(pv)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
recycler.volumeController = volumeController
|
||||
return recycler, nil
|
||||
}
|
||||
|
||||
// shouldRecycle checks a volume and returns nil, if the volume should be
|
||||
// recycled right now. Otherwise it returns an error with reason why it should
|
||||
// not be recycled.
|
||||
func (recycler *PersistentVolumeRecycler) shouldRecycle(pv *api.PersistentVolume) error {
|
||||
if pv.Spec.ClaimRef == nil {
|
||||
return fmt.Errorf("Volume does not have a reference to claim")
|
||||
}
|
||||
if pv.Status.Phase != api.VolumeReleased {
|
||||
return fmt.Errorf("The volume is not in 'Released' phase")
|
||||
}
|
||||
|
||||
// The volume is Released, should we retry recycling?
|
||||
status, found := recycler.releasedVolumes[pv.Name]
|
||||
if !found {
|
||||
// We don't know anything about this volume. The controller has been
|
||||
// restarted or the volume has been marked as Released by another
|
||||
// controller. Recycle/delete this volume as if it was just Released.
|
||||
glog.V(5).Infof("PersistentVolume[%s] not found in local cache, recycling", pv.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check the timestamp
|
||||
expectedRetry := status.lastAttempt.Add(recycler.syncPeriod)
|
||||
if time.Now().After(expectedRetry) {
|
||||
glog.V(5).Infof("PersistentVolume[%s] retrying recycle after timeout", pv.Name)
|
||||
return nil
|
||||
}
|
||||
// It's too early
|
||||
glog.V(5).Infof("PersistentVolume[%s] skipping recycle, it's too early: now: %v, next retry: %v", pv.Name, time.Now(), expectedRetry)
|
||||
return fmt.Errorf("Too early after previous failure")
|
||||
}
|
||||
|
||||
func (recycler *PersistentVolumeRecycler) reclaimVolume(pv *api.PersistentVolume) error {
|
||||
glog.V(5).Infof("Recycler: checking PersistentVolume[%s]\n", pv.Name)
|
||||
// Always load the latest version of the volume
|
||||
newPV, err := recycler.client.GetPersistentVolume(pv.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not find PersistentVolume %s", pv.Name)
|
||||
}
|
||||
pv = newPV
|
||||
|
||||
err = recycler.shouldRecycle(pv)
|
||||
if err == nil {
|
||||
glog.V(5).Infof("Reclaiming PersistentVolume[%s]\n", pv.Name)
|
||||
|
||||
// both handleRecycle and handleDelete block until completion
|
||||
// TODO: allow parallel recycling operations to increase throughput
|
||||
switch pv.Spec.PersistentVolumeReclaimPolicy {
|
||||
case api.PersistentVolumeReclaimRecycle:
|
||||
err = recycler.handleRecycle(pv)
|
||||
case api.PersistentVolumeReclaimDelete:
|
||||
err = recycler.handleDelete(pv)
|
||||
case api.PersistentVolumeReclaimRetain:
|
||||
glog.V(5).Infof("Volume %s is set to retain after release. Skipping.\n", pv.Name)
|
||||
default:
|
||||
err = fmt.Errorf("No PersistentVolumeReclaimPolicy defined for spec: %+v", pv)
|
||||
}
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("Could not recycle volume spec: %+v", err)
|
||||
glog.Errorf(errMsg)
|
||||
return fmt.Errorf(errMsg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
glog.V(3).Infof("PersistentVolume[%s] phase %s - skipping: %v", pv.Name, pv.Status.Phase, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleReleaseFailure evaluates a failed Recycle/Delete operation, updates
|
||||
// internal controller state with new nr. of attempts and timestamp of the last
|
||||
// attempt. Based on the number of failures it returns the next state of the
|
||||
// volume (Released / Failed).
|
||||
func (recycler *PersistentVolumeRecycler) handleReleaseFailure(pv *api.PersistentVolume) api.PersistentVolumePhase {
|
||||
status, found := recycler.releasedVolumes[pv.Name]
|
||||
if !found {
|
||||
// First failure, set retryCount to 0 (will be inceremented few lines below)
|
||||
status = releasedVolumeStatus{}
|
||||
}
|
||||
status.retryCount += 1
|
||||
|
||||
if status.retryCount > recycler.maximumRetry {
|
||||
// This was the last attempt. Remove any internal state and mark the
|
||||
// volume as Failed.
|
||||
glog.V(3).Infof("PersistentVolume[%s] failed %d times - marking Failed", pv.Name, status.retryCount)
|
||||
recycler.removeReleasedVolume(pv)
|
||||
return api.VolumeFailed
|
||||
}
|
||||
|
||||
status.lastAttempt = time.Now()
|
||||
recycler.releasedVolumes[pv.Name] = status
|
||||
return api.VolumeReleased
|
||||
}
|
||||
|
||||
func (recycler *PersistentVolumeRecycler) removeReleasedVolume(pv *api.PersistentVolume) {
|
||||
delete(recycler.releasedVolumes, pv.Name)
|
||||
}
|
||||
|
||||
func (recycler *PersistentVolumeRecycler) handleRecycle(pv *api.PersistentVolume) error {
|
||||
glog.V(5).Infof("Recycling PersistentVolume[%s]\n", pv.Name)
|
||||
|
||||
currentPhase := pv.Status.Phase
|
||||
nextPhase := currentPhase
|
||||
|
||||
spec := volume.NewSpecFromPersistentVolume(pv, false)
|
||||
plugin, err := recycler.pluginMgr.FindRecyclablePluginBySpec(spec)
|
||||
if err != nil {
|
||||
nextPhase = api.VolumeFailed
|
||||
pv.Status.Message = fmt.Sprintf("%v", err)
|
||||
}
|
||||
|
||||
// an error above means a suitable plugin for this volume was not found.
|
||||
// we don't need to attempt recycling when plugin is nil, but we do need to persist the next/failed phase
|
||||
// of the volume so that subsequent syncs won't attempt recycling through this handler func.
|
||||
if plugin != nil {
|
||||
volRecycler, err := plugin.NewRecycler(spec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not obtain Recycler for spec: %#v error: %v", spec, err)
|
||||
}
|
||||
// blocks until completion
|
||||
if err := volRecycler.Recycle(); err != nil {
|
||||
glog.Errorf("PersistentVolume[%s] failed recycling: %+v", pv.Name, err)
|
||||
pv.Status.Message = fmt.Sprintf("Recycling error: %s", err)
|
||||
nextPhase = recycler.handleReleaseFailure(pv)
|
||||
} else {
|
||||
glog.V(5).Infof("PersistentVolume[%s] successfully recycled\n", pv.Name)
|
||||
// The volume has been recycled. Remove any internal state to make
|
||||
// any subsequent bind+recycle cycle working.
|
||||
recycler.removeReleasedVolume(pv)
|
||||
nextPhase = api.VolumePending
|
||||
}
|
||||
}
|
||||
|
||||
if currentPhase != nextPhase {
|
||||
glog.V(5).Infof("PersistentVolume[%s] changing phase from %s to %s\n", pv.Name, currentPhase, nextPhase)
|
||||
pv.Status.Phase = nextPhase
|
||||
_, err := recycler.client.UpdatePersistentVolumeStatus(pv)
|
||||
if err != nil {
|
||||
// Rollback to previous phase
|
||||
pv.Status.Phase = currentPhase
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (recycler *PersistentVolumeRecycler) handleDelete(pv *api.PersistentVolume) error {
|
||||
glog.V(5).Infof("Deleting PersistentVolume[%s]\n", pv.Name)
|
||||
|
||||
currentPhase := pv.Status.Phase
|
||||
nextPhase := currentPhase
|
||||
|
||||
spec := volume.NewSpecFromPersistentVolume(pv, false)
|
||||
plugin, err := recycler.pluginMgr.FindDeletablePluginBySpec(spec)
|
||||
if err != nil {
|
||||
nextPhase = api.VolumeFailed
|
||||
pv.Status.Message = fmt.Sprintf("%v", err)
|
||||
}
|
||||
|
||||
// an error above means a suitable plugin for this volume was not found.
|
||||
// we don't need to attempt deleting when plugin is nil, but we do need to persist the next/failed phase
|
||||
// of the volume so that subsequent syncs won't attempt deletion through this handler func.
|
||||
if plugin != nil {
|
||||
deleter, err := plugin.NewDeleter(spec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not obtain Deleter for spec: %#v error: %v", spec, err)
|
||||
}
|
||||
// blocks until completion
|
||||
err = deleter.Delete()
|
||||
if err != nil {
|
||||
glog.Errorf("PersistentVolume[%s] failed deletion: %+v", pv.Name, err)
|
||||
pv.Status.Message = fmt.Sprintf("Deletion error: %s", err)
|
||||
nextPhase = recycler.handleReleaseFailure(pv)
|
||||
} else {
|
||||
glog.V(5).Infof("PersistentVolume[%s] successfully deleted through plugin\n", pv.Name)
|
||||
recycler.removeReleasedVolume(pv)
|
||||
// after successful deletion through the plugin, we can also remove the PV from the cluster
|
||||
if err := recycler.client.DeletePersistentVolume(pv); err != nil {
|
||||
return fmt.Errorf("error deleting persistent volume: %+v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if currentPhase != nextPhase {
|
||||
glog.V(5).Infof("PersistentVolume[%s] changing phase from %s to %s\n", pv.Name, currentPhase, nextPhase)
|
||||
pv.Status.Phase = nextPhase
|
||||
_, err := recycler.client.UpdatePersistentVolumeStatus(pv)
|
||||
if err != nil {
|
||||
// Rollback to previous phase
|
||||
pv.Status.Phase = currentPhase
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run starts this recycler's control loops
|
||||
func (recycler *PersistentVolumeRecycler) Run() {
|
||||
glog.V(5).Infof("Starting PersistentVolumeRecycler\n")
|
||||
if recycler.stopChannel == nil {
|
||||
recycler.stopChannel = make(chan struct{})
|
||||
go recycler.volumeController.Run(recycler.stopChannel)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop gracefully shuts down this binder
|
||||
func (recycler *PersistentVolumeRecycler) Stop() {
|
||||
glog.V(5).Infof("Stopping PersistentVolumeRecycler\n")
|
||||
if recycler.stopChannel != nil {
|
||||
close(recycler.stopChannel)
|
||||
recycler.stopChannel = nil
|
||||
}
|
||||
}
|
||||
|
||||
// recyclerClient abstracts access to PVs
|
||||
type recyclerClient interface {
|
||||
GetPersistentVolume(name string) (*api.PersistentVolume, error)
|
||||
UpdatePersistentVolume(volume *api.PersistentVolume) (*api.PersistentVolume, error)
|
||||
DeletePersistentVolume(volume *api.PersistentVolume) error
|
||||
UpdatePersistentVolumeStatus(volume *api.PersistentVolume) (*api.PersistentVolume, error)
|
||||
}
|
||||
|
||||
func NewRecyclerClient(c clientset.Interface) recyclerClient {
|
||||
return &realRecyclerClient{c}
|
||||
}
|
||||
|
||||
type realRecyclerClient struct {
|
||||
client clientset.Interface
|
||||
}
|
||||
|
||||
func (c *realRecyclerClient) GetPersistentVolume(name string) (*api.PersistentVolume, error) {
|
||||
return c.client.Core().PersistentVolumes().Get(name)
|
||||
}
|
||||
|
||||
func (c *realRecyclerClient) UpdatePersistentVolume(volume *api.PersistentVolume) (*api.PersistentVolume, error) {
|
||||
return c.client.Core().PersistentVolumes().Update(volume)
|
||||
}
|
||||
|
||||
func (c *realRecyclerClient) DeletePersistentVolume(volume *api.PersistentVolume) error {
|
||||
return c.client.Core().PersistentVolumes().Delete(volume.Name, nil)
|
||||
}
|
||||
|
||||
func (c *realRecyclerClient) UpdatePersistentVolumeStatus(volume *api.PersistentVolume) (*api.PersistentVolume, error) {
|
||||
return c.client.Core().PersistentVolumes().UpdateStatus(volume)
|
||||
}
|
||||
|
||||
// PersistentVolumeRecycler is host to the volume plugins, but does not actually mount any volumes.
|
||||
// Because no mounting is performed, most of the VolumeHost methods are not implemented.
|
||||
func (f *PersistentVolumeRecycler) GetPluginDir(podUID string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (f *PersistentVolumeRecycler) GetPodVolumeDir(podUID types.UID, pluginName, volumeName string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (f *PersistentVolumeRecycler) GetPodPluginDir(podUID types.UID, pluginName string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (f *PersistentVolumeRecycler) GetKubeClient() clientset.Interface {
|
||||
return f.kubeClient
|
||||
}
|
||||
|
||||
func (f *PersistentVolumeRecycler) NewWrapperMounter(volName string, spec volume.Spec, pod *api.Pod, opts volume.VolumeOptions) (volume.Mounter, error) {
|
||||
return nil, fmt.Errorf("NewWrapperMounter not supported by PVClaimBinder's VolumeHost implementation")
|
||||
}
|
||||
|
||||
func (f *PersistentVolumeRecycler) NewWrapperUnmounter(volName string, spec volume.Spec, podUID types.UID) (volume.Unmounter, error) {
|
||||
return nil, fmt.Errorf("NewWrapperUnmounter not supported by PVClaimBinder's VolumeHost implementation")
|
||||
}
|
||||
|
||||
func (f *PersistentVolumeRecycler) GetCloudProvider() cloudprovider.Interface {
|
||||
return f.cloud
|
||||
}
|
||||
|
||||
func (f *PersistentVolumeRecycler) GetMounter() mount.Interface {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *PersistentVolumeRecycler) GetWriter() ioutil.Writer {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *PersistentVolumeRecycler) GetHostName() string {
|
||||
return ""
|
||||
}
|
@ -1,265 +0,0 @@
|
||||
/*
|
||||
Copyright 2015 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 (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/resource"
|
||||
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
|
||||
"k8s.io/kubernetes/pkg/volume"
|
||||
"k8s.io/kubernetes/pkg/volume/host_path"
|
||||
volumetest "k8s.io/kubernetes/pkg/volume/testing"
|
||||
)
|
||||
|
||||
const (
|
||||
mySyncPeriod = 2 * time.Second
|
||||
myMaximumRetry = 3
|
||||
)
|
||||
|
||||
func TestFailedRecycling(t *testing.T) {
|
||||
pv := preparePV()
|
||||
|
||||
mockClient := &mockBinderClient{
|
||||
volume: pv,
|
||||
}
|
||||
|
||||
// no Init called for pluginMgr and no plugins are available. Volume should fail recycling.
|
||||
plugMgr := volume.VolumePluginMgr{}
|
||||
|
||||
recycler := &PersistentVolumeRecycler{
|
||||
kubeClient: fake.NewSimpleClientset(),
|
||||
client: mockClient,
|
||||
pluginMgr: plugMgr,
|
||||
releasedVolumes: make(map[string]releasedVolumeStatus),
|
||||
}
|
||||
|
||||
err := recycler.reclaimVolume(pv)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected non-nil error: %v", err)
|
||||
}
|
||||
|
||||
if mockClient.volume.Status.Phase != api.VolumeFailed {
|
||||
t.Errorf("Expected %s but got %s", api.VolumeFailed, mockClient.volume.Status.Phase)
|
||||
}
|
||||
|
||||
// Use a new volume for the next test
|
||||
pv = preparePV()
|
||||
mockClient.volume = pv
|
||||
|
||||
pv.Spec.PersistentVolumeReclaimPolicy = api.PersistentVolumeReclaimDelete
|
||||
err = recycler.reclaimVolume(pv)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected non-nil error: %v", err)
|
||||
}
|
||||
|
||||
if mockClient.volume.Status.Phase != api.VolumeFailed {
|
||||
t.Errorf("Expected %s but got %s", api.VolumeFailed, mockClient.volume.Status.Phase)
|
||||
}
|
||||
}
|
||||
|
||||
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{}), volumetest.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 TestRecyclingRetryAlwaysFail(t *testing.T) {
|
||||
// Test that recycler controller retries to recycle a volume several times, which always fails.
|
||||
pv := preparePV()
|
||||
|
||||
mockClient := &mockBinderClient{
|
||||
volume: pv,
|
||||
}
|
||||
|
||||
plugMgr := volume.VolumePluginMgr{}
|
||||
// Use a fake NewRecycler function
|
||||
plugMgr.InitPlugins(host_path.ProbeRecyclableVolumePlugins(newAlwaysFailingMockRecycler, volume.VolumeConfig{}), volumetest.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),
|
||||
}
|
||||
|
||||
// myMaximumRetry recycle attempts will fail
|
||||
testRecycleFailures(t, recycler, mockClient, pv, myMaximumRetry)
|
||||
|
||||
// The volume should be failed after myMaximumRetry attempts
|
||||
err := recycler.reclaimVolume(pv)
|
||||
if err != nil {
|
||||
t.Errorf("Last step: Recycler failed: %v", err)
|
||||
}
|
||||
|
||||
if mockClient.volume.Status.Phase != api.VolumeFailed {
|
||||
t.Errorf("Last step: The volume should be Failed, 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 preparePV() *api.PersistentVolume {
|
||||
return &api.PersistentVolume{
|
||||
Spec: api.PersistentVolumeSpec{
|
||||
AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce},
|
||||
Capacity: api.ResourceList{
|
||||
api.ResourceName(api.ResourceStorage): resource.MustParse("8Gi"),
|
||||
},
|
||||
PersistentVolumeSource: api.PersistentVolumeSource{
|
||||
HostPath: &api.HostPathVolumeSource{
|
||||
Path: "/tmp/data02",
|
||||
},
|
||||
},
|
||||
PersistentVolumeReclaimPolicy: api.PersistentVolumeReclaimRecycle,
|
||||
ClaimRef: &api.ObjectReference{
|
||||
Name: "foo",
|
||||
Namespace: "bar",
|
||||
},
|
||||
},
|
||||
Status: api.PersistentVolumeStatus{
|
||||
Phase: api.VolumeReleased,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Test that `count` attempts to recycle a PV fails.
|
||||
func testRecycleFailures(t *testing.T, recycler *PersistentVolumeRecycler, mockClient *mockBinderClient, pv *api.PersistentVolume, count int) {
|
||||
for i := 1; i <= count; i++ {
|
||||
err := recycler.reclaimVolume(pv)
|
||||
if err != nil {
|
||||
t.Errorf("STEP %d: Recycler faled: %v", i, err)
|
||||
}
|
||||
|
||||
// Check the status, it should be failed
|
||||
if mockClient.volume.Status.Phase != api.VolumeReleased {
|
||||
t.Errorf("STEP %d: The volume should be Released, but is %s instead", i, mockClient.volume.Status.Phase)
|
||||
}
|
||||
|
||||
// Check the failed volume cache
|
||||
status, found := recycler.releasedVolumes[pv.Name]
|
||||
if !found {
|
||||
t.Errorf("STEP %d: cannot find released volume status", i)
|
||||
}
|
||||
if status.retryCount != i {
|
||||
t.Errorf("STEP %d: Expected nr. of attempts to be %d, got %d", i, i, status.retryCount)
|
||||
}
|
||||
|
||||
// call reclaimVolume too early, it should not increment the retryCount
|
||||
time.Sleep(mySyncPeriod / 2)
|
||||
err = recycler.reclaimVolume(pv)
|
||||
if err != nil {
|
||||
t.Errorf("STEP %d: Recycler failed: %v", i, err)
|
||||
}
|
||||
|
||||
status, found = recycler.releasedVolumes[pv.Name]
|
||||
if !found {
|
||||
t.Errorf("STEP %d: cannot find released volume status", i)
|
||||
}
|
||||
if status.retryCount != i {
|
||||
t.Errorf("STEP %d: Expected nr. of attempts to be %d, got %d", i, i, status.retryCount)
|
||||
}
|
||||
|
||||
// Call the next reclaimVolume() after full pvRecycleRetryPeriod
|
||||
time.Sleep(mySyncPeriod / 2)
|
||||
}
|
||||
}
|
||||
|
||||
func newFailingMockRecycler(spec *volume.Spec, host volume.VolumeHost, config volume.VolumeConfig) (volume.Recycler, error) {
|
||||
return &failingMockRecycler{
|
||||
path: spec.PersistentVolume.Spec.HostPath.Path,
|
||||
errorCount: myMaximumRetry - 1, // fail two times and then successfully recycle the volume
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newAlwaysFailingMockRecycler(spec *volume.Spec, host volume.VolumeHost, config volume.VolumeConfig) (volume.Recycler, error) {
|
||||
return &failingMockRecycler{
|
||||
path: spec.PersistentVolume.Spec.HostPath.Path,
|
||||
errorCount: 1000, // always fail
|
||||
}, nil
|
||||
}
|
||||
|
||||
type failingMockRecycler struct {
|
||||
path string
|
||||
// How many times should the recycler fail before returning success.
|
||||
errorCount int
|
||||
volume.MetricsNil
|
||||
}
|
||||
|
||||
// Counter of failingMockRecycler.Recycle() calls. Global variable just for
|
||||
// testing. It's too much code to create a custom volume plugin, which would
|
||||
// hold this variable.
|
||||
var failedCallCount = 0
|
||||
|
||||
func (r *failingMockRecycler) GetPath() string {
|
||||
return r.path
|
||||
}
|
||||
|
||||
func (r *failingMockRecycler) Recycle() error {
|
||||
failedCallCount += 1
|
||||
if failedCallCount <= r.errorCount {
|
||||
return fmt.Errorf("Failing for %d. time", failedCallCount)
|
||||
}
|
||||
// return nil means recycle passed
|
||||
return nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user