mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-08-17 04:36:05 +00:00
This commit implements VolumePolicy support for PVC Phase conditions, resolving
vmware-tanzu/velero#7233 where backups fail with ''PVC has no volume backing this claim''
for Pending PVCs.
Changes made:
- Extended VolumePolicy API to support PVC phase conditions
- Added pvcPhaseCondition struct with matching logic
- Modified getMatchAction() to evaluate policies for unbound PVCs before returning errors
- Added case to GetMatchAction() to handle PVC-only scenarios (nil PV)
- Added comprehensive unit tests for PVC phase parsing and matching
Users can now skip Pending PVCs through volume policy configuration:
apiVersion: v1
kind: ConfigMap
metadata:
name: volume-policy
namespace: velero
data:
policy.yaml: |
version: v1
volumePolicies:
- conditions:
pvcPhase: [Pending]
action:
type: skip
chore: rename changelog file to match PR #9166
Renamed changelogs/unreleased/7233-claude to changelogs/unreleased/9166-claude
to match the opened PR at https://github.com/vmware-tanzu/velero/pull/9166
docs: Add PVC phase condition support to VolumePolicy documentation
- Added pvcPhase field to YAML template example
- Documented pvcPhase as a supported condition in the list
- Added comprehensive examples for using PVC phase conditions
- Included examples for Pending, Bound, and Lost phases
- Demonstrated combining PVC phase with other conditions
Co-Authored-By: Tiger Kaovilai <kaovilai@users.noreply.github.com>
303 lines
6.5 KiB
Go
303 lines
6.5 KiB
Go
/*
|
|
Copyright The Velero Contributors.
|
|
|
|
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 resourcepolicies
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
|
|
"github.com/pkg/errors"
|
|
"gopkg.in/yaml.v3"
|
|
corev1api "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
)
|
|
|
|
type volPolicy struct {
|
|
action Action
|
|
conditions []volumeCondition
|
|
}
|
|
|
|
type volumeCondition interface {
|
|
match(v *structuredVolume) bool
|
|
validate() error
|
|
}
|
|
|
|
// capacity consist of the lower and upper boundary
|
|
type capacity struct {
|
|
lower resource.Quantity
|
|
upper resource.Quantity
|
|
}
|
|
|
|
type structuredVolume struct {
|
|
capacity resource.Quantity
|
|
storageClass string
|
|
nfs *nFSVolumeSource
|
|
csi *csiVolumeSource
|
|
volumeType SupportedVolume
|
|
pvcLabels map[string]string
|
|
pvcPhase string
|
|
}
|
|
|
|
func (s *structuredVolume) parsePV(pv *corev1api.PersistentVolume) {
|
|
s.capacity = *pv.Spec.Capacity.Storage()
|
|
s.storageClass = pv.Spec.StorageClassName
|
|
nfs := pv.Spec.NFS
|
|
if nfs != nil {
|
|
s.nfs = &nFSVolumeSource{Server: nfs.Server, Path: nfs.Path}
|
|
}
|
|
|
|
csi := pv.Spec.CSI
|
|
if csi != nil {
|
|
s.csi = &csiVolumeSource{Driver: csi.Driver, VolumeAttributes: csi.VolumeAttributes}
|
|
}
|
|
|
|
s.volumeType = getVolumeTypeFromPV(pv)
|
|
}
|
|
|
|
func (s *structuredVolume) parsePVC(pvc *corev1api.PersistentVolumeClaim) {
|
|
if pvc != nil {
|
|
if len(pvc.GetLabels()) > 0 {
|
|
s.pvcLabels = pvc.Labels
|
|
}
|
|
s.pvcPhase = string(pvc.Status.Phase)
|
|
}
|
|
}
|
|
|
|
func (s *structuredVolume) parsePodVolume(vol *corev1api.Volume) {
|
|
nfs := vol.NFS
|
|
if nfs != nil {
|
|
s.nfs = &nFSVolumeSource{Server: nfs.Server, Path: nfs.Path}
|
|
}
|
|
|
|
csi := vol.CSI
|
|
if csi != nil {
|
|
s.csi = &csiVolumeSource{Driver: csi.Driver, VolumeAttributes: csi.VolumeAttributes}
|
|
}
|
|
|
|
s.volumeType = getVolumeTypeFromVolume(vol)
|
|
}
|
|
|
|
// pvcLabelsCondition defines a condition that matches if the PVC's labels contain all the provided key/value pairs.
|
|
type pvcLabelsCondition struct {
|
|
labels map[string]string
|
|
}
|
|
|
|
func (c *pvcLabelsCondition) match(v *structuredVolume) bool {
|
|
// No labels specified: always match.
|
|
if len(c.labels) == 0 {
|
|
return true
|
|
}
|
|
if v.pvcLabels == nil {
|
|
return false
|
|
}
|
|
selector := labels.SelectorFromSet(c.labels)
|
|
return selector.Matches(labels.Set(v.pvcLabels))
|
|
}
|
|
|
|
func (c *pvcLabelsCondition) validate() error {
|
|
return nil
|
|
}
|
|
|
|
// pvcPhaseCondition defines a condition that matches if the PVC's phase matches any of the provided phases.
|
|
type pvcPhaseCondition struct {
|
|
phases []string
|
|
}
|
|
|
|
func (c *pvcPhaseCondition) match(v *structuredVolume) bool {
|
|
// No phases specified: always match.
|
|
if len(c.phases) == 0 {
|
|
return true
|
|
}
|
|
if v.pvcPhase == "" {
|
|
return false
|
|
}
|
|
for _, phase := range c.phases {
|
|
if v.pvcPhase == phase {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *pvcPhaseCondition) validate() error {
|
|
return nil
|
|
}
|
|
|
|
type capacityCondition struct {
|
|
capacity capacity
|
|
}
|
|
|
|
func (c *capacityCondition) match(v *structuredVolume) bool {
|
|
return c.capacity.isInRange(v.capacity)
|
|
}
|
|
|
|
type storageClassCondition struct {
|
|
storageClass []string
|
|
}
|
|
|
|
func (s *storageClassCondition) match(v *structuredVolume) bool {
|
|
if len(s.storageClass) == 0 {
|
|
return true
|
|
}
|
|
|
|
if v.storageClass == "" {
|
|
return false
|
|
}
|
|
|
|
for _, sc := range s.storageClass {
|
|
if v.storageClass == sc {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
type nfsCondition struct {
|
|
nfs *nFSVolumeSource
|
|
}
|
|
|
|
func (c *nfsCondition) match(v *structuredVolume) bool {
|
|
if c.nfs == nil {
|
|
return true
|
|
}
|
|
if v.nfs == nil {
|
|
return false
|
|
}
|
|
|
|
if c.nfs.Path == "" {
|
|
if c.nfs.Server == "" { // match nfs: {}
|
|
return v.nfs != nil
|
|
}
|
|
if c.nfs.Server != v.nfs.Server {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
if c.nfs.Path != v.nfs.Path {
|
|
return false
|
|
}
|
|
if c.nfs.Server == "" {
|
|
return true
|
|
}
|
|
if c.nfs.Server != v.nfs.Server {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
type csiCondition struct {
|
|
csi *csiVolumeSource
|
|
}
|
|
|
|
func (c *csiCondition) match(v *structuredVolume) bool {
|
|
if c.csi == nil {
|
|
return true
|
|
}
|
|
|
|
if c.csi.Driver == "" { // match csi: {}
|
|
return v.csi != nil
|
|
}
|
|
|
|
if v.csi == nil {
|
|
return false
|
|
}
|
|
|
|
if c.csi.Driver != v.csi.Driver {
|
|
return false
|
|
}
|
|
|
|
if len(c.csi.VolumeAttributes) == 0 {
|
|
return true
|
|
}
|
|
|
|
if len(v.csi.VolumeAttributes) == 0 {
|
|
return false
|
|
}
|
|
|
|
for key, value := range c.csi.VolumeAttributes {
|
|
if value != v.csi.VolumeAttributes[key] {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// parseCapacity parse string into capacity format
|
|
func parseCapacity(cap string) (*capacity, error) {
|
|
if cap == "" {
|
|
cap = ","
|
|
}
|
|
capacities := strings.Split(cap, ",")
|
|
var quantities []resource.Quantity
|
|
if len(capacities) != 2 {
|
|
return nil, fmt.Errorf("wrong format of Capacity %v", cap)
|
|
}
|
|
|
|
for _, v := range capacities {
|
|
if strings.TrimSpace(v) == "" {
|
|
// case similar "10Gi,"
|
|
// if empty, the quantity will assigned with 0
|
|
quantities = append(quantities, *resource.NewQuantity(int64(0), resource.DecimalSI))
|
|
} else {
|
|
quantity, err := resource.ParseQuantity(strings.TrimSpace(v))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("wrong format of Capacity %v with err %v", v, err)
|
|
}
|
|
quantities = append(quantities, quantity)
|
|
}
|
|
}
|
|
|
|
return &capacity{lower: quantities[0], upper: quantities[1]}, nil
|
|
}
|
|
|
|
// isInRange returns true if the quantity y is in range of capacity, or it returns false
|
|
func (c *capacity) isInRange(y resource.Quantity) bool {
|
|
if c.lower.IsZero() && c.upper.Cmp(y) >= 0 {
|
|
// [0, a] y
|
|
return true
|
|
}
|
|
if c.upper.IsZero() && c.lower.Cmp(y) <= 0 {
|
|
// [b, 0] y
|
|
return true
|
|
}
|
|
if !c.lower.IsZero() && !c.upper.IsZero() {
|
|
// [a, b] y
|
|
return c.lower.Cmp(y) <= 0 && c.upper.Cmp(y) >= 0
|
|
}
|
|
return false
|
|
}
|
|
|
|
// unmarshalVolConditions parse map[string]any into volumeConditions format
|
|
// and validate key fields of the map.
|
|
func unmarshalVolConditions(con map[string]any) (*volumeConditions, error) {
|
|
volConditons := &volumeConditions{}
|
|
buffer := new(bytes.Buffer)
|
|
err := yaml.NewEncoder(buffer).Encode(con)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to encode volume conditions")
|
|
}
|
|
|
|
if err := decodeStruct(buffer, volConditons); err != nil {
|
|
return nil, errors.Wrap(err, "failed to decode volume conditions")
|
|
}
|
|
return volConditons, nil
|
|
}
|