From e4dba3163dad08b30b4e933ed8164dffeb41ab0e Mon Sep 17 00:00:00 2001 From: Jeremy Gustie Date: Fri, 9 Feb 2024 12:17:54 -0500 Subject: [PATCH] Add a patch filter for applying patches --- go.mod | 3 +- go.sum | 2 + pkg/filters/patch.go | 89 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 pkg/filters/patch.go diff --git a/go.mod b/go.mod index 5ba58f2..8122512 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/thestormforge/konjure go 1.21 require ( + github.com/evanphx/json-patch/v5 v5.8.1 github.com/fatih/color v1.16.0 github.com/google/go-jsonnet v0.20.0 github.com/google/uuid v1.6.0 @@ -16,6 +17,7 @@ require ( golang.org/x/sync v0.6.0 k8s.io/kube-openapi v0.0.0-20230601164746-7562a1006961 sigs.k8s.io/kustomize/kyaml v0.16.0 + sigs.k8s.io/yaml v1.4.0 ) require ( @@ -40,5 +42,4 @@ require ( golang.org/x/sys v0.16.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index b040271..705c841 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/evanphx/json-patch/v5 v5.8.1 h1:iPEdwg0XayoS+E7Mth9JxwUtOgyVxnDTXHtKhZPlZxA= +github.com/evanphx/json-patch/v5 v5.8.1/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= diff --git a/pkg/filters/patch.go b/pkg/filters/patch.go new file mode 100644 index 0000000..b0d6fa7 --- /dev/null +++ b/pkg/filters/patch.go @@ -0,0 +1,89 @@ +package filters + +import ( + "bytes" + "fmt" + + jsonpatch "github.com/evanphx/json-patch/v5" + "sigs.k8s.io/kustomize/kyaml/yaml" + "sigs.k8s.io/kustomize/kyaml/yaml/merge2" + yaml2 "sigs.k8s.io/yaml" +) + +// UnsupportedPatchError is raised when a patch format is not recognized. +type UnsupportedPatchError struct { + PatchType string +} + +func (e *UnsupportedPatchError) Error() string { + return fmt.Sprintf("unsupported patch type: %q", e.PatchType) +} + +// PatchFilter is used to apply an arbitrary patch. +type PatchFilter struct { + // The media type of the patch being applied. + PatchType string + // The actual raw patch. + PatchData []byte +} + +// Filter applies the configured patch. +func (f *PatchFilter) Filter(node *yaml.RNode) (*yaml.RNode, error) { + switch f.PatchType { + case "application/strategic-merge-patch+json", "strategic", "application/merge-patch+json", "merge", "": + // The patch is likely JSON, parse it as YAML and just clear the style + patchNode := yaml.NewRNode(&yaml.Node{}) + if err := yaml.NewDecoder(bytes.NewReader(f.PatchData)).Decode(patchNode.YNode()); err != nil { + return nil, err + } + f.resetNodeStyle(patchNode.YNode()) + + // Strategic Merge/Merge Patch is just the merge2 logic + opts := yaml.MergeOptions{ + ListIncreaseDirection: yaml.MergeOptionsListPrepend, + } + return merge2.Merge(patchNode, node, opts) + + case "application/json-patch+json", "json": + // The patch is likely JSON, but might be YAML that needs to be converted to JSON + patchData := f.PatchData + if !bytes.HasPrefix(patchData, []byte("[")) { + jsonData, err := yaml2.YAMLToJSON(patchData) + if err != nil { + return nil, err + } + patchData = jsonData + } + jsonPatch, err := jsonpatch.DecodePatch(patchData) + if err != nil { + return nil, err + } + + // This is going to butcher the YAML ordering/comments/etc. + jsonData, err := node.MarshalJSON() + if err != nil { + return nil, err + } + jsonData, err = jsonPatch.Apply(jsonData) + if err != nil { + return nil, err + } + err = node.UnmarshalJSON(jsonData) + if err != nil { + return nil, err + } + return node, nil + + default: + // This patch type is not supported + return nil, &UnsupportedPatchError{PatchType: f.PatchType} + } +} + +// resetNodeStyle clears out the node style, this is useful to discard JSON formatting. +func (f *PatchFilter) resetNodeStyle(node *yaml.Node) { + node.Style = 0 + for _, node := range node.Content { + f.resetNodeStyle(node) + } +}