/* 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 } 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 && len(pvc.GetLabels()) > 0 { s.pvcLabels = pvc.Labels } } 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 } 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 }