mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-09-29 11:16:08 +00:00
Merge pull request #6452 from anshulahuja98/resourcemodifier
Add support for ResourceModifier (AKA Json Substitutions) in restore flow
This commit is contained in:
@@ -53,3 +53,4 @@ tilt-resources/cloud
|
||||
# test generated files
|
||||
test/e2e/report.xml
|
||||
coverage.out
|
||||
__debug_bin*
|
||||
@@ -0,0 +1 @@
|
||||
Add support for resource Modifications in the restore flow. Also known as JSON Substitutions.
|
||||
@@ -359,6 +359,28 @@ spec:
|
||||
from backup.
|
||||
nullable: true
|
||||
type: boolean
|
||||
resourceModifier:
|
||||
description: ResourceModifier specifies the reference to JSON resource
|
||||
patches that should be applied to resources before restoration.
|
||||
nullable: true
|
||||
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
|
||||
x-kubernetes-map-type: atomic
|
||||
restorePVs:
|
||||
description: RestorePVs specifies whether to restore all included
|
||||
PVs from snapshot
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,164 @@
|
||||
package resourcemodifiers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/util/collections"
|
||||
)
|
||||
|
||||
const (
|
||||
ConfigmapRefType = "configmap"
|
||||
ResourceModifierSupportedVersionV1 = "v1"
|
||||
)
|
||||
|
||||
type JSONPatch struct {
|
||||
Operation string `yaml:"operation"`
|
||||
From string `yaml:"from,omitempty"`
|
||||
Path string `yaml:"path"`
|
||||
Value string `yaml:"value,omitempty"`
|
||||
}
|
||||
|
||||
type Conditions struct {
|
||||
Namespaces []string `yaml:"namespaces,omitempty"`
|
||||
GroupKind string `yaml:"groupKind"`
|
||||
ResourceNameRegex string `yaml:"resourceNameRegex"`
|
||||
}
|
||||
|
||||
type ResourceModifierRule struct {
|
||||
Conditions Conditions `yaml:"conditions"`
|
||||
Patches []JSONPatch `yaml:"patches"`
|
||||
}
|
||||
|
||||
type ResourceModifiers struct {
|
||||
Version string `yaml:"version"`
|
||||
ResourceModifierRules []ResourceModifierRule `yaml:"resourceModifierRules"`
|
||||
}
|
||||
|
||||
func GetResourceModifiersFromConfig(cm *v1.ConfigMap) (*ResourceModifiers, 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 modifiers %s/%s configmap", cm.Name, cm.Namespace)
|
||||
}
|
||||
|
||||
var yamlData string
|
||||
for _, v := range cm.Data {
|
||||
yamlData = v
|
||||
}
|
||||
|
||||
resModifiers, err := unmarshalResourceModifiers(&yamlData)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return resModifiers, nil
|
||||
}
|
||||
|
||||
func (p *ResourceModifiers) ApplyResourceModifierRules(obj *unstructured.Unstructured, groupResource string, log logrus.FieldLogger) []error {
|
||||
var errs []error
|
||||
for _, rule := range p.ResourceModifierRules {
|
||||
err := rule.Apply(obj, groupResource, log)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (r *ResourceModifierRule) Apply(obj *unstructured.Unstructured, groupResource string, log logrus.FieldLogger) error {
|
||||
namespaceInclusion := collections.NewIncludesExcludes().Includes(r.Conditions.Namespaces...)
|
||||
if !namespaceInclusion.ShouldInclude(obj.GetNamespace()) {
|
||||
return nil
|
||||
}
|
||||
if !strings.EqualFold(groupResource, r.Conditions.GroupKind) {
|
||||
return nil
|
||||
}
|
||||
if r.Conditions.ResourceNameRegex != "" {
|
||||
match, err := regexp.MatchString(r.Conditions.ResourceNameRegex, obj.GetName())
|
||||
if err != nil {
|
||||
return errors.Errorf("error in matching regex %s", err.Error())
|
||||
}
|
||||
if !match {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
patches, err := r.PatchArrayToByteArray()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("Applying resource modifier patch on %s/%s", obj.GetNamespace(), obj.GetName())
|
||||
err = ApplyPatch(patches, obj, log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// convert all JsonPatch to string array with the format of jsonpatch.Patch and then convert it to byte array
|
||||
func (r *ResourceModifierRule) PatchArrayToByteArray() ([]byte, error) {
|
||||
var patches []string
|
||||
for _, patch := range r.Patches {
|
||||
patches = append(patches, patch.ToString())
|
||||
}
|
||||
patchesStr := strings.Join(patches, ",\n\t")
|
||||
return []byte(fmt.Sprintf(`[%s]`, patchesStr)), nil
|
||||
}
|
||||
|
||||
func (p *JSONPatch) ToString() string {
|
||||
if strings.Contains(p.Value, "\"") {
|
||||
return fmt.Sprintf(`{"op": "%s", "from": "%s", "path": "%s", "value": %s}`, p.Operation, p.From, p.Path, p.Value)
|
||||
}
|
||||
return fmt.Sprintf(`{"op": "%s", "from": "%s", "path": "%s", "value": "%s"}`, p.Operation, p.From, p.Path, p.Value)
|
||||
}
|
||||
|
||||
func ApplyPatch(patch []byte, obj *unstructured.Unstructured, log logrus.FieldLogger) error {
|
||||
jsonPatch, err := jsonpatch.DecodePatch(patch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error in decoding json patch %s", err.Error())
|
||||
}
|
||||
objBytes, err := obj.MarshalJSON()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error in marshaling object %s", err.Error())
|
||||
}
|
||||
modifiedObjBytes, err := jsonPatch.Apply(objBytes)
|
||||
if err != nil {
|
||||
if errors.Is(err, jsonpatch.ErrTestFailed) {
|
||||
log.Infof("Test operation failed for JSON Patch %s", err.Error())
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("error in applying JSON Patch %s", err.Error())
|
||||
}
|
||||
err = obj.UnmarshalJSON(modifiedObjBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error in unmarshalling modified object %s", err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmarshalResourceModifiers(yamlData *string) (*ResourceModifiers, error) {
|
||||
resModifiers := &ResourceModifiers{}
|
||||
err := decodeStruct(strings.NewReader(*yamlData), resModifiers)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode yaml data into resource modifiers %v", err)
|
||||
}
|
||||
return resModifiers, nil
|
||||
}
|
||||
|
||||
// decodeStruct restrict 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)
|
||||
}
|
||||
@@ -0,0 +1,677 @@
|
||||
package resourcemodifiers
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
func TestGetResourceModifiersFromConfig(t *testing.T) {
|
||||
cm1 := &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-configmap",
|
||||
Namespace: "test-namespace",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"sub.yml": "version: v1\nresourceModifierRules:\n- conditions:\n groupKind: persistentvolumeclaims\n resourceNameRegex: \".*\"\n namespaces:\n - bar\n - foo\n patches:\n - operation: replace\n path: \"/spec/storageClassName\"\n value: \"premium\"\n - operation: remove\n path: \"/metadata/labels/test\"\n\n\n",
|
||||
},
|
||||
}
|
||||
|
||||
rules1 := &ResourceModifiers{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupKind: "persistentvolumeclaims",
|
||||
ResourceNameRegex: ".*",
|
||||
Namespaces: []string{"bar", "foo"},
|
||||
},
|
||||
Patches: []JSONPatch{
|
||||
{
|
||||
Operation: "replace",
|
||||
Path: "/spec/storageClassName",
|
||||
Value: "premium",
|
||||
},
|
||||
{
|
||||
Operation: "remove",
|
||||
Path: "/metadata/labels/test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
cm2 := &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-configmap",
|
||||
Namespace: "test-namespace",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"sub.yml": "version: v1\nresourceModifierRules:\n- conditions:\n groupKind: deployments.apps\n resourceNameRegex: \"^test-.*$\"\n namespaces:\n - bar\n - foo\n patches:\n - operation: add\n path: \"/spec/template/spec/containers/0\"\n value: \"{\\\"name\\\": \\\"nginx\\\", \\\"image\\\": \\\"nginx:1.14.2\\\", \\\"ports\\\": [{\\\"containerPort\\\": 80}]}\"\n - operation: copy\n from: \"/spec/template/spec/containers/0\"\n path: \"/spec/template/spec/containers/1\"\n\n\n",
|
||||
},
|
||||
}
|
||||
|
||||
rules2 := &ResourceModifiers{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupKind: "deployments.apps",
|
||||
ResourceNameRegex: "^test-.*$",
|
||||
Namespaces: []string{"bar", "foo"},
|
||||
},
|
||||
Patches: []JSONPatch{
|
||||
{
|
||||
Operation: "add",
|
||||
Path: "/spec/template/spec/containers/0",
|
||||
Value: `{"name": "nginx", "image": "nginx:1.14.2", "ports": [{"containerPort": 80}]}`,
|
||||
},
|
||||
{
|
||||
Operation: "copy",
|
||||
From: "/spec/template/spec/containers/0",
|
||||
Path: "/spec/template/spec/containers/1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cm3 := &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-configmap",
|
||||
Namespace: "test-namespace",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"sub.yml": "version1: v1\nresourceModifierRules:\n- conditions:\n groupKind: deployments.apps\n resourceNameRegex: \"^test-.*$\"\n namespaces:\n - bar\n - foo\n patches:\n - operation: add\n path: \"/spec/template/spec/containers/0\"\n value: \"{\\\"name\\\": \\\"nginx\\\", \\\"image\\\": \\\"nginx:1.14.2\\\", \\\"ports\\\": [{\\\"containerPort\\\": 80}]}\"\n - operation: copy\n from: \"/spec/template/spec/containers/0\"\n path: \"/spec/template/spec/containers/1\"\n\n\n",
|
||||
},
|
||||
}
|
||||
|
||||
type args struct {
|
||||
cm *v1.ConfigMap
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *ResourceModifiers
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "test 1",
|
||||
args: args{
|
||||
cm: cm1,
|
||||
},
|
||||
want: rules1,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "complex payload in add and copy operator",
|
||||
args: args{
|
||||
cm: cm2,
|
||||
},
|
||||
want: rules2,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid payload version1",
|
||||
args: args{
|
||||
cm: cm3,
|
||||
},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "nil configmap",
|
||||
args: args{
|
||||
cm: nil,
|
||||
},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := GetResourceModifiersFromConfig(tt.args.cm)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetResourceModifiersFromConfig() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("GetResourceModifiersFromConfig() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceModifiers_ApplyResourceModifierRules(t *testing.T) {
|
||||
|
||||
pvcStandardSc := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "PersistentVolumeClaim",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "test-pvc",
|
||||
"namespace": "foo",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"storageClassName": "standard",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
pvcPremiumSc := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "PersistentVolumeClaim",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "test-pvc",
|
||||
"namespace": "foo",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"storageClassName": "premium",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
deployNginxOneReplica := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "test-deployment",
|
||||
"namespace": "foo",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"replicas": "1",
|
||||
"template": map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"labels": map[string]interface{}{
|
||||
"app": "nginx",
|
||||
},
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"containers": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "nginx",
|
||||
"image": "nginx:latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
deployNginxTwoReplica := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "test-deployment",
|
||||
"namespace": "foo",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"replicas": "2",
|
||||
"template": map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"labels": map[string]interface{}{
|
||||
"app": "nginx",
|
||||
},
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"containers": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "nginx",
|
||||
"image": "nginx:latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
deployNginxMysql := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "test-deployment",
|
||||
"namespace": "foo",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"replicas": "1",
|
||||
"template": map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"labels": map[string]interface{}{
|
||||
"app": "nginx",
|
||||
},
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"containers": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "nginx",
|
||||
"image": "nginx:latest",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"name": "mysql",
|
||||
"image": "mysql:latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type fields struct {
|
||||
Version string
|
||||
ResourceModifierRules []ResourceModifierRule
|
||||
}
|
||||
type args struct {
|
||||
obj *unstructured.Unstructured
|
||||
groupResource string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
wantObj *unstructured.Unstructured
|
||||
}{
|
||||
{
|
||||
name: "Invalid Regex throws error",
|
||||
fields: fields{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupKind: "persistentvolumeclaims",
|
||||
ResourceNameRegex: "[a-z",
|
||||
Namespaces: []string{"foo"},
|
||||
},
|
||||
Patches: []JSONPatch{
|
||||
{
|
||||
Operation: "test",
|
||||
Path: "/spec/storageClassName",
|
||||
Value: "standard",
|
||||
},
|
||||
{
|
||||
Operation: "replace",
|
||||
Path: "/spec/storageClassName",
|
||||
Value: "premium",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
obj: pvcStandardSc.DeepCopy(),
|
||||
groupResource: "persistentvolumeclaims",
|
||||
},
|
||||
wantErr: true,
|
||||
wantObj: pvcStandardSc.DeepCopy(),
|
||||
},
|
||||
{
|
||||
name: "pvc with standard storage class should be patched to premium",
|
||||
fields: fields{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupKind: "persistentvolumeclaims",
|
||||
ResourceNameRegex: ".*",
|
||||
Namespaces: []string{"foo"},
|
||||
},
|
||||
Patches: []JSONPatch{
|
||||
{
|
||||
Operation: "test",
|
||||
Path: "/spec/storageClassName",
|
||||
Value: "standard",
|
||||
},
|
||||
{
|
||||
Operation: "replace",
|
||||
Path: "/spec/storageClassName",
|
||||
Value: "premium",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
obj: pvcStandardSc.DeepCopy(),
|
||||
groupResource: "persistentvolumeclaims",
|
||||
},
|
||||
wantErr: false,
|
||||
wantObj: pvcPremiumSc.DeepCopy(),
|
||||
},
|
||||
{
|
||||
name: "nginx deployment: 1 -> 2 replicas",
|
||||
fields: fields{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupKind: "deployments.apps",
|
||||
ResourceNameRegex: "^test-.*$",
|
||||
Namespaces: []string{"foo"},
|
||||
},
|
||||
Patches: []JSONPatch{
|
||||
{
|
||||
Operation: "test",
|
||||
Path: "/spec/replicas",
|
||||
Value: "1",
|
||||
},
|
||||
{
|
||||
Operation: "replace",
|
||||
Path: "/spec/replicas",
|
||||
Value: "2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
obj: deployNginxOneReplica.DeepCopy(),
|
||||
groupResource: "deployments.apps",
|
||||
},
|
||||
wantErr: false,
|
||||
wantObj: deployNginxTwoReplica.DeepCopy(),
|
||||
},
|
||||
{
|
||||
name: "nginx deployment: test operator fails, skips substitution, no error",
|
||||
fields: fields{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupKind: "deployments.apps",
|
||||
ResourceNameRegex: "^test-.*$",
|
||||
Namespaces: []string{"foo"},
|
||||
},
|
||||
Patches: []JSONPatch{
|
||||
{
|
||||
Operation: "test",
|
||||
Path: "/spec/replicas",
|
||||
Value: "5",
|
||||
},
|
||||
{
|
||||
Operation: "replace",
|
||||
Path: "/spec/replicas",
|
||||
Value: "2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
obj: deployNginxOneReplica.DeepCopy(),
|
||||
groupResource: "deployments.apps",
|
||||
},
|
||||
wantErr: false,
|
||||
wantObj: deployNginxOneReplica.DeepCopy(),
|
||||
},
|
||||
{
|
||||
name: "nginx deployment: Empty Resource Regex",
|
||||
fields: fields{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupKind: "deployments.apps",
|
||||
Namespaces: []string{"foo"},
|
||||
},
|
||||
Patches: []JSONPatch{
|
||||
{
|
||||
Operation: "test",
|
||||
Path: "/spec/replicas",
|
||||
Value: "1",
|
||||
},
|
||||
{
|
||||
Operation: "replace",
|
||||
Path: "/spec/replicas",
|
||||
Value: "2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
obj: deployNginxOneReplica.DeepCopy(),
|
||||
groupResource: "deployments.apps",
|
||||
},
|
||||
wantErr: false,
|
||||
wantObj: deployNginxTwoReplica.DeepCopy(),
|
||||
},
|
||||
{
|
||||
name: "nginx deployment: Empty Resource Regex and namespaces list",
|
||||
fields: fields{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupKind: "deployments.apps",
|
||||
},
|
||||
Patches: []JSONPatch{
|
||||
{
|
||||
Operation: "test",
|
||||
Path: "/spec/replicas",
|
||||
Value: "1",
|
||||
},
|
||||
{
|
||||
Operation: "replace",
|
||||
Path: "/spec/replicas",
|
||||
Value: "2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
obj: deployNginxOneReplica.DeepCopy(),
|
||||
groupResource: "deployments.apps",
|
||||
},
|
||||
wantErr: false,
|
||||
wantObj: deployNginxTwoReplica.DeepCopy(),
|
||||
},
|
||||
{
|
||||
name: "nginx deployment: namespace doesn't match",
|
||||
fields: fields{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupKind: "deployments.apps",
|
||||
ResourceNameRegex: ".*",
|
||||
Namespaces: []string{"bar"},
|
||||
},
|
||||
Patches: []JSONPatch{
|
||||
{
|
||||
Operation: "test",
|
||||
Path: "/spec/replicas",
|
||||
Value: "1",
|
||||
},
|
||||
{
|
||||
Operation: "replace",
|
||||
Path: "/spec/replicas",
|
||||
Value: "2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
obj: deployNginxOneReplica.DeepCopy(),
|
||||
groupResource: "deployments.apps",
|
||||
},
|
||||
wantErr: false,
|
||||
wantObj: deployNginxOneReplica.DeepCopy(),
|
||||
},
|
||||
{
|
||||
name: "add container mysql to deployment",
|
||||
fields: fields{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupKind: "deployments.apps",
|
||||
ResourceNameRegex: "^test-.*$",
|
||||
Namespaces: []string{"foo"},
|
||||
},
|
||||
Patches: []JSONPatch{
|
||||
{
|
||||
Operation: "add",
|
||||
Path: "/spec/template/spec/containers/1",
|
||||
Value: `{"name": "mysql", "image": "mysql:latest"}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
obj: deployNginxOneReplica,
|
||||
groupResource: "deployments.apps",
|
||||
},
|
||||
wantErr: false,
|
||||
wantObj: deployNginxMysql,
|
||||
},
|
||||
{
|
||||
name: "Copy container 0 to container 1 and then modify container 1",
|
||||
fields: fields{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupKind: "deployments.apps",
|
||||
ResourceNameRegex: "^test-.*$",
|
||||
Namespaces: []string{"foo"},
|
||||
},
|
||||
Patches: []JSONPatch{
|
||||
{
|
||||
Operation: "copy",
|
||||
From: "/spec/template/spec/containers/0",
|
||||
Path: "/spec/template/spec/containers/1",
|
||||
},
|
||||
{
|
||||
Operation: "test",
|
||||
Path: "/spec/template/spec/containers/1/image",
|
||||
Value: "nginx:latest",
|
||||
},
|
||||
{
|
||||
Operation: "replace",
|
||||
Path: "/spec/template/spec/containers/1/name",
|
||||
Value: "mysql",
|
||||
},
|
||||
{
|
||||
Operation: "replace",
|
||||
Path: "/spec/template/spec/containers/1/image",
|
||||
Value: "mysql:latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
obj: deployNginxOneReplica.DeepCopy(),
|
||||
groupResource: "deployments.apps",
|
||||
},
|
||||
wantErr: false,
|
||||
wantObj: deployNginxMysql.DeepCopy(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := &ResourceModifiers{
|
||||
Version: tt.fields.Version,
|
||||
ResourceModifierRules: tt.fields.ResourceModifierRules,
|
||||
}
|
||||
got := p.ApplyResourceModifierRules(tt.args.obj, tt.args.groupResource, logrus.New())
|
||||
|
||||
assert.Equal(t, tt.wantErr, len(got) > 0)
|
||||
assert.Equal(t, *tt.wantObj, *tt.args.obj)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONPatch_ToString(t *testing.T) {
|
||||
type fields struct {
|
||||
Operation string
|
||||
From string
|
||||
Path string
|
||||
Value string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "test",
|
||||
fields: fields{
|
||||
Operation: "test",
|
||||
Path: "/spec/replicas",
|
||||
Value: "1",
|
||||
},
|
||||
want: `{"op": "test", "from": "", "path": "/spec/replicas", "value": "1"}`,
|
||||
},
|
||||
{
|
||||
name: "replace",
|
||||
fields: fields{
|
||||
Operation: "replace",
|
||||
Path: "/spec/replicas",
|
||||
Value: "2",
|
||||
},
|
||||
want: `{"op": "replace", "from": "", "path": "/spec/replicas", "value": "2"}`,
|
||||
},
|
||||
{
|
||||
name: "add complex interfaces",
|
||||
fields: fields{
|
||||
Operation: "add",
|
||||
Path: "/spec/template/spec/containers/0",
|
||||
Value: `{"name": "nginx", "image": "nginx:1.14.2", "ports": [{"containerPort": 80}]}`,
|
||||
},
|
||||
want: `{"op": "add", "from": "", "path": "/spec/template/spec/containers/0", "value": {"name": "nginx", "image": "nginx:1.14.2", "ports": [{"containerPort": 80}]}}`,
|
||||
},
|
||||
{
|
||||
name: "remove",
|
||||
fields: fields{
|
||||
Operation: "remove",
|
||||
Path: "/spec/template/spec/containers/0",
|
||||
},
|
||||
want: `{"op": "remove", "from": "", "path": "/spec/template/spec/containers/0", "value": ""}`,
|
||||
},
|
||||
{
|
||||
name: "move",
|
||||
fields: fields{
|
||||
Operation: "move",
|
||||
From: "/spec/template/spec/containers/0",
|
||||
Path: "/spec/template/spec/containers/1",
|
||||
},
|
||||
want: `{"op": "move", "from": "/spec/template/spec/containers/0", "path": "/spec/template/spec/containers/1", "value": ""}`,
|
||||
},
|
||||
{
|
||||
name: "copy",
|
||||
fields: fields{
|
||||
Operation: "copy",
|
||||
From: "/spec/template/spec/containers/0",
|
||||
Path: "/spec/template/spec/containers/1",
|
||||
},
|
||||
want: `{"op": "copy", "from": "/spec/template/spec/containers/0", "path": "/spec/template/spec/containers/1", "value": ""}`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := &JSONPatch{
|
||||
Operation: tt.fields.Operation,
|
||||
From: tt.fields.From,
|
||||
Path: tt.fields.Path,
|
||||
Value: tt.fields.Value,
|
||||
}
|
||||
if got := p.ToString(); got != tt.want {
|
||||
t.Errorf("JSONPatch.ToString() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package resourcemodifiers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func (r *ResourceModifierRule) Validate() error {
|
||||
if err := r.Conditions.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, patch := range r.Patches {
|
||||
if err := patch.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ResourceModifiers) Validate() error {
|
||||
if !strings.EqualFold(p.Version, ResourceModifierSupportedVersionV1) {
|
||||
return fmt.Errorf("unsupported resource modifier version %s", p.Version)
|
||||
}
|
||||
if len(p.ResourceModifierRules) == 0 {
|
||||
return fmt.Errorf("resource modifier rules cannot be empty")
|
||||
}
|
||||
for _, rule := range p.ResourceModifierRules {
|
||||
if err := rule.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *JSONPatch) Validate() error {
|
||||
if p.Operation == "" {
|
||||
return fmt.Errorf("operation cannot be empty")
|
||||
}
|
||||
if operation := strings.ToLower(p.Operation); operation != "add" && operation != "remove" && operation != "replace" && operation != "test" && operation != "move" && operation != "copy" {
|
||||
return fmt.Errorf("unsupported operation %s", p.Operation)
|
||||
}
|
||||
if p.Path == "" {
|
||||
return fmt.Errorf("path cannot be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conditions) Validate() error {
|
||||
if c.GroupKind == "" {
|
||||
return fmt.Errorf("groupkind cannot be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package resourcemodifiers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResourceModifiers_Validate(t *testing.T) {
|
||||
type fields struct {
|
||||
Version string
|
||||
ResourceModifierRules []ResourceModifierRule
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "correct version, non 0 length ResourceModifierRules",
|
||||
fields: fields{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupKind: "persistentvolumeclaims",
|
||||
ResourceNameRegex: ".*",
|
||||
Namespaces: []string{"bar", "foo"},
|
||||
},
|
||||
Patches: []JSONPatch{
|
||||
{
|
||||
Operation: "replace",
|
||||
Path: "/spec/storageClassName",
|
||||
Value: "premium",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "incorrect version, non 0 length ResourceModifierRules",
|
||||
fields: fields{
|
||||
Version: "v2",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupKind: "persistentvolumeclaims",
|
||||
ResourceNameRegex: ".*",
|
||||
Namespaces: []string{"bar", "foo"},
|
||||
},
|
||||
Patches: []JSONPatch{
|
||||
{
|
||||
Operation: "replace",
|
||||
Path: "/spec/storageClassName",
|
||||
Value: "premium",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "correct version, 0 length ResourceModifierRules",
|
||||
fields: fields{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "patch has invalid operation",
|
||||
fields: fields{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupKind: "persistentvolumeclaims",
|
||||
ResourceNameRegex: ".*",
|
||||
Namespaces: []string{"bar", "foo"},
|
||||
},
|
||||
Patches: []JSONPatch{
|
||||
{
|
||||
Operation: "invalid",
|
||||
Path: "/spec/storageClassName",
|
||||
Value: "premium",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Condition has empty GroupKind",
|
||||
fields: fields{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupKind: "",
|
||||
ResourceNameRegex: ".*",
|
||||
Namespaces: []string{"bar", "foo"},
|
||||
},
|
||||
Patches: []JSONPatch{
|
||||
{
|
||||
Operation: "invalid",
|
||||
Path: "/spec/storageClassName",
|
||||
Value: "premium",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := &ResourceModifiers{
|
||||
Version: tt.fields.Version,
|
||||
ResourceModifierRules: tt.fields.ResourceModifierRules,
|
||||
}
|
||||
if err := p.Validate(); (err != nil) != tt.wantErr {
|
||||
t.Errorf("ResourceModifiers.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJsonPatch_Validate(t *testing.T) {
|
||||
type fields struct {
|
||||
Operation string
|
||||
Path string
|
||||
Value string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "not empty operation, path, and new value, valid scenario",
|
||||
fields: fields{
|
||||
Operation: "replace",
|
||||
Path: "/spec/storageClassName",
|
||||
Value: "premium",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty operation throws error",
|
||||
fields: fields{
|
||||
Operation: "",
|
||||
Path: "/spec/storageClassName",
|
||||
Value: "premium",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty path throws error",
|
||||
fields: fields{
|
||||
Operation: "replace",
|
||||
Path: "",
|
||||
Value: "premium",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid operation throws error",
|
||||
fields: fields{
|
||||
Operation: "invalid",
|
||||
Path: "/spec/storageClassName",
|
||||
Value: "premium",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := &JSONPatch{
|
||||
Operation: tt.fields.Operation,
|
||||
Path: tt.fields.Path,
|
||||
Value: tt.fields.Value,
|
||||
}
|
||||
if err := p.Validate(); (err != nil) != tt.wantErr {
|
||||
t.Errorf("JsonPatch.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
@@ -117,6 +118,11 @@ type RestoreSpec struct {
|
||||
// The default value is 1 hour.
|
||||
// +optional
|
||||
ItemOperationTimeout metav1.Duration `json:"itemOperationTimeout,omitempty"`
|
||||
|
||||
// ResourceModifier specifies the reference to JSON resource patches that should be applied to resources before restoration.
|
||||
// +optional
|
||||
// +nullable
|
||||
ResourceModifier *v1.TypedLocalObjectReference `json:"resourceModifier,omitempty"`
|
||||
}
|
||||
|
||||
// RestoreHooks contains custom behaviors that should be executed during or post restore.
|
||||
|
||||
@@ -1317,6 +1317,11 @@ func (in *RestoreSpec) DeepCopyInto(out *RestoreSpec) {
|
||||
}
|
||||
in.Hooks.DeepCopyInto(&out.Hooks)
|
||||
out.ItemOperationTimeout = in.ItemOperationTimeout
|
||||
if in.ResourceModifier != nil {
|
||||
in, out := &in.ResourceModifier, &out.ResourceModifier
|
||||
*out = new(corev1.TypedLocalObjectReference)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestoreSpec.
|
||||
|
||||
@@ -25,11 +25,13 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/resourcemodifiers"
|
||||
api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd"
|
||||
@@ -75,26 +77,27 @@ func NewCreateCommand(f client.Factory, use string) *cobra.Command {
|
||||
}
|
||||
|
||||
type CreateOptions struct {
|
||||
BackupName string
|
||||
ScheduleName string
|
||||
RestoreName string
|
||||
RestoreVolumes flag.OptionalBool
|
||||
PreserveNodePorts flag.OptionalBool
|
||||
Labels flag.Map
|
||||
IncludeNamespaces flag.StringArray
|
||||
ExcludeNamespaces flag.StringArray
|
||||
ExistingResourcePolicy string
|
||||
IncludeResources flag.StringArray
|
||||
ExcludeResources flag.StringArray
|
||||
StatusIncludeResources flag.StringArray
|
||||
StatusExcludeResources flag.StringArray
|
||||
NamespaceMappings flag.Map
|
||||
Selector flag.LabelSelector
|
||||
IncludeClusterResources flag.OptionalBool
|
||||
Wait bool
|
||||
AllowPartiallyFailed flag.OptionalBool
|
||||
ItemOperationTimeout time.Duration
|
||||
client kbclient.WithWatch
|
||||
BackupName string
|
||||
ScheduleName string
|
||||
RestoreName string
|
||||
RestoreVolumes flag.OptionalBool
|
||||
PreserveNodePorts flag.OptionalBool
|
||||
Labels flag.Map
|
||||
IncludeNamespaces flag.StringArray
|
||||
ExcludeNamespaces flag.StringArray
|
||||
ExistingResourcePolicy string
|
||||
IncludeResources flag.StringArray
|
||||
ExcludeResources flag.StringArray
|
||||
StatusIncludeResources flag.StringArray
|
||||
StatusExcludeResources flag.StringArray
|
||||
NamespaceMappings flag.Map
|
||||
Selector flag.LabelSelector
|
||||
IncludeClusterResources flag.OptionalBool
|
||||
Wait bool
|
||||
AllowPartiallyFailed flag.OptionalBool
|
||||
ItemOperationTimeout time.Duration
|
||||
ResourceModifierConfigMap string
|
||||
client kbclient.WithWatch
|
||||
}
|
||||
|
||||
func NewCreateOptions() *CreateOptions {
|
||||
@@ -139,6 +142,8 @@ func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) {
|
||||
f.NoOptDefVal = cmd.TRUE
|
||||
|
||||
flags.BoolVarP(&o.Wait, "wait", "w", o.Wait, "Wait for the operation to complete.")
|
||||
|
||||
flags.StringVar(&o.ResourceModifierConfigMap, "resource-modifier-configmap", "", "Reference to the resource modifier configmap that restore will use")
|
||||
}
|
||||
|
||||
func (o *CreateOptions) Complete(args []string, f client.Factory) error {
|
||||
@@ -272,6 +277,15 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error {
|
||||
}
|
||||
}
|
||||
|
||||
var resModifiers *corev1.TypedLocalObjectReference = nil
|
||||
|
||||
if o.ResourceModifierConfigMap != "" {
|
||||
resModifiers = &corev1.TypedLocalObjectReference{
|
||||
Kind: resourcemodifiers.ConfigmapRefType,
|
||||
Name: o.ResourceModifierConfigMap,
|
||||
}
|
||||
}
|
||||
|
||||
restore := &api.Restore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: f.Namespace(),
|
||||
@@ -291,6 +305,7 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error {
|
||||
RestorePVs: o.RestoreVolumes.Value,
|
||||
PreserveNodePorts: o.PreserveNodePorts.Value,
|
||||
IncludeClusterResources: o.IncludeClusterResources.Value,
|
||||
ResourceModifier: resModifiers,
|
||||
ItemOperationTimeout: metav1.Duration{
|
||||
Duration: o.ItemOperationTimeout,
|
||||
},
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
@@ -39,6 +40,7 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/hook"
|
||||
"github.com/vmware-tanzu/velero/internal/resourcemodifiers"
|
||||
api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/itemoperation"
|
||||
"github.com/vmware-tanzu/velero/pkg/label"
|
||||
@@ -163,7 +165,7 @@ func (r *restoreReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
|
||||
original := restore.DeepCopy()
|
||||
|
||||
// Validate the restore and fetch the backup
|
||||
info := r.validateAndComplete(restore)
|
||||
info, resourceModifiers := r.validateAndComplete(restore)
|
||||
|
||||
// Register attempts after validation so we don't have to fetch the backup multiple times
|
||||
backupScheduleName := restore.Spec.ScheduleName
|
||||
@@ -197,7 +199,7 @@ func (r *restoreReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
if err := r.runValidatedRestore(restore, info); err != nil {
|
||||
if err := r.runValidatedRestore(restore, info, resourceModifiers); err != nil {
|
||||
log.WithError(err).Debug("Restore failed")
|
||||
restore.Status.Phase = api.RestorePhaseFailed
|
||||
restore.Status.FailureReason = err.Error()
|
||||
@@ -241,7 +243,7 @@ func (r *restoreReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func (r *restoreReconciler) validateAndComplete(restore *api.Restore) backupInfo {
|
||||
func (r *restoreReconciler) validateAndComplete(restore *api.Restore) (backupInfo, *resourcemodifiers.ResourceModifiers) {
|
||||
// add non-restorable resources to restore's excluded resources
|
||||
excludedResources := sets.NewString(restore.Spec.ExcludedResources...)
|
||||
for _, nonrestorable := range nonRestorableResources {
|
||||
@@ -276,7 +278,7 @@ func (r *restoreReconciler) validateAndComplete(restore *api.Restore) backupInfo
|
||||
// validate that exactly one of BackupName and ScheduleName have been specified
|
||||
if !backupXorScheduleProvided(restore) {
|
||||
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, "Either a backup or schedule must be specified as a source for the restore, but not both")
|
||||
return backupInfo{}
|
||||
return backupInfo{}, nil
|
||||
}
|
||||
|
||||
// validate Restore Init Hook's InitContainers
|
||||
@@ -307,7 +309,7 @@ func (r *restoreReconciler) validateAndComplete(restore *api.Restore) backupInfo
|
||||
backupList := &api.BackupList{}
|
||||
if err := r.kbClient.List(context.Background(), backupList, &client.ListOptions{LabelSelector: selector}); err != nil {
|
||||
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, "Unable to list backups for schedule")
|
||||
return backupInfo{}
|
||||
return backupInfo{}, nil
|
||||
}
|
||||
if len(backupList.Items) == 0 {
|
||||
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, "No backups found for schedule")
|
||||
@@ -317,14 +319,14 @@ func (r *restoreReconciler) validateAndComplete(restore *api.Restore) backupInfo
|
||||
restore.Spec.BackupName = backup.Name
|
||||
} else {
|
||||
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, "No completed backups found for schedule")
|
||||
return backupInfo{}
|
||||
return backupInfo{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
info, err := r.fetchBackupInfo(restore.Spec.BackupName)
|
||||
if err != nil {
|
||||
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, fmt.Sprintf("Error retrieving backup: %v", err))
|
||||
return backupInfo{}
|
||||
return backupInfo{}, nil
|
||||
}
|
||||
|
||||
// Fill in the ScheduleName so it's easier to consume for metrics.
|
||||
@@ -332,7 +334,26 @@ func (r *restoreReconciler) validateAndComplete(restore *api.Restore) backupInfo
|
||||
restore.Spec.ScheduleName = info.backup.GetLabels()[api.ScheduleNameLabel]
|
||||
}
|
||||
|
||||
return info
|
||||
var resourceModifiers *resourcemodifiers.ResourceModifiers = nil
|
||||
if restore.Spec.ResourceModifier != nil && restore.Spec.ResourceModifier.Kind == resourcemodifiers.ConfigmapRefType {
|
||||
ResourceModifierConfigMap := &corev1api.ConfigMap{}
|
||||
err := r.kbClient.Get(context.Background(), client.ObjectKey{Namespace: restore.Namespace, Name: restore.Spec.ResourceModifier.Name}, ResourceModifierConfigMap)
|
||||
if err != nil {
|
||||
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, fmt.Sprintf("failed to get resource modifiers configmap %s/%s", restore.Namespace, restore.Spec.ResourceModifier.Name))
|
||||
return backupInfo{}, nil
|
||||
}
|
||||
resourceModifiers, err = resourcemodifiers.GetResourceModifiersFromConfig(ResourceModifierConfigMap)
|
||||
if err != nil {
|
||||
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, errors.Wrapf(err, fmt.Sprintf("Error in parsing resource modifiers provided in configmap %s/%s", restore.Namespace, restore.Spec.ResourceModifier.Name)).Error())
|
||||
return backupInfo{}, nil
|
||||
} else if err = resourceModifiers.Validate(); err != nil {
|
||||
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, errors.Wrapf(err, fmt.Sprintf("Validation error in resource modifiers provided in configmap %s/%s", restore.Namespace, restore.Spec.ResourceModifier.Name)).Error())
|
||||
return backupInfo{}, nil
|
||||
}
|
||||
r.logger.Infof("Retrieved Resource modifiers provided in configmap %s/%s", restore.Namespace, restore.Spec.ResourceModifier.Name)
|
||||
}
|
||||
|
||||
return info, resourceModifiers
|
||||
}
|
||||
|
||||
// backupXorScheduleProvided returns true if exactly one of BackupName and
|
||||
@@ -405,7 +426,7 @@ func fetchBackupInfoInternal(kbClient client.Client, namespace, backupName strin
|
||||
// The log and results files are uploaded to backup storage. Any error returned from this function
|
||||
// means that the restore failed. This function updates the restore API object with warning and error
|
||||
// counts, but *does not* update its phase or patch it via the API.
|
||||
func (r *restoreReconciler) runValidatedRestore(restore *api.Restore, info backupInfo) error {
|
||||
func (r *restoreReconciler) runValidatedRestore(restore *api.Restore, info backupInfo, resourceModifiers *resourcemodifiers.ResourceModifiers) error {
|
||||
// instantiate the per-restore logger that will output both to a temp file
|
||||
// (for upload to object storage) and to stdout.
|
||||
restoreLog, err := logging.NewTempFileLogger(r.restoreLogLevel, r.logFormat, nil, logrus.Fields{"restore": kubeutil.NamespaceAndName(restore)})
|
||||
@@ -458,13 +479,15 @@ func (r *restoreReconciler) runValidatedRestore(restore *api.Restore, info backu
|
||||
for i := range podVolumeBackupList.Items {
|
||||
podVolumeBackups = append(podVolumeBackups, &podVolumeBackupList.Items[i])
|
||||
}
|
||||
|
||||
restoreReq := &pkgrestore.Request{
|
||||
Log: restoreLog,
|
||||
Restore: restore,
|
||||
Backup: info.backup,
|
||||
PodVolumeBackups: podVolumeBackups,
|
||||
VolumeSnapshots: volumeSnapshots,
|
||||
BackupReader: backupFile,
|
||||
Log: restoreLog,
|
||||
Restore: restore,
|
||||
Backup: info.backup,
|
||||
PodVolumeBackups: podVolumeBackups,
|
||||
VolumeSnapshots: volumeSnapshots,
|
||||
BackupReader: backupFile,
|
||||
ResourceModifiers: resourceModifiers,
|
||||
}
|
||||
restoreWarnings, restoreErrors := r.restorer.RestoreWithResolvers(restoreReq, actionsResolver, pluginManager)
|
||||
|
||||
|
||||
@@ -34,6 +34,9 @@ import (
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/resourcemodifiers"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/builder"
|
||||
"github.com/vmware-tanzu/velero/pkg/metrics"
|
||||
@@ -659,6 +662,145 @@ func TestValidateAndCompleteWhenScheduleNameSpecified(t *testing.T) {
|
||||
assert.Equal(t, "foo", restore.Spec.BackupName)
|
||||
}
|
||||
|
||||
func TestValidateAndCompleteWithResourceModifierSpecified(t *testing.T) {
|
||||
formatFlag := logging.FormatText
|
||||
|
||||
var (
|
||||
logger = velerotest.NewLogger()
|
||||
pluginManager = &pluginmocks.Manager{}
|
||||
fakeClient = velerotest.NewFakeControllerRuntimeClient(t)
|
||||
backupStore = &persistencemocks.BackupStore{}
|
||||
)
|
||||
|
||||
r := NewRestoreReconciler(
|
||||
context.Background(),
|
||||
velerov1api.DefaultNamespace,
|
||||
nil,
|
||||
fakeClient,
|
||||
logger,
|
||||
logrus.DebugLevel,
|
||||
func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager },
|
||||
NewFakeSingleObjectBackupStoreGetter(backupStore),
|
||||
metrics.NewServerMetrics(),
|
||||
formatFlag,
|
||||
60*time.Minute,
|
||||
)
|
||||
|
||||
restore := &velerov1api.Restore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: velerov1api.DefaultNamespace,
|
||||
Name: "restore-1",
|
||||
},
|
||||
Spec: velerov1api.RestoreSpec{
|
||||
BackupName: "backup-1",
|
||||
ResourceModifier: &corev1.TypedLocalObjectReference{
|
||||
Kind: resourcemodifiers.ConfigmapRefType,
|
||||
Name: "test-configmap",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
location := builder.ForBackupStorageLocation("velero", "default").Provider("myCloud").Bucket("bucket").Result()
|
||||
require.NoError(t, r.kbClient.Create(context.Background(), location))
|
||||
|
||||
require.NoError(t, r.kbClient.Create(
|
||||
context.Background(),
|
||||
defaultBackup().
|
||||
ObjectMeta(
|
||||
builder.WithName("backup-1"),
|
||||
).StorageLocation("default").
|
||||
Phase(velerov1api.BackupPhaseCompleted).
|
||||
Result(),
|
||||
))
|
||||
|
||||
r.validateAndComplete(restore)
|
||||
assert.Contains(t, restore.Status.ValidationErrors[0], "failed to get resource modifiers configmap")
|
||||
|
||||
restore1 := &velerov1api.Restore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: velerov1api.DefaultNamespace,
|
||||
Name: "restore-1",
|
||||
},
|
||||
Spec: velerov1api.RestoreSpec{
|
||||
BackupName: "backup-1",
|
||||
ResourceModifier: &corev1.TypedLocalObjectReference{
|
||||
Kind: resourcemodifiers.ConfigmapRefType,
|
||||
Name: "test-configmap",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cm1 := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-configmap",
|
||||
Namespace: velerov1api.DefaultNamespace,
|
||||
},
|
||||
Data: map[string]string{
|
||||
"sub.yml": "version: v1\nresourceModifierRules:\n- conditions:\n groupKind: persistentvolumeclaims\n resourceNameRegex: \".*\"\n namespaces:\n - bar\n - foo\n patches:\n - operation: replace\n path: \"/spec/storageClassName\"\n value: \"premium\"\n - operation: remove\n path: \"/metadata/labels/test\"\n\n\n",
|
||||
},
|
||||
}
|
||||
require.NoError(t, r.kbClient.Create(context.Background(), cm1))
|
||||
|
||||
r.validateAndComplete(restore1)
|
||||
assert.Nil(t, restore1.Status.ValidationErrors)
|
||||
|
||||
restore2 := &velerov1api.Restore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: velerov1api.DefaultNamespace,
|
||||
Name: "restore-1",
|
||||
},
|
||||
Spec: velerov1api.RestoreSpec{
|
||||
BackupName: "backup-1",
|
||||
ResourceModifier: &corev1.TypedLocalObjectReference{
|
||||
Kind: resourcemodifiers.ConfigmapRefType,
|
||||
Name: "test-configmap-invalid",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
invalidVersionCm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-configmap-invalid",
|
||||
Namespace: velerov1api.DefaultNamespace,
|
||||
},
|
||||
Data: map[string]string{
|
||||
"sub.yml": "version1: v1\nresourceModifierRules:\n- conditions:\n groupKind: persistentvolumeclaims\n resourceNameRegex: \".*\"\n namespaces:\n - bar\n - foo\n patches:\n - operation: replace\n path: \"/spec/storageClassName\"\n value: \"premium\"\n - operation: remove\n path: \"/metadata/labels/test\"\n\n\n",
|
||||
},
|
||||
}
|
||||
require.NoError(t, r.kbClient.Create(context.Background(), invalidVersionCm))
|
||||
|
||||
r.validateAndComplete(restore2)
|
||||
assert.Contains(t, restore2.Status.ValidationErrors[0], "Error in parsing resource modifiers provided in configmap")
|
||||
|
||||
restore3 := &velerov1api.Restore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: velerov1api.DefaultNamespace,
|
||||
Name: "restore-1",
|
||||
},
|
||||
Spec: velerov1api.RestoreSpec{
|
||||
BackupName: "backup-1",
|
||||
ResourceModifier: &corev1.TypedLocalObjectReference{
|
||||
Kind: resourcemodifiers.ConfigmapRefType,
|
||||
Name: "test-configmap-invalid-operator",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
invalidOperatorCm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-configmap-invalid-operator",
|
||||
Namespace: velerov1api.DefaultNamespace,
|
||||
},
|
||||
Data: map[string]string{
|
||||
"sub.yml": "version: v1\nresourceModifierRules:\n- conditions:\n groupKind: persistentvolumeclaims\n resourceNameRegex: \".*\"\n namespaces:\n - bar\n - foo\n patches:\n - operation: invalid\n path: \"/spec/storageClassName\"\n value: \"premium\"\n - operation: remove\n path: \"/metadata/labels/test\"\n\n\n",
|
||||
},
|
||||
}
|
||||
require.NoError(t, r.kbClient.Create(context.Background(), invalidOperatorCm))
|
||||
|
||||
r.validateAndComplete(restore3)
|
||||
assert.Contains(t, restore3.Status.ValidationErrors[0], "Validation error in resource modifiers provided in configmap")
|
||||
}
|
||||
|
||||
func TestBackupXorScheduleProvided(t *testing.T) {
|
||||
r := &velerov1api.Restore{}
|
||||
assert.False(t, backupXorScheduleProvided(r))
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/resourcemodifiers"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/itemoperation"
|
||||
"github.com/vmware-tanzu/velero/pkg/volume"
|
||||
@@ -57,6 +58,7 @@ type Request struct {
|
||||
BackupReader io.Reader
|
||||
RestoredItems map[itemKey]restoredItemStatus
|
||||
itemOperationsList *[]*itemoperation.RestoreOperation
|
||||
ResourceModifiers *resourcemodifiers.ResourceModifiers
|
||||
}
|
||||
|
||||
type restoredItemStatus struct {
|
||||
|
||||
@@ -48,6 +48,7 @@ import (
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/credentials"
|
||||
"github.com/vmware-tanzu/velero/internal/hook"
|
||||
"github.com/vmware-tanzu/velero/internal/resourcemodifiers"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/archive"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
@@ -305,6 +306,7 @@ func (kr *kubernetesRestorer) RestoreWithResolvers(
|
||||
hooksCancelFunc: hooksCancelFunc,
|
||||
kbClient: kr.kbClient,
|
||||
itemOperationsList: req.GetItemOperationsList(),
|
||||
resourceModifiers: req.ResourceModifiers,
|
||||
}
|
||||
|
||||
return restoreCtx.execute()
|
||||
@@ -350,6 +352,7 @@ type restoreContext struct {
|
||||
hooksCancelFunc go_context.CancelFunc
|
||||
kbClient crclient.Client
|
||||
itemOperationsList *[]*itemoperation.RestoreOperation
|
||||
resourceModifiers *resourcemodifiers.ResourceModifiers
|
||||
}
|
||||
|
||||
type resourceClientKey struct {
|
||||
@@ -1355,6 +1358,14 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
|
||||
// and which backup they came from.
|
||||
addRestoreLabels(obj, ctx.restore.Name, ctx.restore.Spec.BackupName)
|
||||
|
||||
if ctx.resourceModifiers != nil {
|
||||
if errList := ctx.resourceModifiers.ApplyResourceModifierRules(obj, groupResource.String(), ctx.log); errList != nil {
|
||||
for _, err := range errList {
|
||||
errs.Add(namespace, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.log.Infof("Attempting to restore %s: %v", obj.GroupVersionKind().Kind, name)
|
||||
createdObj, restoreErr := resourceClient.Create(obj)
|
||||
if restoreErr == nil {
|
||||
|
||||
Reference in New Issue
Block a user