diff --git a/k8sdeps/kunstruct/kunstruct.go b/k8sdeps/kunstruct/kunstruct.go index dd854b557..477c3fa50 100644 --- a/k8sdeps/kunstruct/kunstruct.go +++ b/k8sdeps/kunstruct/kunstruct.go @@ -69,6 +69,11 @@ func (fs *UnstructAdapter) GetGvk() gvk.Gvk { } } +// SetGvk set the Gvk of the object to the input Gvk +func (fs *UnstructAdapter) SetGvk(g gvk.Gvk) { + fs.SetGroupVersionKind(toSchemaGvk(g)) +} + // Copy provides a copy behind an interface. func (fs *UnstructAdapter) Copy() ifc.Kunstructured { return &UnstructAdapter{*fs.DeepCopy()} diff --git a/pkg/ifc/ifc.go b/pkg/ifc/ifc.go index 188e67c60..6f8e892c6 100644 --- a/pkg/ifc/ifc.go +++ b/pkg/ifc/ifc.go @@ -54,6 +54,7 @@ type Kunstructured interface { MarshalJSON() ([]byte, error) UnmarshalJSON([]byte) error GetGvk() gvk.Gvk + SetGvk(gvk.Gvk) GetKind() string GetName() string SetName(string) diff --git a/plugin/builtin/PatchTransformer.go b/plugin/builtin/PatchTransformer.go new file mode 100644 index 000000000..7eefd39c1 --- /dev/null +++ b/plugin/builtin/PatchTransformer.go @@ -0,0 +1,146 @@ +// Code generated by pluginator on PatchTransformer; DO NOT EDIT. +package builtin + +import ( + "fmt" + "github.com/evanphx/json-patch" + "github.com/pkg/errors" + "sigs.k8s.io/kustomize/v3/pkg/ifc" + "sigs.k8s.io/kustomize/v3/pkg/resmap" + "sigs.k8s.io/kustomize/v3/pkg/resource" + "sigs.k8s.io/kustomize/v3/pkg/types" + "sigs.k8s.io/yaml" +) + +type PatchTransformerPlugin struct { + ldr ifc.Loader + rf *resmap.Factory + loadedPatch *resource.Resource + decodedPatch jsonpatch.Patch + Path string `json:"path,omitempty" yaml:"path,omitempty"` + Patch string `json:"patch,omitempty" yaml:"patch,omitempty"` + Target *types.Selector `json:"target,omitempty", yaml:"target,omitempty"` +} + +//noinspection GoUnusedGlobalVariable +func NewPatchTransformerPlugin() *PatchTransformerPlugin { + return &PatchTransformerPlugin{} +} + +func (p *PatchTransformerPlugin) Config( + ldr ifc.Loader, rf *resmap.Factory, c []byte) (err error) { + p.ldr = ldr + p.rf = rf + err = yaml.Unmarshal(c, p) + if err != nil { + return err + } + if p.Patch == "" && p.Path == "" { + err = fmt.Errorf( + "must specify one of patch and path in\n%s", string(c)) + return + } + if p.Patch != "" && p.Path != "" { + err = fmt.Errorf( + "patch and path can't be set at the same time\n%s", string(c)) + return + } + var in []byte + if p.Path != "" { + in, err = ldr.Load(p.Path) + if err != nil { + return + } + } + if p.Patch != "" { + in = []byte(p.Patch) + } + + patchSM, errSM := p.rf.RF().FromBytes(in) + patchJson, errJson := jsonPatchFromBytes(in) + if errSM != nil && errJson != nil { + err = fmt.Errorf( + "unable to get either a Strategic Merge Patch or JSON patch 6902 from %s", p.Patch) + return + } + if errSM == nil && errJson != nil { + p.loadedPatch = patchSM + } + if errJson == nil && errSM != nil { + p.decodedPatch = patchJson + } + if patchSM != nil && patchJson != nil { + err = fmt.Errorf( + "a patch can't be both a Strategic Merge Patch and JSON patch 6902 %s", p.Patch) + } + + return nil +} + +func (p *PatchTransformerPlugin) Transform(m resmap.ResMap) error { + if p.loadedPatch != nil && p.Target == nil { + target, err := m.GetById(p.loadedPatch.OrgId()) + if err != nil { + return err + } + err = target.Patch(p.loadedPatch.Kunstructured) + if err != nil { + return err + } + } + + if p.Target == nil { + return fmt.Errorf("must specify a target for patch %s", p.Patch) + } + + resources, err := m.Select(*p.Target) + if err != nil { + return err + } + for _, resource := range resources { + if p.decodedPatch != nil { + rawObj, err := resource.MarshalJSON() + if err != nil { + return err + } + modifiedObj, err := p.decodedPatch.Apply(rawObj) + if err != nil { + return errors.Wrapf( + err, "failed to apply json patch '%s'", p.Patch) + } + err = resource.UnmarshalJSON(modifiedObj) + if err != nil { + return err + } + } + if p.loadedPatch != nil { + p.loadedPatch.SetName(resource.GetName()) + p.loadedPatch.SetNamespace(resource.GetNamespace()) + p.loadedPatch.SetGvk(resource.GetGvk()) + err = resource.Patch(p.loadedPatch.Kunstructured) + if err != nil { + return err + } + } + } + return nil +} + +// jsonPatchFromBytes loads a Json 6902 patch from +// a bytes input +func jsonPatchFromBytes( + in []byte) (jsonpatch.Patch, error) { + ops := string(in) + if ops == "" { + return nil, fmt.Errorf("empty json patch operations") + } + + if ops[0] != '[' { + jsonOps, err := yaml.YAMLToJSON(in) + if err != nil { + return nil, err + } + ops = string(jsonOps) + } + return jsonpatch.DecodePatch([]byte(ops)) +} diff --git a/plugin/builtin/patchtransformer/PatchTransformer.go b/plugin/builtin/patchtransformer/PatchTransformer.go new file mode 100644 index 000000000..1b02717ba --- /dev/null +++ b/plugin/builtin/patchtransformer/PatchTransformer.go @@ -0,0 +1,147 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +//go:generate go run sigs.k8s.io/kustomize/v3/cmd/pluginator +package main + +import ( + "fmt" + "github.com/evanphx/json-patch" + "github.com/pkg/errors" + "sigs.k8s.io/kustomize/v3/pkg/ifc" + "sigs.k8s.io/kustomize/v3/pkg/resmap" + "sigs.k8s.io/kustomize/v3/pkg/resource" + "sigs.k8s.io/kustomize/v3/pkg/types" + "sigs.k8s.io/yaml" +) + +type plugin struct { + ldr ifc.Loader + rf *resmap.Factory + loadedPatch *resource.Resource + decodedPatch jsonpatch.Patch + Path string `json:"path,omitempty" yaml:"path,omitempty"` + Patch string `json:"patch,omitempty" yaml:"patch,omitempty"` + Target *types.Selector `json:"target,omitempty", yaml:"target,omitempty"` +} + +//noinspection GoUnusedGlobalVariable +var KustomizePlugin plugin + +func (p *plugin) Config( + ldr ifc.Loader, rf *resmap.Factory, c []byte) (err error) { + p.ldr = ldr + p.rf = rf + err = yaml.Unmarshal(c, p) + if err != nil { + return err + } + if p.Patch == "" && p.Path == "" { + err = fmt.Errorf( + "must specify one of patch and path in\n%s", string(c)) + return + } + if p.Patch != "" && p.Path != "" { + err = fmt.Errorf( + "patch and path can't be set at the same time\n%s", string(c)) + return + } + var in []byte + if p.Path != "" { + in, err = ldr.Load(p.Path) + if err != nil { + return + } + } + if p.Patch != "" { + in = []byte(p.Patch) + } + + patchSM, errSM := p.rf.RF().FromBytes(in) + patchJson, errJson := jsonPatchFromBytes(in) + if errSM != nil && errJson != nil { + err = fmt.Errorf( + "unable to get either a Strategic Merge Patch or JSON patch 6902 from %s", p.Patch) + return + } + if errSM == nil && errJson != nil { + p.loadedPatch = patchSM + } + if errJson == nil && errSM != nil { + p.decodedPatch = patchJson + } + if patchSM != nil && patchJson != nil { + err = fmt.Errorf( + "a patch can't be both a Strategic Merge Patch and JSON patch 6902 %s", p.Patch) + } + + return nil +} + +func (p *plugin) Transform(m resmap.ResMap) error { + if p.loadedPatch != nil && p.Target == nil { + target, err := m.GetById(p.loadedPatch.OrgId()) + if err != nil { + return err + } + err = target.Patch(p.loadedPatch.Kunstructured) + if err != nil { + return err + } + } + + if p.Target == nil { + return fmt.Errorf("must specify a target for patch %s", p.Patch) + } + + resources, err := m.Select(*p.Target) + if err != nil { + return err + } + for _, resource := range resources { + if p.decodedPatch != nil { + rawObj, err := resource.MarshalJSON() + if err != nil { + return err + } + modifiedObj, err := p.decodedPatch.Apply(rawObj) + if err != nil { + return errors.Wrapf( + err, "failed to apply json patch '%s'", p.Patch) + } + err = resource.UnmarshalJSON(modifiedObj) + if err != nil { + return err + } + } + if p.loadedPatch != nil { + p.loadedPatch.SetName(resource.GetName()) + p.loadedPatch.SetNamespace(resource.GetNamespace()) + p.loadedPatch.SetGvk(resource.GetGvk()) + err = resource.Patch(p.loadedPatch.Kunstructured) + if err != nil { + return err + } + } + } + return nil +} + +// jsonPatchFromBytes loads a Json 6902 patch from +// a bytes input +func jsonPatchFromBytes( + in []byte) (jsonpatch.Patch, error) { + ops := string(in) + if ops == "" { + return nil, fmt.Errorf("empty json patch operations") + } + + if ops[0] != '[' { + jsonOps, err := yaml.YAMLToJSON(in) + if err != nil { + return nil, err + } + ops = string(jsonOps) + } + return jsonpatch.DecodePatch([]byte(ops)) +} diff --git a/plugin/builtin/patchtransformer/PatchTransformer_test.go b/plugin/builtin/patchtransformer/PatchTransformer_test.go new file mode 100644 index 000000000..9e5787fb1 --- /dev/null +++ b/plugin/builtin/patchtransformer/PatchTransformer_test.go @@ -0,0 +1,340 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package main_test + +import ( + kusttest_test "sigs.k8s.io/kustomize/v3/pkg/kusttest" + "sigs.k8s.io/kustomize/v3/pkg/plugins" + "strings" + "testing" +) + +const ( + target = ` +apiVersion: apps/v1 +metadata: + name: myDeploy + labels: + old-label: old-value +kind: Deployment +spec: + replica: 2 + template: + metadata: + labels: + old-label: old-value + spec: + containers: + - name: nginx + image: nginx +--- +apiVersion: apps/v1 +metadata: + name: yourDeploy + labels: + new-label: new-value +kind: Deployment +spec: + replica: 1 + template: + metadata: + labels: + new-label: new-value + spec: + containers: + - name: nginx + image: nginx:1.7.9 +--- +apiVersion: apps/v1 +metadata: + name: myDeploy + label: + old-label: old-value +kind: MyKind +spec: + template: + metadata: + labels: + old-label: old-value + spec: + containers: + - name: nginx + image: nginx +` +) + +func TestPatchTransformerMissingFile(t *testing.T) { + tc := plugins.NewEnvForTest(t).Set() + defer tc.Reset() + + tc.BuildGoPlugin( + "builtin", "", "PatchTransformer") + th := kusttest_test.NewKustTestPluginHarness(t, "/app") + + _, err := th.RunTransformer(` +apiVersion: builtin +kind: PatchTransformer +metadata: + name: notImportantHere +path: patch.yaml +`, target) + if err == nil { + t.Fatalf("expected error") + } + if !strings.Contains(err.Error(), + "cannot read file \"/app/patch.yaml\"") { + t.Fatalf("unexpected err: %v", err) + } +} + +func TestPatchTransformerBadPatch(t *testing.T) { + tc := plugins.NewEnvForTest(t).Set() + defer tc.Reset() + + tc.BuildGoPlugin( + "builtin", "", "PatchTransformer") + th := kusttest_test.NewKustTestPluginHarness(t, "/app") + + _, err := th.RunTransformer(` +apiVersion: builtin +kind: PatchTransformer +metadata: + name: notImportantHere +patch: "thisIsNotAPatch" +`, target) + if err == nil { + t.Fatalf("expected error") + } + if !strings.Contains(err.Error(), + "unable to get either a Strategic Merge Patch or JSON patch 6902 from") { + t.Fatalf("unexpected err: %v", err) + } +} + +func TestPatchTransformerMissingSelector(t *testing.T) { + tc := plugins.NewEnvForTest(t).Set() + defer tc.Reset() + + tc.BuildGoPlugin( + "builtin", "", "PatchTransformer") + th := kusttest_test.NewKustTestPluginHarness(t, "/app") + + _, err := th.RunTransformer(` +apiVersion: builtin +kind: PatchTransformer +metadata: + name: notImportantHere +patch: '[{"op": "add", "path": "/spec/template/spec/dnsPolicy", "value": "ClusterFirst"}]' +`, target) + if err == nil { + t.Fatalf("expected error") + } + if !strings.Contains(err.Error(), + "must specify a target for patch") { + t.Fatalf("unexpected err: %v", err) + } +} + +func TestPatchTransformerBothEmptyPathAndPatch(t *testing.T) { + tc := plugins.NewEnvForTest(t).Set() + defer tc.Reset() + + tc.BuildGoPlugin( + "builtin", "", "PatchTransformer") + + th := kusttest_test.NewKustTestPluginHarness(t, "/app") + + _, err := th.RunTransformer(` +apiVersion: builtin +kind: PatchTransformer +metadata: + name: notImportantHere +`, target) + if err == nil { + t.Fatalf("expected error") + } + if !strings.Contains(err.Error(), "must specify one of patch and path in") { + t.Fatalf("unexpected err: %v", err) + } +} + +func TestPatchTransformerBothNonEmptyPathAndPatch(t *testing.T) { + tc := plugins.NewEnvForTest(t).Set() + defer tc.Reset() + + tc.BuildGoPlugin( + "builtin", "", "PatchTransformer") + + th := kusttest_test.NewKustTestPluginHarness(t, "/app") + + _, err := th.RunTransformer(` +apiVersion: builtin +kind: PatchTransformer +metadata: + name: notImportantHere +Path: patch.yaml +Patch: "something" +`, target) + if err == nil { + t.Fatalf("expected error") + } + if !strings.Contains(err.Error(), "patch and path can't be set at the same time") { + t.Fatalf("unexpected err: %v", err) + } +} + +func TestPatchTransformerFromFiles(t *testing.T) { + tc := plugins.NewEnvForTest(t).Set() + defer tc.Reset() + + tc.BuildGoPlugin( + "builtin", "", "PatchTransformer") + + th := kusttest_test.NewKustTestPluginHarness(t, "/app") + + th.WriteF("/app/patch.yaml", ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myDeploy +spec: + replica: 3 +`) + + rm := th.LoadAndRunTransformer(` +apiVersion: builtin +kind: PatchTransformer +metadata: + name: notImportantHere +path: patch.yaml +target: + name: .*Deploy +`, target) + + th.AssertActualEqualsExpected(rm, ` +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + old-label: old-value + name: myDeploy +spec: + replica: 3 + template: + metadata: + labels: + old-label: old-value + spec: + containers: + - image: nginx + name: nginx +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + new-label: new-value + name: yourDeploy +spec: + replica: 3 + template: + metadata: + labels: + new-label: new-value + spec: + containers: + - image: nginx:1.7.9 + name: nginx +--- +apiVersion: apps/v1 +kind: MyKind +metadata: + label: + old-label: old-value + name: myDeploy +spec: + replica: 3 + template: + metadata: + labels: + old-label: old-value + spec: + containers: + - image: nginx + name: nginx +`) +} + +func TestPatchTransformerWithInline(t *testing.T) { + tc := plugins.NewEnvForTest(t).Set() + defer tc.Reset() + + tc.BuildGoPlugin( + "builtin", "", "PatchTransformer") + + th := kusttest_test.NewKustTestPluginHarness(t, "/app") + + rm := th.LoadAndRunTransformer(` +apiVersion: builtin +kind: PatchTransformer +metadata: + name: notImportantHere +patch: '[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value": "nginx:latest"}]' +target: + name: .*Deploy + kind: Deployment +`, target) + + th.AssertActualEqualsExpected(rm, ` +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + old-label: old-value + name: myDeploy +spec: + replica: 2 + template: + metadata: + labels: + old-label: old-value + spec: + containers: + - image: nginx:latest + name: nginx +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + new-label: new-value + name: yourDeploy +spec: + replica: 1 + template: + metadata: + labels: + new-label: new-value + spec: + containers: + - image: nginx:latest + name: nginx +--- +apiVersion: apps/v1 +kind: MyKind +metadata: + label: + old-label: old-value + name: myDeploy +spec: + template: + metadata: + labels: + old-label: old-value + spec: + containers: + - image: nginx + name: nginx +`) +}