diff --git a/pkg/controller/volume/persistentvolume/controller.go b/pkg/controller/volume/persistentvolume/controller.go index f08e79fb0a4..5cf4a0b0b13 100644 --- a/pkg/controller/volume/persistentvolume/controller.go +++ b/pkg/controller/volume/persistentvolume/controller.go @@ -1076,11 +1076,20 @@ func (ctrl *PersistentVolumeController) deleteVolumeOperation(arg interface{}) { if err != nil { // Delete failed, update the volume and emit an event. glog.V(3).Infof("deletion of volume %q failed: %v", volume.Name, err) - if _, err = ctrl.updateVolumePhaseWithEvent(volume, api.VolumeFailed, api.EventTypeWarning, "VolumeFailedDelete", err.Error()); err != nil { - glog.V(4).Infof("deleteVolumeOperation [%s]: failed to mark volume as failed: %v", volume.Name, err) - // Save failed, retry on the next deletion attempt - return + if vol.IsDeletedVolumeInUse(err) { + // The plugin needs more time, don't mark the volume as Failed + // and send Normal event only + ctrl.eventRecorder.Event(volume, api.EventTypeNormal, "VolumeDelete", err.Error()) + } else { + // The plugin failed, mark the volume as Failed and send Warning + // event + if _, err = ctrl.updateVolumePhaseWithEvent(volume, api.VolumeFailed, api.EventTypeWarning, "VolumeFailedDelete", err.Error()); err != nil { + glog.V(4).Infof("deleteVolumeOperation [%s]: failed to mark volume as failed: %v", volume.Name, err) + // Save failed, retry on the next deletion attempt + return + } } + // Despite the volume being Failed, the controller will retry deleting // the volume in every syncVolume() call. return @@ -1174,7 +1183,7 @@ func (ctrl *PersistentVolumeController) doDeleteVolume(volume *api.PersistentVol if err = deleter.Delete(); err != nil { // Deleter failed - return false, fmt.Errorf("Delete of volume %q failed: %v", volume.Name, err) + return false, err } glog.V(2).Infof("volume %q deleted", volume.Name) diff --git a/pkg/volume/volume.go b/pkg/volume/volume.go index fe8fbe1380e..25ee38879ee 100644 --- a/pkg/volume/volume.go +++ b/pkg/volume/volume.go @@ -129,6 +129,12 @@ type Provisioner interface { type Deleter interface { Volume // This method should block until completion. + // deletedVolumeInUseError returned from this function will not be reported + // as error and it will be sent as "Info" event to the PV being deleted. The + // volume controller will retry deleting the volume in the next periodic + // sync. This can be used to postpone deletion of a volume that is being + // dettached from a node. Deletion of such volume would fail anyway and such + // error would confuse users. Delete() error } @@ -171,6 +177,31 @@ type Detacher interface { UnmountDevice(deviceMountPath string) error } +// NewDeletedVolumeInUseError returns a new instance of DeletedVolumeInUseError +// error. +func NewDeletedVolumeInUseError(message string) error { + return deletedVolumeInUseError(message) +} + +type deletedVolumeInUseError string + +var _ error = deletedVolumeInUseError("") + +// IsDeletedVolumeInUse returns true if an error returned from Delete() is +// deletedVolumeInUseError +func IsDeletedVolumeInUse(err error) bool { + switch err.(type) { + case deletedVolumeInUseError: + return true + default: + return false + } +} + +func (err deletedVolumeInUseError) Error() string { + return string(err) +} + func RenameDirectory(oldPath, newName string) (string, error) { newPath, err := ioutil.TempDir(path.Dir(oldPath), newName) if err != nil {