Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support JSON Merge Patch and Strategic Merge Patch in Resource Modifiers #6917

Merged
merged 13 commits into from
Nov 2, 2023
1 change: 1 addition & 0 deletions changelogs/unreleased/6917-27149chen
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support JSON Merge Patch and Strategic Merge Patch in Resource Modifiers
8 changes: 4 additions & 4 deletions design/merge-patch-and-strategic-in-resource-modifier.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
27149chen marked this conversation as resolved.
Show resolved Hide resolved
{
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,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
)

Expand Down Expand Up @@ -163,7 +164,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
)

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1392,8 +1392,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=
Expand Down
45 changes: 45 additions & 0 deletions internal/resourcemodifiers/json_merge_patch.go
Original file line number Diff line number Diff line change
@@ -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)
}

Check warning on line 24 in internal/resourcemodifiers/json_merge_patch.go

View check run for this annotation

Codecov / codecov/patch

internal/resourcemodifiers/json_merge_patch.go#L23-L24

Added lines #L23 - L24 were not covered by tests

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

Check warning on line 35 in internal/resourcemodifiers/json_merge_patch.go

View check run for this annotation

Codecov / codecov/patch

internal/resourcemodifiers/json_merge_patch.go#L34-L35

Added lines #L34 - L35 were not covered by tests
}

updated := &unstructured.Unstructured{}
err = updated.UnmarshalJSON(objBytes)
if err != nil {
return nil, fmt.Errorf("error in unmarshalling modified object %s", err.Error())
}

Check warning on line 42 in internal/resourcemodifiers/json_merge_patch.go

View check run for this annotation

Codecov / codecov/patch

internal/resourcemodifiers/json_merge_patch.go#L41-L42

Added lines #L41 - L42 were not covered by tests

return updated, nil
}
41 changes: 41 additions & 0 deletions internal/resourcemodifiers/json_merge_patch_test.go
Original file line number Diff line number Diff line change
@@ -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 changes: 96 additions & 0 deletions internal/resourcemodifiers/json_patch.go
Original file line number Diff line number Diff line change
@@ -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())

Check warning on line 62 in internal/resourcemodifiers/json_patch.go

View check run for this annotation

Codecov / codecov/patch

internal/resourcemodifiers/json_patch.go#L62

Added line #L62 was not covered by tests
}

updated := &unstructured.Unstructured{}
err = updated.UnmarshalJSON(modifiedObjBytes)
if err != nil {
return nil, fmt.Errorf("error in unmarshalling modified object %s", err.Error())
}

Check warning on line 69 in internal/resourcemodifiers/json_patch.go

View check run for this annotation

Codecov / codecov/patch

internal/resourcemodifiers/json_patch.go#L68-L69

Added lines #L68 - L69 were not covered by tests

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

Check warning on line 79 in internal/resourcemodifiers/json_patch.go

View check run for this annotation

Codecov / codecov/patch

internal/resourcemodifiers/json_patch.go#L78-L79

Added lines #L78 - L79 were not covered by tests

objBytes, err := u.MarshalJSON()
if err != nil {
return nil, fmt.Errorf("error in marshaling object %s", err.Error())
}

Check warning on line 84 in internal/resourcemodifiers/json_patch.go

View check run for this annotation

Codecov / codecov/patch

internal/resourcemodifiers/json_patch.go#L83-L84

Added lines #L83 - L84 were not covered by tests

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