/* 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" "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 } 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} } s.volumeType = getVolumeTypeFromPV(pv) } 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} } s.volumeType = getVolumeTypeFromVolume(vol) } 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 } return c.csi.Driver == v.csi.Driver } // 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]interface{} into volumeConditions format // and validate key fields of the map. func unmarshalVolConditions(con map[string]interface{}) (*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 }