Handle backup of volume by resource policies (#5901)
* Handle backup of volume by resource policies Signed-off-by: Ming <mqiu@vmware.com>pull/6009/head
parent
ec88dc5203
commit
086dbd344f
|
@ -430,6 +430,26 @@ spec:
|
|||
"objectname".
|
||||
nullable: true
|
||||
type: object
|
||||
resourcePolices:
|
||||
description: ResourcePolicies specifies the referenced resource policies
|
||||
that backup should follow
|
||||
properties:
|
||||
apiGroup:
|
||||
description: APIGroup is the group for the resource being referenced.
|
||||
If APIGroup is not specified, the specified Kind must be in
|
||||
the core API group. For any other third-party types, APIGroup
|
||||
is required.
|
||||
type: string
|
||||
kind:
|
||||
description: Kind is the type of resource being referenced
|
||||
type: string
|
||||
name:
|
||||
description: Name is the name of resource being referenced
|
||||
type: string
|
||||
required:
|
||||
- kind
|
||||
- name
|
||||
type: object
|
||||
snapshotVolumes:
|
||||
description: SnapshotVolumes specifies whether to take snapshots of
|
||||
any PV's referenced in the set of objects included in the Backup.
|
||||
|
|
|
@ -466,6 +466,26 @@ spec:
|
|||
simply use "objectname".
|
||||
nullable: true
|
||||
type: object
|
||||
resourcePolices:
|
||||
description: ResourcePolicies specifies the referenced resource
|
||||
policies that backup should follow
|
||||
properties:
|
||||
apiGroup:
|
||||
description: APIGroup is the group for the resource being
|
||||
referenced. If APIGroup is not specified, the specified
|
||||
Kind must be in the core API group. For any other third-party
|
||||
types, APIGroup is required.
|
||||
type: string
|
||||
kind:
|
||||
description: Kind is the type of resource being referenced
|
||||
type: string
|
||||
name:
|
||||
description: Name is the name of resource being referenced
|
||||
type: string
|
||||
required:
|
||||
- kind
|
||||
- name
|
||||
type: object
|
||||
snapshotVolumes:
|
||||
description: SnapshotVolumes specifies whether to take snapshots
|
||||
of any PV's referenced in the set of objects included in the
|
||||
|
|
File diff suppressed because one or more lines are too long
2
go.mod
2
go.mod
|
@ -41,6 +41,7 @@ require (
|
|||
google.golang.org/api v0.74.0
|
||||
google.golang.org/grpc v1.45.0
|
||||
google.golang.org/protobuf v1.28.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
k8s.io/api v0.25.6
|
||||
k8s.io/apiextensions-apiserver v0.24.2
|
||||
k8s.io/apimachinery v0.25.6
|
||||
|
@ -144,7 +145,6 @@ require (
|
|||
gopkg.in/ini.v1 v1.66.2 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/component-base v0.24.2 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
package resourcepolicies
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
type VolumeActionType string
|
||||
|
||||
const Skip VolumeActionType = "skip"
|
||||
|
||||
// Action defined as one action for a specific way of backup
|
||||
type Action struct {
|
||||
// Type defined specific type of action, currently only support 'skip'
|
||||
Type VolumeActionType `yaml:"type"`
|
||||
// Parameters defined map of parameters when executing a specific action
|
||||
Parameters map[string]interface{} `yaml:"parameters,omitempty"`
|
||||
}
|
||||
|
||||
// VolumePolicy defined policy to conditions to match Volumes and related action to handle matched Volumes
|
||||
type VolumePolicy struct {
|
||||
// Conditions defined list of conditions to match Volumes
|
||||
Conditions map[string]interface{} `yaml:"conditions"`
|
||||
Action Action `yaml:"action"`
|
||||
}
|
||||
|
||||
// currently only support configmap type of resource config
|
||||
const ConfigmapRefType string = "configmap"
|
||||
|
||||
// resourcePolicies currently defined slice of volume policies to handle backup
|
||||
type resourcePolicies struct {
|
||||
Version string `yaml:"version"`
|
||||
VolumePolicies []VolumePolicy `yaml:"volumePolicies"`
|
||||
// we may support other resource policies in the future, and they could be added separately
|
||||
// OtherResourcePolicies: []OtherResourcePolicy
|
||||
}
|
||||
|
||||
type Policies struct {
|
||||
Version string
|
||||
VolumePolicies []volumePolicy
|
||||
// OtherPolicies
|
||||
}
|
||||
|
||||
func unmarshalResourcePolicies(YamlData *string) (*resourcePolicies, error) {
|
||||
resPolicies := &resourcePolicies{}
|
||||
if err := decodeStruct(strings.NewReader(*YamlData), resPolicies); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode yaml data into resource policies %v", err)
|
||||
} else {
|
||||
return resPolicies, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (policies *Policies) buildPolicy(resPolicies *resourcePolicies) error {
|
||||
for _, vp := range resPolicies.VolumePolicies {
|
||||
con, err := unmarshalVolConditions(vp.Conditions)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to unmarshl volume conditions")
|
||||
}
|
||||
volCap, err := parseCapacity(con.Capacity)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to parse condition capacity %s", con.Capacity)
|
||||
}
|
||||
var p volumePolicy
|
||||
p.action = vp.Action
|
||||
p.conditions = append(p.conditions, &capacityCondition{capacity: *volCap})
|
||||
p.conditions = append(p.conditions, &storageClassCondition{storageClass: con.StorageClass})
|
||||
p.conditions = append(p.conditions, &nfsCondition{nfs: con.NFS})
|
||||
p.conditions = append(p.conditions, &csiCondition{csi: con.CSI})
|
||||
policies.VolumePolicies = append(policies.VolumePolicies, p)
|
||||
}
|
||||
|
||||
// Other resource policies
|
||||
|
||||
policies.Version = resPolicies.Version
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Policies) Match(res interface{}) *Action {
|
||||
vol, ok := res.(*StructuredVolume)
|
||||
if ok {
|
||||
for _, policy := range p.VolumePolicies {
|
||||
isAllMatch := false
|
||||
for _, con := range policy.conditions {
|
||||
if !con.Match(vol) {
|
||||
isAllMatch = false
|
||||
break
|
||||
}
|
||||
isAllMatch = true
|
||||
}
|
||||
if isAllMatch {
|
||||
return &policy.action
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Policies) Validate() error {
|
||||
if p.Version != currentSupportDataVersion {
|
||||
return fmt.Errorf("incompatible version number %s with supported version %s", p.Version, currentSupportDataVersion)
|
||||
}
|
||||
|
||||
for _, policy := range p.VolumePolicies {
|
||||
if err := policy.action.Validate(); err != nil {
|
||||
return errors.Wrap(err, "failed to validate config")
|
||||
}
|
||||
for _, con := range policy.conditions {
|
||||
if err := con.Validate(); err != nil {
|
||||
return errors.Wrap(err, "failed to validate conditions config")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetResourcePoliciesFromConfig(cm *v1.ConfigMap) (*Policies, error) {
|
||||
if cm == nil {
|
||||
return nil, fmt.Errorf("could not parse config from nil configmap")
|
||||
}
|
||||
if len(cm.Data) != 1 {
|
||||
return nil, fmt.Errorf("illegal resource policies %s/%s configmap", cm.Name, cm.Namespace)
|
||||
}
|
||||
|
||||
var yamlData string
|
||||
for _, v := range cm.Data {
|
||||
yamlData = v
|
||||
}
|
||||
|
||||
resPolicies, err := unmarshalResourcePolicies(&yamlData)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
policies := &Policies{}
|
||||
if err := policies.buildPolicy(resPolicies); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return policies, nil
|
||||
}
|
|
@ -0,0 +1,244 @@
|
|||
package resourcepolicies
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestLoadResourcePolicies(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
yamlData string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "unknown key in yaml",
|
||||
yamlData: `version: v1
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
capacity: "0,100Gi"
|
||||
unknown: {}
|
||||
storageClass:
|
||||
- gp2
|
||||
- ebs-sc
|
||||
action:
|
||||
type: skip`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "reduplicated key in yaml",
|
||||
yamlData: `version: v1
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
capacity: "0,100Gi"
|
||||
capacity: "0,100Gi"
|
||||
storageClass:
|
||||
- gp2
|
||||
- ebs-sc
|
||||
action:
|
||||
type: skip`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "error format of storageClass",
|
||||
yamlData: `version: v1
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
capacity: "0,100Gi"
|
||||
storageClass: gp2
|
||||
action:
|
||||
type: skip`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "error format of csi",
|
||||
yamlData: `version: v1
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
capacity: "0,100Gi"
|
||||
csi: gp2
|
||||
action:
|
||||
type: skip`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "error format of nfs",
|
||||
yamlData: `version: v1
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
capacity: "0,100Gi"
|
||||
csi: {}
|
||||
nfs: abc
|
||||
action:
|
||||
type: skip`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "supported formart volume policies",
|
||||
yamlData: `version: v1
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
capacity: "0,100Gi"
|
||||
csi:
|
||||
driver: aws.efs.csi.driver
|
||||
nfs: {}
|
||||
storageClass:
|
||||
- gp2
|
||||
- ebs-sc
|
||||
action:
|
||||
type: skip`,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := unmarshalResourcePolicies(&tc.yamlData)
|
||||
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Fatalf("Expected error %v, but got error %v", tc.wantErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetResourceMatchedAction(t *testing.T) {
|
||||
resPolicies := &resourcePolicies{
|
||||
Version: "v1",
|
||||
VolumePolicies: []VolumePolicy{
|
||||
{
|
||||
Action: Action{Type: "skip"},
|
||||
Conditions: map[string]interface{}{
|
||||
"capacity": "0,10Gi",
|
||||
"storageClass": []string{"gp2", "ebs-sc"},
|
||||
"csi": interface{}(
|
||||
map[string]interface{}{
|
||||
"driver": "aws.efs.csi.driver",
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
Action: Action{Type: "volume-snapshot"},
|
||||
Conditions: map[string]interface{}{
|
||||
"capacity": "10,100Gi",
|
||||
"storageClass": []string{"gp2", "ebs-sc"},
|
||||
"csi": interface{}(
|
||||
map[string]interface{}{
|
||||
"driver": "aws.efs.csi.driver",
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
Action: Action{Type: "file-system-backup"},
|
||||
Conditions: map[string]interface{}{
|
||||
"storageClass": []string{"gp2", "ebs-sc"},
|
||||
"csi": interface{}(
|
||||
map[string]interface{}{
|
||||
"driver": "aws.efs.csi.driver",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
testCases := []struct {
|
||||
name string
|
||||
volume *StructuredVolume
|
||||
expectedAction *Action
|
||||
}{
|
||||
{
|
||||
name: "match policy",
|
||||
volume: &StructuredVolume{
|
||||
capacity: *resource.NewQuantity(5<<30, resource.BinarySI),
|
||||
storageClass: "ebs-sc",
|
||||
csi: &csiVolumeSource{Driver: "aws.efs.csi.driver"},
|
||||
},
|
||||
expectedAction: &Action{Type: "skip"},
|
||||
},
|
||||
{
|
||||
name: "both matches return the first policy",
|
||||
volume: &StructuredVolume{
|
||||
capacity: *resource.NewQuantity(50<<30, resource.BinarySI),
|
||||
storageClass: "ebs-sc",
|
||||
csi: &csiVolumeSource{Driver: "aws.efs.csi.driver"},
|
||||
},
|
||||
expectedAction: &Action{Type: "volume-snapshot"},
|
||||
},
|
||||
{
|
||||
name: "dismatch all policies",
|
||||
volume: &StructuredVolume{
|
||||
capacity: *resource.NewQuantity(50<<30, resource.BinarySI),
|
||||
storageClass: "ebs-sc",
|
||||
nfs: &nFSVolumeSource{},
|
||||
},
|
||||
expectedAction: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
policies := &Policies{}
|
||||
err := policies.buildPolicy(resPolicies)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to build policy with error %v", err)
|
||||
}
|
||||
|
||||
action := policies.Match(tc.volume)
|
||||
if action == nil {
|
||||
if tc.expectedAction != nil {
|
||||
t.Errorf("Expected action %v, but got result nil", tc.expectedAction.Type)
|
||||
}
|
||||
} else {
|
||||
if tc.expectedAction != nil {
|
||||
if action.Type != tc.expectedAction.Type {
|
||||
t.Errorf("Expected action %v, but got result %v", tc.expectedAction.Type, action.Type)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("Expected action nil, but got result %v", action.Type)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetResourcePoliciesFromConfig(t *testing.T) {
|
||||
// Create a test ConfigMap
|
||||
cm := &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-configmap",
|
||||
Namespace: "test-namespace",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"test-data": "version: v1\nvolumePolicies:\n- conditions:\n capacity: '0,10Gi'\n action:\n type: skip",
|
||||
},
|
||||
}
|
||||
|
||||
// Call the function and check for errors
|
||||
resPolicies, err := GetResourcePoliciesFromConfig(cm)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Check that the returned resourcePolicies object contains the expected data
|
||||
assert.Equal(t, "v1", resPolicies.Version)
|
||||
assert.Len(t, resPolicies.VolumePolicies, 1)
|
||||
policies := resourcePolicies{
|
||||
Version: "v1",
|
||||
VolumePolicies: []VolumePolicy{
|
||||
{
|
||||
Conditions: map[string]interface{}{
|
||||
"capacity": "0,10Gi",
|
||||
},
|
||||
Action: Action{
|
||||
Type: Skip,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
p := &Policies{}
|
||||
err = p.buildPolicy(&policies)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build policy with error %v", err)
|
||||
}
|
||||
assert.Equal(t, p, resPolicies)
|
||||
}
|
|
@ -0,0 +1,201 @@
|
|||
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 volumePolicy 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
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
}
|
||||
|
||||
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 == "" {
|
||||
return true
|
||||
}
|
||||
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 v.csi == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return c.csi.Driver == v.csi.Driver
|
||||
}
|
||||
|
||||
// parseCapacity parse string into capacity format
|
||||
func parseCapacity(capacity string) (*Capacity, error) {
|
||||
if capacity == "" {
|
||||
capacity = ","
|
||||
}
|
||||
capacities := strings.Split(capacity, ",")
|
||||
var quantities []resource.Quantity
|
||||
if len(capacities) != 2 {
|
||||
return nil, fmt.Errorf("wrong format of Capacity %v", capacity)
|
||||
} else {
|
||||
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 {
|
||||
if quantity, err := resource.ParseQuantity(strings.TrimSpace(v)); err != nil {
|
||||
return nil, fmt.Errorf("wrong format of Capacity %v with err %v", v, err)
|
||||
} else {
|
||||
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
|
||||
}
|
|
@ -0,0 +1,412 @@
|
|||
package resourcepolicies
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
)
|
||||
|
||||
func setStructuredVolume(capacity resource.Quantity, sc string, nfs *nFSVolumeSource, csi *csiVolumeSource) *StructuredVolume {
|
||||
return &StructuredVolume{
|
||||
capacity: capacity,
|
||||
storageClass: sc,
|
||||
nfs: nfs,
|
||||
csi: csi,
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCapacity(t *testing.T) {
|
||||
var emptyCapacity Capacity
|
||||
tests := []struct {
|
||||
input string
|
||||
expected Capacity
|
||||
expectedErr error
|
||||
}{
|
||||
{"10Gi,20Gi", Capacity{lower: *resource.NewQuantity(10<<30, resource.BinarySI), upper: *resource.NewQuantity(20<<30, resource.BinarySI)}, nil},
|
||||
{"10Gi,", Capacity{lower: *resource.NewQuantity(10<<30, resource.BinarySI), upper: *resource.NewQuantity(0, resource.DecimalSI)}, nil},
|
||||
{"10Gi", emptyCapacity, fmt.Errorf("wrong format of Capacity 10Gi")},
|
||||
{"", emptyCapacity, nil},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test // capture range variable
|
||||
t.Run(test.input, func(t *testing.T) {
|
||||
actual, actualErr := parseCapacity(test.input)
|
||||
if test.expected != emptyCapacity {
|
||||
assert.Equal(t, test.expected.lower.Cmp(actual.lower), 0)
|
||||
assert.Equal(t, test.expected.upper.Cmp(actual.upper), 0)
|
||||
}
|
||||
assert.Equal(t, test.expectedErr, actualErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapacityIsInRange(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
capacity *Capacity
|
||||
quantity resource.Quantity
|
||||
isInRange bool
|
||||
}{
|
||||
{&Capacity{*resource.NewQuantity(0, resource.BinarySI), *resource.NewQuantity(10<<30, resource.BinarySI)}, *resource.NewQuantity(5<<30, resource.BinarySI), true},
|
||||
{&Capacity{*resource.NewQuantity(0, resource.BinarySI), *resource.NewQuantity(10<<30, resource.BinarySI)}, *resource.NewQuantity(15<<30, resource.BinarySI), false},
|
||||
{&Capacity{*resource.NewQuantity(20<<30, resource.BinarySI), *resource.NewQuantity(0, resource.DecimalSI)}, *resource.NewQuantity(25<<30, resource.BinarySI), true},
|
||||
{&Capacity{*resource.NewQuantity(20<<30, resource.BinarySI), *resource.NewQuantity(0, resource.DecimalSI)}, *resource.NewQuantity(15<<30, resource.BinarySI), false},
|
||||
{&Capacity{*resource.NewQuantity(10<<30, resource.BinarySI), *resource.NewQuantity(20<<30, resource.BinarySI)}, *resource.NewQuantity(15<<30, resource.BinarySI), true},
|
||||
{&Capacity{*resource.NewQuantity(10<<30, resource.BinarySI), *resource.NewQuantity(20<<30, resource.BinarySI)}, *resource.NewQuantity(5<<30, resource.BinarySI), false},
|
||||
{&Capacity{*resource.NewQuantity(10<<30, resource.BinarySI), *resource.NewQuantity(20<<30, resource.BinarySI)}, *resource.NewQuantity(25<<30, resource.BinarySI), false},
|
||||
{&Capacity{*resource.NewQuantity(0, resource.BinarySI), *resource.NewQuantity(0, resource.BinarySI)}, *resource.NewQuantity(5<<30, resource.BinarySI), true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test // capture range variable
|
||||
t.Run(fmt.Sprintf("%v with %v", test.capacity, test.quantity), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
actual := test.capacity.isInRange(test.quantity)
|
||||
|
||||
assert.Equal(t, test.isInRange, actual)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorageClassConditionMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
condition *storageClassCondition
|
||||
volume *StructuredVolume
|
||||
expectedMatch bool
|
||||
}{
|
||||
{
|
||||
name: "match single storage class",
|
||||
condition: &storageClassCondition{[]string{"gp2"}},
|
||||
volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "gp2", nil, nil),
|
||||
expectedMatch: true,
|
||||
},
|
||||
{
|
||||
name: "match multiple storage classes",
|
||||
condition: &storageClassCondition{[]string{"gp2", "ebs-sc"}},
|
||||
volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "gp2", nil, nil),
|
||||
expectedMatch: true,
|
||||
},
|
||||
{
|
||||
name: "mismatch storage class",
|
||||
condition: &storageClassCondition{[]string{"gp2"}},
|
||||
volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "ebs-sc", nil, nil),
|
||||
expectedMatch: false,
|
||||
},
|
||||
{
|
||||
name: "empty storage class",
|
||||
condition: &storageClassCondition{[]string{}},
|
||||
volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "ebs-sc", nil, nil),
|
||||
expectedMatch: true,
|
||||
},
|
||||
{
|
||||
name: "empty volume storage class",
|
||||
condition: &storageClassCondition{[]string{"gp2"}},
|
||||
volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "", nil, nil),
|
||||
expectedMatch: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
match := tt.condition.Match(tt.volume)
|
||||
if match != tt.expectedMatch {
|
||||
t.Errorf("expected %v, but got %v", tt.expectedMatch, match)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNFSConditionMatch(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
condition *nfsCondition
|
||||
volume *StructuredVolume
|
||||
expectedMatch bool
|
||||
}{
|
||||
{
|
||||
name: "match nfs condition",
|
||||
condition: &nfsCondition{&nFSVolumeSource{Server: "192.168.10.20"}},
|
||||
volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "", &nFSVolumeSource{Server: "192.168.10.20"}, nil),
|
||||
expectedMatch: true,
|
||||
},
|
||||
{
|
||||
name: "empty nfs condition",
|
||||
condition: &nfsCondition{nil},
|
||||
volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "", &nFSVolumeSource{Server: "192.168.10.20"}, nil),
|
||||
expectedMatch: true,
|
||||
},
|
||||
{
|
||||
name: "empty nfs server and path condition",
|
||||
condition: &nfsCondition{&nFSVolumeSource{Server: "", Path: ""}},
|
||||
volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "", &nFSVolumeSource{Server: "192.168.10.20"}, nil),
|
||||
expectedMatch: true,
|
||||
},
|
||||
{
|
||||
name: "server dismatch",
|
||||
condition: &nfsCondition{&nFSVolumeSource{Server: "192.168.10.20", Path: ""}},
|
||||
volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "", &nFSVolumeSource{Server: ""}, nil),
|
||||
expectedMatch: false,
|
||||
},
|
||||
{
|
||||
name: "empty nfs server condition",
|
||||
condition: &nfsCondition{&nFSVolumeSource{Path: "/mnt/data"}},
|
||||
volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "", &nFSVolumeSource{Server: "192.168.10.20", Path: "/mnt/data"}, nil),
|
||||
expectedMatch: true,
|
||||
},
|
||||
{
|
||||
name: "empty nfs volume",
|
||||
condition: &nfsCondition{&nFSVolumeSource{Server: "192.168.10.20"}},
|
||||
volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "", nil, nil),
|
||||
expectedMatch: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
match := tt.condition.Match(tt.volume)
|
||||
if match != tt.expectedMatch {
|
||||
t.Errorf("expected %v, but got %v", tt.expectedMatch, match)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCSIConditionMatch(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
condition *csiCondition
|
||||
volume *StructuredVolume
|
||||
expectedMatch bool
|
||||
}{
|
||||
{
|
||||
name: "match csi condition",
|
||||
condition: &csiCondition{&csiVolumeSource{Driver: "test"}},
|
||||
volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "", nil, &csiVolumeSource{Driver: "test"}),
|
||||
expectedMatch: true,
|
||||
},
|
||||
{
|
||||
name: "empty csi condition",
|
||||
condition: &csiCondition{nil},
|
||||
volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "", nil, &csiVolumeSource{Driver: "test"}),
|
||||
expectedMatch: true,
|
||||
},
|
||||
{
|
||||
name: "empty csi volume",
|
||||
condition: &csiCondition{&csiVolumeSource{Driver: "test"}},
|
||||
volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "", nil, &csiVolumeSource{}),
|
||||
expectedMatch: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
match := tt.condition.Match(tt.volume)
|
||||
if match != tt.expectedMatch {
|
||||
t.Errorf("expected %v, but got %v", tt.expectedMatch, match)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalVolumeConditions(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input map[string]interface{}
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "Valid input",
|
||||
input: map[string]interface{}{
|
||||
"capacity": "1Gi,10Gi",
|
||||
"storageClass": []string{
|
||||
"gp2",
|
||||
"ebs-sc",
|
||||
},
|
||||
"csi": &csiVolumeSource{
|
||||
Driver: "aws.efs.csi.driver",
|
||||
},
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "Invalid input: invalid capacity filed name",
|
||||
input: map[string]interface{}{
|
||||
"Capacity": "1Gi,10Gi",
|
||||
},
|
||||
expectedError: "field Capacity not found",
|
||||
},
|
||||
{
|
||||
name: "Invalid input: invalid storage class format",
|
||||
input: map[string]interface{}{
|
||||
"storageClass": "ebs-sc",
|
||||
},
|
||||
expectedError: "str `ebs-sc` into []string",
|
||||
},
|
||||
{
|
||||
name: "Invalid input: invalid csi format",
|
||||
input: map[string]interface{}{
|
||||
"csi": "csi.driver",
|
||||
},
|
||||
expectedError: "str `csi.driver` into resourcepolicies.csiVolumeSource",
|
||||
},
|
||||
{
|
||||
name: "Invalid input: unknown field",
|
||||
input: map[string]interface{}{
|
||||
"unknown": "foo",
|
||||
},
|
||||
expectedError: "field unknown not found in type",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := unmarshalVolConditions(tc.input)
|
||||
if tc.expectedError != "" {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error '%s', but got nil", tc.expectedError)
|
||||
} else if !strings.Contains(err.Error(), tc.expectedError) {
|
||||
t.Errorf("Expected error '%s', but got '%v'", tc.expectedError, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePodVolume(t *testing.T) {
|
||||
// Mock data
|
||||
nfsVolume := corev1api.Volume{}
|
||||
nfsVolume.NFS = &corev1api.NFSVolumeSource{
|
||||
Server: "nfs.example.com",
|
||||
Path: "/exports/data",
|
||||
}
|
||||
csiVolume := corev1api.Volume{}
|
||||
csiVolume.CSI = &corev1api.CSIVolumeSource{
|
||||
Driver: "csi.example.com",
|
||||
}
|
||||
emptyVolume := corev1api.Volume{}
|
||||
|
||||
// Test cases
|
||||
testCases := []struct {
|
||||
name string
|
||||
inputVolume *corev1api.Volume
|
||||
expectedNFS *nFSVolumeSource
|
||||
expectedCSI *csiVolumeSource
|
||||
}{
|
||||
{
|
||||
name: "NFS volume",
|
||||
inputVolume: &nfsVolume,
|
||||
expectedNFS: &nFSVolumeSource{Server: "nfs.example.com", Path: "/exports/data"},
|
||||
},
|
||||
{
|
||||
name: "CSI volume",
|
||||
inputVolume: &csiVolume,
|
||||
expectedCSI: &csiVolumeSource{Driver: "csi.example.com"},
|
||||
},
|
||||
{
|
||||
name: "Empty volume",
|
||||
inputVolume: &emptyVolume,
|
||||
expectedNFS: nil,
|
||||
expectedCSI: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Call the function
|
||||
structuredVolume := &StructuredVolume{}
|
||||
structuredVolume.ParsePodVolume(tc.inputVolume)
|
||||
|
||||
// Check the results
|
||||
if tc.expectedNFS != nil {
|
||||
if structuredVolume.nfs == nil {
|
||||
t.Errorf("Expected a non-nil NFS volume source")
|
||||
} else if *tc.expectedNFS != *structuredVolume.nfs {
|
||||
t.Errorf("NFS volume source does not match expected value")
|
||||
}
|
||||
}
|
||||
if tc.expectedCSI != nil {
|
||||
if structuredVolume.csi == nil {
|
||||
t.Errorf("Expected a non-nil CSI volume source")
|
||||
} else if *tc.expectedCSI != *structuredVolume.csi {
|
||||
t.Errorf("CSI volume source does not match expected value")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePV(t *testing.T) {
|
||||
// Mock data
|
||||
nfsVolume := corev1api.PersistentVolume{}
|
||||
nfsVolume.Spec.Capacity = corev1api.ResourceList{corev1api.ResourceStorage: resource.MustParse("1Gi")}
|
||||
nfsVolume.Spec.NFS = &corev1api.NFSVolumeSource{Server: "nfs.example.com", Path: "/exports/data"}
|
||||
csiVolume := corev1api.PersistentVolume{}
|
||||
csiVolume.Spec.Capacity = corev1api.ResourceList{corev1api.ResourceStorage: resource.MustParse("2Gi")}
|
||||
csiVolume.Spec.CSI = &corev1api.CSIPersistentVolumeSource{Driver: "csi.example.com"}
|
||||
emptyVolume := corev1api.PersistentVolume{}
|
||||
|
||||
// Test cases
|
||||
testCases := []struct {
|
||||
name string
|
||||
inputVolume *corev1api.PersistentVolume
|
||||
expectedNFS *nFSVolumeSource
|
||||
expectedCSI *csiVolumeSource
|
||||
}{
|
||||
{
|
||||
name: "NFS volume",
|
||||
inputVolume: &nfsVolume,
|
||||
expectedNFS: &nFSVolumeSource{Server: "nfs.example.com", Path: "/exports/data"},
|
||||
expectedCSI: nil,
|
||||
},
|
||||
{
|
||||
name: "CSI volume",
|
||||
inputVolume: &csiVolume,
|
||||
expectedNFS: nil,
|
||||
expectedCSI: &csiVolumeSource{Driver: "csi.example.com"},
|
||||
},
|
||||
{
|
||||
name: "Empty volume",
|
||||
inputVolume: &emptyVolume,
|
||||
expectedNFS: nil,
|
||||
expectedCSI: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Call the function
|
||||
structuredVolume := &StructuredVolume{}
|
||||
structuredVolume.ParsePV(tc.inputVolume)
|
||||
// Check the results
|
||||
if structuredVolume.capacity != *tc.inputVolume.Spec.Capacity.Storage() {
|
||||
t.Errorf("Capacity does not match expected value")
|
||||
}
|
||||
if structuredVolume.storageClass != tc.inputVolume.Spec.StorageClassName {
|
||||
t.Errorf("Storage class does not match expected value")
|
||||
}
|
||||
if tc.expectedNFS != nil {
|
||||
if structuredVolume.nfs == nil {
|
||||
t.Errorf("Expected a non-nil NFS volume source")
|
||||
} else if *tc.expectedNFS != *structuredVolume.nfs {
|
||||
t.Errorf("NFS volume source does not match expected value")
|
||||
}
|
||||
}
|
||||
if tc.expectedCSI != nil {
|
||||
if structuredVolume.csi == nil {
|
||||
t.Errorf("Expected a non-nil CSI volume source")
|
||||
} else if *tc.expectedCSI != *structuredVolume.csi {
|
||||
t.Errorf("CSI volume source does not match expected value")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package resourcepolicies
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const currentSupportDataVersion = "v1"
|
||||
|
||||
type csiVolumeSource struct {
|
||||
Driver string `yaml:"driver,omitempty"`
|
||||
}
|
||||
|
||||
type nFSVolumeSource struct {
|
||||
// Server is the hostname or IP address of the NFS server
|
||||
Server string `yaml:"server,omitempty"`
|
||||
// Path is the exported NFS share
|
||||
Path string `yaml:"path,omitempty"`
|
||||
}
|
||||
|
||||
// volumeConditions defined the current format of conditions we parsed
|
||||
type volumeConditions struct {
|
||||
Capacity string `yaml:"capacity,omitempty"`
|
||||
StorageClass []string `yaml:"storageClass,omitempty"`
|
||||
NFS *nFSVolumeSource `yaml:"nfs,omitempty"`
|
||||
CSI *csiVolumeSource `yaml:"csi,omitempty"`
|
||||
}
|
||||
|
||||
func (c *capacityCondition) Validate() error {
|
||||
// [0, a]
|
||||
// [a, b]
|
||||
// [b, 0]
|
||||
// ==> low <= upper or upper is zero
|
||||
if (c.capacity.upper.Cmp(c.capacity.lower) >= 0) ||
|
||||
(!c.capacity.lower.IsZero() && c.capacity.upper.IsZero()) {
|
||||
return nil
|
||||
}
|
||||
return errors.Errorf("illegal values for capacity %v", c.capacity)
|
||||
|
||||
}
|
||||
|
||||
func (s *storageClassCondition) Validate() error {
|
||||
// validate by yamlv3
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *nfsCondition) Validate() error {
|
||||
// validate by yamlv3
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *csiCondition) Validate() error {
|
||||
// validate by yamlv3
|
||||
return nil
|
||||
}
|
||||
|
||||
// decodeStruct restric validate the keys in decoded mappings to exist as fields in the struct being decoded into
|
||||
func decodeStruct(r io.Reader, s interface{}) error {
|
||||
dec := yaml.NewDecoder(r)
|
||||
dec.KnownFields(true)
|
||||
return dec.Decode(s)
|
||||
}
|
||||
|
||||
// Validate check action format
|
||||
func (a *Action) Validate() error {
|
||||
// Validate Type
|
||||
if a.Type != Skip {
|
||||
return fmt.Errorf("invalid action type %s", a.Type)
|
||||
}
|
||||
|
||||
// TODO validate parameters
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,251 @@
|
|||
package resourcepolicies
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
)
|
||||
|
||||
func TestCapacityConditionValidate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
capacity *Capacity
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "lower and upper are both zero",
|
||||
capacity: &Capacity{lower: *resource.NewQuantity(0, resource.DecimalSI), upper: *resource.NewQuantity(0, resource.DecimalSI)},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "lower is zero and upper is greater than zero",
|
||||
capacity: &Capacity{lower: *resource.NewQuantity(0, resource.DecimalSI), upper: *resource.NewQuantity(100, resource.DecimalSI)},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "lower is greater than upper",
|
||||
capacity: &Capacity{lower: *resource.NewQuantity(100, resource.DecimalSI), upper: *resource.NewQuantity(50, resource.DecimalSI)},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "lower and upper are equal",
|
||||
capacity: &Capacity{lower: *resource.NewQuantity(100, resource.DecimalSI), upper: *resource.NewQuantity(100, resource.DecimalSI)},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "lower is greater than zero and upper is zero",
|
||||
capacity: &Capacity{lower: *resource.NewQuantity(100, resource.DecimalSI), upper: *resource.NewQuantity(0, resource.DecimalSI)},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "lower and upper are both not zero and lower is less than upper",
|
||||
capacity: &Capacity{lower: *resource.NewQuantity(100, resource.DecimalSI), upper: *resource.NewQuantity(200, resource.DecimalSI)},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "lower and upper are both not zero and lower is equal to upper",
|
||||
capacity: &Capacity{lower: *resource.NewQuantity(100, resource.DecimalSI), upper: *resource.NewQuantity(100, resource.DecimalSI)},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "lower and upper are both not zero and lower is greater than upper",
|
||||
capacity: &Capacity{lower: *resource.NewQuantity(200, resource.DecimalSI), upper: *resource.NewQuantity(100, resource.DecimalSI)},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c := &capacityCondition{capacity: *tc.capacity}
|
||||
err := c.Validate()
|
||||
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Fatalf("Expected error %v, but got error %v", tc.wantErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
res *resourcePolicies
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "unknown key in yaml",
|
||||
res: &resourcePolicies{
|
||||
Version: "v1",
|
||||
VolumePolicies: []VolumePolicy{
|
||||
{
|
||||
Action: Action{Type: "skip"},
|
||||
Conditions: map[string]interface{}{
|
||||
"capacity": "0,10Gi",
|
||||
"unknown": "",
|
||||
"storageClass": []string{"gp2", "ebs-sc"},
|
||||
"csi": interface{}(
|
||||
map[string]interface{}{
|
||||
"driver": "aws.efs.csi.driver",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "error format of capacity",
|
||||
res: &resourcePolicies{
|
||||
Version: "v1",
|
||||
VolumePolicies: []VolumePolicy{
|
||||
{
|
||||
Action: Action{Type: "skip"},
|
||||
Conditions: map[string]interface{}{
|
||||
"capacity": "10Gi",
|
||||
"storageClass": []string{"gp2", "ebs-sc"},
|
||||
"csi": interface{}(
|
||||
map[string]interface{}{
|
||||
"driver": "aws.efs.csi.driver",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "error format of storageClass",
|
||||
res: &resourcePolicies{
|
||||
Version: "v1",
|
||||
VolumePolicies: []VolumePolicy{
|
||||
{
|
||||
Action: Action{Type: "skip"},
|
||||
Conditions: map[string]interface{}{
|
||||
"capacity": "0,10Gi",
|
||||
"storageClass": "ebs-sc",
|
||||
"csi": interface{}(
|
||||
map[string]interface{}{
|
||||
"driver": "aws.efs.csi.driver",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "error format of csi",
|
||||
res: &resourcePolicies{
|
||||
Version: "v1",
|
||||
VolumePolicies: []VolumePolicy{
|
||||
{
|
||||
Action: Action{Type: "skip"},
|
||||
Conditions: map[string]interface{}{
|
||||
"capacity": "0,10Gi",
|
||||
"storageClass": []string{"gp2", "ebs-sc"},
|
||||
"csi": "aws.efs.csi.driver",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "unsupported version",
|
||||
res: &resourcePolicies{
|
||||
Version: "v2",
|
||||
VolumePolicies: []VolumePolicy{
|
||||
{
|
||||
Action: Action{Type: "skip"},
|
||||
Conditions: map[string]interface{}{
|
||||
"capacity": "0,10Gi",
|
||||
"csi": interface{}(
|
||||
map[string]interface{}{
|
||||
"driver": "aws.efs.csi.driver",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "unsupported action",
|
||||
res: &resourcePolicies{
|
||||
Version: "v1",
|
||||
VolumePolicies: []VolumePolicy{
|
||||
{
|
||||
Action: Action{Type: "unsupported"},
|
||||
Conditions: map[string]interface{}{
|
||||
"capacity": "0,10Gi",
|
||||
"csi": interface{}(
|
||||
map[string]interface{}{
|
||||
"driver": "aws.efs.csi.driver",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "error format of nfs",
|
||||
res: &resourcePolicies{
|
||||
Version: "v1",
|
||||
VolumePolicies: []VolumePolicy{
|
||||
{
|
||||
Action: Action{Type: "skip"},
|
||||
Conditions: map[string]interface{}{
|
||||
"capacity": "0,10Gi",
|
||||
"storageClass": []string{"gp2", "ebs-sc"},
|
||||
"nfs": "aws.efs.csi.driver",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "supported formart volume policies",
|
||||
res: &resourcePolicies{
|
||||
Version: "v1",
|
||||
VolumePolicies: []VolumePolicy{
|
||||
{
|
||||
Action: Action{Type: "skip"},
|
||||
Conditions: map[string]interface{}{
|
||||
"capacity": "0,10Gi",
|
||||
"storageClass": []string{"gp2", "ebs-sc"},
|
||||
"csi": interface{}(
|
||||
map[string]interface{}{
|
||||
"driver": "aws.efs.csi.driver",
|
||||
}),
|
||||
"nfs": interface{}(
|
||||
map[string]interface{}{
|
||||
"server": "192.168.20.90",
|
||||
"path": "/mnt/data/",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
policies := &Policies{}
|
||||
err1 := policies.buildPolicy(tc.res)
|
||||
err2 := policies.Validate()
|
||||
|
||||
if tc.wantErr {
|
||||
if err1 == nil && err2 == nil {
|
||||
t.Fatalf("Expected error %v, but not get error", tc.wantErr)
|
||||
}
|
||||
} else {
|
||||
if err1 != nil || err2 != nil {
|
||||
t.Fatalf("Expected error %v, but got error %v %v", tc.wantErr, err1, err2)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
package v1
|
||||
|
||||
import (
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
|
@ -159,6 +160,9 @@ type BackupSpec struct {
|
|||
// The default value is 1 hour.
|
||||
// +optional
|
||||
ItemOperationTimeout metav1.Duration `json:"itemOperationTimeout,omitempty"`
|
||||
// ResourcePolicies specifies the referenced resource policies that backup should follow
|
||||
// +optional
|
||||
ResourcePolicies *v1.TypedLocalObjectReference `json:"resourcePolices,omitempty"`
|
||||
}
|
||||
|
||||
// BackupHooks contains custom behaviors that should be executed at different phases of the backup.
|
||||
|
|
|
@ -371,6 +371,11 @@ func (in *BackupSpec) DeepCopyInto(out *BackupSpec) {
|
|||
}
|
||||
out.CSISnapshotTimeout = in.CSISnapshotTimeout
|
||||
out.ItemOperationTimeout = in.ItemOperationTimeout
|
||||
if in.ResourcePolicies != nil {
|
||||
in, out := &in.ResourcePolicies, &out.ResourcePolicies
|
||||
*out = new(corev1.TypedLocalObjectReference)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupSpec.
|
||||
|
|
|
@ -290,6 +290,7 @@ func (kb *kubernetesBackupper) BackupWithResolvers(log logrus.FieldLogger,
|
|||
backupRequest: backupRequest,
|
||||
tarWriter: tw,
|
||||
dynamicFactory: kb.dynamicFactory,
|
||||
kbClient: kb.kbClient,
|
||||
discoveryHelper: kb.discoveryHelper,
|
||||
podVolumeBackupper: podVolumeBackupper,
|
||||
podVolumeSnapshotTracker: newPVCSnapshotTracker(),
|
||||
|
@ -581,6 +582,7 @@ func (kb *kubernetesBackupper) FinalizeBackup(log logrus.FieldLogger,
|
|||
backupRequest: backupRequest,
|
||||
tarWriter: tw,
|
||||
dynamicFactory: kb.dynamicFactory,
|
||||
kbClient: kb.kbClient,
|
||||
discoveryHelper: kb.discoveryHelper,
|
||||
itemHookHandler: &hook.NoOpItemHookHandler{},
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@ import (
|
|||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/resourcepolicies"
|
||||
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/builder"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
|
@ -2882,7 +2883,7 @@ type fakePodVolumeBackupper struct{}
|
|||
|
||||
// BackupPodVolumes returns one pod volume backup per entry in volumes, with namespace "velero"
|
||||
// and name "pvb-<pod-namespace>-<pod-name>-<volume-name>".
|
||||
func (b *fakePodVolumeBackupper) BackupPodVolumes(backup *velerov1.Backup, pod *corev1.Pod, volumes []string, _ logrus.FieldLogger) ([]*velerov1.PodVolumeBackup, []error) {
|
||||
func (b *fakePodVolumeBackupper) BackupPodVolumes(backup *velerov1.Backup, pod *corev1.Pod, volumes []string, _ *resourcepolicies.Policies, _ logrus.FieldLogger) ([]*velerov1.PodVolumeBackup, []error) {
|
||||
var res []*velerov1.PodVolumeBackup
|
||||
|
||||
anno := pod.GetAnnotations()
|
||||
|
|
|
@ -18,6 +18,7 @@ package backup
|
|||
|
||||
import (
|
||||
"archive/tar"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
@ -36,7 +37,10 @@ import (
|
|||
kubeerrs "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
|
||||
kbClient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/hook"
|
||||
"github.com/vmware-tanzu/velero/internal/resourcepolicies"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/archive"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
|
@ -61,6 +65,7 @@ type itemBackupper struct {
|
|||
backupRequest *Request
|
||||
tarWriter tarWriter
|
||||
dynamicFactory client.DynamicFactory
|
||||
kbClient kbClient.Client
|
||||
discoveryHelper discovery.Helper
|
||||
podVolumeBackupper podvolume.Backupper
|
||||
podVolumeSnapshotTracker *pvcSnapshotTracker
|
||||
|
@ -222,6 +227,7 @@ func (ib *itemBackupper) backupItemInternal(logger logrus.FieldLogger, obj runti
|
|||
}
|
||||
return false, itemFiles, kubeerrs.NewAggregate(backupErrs)
|
||||
}
|
||||
|
||||
itemFiles = append(itemFiles, additionalItemFiles...)
|
||||
obj = updatedObj
|
||||
if metadata, err = meta.Accessor(obj); err != nil {
|
||||
|
@ -300,7 +306,7 @@ func (ib *itemBackupper) backupPodVolumes(log logrus.FieldLogger, pod *corev1api
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
return ib.podVolumeBackupper.BackupPodVolumes(ib.backupRequest.Backup, pod, volumes, log)
|
||||
return ib.podVolumeBackupper.BackupPodVolumes(ib.backupRequest.Backup, pod, volumes, ib.backupRequest.ResPolicies, log)
|
||||
}
|
||||
|
||||
func (ib *itemBackupper) executeActions(
|
||||
|
@ -318,7 +324,15 @@ func (ib *itemBackupper) executeActions(
|
|||
}
|
||||
log.Info("Executing custom action")
|
||||
|
||||
if act, err := ib.checkResourcePolicies(obj, &groupResource, action.Name()); err != nil {
|
||||
return nil, itemFiles, errors.WithStack(err)
|
||||
} else if act != nil && act.Type == resourcepolicies.Skip {
|
||||
log.Infof("skip snapshot of pvc %s/%s bound pv for the matched resource policies", namespace, name)
|
||||
continue
|
||||
}
|
||||
|
||||
updatedItem, additionalItemIdentifiers, operationID, postOperationItems, err := action.Execute(obj, ib.backupRequest.Backup)
|
||||
|
||||
if err != nil {
|
||||
return nil, itemFiles, errors.Wrapf(err, "error executing custom action (groupResource=%s, namespace=%s, name=%s)", groupResource.String(), namespace, name)
|
||||
}
|
||||
|
@ -469,6 +483,16 @@ func (ib *itemBackupper) takePVSnapshot(obj runtime.Unstructured, log logrus.Fie
|
|||
return nil
|
||||
}
|
||||
|
||||
if ib.backupRequest.ResPolicies != nil {
|
||||
structuredVolume := &resourcepolicies.StructuredVolume{}
|
||||
structuredVolume.ParsePV(pv)
|
||||
action := ib.backupRequest.ResPolicies.Match(structuredVolume)
|
||||
if action != nil && action.Type == resourcepolicies.Skip {
|
||||
log.Infof("skip snapshot of pv %s for the matched resource policies", pv.Name)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: -- once failure-domain.beta.kubernetes.io/zone is no longer
|
||||
// supported in any velero-supported version of Kubernetes, remove fallback checking of it
|
||||
pvFailureDomainZone, labelFound := pv.Labels[zoneLabel]
|
||||
|
@ -555,6 +579,30 @@ func (ib *itemBackupper) takePVSnapshot(obj runtime.Unstructured, log logrus.Fie
|
|||
return kubeerrs.NewAggregate(errs)
|
||||
}
|
||||
|
||||
func (ib *itemBackupper) checkResourcePolicies(obj runtime.Unstructured, groupResource *schema.GroupResource, actionName string) (*resourcepolicies.Action, error) {
|
||||
if ib.backupRequest.ResPolicies != nil && groupResource.String() == "persistentvolumeclaims" && actionName == "velero.io/csi-pvc-backupper" {
|
||||
pvc := corev1api.PersistentVolumeClaim{}
|
||||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), &pvc); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
pvName := pvc.Spec.VolumeName
|
||||
if pvName == "" {
|
||||
return nil, errors.Errorf("PVC has no volume backing this claim")
|
||||
}
|
||||
|
||||
pv := &corev1api.PersistentVolume{}
|
||||
if err := ib.kbClient.Get(context.Background(), kbClient.ObjectKey{Name: pvName}, pv); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
volume := resourcepolicies.StructuredVolume{}
|
||||
volume.ParsePV(pv)
|
||||
return ib.backupRequest.ResPolicies.Match(&volume), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func volumeSnapshot(backup *velerov1api.Backup, volumeName, volumeID, volumeType, az, location string, iops *int64) *volume.Snapshot {
|
||||
return &volume.Snapshot{
|
||||
Spec: volume.SnapshotSpec{
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/hook"
|
||||
"github.com/vmware-tanzu/velero/internal/resourcepolicies"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/itemoperation"
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/framework"
|
||||
|
@ -52,6 +53,7 @@ type Request struct {
|
|||
BackedUpItems map[itemKey]struct{}
|
||||
CSISnapshots []snapshotv1api.VolumeSnapshot
|
||||
itemOperationsList *[]*itemoperation.BackupOperation
|
||||
ResPolicies *resourcepolicies.Policies
|
||||
}
|
||||
|
||||
// GetItemOperationsList returns ItemOperationsList, initializing it if necessary
|
||||
|
|
|
@ -20,8 +20,10 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/resourcepolicies"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
@ -123,6 +125,10 @@ func (b *BackupBuilder) FromSchedule(schedule *velerov1api.Schedule) *BackupBuil
|
|||
})
|
||||
}
|
||||
|
||||
if schedule.Spec.Template.ResourcePolicies != nil {
|
||||
b.ResourcePolicies(schedule.Spec.Template.ResourcePolicies.Name)
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
|
@ -275,3 +281,9 @@ func (b *BackupBuilder) ItemOperationTimeout(timeout time.Duration) *BackupBuild
|
|||
b.object.Spec.ItemOperationTimeout.Duration = timeout
|
||||
return b
|
||||
}
|
||||
|
||||
// resourcePolicies sets the Backup's resource polices.
|
||||
func (b *BackupBuilder) ResourcePolicies(name string) *BackupBuilder {
|
||||
b.object.Spec.ResourcePolicies = &v1.TypedLocalObjectReference{Kind: resourcepolicies.ConfigmapRefType, Name: name}
|
||||
return b
|
||||
}
|
||||
|
|
|
@ -104,6 +104,7 @@ type CreateOptions struct {
|
|||
OrderedResources string
|
||||
CSISnapshotTimeout time.Duration
|
||||
ItemOperationTimeout time.Duration
|
||||
ResPoliciesConfigmap string
|
||||
client veleroclient.Interface
|
||||
}
|
||||
|
||||
|
@ -143,6 +144,8 @@ func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) {
|
|||
|
||||
f = flags.VarPF(&o.DefaultVolumesToFsBackup, "default-volumes-to-fs-backup", "", "Use pod volume file system backup by default for volumes")
|
||||
f.NoOptDefVal = "true"
|
||||
|
||||
flags.StringVar(&o.ResPoliciesConfigmap, "resource-policies-configmap", "", "Reference to the resource policies configmap that backup using")
|
||||
}
|
||||
|
||||
// BindWait binds the wait flag separately so it is not called by other create
|
||||
|
@ -374,6 +377,9 @@ func (o *CreateOptions) BuildBackup(namespace string) (*velerov1api.Backup, erro
|
|||
if o.DefaultVolumesToFsBackup.Value != nil {
|
||||
backupBuilder.DefaultVolumesToFsBackup(*o.DefaultVolumesToFsBackup.Value)
|
||||
}
|
||||
if o.ResPoliciesConfigmap != "" {
|
||||
backupBuilder.ResourcePolicies(o.ResPoliciesConfigmap)
|
||||
}
|
||||
}
|
||||
|
||||
backup := backupBuilder.ObjectMeta(builder.WithLabelsMap(o.Labels.Data())).Result()
|
||||
|
|
|
@ -23,8 +23,10 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/resourcepolicies"
|
||||
api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd"
|
||||
|
@ -158,6 +160,10 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error {
|
|||
},
|
||||
}
|
||||
|
||||
if o.BackupOptions.ResPoliciesConfigmap != "" {
|
||||
schedule.Spec.Template.ResourcePolicies = &v1.TypedLocalObjectReference{Kind: resourcepolicies.ConfigmapRefType, Name: o.BackupOptions.ResPoliciesConfigmap}
|
||||
}
|
||||
|
||||
if printed, err := output.PrintWithFormat(c, schedule); printed || err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ import (
|
|||
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/credentials"
|
||||
"github.com/vmware-tanzu/velero/internal/resourcepolicies"
|
||||
"github.com/vmware-tanzu/velero/internal/storage"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
pkgbackup "github.com/vmware-tanzu/velero/pkg/backup"
|
||||
|
@ -459,6 +460,21 @@ func (b *backupReconciler) prepareBackupRequest(backup *velerov1api.Backup, logg
|
|||
request.Status.ValidationErrors = append(request.Status.ValidationErrors, fmt.Sprintf("encountered labelSelector as well as orLabelSelectors in backup spec, only one can be specified"))
|
||||
}
|
||||
|
||||
if request.Spec.ResourcePolicies != nil && request.Spec.ResourcePolicies.Kind == resourcepolicies.ConfigmapRefType {
|
||||
policiesConfigmap := &v1.ConfigMap{}
|
||||
err := b.kbClient.Get(context.Background(), kbclient.ObjectKey{Namespace: request.Namespace, Name: request.Spec.ResourcePolicies.Name}, policiesConfigmap)
|
||||
if err != nil {
|
||||
request.Status.ValidationErrors = append(request.Status.ValidationErrors, fmt.Sprintf("failed to get resource policies %s/%s configmap with err %v", request.Namespace, request.Spec.ResourcePolicies.Name, err))
|
||||
}
|
||||
res, err := resourcepolicies.GetResourcePoliciesFromConfig(policiesConfigmap)
|
||||
if err != nil {
|
||||
request.Status.ValidationErrors = append(request.Status.ValidationErrors, errors.Wrapf(err, fmt.Sprintf("resource policies %s/%s", request.Namespace, request.Spec.ResourcePolicies.Name)).Error())
|
||||
} else if err = res.Validate(); err != nil {
|
||||
request.Status.ValidationErrors = append(request.Status.ValidationErrors, errors.Wrapf(err, fmt.Sprintf("resource policies %s/%s", request.Namespace, request.Spec.ResourcePolicies.Name)).Error())
|
||||
}
|
||||
request.ResPolicies = res
|
||||
}
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
|
|
|
@ -258,10 +258,8 @@ func getNextRunTime(schedule *velerov1.Schedule, cronSchedule cron.Schedule, asO
|
|||
|
||||
func getBackup(item *velerov1.Schedule, timestamp time.Time) *velerov1.Backup {
|
||||
name := item.TimestampedName(timestamp)
|
||||
backup := builder.
|
||||
return builder.
|
||||
ForBackup(item.Namespace, name).
|
||||
FromSchedule(item).
|
||||
Result()
|
||||
|
||||
return backup
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import (
|
|||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/resourcepolicies"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
clientset "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned"
|
||||
"github.com/vmware-tanzu/velero/pkg/label"
|
||||
|
@ -41,7 +42,7 @@ import (
|
|||
// Backupper can execute pod volume backups of volumes in a pod.
|
||||
type Backupper interface {
|
||||
// BackupPodVolumes backs up all specified volumes in a pod.
|
||||
BackupPodVolumes(backup *velerov1api.Backup, pod *corev1api.Pod, volumesToBackup []string, log logrus.FieldLogger) ([]*velerov1api.PodVolumeBackup, []error)
|
||||
BackupPodVolumes(backup *velerov1api.Backup, pod *corev1api.Pod, volumesToBackup []string, resPolicies *resourcepolicies.Policies, log logrus.FieldLogger) ([]*velerov1api.PodVolumeBackup, []error)
|
||||
}
|
||||
|
||||
type backupper struct {
|
||||
|
@ -110,7 +111,23 @@ func resultsKey(ns, name string) string {
|
|||
return fmt.Sprintf("%s/%s", ns, name)
|
||||
}
|
||||
|
||||
func (b *backupper) BackupPodVolumes(backup *velerov1api.Backup, pod *corev1api.Pod, volumesToBackup []string, log logrus.FieldLogger) ([]*velerov1api.PodVolumeBackup, []error) {
|
||||
func (b *backupper) checkResourcePolicies(resPolicies *resourcepolicies.Policies, pvc *corev1api.PersistentVolumeClaim, volume *corev1api.Volume) (*resourcepolicies.Action, error) {
|
||||
structuredVolume := &resourcepolicies.StructuredVolume{}
|
||||
if pvc != nil {
|
||||
pv, err := b.pvClient.PersistentVolumes().Get(context.TODO(), pvc.Spec.VolumeName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error getting pv for pvc %s", pvc.Spec.VolumeName)
|
||||
}
|
||||
structuredVolume.ParsePV(pv)
|
||||
} else if volume != nil {
|
||||
structuredVolume.ParsePodVolume(volume)
|
||||
} else {
|
||||
return nil, errors.Errorf("failed to check resource policies for empty volume")
|
||||
}
|
||||
return resPolicies.Match(structuredVolume), nil
|
||||
}
|
||||
|
||||
func (b *backupper) BackupPodVolumes(backup *velerov1api.Backup, pod *corev1api.Pod, volumesToBackup []string, resPolicies *resourcepolicies.Policies, log logrus.FieldLogger) ([]*velerov1api.PodVolumeBackup, []error) {
|
||||
if len(volumesToBackup) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -201,6 +218,16 @@ func (b *backupper) BackupPodVolumes(backup *velerov1api.Backup, pod *corev1api.
|
|||
continue
|
||||
}
|
||||
|
||||
if resPolicies != nil {
|
||||
if action, err := b.checkResourcePolicies(resPolicies, pvc, &volume); err != nil {
|
||||
errs = append(errs, errors.Wrapf(err, "error getting pv for pvc %s", pvc.Spec.VolumeName))
|
||||
continue
|
||||
} else if action != nil && action.Type == resourcepolicies.Skip {
|
||||
log.Infof("skip backup of volume %s for the matched resource policies", volumeName)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
volumeBackup := newPodVolumeBackup(backup, pod, volume, repo.Spec.ResticIdentifier, b.uploaderType, pvc)
|
||||
if volumeBackup, err = b.veleroClient.VeleroV1().PodVolumeBackups(volumeBackup.Namespace).Create(context.TODO(), volumeBackup, metav1.CreateOptions{}); err != nil {
|
||||
errs = append(errs, err)
|
||||
|
|
Loading…
Reference in New Issue