diff --git a/api/internal/target/kusttarget.go b/api/internal/target/kusttarget.go index f99cd0f3b..257d08922 100644 --- a/api/internal/target/kusttarget.go +++ b/api/internal/target/kusttarget.go @@ -155,8 +155,12 @@ func (kt *KustTarget) addHashesToNames( // not yet fixed. func (kt *KustTarget) AccumulateTarget() ( ra *accumulator.ResAccumulator, err error) { - ra = accumulator.MakeEmptyAccumulator() - err = kt.accumulateResources(ra, kt.kustomization.Resources) + return kt.accumulateTarget(accumulator.MakeEmptyAccumulator()) +} + +func (kt *KustTarget) accumulateTarget(ra *accumulator.ResAccumulator) ( + resRa *accumulator.ResAccumulator, err error) { + ra, err = kt.accumulateResources(ra, kt.kustomization.Resources) if err != nil { return nil, errors.Wrap(err, "accumulating resources") } @@ -224,7 +228,7 @@ func (kt *KustTarget) runGenerators( func (kt *KustTarget) configureExternalGenerators() ([]resmap.Generator, error) { ra := accumulator.MakeEmptyAccumulator() - err := kt.accumulateResources(ra, kt.kustomization.Generators) + ra, err := kt.accumulateResources(ra, kt.kustomization.Generators) if err != nil { return nil, err } @@ -250,7 +254,7 @@ func (kt *KustTarget) runTransformers(ra *accumulator.ResAccumulator) error { func (kt *KustTarget) configureExternalTransformers() ([]resmap.Transformer, error) { ra := accumulator.MakeEmptyAccumulator() - err := kt.accumulateResources(ra, kt.kustomization.Transformers) + ra, err := kt.accumulateResources(ra, kt.kustomization.Transformers) if err != nil { return nil, err } @@ -260,44 +264,52 @@ func (kt *KustTarget) configureExternalTransformers() ([]resmap.Transformer, err // accumulateResources fills the given resourceAccumulator // with resources read from the given list of paths. func (kt *KustTarget) accumulateResources( - ra *accumulator.ResAccumulator, paths []string) error { + ra *accumulator.ResAccumulator, paths []string) (*accumulator.ResAccumulator, error) { for _, path := range paths { // try loading resource as file then as base (directory or git repository) if errF := kt.accumulateFile(ra, path); errF != nil { ldr, errL := kt.ldr.New(path) if errL != nil { - return fmt.Errorf("accumulateFile %q, loader.New %q", errF, errL) + return nil, fmt.Errorf("accumulateFile %q, loader.New %q", errF, errL) } - errD := kt.accumulateDirectory(ra, ldr) + var errD error + ra, errD = kt.accumulateDirectory(ra, ldr) if errD != nil { - return fmt.Errorf("accumulateFile %q, accumulateDirector: %q", errF, errD) + return nil, fmt.Errorf("accumulateFile %q, accumulateDirector: %q", errF, errD) } } } - return nil + return ra, nil } func (kt *KustTarget) accumulateDirectory( - ra *accumulator.ResAccumulator, ldr ifc.Loader) error { + ra *accumulator.ResAccumulator, ldr ifc.Loader) (*accumulator.ResAccumulator, error) { defer ldr.Cleanup() subKt := NewKustTarget( ldr, kt.validator, kt.rFactory, kt.tFactory, kt.pLdr) err := subKt.Load() if err != nil { - return errors.Wrapf( + return nil, errors.Wrapf( err, "couldn't make target for path '%s'", ldr.Root()) } - subRa, err := subKt.AccumulateTarget() + + var subRa *accumulator.ResAccumulator + if subKt.kustomization.Kind == types.KustomizationPatchKind { + subRa, err = subKt.accumulateTarget(ra) + ra = accumulator.MakeEmptyAccumulator() + } else { + subRa, err = subKt.AccumulateTarget() + } if err != nil { - return errors.Wrapf( + return nil, errors.Wrapf( err, "recursed accumulation of path '%s'", ldr.Root()) } err = ra.MergeAccumulator(subRa) if err != nil { - return errors.Wrapf( + return nil, errors.Wrapf( err, "recursed merging from path '%s'", ldr.Root()) } - return nil + return ra, nil } func (kt *KustTarget) accumulateFile( diff --git a/api/krusty/kustomizationpatch_test.go b/api/krusty/kustomizationpatch_test.go new file mode 100644 index 000000000..362115b40 --- /dev/null +++ b/api/krusty/kustomizationpatch_test.go @@ -0,0 +1,334 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package krusty_test + +import ( + "testing" + + kusttest_test "sigs.k8s.io/kustomize/api/testutils/kusttest" +) + +// Test kustomization.yaml files with kind: KustomizationPatch +func writeKustomizationPatchBase(th kusttest_test.Harness) { + th.WriteK("/app/base", ` +resources: +- deploy.yaml +configMapGenerator: +- name: my-configmap + literals: + - testValue=1 + - otherValue=10 +`) + th.WriteF("/app/base/deploy.yaml", ` +apiVersion: v1 +kind: Deployment +metadata: + name: storefront +spec: + replicas: 1 +`) +} + +func writeKustomizationPatchPatch(th kusttest_test.Harness) { + th.WriteF("/app/patch/kustomization.yaml", ` +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: KustomizationPatch +namePrefix: patched- +replicas: +- name: storefront + count: 3 +resources: + - stub.yaml +configMapGenerator: +- name: my-configmap + behavior: merge + literals: + - testValue=2 + - patchValue=5 +`) + th.WriteF("/app/patch/stub.yaml", ` +apiVersion: v1 +kind: Deployment +metadata: + name: stub +spec: + replicas: 1 +`) +} + +func writeKustomizationPatchProd(th kusttest_test.Harness) { + th.WriteK("/app/prod", ` +resources: +- ../base +- ../patch +- db +`) + th.WriteF("/app/prod/db", ` +apiVersion: v1 +kind: Deployment +metadata: + name: db +spec: + type: Logical +`) +} + +// KustomizationPatch are inserted into the resource hierarchy as the parent of those +// resources that come before it in the resources list of the parent Kustomization. +func TestBasicKustomizationPatch(t *testing.T) { + th := kusttest_test.MakeHarness(t) + writeKustomizationPatchBase(th) + writeKustomizationPatchPatch(th) + writeKustomizationPatchProd(th) + m := th.Run("/app/prod", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected(m, ` +apiVersion: v1 +kind: Deployment +metadata: + name: patched-storefront +spec: + replicas: 3 +--- +apiVersion: v1 +data: + otherValue: "10" + patchValue: "5" + testValue: "2" +kind: ConfigMap +metadata: + annotations: {} + labels: {} + name: patched-my-configmap-5g55h28cdh +--- +apiVersion: v1 +kind: Deployment +metadata: + name: patched-stub +spec: + replicas: 1 +--- +apiVersion: v1 +kind: Deployment +metadata: + name: db +spec: + type: Logical +`) +} + +func TestMultipleKustomizationPatches(t *testing.T) { + th := kusttest_test.MakeHarness(t) + writeKustomizationPatchBase(th) + writeKustomizationPatchPatch(th) + writeKustomizationPatchProd(th) + th.WriteF("/app/additionalpatch/kustomization.yaml", ` +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: KustomizationPatch +configMapGenerator: +- name: my-configmap + behavior: merge + literals: + - otherValue=9 +`) + th.WriteK("/app/prod", ` +resources: +- ../base +- ../patch +- ../additionalpatch +- db +`) + m := th.Run("/app/prod", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected(m, ` +apiVersion: v1 +kind: Deployment +metadata: + name: patched-storefront +spec: + replicas: 3 +--- +apiVersion: v1 +data: + otherValue: "9" + patchValue: "5" + testValue: "2" +kind: ConfigMap +metadata: + annotations: {} + labels: {} + name: patched-my-configmap-9fddc87cdk +--- +apiVersion: v1 +kind: Deployment +metadata: + name: patched-stub +spec: + replicas: 1 +--- +apiVersion: v1 +kind: Deployment +metadata: + name: db +spec: + type: Logical +`) +} + +func TestNestedKustomizationPatches(t *testing.T) { + th := kusttest_test.MakeHarness(t) + writeKustomizationPatchBase(th) + writeKustomizationPatchPatch(th) + writeKustomizationPatchProd(th) + th.WriteF("/app/additionalpatch/kustomization.yaml", ` +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: KustomizationPatch +resources: +- ../patch +configMapGenerator: +- name: my-configmap + behavior: merge + literals: + - otherValue=9 +`) + th.WriteK("/app/prod", ` +resources: +- ../base +- ../additionalpatch +- db +`) + m := th.Run("/app/prod", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected(m, ` +apiVersion: v1 +kind: Deployment +metadata: + name: patched-storefront +spec: + replicas: 3 +--- +apiVersion: v1 +data: + otherValue: "9" + patchValue: "5" + testValue: "2" +kind: ConfigMap +metadata: + annotations: {} + labels: {} + name: patched-my-configmap-9fddc87cdk +--- +apiVersion: v1 +kind: Deployment +metadata: + name: patched-stub +spec: + replicas: 1 +--- +apiVersion: v1 +kind: Deployment +metadata: + name: db +spec: + type: Logical +`) +} + +// If a patch sets a name prefix on a base, then that base can also be separately included +// without being affected by the patch in another branch of the resource tree +func TestBasicKustomizationPatchWithRepeatedBase(t *testing.T) { + th := kusttest_test.MakeHarness(t) + writeKustomizationPatchBase(th) + writeKustomizationPatchPatch(th) + writeKustomizationPatchProd(th) + th.WriteK("/app/repeated", ` +resources: +- ../base +- ../prod +`) + m := th.Run("/app/repeated", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected(m, ` +apiVersion: v1 +kind: Deployment +metadata: + name: storefront +spec: + replicas: 1 +--- +apiVersion: v1 +data: + otherValue: "10" + testValue: "1" +kind: ConfigMap +metadata: + name: my-configmap-7k9t4h74f8 +--- +apiVersion: v1 +kind: Deployment +metadata: + name: patched-storefront +spec: + replicas: 3 +--- +apiVersion: v1 +data: + otherValue: "10" + patchValue: "5" + testValue: "2" +kind: ConfigMap +metadata: + annotations: {} + labels: {} + name: patched-my-configmap-5g55h28cdh +--- +apiVersion: v1 +kind: Deployment +metadata: + name: patched-stub +spec: + replicas: 1 +--- +apiVersion: v1 +kind: Deployment +metadata: + name: db +spec: + type: Logical +`) +} + +func TestApplyingKustomizationPatchDirectlySameAsKustomization(t *testing.T) { + th := kusttest_test.MakeHarness(t) + writeKustomizationPatchBase(th) + writeKustomizationPatchPatch(th) + th.WriteF("/app/solopatch/kustomization.yaml", ` +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: KustomizationPatch +resources: +- ../base +configMapGenerator: +- name: my-configmap + behavior: merge + literals: + - patchValue=5 + - testValue=2 +`) + m := th.Run("/app/solopatch", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected(m, ` +apiVersion: v1 +kind: Deployment +metadata: + name: storefront +spec: + replicas: 1 +--- +apiVersion: v1 +data: + otherValue: "10" + patchValue: "5" + testValue: "2" +kind: ConfigMap +metadata: + annotations: {} + labels: {} + name: my-configmap-t86ktk6tdk +`) +} diff --git a/api/types/kustomization.go b/api/types/kustomization.go index 4be803f53..135e3f762 100644 --- a/api/types/kustomization.go +++ b/api/types/kustomization.go @@ -4,9 +4,10 @@ package types const ( - KustomizationVersion = "kustomize.config.k8s.io/v1beta1" - KustomizationKind = "Kustomization" - MetadataNamespacePath = "metadata/namespace" + KustomizationVersion = "kustomize.config.k8s.io/v1beta1" + KustomizationKind = "Kustomization" + KustomizationPatchKind = "KustomizationPatch" + MetadataNamespacePath = "metadata/namespace" ) // Kustomization holds the information needed to generate customized k8s api resources. @@ -143,8 +144,8 @@ func (k *Kustomization) EnforceFields() []string { if k.APIVersion != "" && k.APIVersion != KustomizationVersion { errs = append(errs, "apiVersion should be "+KustomizationVersion) } - if k.Kind != "" && k.Kind != KustomizationKind { - errs = append(errs, "kind should be "+KustomizationKind) + if k.Kind != "" && k.Kind != KustomizationKind && k.Kind != KustomizationPatchKind { + errs = append(errs, "kind should be "+KustomizationKind+" or "+KustomizationPatchKind) } return errs }