From 7f0e9e3a6a4673d6884b5f5f9cdd8cf55d7e7761 Mon Sep 17 00:00:00 2001 From: Jingfang Liu Date: Thu, 30 Aug 2018 13:46:08 -0700 Subject: [PATCH] Add patchJson6902 transformer --- pkg/patch/jsonpatch.go | 15 +- pkg/patch/transformer/patchjson6902.go | 48 +++++++ pkg/patch/transformer/patchjson6902_test.go | 1 + pkg/patch/transformer/patchjson6902json.go | 94 ++++++++++++ .../transformer/patchjson6902json_test.go | 115 +++++++++++++++ pkg/patch/transformer/patchjson6902yaml.go | 92 ++++++++++++ .../transformer/patchjson6902yaml_test.go | 135 ++++++++++++++++++ 7 files changed, 496 insertions(+), 4 deletions(-) create mode 100644 pkg/patch/transformer/patchjson6902.go create mode 100644 pkg/patch/transformer/patchjson6902_test.go create mode 100644 pkg/patch/transformer/patchjson6902json.go create mode 100644 pkg/patch/transformer/patchjson6902json_test.go create mode 100644 pkg/patch/transformer/patchjson6902yaml.go create mode 100644 pkg/patch/transformer/patchjson6902yaml_test.go diff --git a/pkg/patch/jsonpatch.go b/pkg/patch/jsonpatch.go index 997317d16..8fca0b1e2 100644 --- a/pkg/patch/jsonpatch.go +++ b/pkg/patch/jsonpatch.go @@ -16,18 +16,25 @@ limitations under the License. package patch +import ( + yamlpatch "github.com/krishicks/yaml-patch" +) + // PatchJson6902 represents a json patch for an object // with format documented https://tools.ietf.org/html/rfc6902. type PatchJson6902 struct { - // Relative file path within the kustomization for a json patch file. - Path string `json:"path" yaml:"path"` - // Target refers to a Kubernetes object that the json patch will be // applied to. It must refer to a Kubernetes resource under the // purview of this kustomization. Target should use the // raw name of the object (the name specified in its YAML, // before addition of a namePrefix). - Target Target `json:"target" yaml:"target"` + Target *Target `json:"target" yaml:"target"` + + // jsonPatch is a list of operations in YAML format that follows JSON 6902 rule. + JsonPatch yamlpatch.Patch `json:"jsonPatch,omitempty" yaml:"jsonPatch,omitempty"` + + // relative file path for a json patch file inside a kustomization + Path string `json:"path,omitempty" yaml:"path,omitempty"` } // Target represents the kubernetes object that the patch is applied to diff --git a/pkg/patch/transformer/patchjson6902.go b/pkg/patch/transformer/patchjson6902.go new file mode 100644 index 000000000..96a026baf --- /dev/null +++ b/pkg/patch/transformer/patchjson6902.go @@ -0,0 +1,48 @@ +/* +Copyright 2018 The Kubernetes Authors. + +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 transformer + +import ( + "fmt" + + "github.com/kubernetes-sigs/kustomize/pkg/loader" + "github.com/kubernetes-sigs/kustomize/pkg/patch" + "github.com/kubernetes-sigs/kustomize/pkg/transformers" +) + +// NewPatchJson6902Transformer constructs a PatchJson6902 transformer. +func NewPatchJson6902Transformer(l loader.Loader, p patch.PatchJson6902) (transformers.Transformer, error) { + if p.Target == nil { + return nil, fmt.Errorf("must specify the target field in patchesJson6902") + } + if p.Path != "" && p.JsonPatch != nil { + return nil, fmt.Errorf("cannot specify path and jsonPath at the same time") + } + + if p.JsonPatch != nil { + return NewPatchJson6902YAMLTransformer(p.Target, p.JsonPatch) + } + if p.Path != "" { + operations, err := l.Load(p.Path) + if err != nil { + return nil, err + } + return NewPatchJson6902JSONTransformer(p.Target, operations) + } + + return transformers.NewNoOpTransformer(), nil +} diff --git a/pkg/patch/transformer/patchjson6902_test.go b/pkg/patch/transformer/patchjson6902_test.go new file mode 100644 index 000000000..38ef5e169 --- /dev/null +++ b/pkg/patch/transformer/patchjson6902_test.go @@ -0,0 +1 @@ +package transformer diff --git a/pkg/patch/transformer/patchjson6902json.go b/pkg/patch/transformer/patchjson6902json.go new file mode 100644 index 000000000..1547c91d5 --- /dev/null +++ b/pkg/patch/transformer/patchjson6902json.go @@ -0,0 +1,94 @@ +/* +Copyright 2018 The Kubernetes Authors. + +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 transformer + +import ( + "fmt" + "log" + + jsonpatch "github.com/evanphx/json-patch" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/kubernetes-sigs/kustomize/pkg/patch" + "github.com/kubernetes-sigs/kustomize/pkg/resmap" + "github.com/kubernetes-sigs/kustomize/pkg/resource" + "github.com/kubernetes-sigs/kustomize/pkg/transformers" +) + +// patchJson6902Transformer applies patches. +type patchJson6902JSONTransformer struct { + target *patch.Target + operations []byte +} + +var _ transformers.Transformer = &patchJson6902JSONTransformer{} + +// NewPatchJson6902JSONTransformer constructs a PatchJson6902 transformer. +func NewPatchJson6902JSONTransformer(t *patch.Target, o []byte) (transformers.Transformer, error) { + return &patchJson6902JSONTransformer{target: t, operations: o}, nil +} + +// Transform apply the json patches on top of the base resources. +func (t *patchJson6902JSONTransformer) Transform(baseResourceMap resmap.ResMap) error { + targetId := resource.NewResIdWithPrefixNamespace( + schema.GroupVersionKind{ + Group: t.target.Group, + Version: t.target.Version, + Kind: t.target.Kind, + }, + t.target.Name, + "", + t.target.Namespace, + ) + + matchedIds := baseResourceMap.FindByGVKN(targetId) + if targetId.Namespace() != "" { + ids := []resource.ResId{} + for _, id := range matchedIds { + if id.Namespace() == targetId.Namespace() { + ids = append(ids, id) + } + } + matchedIds = ids + } + if len(matchedIds) == 0 { + log.Printf("Couldn't find any object to apply the json patch %v, skipping it.", targetId) + return nil + } + if len(matchedIds) > 1 { + return fmt.Errorf("found multiple objects that the patch can apply %v", matchedIds) + } + + decodedPatch, err := jsonpatch.DecodePatch(t.operations) + if err != nil { + return err + } + obj := baseResourceMap[matchedIds[0]] + rawObj, err := obj.Unstructured.MarshalJSON() + if err != nil { + return err + } + modifiedObj, err := decodedPatch.Apply(rawObj) + if err != nil { + return err + } + err = obj.UnmarshalJSON(modifiedObj) + if err != nil { + return err + } + return nil +} diff --git a/pkg/patch/transformer/patchjson6902json_test.go b/pkg/patch/transformer/patchjson6902json_test.go new file mode 100644 index 000000000..d6b00260b --- /dev/null +++ b/pkg/patch/transformer/patchjson6902json_test.go @@ -0,0 +1,115 @@ +/* +Copyright 2018 The Kubernetes Authors. + +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 transformer + +import ( + "reflect" + "testing" + + "github.com/kubernetes-sigs/kustomize/pkg/patch" + "github.com/kubernetes-sigs/kustomize/pkg/resmap" + "github.com/kubernetes-sigs/kustomize/pkg/resource" +) + +func TestJsonPatchJSONTransformer_Transform(t *testing.T) { + base := resmap.ResMap{ + resource.NewResId(deploy, "deploy1"): resource.NewResourceFromMap( + map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "deploy1", + }, + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "old-label": "old-value", + }, + }, + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "nginx", + "image": "nginx", + }, + }, + }, + }, + }, + }), + } + + target := patch.Target{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + Name: "deploy1", + } + + operations := []byte(`[ + {"op": "replace", "path": "/spec/template/spec/containers/0/name", "value": "my-nginx"}, + {"op": "add", "path": "/spec/replica", "value": "3"}, + {"op": "add", "path": "/spec/template/spec/containers/0/command", "value": ["arg1", "arg2", "arg3"]} +]`) + + expected := resmap.ResMap{ + resource.NewResId(deploy, "deploy1"): resource.NewResourceFromMap( + map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "deploy1", + }, + "spec": map[string]interface{}{ + "replica": "3", + "template": map[string]interface{}{ + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "old-label": "old-value", + }, + }, + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "image": "nginx", + "name": "my-nginx", + "command": []interface{}{ + "arg1", + "arg2", + "arg3", + }, + }, + }, + }, + }, + }, + }), + } + jpt, err := NewPatchJson6902JSONTransformer(&target, operations) + if err != nil { + t.Fatalf("unexpected error : %v", err) + } + err = jpt.Transform(base) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(base, expected) { + err = expected.ErrorIfNotEqual(base) + t.Fatalf("actual doesn't match expected: %v", err) + } +} diff --git a/pkg/patch/transformer/patchjson6902yaml.go b/pkg/patch/transformer/patchjson6902yaml.go new file mode 100644 index 000000000..3bc1ccdb2 --- /dev/null +++ b/pkg/patch/transformer/patchjson6902yaml.go @@ -0,0 +1,92 @@ +/* +Copyright 2018 The Kubernetes Authors. + +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 transformer + +import ( + "fmt" + "log" + + "github.com/ghodss/yaml" + yamlpatch "github.com/krishicks/yaml-patch" + + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/kubernetes-sigs/kustomize/pkg/patch" + "github.com/kubernetes-sigs/kustomize/pkg/resmap" + "github.com/kubernetes-sigs/kustomize/pkg/resource" + "github.com/kubernetes-sigs/kustomize/pkg/transformers" +) + +// patchJson6902Transformer applies patches. +type patchJson6902YAMLTransformer struct { + target *patch.Target + patch yamlpatch.Patch +} + +var _ transformers.Transformer = &patchJson6902YAMLTransformer{} + +// NewPatchJson6902YAMLTransformer constructs a PatchJson6902 transformer. +func NewPatchJson6902YAMLTransformer(t *patch.Target, p yamlpatch.Patch) (transformers.Transformer, error) { + return &patchJson6902YAMLTransformer{target: t, patch: p}, nil +} + +// Transform apply the json patches on top of the base resources. +func (t *patchJson6902YAMLTransformer) Transform(baseResourceMap resmap.ResMap) error { + targetId := resource.NewResIdWithPrefixNamespace( + schema.GroupVersionKind{ + Group: t.target.Group, + Version: t.target.Version, + Kind: t.target.Kind, + }, + t.target.Name, + "", + t.target.Namespace, + ) + + matchedIds := baseResourceMap.FindByGVKN(targetId) + if targetId.Namespace() != "" { + ids := []resource.ResId{} + for _, id := range matchedIds { + if id.Namespace() == targetId.Namespace() { + ids = append(ids, id) + } + } + matchedIds = ids + } + if len(matchedIds) == 0 { + log.Printf("Couldn't find any object to apply the json patch %v, skipping it.", targetId) + return nil + } + if len(matchedIds) > 1 { + return fmt.Errorf("found multiple objects that the patch can apply %v", matchedIds) + } + + obj := baseResourceMap[matchedIds[0]] + rawObj, err := yaml.Marshal(obj.Unstructured.Object) + if err != nil { + return err + } + modifiedObj, err := t.patch.Apply(rawObj) + if err != nil { + return err + } + err = yaml.Unmarshal(modifiedObj, &obj.Unstructured.Object) + if err != nil { + return err + } + return nil +} diff --git a/pkg/patch/transformer/patchjson6902yaml_test.go b/pkg/patch/transformer/patchjson6902yaml_test.go new file mode 100644 index 000000000..79ba7e52e --- /dev/null +++ b/pkg/patch/transformer/patchjson6902yaml_test.go @@ -0,0 +1,135 @@ +/* +Copyright 2018 The Kubernetes Authors. + +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 transformer + +import ( + "reflect" + "testing" + + yamlpatch "github.com/krishicks/yaml-patch" + "github.com/kubernetes-sigs/kustomize/pkg/patch" + "github.com/kubernetes-sigs/kustomize/pkg/resmap" + "github.com/kubernetes-sigs/kustomize/pkg/resource" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var deploy = schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"} + +func TestJsonPatchYAMLTransformer_Transform(t *testing.T) { + base := resmap.ResMap{ + resource.NewResId(deploy, "deploy1"): resource.NewResourceFromMap( + map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "deploy1", + }, + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "old-label": "old-value", + }, + }, + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "nginx", + "image": "nginx", + }, + }, + }, + }, + }, + }), + } + + target := patch.Target{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + Name: "deploy1", + } + + var image, replica, command interface{} + image = "my-nginx" + replica = "3" + command = []string{"arg1", "arg2", "arg3"} + patch := yamlpatch.Patch{ + { + Op: "replace", + Path: "/spec/template/spec/containers/0/name", + Value: yamlpatch.NewNode(&image), + }, + { + Op: "add", + Path: "/spec/replica", + Value: yamlpatch.NewNode(&replica), + }, + { + Op: "add", + Path: "/spec/template/spec/containers/0/command", + Value: yamlpatch.NewNode(&command), + }, + } + + expected := resmap.ResMap{ + resource.NewResId(deploy, "deploy1"): resource.NewResourceFromMap( + map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "deploy1", + }, + "spec": map[string]interface{}{ + "replica": "3", + "template": map[string]interface{}{ + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "old-label": "old-value", + }, + }, + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "image": "nginx", + "name": "my-nginx", + "command": []interface{}{ + "arg1", + "arg2", + "arg3", + }, + }, + }, + }, + }, + }, + }), + } + jpt, err := NewPatchJson6902YAMLTransformer(&target, patch) + if err != nil { + t.Fatalf("unexpected error : %v", err) + } + err = jpt.Transform(base) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(base, expected) { + err = expected.ErrorIfNotEqual(base) + t.Fatalf("actual doesn't match expected: %v", err) + } +}