Merge pull request #6917 from 27149chen/rm-improvement

support JSON Merge Patch and Strategic Merge Patch in Resource Modifiers
This commit is contained in:
Wenkai Yin(尹文开)
2023-11-02 10:36:40 +08:00
committed by GitHub
15 changed files with 1399 additions and 78 deletions
+1
View File
@@ -0,0 +1 @@
Support JSON Merge Patch and Strategic Merge Patch in Resource Modifiers
@@ -88,10 +88,10 @@ Example of StrategicPatches in ResourceModifierRule
version: v1
resourceModifierRules:
- conditions:
groupResource: pods
resourceNameRegex: "^my-pod$"
namespaces:
- ns1
groupResource: pods
resourceNameRegex: "^my-pod$"
namespaces:
- ns1
strategicPatches:
- patchData: |
{
+1 -1
View File
@@ -64,6 +64,7 @@ require (
k8s.io/metrics v0.25.6
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed
sigs.k8s.io/controller-runtime v0.12.2
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd
sigs.k8s.io/yaml v1.3.0
)
@@ -184,7 +185,6 @@ require (
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/component-base v0.24.2 // indirect
k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
)
+2 -2
View File
@@ -1428,8 +1428,8 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30/go.mod h1:fEO7lR
sigs.k8s.io/controller-runtime v0.12.2 h1:nqV02cvhbAj7tbt21bpPpTByrXGn2INHRsi39lXy9sE=
sigs.k8s.io/controller-runtime v0.12.2/go.mod h1:qKsk4WE6zW2Hfj0G4v10EnNB2jMG1C+NTb8h+DwCoU0=
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY=
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k=
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/kustomize/api v0.8.11/go.mod h1:a77Ls36JdfCWojpUqR6m60pdGY1AYFix4AH83nJtY1g=
sigs.k8s.io/kustomize/api v0.11.4/go.mod h1:k+8RsqYbgpkIrJ4p9jcdPqe8DprLxFUUO0yNOq8C+xI=
sigs.k8s.io/kustomize/kyaml v0.11.0/go.mod h1:GNMwjim4Ypgp/MueD3zXHLRJEjz7RvtPae0AwlvEMFM=
@@ -0,0 +1,45 @@
package resourcemodifiers
import (
"fmt"
jsonpatch "github.com/evanphx/json-patch"
"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/yaml"
)
type JSONMergePatch struct {
PatchData string `json:"patchData,omitempty"`
}
type JSONMergePatcher struct {
patches []JSONMergePatch
}
func (p *JSONMergePatcher) Patch(u *unstructured.Unstructured, _ logrus.FieldLogger) (*unstructured.Unstructured, error) {
objBytes, err := u.MarshalJSON()
if err != nil {
return nil, fmt.Errorf("error in marshaling object %s", err)
}
for _, patch := range p.patches {
patchBytes, err := yaml.YAMLToJSON([]byte(patch.PatchData))
if err != nil {
return nil, fmt.Errorf("error in converting YAML to JSON %s", err)
}
objBytes, err = jsonpatch.MergePatch(objBytes, patchBytes)
if err != nil {
return nil, fmt.Errorf("error in applying JSON Patch: %s", err.Error())
}
}
updated := &unstructured.Unstructured{}
err = updated.UnmarshalJSON(objBytes)
if err != nil {
return nil, fmt.Errorf("error in unmarshalling modified object %s", err.Error())
}
return updated, nil
}
@@ -0,0 +1,41 @@
package resourcemodifiers
import (
"testing"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
)
func TestJsonMergePatchFailure(t *testing.T) {
tests := []struct {
name string
data string
}{
{
name: "patch with bad yaml",
data: "a: b:",
},
{
name: "patch with bad json",
data: `{"a"::1}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
scheme := runtime.NewScheme()
err := clientgoscheme.AddToScheme(scheme)
assert.NoError(t, err)
pt := &JSONMergePatcher{
patches: []JSONMergePatch{{PatchData: tt.data}},
}
u := &unstructured.Unstructured{}
_, err = pt.Patch(u, logrus.New())
assert.Error(t, err)
})
}
}
+96
View File
@@ -0,0 +1,96 @@
package resourcemodifiers
import (
"errors"
"fmt"
"strconv"
"strings"
jsonpatch "github.com/evanphx/json-patch"
"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
type JSONPatch struct {
Operation string `json:"operation"`
From string `json:"from,omitempty"`
Path string `json:"path"`
Value string `json:"value,omitempty"`
}
func (p *JSONPatch) ToString() string {
if addQuotes(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 addQuotes(value string) bool {
if value == "" {
return true
}
// if value is null, then don't add quotes
if value == "null" {
return false
}
// if value is a boolean, then don't add quotes
if _, err := strconv.ParseBool(value); err == nil {
return false
}
// if value is a json object or array, then don't add quotes.
if strings.HasPrefix(value, "{") || strings.HasPrefix(value, "[") {
return false
}
// if value is a number, then don't add quotes
if _, err := strconv.ParseFloat(value, 64); err == nil {
return false
}
return true
}
type JSONPatcher struct {
patches []JSONPatch `yaml:"patches"`
}
func (p *JSONPatcher) Patch(u *unstructured.Unstructured, logger logrus.FieldLogger) (*unstructured.Unstructured, error) {
modifiedObjBytes, err := p.applyPatch(u)
if err != nil {
if errors.Is(err, jsonpatch.ErrTestFailed) {
logger.Infof("Test operation failed for JSON Patch %s", err.Error())
return u.DeepCopy(), nil
}
return nil, fmt.Errorf("error in applying JSON Patch %s", err.Error())
}
updated := &unstructured.Unstructured{}
err = updated.UnmarshalJSON(modifiedObjBytes)
if err != nil {
return nil, fmt.Errorf("error in unmarshalling modified object %s", err.Error())
}
return updated, nil
}
func (p *JSONPatcher) applyPatch(u *unstructured.Unstructured) ([]byte, error) {
patchBytes := p.patchArrayToByteArray()
jsonPatch, err := jsonpatch.DecodePatch(patchBytes)
if err != nil {
return nil, fmt.Errorf("error in decoding json patch %s", err.Error())
}
objBytes, err := u.MarshalJSON()
if err != nil {
return nil, fmt.Errorf("error in marshaling object %s", err.Error())
}
return jsonPatch.Apply(objBytes)
}
func (p *JSONPatcher) patchArrayToByteArray() []byte {
var patches []string
for _, patch := range p.patches {
patches = append(patches, patch.ToString())
}
patchesStr := strings.Join(patches, ",\n\t")
return []byte(fmt.Sprintf(`[%s]`, patchesStr))
}
@@ -18,16 +18,16 @@ package resourcemodifiers
import (
"fmt"
"regexp"
"strconv"
"strings"
jsonpatch "github.com/evanphx/json-patch"
"github.com/gobwas/glob"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
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/labels"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/yaml"
"github.com/vmware-tanzu/velero/pkg/util/collections"
@@ -38,11 +38,9 @@ const (
ResourceModifierSupportedVersionV1 = "v1"
)
type JSONPatch struct {
Operation string `json:"operation"`
From string `json:"from,omitempty"`
Path string `json:"path"`
Value string `json:"value,omitempty"`
type MatchRule struct {
Path string `json:"path,omitempty"`
Value string `json:"value,omitempty"`
}
type Conditions struct {
@@ -50,11 +48,14 @@ type Conditions struct {
GroupResource string `json:"groupResource"`
ResourceNameRegex string `json:"resourceNameRegex,omitempty"`
LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"`
Matches []MatchRule `json:"matches,omitempty"`
}
type ResourceModifierRule struct {
Conditions Conditions `json:"conditions"`
Patches []JSONPatch `json:"patches"`
Conditions Conditions `json:"conditions"`
Patches []JSONPatch `json:"patches,omitempty"`
MergePatches []JSONMergePatch `json:"mergePatches,omitempty"`
StrategicPatches []StrategicMergePatch `json:"strategicPatches,omitempty"`
}
type ResourceModifiers struct {
@@ -83,10 +84,10 @@ func GetResourceModifiersFromConfig(cm *v1.ConfigMap) (*ResourceModifiers, error
return resModifiers, nil
}
func (p *ResourceModifiers) ApplyResourceModifierRules(obj *unstructured.Unstructured, groupResource string, log logrus.FieldLogger) []error {
func (p *ResourceModifiers) ApplyResourceModifierRules(obj *unstructured.Unstructured, groupResource string, scheme *runtime.Scheme, log logrus.FieldLogger) []error {
var errs []error
for _, rule := range p.ResourceModifierRules {
err := rule.Apply(obj, groupResource, log)
err := rule.apply(obj, groupResource, scheme, log)
if err != nil {
errs = append(errs, err)
}
@@ -95,13 +96,22 @@ func (p *ResourceModifiers) ApplyResourceModifierRules(obj *unstructured.Unstruc
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
func (r *ResourceModifierRule) apply(obj *unstructured.Unstructured, groupResource string, scheme *runtime.Scheme, log logrus.FieldLogger) error {
ns := obj.GetNamespace()
if ns != "" {
namespaceInclusion := collections.NewIncludesExcludes().Includes(r.Conditions.Namespaces...)
if !namespaceInclusion.ShouldInclude(ns) {
return nil
}
}
if r.Conditions.GroupResource != groupResource {
g, err := glob.Compile(r.Conditions.GroupResource, '.')
if err != nil {
log.Errorf("Bad glob pattern of groupResource in condition, groupResource: %s, err: %s", r.Conditions.GroupResource, err)
return err
}
if !g.Match(groupResource) {
return nil
}
@@ -125,87 +135,82 @@ func (r *ResourceModifierRule) Apply(obj *unstructured.Unstructured, groupResour
}
}
patches, err := r.PatchArrayToByteArray()
match, err := matchConditions(obj, r.Conditions.Matches, log)
if err != nil {
return err
} else if !match {
log.Info("Conditions do not match, skip it")
return nil
}
log.Infof("Applying resource modifier patch on %s/%s", obj.GetNamespace(), obj.GetName())
err = ApplyPatch(patches, obj, log)
err = r.applyPatch(obj, scheme, log)
if err != nil {
return err
}
return nil
}
// PatchArrayToByteArray converts 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())
func matchConditions(u *unstructured.Unstructured, rules []MatchRule, _ logrus.FieldLogger) (bool, error) {
if len(rules) == 0 {
return true, nil
}
patchesStr := strings.Join(patches, ",\n\t")
return []byte(fmt.Sprintf(`[%s]`, patchesStr)), nil
}
func (p *JSONPatch) ToString() string {
if addQuotes(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)
}
var fixed []JSONPatch
for _, rule := range rules {
if rule.Path == "" {
return false, fmt.Errorf("path is required for match rule")
}
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())
fixed = append(fixed, JSONPatch{
Operation: "test",
Path: rule.Path,
Value: rule.Value,
})
}
objBytes, err := obj.MarshalJSON()
if err != nil {
return fmt.Errorf("error in marshaling object %s", err.Error())
}
modifiedObjBytes, err := jsonPatch.Apply(objBytes)
p := &JSONPatcher{patches: fixed}
_, err := p.applyPatch(u)
if err != nil {
if errors.Is(err, jsonpatch.ErrTestFailed) {
log.Infof("Test operation failed for JSON Patch %s", err.Error())
return nil
return false, nil
}
return fmt.Errorf("error in applying JSON Patch %s", err.Error())
return false, err
}
err = obj.UnmarshalJSON(modifiedObjBytes)
if err != nil {
return fmt.Errorf("error in unmarshalling modified object %s", err.Error())
}
return nil
return true, nil
}
func unmarshalResourceModifiers(yamlData []byte) (*ResourceModifiers, error) {
resModifiers := &ResourceModifiers{}
err := yaml.UnmarshalStrict(yamlData, resModifiers)
if err != nil {
return nil, fmt.Errorf("failed to decode yaml data into resource modifiers %v", err)
return nil, fmt.Errorf("failed to decode yaml data into resource modifiers, err: %s", err)
}
return resModifiers, nil
}
func addQuotes(value string) bool {
if value == "" {
return true
type patcher interface {
Patch(u *unstructured.Unstructured, logger logrus.FieldLogger) (*unstructured.Unstructured, error)
}
func (r *ResourceModifierRule) applyPatch(u *unstructured.Unstructured, scheme *runtime.Scheme, logger logrus.FieldLogger) error {
var p patcher
if len(r.Patches) > 0 {
p = &JSONPatcher{patches: r.Patches}
} else if len(r.MergePatches) > 0 {
p = &JSONMergePatcher{patches: r.MergePatches}
} else if len(r.StrategicPatches) > 0 {
p = &StrategicMergePatcher{patches: r.StrategicPatches, scheme: scheme}
} else {
return fmt.Errorf("no patch data found")
}
// if value is null, then don't add quotes
if value == "null" {
return false
updated, err := p.Patch(u, logger)
if err != nil {
return fmt.Errorf("error in applying patch %s", err)
}
// if value is a boolean, then don't add quotes
if _, err := strconv.ParseBool(value); err == nil {
return false
}
// if value is a json object or array, then don't add quotes.
if strings.HasPrefix(value, "{") || strings.HasPrefix(value, "[") {
return false
}
// if value is a number, then don't add quotes
if _, err := strconv.ParseFloat(value, 64); err == nil {
return false
}
return true
u.SetUnstructuredContent(updated.Object)
return nil
}
@@ -24,6 +24,10 @@ import (
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) {
@@ -131,6 +135,128 @@ func TestGetResourceModifiersFromConfig(t *testing.T) {
},
}
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"}]}}`,
},
},
},
},
}
type args struct {
cm *v1.ConfigMap
}
@@ -180,6 +306,38 @@ func TestGetResourceModifiersFromConfig(t *testing.T) {
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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -502,6 +660,38 @@ func TestResourceModifiers_ApplyResourceModifierRules(t *testing.T) {
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{
@@ -719,7 +909,7 @@ func TestResourceModifiers_ApplyResourceModifierRules(t *testing.T) {
Version: tt.fields.Version,
ResourceModifierRules: tt.fields.ResourceModifierRules,
}
got := p.ApplyResourceModifierRules(tt.args.obj, tt.args.groupResource, logrus.New())
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)
@@ -727,6 +917,633 @@ func TestResourceModifiers_ApplyResourceModifierRules(t *testing.T) {
}
}
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(),
},
}
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
@@ -24,6 +24,21 @@ func (r *ResourceModifierRule) Validate() error {
if err := r.Conditions.Validate(); err != nil {
return err
}
count := 0
for _, size := range []int{
len(r.Patches),
len(r.MergePatches),
len(r.StrategicPatches),
} {
if size != 0 {
count++
}
if count >= 2 {
return fmt.Errorf("only one of patches, mergePatches, strategicPatches can be specified")
}
}
for _, patch := range r.Patches {
if err := patch.Validate(); err != nil {
return err
@@ -129,6 +129,32 @@ func TestResourceModifiers_Validate(t *testing.T) {
},
wantErr: true,
},
{
name: "More than one patch type in a rule",
fields: fields{
Version: "v1",
ResourceModifierRules: []ResourceModifierRule{
{
Conditions: Conditions{
GroupResource: "*",
},
Patches: []JSONPatch{
{
Operation: "test",
Path: "/spec/storageClassName",
Value: "premium",
},
},
MergePatches: []JSONMergePatch{
{
PatchData: `{"metadata":{"labels":{"a":null}}}`,
},
},
},
},
},
wantErr: true,
},
}
for _, tt := range tests {
@@ -0,0 +1,143 @@
package resourcemodifiers
import (
"fmt"
"net/http"
"github.com/sirupsen/logrus"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/mergepatch"
"k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/apimachinery/pkg/util/validation/field"
kubejson "sigs.k8s.io/json"
"sigs.k8s.io/yaml"
)
type StrategicMergePatch struct {
PatchData string `json:"patchData,omitempty"`
}
type StrategicMergePatcher struct {
patches []StrategicMergePatch
scheme *runtime.Scheme
}
func (p *StrategicMergePatcher) Patch(u *unstructured.Unstructured, _ logrus.FieldLogger) (*unstructured.Unstructured, error) {
gvk := u.GetObjectKind().GroupVersionKind()
schemaReferenceObj, err := p.scheme.New(gvk)
if err != nil {
return nil, err
}
origin := u.DeepCopy()
updated := u.DeepCopy()
for _, patch := range p.patches {
patchBytes, err := yaml.YAMLToJSON([]byte(patch.PatchData))
if err != nil {
return nil, fmt.Errorf("error in converting YAML to JSON %s", err)
}
err = strategicPatchObject(origin, patchBytes, updated, schemaReferenceObj)
if err != nil {
return nil, fmt.Errorf("error in applying Strategic Patch %s", err.Error())
}
origin = updated.DeepCopy()
}
return updated, nil
}
// strategicPatchObject applies a strategic merge patch of `patchBytes` to
// `originalObject` and stores the result in `objToUpdate`.
// It additionally returns the map[string]interface{} representation of the
// `originalObject` and `patchBytes`.
// NOTE: Both `originalObject` and `objToUpdate` are supposed to be versioned.
func strategicPatchObject(
originalObject runtime.Object,
patchBytes []byte,
objToUpdate runtime.Object,
schemaReferenceObj runtime.Object,
) error {
originalObjMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(originalObject)
if err != nil {
return err
}
patchMap := make(map[string]interface{})
var strictErrs []error
strictErrs, err = kubejson.UnmarshalStrict(patchBytes, &patchMap)
if err != nil {
return apierrors.NewBadRequest(err.Error())
}
if err := applyPatchToObject(originalObjMap, patchMap, objToUpdate, schemaReferenceObj, strictErrs); err != nil {
return err
}
return nil
}
// applyPatchToObject applies a strategic merge patch of <patchMap> to
// <originalMap> and stores the result in <objToUpdate>.
// NOTE: <objToUpdate> must be a versioned object.
func applyPatchToObject(
originalMap map[string]interface{},
patchMap map[string]interface{},
objToUpdate runtime.Object,
schemaReferenceObj runtime.Object,
strictErrs []error,
) error {
patchedObjMap, err := strategicpatch.StrategicMergeMapPatch(originalMap, patchMap, schemaReferenceObj)
if err != nil {
return interpretStrategicMergePatchError(err)
}
// Rather than serialize the patched map to JSON, then decode it to an object, we go directly from a map to an object
converter := runtime.DefaultUnstructuredConverter
if err := converter.FromUnstructuredWithValidation(patchedObjMap, objToUpdate, true); err != nil {
strictError, isStrictError := runtime.AsStrictDecodingError(err)
switch {
case !isStrictError:
// disregard any sttrictErrs, because it's an incomplete
// list of strict errors given that we don't know what fields were
// unknown because StrategicMergeMapPatch failed.
// Non-strict errors trump in this case.
return apierrors.NewInvalid(schema.GroupKind{}, "", field.ErrorList{
field.Invalid(field.NewPath("patch"), fmt.Sprintf("%+v", patchMap), err.Error()),
})
//case validationDirective == metav1.FieldValidationWarn:
// addStrictDecodingWarnings(requestContext, append(strictErrs, strictError.Errors()...))
default:
strictDecodingError := runtime.NewStrictDecodingError(append(strictErrs, strictError.Errors()...))
return apierrors.NewInvalid(schema.GroupKind{}, "", field.ErrorList{
field.Invalid(field.NewPath("patch"), fmt.Sprintf("%+v", patchMap), strictDecodingError.Error()),
})
}
} else if len(strictErrs) > 0 {
switch {
//case validationDirective == metav1.FieldValidationWarn:
// addStrictDecodingWarnings(requestContext, strictErrs)
default:
return apierrors.NewInvalid(schema.GroupKind{}, "", field.ErrorList{
field.Invalid(field.NewPath("patch"), fmt.Sprintf("%+v", patchMap), runtime.NewStrictDecodingError(strictErrs).Error()),
})
}
}
return nil
}
// interpretStrategicMergePatchError interprets the error type and returns an error with appropriate HTTP code.
func interpretStrategicMergePatchError(err error) error {
switch err {
case mergepatch.ErrBadJSONDoc, mergepatch.ErrBadPatchFormatForPrimitiveList, mergepatch.ErrBadPatchFormatForRetainKeys, mergepatch.ErrBadPatchFormatForSetElementOrderList, mergepatch.ErrUnsupportedStrategicMergePatchFormat:
return apierrors.NewBadRequest(err.Error())
case mergepatch.ErrNoListOfLists, mergepatch.ErrPatchContentNotMatchRetainKeys:
return apierrors.NewGenericServerResponse(http.StatusUnprocessableEntity, "", schema.GroupResource{}, "", err.Error(), 0, false)
default:
return err
}
}
@@ -0,0 +1,52 @@
package resourcemodifiers
import (
"testing"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
)
func TestStrategicMergePatchFailure(t *testing.T) {
tests := []struct {
name string
data string
kind string
}{
{
name: "patch with unknown kind",
data: "{}",
kind: "BadKind",
},
{
name: "patch with bad yaml",
data: "a: b:",
kind: "Pod",
},
{
name: "patch with bad json",
data: `{"a"::1}`,
kind: "Pod",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
scheme := runtime.NewScheme()
err := clientgoscheme.AddToScheme(scheme)
assert.NoError(t, err)
pt := &StrategicMergePatcher{
patches: []StrategicMergePatch{{PatchData: tt.data}},
scheme: scheme,
}
u := &unstructured.Unstructured{}
u.SetGroupVersionKind(schema.GroupVersionKind{Version: "v1", Kind: tt.kind})
_, err = pt.Patch(u, logrus.New())
assert.Error(t, err)
})
}
}
+1 -1
View File
@@ -1474,7 +1474,7 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
}
if ctx.resourceModifiers != nil {
if errList := ctx.resourceModifiers.ApplyResourceModifierRules(obj, groupResource.String(), ctx.log); errList != nil {
if errList := ctx.resourceModifiers.ApplyResourceModifierRules(obj, groupResource.String(), ctx.kbClient.Scheme(), ctx.log); errList != nil {
for _, err := range errList {
errs.Add(namespace, err)
}
@@ -105,3 +105,83 @@ resourceModifierRules:
- Update a container's image using a json patch with positional arrays
kubectl patch pod valid-pod -type='json' -p='[{"op": "replace", "path": "/spec/containers/0/image", "value":"new image"}]'
- Before creating the resource modifier yaml, you can try it out using kubectl patch command. The same commands should work as it is.
#### JSON Merge Patch
You can modify a resource using JSON Merge Patch
```yaml
version: v1
resourceModifierRules:
- conditions:
groupResource: pods
namespaces:
- ns1
mergePatches:
- patchData: |
{
"metadata": {
"annotations": {
"foo": null
}
}
}
```
- The above configmap will apply the Merge Patch to all the pods in namespace ns1 and remove the annotation `foo` from the pods.
- Both json and yaml format are supported for the patchData.
- For more details, please refer to [this doc](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/)
#### Strategic Merge Patch
You can modify a resource using Strategic Merge Patch
```yaml
version: v1
resourceModifierRules:
- conditions:
groupResource: pods
resourceNameRegex: "^my-pod$"
namespaces:
- ns1
strategicPatches:
- patchData: |
{
"spec": {
"containers": [
{
"name": "nginx",
"image": "repo2/nginx"
}
]
}
}
```
- The above configmap will apply the Strategic Merge Patch to the pod with name my-pod in namespace ns1 and update the image of container nginx to `repo2/nginx`.
- Both json and yaml format are supported for the patchData.
- For more details, please refer to [this doc](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/)
### Conditional Patches in ALL Patch Types
A new field `matches` is added in conditions to support conditional patches.
Example of matches in conditions
```yaml
version: v1
resourceModifierRules:
- conditions:
groupResource: persistentvolumeclaims.storage.k8s.io
matches:
- path: "/spec/storageClassName"
value: "premium"
mergePatches:
- patchData: |
{
"metadata": {
"annotations": {
"foo": null
}
}
}
```
- The above configmap will apply the Merge Patch to all the PVCs in all namespaces with storageClassName premium and remove the annotation `foo` from the PVCs.
- You can specify multiple rules in the `matches` list. The patch will be applied only if all the matches are satisfied.
### Wildcard Support for GroupResource
The user can specify a wildcard for groupResource in the conditions' struct. This will allow the user to apply the patches for all the resources of a particular group or all resources in all groups. For example, `*.apps` will apply to all the resources in the `apps` group, `*` will apply to all the resources in core group, `*.*` will apply to all the resources in all groups.
- If both `*.groupName` and `namespaces` are specified, the patches will be applied to all the namespaced resources in this group in the specified namespaces and all the cluster resources in this group.