support JSON Merge Patch and Strategic Merge Patch in Resource Modifiers

Signed-off-by: lou <alex1988@outlook.com>
This commit is contained in:
lou
2023-09-25 18:00:18 +08:00
parent 4bf87c01ea
commit d8b9328310
6 changed files with 993 additions and 71 deletions

View File

@@ -0,0 +1,46 @@
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 {
PatchBytes []byte `json:"patchBytes,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)
}
var modifiedObjBytes []byte
for _, patch := range p.patches {
patchBytes, err := yaml.YAMLToJSON(patch.PatchBytes)
if err != nil {
return nil, fmt.Errorf("error in converting YAML to JSON %s", err)
}
modifiedObjBytes, 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(modifiedObjBytes)
if err != nil {
return nil, fmt.Errorf("error in unmarshalling modified object %s", err.Error())
}
return updated, nil
}

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))
}

View File

@@ -3,16 +3,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"
@@ -23,23 +23,20 @@ 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 Conditions struct {
Namespaces []string `json:"namespaces,omitempty"`
GroupResource string `json:"groupResource"`
ResourceNameRegex string `json:"resourceNameRegex,omitempty"`
LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"`
Matches []JSONPatch `json:"matches,omitempty"`
}
type ResourceModifierRule struct {
Conditions Conditions `json:"conditions"`
Patches []JSONPatch `json:"patches"`
Conditions Conditions `json:"conditions"`
PatchData string `json:"patchData,omitempty"`
Patches []JSONPatch `json:"patches,omitempty"`
MergePatches []JSONMergePatch `json:"mergePatches,omitempty"`
StrategicPatches []StrategicMergePatch `json:"strategicPatches,omitempty"`
}
type ResourceModifiers struct {
@@ -68,10 +65,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)
}
@@ -80,13 +77,19 @@ func (p *ResourceModifiers) ApplyResourceModifierRules(obj *unstructured.Unstruc
return errs
}
func (r *ResourceModifierRule) Apply(obj *unstructured.Unstructured, groupResource string, log logrus.FieldLogger) error {
func (r *ResourceModifierRule) apply(obj *unstructured.Unstructured, groupResource string, scheme *runtime.Scheme, log logrus.FieldLogger) error {
namespaceInclusion := collections.NewIncludesExcludes().Includes(r.Conditions.Namespaces...)
if !namespaceInclusion.ShouldInclude(obj.GetNamespace()) {
return nil
}
if r.Conditions.GroupResource != groupResource {
g, err := glob.Compile(r.Conditions.GroupResource)
if err != nil {
log.Errorf("bad glob pattern %s, err: %s", r.Conditions.GroupResource, err)
return err
}
if !g.Match(groupResource) {
return nil
}
@@ -110,57 +113,44 @@ 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, patches []JSONPatch, logger logrus.FieldLogger) (bool, error) {
if len(patches) == 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)
var fixed []JSONPatch
for _, patch := range patches {
patch.From = ""
patch.Operation = "test"
fixed = append(fixed, patch)
}
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)
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) {
@@ -172,25 +162,27 @@ func unmarshalResourceModifiers(yamlData []byte) (*ResourceModifiers, error) {
return resModifiers, nil
}
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 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")
}
updated, err := p.Patch(u, logger)
if err != nil {
return fmt.Errorf("error in applying patch %s", err)
}
u.SetUnstructuredContent(updated.Object)
return nil
}

View File

@@ -9,6 +9,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) {
@@ -704,7 +708,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)
@@ -712,6 +716,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{
{
PatchBytes: []byte(`{"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{
{
PatchBytes: []byte(`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{
{
PatchBytes: []byte(`{"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{
{
PatchBytes: []byte(`{"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{
{
PatchBytes: []byte(`{"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{
{
PatchBytes: []byte(`{"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{
{
PatchBytes: []byte(`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{
{
PatchBytes: []byte(`{"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{
{
PatchBytes: []byte(`{"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{
{
PatchBytes: []byte(`{"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{
{
PatchBytes: []byte(`{"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{
{
PatchBytes: []byte(`{"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: []JSONPatch{
{
Path: "/metadata/labels/a",
Value: "b",
},
},
},
MergePatches: []JSONMergePatch{
{
PatchBytes: []byte(`{"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: []JSONPatch{
{
Path: "/metadata/labels/a",
Value: "c",
},
},
},
MergePatches: []JSONMergePatch{
{
PatchBytes: []byte(`{"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

View File

@@ -0,0 +1,157 @@
package resourcemodifiers
import (
"fmt"
"net/http"
"github.com/sirupsen/logrus"
apierrors "k8s.io/apimachinery/pkg/api/errors"
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/schema"
"k8s.io/apimachinery/pkg/util/mergepatch"
"k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/apimachinery/pkg/util/validation/field"
"sigs.k8s.io/json"
"sigs.k8s.io/yaml"
)
type StrategicMergePatch struct {
PatchBytes []byte `json:"patchBytes,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
}
if err != nil {
return nil, fmt.Errorf("error in unmarshaling object %s", err.Error())
}
origin := u.DeepCopy()
updated := u.DeepCopy()
for _, patch := range p.patches {
patchBytes, err := yaml.YAMLToJSON(patch.PatchBytes)
if err != nil {
return nil, fmt.Errorf("error in converting YAML to JSON %s", err)
}
err = strategicPatchObject(origin, patchBytes, updated, schemaReferenceObj, metav1.FieldValidationStrict)
if err != nil {
return nil, fmt.Errorf("error in applying JSON 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,
validationDirective string,
) error {
originalObjMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(originalObject)
if err != nil {
return err
}
patchMap := make(map[string]interface{})
var strictErrs []error
if validationDirective == metav1.FieldValidationWarn || validationDirective == metav1.FieldValidationStrict {
strictErrs, err = json.UnmarshalStrict(patchBytes, &patchMap)
if err != nil {
return apierrors.NewBadRequest(err.Error())
}
} else {
if err = json.UnmarshalCaseSensitivePreserveInts(patchBytes, &patchMap); err != nil {
return apierrors.NewBadRequest(err.Error())
}
}
if err := applyPatchToObject(originalObjMap, patchMap, objToUpdate, schemaReferenceObj, strictErrs, validationDirective); 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,
validationDirective string,
) 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
returnUnknownFields := validationDirective == metav1.FieldValidationWarn || validationDirective == metav1.FieldValidationStrict
if err := converter.FromUnstructuredWithValidation(patchedObjMap, objToUpdate, returnUnknownFields); 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
}
}

View File

@@ -1353,7 +1353,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)
}