velero/internal/resourcemodifiers/resource_modifiers_test.go

1909 lines
46 KiB
Go

/*
Copyright The Velero Contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package 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"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer/yaml"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
)
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 groupResource: 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{
GroupResource: "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 groupResource: 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{
GroupResource: "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 groupResource: 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",
},
}
cm4 := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-configmap",
Namespace: "test-namespace",
},
Data: map[string]string{
"sub.yml": "version: v1\nresourceModifierRules:\n- conditions:\n groupResource: deployments.apps\n labelSelector:\n matchLabels:\n a: b\n",
},
}
rules4 := &ResourceModifiers{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "deployments.apps",
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"a": "b",
},
},
},
},
},
}
cm5 := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-configmap",
Namespace: "test-namespace",
},
Data: map[string]string{
"sub.yml": "version: v1\nresourceModifierRules:\n- conditions:\n groupResource: pods\n namespaces:\n - ns1\n matches:\n - path: /metadata/annotations/foo\n value: bar\n mergePatches:\n - patchData: |\n metadata:\n annotations:\n foo: null",
},
}
rules5 := &ResourceModifiers{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "pods",
Namespaces: []string{
"ns1",
},
Matches: []MatchRule{
{
Path: "/metadata/annotations/foo",
Value: "bar",
},
},
},
MergePatches: []JSONMergePatch{
{
PatchData: "metadata:\n annotations:\n foo: null",
},
},
},
},
}
cm6 := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-configmap",
Namespace: "test-namespace",
},
Data: map[string]string{
"sub.yml": "version: v1\nresourceModifierRules:\n- conditions:\n groupResource: pods\n namespaces:\n - ns1\n strategicPatches:\n - patchData: |\n spec:\n containers:\n - name: nginx\n image: repo2/nginx",
},
}
rules6 := &ResourceModifiers{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "pods",
Namespaces: []string{
"ns1",
},
},
StrategicPatches: []StrategicMergePatch{
{
PatchData: "spec:\n containers:\n - name: nginx\n image: repo2/nginx",
},
},
},
},
}
cm7 := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-configmap",
Namespace: "test-namespace",
},
Data: map[string]string{
"sub.yml": "version: v1\nresourceModifierRules:\n- conditions:\n groupResource: pods\n namespaces:\n - ns1\n mergePatches:\n - patchData: |\n {\"metadata\":{\"annotations\":{\"foo\":null}}}",
},
}
rules7 := &ResourceModifiers{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "pods",
Namespaces: []string{
"ns1",
},
},
MergePatches: []JSONMergePatch{
{
PatchData: `{"metadata":{"annotations":{"foo":null}}}`,
},
},
},
},
}
cm8 := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-configmap",
Namespace: "test-namespace",
},
Data: map[string]string{
"sub.yml": "version: v1\nresourceModifierRules:\n- conditions:\n groupResource: pods\n namespaces:\n - ns1\n strategicPatches:\n - patchData: |\n {\"spec\":{\"containers\":[{\"name\": \"nginx\",\"image\": \"repo2/nginx\"}]}}",
},
}
rules8 := &ResourceModifiers{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "pods",
Namespaces: []string{
"ns1",
},
},
StrategicPatches: []StrategicMergePatch{
{
PatchData: `{"spec":{"containers":[{"name": "nginx","image": "repo2/nginx"}]}}`,
},
},
},
},
}
cm9 := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-configmap",
Namespace: "test-namespace",
},
Data: map[string]string{
"sub.yml": "version: v1\nresourceModifierRules:\n- conditions:\n groupResource: deployments.apps\n resourceNameRegex: \"^test-.*$\"\n namespaces:\n - bar\n - foo\n patches:\n - operation: replace\n path: \"/value/bool\"\n value: \"\\\"true\\\"\"\n\n\n",
},
}
rules9 := &ResourceModifiers{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "deployments.apps",
ResourceNameRegex: "^test-.*$",
Namespaces: []string{"bar", "foo"},
},
Patches: []JSONPatch{
{
Operation: "replace",
Path: "/value/bool",
Value: `"true"`,
},
},
},
},
}
cm10 := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-configmap",
Namespace: "test-namespace",
},
Data: map[string]string{
"sub.yml": "version: v1\nresourceModifierRules:\n- conditions:\n groupResource: deployments.apps\n resourceNameRegex: \"^test-.*$\"\n namespaces:\n - bar\n - foo\n patches:\n - operation: replace\n path: \"/value/bool\"\n value: \"true\"\n\n\n",
},
}
rules10 := &ResourceModifiers{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "deployments.apps",
ResourceNameRegex: "^test-.*$",
Namespaces: []string{"bar", "foo"},
},
Patches: []JSONPatch{
{
Operation: "replace",
Path: "/value/bool",
Value: "true",
},
},
},
},
}
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: "match labels",
args: args{
cm: cm4,
},
want: rules4,
wantErr: false,
},
{
name: "nil configmap",
args: args{
cm: nil,
},
want: nil,
wantErr: true,
},
{
name: "complex yaml data with json merge patch",
args: args{
cm: cm5,
},
want: rules5,
wantErr: false,
},
{
name: "complex yaml data with strategic merge patch",
args: args{
cm: cm6,
},
want: rules6,
wantErr: false,
},
{
name: "complex json data with json merge patch",
args: args{
cm: cm7,
},
want: rules7,
wantErr: false,
},
{
name: "complex json data with strategic merge patch",
args: args{
cm: cm8,
},
want: rules8,
wantErr: false,
},
{
name: "bool value as string",
args: args{
cm: cm9,
},
want: rules9,
wantErr: false,
},
{
name: "bool value as bool",
args: args{
cm: cm10,
},
want: rules10,
wantErr: false,
},
}
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",
},
},
}
pvcGoldSc := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "PersistentVolumeClaim",
"metadata": map[string]interface{}{
"name": "test-pvc",
"namespace": "foo",
},
"spec": map[string]interface{}{
"storageClassName": "gold",
},
},
}
deployNginxOneReplica := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "test-deployment",
"namespace": "foo",
"labels": map[string]interface{}{
"app": "nginx",
},
},
"spec": map[string]interface{}{
"replicas": int64(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",
"labels": map[string]interface{}{
"app": "nginx",
},
},
"spec": map[string]interface{}{
"replicas": int64(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",
"labels": map[string]interface{}{
"app": "nginx",
},
},
"spec": map[string]interface{}{
"replicas": int64(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",
},
},
},
},
},
},
}
cmTrue := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"data": map[string]interface{}{
"test": "true",
},
},
}
cmFalse := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"data": map[string]interface{}{
"test": "false",
},
},
}
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: "configmap true false string",
fields: fields{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "configmaps",
ResourceNameRegex: ".*",
},
Patches: []JSONPatch{
{
Operation: "replace",
Path: "/data/test",
Value: `"false"`,
},
},
},
},
},
args: args{
obj: cmTrue.DeepCopy(),
groupResource: "configmaps",
},
wantErr: false,
wantObj: cmFalse.DeepCopy(),
},
{
name: "Invalid Regex throws error",
fields: fields{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "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{
GroupResource: "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: "pvc with standard storage class should be patched to premium, even when rules are [standard => premium, premium => gold]",
fields: fields{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "persistentvolumeclaims",
ResourceNameRegex: ".*",
Matches: []MatchRule{
{
Path: "/spec/storageClassName",
Value: "standard",
},
},
},
Patches: []JSONPatch{
{
Operation: "replace",
Path: "/spec/storageClassName",
Value: "premium",
},
},
},
{
Conditions: Conditions{
GroupResource: "persistentvolumeclaims",
ResourceNameRegex: ".*",
Matches: []MatchRule{
{
Path: "/spec/storageClassName",
Value: "premium",
},
},
},
Patches: []JSONPatch{
{
Operation: "replace",
Path: "/spec/storageClassName",
Value: "gold",
},
},
},
},
},
args: args{
obj: pvcStandardSc.DeepCopy(),
groupResource: "persistentvolumeclaims",
},
wantErr: false,
wantObj: pvcPremiumSc.DeepCopy(),
},
{
name: "pvc with standard storage class should be patched to gold, even when rules are [standard => premium, standard => gold]",
fields: fields{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "persistentvolumeclaims",
ResourceNameRegex: ".*",
Matches: []MatchRule{
{
Path: "/spec/storageClassName",
Value: "standard",
},
},
},
Patches: []JSONPatch{
{
Operation: "replace",
Path: "/spec/storageClassName",
Value: "premium",
},
},
},
{
Conditions: Conditions{
GroupResource: "persistentvolumeclaims",
ResourceNameRegex: ".*",
Matches: []MatchRule{
{
Path: "/spec/storageClassName",
Value: "standard",
},
},
},
Patches: []JSONPatch{
{
Operation: "replace",
Path: "/spec/storageClassName",
Value: "gold",
},
},
},
},
},
args: args{
obj: pvcStandardSc.DeepCopy(),
groupResource: "persistentvolumeclaims",
},
wantErr: false,
wantObj: pvcGoldSc.DeepCopy(),
},
{
name: "nginx deployment: 1 -> 2 replicas",
fields: fields{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "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{
GroupResource: "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{
GroupResource: "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",
fields: fields{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "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{
GroupResource: "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{
GroupResource: "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{
GroupResource: "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{
GroupResource: "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(),
},
{
name: "nginx deployment: match label selector",
fields: fields{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "deployments.apps",
Namespaces: []string{"foo"},
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "nginx",
},
},
},
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: mismatch label selector",
fields: fields{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "deployments.apps",
Namespaces: []string{"foo"},
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "nginx-mismatch",
},
},
},
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(),
},
}
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, nil, logrus.New())
assert.Equal(t, tt.wantErr, len(got) > 0)
assert.Equal(t, *tt.wantObj, *tt.args.obj)
})
}
}
var podYAMLWithNginxImage = `
apiVersion: v1
kind: Pod
metadata:
name: pod1
namespace: fake
spec:
containers:
- image: nginx
name: nginx
`
var podYAMLWithNginx1Image = `
apiVersion: v1
kind: Pod
metadata:
name: pod1
namespace: fake
spec:
containers:
- image: nginx1
name: nginx
`
var podYAMLWithNFSVolume = `
apiVersion: v1
kind: Pod
metadata:
name: pod1
namespace: fake
spec:
containers:
- image: fake
name: fake
volumeMounts:
- mountPath: /fake1
name: vol1
- mountPath: /fake2
name: vol2
volumes:
- name: vol1
nfs:
path: /fake2
- name: vol2
emptyDir: {}
`
var podYAMLWithPVCVolume = `
apiVersion: v1
kind: Pod
metadata:
name: pod1
namespace: fake
spec:
containers:
- image: fake
name: fake
volumeMounts:
- mountPath: /fake1
name: vol1
- mountPath: /fake2
name: vol2
volumes:
- name: vol1
persistentVolumeClaim:
claimName: pvc1
- name: vol2
emptyDir: {}
`
var svcYAMLWithPort8000 = `
apiVersion: v1
kind: Service
metadata:
name: svc1
namespace: fake
spec:
ports:
- name: fake1
port: 8001
protocol: TCP
targetPort: 8001
- name: fake
port: 8000
protocol: TCP
targetPort: 8000
- name: fake2
port: 8002
protocol: TCP
targetPort: 8002
`
var svcYAMLWithPort9000 = `
apiVersion: v1
kind: Service
metadata:
name: svc1
namespace: fake
spec:
ports:
- name: fake1
port: 8001
protocol: TCP
targetPort: 8001
- name: fake
port: 9000
protocol: TCP
targetPort: 9000
- name: fake2
port: 8002
protocol: TCP
targetPort: 8002
`
var cmYAMLWithLabelAToB = `
apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
namespace: fake
labels:
a: b
c: d
`
var cmYAMLWithLabelAToC = `
apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
namespace: fake
labels:
a: c
c: d
`
var cmYAMLWithoutLabelA = `
apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
namespace: fake
labels:
c: d
`
func TestResourceModifiers_ApplyResourceModifierRules_StrategicMergePatch(t *testing.T) {
scheme := runtime.NewScheme()
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
unstructuredSerializer := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
o1, _, err := unstructuredSerializer.Decode([]byte(podYAMLWithNFSVolume), nil, nil)
assert.NoError(t, err)
podWithNFSVolume := o1.(*unstructured.Unstructured)
o2, _, err := unstructuredSerializer.Decode([]byte(podYAMLWithPVCVolume), nil, nil)
assert.NoError(t, err)
podWithPVCVolume := o2.(*unstructured.Unstructured)
o3, _, err := unstructuredSerializer.Decode([]byte(svcYAMLWithPort8000), nil, nil)
assert.NoError(t, err)
svcWithPort8000 := o3.(*unstructured.Unstructured)
o4, _, err := unstructuredSerializer.Decode([]byte(svcYAMLWithPort9000), nil, nil)
assert.NoError(t, err)
svcWithPort9000 := o4.(*unstructured.Unstructured)
o5, _, err := unstructuredSerializer.Decode([]byte(podYAMLWithNginxImage), nil, nil)
assert.NoError(t, err)
podWithNginxImage := o5.(*unstructured.Unstructured)
o6, _, err := unstructuredSerializer.Decode([]byte(podYAMLWithNginx1Image), nil, nil)
assert.NoError(t, err)
podWithNginx1Image := o6.(*unstructured.Unstructured)
tests := []struct {
name string
rm *ResourceModifiers
obj *unstructured.Unstructured
groupResource string
wantErr bool
wantObj *unstructured.Unstructured
}{
{
name: "update image",
rm: &ResourceModifiers{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "pods",
Namespaces: []string{"fake"},
},
StrategicPatches: []StrategicMergePatch{
{
PatchData: `{"spec":{"containers":[{"name":"nginx","image":"nginx1"}]}}`,
},
},
},
},
},
obj: podWithNginxImage.DeepCopy(),
groupResource: "pods",
wantErr: false,
wantObj: podWithNginx1Image.DeepCopy(),
},
{
name: "update image with yaml format",
rm: &ResourceModifiers{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "pods",
Namespaces: []string{"fake"},
},
StrategicPatches: []StrategicMergePatch{
{
PatchData: `spec:
containers:
- name: nginx
image: nginx1`,
},
},
},
},
},
obj: podWithNginxImage.DeepCopy(),
groupResource: "pods",
wantErr: false,
wantObj: podWithNginx1Image.DeepCopy(),
},
{
name: "replace nfs with pvc in volume",
rm: &ResourceModifiers{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "pods",
Namespaces: []string{"fake"},
},
StrategicPatches: []StrategicMergePatch{
{
PatchData: `{"spec":{"volumes":[{"nfs":null,"name":"vol1","persistentVolumeClaim":{"claimName":"pvc1"}}]}}`,
},
},
},
},
},
obj: podWithNFSVolume.DeepCopy(),
groupResource: "pods",
wantErr: false,
wantObj: podWithPVCVolume.DeepCopy(),
},
{
name: "replace any other volume source with pvc in volume",
rm: &ResourceModifiers{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "pods",
Namespaces: []string{"fake"},
},
StrategicPatches: []StrategicMergePatch{
{
PatchData: `{"spec":{"volumes":[{"$retainKeys":["name","persistentVolumeClaim"],"name":"vol1","persistentVolumeClaim":{"claimName":"pvc1"}}]}}`,
},
},
},
},
},
obj: podWithNFSVolume.DeepCopy(),
groupResource: "pods",
wantErr: false,
wantObj: podWithPVCVolume.DeepCopy(),
},
{
name: "update a service port",
rm: &ResourceModifiers{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "services",
Namespaces: []string{"fake"},
},
StrategicPatches: []StrategicMergePatch{
{
PatchData: `{"spec":{"$setElementOrder/ports":[{"port":8001},{"port":9000},{"port":8002}],"ports":[{"name":"fake","port":9000,"protocol":"TCP","targetPort":9000},{"$patch":"delete","port":8000}]}}`,
},
},
},
},
},
obj: svcWithPort8000.DeepCopy(),
groupResource: "services",
wantErr: false,
wantObj: svcWithPort9000.DeepCopy(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.rm.ApplyResourceModifierRules(tt.obj, tt.groupResource, scheme, logrus.New())
assert.Equal(t, tt.wantErr, len(got) > 0)
assert.Equal(t, *tt.wantObj, *tt.obj)
})
}
}
func TestResourceModifiers_ApplyResourceModifierRules_JSONMergePatch(t *testing.T) {
unstructuredSerializer := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
o1, _, err := unstructuredSerializer.Decode([]byte(cmYAMLWithLabelAToB), nil, nil)
assert.NoError(t, err)
cmWithLabelAToB := o1.(*unstructured.Unstructured)
o2, _, err := unstructuredSerializer.Decode([]byte(cmYAMLWithLabelAToC), nil, nil)
assert.NoError(t, err)
cmWithLabelAToC := o2.(*unstructured.Unstructured)
o3, _, err := unstructuredSerializer.Decode([]byte(cmYAMLWithoutLabelA), nil, nil)
assert.NoError(t, err)
cmWithoutLabelA := o3.(*unstructured.Unstructured)
tests := []struct {
name string
rm *ResourceModifiers
obj *unstructured.Unstructured
groupResource string
wantErr bool
wantObj *unstructured.Unstructured
}{
{
name: "update labels",
rm: &ResourceModifiers{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "configmaps",
Namespaces: []string{"fake"},
},
MergePatches: []JSONMergePatch{
{
PatchData: `{"metadata":{"labels":{"a":"c"}}}`,
},
},
},
},
},
obj: cmWithLabelAToB.DeepCopy(),
groupResource: "configmaps",
wantErr: false,
wantObj: cmWithLabelAToC.DeepCopy(),
},
{
name: "update labels in yaml format",
rm: &ResourceModifiers{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "configmaps",
Namespaces: []string{"fake"},
},
MergePatches: []JSONMergePatch{
{
PatchData: `metadata:
labels:
a: c`,
},
},
},
},
},
obj: cmWithLabelAToB.DeepCopy(),
groupResource: "configmaps",
wantErr: false,
wantObj: cmWithLabelAToC.DeepCopy(),
},
{
name: "delete labels",
rm: &ResourceModifiers{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "configmaps",
Namespaces: []string{"fake"},
},
MergePatches: []JSONMergePatch{
{
PatchData: `{"metadata":{"labels":{"a":null}}}`,
},
},
},
},
},
obj: cmWithLabelAToB.DeepCopy(),
groupResource: "configmaps",
wantErr: false,
wantObj: cmWithoutLabelA.DeepCopy(),
},
{
name: "add labels",
rm: &ResourceModifiers{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "configmaps",
Namespaces: []string{"fake"},
},
MergePatches: []JSONMergePatch{
{
PatchData: `{"metadata":{"labels":{"a":"b"}}}`,
},
},
},
},
},
obj: cmWithoutLabelA.DeepCopy(),
groupResource: "configmaps",
wantErr: false,
wantObj: cmWithLabelAToB.DeepCopy(),
},
{
name: "delete non-existing labels",
rm: &ResourceModifiers{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "configmaps",
Namespaces: []string{"fake"},
},
MergePatches: []JSONMergePatch{
{
PatchData: `{"metadata":{"labels":{"a":null}}}`,
},
},
},
},
},
obj: cmWithoutLabelA.DeepCopy(),
groupResource: "configmaps",
wantErr: false,
wantObj: cmWithoutLabelA.DeepCopy(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.rm.ApplyResourceModifierRules(tt.obj, tt.groupResource, nil, logrus.New())
assert.Equal(t, tt.wantErr, len(got) > 0)
assert.Equal(t, *tt.wantObj, *tt.obj)
})
}
}
func TestResourceModifiers_wildcard_in_GroupResource(t *testing.T) {
unstructuredSerializer := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
o1, _, err := unstructuredSerializer.Decode([]byte(cmYAMLWithLabelAToB), nil, nil)
assert.NoError(t, err)
cmWithLabelAToB := o1.(*unstructured.Unstructured)
o2, _, err := unstructuredSerializer.Decode([]byte(cmYAMLWithLabelAToC), nil, nil)
assert.NoError(t, err)
cmWithLabelAToC := o2.(*unstructured.Unstructured)
tests := []struct {
name string
rm *ResourceModifiers
obj *unstructured.Unstructured
groupResource string
wantErr bool
wantObj *unstructured.Unstructured
}{
{
name: "match all groups and resources",
rm: &ResourceModifiers{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "*",
Namespaces: []string{"fake"},
},
MergePatches: []JSONMergePatch{
{
PatchData: `{"metadata":{"labels":{"a":"c"}}}`,
},
},
},
},
},
obj: cmWithLabelAToB.DeepCopy(),
groupResource: "configmaps",
wantErr: false,
wantObj: cmWithLabelAToC.DeepCopy(),
},
{
name: "match all resources in group apps",
rm: &ResourceModifiers{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "*.apps",
Namespaces: []string{"fake"},
},
MergePatches: []JSONMergePatch{
{
PatchData: `{"metadata":{"labels":{"a":"c"}}}`,
},
},
},
},
},
obj: cmWithLabelAToB.DeepCopy(),
groupResource: "fake.apps",
wantErr: false,
wantObj: cmWithLabelAToC.DeepCopy(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.rm.ApplyResourceModifierRules(tt.obj, tt.groupResource, nil, logrus.New())
assert.Equal(t, tt.wantErr, len(got) > 0)
assert.Equal(t, *tt.wantObj, *tt.obj)
})
}
}
func TestResourceModifiers_conditional_patches(t *testing.T) {
unstructuredSerializer := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
o1, _, err := unstructuredSerializer.Decode([]byte(cmYAMLWithLabelAToB), nil, nil)
assert.NoError(t, err)
cmWithLabelAToB := o1.(*unstructured.Unstructured)
o2, _, err := unstructuredSerializer.Decode([]byte(cmYAMLWithLabelAToC), nil, nil)
assert.NoError(t, err)
cmWithLabelAToC := o2.(*unstructured.Unstructured)
tests := []struct {
name string
rm *ResourceModifiers
obj *unstructured.Unstructured
groupResource string
wantErr bool
wantObj *unstructured.Unstructured
}{
{
name: "match conditions and apply patches",
rm: &ResourceModifiers{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "*",
Namespaces: []string{"fake"},
Matches: []MatchRule{
{
Path: "/metadata/labels/a",
Value: "b",
},
},
},
MergePatches: []JSONMergePatch{
{
PatchData: `{"metadata":{"labels":{"a":"c"}}}`,
},
},
},
},
},
obj: cmWithLabelAToB.DeepCopy(),
groupResource: "configmaps",
wantErr: false,
wantObj: cmWithLabelAToC.DeepCopy(),
},
{
name: "mismatch conditions and skip patches",
rm: &ResourceModifiers{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "*",
Namespaces: []string{"fake"},
Matches: []MatchRule{
{
Path: "/metadata/labels/a",
Value: "c",
},
},
},
MergePatches: []JSONMergePatch{
{
PatchData: `{"metadata":{"labels":{"a":"c"}}}`,
},
},
},
},
},
obj: cmWithLabelAToB.DeepCopy(),
groupResource: "configmaps",
wantErr: false,
wantObj: cmWithLabelAToB.DeepCopy(),
},
{
name: "missing condition path and skip patches",
rm: &ResourceModifiers{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "*",
Namespaces: []string{"fake"},
Matches: []MatchRule{
{
Path: "/metadata/labels/a/b",
Value: "c",
},
},
},
MergePatches: []JSONMergePatch{
{
PatchData: `{"metadata":{"labels":{"a":"c"}}}`,
},
},
},
},
},
obj: cmWithLabelAToB.DeepCopy(),
groupResource: "configmaps",
wantErr: false,
wantObj: cmWithLabelAToB.DeepCopy(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.rm.ApplyResourceModifierRules(tt.obj, tt.groupResource, nil, logrus.New())
assert.Equal(t, tt.wantErr, len(got) > 0)
assert.Equal(t, *tt.wantObj, *tt.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 integer",
fields: fields{
Operation: "replace",
Path: "/spec/replicas",
Value: "2",
},
want: `{"op": "replace", "from": "", "path": "/spec/replicas", "value": 2}`,
},
{
name: "replace array",
fields: fields{
Operation: "replace",
Path: "/spec/template/spec/containers/0/ports",
Value: `[{"containerPort": 80}]`,
},
want: `{"op": "replace", "from": "", "path": "/spec/template/spec/containers/0/ports", "value": [{"containerPort": 80}]}`,
},
{
name: "replace with null",
fields: fields{
Operation: "replace",
Path: "/spec/template/spec/containers/0/ports",
Value: `null`,
},
want: `{"op": "replace", "from": "", "path": "/spec/template/spec/containers/0/ports", "value": null}`,
},
{
name: "add json object",
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)
}
})
}
}