mirror of
https://github.com/hanzoai/volume.git
synced 2026-11-03 04:24:40 +00:00
A fork of Rancher's local-path-provisioner at v0.0.36. The behaviour is unchanged and deliberately so — a directory on the node a pod lands on is the right thing for a disk. What changes is identity. The provisioner answers to hanzo.ai/volume rather than rancher.io/local-path. That is not branding: two provisioners sharing one name race for the same claims, and a stock k3s carries the upstream one, so a cluster running both would settle our storage by chance. Data lives at /var/lib/hanzo/volume, so reading a Hanzo cluster does not mean learning another vendor's directory. Apache-2.0 as published, unaltered, with Rancher's attribution in NOTICE. The README says what this is not: durability. A volume here dies with its node. That is hanzoai/replicate's question, one layer up, and keeping the two apart is what lets the disk stay this simple.
1722 lines
62 KiB
Go
1722 lines
62 KiB
Go
/*
|
|
Copyright 2016 The Kubernetes Authors.
|
|
|
|
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 controller
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
"golang.org/x/time/rate"
|
|
v1 "k8s.io/api/core/v1"
|
|
storage "k8s.io/api/storage/v1"
|
|
storagebeta "k8s.io/api/storage/v1beta1"
|
|
apierrs "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
|
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
|
"k8s.io/apimachinery/pkg/util/uuid"
|
|
"k8s.io/apimachinery/pkg/util/wait"
|
|
"k8s.io/client-go/informers"
|
|
"k8s.io/client-go/kubernetes"
|
|
"k8s.io/client-go/kubernetes/scheme"
|
|
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
|
corelistersv1 "k8s.io/client-go/listers/core/v1"
|
|
"k8s.io/client-go/tools/cache"
|
|
"k8s.io/client-go/tools/leaderelection"
|
|
"k8s.io/client-go/tools/leaderelection/resourcelock"
|
|
"k8s.io/client-go/tools/record"
|
|
ref "k8s.io/client-go/tools/reference"
|
|
"k8s.io/client-go/util/workqueue"
|
|
klog "k8s.io/klog/v2"
|
|
"sigs.k8s.io/sig-storage-lib-external-provisioner/v11/controller/metrics"
|
|
"sigs.k8s.io/sig-storage-lib-external-provisioner/v11/util"
|
|
)
|
|
|
|
// This annotation is added to a PV that has been dynamically provisioned by
|
|
// Kubernetes. Its value is name of volume plugin that created the volume.
|
|
// It serves both user (to show where a PV comes from) and Kubernetes (to
|
|
// recognize dynamically provisioned PVs in its decisions).
|
|
const annDynamicallyProvisioned = "pv.kubernetes.io/provisioned-by"
|
|
|
|
// AnnMigratedTo annotation is added to a PVC that is supposed to be
|
|
// dynamically provisioned/deleted by by its corresponding CSI driver
|
|
// through the CSIMigration feature flags. It allows external provisioners
|
|
// to determine which PVs are considered migrated and safe to operate on for
|
|
// Deletion.
|
|
const annMigratedTo = "pv.kubernetes.io/migrated-to"
|
|
|
|
const annBetaStorageProvisioner = "volume.beta.kubernetes.io/storage-provisioner"
|
|
const annStorageProvisioner = "volume.kubernetes.io/storage-provisioner"
|
|
|
|
// This annotation is added to a PVC that has been triggered by scheduler to
|
|
// be dynamically provisioned. Its value is the name of the selected node.
|
|
const annSelectedNode = "volume.kubernetes.io/selected-node"
|
|
|
|
// This annotation is present on K8s 1.11 release.
|
|
const annAlphaSelectedNode = "volume.alpha.kubernetes.io/selected-node"
|
|
|
|
// Finalizer for PVs so we know to clean them up
|
|
const finalizerPV = "external-provisioner.volume.kubernetes.io/finalizer"
|
|
|
|
const uidIndex = "uid"
|
|
|
|
// ControllerSubsystem is prometheus subsystem name.
|
|
const controllerSubsystem = "controller"
|
|
|
|
var (
|
|
errStopProvision = errors.New("stop provisioning")
|
|
)
|
|
|
|
type VolumeNameHook func(claim *v1.PersistentVolumeClaim) string
|
|
|
|
// ProvisionController is a controller that provisions PersistentVolumes for
|
|
// PersistentVolumeClaims.
|
|
type ProvisionController struct {
|
|
client kubernetes.Interface
|
|
|
|
// The name of the provisioner for which this controller dynamically
|
|
// provisions volumes. The value of annDynamicallyProvisioned and
|
|
// annStorageProvisioner to set & watch for, respectively
|
|
provisionerName string
|
|
|
|
// additional provisioner names (beyond provisionerName) that the
|
|
// provisioner should watch for and handle in annStorageProvisioner
|
|
additionalProvisionerNames []string
|
|
|
|
// The provisioner the controller will use to provision and delete volumes.
|
|
// Presumably this implementer of Provisioner carries its own
|
|
// volume-specific options and such that it needs in order to provision
|
|
// volumes.
|
|
provisioner Provisioner
|
|
|
|
claimInformer cache.SharedIndexInformer
|
|
claimsIndexer cache.Indexer
|
|
volumeInformer cache.SharedInformer
|
|
volumes cache.Store
|
|
classInformer cache.SharedInformer
|
|
nodeLister corelistersv1.NodeLister
|
|
classes cache.Store
|
|
|
|
// To determine if the informer is internal or external
|
|
customClaimInformer, customVolumeInformer, customClassInformer bool
|
|
|
|
claimQueue workqueue.RateLimitingInterface
|
|
volumeQueue workqueue.RateLimitingInterface
|
|
|
|
// Identity of this controller, generated at creation time and not persisted
|
|
// across restarts. Useful only for debugging, for seeing the source of
|
|
// events. controller.provisioner may have its own, different notion of
|
|
// identity which may/may not persist across restarts
|
|
id string
|
|
component string
|
|
eventRecorder record.EventRecorder
|
|
|
|
resyncPeriod time.Duration
|
|
provisionTimeout time.Duration
|
|
deletionTimeout time.Duration
|
|
|
|
rateLimiter workqueue.RateLimiter
|
|
exponentialBackOffOnError bool
|
|
threadiness int
|
|
|
|
createProvisionedPVBackoff *wait.Backoff
|
|
createProvisionedPVRetryCount int
|
|
createProvisionedPVInterval time.Duration
|
|
createProvisionerPVLimiter workqueue.RateLimiter
|
|
|
|
failedProvisionThreshold, failedDeleteThreshold int
|
|
|
|
// The metrics collection used by this controller.
|
|
metrics metrics.Metrics
|
|
// The port for metrics server to serve on.
|
|
metricsPort int32
|
|
// The IP address for metrics server to serve on.
|
|
metricsAddress string
|
|
// The path of metrics endpoint path.
|
|
metricsPath string
|
|
|
|
// Whether to add a finalizer marking the provisioner as the owner of the PV
|
|
// with clean up duty.
|
|
// TODO: upstream and we may have a race b/w applying reclaim policy and not if pv has protection finalizer
|
|
addFinalizer bool
|
|
|
|
// Whether to do kubernetes leader election at all. It should basically
|
|
// always be done when possible to avoid duplicate Provision attempts.
|
|
leaderElection bool
|
|
leaderElectionNamespace string
|
|
// Parameters of leaderelection.LeaderElectionConfig.
|
|
leaseDuration, renewDeadline, retryPeriod time.Duration
|
|
|
|
hasRun bool
|
|
hasRunLock *sync.Mutex
|
|
|
|
// Map UID -> *PVC with all claims that may be provisioned in the background.
|
|
claimsInProgress sync.Map
|
|
|
|
volumeStore VolumeStore
|
|
|
|
volumeNameHook VolumeNameHook
|
|
}
|
|
|
|
const (
|
|
// DefaultResyncPeriod is used when option function ResyncPeriod is omitted
|
|
DefaultResyncPeriod = 15 * time.Minute
|
|
// DefaultThreadiness is used when option function Threadiness is omitted
|
|
DefaultThreadiness = 4
|
|
// DefaultExponentialBackOffOnError is used when option function ExponentialBackOffOnError is omitted
|
|
DefaultExponentialBackOffOnError = true
|
|
// DefaultCreateProvisionedPVRetryCount is used when option function CreateProvisionedPVRetryCount is omitted
|
|
DefaultCreateProvisionedPVRetryCount = 5
|
|
// DefaultCreateProvisionedPVInterval is used when option function CreateProvisionedPVInterval is omitted
|
|
DefaultCreateProvisionedPVInterval = 10 * time.Second
|
|
// DefaultFailedProvisionThreshold is used when option function FailedProvisionThreshold is omitted
|
|
DefaultFailedProvisionThreshold = 15
|
|
// DefaultFailedDeleteThreshold is used when option function FailedDeleteThreshold is omitted
|
|
DefaultFailedDeleteThreshold = 15
|
|
// DefaultLeaderElection is used when option function LeaderElection is omitted
|
|
DefaultLeaderElection = true
|
|
// DefaultLeaseDuration is used when option function LeaseDuration is omitted
|
|
DefaultLeaseDuration = 15 * time.Second
|
|
// DefaultRenewDeadline is used when option function RenewDeadline is omitted
|
|
DefaultRenewDeadline = 10 * time.Second
|
|
// DefaultRetryPeriod is used when option function RetryPeriod is omitted
|
|
DefaultRetryPeriod = 2 * time.Second
|
|
// DefaultMetricsPort is used when option function MetricsPort is omitted
|
|
DefaultMetricsPort = 0
|
|
// DefaultMetricsAddress is used when option function MetricsAddress is omitted
|
|
DefaultMetricsAddress = "0.0.0.0"
|
|
// DefaultMetricsPath is used when option function MetricsPath is omitted
|
|
DefaultMetricsPath = "/metrics"
|
|
// DefaultAddFinalizer is used when option function AddFinalizer is omitted
|
|
DefaultAddFinalizer = false
|
|
)
|
|
|
|
var errRuntime = fmt.Errorf("cannot call option functions after controller has Run")
|
|
|
|
// ResyncPeriod is how often the controller relists PVCs, PVs, & storage
|
|
// classes. OnUpdate will be called even if nothing has changed, meaning failed
|
|
// operations may be retried on a PVC/PV every resyncPeriod regardless of
|
|
// whether it changed. Defaults to 15 minutes.
|
|
func ResyncPeriod(resyncPeriod time.Duration) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
c.resyncPeriod = resyncPeriod
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Threadiness is the number of claim and volume workers each to launch.
|
|
// Defaults to 4.
|
|
func Threadiness(threadiness int) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
c.threadiness = threadiness
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// RateLimiter is the workqueue.RateLimiter to use for the provisioning and
|
|
// deleting work queues. If set, ExponentialBackOffOnError is ignored.
|
|
func RateLimiter(rateLimiter workqueue.RateLimiter) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
c.rateLimiter = rateLimiter
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// ExponentialBackOffOnError determines whether to exponentially back off from
|
|
// failures of Provision and Delete. Defaults to true.
|
|
func ExponentialBackOffOnError(exponentialBackOffOnError bool) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
c.exponentialBackOffOnError = exponentialBackOffOnError
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// CreateProvisionedPVRetryCount is the number of retries when we create a PV
|
|
// object for a provisioned volume. Defaults to 5.
|
|
// If PV is not saved after given number of retries, corresponding storage asset (volume) is deleted!
|
|
// Only one of CreateProvisionedPVInterval+CreateProvisionedPVRetryCount or CreateProvisionedPVBackoff or
|
|
// CreateProvisionedPVLimiter can be used.
|
|
// Deprecated: Use CreateProvisionedPVLimiter instead, it tries indefinitely.
|
|
func CreateProvisionedPVRetryCount(createProvisionedPVRetryCount int) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
if c.createProvisionedPVBackoff != nil {
|
|
return fmt.Errorf("CreateProvisionedPVBackoff cannot be used together with CreateProvisionedPVRetryCount")
|
|
}
|
|
if c.createProvisionerPVLimiter != nil {
|
|
return fmt.Errorf("CreateProvisionedPVBackoff cannot be used together with CreateProvisionedPVLimiter")
|
|
}
|
|
c.createProvisionedPVRetryCount = createProvisionedPVRetryCount
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// CreateProvisionedPVInterval is the interval between retries when we create a
|
|
// PV object for a provisioned volume. Defaults to 10 seconds.
|
|
// If PV is not saved after given number of retries, corresponding storage asset (volume) is deleted!
|
|
// Only one of CreateProvisionedPVInterval+CreateProvisionedPVRetryCount or CreateProvisionedPVBackoff or
|
|
// CreateProvisionedPVLimiter can be used.
|
|
// Deprecated: Use CreateProvisionedPVLimiter instead, it tries indefinitely.
|
|
func CreateProvisionedPVInterval(createProvisionedPVInterval time.Duration) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
if c.createProvisionedPVBackoff != nil {
|
|
return fmt.Errorf("CreateProvisionedPVBackoff cannot be used together with CreateProvisionedPVInterval")
|
|
}
|
|
if c.createProvisionerPVLimiter != nil {
|
|
return fmt.Errorf("CreateProvisionedPVInterval cannot be used together with CreateProvisionedPVLimiter")
|
|
}
|
|
c.createProvisionedPVInterval = createProvisionedPVInterval
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// CreateProvisionedPVBackoff is the configuration of exponential backoff between retries when we create a
|
|
// PV object for a provisioned volume. Defaults to linear backoff, 10 seconds 5 times.
|
|
// If PV is not saved after given number of retries, corresponding storage asset (volume) is deleted!
|
|
// Only one of CreateProvisionedPVInterval+CreateProvisionedPVRetryCount or CreateProvisionedPVBackoff or
|
|
// CreateProvisionedPVLimiter can be used.
|
|
// Deprecated: Use CreateProvisionedPVLimiter instead, it tries indefinitely.
|
|
func CreateProvisionedPVBackoff(backoff wait.Backoff) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
if c.createProvisionedPVRetryCount != 0 {
|
|
return fmt.Errorf("CreateProvisionedPVBackoff cannot be used together with CreateProvisionedPVRetryCount")
|
|
}
|
|
if c.createProvisionedPVInterval != 0 {
|
|
return fmt.Errorf("CreateProvisionedPVBackoff cannot be used together with CreateProvisionedPVInterval")
|
|
}
|
|
if c.createProvisionerPVLimiter != nil {
|
|
return fmt.Errorf("CreateProvisionedPVBackoff cannot be used together with CreateProvisionedPVLimiter")
|
|
}
|
|
c.createProvisionedPVBackoff = &backoff
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// CreateProvisionedPVLimiter is the configuration of rate limiter for queue of unsaved PersistentVolumes.
|
|
// If set, PVs that fail to be saved to Kubernetes API server will be re-enqueued to a separate workqueue
|
|
// with this limiter and re-tried until they are saved to API server. There is no limit of retries.
|
|
// The main difference to other CreateProvisionedPV* option is that the storage asset is never deleted
|
|
// and the controller continues saving PV to API server indefinitely.
|
|
// This option cannot be used with CreateProvisionedPVBackoff or CreateProvisionedPVInterval
|
|
// or CreateProvisionedPVRetryCount.
|
|
func CreateProvisionedPVLimiter(limiter workqueue.RateLimiter) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
if c.createProvisionedPVRetryCount != 0 {
|
|
return fmt.Errorf("CreateProvisionedPVLimiter cannot be used together with CreateProvisionedPVRetryCount")
|
|
}
|
|
if c.createProvisionedPVInterval != 0 {
|
|
return fmt.Errorf("CreateProvisionedPVLimiter cannot be used together with CreateProvisionedPVInterval")
|
|
}
|
|
if c.createProvisionedPVBackoff != nil {
|
|
return fmt.Errorf("CreateProvisionedPVLimiter cannot be used together with CreateProvisionedPVBackoff")
|
|
}
|
|
c.createProvisionerPVLimiter = limiter
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// FailedProvisionThreshold is the threshold for max number of retries on
|
|
// failures of Provision. Set to 0 to retry indefinitely. Defaults to 15.
|
|
func FailedProvisionThreshold(failedProvisionThreshold int) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
c.failedProvisionThreshold = failedProvisionThreshold
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// FailedDeleteThreshold is the threshold for max number of retries on failures
|
|
// of Delete. Set to 0 to retry indefinitely. Defaults to 15.
|
|
func FailedDeleteThreshold(failedDeleteThreshold int) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
c.failedDeleteThreshold = failedDeleteThreshold
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// LeaderElection determines whether to enable leader election or not. Defaults
|
|
// to true.
|
|
func LeaderElection(leaderElection bool) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
c.leaderElection = leaderElection
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// LeaderElectionNamespace is the kubernetes namespace in which to create the
|
|
// leader election object. Defaults to the same namespace in which the
|
|
// the controller runs.
|
|
func LeaderElectionNamespace(leaderElectionNamespace string) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
c.leaderElectionNamespace = leaderElectionNamespace
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// LeaseDuration is the duration that non-leader candidates will
|
|
// wait to force acquire leadership. This is measured against time of
|
|
// last observed ack. Defaults to 15 seconds.
|
|
func LeaseDuration(leaseDuration time.Duration) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
c.leaseDuration = leaseDuration
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// RenewDeadline is the duration that the acting master will retry
|
|
// refreshing leadership before giving up. Defaults to 10 seconds.
|
|
func RenewDeadline(renewDeadline time.Duration) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
c.renewDeadline = renewDeadline
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// RetryPeriod is the duration the LeaderElector clients should wait
|
|
// between tries of actions. Defaults to 2 seconds.
|
|
func RetryPeriod(retryPeriod time.Duration) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
c.retryPeriod = retryPeriod
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// ClaimsInformer sets the informer to use for accessing PersistentVolumeClaims.
|
|
// Defaults to using a internal informer.
|
|
func ClaimsInformer(informer cache.SharedIndexInformer) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
c.claimInformer = informer
|
|
c.customClaimInformer = true
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// VolumesInformer sets the informer to use for accessing PersistentVolumes.
|
|
// Defaults to using a internal informer.
|
|
func VolumesInformer(informer cache.SharedInformer) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
c.volumeInformer = informer
|
|
c.customVolumeInformer = true
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// ClassesInformer sets the informer to use for accessing StorageClasses.
|
|
// The informer must use the versioned resource appropriate for the Kubernetes cluster version
|
|
// (that is, v1.StorageClass for >= 1.6, and v1beta1.StorageClass for < 1.6).
|
|
// Defaults to using a internal informer.
|
|
func ClassesInformer(informer cache.SharedInformer) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
c.classInformer = informer
|
|
c.customClassInformer = true
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// NodesLister sets the informer to use for accessing Nodes.
|
|
// This is needed only for PVCs which have a selected node.
|
|
// Defaults to using a GET instead of an informer.
|
|
//
|
|
// Which approach is better depends on factors like cluster size and
|
|
// ratio of PVCs with a selected node.
|
|
func NodesLister(nodeLister corelistersv1.NodeLister) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
c.nodeLister = nodeLister
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// MetricsInstance defines which metrics collection to update. Default: metrics.Metrics.
|
|
func MetricsInstance(m metrics.Metrics) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
c.metrics = m
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// MetricsPort sets the port that metrics server serves on. Default: 0, set to non-zero to enable.
|
|
func MetricsPort(metricsPort int32) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
c.metricsPort = metricsPort
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// MetricsAddress sets the ip address that metrics serve serves on.
|
|
func MetricsAddress(metricsAddress string) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
c.metricsAddress = metricsAddress
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// MetricsPath sets the endpoint path of metrics server.
|
|
func MetricsPath(metricsPath string) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
c.metricsPath = metricsPath
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// AdditionalProvisionerNames sets additional names for the provisioner
|
|
func AdditionalProvisionerNames(additionalProvisionerNames []string) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
c.additionalProvisionerNames = additionalProvisionerNames
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// AddFinalizer determines whether to add a finalizer marking the provisioner
|
|
// as the owner of the PV with clean up duty. A PV having the finalizer means
|
|
// the provisioner wants to keep it around so that it can reclaim it.
|
|
func AddFinalizer(addFinalizer bool) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
c.addFinalizer = addFinalizer
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// ProvisionTimeout sets the amount of time that provisioning a volume may take.
|
|
// The default is unlimited.
|
|
func ProvisionTimeout(timeout time.Duration) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
c.provisionTimeout = timeout
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// DeletionTimeout sets the amount of time that deleting a volume may take.
|
|
// The default is unlimited.
|
|
func DeletionTimeout(timeout time.Duration) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
c.deletionTimeout = timeout
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func VolumeName(hook VolumeNameHook) func(*ProvisionController) error {
|
|
return func(c *ProvisionController) error {
|
|
if c.HasRun() {
|
|
return errRuntime
|
|
}
|
|
c.volumeNameHook = hook
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// HasRun returns whether the controller has Run
|
|
func (ctrl *ProvisionController) HasRun() bool {
|
|
ctrl.hasRunLock.Lock()
|
|
defer ctrl.hasRunLock.Unlock()
|
|
return ctrl.hasRun
|
|
}
|
|
|
|
// NewProvisionController creates a new provision controller using
|
|
// the given configuration parameters and with private (non-shared) informers.
|
|
func NewProvisionController(
|
|
ctx context.Context,
|
|
client kubernetes.Interface,
|
|
provisionerName string,
|
|
provisioner Provisioner,
|
|
options ...func(*ProvisionController) error,
|
|
) *ProvisionController {
|
|
logger := klog.FromContext(ctx)
|
|
id, err := os.Hostname()
|
|
if err != nil {
|
|
logger.Error(err, "Error getting hostname")
|
|
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
|
|
}
|
|
// add a uniquifier so that two processes on the same host don't accidentally both become active
|
|
id = id + "_" + string(uuid.NewUUID())
|
|
component := provisionerName + "_" + id
|
|
|
|
v1.AddToScheme(scheme.Scheme)
|
|
broadcaster := record.NewBroadcaster(record.WithContext(ctx))
|
|
broadcaster.StartStructuredLogging(0)
|
|
broadcaster.StartRecordingToSink(&corev1.EventSinkImpl{Interface: client.CoreV1().Events(v1.NamespaceAll)})
|
|
eventRecorder := broadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: component}).WithLogger(logger)
|
|
|
|
controller := &ProvisionController{
|
|
client: client,
|
|
provisionerName: provisionerName,
|
|
provisioner: provisioner,
|
|
id: id,
|
|
component: component,
|
|
eventRecorder: eventRecorder,
|
|
resyncPeriod: DefaultResyncPeriod,
|
|
exponentialBackOffOnError: DefaultExponentialBackOffOnError,
|
|
threadiness: DefaultThreadiness,
|
|
failedProvisionThreshold: DefaultFailedProvisionThreshold,
|
|
failedDeleteThreshold: DefaultFailedDeleteThreshold,
|
|
leaderElection: DefaultLeaderElection,
|
|
leaderElectionNamespace: getInClusterNamespace(),
|
|
leaseDuration: DefaultLeaseDuration,
|
|
renewDeadline: DefaultRenewDeadline,
|
|
retryPeriod: DefaultRetryPeriod,
|
|
metrics: metrics.New(controllerSubsystem),
|
|
metricsPort: DefaultMetricsPort,
|
|
metricsAddress: DefaultMetricsAddress,
|
|
metricsPath: DefaultMetricsPath,
|
|
addFinalizer: DefaultAddFinalizer,
|
|
hasRun: false,
|
|
hasRunLock: &sync.Mutex{},
|
|
volumeNameHook: getProvisionedVolumeNameForClaim,
|
|
}
|
|
|
|
for _, option := range options {
|
|
err := option(controller)
|
|
if err != nil {
|
|
logger.Error(err, "Error processing controller options")
|
|
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
|
|
}
|
|
}
|
|
|
|
var rateLimiter workqueue.RateLimiter
|
|
if controller.rateLimiter != nil {
|
|
// rateLimiter set via parameter takes precedence
|
|
rateLimiter = controller.rateLimiter
|
|
} else if controller.exponentialBackOffOnError {
|
|
rateLimiter = workqueue.NewMaxOfRateLimiter(
|
|
workqueue.NewItemExponentialFailureRateLimiter(15*time.Second, 1000*time.Second),
|
|
&workqueue.BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(10), 100)},
|
|
)
|
|
} else {
|
|
rateLimiter = workqueue.NewMaxOfRateLimiter(
|
|
workqueue.NewItemExponentialFailureRateLimiter(15*time.Second, 15*time.Second),
|
|
&workqueue.BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(10), 100)},
|
|
)
|
|
}
|
|
controller.claimQueue = workqueue.NewNamedRateLimitingQueue(rateLimiter, "claims")
|
|
controller.volumeQueue = workqueue.NewNamedRateLimitingQueue(rateLimiter, "volumes")
|
|
|
|
informer := informers.NewSharedInformerFactory(client, controller.resyncPeriod)
|
|
|
|
// ----------------------
|
|
// PersistentVolumeClaims
|
|
|
|
claimHandler := cache.ResourceEventHandlerFuncs{
|
|
AddFunc: func(obj interface{}) { controller.enqueueClaim(obj) },
|
|
UpdateFunc: func(oldObj, newObj interface{}) { controller.enqueueClaim(newObj) },
|
|
DeleteFunc: func(obj interface{}) {
|
|
// NOOP. The claim is either in claimsInProgress and in the queue, so it will be processed as usual
|
|
// or it's not in claimsInProgress and then we don't care
|
|
},
|
|
}
|
|
|
|
if controller.claimInformer != nil {
|
|
controller.claimInformer.AddEventHandlerWithResyncPeriod(claimHandler, controller.resyncPeriod)
|
|
} else {
|
|
controller.claimInformer = informer.Core().V1().PersistentVolumeClaims().Informer()
|
|
controller.claimInformer.AddEventHandler(claimHandler)
|
|
}
|
|
err = controller.claimInformer.AddIndexers(cache.Indexers{uidIndex: func(obj interface{}) ([]string, error) {
|
|
uid, err := getObjectUID(obj)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []string{uid}, nil
|
|
}})
|
|
if err != nil {
|
|
logger.Error(err, "Error setting indexer for pvc informer", "indexer", uidIndex)
|
|
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
|
|
}
|
|
controller.claimsIndexer = controller.claimInformer.GetIndexer()
|
|
|
|
// -----------------
|
|
// PersistentVolumes
|
|
|
|
volumeHandler := cache.ResourceEventHandlerFuncs{
|
|
AddFunc: func(obj interface{}) { controller.enqueueVolume(obj) },
|
|
UpdateFunc: func(oldObj, newObj interface{}) { controller.enqueueVolume(newObj) },
|
|
DeleteFunc: func(obj interface{}) { controller.forgetVolume(obj) },
|
|
}
|
|
|
|
if controller.volumeInformer != nil {
|
|
controller.volumeInformer.AddEventHandlerWithResyncPeriod(volumeHandler, controller.resyncPeriod)
|
|
} else {
|
|
controller.volumeInformer = informer.Core().V1().PersistentVolumes().Informer()
|
|
controller.volumeInformer.AddEventHandler(volumeHandler)
|
|
}
|
|
controller.volumes = controller.volumeInformer.GetStore()
|
|
|
|
// --------------
|
|
// StorageClasses
|
|
|
|
// no resource event handler needed for StorageClasses
|
|
if controller.classInformer == nil {
|
|
controller.classInformer = informer.Storage().V1().StorageClasses().Informer()
|
|
}
|
|
controller.classes = controller.classInformer.GetStore()
|
|
|
|
if controller.createProvisionerPVLimiter != nil {
|
|
logger.V(2).Info("Using saving PVs to API server in background")
|
|
controller.volumeStore = NewVolumeStoreQueue(client, controller.createProvisionerPVLimiter, controller.claimsIndexer, controller.eventRecorder)
|
|
} else {
|
|
if controller.createProvisionedPVBackoff == nil {
|
|
// Use linear backoff with createProvisionedPVInterval and createProvisionedPVRetryCount by default.
|
|
if controller.createProvisionedPVInterval == 0 {
|
|
controller.createProvisionedPVInterval = DefaultCreateProvisionedPVInterval
|
|
}
|
|
if controller.createProvisionedPVRetryCount == 0 {
|
|
controller.createProvisionedPVRetryCount = DefaultCreateProvisionedPVRetryCount
|
|
}
|
|
controller.createProvisionedPVBackoff = &wait.Backoff{
|
|
Duration: controller.createProvisionedPVInterval,
|
|
Factor: 1, // linear backoff
|
|
Steps: controller.createProvisionedPVRetryCount,
|
|
// Cap: controller.createProvisionedPVInterval,
|
|
}
|
|
}
|
|
logger.V(2).Info("Using blocking saving PVs to API server")
|
|
controller.volumeStore = NewBackoffStore(client, controller.eventRecorder, controller.createProvisionedPVBackoff, controller)
|
|
}
|
|
|
|
return controller
|
|
}
|
|
|
|
func getObjectUID(obj interface{}) (string, error) {
|
|
var object metav1.Object
|
|
var ok bool
|
|
if object, ok = obj.(metav1.Object); !ok {
|
|
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
|
|
if !ok {
|
|
return "", fmt.Errorf("error decoding object, invalid type")
|
|
}
|
|
object, ok = tombstone.Obj.(metav1.Object)
|
|
if !ok {
|
|
return "", fmt.Errorf("error decoding object tombstone, invalid type")
|
|
}
|
|
}
|
|
return string(object.GetUID()), nil
|
|
}
|
|
|
|
// enqueueClaim takes an obj and converts it into UID that is then put onto claim work queue.
|
|
func (ctrl *ProvisionController) enqueueClaim(obj interface{}) {
|
|
uid, err := getObjectUID(obj)
|
|
if err != nil {
|
|
utilruntime.HandleError(err)
|
|
return
|
|
}
|
|
ctrl.claimQueue.Add(uid)
|
|
}
|
|
|
|
// enqueueVolume takes an obj and converts it into a namespace/name string which
|
|
// is then put onto the given work queue.
|
|
func (ctrl *ProvisionController) enqueueVolume(obj interface{}) {
|
|
var key string
|
|
var err error
|
|
if key, err = cache.DeletionHandlingMetaNamespaceKeyFunc(obj); err != nil {
|
|
utilruntime.HandleError(err)
|
|
return
|
|
}
|
|
ctrl.volumeQueue.Add(key)
|
|
}
|
|
|
|
// forgetVolume Forgets an obj from the given work queue, telling the queue to
|
|
// stop tracking its retries because e.g. the obj was deleted
|
|
func (ctrl *ProvisionController) forgetVolume(obj interface{}) {
|
|
var key string
|
|
var err error
|
|
if key, err = cache.DeletionHandlingMetaNamespaceKeyFunc(obj); err != nil {
|
|
utilruntime.HandleError(err)
|
|
return
|
|
}
|
|
ctrl.volumeQueue.Forget(key)
|
|
ctrl.volumeQueue.Done(key)
|
|
}
|
|
|
|
// Run starts all of this controller's control loops
|
|
func (ctrl *ProvisionController) Run(ctx context.Context) {
|
|
run := func(ctx context.Context) {
|
|
logger := klog.FromContext(ctx)
|
|
logger.Info("Starting provisioner controller", "component", ctrl.component)
|
|
defer utilruntime.HandleCrash()
|
|
defer ctrl.claimQueue.ShutDown()
|
|
defer ctrl.volumeQueue.ShutDown()
|
|
|
|
ctrl.hasRunLock.Lock()
|
|
ctrl.hasRun = true
|
|
ctrl.hasRunLock.Unlock()
|
|
if ctrl.metricsPort > 0 {
|
|
prometheus.MustRegister([]prometheus.Collector{
|
|
ctrl.metrics.PersistentVolumeClaimProvisionTotal,
|
|
ctrl.metrics.PersistentVolumeClaimProvisionFailedTotal,
|
|
ctrl.metrics.PersistentVolumeClaimProvisionDurationSeconds,
|
|
ctrl.metrics.PersistentVolumeDeleteTotal,
|
|
ctrl.metrics.PersistentVolumeDeleteFailedTotal,
|
|
ctrl.metrics.PersistentVolumeDeleteDurationSeconds,
|
|
}...)
|
|
http.Handle(ctrl.metricsPath, promhttp.Handler())
|
|
address := net.JoinHostPort(ctrl.metricsAddress, strconv.FormatInt(int64(ctrl.metricsPort), 10))
|
|
logger.Info("Starting metrics server", "address", address)
|
|
go wait.Forever(func() {
|
|
err := http.ListenAndServe(address, nil)
|
|
if err != nil {
|
|
logger.Error(err, "Failed to listen metrics server", "address", address)
|
|
}
|
|
}, 5*time.Second)
|
|
}
|
|
|
|
// If a external SharedInformer has been passed in, this controller
|
|
// should not call Run again
|
|
if !ctrl.customClaimInformer {
|
|
go ctrl.claimInformer.Run(ctx.Done())
|
|
}
|
|
if !ctrl.customVolumeInformer {
|
|
go ctrl.volumeInformer.Run(ctx.Done())
|
|
}
|
|
if !ctrl.customClassInformer {
|
|
go ctrl.classInformer.Run(ctx.Done())
|
|
}
|
|
|
|
if !cache.WaitForCacheSync(ctx.Done(), ctrl.claimInformer.HasSynced, ctrl.volumeInformer.HasSynced, ctrl.classInformer.HasSynced) {
|
|
return
|
|
}
|
|
|
|
for i := 0; i < ctrl.threadiness; i++ {
|
|
go wait.Until(func() { ctrl.runClaimWorker(ctx) }, time.Second, ctx.Done())
|
|
go wait.Until(func() { ctrl.runVolumeWorker(ctx) }, time.Second, ctx.Done())
|
|
}
|
|
|
|
logger.Info("Started provisioner controller", "component", ctrl.component)
|
|
|
|
<-ctx.Done()
|
|
}
|
|
|
|
go ctrl.volumeStore.Run(ctx, DefaultThreadiness)
|
|
|
|
logger := klog.FromContext(ctx)
|
|
if ctrl.leaderElection {
|
|
rl, err := resourcelock.New(resourcelock.LeasesResourceLock,
|
|
ctrl.leaderElectionNamespace,
|
|
strings.Replace(ctrl.provisionerName, "/", "-", -1),
|
|
ctrl.client.CoreV1(),
|
|
ctrl.client.CoordinationV1(),
|
|
resourcelock.ResourceLockConfig{
|
|
Identity: ctrl.id,
|
|
EventRecorder: ctrl.eventRecorder,
|
|
})
|
|
if err != nil {
|
|
logger.Error(err, "Error creating lock")
|
|
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
|
|
}
|
|
|
|
leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{
|
|
Lock: rl,
|
|
LeaseDuration: ctrl.leaseDuration,
|
|
RenewDeadline: ctrl.renewDeadline,
|
|
RetryPeriod: ctrl.retryPeriod,
|
|
Callbacks: leaderelection.LeaderCallbacks{
|
|
OnStartedLeading: run,
|
|
OnStoppedLeading: func() {
|
|
logger.Error(nil, "Leaderelection lost")
|
|
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
|
|
},
|
|
},
|
|
})
|
|
panic("unreachable")
|
|
} else {
|
|
run(ctx)
|
|
}
|
|
}
|
|
|
|
func (ctrl *ProvisionController) runClaimWorker(ctx context.Context) {
|
|
for ctrl.processNextClaimWorkItem(ctx) {
|
|
}
|
|
}
|
|
|
|
func (ctrl *ProvisionController) runVolumeWorker(ctx context.Context) {
|
|
for ctrl.processNextVolumeWorkItem(ctx) {
|
|
}
|
|
}
|
|
|
|
// processNextClaimWorkItem processes items from claimQueue
|
|
func (ctrl *ProvisionController) processNextClaimWorkItem(ctx context.Context) bool {
|
|
obj, shutdown := ctrl.claimQueue.Get()
|
|
|
|
if shutdown {
|
|
return false
|
|
}
|
|
|
|
logger := klog.FromContext(ctx)
|
|
err := func() error {
|
|
// Apply per-operation timeout.
|
|
if ctrl.provisionTimeout != 0 {
|
|
timeout, cancel := context.WithTimeout(ctx, ctrl.provisionTimeout)
|
|
defer cancel()
|
|
ctx = timeout
|
|
}
|
|
defer ctrl.claimQueue.Done(obj)
|
|
var key string
|
|
var ok bool
|
|
if key, ok = obj.(string); !ok {
|
|
ctrl.claimQueue.Forget(obj)
|
|
return fmt.Errorf("expected string in workqueue but got %#v", obj)
|
|
}
|
|
|
|
if err := ctrl.syncClaimHandler(ctx, key); err != nil {
|
|
if ctrl.failedProvisionThreshold == 0 {
|
|
logger.Info("Retrying syncing claim", "key", key, "failures", ctrl.claimQueue.NumRequeues(obj))
|
|
ctrl.claimQueue.AddRateLimited(obj)
|
|
} else if ctrl.claimQueue.NumRequeues(obj) < ctrl.failedProvisionThreshold {
|
|
logger.Info("Retrying syncing claim because failures < threshold", "key", key, "failures", ctrl.claimQueue.NumRequeues(obj), "threshold", ctrl.failedProvisionThreshold)
|
|
ctrl.claimQueue.AddRateLimited(obj)
|
|
} else {
|
|
logger.Error(nil, "Giving up syncing claim because failures >= threshold", "key", key, "failures", ctrl.claimQueue.NumRequeues(obj), "threshold", ctrl.failedProvisionThreshold)
|
|
logger.V(2).Info("Removing PVC from claims in progress", "key", key)
|
|
ctrl.claimsInProgress.Delete(key) // This can leak a volume that's being provisioned in the background!
|
|
// Done but do not Forget: it will not be in the queue but NumRequeues
|
|
// will be saved until the obj is deleted from kubernetes
|
|
}
|
|
return fmt.Errorf("error syncing claim %q: %s", key, err.Error())
|
|
}
|
|
|
|
ctrl.claimQueue.Forget(obj)
|
|
// Silently remove the PVC from list of volumes in progress. The provisioning either succeeded
|
|
// or the PVC was ignored by this provisioner.
|
|
ctrl.claimsInProgress.Delete(key)
|
|
return nil
|
|
}()
|
|
|
|
if err != nil {
|
|
utilruntime.HandleError(err)
|
|
return true
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// processNextVolumeWorkItem processes items from volumeQueue
|
|
func (ctrl *ProvisionController) processNextVolumeWorkItem(ctx context.Context) bool {
|
|
obj, shutdown := ctrl.volumeQueue.Get()
|
|
|
|
if shutdown {
|
|
return false
|
|
}
|
|
|
|
logger := klog.FromContext(ctx)
|
|
err := func() error {
|
|
// Apply per-operation timeout.
|
|
if ctrl.deletionTimeout != 0 {
|
|
timeout, cancel := context.WithTimeout(ctx, ctrl.deletionTimeout)
|
|
defer cancel()
|
|
ctx = timeout
|
|
}
|
|
defer ctrl.volumeQueue.Done(obj)
|
|
var key string
|
|
var ok bool
|
|
if key, ok = obj.(string); !ok {
|
|
ctrl.volumeQueue.Forget(obj)
|
|
return fmt.Errorf("expected string in workqueue but got %#v", obj)
|
|
}
|
|
|
|
if err := ctrl.syncVolumeHandler(ctx, key); err != nil {
|
|
if ctrl.failedDeleteThreshold == 0 {
|
|
logger.Info("Retrying syncing volume", "key", key, "failures", ctrl.volumeQueue.NumRequeues(obj))
|
|
ctrl.volumeQueue.AddRateLimited(obj)
|
|
} else if ctrl.volumeQueue.NumRequeues(obj) < ctrl.failedDeleteThreshold {
|
|
logger.Info("Retrying syncing volume because failures < threshold", "key", key, "failures", ctrl.volumeQueue.NumRequeues(obj), "threshold", ctrl.failedDeleteThreshold)
|
|
ctrl.volumeQueue.AddRateLimited(obj)
|
|
} else {
|
|
logger.Info("Giving up syncing volume because failures >= threshold", "key", key, "failures", ctrl.volumeQueue.NumRequeues(obj), "threshold", ctrl.failedDeleteThreshold)
|
|
// Done but do not Forget: it will not be in the queue but NumRequeues
|
|
// will be saved until the obj is deleted from kubernetes
|
|
}
|
|
return fmt.Errorf("error syncing volume %q: %s", key, err.Error())
|
|
}
|
|
|
|
ctrl.volumeQueue.Forget(obj)
|
|
return nil
|
|
}()
|
|
|
|
if err != nil {
|
|
utilruntime.HandleError(err)
|
|
return true
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// syncClaimHandler gets the claim from informer's cache then calls syncClaim. A non-nil error triggers requeuing of the claim.
|
|
func (ctrl *ProvisionController) syncClaimHandler(ctx context.Context, key string) error {
|
|
objs, err := ctrl.claimsIndexer.ByIndex(uidIndex, key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var claimObj interface{}
|
|
if len(objs) > 0 {
|
|
claimObj = objs[0]
|
|
} else {
|
|
obj, found := ctrl.claimsInProgress.Load(key)
|
|
if !found {
|
|
utilruntime.HandleError(fmt.Errorf("claim %q in work queue no longer exists", key))
|
|
return nil
|
|
}
|
|
claimObj = obj
|
|
}
|
|
return ctrl.syncClaim(ctx, claimObj)
|
|
}
|
|
|
|
// syncVolumeHandler gets the volume from informer's cache then calls syncVolume
|
|
func (ctrl *ProvisionController) syncVolumeHandler(ctx context.Context, key string) error {
|
|
volumeObj, exists, err := ctrl.volumes.GetByKey(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !exists {
|
|
// Already deleted, nothing to do anymore.
|
|
return nil
|
|
}
|
|
|
|
return ctrl.syncVolume(ctx, volumeObj)
|
|
}
|
|
|
|
// syncClaim checks if the claim should have a volume provisioned for it and
|
|
// provisions one if so. Returns an error if the claim is to be requeued.
|
|
func (ctrl *ProvisionController) syncClaim(ctx context.Context, obj interface{}) error {
|
|
claim, ok := obj.(*v1.PersistentVolumeClaim)
|
|
if !ok {
|
|
return fmt.Errorf("expected claim but got %+v", obj)
|
|
}
|
|
|
|
should, err := ctrl.shouldProvision(ctx, claim)
|
|
if err != nil {
|
|
ctrl.updateProvisionStats(claim, err, time.Time{})
|
|
return err
|
|
} else if should {
|
|
startTime := time.Now()
|
|
logger := klog.FromContext(ctx)
|
|
|
|
status, err := ctrl.provisionClaimOperation(ctx, claim)
|
|
ctrl.updateProvisionStats(claim, err, startTime)
|
|
if err == nil || status == ProvisioningFinished {
|
|
// Provisioning is 100% finished / not in progress.
|
|
switch err {
|
|
case nil:
|
|
logger.V(5).Info("Claim processing succeeded, removing PVC from claims in progress", "claimUID", claim.UID)
|
|
case errStopProvision:
|
|
logger.V(5).Info("Stop provisioning, removing PVC from claims in progress", "claimUID", claim.UID)
|
|
// Our caller would requeue if we pass on this special error; return nil instead.
|
|
err = nil
|
|
default:
|
|
logger.V(2).Info("Final error received, removing PVC from claims in progress", "claimUID", claim.UID)
|
|
}
|
|
ctrl.claimsInProgress.Delete(string(claim.UID))
|
|
return err
|
|
}
|
|
if status == ProvisioningInBackground {
|
|
// Provisioning is in progress in background.
|
|
logger.V(2).Info("Temporary error received, adding PVC to claims in progress", "claimUID", claim.UID)
|
|
ctrl.claimsInProgress.Store(string(claim.UID), claim)
|
|
} else {
|
|
// status == ProvisioningNoChange.
|
|
// Don't change claimsInProgress:
|
|
// - the claim is already there if previous status was ProvisioningInBackground.
|
|
// - the claim is not there if if previous status was ProvisioningFinished.
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// syncVolume checks if the volume should be deleted and deletes if so
|
|
func (ctrl *ProvisionController) syncVolume(ctx context.Context, obj interface{}) error {
|
|
volume, ok := obj.(*v1.PersistentVolume)
|
|
if !ok {
|
|
return fmt.Errorf("expected volume but got %+v", obj)
|
|
}
|
|
|
|
if !ctrl.isProvisionerForVolume(ctx, volume) {
|
|
// Current provisioner is not responsible for the volume
|
|
return nil
|
|
}
|
|
|
|
volume, err := ctrl.handleProtectionFinalizer(ctx, volume)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if ctrl.shouldDelete(ctx, volume) {
|
|
klog.FromContext(ctx).V(5).Info("shouldDelete", "PV", volume.Name)
|
|
startTime := time.Now()
|
|
err = ctrl.deleteVolumeOperation(ctx, volume)
|
|
ctrl.updateDeleteStats(volume, err, startTime)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ctrl *ProvisionController) isProvisionerForVolume(ctx context.Context, volume *v1.PersistentVolume) bool {
|
|
if metav1.HasAnnotation(volume.ObjectMeta, annDynamicallyProvisioned) {
|
|
provisionPluginName := volume.Annotations[annDynamicallyProvisioned]
|
|
migratedAnn := volume.Annotations[annMigratedTo]
|
|
// Determine if the PV is owned by the current provisioner.
|
|
if !ctrl.knownProvisioner(provisionPluginName) && !ctrl.knownProvisioner(migratedAnn) {
|
|
// The current provisioner is not responsible for adding the finalizer
|
|
return false
|
|
}
|
|
} else {
|
|
// Statically provisioned volume. Check if the volume type is CSI.
|
|
if volume.Spec.PersistentVolumeSource.CSI != nil {
|
|
volumeProvisioner := volume.Spec.CSI.Driver
|
|
if !ctrl.knownProvisioner(volumeProvisioner) {
|
|
return false
|
|
}
|
|
} else {
|
|
// Check if the volume is being migrated
|
|
migratedAnn := volume.Annotations[annMigratedTo]
|
|
if !ctrl.knownProvisioner(migratedAnn) {
|
|
// The current provisioner is not responsible for adding the finalizer
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (ctrl *ProvisionController) handleProtectionFinalizer(ctx context.Context, volume *v1.PersistentVolume) (*v1.PersistentVolume, error) {
|
|
var modified bool
|
|
klog.FromContext(ctx).V(4).Info("handleProtectionFinalizer", "PV", volume)
|
|
reclaimPolicy := volume.Spec.PersistentVolumeReclaimPolicy
|
|
volumeFinalizers := volume.ObjectMeta.Finalizers
|
|
|
|
// Add the finalizer only if `addFinalizer` config option is enabled, finalizer doesn't exist and PV is not already
|
|
// under deletion.
|
|
if ctrl.addFinalizer && reclaimPolicy == v1.PersistentVolumeReclaimDelete && volume.DeletionTimestamp == nil && volume.Status.Phase == v1.VolumeBound {
|
|
volumeFinalizers, modified = addFinalizer(volumeFinalizers, finalizerPV)
|
|
}
|
|
|
|
// Check if the `addFinalizer` config option is disabled, i.e, rollback scenario, or the reclaim policy is changed
|
|
// to `Retain` or `Recycle`
|
|
if !ctrl.addFinalizer || reclaimPolicy == v1.PersistentVolumeReclaimRetain || reclaimPolicy == v1.PersistentVolumeReclaimRecycle {
|
|
volumeFinalizers, modified = removeFinalizer(volumeFinalizers, finalizerPV)
|
|
}
|
|
|
|
if modified {
|
|
newVolume, err := ctrl.patchPersistentVolumeWithFinalizers(ctx, volume, volumeFinalizers)
|
|
if err != nil {
|
|
return volume, fmt.Errorf("failed to modify finalizers to %+v on volume %s err: %+v", volumeFinalizers, volume.Name, err)
|
|
}
|
|
volume = newVolume
|
|
}
|
|
return volume, nil
|
|
}
|
|
|
|
// knownProvisioner checks if provisioner name has been
|
|
// configured to provision volumes for
|
|
func (ctrl *ProvisionController) knownProvisioner(provisioner string) bool {
|
|
if provisioner == ctrl.provisionerName {
|
|
return true
|
|
}
|
|
for _, p := range ctrl.additionalProvisionerNames {
|
|
if p == provisioner {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// shouldProvision returns whether a claim should have a volume provisioned for
|
|
// it, i.e. whether a Provision is "desired"
|
|
func (ctrl *ProvisionController) shouldProvision(ctx context.Context, claim *v1.PersistentVolumeClaim) (bool, error) {
|
|
if claim.Spec.VolumeName != "" {
|
|
return false, nil
|
|
}
|
|
|
|
if qualifier, ok := ctrl.provisioner.(Qualifier); ok {
|
|
if !qualifier.ShouldProvision(ctx, claim) {
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
provisioner, found := claim.Annotations[annStorageProvisioner]
|
|
if !found {
|
|
provisioner, found = claim.Annotations[annBetaStorageProvisioner]
|
|
}
|
|
|
|
if found {
|
|
if ctrl.knownProvisioner(provisioner) {
|
|
claimClass := util.GetPersistentVolumeClaimClass(claim)
|
|
class, err := ctrl.getStorageClass(claimClass)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if class.VolumeBindingMode != nil && *class.VolumeBindingMode == storage.VolumeBindingWaitForFirstConsumer {
|
|
// When claim is in delay binding mode, annSelectedNode is
|
|
// required to provision volume.
|
|
// Though PV controller set annStorageProvisioner only when
|
|
// annSelectedNode is set, but provisioner may remove
|
|
// annSelectedNode to notify scheduler to reschedule again.
|
|
if selectedNode, ok := claim.Annotations[annSelectedNode]; ok && selectedNode != "" {
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
}
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// shouldDelete returns whether a volume should have its backing volume
|
|
// deleted, i.e. whether a Delete is "desired"
|
|
func (ctrl *ProvisionController) shouldDelete(ctx context.Context, volume *v1.PersistentVolume) bool {
|
|
logger := klog.FromContext(ctx)
|
|
logger.V(5).Info("shouldDelete", "PV", volume.Name)
|
|
if deletionGuard, ok := ctrl.provisioner.(DeletionGuard); ok {
|
|
if !deletionGuard.ShouldDelete(ctx, volume) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if ctrl.addFinalizer {
|
|
if !ctrl.checkFinalizer(volume, finalizerPV) && volume.ObjectMeta.DeletionTimestamp != nil {
|
|
// The finalizer was removed, i.e. the volume has been already deleted.
|
|
logger.V(5).Info("shouldDelete is false: finalizer already removed from volume", "PV", volume.Name)
|
|
return false
|
|
}
|
|
} else {
|
|
if volume.ObjectMeta.DeletionTimestamp != nil {
|
|
logger.V(5).Info("shouldDelete is false: DeletionTimestamp != nil", "PV", volume.Name)
|
|
return false
|
|
}
|
|
}
|
|
|
|
if volume.Status.Phase != v1.VolumeReleased {
|
|
logger.V(5).Info("shouldDelete is false: PersistentVolumePhase is not Released", "PV", volume.Name)
|
|
return false
|
|
}
|
|
|
|
if volume.Spec.PersistentVolumeReclaimPolicy != v1.PersistentVolumeReclaimDelete {
|
|
logger.V(5).Info("shouldDelete is false: volume does not have Delete reclaim policy", "PV", volume.Name)
|
|
return false
|
|
}
|
|
|
|
logger.V(5).Info("shouldDelete is true", "PV", volume.Name)
|
|
return true
|
|
}
|
|
|
|
// canProvision returns error if provisioner can't provision claim.
|
|
func (ctrl *ProvisionController) canProvision(ctx context.Context, claim *v1.PersistentVolumeClaim) error {
|
|
// Check if this provisioner supports Block volume
|
|
if util.CheckPersistentVolumeClaimModeBlock(claim) && !ctrl.supportsBlock(ctx) {
|
|
return fmt.Errorf("%s does not support block volume provisioning", ctrl.provisionerName)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ctrl *ProvisionController) checkFinalizer(volume *v1.PersistentVolume, finalizer string) bool {
|
|
for _, f := range volume.ObjectMeta.Finalizers {
|
|
if f == finalizer {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (ctrl *ProvisionController) updateProvisionStats(claim *v1.PersistentVolumeClaim, err error, startTime time.Time) {
|
|
class := ""
|
|
source := ""
|
|
if claim.Spec.StorageClassName != nil {
|
|
class = *claim.Spec.StorageClassName
|
|
}
|
|
if claim.Spec.DataSource != nil {
|
|
source = claim.Spec.DataSource.Kind
|
|
}
|
|
if err != nil {
|
|
ctrl.metrics.PersistentVolumeClaimProvisionFailedTotal.WithLabelValues(class, source).Inc()
|
|
} else {
|
|
ctrl.metrics.PersistentVolumeClaimProvisionDurationSeconds.WithLabelValues(class, source).Observe(time.Since(startTime).Seconds())
|
|
ctrl.metrics.PersistentVolumeClaimProvisionTotal.WithLabelValues(class, source).Inc()
|
|
}
|
|
}
|
|
|
|
func (ctrl *ProvisionController) updateDeleteStats(volume *v1.PersistentVolume, err error, startTime time.Time) {
|
|
class := volume.Spec.StorageClassName
|
|
if err != nil {
|
|
ctrl.metrics.PersistentVolumeDeleteFailedTotal.WithLabelValues(class).Inc()
|
|
} else {
|
|
ctrl.metrics.PersistentVolumeDeleteDurationSeconds.WithLabelValues(class).Observe(time.Since(startTime).Seconds())
|
|
ctrl.metrics.PersistentVolumeDeleteTotal.WithLabelValues(class).Inc()
|
|
}
|
|
}
|
|
|
|
// patchPersistentVolumeWithFinalizers patches the PersistentVolume with the given finalizers
|
|
func (ctrl *ProvisionController) patchPersistentVolumeWithFinalizers(ctx context.Context, volume *v1.PersistentVolume, finalizers []string) (*v1.PersistentVolume, error) {
|
|
oldData, err := json.Marshal(volume)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
volumeCopy := volume.DeepCopy()
|
|
volumeCopy.ObjectMeta.Finalizers = finalizers
|
|
newData, err := json.Marshal(volumeCopy)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
patchBytes, err := strategicpatch.CreateTwoWayMergePatch(oldData, newData, &v1.PersistentVolume{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pv, err := ctrl.client.CoreV1().PersistentVolumes().Patch(ctx, volume.Name, types.StrategicMergePatchType, patchBytes, metav1.PatchOptions{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return pv, nil
|
|
}
|
|
|
|
// rescheduleProvisioning signal back to the scheduler to retry dynamic provisioning
|
|
// by removing the annSelectedNode annotation
|
|
func (ctrl *ProvisionController) rescheduleProvisioning(ctx context.Context, claim *v1.PersistentVolumeClaim) error {
|
|
if _, ok := claim.Annotations[annSelectedNode]; !ok {
|
|
// Provisioning not triggered by the scheduler, skip
|
|
return nil
|
|
}
|
|
|
|
// The claim from method args can be pointing to watcher cache. We must not
|
|
// modify these, therefore create a copy.
|
|
newClaim := claim.DeepCopy()
|
|
delete(newClaim.Annotations, annSelectedNode)
|
|
// Try to update the PVC object
|
|
if _, err := ctrl.client.CoreV1().PersistentVolumeClaims(newClaim.Namespace).Update(ctx, newClaim, metav1.UpdateOptions{}); err != nil {
|
|
return fmt.Errorf("delete annotation 'annSelectedNode' for PersistentVolumeClaim %q: %v", klog.KObj(newClaim), err)
|
|
}
|
|
|
|
// Save updated claim into informer cache to avoid operations on old claim.
|
|
if err := ctrl.claimInformer.GetStore().Update(newClaim); err != nil {
|
|
// This shouldn't happen because it is a local
|
|
// operation. The only situation in which Update fails
|
|
// is when the object is invalid, which isn't the case
|
|
// here
|
|
// (https://github.com/kubernetes/client-go/blob/eb0bad8167df60e402297b26e2cee1bddffde108/tools/cache/store.go#L154-L162).
|
|
// Log the error and hope that a regular cache update will resolve it.
|
|
klog.FromContext(ctx).Info("Update claim informer cache for PersistentVolumeClaim", "PVC", klog.KObj(newClaim), "err", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// provisionClaimOperation attempts to provision a volume for the given claim.
|
|
// Returns nil error only when the volume was provisioned (in which case it also returns ProvisioningFinished),
|
|
// a normal error when the volume was not provisioned and provisioning should be retried (requeue the claim),
|
|
// or the special errStopProvision when provisioning was impossible and no further attempts to provision should be tried.
|
|
func (ctrl *ProvisionController) provisionClaimOperation(ctx context.Context, claim *v1.PersistentVolumeClaim) (ProvisioningState, error) {
|
|
// Most code here is identical to that found in controller.go of kube's PV controller...
|
|
claimClass := util.GetPersistentVolumeClaimClass(claim)
|
|
logger := klog.LoggerWithValues(klog.FromContext(ctx), "PVC", klog.KObj(claim), "StorageClass", claimClass)
|
|
logger.V(4).Info("Started")
|
|
|
|
// A previous doProvisionClaim may just have finished while we were waiting for
|
|
// the locks. Check that PV (with deterministic name) hasn't been provisioned
|
|
// yet.
|
|
pvName := ctrl.volumeNameHook(claim)
|
|
_, exists, err := ctrl.volumes.GetByKey(pvName)
|
|
if err == nil && exists {
|
|
// Volume has been already provisioned, nothing to do.
|
|
logger.V(4).Info("PersistentVolume already exists, skipping", "PV", pvName)
|
|
return ProvisioningFinished, errStopProvision
|
|
}
|
|
|
|
// Prepare a claimRef to the claim early (to fail before a volume is
|
|
// provisioned)
|
|
claimRef, err := ref.GetReference(scheme.Scheme, claim)
|
|
if err != nil {
|
|
logger.Error(err, "Unexpected error getting claim reference")
|
|
return ProvisioningNoChange, err
|
|
}
|
|
|
|
// Check if this provisioner can provision this claim.
|
|
if err = ctrl.canProvision(ctx, claim); err != nil {
|
|
ctrl.eventRecorder.Event(claim, v1.EventTypeWarning, "ProvisioningFailed", err.Error())
|
|
logger.Error(err, "Failed to provision volume")
|
|
return ProvisioningFinished, errStopProvision
|
|
}
|
|
|
|
// For any issues getting fields from StorageClass (including reclaimPolicy & mountOptions),
|
|
// retry the claim because the storageClass can be fixed/(re)created independently of the claim
|
|
class, err := ctrl.getStorageClass(claimClass)
|
|
if err != nil {
|
|
logger.Error(err, "Error getting claim's StorageClass's fields")
|
|
return ProvisioningFinished, err
|
|
}
|
|
if !ctrl.knownProvisioner(class.Provisioner) {
|
|
// class.Provisioner has either changed since shouldProvision() or
|
|
// annDynamicallyProvisioned contains different provisioner than
|
|
// class.Provisioner.
|
|
logger.Error(nil, "Unknown provisioner requested in claim's StorageClass", "provisioner", class.Provisioner)
|
|
return ProvisioningFinished, errStopProvision
|
|
}
|
|
|
|
var selectedNode *v1.Node
|
|
// Get SelectedNode
|
|
if nodeName, ok := getString(claim.Annotations, annSelectedNode, annAlphaSelectedNode); ok {
|
|
if ctrl.nodeLister != nil {
|
|
selectedNode, err = ctrl.nodeLister.Get(nodeName)
|
|
} else {
|
|
selectedNode, err = ctrl.client.CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{}) // TODO (verult) cache Nodes
|
|
}
|
|
if err != nil {
|
|
// if node does not exist, reschedule and remove volume.kubernetes.io/selected-node annotation
|
|
if apierrs.IsNotFound(err) {
|
|
ctx2 := klog.NewContext(ctx, logger)
|
|
return ctrl.provisionVolumeErrorHandling(ctx2, ProvisioningReschedule, err, claim)
|
|
}
|
|
err = fmt.Errorf("failed to get target node: %v", err)
|
|
ctrl.eventRecorder.Event(claim, v1.EventTypeWarning, "ProvisioningFailed", err.Error())
|
|
return ProvisioningNoChange, err
|
|
}
|
|
}
|
|
|
|
options := ProvisionOptions{
|
|
StorageClass: class,
|
|
PVName: pvName,
|
|
PVC: claim,
|
|
SelectedNode: selectedNode,
|
|
}
|
|
|
|
ctrl.eventRecorder.Event(claim, v1.EventTypeNormal, "Provisioning", fmt.Sprintf("External provisioner is provisioning volume for claim %q", klog.KObj(claim)))
|
|
|
|
volume, result, err := ctrl.provisioner.Provision(ctx, options)
|
|
if err != nil {
|
|
if ierr, ok := err.(*IgnoredError); ok {
|
|
// Provision ignored, do nothing and hope another provisioner will provision it.
|
|
logger.V(4).Info("Volume provision ignored", "reason", ierr)
|
|
return ProvisioningFinished, errStopProvision
|
|
}
|
|
|
|
ctx2 := klog.NewContext(ctx, logger)
|
|
err = fmt.Errorf("failed to provision volume with StorageClass %q: %v", claimClass, err)
|
|
return ctrl.provisionVolumeErrorHandling(ctx2, result, err, claim)
|
|
}
|
|
|
|
logger.V(4).Info("Volume is provisioned", "PV", volume.Name)
|
|
|
|
// Set ClaimRef and the PV controller will bind and set annBoundByController for us
|
|
volume.Spec.ClaimRef = claimRef
|
|
|
|
// Add external provisioner finalizer if it doesn't already have it
|
|
if ctrl.addFinalizer && !ctrl.checkFinalizer(volume, finalizerPV) {
|
|
volume.ObjectMeta.Finalizers = append(volume.ObjectMeta.Finalizers, finalizerPV)
|
|
}
|
|
|
|
metav1.SetMetaDataAnnotation(&volume.ObjectMeta, annDynamicallyProvisioned, class.Provisioner)
|
|
volume.Spec.StorageClassName = claimClass
|
|
|
|
logger.V(4).Info("Succeeded")
|
|
|
|
if err := ctrl.volumeStore.StoreVolume(logger, claim, volume); err != nil {
|
|
return ProvisioningFinished, err
|
|
}
|
|
return ProvisioningFinished, nil
|
|
}
|
|
|
|
func (ctrl *ProvisionController) provisionVolumeErrorHandling(ctx context.Context, result ProvisioningState, err error, claim *v1.PersistentVolumeClaim) (ProvisioningState, error) {
|
|
logger := klog.FromContext(ctx)
|
|
ctrl.eventRecorder.Event(claim, v1.EventTypeWarning, "ProvisioningFailed", err.Error())
|
|
if _, ok := claim.Annotations[annSelectedNode]; ok && result == ProvisioningReschedule {
|
|
// For dynamic PV provisioning with delayed binding, the provisioner may fail
|
|
// because the node is wrong (permanent error) or currently unusable (not enough
|
|
// capacity). If the provisioner wants to give up scheduling with the currently
|
|
// selected node, then it can ask for that by returning ProvisioningReschedule
|
|
// as state.
|
|
//
|
|
// `selectedNode` must be removed to notify scheduler to schedule again.
|
|
if errLabel := ctrl.rescheduleProvisioning(ctx, claim); errLabel != nil {
|
|
logger.Info("Volume rescheduling failed", "err", errLabel)
|
|
// If unsetting that label fails in ctrl.rescheduleProvisioning, we
|
|
// keep the volume in the work queue as if the provisioner had
|
|
// returned ProvisioningFinished and simply try again later.
|
|
return ProvisioningFinished, err
|
|
}
|
|
// Label was removed, stop working on the volume.
|
|
logger.V(2).Info("Volume rescheduled because", "err", err)
|
|
return ProvisioningFinished, errStopProvision
|
|
}
|
|
|
|
// ProvisioningReschedule shouldn't have been returned for volumes without selected node,
|
|
// but if we get it anyway, then treat it like ProvisioningFinished because we cannot
|
|
// reschedule.
|
|
if result == ProvisioningReschedule {
|
|
result = ProvisioningFinished
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
// deleteVolumeOperation attempts to delete the volume backing the given
|
|
// volume. Returns error, which indicates whether deletion should be retried
|
|
// (requeue the volume) or not
|
|
func (ctrl *ProvisionController) deleteVolumeOperation(ctx context.Context, volume *v1.PersistentVolume) error {
|
|
logger := klog.LoggerWithValues(klog.FromContext(ctx), "PV", volume.Name)
|
|
logger.V(4).Info("Started")
|
|
|
|
err := ctrl.provisioner.Delete(ctx, volume)
|
|
if err != nil {
|
|
if ierr, ok := err.(*IgnoredError); ok {
|
|
// Delete ignored, do nothing and hope another provisioner will delete it.
|
|
logger.V(4).Info("Volume deletion ignored", "reason", ierr)
|
|
return nil
|
|
}
|
|
// Delete failed, emit an event.
|
|
logger.Error(err, "Volume deletion failed")
|
|
ctrl.eventRecorder.Event(volume, v1.EventTypeWarning, "VolumeFailedDelete", err.Error())
|
|
return err
|
|
}
|
|
|
|
logger.V(4).Info("Volume deleted")
|
|
|
|
// Delete the volume
|
|
if err = ctrl.client.CoreV1().PersistentVolumes().Delete(ctx, volume.Name, metav1.DeleteOptions{}); err != nil {
|
|
// Oops, could not delete the volume and therefore the controller will
|
|
// try to delete the volume again on next update.
|
|
logger.Info("Failed to delete persistentvolume", "err", err)
|
|
return err
|
|
}
|
|
|
|
if ctrl.addFinalizer {
|
|
if len(volume.ObjectMeta.Finalizers) > 0 {
|
|
// Remove external-provisioner finalizer
|
|
|
|
// need to get the pv again because the delete has updated the object with a deletion timestamp
|
|
volumeObj, exists, err := ctrl.volumes.GetByKey(volume.Name)
|
|
if err != nil {
|
|
logger.Info("Failed to get persistentvolume to update finalizer", "err", err)
|
|
return err
|
|
}
|
|
if !exists {
|
|
// If the volume is not found return
|
|
return nil
|
|
}
|
|
newVolume, ok := volumeObj.(*v1.PersistentVolume)
|
|
if !ok {
|
|
return fmt.Errorf("expected volume but got %+v", volumeObj)
|
|
}
|
|
finalizers, modified := removeFinalizer(newVolume.ObjectMeta.Finalizers, finalizerPV)
|
|
// Only update the finalizers if we actually removed something
|
|
if modified {
|
|
if _, err = ctrl.patchPersistentVolumeWithFinalizers(ctx, newVolume, finalizers); err != nil {
|
|
if !apierrs.IsNotFound(err) {
|
|
// Couldn't remove finalizer and the object still exists, the controller may
|
|
// try to remove the finalizer again on the next update
|
|
logger.Info("Failed to remove finalizer for persistentvolume", "err", err)
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
logger.V(4).Info("PersistentVolume deleted succeeded")
|
|
return nil
|
|
}
|
|
|
|
// removeFinalizer removes finalizer from slice, returns the new slice and whether modified.
|
|
// It does not modify the original slice.
|
|
func removeFinalizer(finalizers []string, finalizerToRemove string) ([]string, bool) {
|
|
ret := make([]string, 0, len(finalizers))
|
|
for _, finalizer := range finalizers {
|
|
if finalizer != finalizerToRemove {
|
|
ret = append(ret, finalizer)
|
|
}
|
|
}
|
|
|
|
if len(ret) == 0 {
|
|
ret = nil
|
|
}
|
|
|
|
return ret, len(ret) != len(finalizers)
|
|
}
|
|
|
|
// addFinalizer adds finalizer to slice, returns slice and whether modified.
|
|
func addFinalizer(finalizers []string, finalizerToAdd string) ([]string, bool) {
|
|
for _, finalizer := range finalizers {
|
|
if finalizer == finalizerToAdd {
|
|
// finalizer already exists
|
|
return finalizers, false
|
|
}
|
|
}
|
|
|
|
return append(finalizers, finalizerToAdd), true
|
|
}
|
|
|
|
// getInClusterNamespace returns the namespace in which the controller runs.
|
|
func getInClusterNamespace() string {
|
|
if ns := os.Getenv("POD_NAMESPACE"); ns != "" {
|
|
return ns
|
|
}
|
|
|
|
// Fall back to the namespace associated with the service account token, if available
|
|
if data, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); err == nil {
|
|
if ns := strings.TrimSpace(string(data)); len(ns) > 0 {
|
|
return ns
|
|
}
|
|
}
|
|
|
|
return "default"
|
|
}
|
|
|
|
// getProvisionedVolumeNameForClaim returns PV.Name for the provisioned volume.
|
|
// The name must be unique.
|
|
func getProvisionedVolumeNameForClaim(claim *v1.PersistentVolumeClaim) string {
|
|
return "pvc-" + string(claim.UID)
|
|
}
|
|
|
|
// getStorageClass retrives storage class object by name.
|
|
func (ctrl *ProvisionController) getStorageClass(name string) (*storage.StorageClass, error) {
|
|
classObj, found, err := ctrl.classes.GetByKey(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !found {
|
|
return nil, fmt.Errorf("storageClass %q not found", name)
|
|
}
|
|
switch class := classObj.(type) {
|
|
case *storage.StorageClass:
|
|
return class, nil
|
|
case *storagebeta.StorageClass:
|
|
// convert storagebeta.StorageClass to storage.StorageClass
|
|
return &storage.StorageClass{
|
|
ObjectMeta: class.ObjectMeta,
|
|
Provisioner: class.Provisioner,
|
|
Parameters: class.Parameters,
|
|
ReclaimPolicy: class.ReclaimPolicy,
|
|
MountOptions: class.MountOptions,
|
|
AllowVolumeExpansion: class.AllowVolumeExpansion,
|
|
VolumeBindingMode: (*storage.VolumeBindingMode)(class.VolumeBindingMode),
|
|
AllowedTopologies: class.AllowedTopologies,
|
|
}, nil
|
|
}
|
|
return nil, fmt.Errorf("cannot convert object to StorageClass: %+v", classObj)
|
|
}
|
|
|
|
// supportsBlock returns whether a provisioner supports block volume.
|
|
// Provisioners that implement BlockProvisioner interface and return true to SupportsBlock
|
|
// will be regarded as supported for block volume.
|
|
func (ctrl *ProvisionController) supportsBlock(ctx context.Context) bool {
|
|
if blockProvisioner, ok := ctrl.provisioner.(BlockProvisioner); ok {
|
|
return blockProvisioner.SupportsBlock(ctx)
|
|
}
|
|
return false
|
|
}
|
|
|
|
func getString(m map[string]string, key string, alts ...string) (string, bool) {
|
|
if m == nil {
|
|
return "", false
|
|
}
|
|
keys := append([]string{key}, alts...)
|
|
for _, k := range keys {
|
|
if v, ok := m[k]; ok {
|
|
return v, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|