Make api/plugins

This commit is contained in:
jregan
2019-10-20 14:03:56 -07:00
parent 286b9c1aed
commit 951d15bf17
120 changed files with 668 additions and 411 deletions

View File

@@ -0,0 +1,121 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package expansion provides functions find and replace $(FOO) style variables in strings.
package expansion
import (
"bytes"
"fmt"
)
const (
operator = '$'
referenceOpener = '('
referenceCloser = ')'
)
// syntaxWrap returns the input string wrapped by the expansion syntax.
func syntaxWrap(input string) string {
return string(operator) + string(referenceOpener) + input + string(referenceCloser)
}
// MappingFuncFor returns a mapping function for use with Expand that
// implements the expansion semantics defined in the expansion spec; it
// returns the input string wrapped in the expansion syntax if no mapping
// for the input is found.
func MappingFuncFor(
counts map[string]int,
context ...map[string]interface{}) func(string) interface{} {
return func(input string) interface{} {
for _, vars := range context {
val, ok := vars[input]
if ok {
counts[input]++
switch typedV := val.(type) {
case string, int64, float64, bool:
return typedV
default:
return syntaxWrap(input)
}
}
}
return syntaxWrap(input)
}
}
// Expand replaces variable references in the input string according to
// the expansion spec using the given mapping function to resolve the
// values of variables.
func Expand(input string, mapping func(string) interface{}) interface{} {
var buf bytes.Buffer
checkpoint := 0
for cursor := 0; cursor < len(input); cursor++ {
if input[cursor] == operator && cursor+1 < len(input) {
// Copy the portion of the input string since the last
// checkpoint into the buffer
buf.WriteString(input[checkpoint:cursor])
// Attempt to read the variable name as defined by the
// syntax from the input string
read, isVar, advance := tryReadVariableName(input[cursor+1:])
if isVar {
// We were able to read a variable name correctly;
// apply the mapping to the variable name and copy the
// bytes into the buffer
mapped := mapping(read)
if input == syntaxWrap(read) {
// Preserve the type of variable
return mapped
}
// Variable is used in a middle of a string
buf.WriteString(fmt.Sprintf("%v", mapped))
} else {
// Not a variable name; copy the read bytes into the buffer
buf.WriteString(read)
}
// Advance the cursor in the input string to account for
// bytes consumed to read the variable name expression
cursor += advance
// Advance the checkpoint in the input string
checkpoint = cursor + 1
}
}
// Return the buffer and any remaining unwritten bytes in the
// input string.
return buf.String() + input[checkpoint:]
}
// tryReadVariableName attempts to read a variable name from the input
// string and returns the content read from the input, whether that content
// represents a variable name to perform mapping on, and the number of bytes
// consumed in the input string.
//
// The input string is assumed not to contain the initial operator.
func tryReadVariableName(input string) (string, bool, int) {
switch input[0] {
case operator:
// Escaped operator; return it.
return input[0:1], false, 1
case referenceOpener:
// Scan to expression closer
for i := 1; i < len(input); i++ {
if input[i] == referenceCloser {
return input[1:i], true, i + 1
}
}
// Incomplete reference; return it.
return string(operator) + string(referenceOpener), false, 1
default:
// Not the beginning of an expression, ie, an operator
// that doesn't begin an expression. Return the operator
// and the first rune in the string.
return string(operator) + string(input[0]), false, 1
}
}

View File

@@ -0,0 +1,365 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package expansion_test
import (
"fmt"
"testing"
. "sigs.k8s.io/kustomize/v3/api/accumulator/expansion"
)
type expected struct {
count int
edited string
}
func TestMapReference(t *testing.T) {
type env struct {
Name string
Value interface{}
}
envs := []env{
{
Name: "FOO",
Value: "bar",
},
{
Name: "ZOO",
Value: "$(FOO)-1",
},
{
Name: "BLU",
Value: "$(ZOO)-2",
},
{
Name: "INT",
Value: 2,
},
{
Name: "ZINT",
Value: "$(INT)",
},
{
Name: "BOOL",
Value: true,
},
{
Name: "ZBOOL",
Value: "$(BOOL)",
},
}
declaredEnv := map[string]interface{}{
"FOO": "bar",
"ZOO": "$(FOO)-1",
"BLU": "$(ZOO)-2",
"INT": "2",
"ZINT": "$(INT)",
"BOOL": "true",
"ZBOOL": "$(BOOL)",
}
counts := make(map[string]int)
mapping := MappingFuncFor(counts, declaredEnv)
for _, env := range envs {
declaredEnv[env.Name] = Expand(fmt.Sprintf("%v", env.Value), mapping)
}
expectedEnv := map[string]expected{
"FOO": {count: 1, edited: "bar"},
"ZOO": {count: 1, edited: "bar-1"},
"BLU": {count: 0, edited: "bar-1-2"},
"INT": {count: 1, edited: "2"},
"ZINT": {count: 0, edited: "2"},
"BOOL": {count: 1, edited: "true"},
"ZBOOL": {count: 0, edited: "true"},
}
for k, v := range expectedEnv {
if e, a := v, declaredEnv[k]; e.edited != a || e.count != counts[k] {
t.Errorf("Expected %v count=%d, got %v count=%d",
e.edited, e.count, a, counts[k])
} else {
delete(declaredEnv, k)
}
}
if len(declaredEnv) != 0 {
t.Errorf("Unexpected keys in declared env: %v", declaredEnv)
}
}
func TestMapping(t *testing.T) {
context := map[string]interface{}{
"VAR_A": "A",
"VAR_B": "B",
"VAR_C": "C",
"VAR_REF": "$(VAR_A)",
"VAR_EMPTY": "",
}
doExpansionTest(t, context)
}
func TestMappingDual(t *testing.T) {
context := map[string]interface{}{
"VAR_A": "A",
"VAR_EMPTY": "",
}
context2 := map[string]interface{}{
"VAR_B": "B",
"VAR_C": "C",
"VAR_REF": "$(VAR_A)",
}
doExpansionTest(t, context, context2)
}
func doExpansionTest(t *testing.T, context ...map[string]interface{}) {
cases := []struct {
name string
input string
expected string
counts map[string]int
}{
{
name: "whole string",
input: "$(VAR_A)",
expected: "A",
counts: map[string]int{"VAR_A": 1},
},
{
name: "repeat",
input: "$(VAR_A)-$(VAR_A)",
expected: "A-A",
counts: map[string]int{"VAR_A": 2},
},
{
name: "multiple repeats",
input: "$(VAR_A)-$(VAR_B)-$(VAR_B)-$(VAR_B)-$(VAR_A)",
expected: "A-B-B-B-A",
counts: map[string]int{"VAR_A": 2, "VAR_B": 3},
},
{
name: "beginning",
input: "$(VAR_A)-1",
expected: "A-1",
counts: map[string]int{"VAR_A": 1},
},
{
name: "middle",
input: "___$(VAR_B)___",
expected: "___B___",
counts: map[string]int{"VAR_B": 1},
},
{
name: "end",
input: "___$(VAR_C)",
expected: "___C",
counts: map[string]int{"VAR_C": 1},
},
{
name: "compound",
input: "$(VAR_A)_$(VAR_B)_$(VAR_C)",
expected: "A_B_C",
counts: map[string]int{"VAR_A": 1, "VAR_B": 1, "VAR_C": 1},
},
{
name: "escape & expand",
input: "$$(VAR_B)_$(VAR_A)",
expected: "$(VAR_B)_A",
counts: map[string]int{"VAR_A": 1},
},
{
name: "compound escape",
input: "$$(VAR_A)_$$(VAR_B)",
expected: "$(VAR_A)_$(VAR_B)",
},
{
name: "mixed in escapes",
input: "f000-$$VAR_A",
expected: "f000-$VAR_A",
},
{
name: "backslash escape ignored",
input: "foo\\$(VAR_C)bar",
expected: "foo\\Cbar",
counts: map[string]int{"VAR_C": 1},
},
{
name: "backslash escape ignored",
input: "foo\\\\$(VAR_C)bar",
expected: "foo\\\\Cbar",
counts: map[string]int{"VAR_C": 1},
},
{
name: "lots of backslashes",
input: "foo\\\\\\\\$(VAR_A)bar",
expected: "foo\\\\\\\\Abar",
counts: map[string]int{"VAR_A": 1},
},
{
name: "nested var references",
input: "$(VAR_A$(VAR_B))",
expected: "$(VAR_A$(VAR_B))",
},
{
name: "nested var references second type",
input: "$(VAR_A$(VAR_B)",
expected: "$(VAR_A$(VAR_B)",
},
{
name: "value is a reference",
input: "$(VAR_REF)",
expected: "$(VAR_A)",
counts: map[string]int{"VAR_REF": 1},
},
{
name: "value is a reference x 2",
input: "%%$(VAR_REF)--$(VAR_REF)%%",
expected: "%%$(VAR_A)--$(VAR_A)%%",
counts: map[string]int{"VAR_REF": 2},
},
{
name: "empty var",
input: "foo$(VAR_EMPTY)bar",
expected: "foobar",
counts: map[string]int{"VAR_EMPTY": 1},
},
{
name: "unterminated expression",
input: "foo$(VAR_Awhoops!",
expected: "foo$(VAR_Awhoops!",
},
{
name: "expression without operator",
input: "f00__(VAR_A)__",
expected: "f00__(VAR_A)__",
},
{
name: "shell special vars pass through",
input: "$?_boo_$!",
expected: "$?_boo_$!",
},
{
name: "bare operators are ignored",
input: "$VAR_A",
expected: "$VAR_A",
},
{
name: "undefined vars are passed through",
input: "$(VAR_DNE)",
expected: "$(VAR_DNE)",
},
{
name: "multiple (even) operators, var undefined",
input: "$$$$$$(BIG_MONEY)",
expected: "$$$(BIG_MONEY)",
},
{
name: "multiple (even) operators, var defined",
input: "$$$$$$(VAR_A)",
expected: "$$$(VAR_A)",
},
{
name: "multiple (odd) operators, var undefined",
input: "$$$$$$$(GOOD_ODDS)",
expected: "$$$$(GOOD_ODDS)",
},
{
name: "multiple (odd) operators, var defined",
input: "$$$$$$$(VAR_A)",
expected: "$$$A",
counts: map[string]int{"VAR_A": 1},
},
{
name: "missing open expression",
input: "$VAR_A)",
expected: "$VAR_A)",
},
{
name: "shell syntax ignored",
input: "${VAR_A}",
expected: "${VAR_A}",
},
{
name: "trailing incomplete expression not consumed",
input: "$(VAR_B)_______$(A",
expected: "B_______$(A",
counts: map[string]int{"VAR_B": 1},
},
{
name: "trailing incomplete expression, no content, is not consumed",
input: "$(VAR_C)_______$(",
expected: "C_______$(",
counts: map[string]int{"VAR_C": 1},
},
{
name: "operator at end of input string is preserved",
input: "$(VAR_A)foobarzab$",
expected: "Afoobarzab$",
counts: map[string]int{"VAR_A": 1},
},
{
name: "shell escaped incomplete expr",
input: "foo-\\$(VAR_A",
expected: "foo-\\$(VAR_A",
},
{
name: "lots of $( in middle",
input: "--$($($($($--",
expected: "--$($($($($--",
},
{
name: "lots of $( in beginning",
input: "$($($($($--foo$(",
expected: "$($($($($--foo$(",
},
{
name: "lots of $( at end",
input: "foo0--$($($($(",
expected: "foo0--$($($($(",
},
{
name: "escaped operators in variable names are not escaped",
input: "$(foo$$var)",
expected: "$(foo$$var)",
},
{
name: "newline not expanded",
input: "\n",
expected: "\n",
},
}
for _, tc := range cases {
counts := make(map[string]int)
mapping := MappingFuncFor(counts, context...)
expanded := Expand(fmt.Sprintf("%v", tc.input), mapping)
if e, a := tc.expected, expanded; e != a {
t.Errorf("%v: expected %q, got %q", tc.name, e, a)
}
if len(counts) != len(tc.counts) {
t.Errorf("%v: len(counts)=%d != len(tc.counts)=%d",
tc.name, len(counts), len(tc.counts))
}
if len(tc.counts) > 0 {
for k, expectedCount := range tc.counts {
c, ok := counts[k]
if ok {
if c != expectedCount {
t.Errorf(
"%v: k=%s, expected count %d, got %d",
tc.name, k, expectedCount, c)
}
} else {
t.Errorf(
"%v: k=%s, expected count %d, got zero",
tc.name, k, expectedCount)
}
}
}
}
}

View File

@@ -0,0 +1,190 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package accumulator
import (
"encoding/json"
"strings"
"github.com/go-openapi/spec"
"github.com/pkg/errors"
"k8s.io/kube-openapi/pkg/common"
"sigs.k8s.io/kustomize/v3/api/builtinconfig"
"sigs.k8s.io/kustomize/v3/api/ifc"
"sigs.k8s.io/kustomize/v3/api/resid"
"sigs.k8s.io/kustomize/v3/api/types"
"sigs.k8s.io/yaml"
)
type myProperties map[string]spec.Schema
type nameToApiMap map[string]common.OpenAPIDefinition
// LoadConfigFromCRDs parse CRD schemas from paths into a TransformerConfig
func LoadConfigFromCRDs(
ldr ifc.Loader, paths []string) (*builtinconfig.TransformerConfig, error) {
tc := builtinconfig.MakeEmptyConfig()
for _, path := range paths {
content, err := ldr.Load(path)
if err != nil {
return nil, err
}
m, err := makeNameToApiMap(content)
if err != nil {
return nil, errors.Wrapf(err, "unable to parse open API definition from '%s'", path)
}
otherTc, err := makeConfigFromApiMap(m)
if err != nil {
return nil, err
}
tc, err = tc.Merge(otherTc)
if err != nil {
return nil, err
}
}
return tc, nil
}
func makeNameToApiMap(content []byte) (result nameToApiMap, err error) {
if content[0] == '{' {
err = json.Unmarshal(content, &result)
} else {
err = yaml.Unmarshal(content, &result)
}
return
}
func makeConfigFromApiMap(m nameToApiMap) (*builtinconfig.TransformerConfig, error) {
result := builtinconfig.MakeEmptyConfig()
for name, api := range m {
if !looksLikeAk8sType(api.Schema.SchemaProps.Properties) {
continue
}
tc := builtinconfig.MakeEmptyConfig()
err := loadCrdIntoConfig(
tc, makeGvkFromTypeName(name), m, name, []string{})
if err != nil {
return result, err
}
result, err = result.Merge(tc)
if err != nil {
return result, err
}
}
return result, nil
}
// TODO: Get Group and Version for CRD from the
// openAPI definition once
// "x-kubernetes-group-version-kind" is available in CRD
func makeGvkFromTypeName(n string) resid.Gvk {
names := strings.Split(n, ".")
kind := names[len(names)-1]
return resid.Gvk{Kind: kind}
}
func looksLikeAk8sType(properties myProperties) bool {
_, ok := properties["kind"]
if !ok {
return false
}
_, ok = properties["apiVersion"]
if !ok {
return false
}
_, ok = properties["metadata"]
if !ok {
return false
}
return true
}
const (
// "x-kubernetes-annotation": ""
xAnnotation = "x-kubernetes-annotation"
// "x-kubernetes-label-selector": ""
xLabelSelector = "x-kubernetes-label-selector"
// "x-kubernetes-identity": ""
xIdentity = "x-kubernetes-identity"
// "x-kubernetes-object-ref-api-version": <apiVersion name>
xVersion = "x-kubernetes-object-ref-api-version"
// "x-kubernetes-object-ref-kind": <kind name>
xKind = "x-kubernetes-object-ref-kind"
// "x-kubernetes-object-ref-name-key": "name"
// default is "name"
xNameKey = "x-kubernetes-object-ref-name-key"
)
// loadCrdIntoConfig loads a CRD spec into a TransformerConfig
func loadCrdIntoConfig(
theConfig *builtinconfig.TransformerConfig, theGvk resid.Gvk, theMap nameToApiMap,
typeName string, path []string) (err error) {
api, ok := theMap[typeName]
if !ok {
return nil
}
for propName, property := range api.Schema.SchemaProps.Properties {
_, annotate := property.Extensions.GetString(xAnnotation)
if annotate {
err = theConfig.AddAnnotationFieldSpec(
makeFs(theGvk, append(path, propName)))
if err != nil {
return
}
}
_, label := property.Extensions.GetString(xLabelSelector)
if label {
err = theConfig.AddLabelFieldSpec(
makeFs(theGvk, append(path, propName)))
if err != nil {
return
}
}
_, identity := property.Extensions.GetString(xIdentity)
if identity {
err = theConfig.AddPrefixFieldSpec(
makeFs(theGvk, append(path, propName)))
if err != nil {
return
}
}
version, ok := property.Extensions.GetString(xVersion)
if ok {
kind, ok := property.Extensions.GetString(xKind)
if ok {
nameKey, ok := property.Extensions.GetString(xNameKey)
if !ok {
nameKey = "name"
}
err = theConfig.AddNamereferenceFieldSpec(
builtinconfig.NameBackReferences{
Gvk: resid.Gvk{Kind: kind, Version: version},
FieldSpecs: []types.FieldSpec{
makeFs(theGvk, append(path, propName, nameKey))},
})
if err != nil {
return
}
}
}
if property.Ref.GetURL() != nil {
loadCrdIntoConfig(
theConfig, theGvk, theMap,
property.Ref.String(), append(path, propName))
}
}
return nil
}
func makeFs(in resid.Gvk, path []string) types.FieldSpec {
return types.FieldSpec{
CreateIfNotPresent: false,
Gvk: in,
Path: strings.Join(path, "/"),
}
}

View File

@@ -0,0 +1,182 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package accumulator_test
import (
"reflect"
"testing"
. "sigs.k8s.io/kustomize/v3/api/accumulator"
"sigs.k8s.io/kustomize/v3/api/builtinconfig"
"sigs.k8s.io/kustomize/v3/api/ifc"
"sigs.k8s.io/kustomize/v3/api/resid"
"sigs.k8s.io/kustomize/v3/api/types"
"sigs.k8s.io/kustomize/v3/internal/loadertest"
)
// This defines two CRD's: Bee and MyKind.
//
// Bee is boring, it's spec has no dependencies.
//
// MyKind, however, has a spec that contains
// a Bee and a (k8s native) Secret.
const (
crdContent = `
{
"github.com/example/pkg/apis/jingfang/v1beta1.Bee": {
"Schema": {
"description": "Bee",
"properties": {
"apiVersion": {
"description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert
recognized schemas to the latest internal value, and may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources",
"type": "string"
},
"kind": {
"description": "Kind is a string value representing the REST resource this object represents. Servers may infer
this from the endpoint the client submits requests to. Cannot be updated. In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds",
"type": "string"
},
"metadata": {
"$ref": "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"
},
"spec": {
"$ref": "github.com/example/pkg/apis/jingfang/v1beta1.BeeSpec"
},
"status": {
"$ref": "github.com/example/pkg/apis/jingfang/v1beta1.BeeStatus"
}
}
},
"Dependencies": [
"github.com/example/pkg/apis/jingfang/v1beta1.BeeSpec",
"github.com/example/pkg/apis/jingfang/v1beta1.BeeStatus",
"k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"
]
},
"github.com/example/pkg/apis/jingfang/v1beta1.BeeSpec": {
"Schema": {
"description": "BeeSpec defines the desired state of Bee"
},
"Dependencies": []
},
"github.com/example/pkg/apis/jingfang/v1beta1.BeeStatus": {
"Schema": {
"description": "BeeStatus defines the observed state of Bee"
},
"Dependencies": []
},
"github.com/example/pkg/apis/jingfang/v1beta1.MyKind": {
"Schema": {
"description": "MyKind",
"properties": {
"apiVersion": {
"description": "APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources",
"type": "string"
},
"kind": {
"description": "Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to. Cannot be updated.
In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds",
"type": "string"
},
"metadata": {
"$ref": "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"
},
"spec": {
"$ref": "github.com/example/pkg/apis/jingfang/v1beta1.MyKindSpec"
},
"status": {
"$ref": "github.com/example/pkg/apis/jingfang/v1beta1.MyKindStatus"
}
}
},
"Dependencies": [
"github.com/example/pkg/apis/jingfang/v1beta1.MyKindSpec",
"github.com/example/pkg/apis/jingfang/v1beta1.MyKindStatus",
"k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"
]
},
"github.com/example/pkg/apis/jingfang/v1beta1.MyKindSpec": {
"Schema": {
"description": "MyKindSpec defines the desired state of MyKind",
"properties": {
"beeRef": {
"x-kubernetes-object-ref-api-version": "v1beta1",
"x-kubernetes-object-ref-kind": "Bee",
"$ref": "github.com/example/pkg/apis/jingfang/v1beta1.Bee"
},
"secretRef": {
"description": "If defined, we use this secret for configuring the MYSQL_ROOT_PASSWORD
If it is not set we generate a secret dynamically",
"x-kubernetes-object-ref-api-version": "v1",
"x-kubernetes-object-ref-kind": "Secret",
"$ref": "k8s.io/api/core/v1.LocalObjectReference"
}
}
},
"Dependencies": [
"github.com/example/pkg/apis/jingfang/v1beta1.Bee",
"k8s.io/api/core/v1.LocalObjectReference"
]
},
"github.com/example/pkg/apis/jingfang/v1beta1.MyKindStatus": {
"Schema": {
"description": "MyKindStatus defines the observed state of MyKind"
},
"Dependencies": []
}
}
`
)
func makeLoader(t *testing.T) ifc.Loader {
ldr := loadertest.NewFakeLoader("/testpath")
err := ldr.AddFile("/testpath/crd.json", []byte(crdContent))
if err != nil {
t.Fatalf("Failed to setup fake ldr.")
}
return ldr
}
func TestLoadCRDs(t *testing.T) {
nbrs := []builtinconfig.NameBackReferences{
{
Gvk: resid.Gvk{Kind: "Secret", Version: "v1"},
FieldSpecs: []types.FieldSpec{
{
CreateIfNotPresent: false,
Gvk: resid.Gvk{Kind: "MyKind"},
Path: "spec/secretRef/name",
},
},
},
{
Gvk: resid.Gvk{Kind: "Bee", Version: "v1beta1"},
FieldSpecs: []types.FieldSpec{
{
CreateIfNotPresent: false,
Gvk: resid.Gvk{Kind: "MyKind"},
Path: "spec/beeRef/name",
},
},
},
}
expectedTc := &builtinconfig.TransformerConfig{
NameReference: nbrs,
}
actualTc, err := LoadConfigFromCRDs(makeLoader(t), []string{"crd.json"})
if err != nil {
t.Fatalf("unexpected error:%v", err)
}
if !reflect.DeepEqual(actualTc, expectedTc) {
t.Fatalf("expected\n %v\n but got\n %v\n", expectedTc, actualTc)
}
}

View File

@@ -0,0 +1,274 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package accumulator
import (
"fmt"
"log"
"sigs.k8s.io/kustomize/v3/api/builtinconfig"
"sigs.k8s.io/kustomize/v3/api/resid"
"sigs.k8s.io/kustomize/v3/api/resmap"
"sigs.k8s.io/kustomize/v3/api/resource"
"sigs.k8s.io/kustomize/v3/api/transform"
)
type nameReferenceTransformer struct {
backRefs []builtinconfig.NameBackReferences
}
var _ resmap.Transformer = &nameReferenceTransformer{}
// newNameReferenceTransformer constructs a nameReferenceTransformer
// with a given slice of NameBackReferences.
func newNameReferenceTransformer(br []builtinconfig.NameBackReferences) resmap.Transformer {
if br == nil {
log.Fatal("backrefs not expected to be nil")
}
return &nameReferenceTransformer{backRefs: br}
}
// Transform updates name references in resource A that
// refer to resource B, given that B's name may have
// changed.
//
// For example, a HorizontalPodAutoscaler (HPA)
// necessarily refers to a Deployment, the thing that
// the HPA scales. The Deployment name might change
// (e.g. prefix added), and the reference in the HPA
// has to be fixed.
//
// In the outer loop over the ResMap below, say we
// encounter a specific HPA. Then, in scanning backrefs,
// we encounter an entry like
//
// - kind: Deployment
// fieldSpecs:
// - kind: HorizontalPodAutoscaler
// path: spec/scaleTargetRef/name
//
// This entry says that an HPA, via its
// 'spec/scaleTargetRef/name' field, may refer to a
// Deployment. This match to HPA means we may need to
// modify the value in its 'spec/scaleTargetRef/name'
// field, by searching for the thing it refers to,
// and getting its new name.
//
// As a filter, and search optimization, we compute a
// subset of all resources that the HPA could refer to,
// by excluding objects from other namespaces, and
// excluding objects that don't have the same prefix-
// suffix mods as the HPA.
//
// We look in this subset for all Deployment objects
// with a resId that has a Name matching the field value
// present in the HPA. If no match do nothing; if more
// than one match, it's an error.
//
// We overwrite the HPA name field with the value found
// in the Deployment's name field (the name in the raw
// object - the modified name - not the unmodified name
// in the Deployment's resId).
//
// This process assumes that the name stored in a ResId
// (the ResMap key) isn't modified by name transformers.
// Name transformers should only modify the name in the
// body of the resource object (the value in the ResMap).
//
func (o *nameReferenceTransformer) Transform(m resmap.ResMap) error {
// TODO: Too much looping, here and in transitive calls.
for _, referrer := range m.Resources() {
var candidates resmap.ResMap
for _, target := range o.backRefs {
for _, fSpec := range target.FieldSpecs {
if referrer.OrgId().IsSelected(&fSpec.Gvk) {
if candidates == nil {
candidates = m.SubsetThatCouldBeReferencedByResource(referrer)
}
err := transform.MutateField(
referrer.Map(),
fSpec.PathSlice(),
fSpec.CreateIfNotPresent,
o.getNewNameFunc(
// referrer could be an HPA instance,
// target could be Gvk for Deployment,
// candidate a list of resources "reachable"
// from the HPA.
referrer, target.Gvk, candidates))
if err != nil {
return err
}
}
}
}
}
return nil
}
// selectReferral picks the referral among a subset of candidates.
// It returns the current name and namespace of the selected candidate.
// Note that the content of the referricalCandidateSubset slice is most of the time
// identical to the referralCandidates resmap. Still in some cases, such
// as ClusterRoleBinding, the subset only contains the resources of a specific
// namespace.
func (o *nameReferenceTransformer) selectReferral(
oldName string,
referrer *resource.Resource,
target resid.Gvk,
referralCandidates resmap.ResMap,
referralCandidateSubset []*resource.Resource) (interface{}, interface{}, error) {
for _, res := range referralCandidateSubset {
id := res.OrgId()
if id.IsSelected(&target) && res.GetOriginalName() == oldName {
matches := referralCandidates.GetMatchingResourcesByOriginalId(id.Equals)
// If there's more than one match, there's no way
// to know which one to pick, so emit error.
if len(matches) > 1 {
return nil, nil, fmt.Errorf(
"multiple matches for %s:\n %v",
id, getIds(matches))
}
// In the resource, note that it is referenced
// by the referrer.
res.AppendRefBy(referrer.CurId())
// Return transformed name of the object,
// complete with prefixes, hashes, etc.
return res.GetName(), res.GetNamespace(), nil
}
}
return oldName, nil, nil
}
// utility function to replace a simple string by the new name
func (o *nameReferenceTransformer) getSimpleNameField(
oldName string,
referrer *resource.Resource,
target resid.Gvk,
referralCandidates resmap.ResMap,
referralCandidateSubset []*resource.Resource) (interface{}, error) {
newName, _, err := o.selectReferral(oldName, referrer, target,
referralCandidates, referralCandidateSubset)
return newName, err
}
// utility function to replace name field within a map[string]interface{}
// and leverage the namespace field.
func (o *nameReferenceTransformer) getNameAndNsStruct(
inMap map[string]interface{},
referrer *resource.Resource,
target resid.Gvk,
referralCandidates resmap.ResMap) (interface{}, error) {
// Example:
if _, ok := inMap["name"]; !ok {
return nil, fmt.Errorf(
"%#v is expected to contain a name field", inMap)
}
oldName, ok := inMap["name"].(string)
if !ok {
return nil, fmt.Errorf(
"%#v is expected to contain a name field of type string", oldName)
}
subset := referralCandidates.Resources()
if namespacevalue, ok := inMap["namespace"]; ok {
namespace := namespacevalue.(string)
bynamespace := referralCandidates.GroupedByOriginalNamespace()
if _, ok := bynamespace[namespace]; !ok {
return inMap, nil
}
subset = bynamespace[namespace]
}
newname, newnamespace, err := o.selectReferral(oldName, referrer, target,
referralCandidates, subset)
if err != nil {
return nil, err
}
if (newname == oldName) && (newnamespace == nil) {
// no candidate found.
return inMap, nil
}
inMap["name"] = newname
if newnamespace != "" {
// We don't want value "" to replace value "default" since
// the empty string is handled as a wild card here not default namespace
// by kubernetes.
inMap["namespace"] = newnamespace
}
return inMap, nil
}
func (o *nameReferenceTransformer) getNewNameFunc(
referrer *resource.Resource,
target resid.Gvk,
referralCandidates resmap.ResMap) func(in interface{}) (interface{}, error) {
return func(in interface{}) (interface{}, error) {
switch in.(type) {
case string:
oldName, _ := in.(string)
return o.getSimpleNameField(oldName, referrer, target,
referralCandidates, referralCandidates.Resources())
case map[string]interface{}:
// Kind: ValidatingWebhookConfiguration
// FieldSpec is webhooks/clientConfig/service
oldMap, _ := in.(map[string]interface{})
return o.getNameAndNsStruct(oldMap, referrer, target,
referralCandidates)
case []interface{}:
l, _ := in.([]interface{})
for idx, item := range l {
switch item.(type) {
case string:
// Kind: Role/ClusterRole
// FieldSpec is rules.resourceNames
oldName, _ := item.(string)
newName, err := o.getSimpleNameField(oldName, referrer, target,
referralCandidates, referralCandidates.Resources())
if err != nil {
return nil, err
}
l[idx] = newName
case map[string]interface{}:
// Kind: RoleBinding/ClusterRoleBinding
// FieldSpec is subjects
// Note: The corresponding fieldSpec had been changed from
// from path: subjects/name to just path: subjects. This is
// what get mutatefield to request the mapping of the whole
// map containing namespace and name instead of just a simple
// string field containing the name
oldMap, _ := item.(map[string]interface{})
newMap, err := o.getNameAndNsStruct(oldMap, referrer, target,
referralCandidates)
if err != nil {
return nil, err
}
l[idx] = newMap
default:
return nil, fmt.Errorf(
"%#v is expected to be either a []string or a []map[string]interface{}", in)
}
}
return in, nil
default:
return nil, fmt.Errorf(
"%#v is expected to be either a string or a []interface{}", in)
}
}
}
func getIds(rs []*resource.Resource) []string {
var result []string
for _, r := range rs {
result = append(result, r.CurId().String()+"\n")
}
return result
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,109 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package accumulator
import (
"fmt"
"sigs.k8s.io/kustomize/v3/api/accumulator/expansion"
"sigs.k8s.io/kustomize/v3/api/resmap"
"sigs.k8s.io/kustomize/v3/api/transform"
"sigs.k8s.io/kustomize/v3/api/types"
)
type refVarTransformer struct {
varMap map[string]interface{}
replacementCounts map[string]int
fieldSpecs []types.FieldSpec
mappingFunc func(string) interface{}
}
// newRefVarTransformer returns a new refVarTransformer
// that replaces $(VAR) style variables with values.
// The fieldSpecs are the places to look for occurrences of $(VAR).
func newRefVarTransformer(
varMap map[string]interface{}, fs []types.FieldSpec) *refVarTransformer {
return &refVarTransformer{
varMap: varMap,
fieldSpecs: fs,
}
}
// replaceVars accepts as 'in' a string, or string array, which can have
// embedded instances of $VAR style variables, e.g. a container command string.
// The function returns the string with the variables expanded to their final
// values.
func (rv *refVarTransformer) replaceVars(in interface{}) (interface{}, error) {
switch vt := in.(type) {
case []interface{}:
var xs []interface{}
for _, a := range in.([]interface{}) {
xs = append(xs, expansion.Expand(a.(string), rv.mappingFunc))
}
return xs, nil
case map[string]interface{}:
inMap := in.(map[string]interface{})
xs := make(map[string]interface{}, len(inMap))
for k, v := range inMap {
s, ok := v.(string)
if !ok {
// This field not contain a $(VAR) since it is not
// of string type. For instance .spec.replicas: 3 in
// a Deployment object
xs[k] = v
} else {
// This field can potentially contains a $(VAR) since it is
// of string type. For instance .spec.replicas: $(REPLICAS)
// in a Deployment object
xs[k] = expansion.Expand(s, rv.mappingFunc)
}
}
return xs, nil
case interface{}:
s, ok := in.(string)
if !ok {
// This field not contain a $(VAR) since it is not of string type.
return in, nil
}
// This field can potentially contain a $(VAR) since it is
// of string type.
return expansion.Expand(s, rv.mappingFunc), nil
case nil:
return nil, nil
default:
return "", fmt.Errorf("invalid type encountered %T", vt)
}
}
// UnusedVars returns slice of Var names that were unused
// after a Transform run.
func (rv *refVarTransformer) UnusedVars() []string {
var unused []string
for k := range rv.varMap {
_, ok := rv.replacementCounts[k]
if !ok {
unused = append(unused, k)
}
}
return unused
}
// Transform replaces $(VAR) style variables with values.
func (rv *refVarTransformer) Transform(m resmap.ResMap) error {
rv.replacementCounts = make(map[string]int)
rv.mappingFunc = expansion.MappingFuncFor(
rv.replacementCounts, rv.varMap)
for _, res := range m.Resources() {
for _, fieldSpec := range rv.fieldSpecs {
if res.OrgId().IsSelected(&fieldSpec.Gvk) {
if err := transform.MutateField(
res.Map(), fieldSpec.PathSlice(),
false, rv.replaceVars); err != nil {
return err
}
}
}
}
return nil
}

View File

@@ -0,0 +1,137 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package accumulator
import (
"reflect"
"testing"
"sigs.k8s.io/kustomize/v3/api/resid"
"sigs.k8s.io/kustomize/v3/api/resmap"
"sigs.k8s.io/kustomize/v3/api/resource"
"sigs.k8s.io/kustomize/v3/api/testutils/resmaptest"
"sigs.k8s.io/kustomize/v3/api/types"
"sigs.k8s.io/kustomize/v3/k8sdeps/kunstruct"
)
func TestRefVarTransformer(t *testing.T) {
type given struct {
varMap map[string]interface{}
fs []types.FieldSpec
res resmap.ResMap
}
type expected struct {
res resmap.ResMap
unused []string
}
testCases := []struct {
description string
given given
expected expected
}{
{
description: "var replacement in map[string]",
given: given{
varMap: map[string]interface{}{
"FOO": "replacementForFoo",
"BAR": "replacementForBar",
"BAZ": int64(5),
"BOO": true,
},
fs: []types.FieldSpec{
{Gvk: resid.Gvk{Version: "v1", Kind: "ConfigMap"}, Path: "data/map"},
{Gvk: resid.Gvk{Version: "v1", Kind: "ConfigMap"}, Path: "data/slice"},
{Gvk: resid.Gvk{Version: "v1", Kind: "ConfigMap"}, Path: "data/interface"},
{Gvk: resid.Gvk{Version: "v1", Kind: "ConfigMap"}, Path: "data/nil"},
{Gvk: resid.Gvk{Version: "v1", Kind: "ConfigMap"}, Path: "data/num"},
},
res: resmaptest_test.NewRmBuilder(
t, resource.NewFactory(kunstruct.NewKunstructuredFactoryImpl())).
Add(map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm1",
},
"data": map[string]interface{}{
"map": map[string]interface{}{
"item1": "$(FOO)",
"item2": "bla",
"item3": "$(BAZ)",
"item4": "$(BAZ)+$(BAZ)",
"item5": "$(BOO)",
"item6": "if $(BOO)",
"item7": 2019,
},
"slice": []interface{}{
"$(FOO)",
"bla",
"$(BAZ)",
"$(BAZ)+$(BAZ)",
"$(BOO)",
"if $(BOO)",
},
"interface": "$(FOO)",
"nil": nil,
"num": 2019,
}}).ResMap(),
},
expected: expected{
res: resmaptest_test.NewRmBuilder(
t, resource.NewFactory(kunstruct.NewKunstructuredFactoryImpl())).
Add(map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm1",
},
"data": map[string]interface{}{
"map": map[string]interface{}{
"item1": "replacementForFoo",
"item2": "bla",
"item3": int64(5),
"item4": "5+5",
"item5": true,
"item6": "if true",
"item7": 2019,
},
"slice": []interface{}{
"replacementForFoo",
"bla",
int64(5),
"5+5",
true,
"if true",
},
"interface": "replacementForFoo",
"nil": nil,
"num": 2019,
}}).ResMap(),
unused: []string{"BAR"},
},
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
// arrange
tr := newRefVarTransformer(tc.given.varMap, tc.given.fs)
// act
err := tr.Transform(tc.given.res)
// assert
if err != nil {
t.Errorf("unexpected error: %v", err)
}
a, e := tc.given.res, tc.expected.res
if !reflect.DeepEqual(a, e) {
err = e.ErrorIfNotEqualLists(a)
t.Fatalf("actual doesn't match expected: \nACTUAL:\n%v\nEXPECTED:\n%v\nERR: %v", a, e, err)
}
})
}
}

View File

@@ -0,0 +1,166 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package accumulator
import (
"fmt"
"log"
"strings"
"sigs.k8s.io/kustomize/v3/api/builtinconfig"
"sigs.k8s.io/kustomize/v3/api/resid"
"sigs.k8s.io/kustomize/v3/api/resmap"
"sigs.k8s.io/kustomize/v3/api/types"
)
// ResAccumulator accumulates resources and the rules
// used to customize those resources. It's a ResMap
// plus stuff needed to modify the ResMap.
type ResAccumulator struct {
resMap resmap.ResMap
tConfig *builtinconfig.TransformerConfig
varSet types.VarSet
}
func MakeEmptyAccumulator() *ResAccumulator {
ra := &ResAccumulator{}
ra.resMap = resmap.New()
ra.tConfig = &builtinconfig.TransformerConfig{}
ra.varSet = types.NewVarSet()
return ra
}
// ResMap returns a copy of the internal resMap.
func (ra *ResAccumulator) ResMap() resmap.ResMap {
return ra.resMap.ShallowCopy()
}
// Vars returns a copy of underlying vars.
func (ra *ResAccumulator) Vars() []types.Var {
return ra.varSet.AsSlice()
}
func (ra *ResAccumulator) AppendAll(
resources resmap.ResMap) error {
return ra.resMap.AppendAll(resources)
}
func (ra *ResAccumulator) AbsorbAll(
resources resmap.ResMap) error {
return ra.resMap.AbsorbAll(resources)
}
func (ra *ResAccumulator) MergeConfig(
tConfig *builtinconfig.TransformerConfig) (err error) {
ra.tConfig, err = ra.tConfig.Merge(tConfig)
return err
}
func (ra *ResAccumulator) GetTransformerConfig() *builtinconfig.TransformerConfig {
return ra.tConfig
}
func (ra *ResAccumulator) MergeVars(incoming []types.Var) error {
for _, v := range incoming {
targetId := resid.NewResIdWithNamespace(v.ObjRef.GVK(), v.ObjRef.Name, v.ObjRef.Namespace)
idMatcher := targetId.GvknEquals
if targetId.Namespace != "" || !targetId.IsNamespaceableKind() {
// Preserve backward compatibility. An empty namespace means
// wildcard search on the namespace hence we still use GvknEquals
idMatcher = targetId.Equals
}
matched := ra.resMap.GetMatchingResourcesByOriginalId(idMatcher)
if len(matched) > 1 {
return fmt.Errorf(
"found %d resId matches for var %s "+
"(unable to disambiguate)",
len(matched), v)
}
if len(matched) == 1 {
matched[0].AppendRefVarName(v)
}
}
return ra.varSet.MergeSlice(incoming)
}
func (ra *ResAccumulator) MergeAccumulator(other *ResAccumulator) (err error) {
err = ra.AppendAll(other.resMap)
if err != nil {
return err
}
err = ra.MergeConfig(other.tConfig)
if err != nil {
return err
}
return ra.varSet.MergeSet(other.varSet)
}
func (ra *ResAccumulator) findVarValueFromResources(v types.Var) (interface{}, error) {
for _, res := range ra.resMap.Resources() {
for _, varName := range res.GetRefVarNames() {
if varName == v.Name {
s, err := res.GetFieldValue(v.FieldRef.FieldPath)
if err != nil {
return "", fmt.Errorf(
"field specified in var '%v' "+
"not found in corresponding resource", v)
}
return s, nil
}
}
}
return "", fmt.Errorf(
"var '%v' cannot be mapped to a field "+
"in the set of known resources", v)
}
// makeVarReplacementMap returns a map of Var names to
// their final values. The values are strings intended
// for substitution wherever the $(var.Name) occurs.
func (ra *ResAccumulator) makeVarReplacementMap() (map[string]interface{}, error) {
result := map[string]interface{}{}
for _, v := range ra.Vars() {
s, err := ra.findVarValueFromResources(v)
if err != nil {
return nil, err
}
result[v.Name] = s
}
return result, nil
}
func (ra *ResAccumulator) Transform(t resmap.Transformer) error {
return t.Transform(ra.resMap)
}
func (ra *ResAccumulator) ResolveVars() error {
replacementMap, err := ra.makeVarReplacementMap()
if err != nil {
return err
}
if len(replacementMap) == 0 {
return nil
}
t := newRefVarTransformer(
replacementMap, ra.tConfig.VarReference)
err = ra.Transform(t)
if len(t.UnusedVars()) > 0 {
log.Printf(
"well-defined vars that were never replaced: %s\n",
strings.Join(t.UnusedVars(), ","))
}
return err
}
func (ra *ResAccumulator) FixBackReferences() (err error) {
if ra.tConfig.NameReference == nil {
return nil
}
return ra.Transform(newNameReferenceTransformer(
ra.tConfig.NameReference))
}

View File

@@ -0,0 +1,417 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package accumulator_test
import (
"bytes"
"log"
"os"
"strings"
"testing"
. "sigs.k8s.io/kustomize/v3/api/accumulator"
"sigs.k8s.io/kustomize/v3/api/builtinconfig"
"sigs.k8s.io/kustomize/v3/api/resid"
"sigs.k8s.io/kustomize/v3/api/resmap"
"sigs.k8s.io/kustomize/v3/api/resource"
"sigs.k8s.io/kustomize/v3/api/testutils/resmaptest"
"sigs.k8s.io/kustomize/v3/api/types"
"sigs.k8s.io/kustomize/v3/k8sdeps/kunstruct"
)
func makeResAccumulator(t *testing.T) (*ResAccumulator, *resource.Factory) {
ra := MakeEmptyAccumulator()
err := ra.MergeConfig(builtinconfig.MakeDefaultConfig())
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
rf := resource.NewFactory(
kunstruct.NewKunstructuredFactoryImpl())
err = ra.AppendAll(
resmaptest_test.NewRmBuilder(t, rf).
Add(map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "deploy1",
},
"spec": map[string]interface{}{
"template": map[string]interface{}{
"spec": map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{
"command": []interface{}{
"myserver",
"--somebackendService $(SERVICE_ONE)",
"--yetAnother $(SERVICE_TWO)",
},
},
},
},
},
}}).
Add(map[string]interface{}{
"apiVersion": "v1",
"kind": "Service",
"metadata": map[string]interface{}{
"name": "backendOne",
}}).
Add(map[string]interface{}{
"apiVersion": "v1",
"kind": "Service",
"metadata": map[string]interface{}{
"name": "backendTwo",
}}).ResMap())
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
return ra, rf
}
func TestResolveVarsHappy(t *testing.T) {
ra, _ := makeResAccumulator(t)
err := ra.MergeVars([]types.Var{
{
Name: "SERVICE_ONE",
ObjRef: types.Target{
Gvk: resid.Gvk{Version: "v1", Kind: "Service"},
Name: "backendOne"},
},
{
Name: "SERVICE_TWO",
ObjRef: types.Target{
Gvk: resid.Gvk{Version: "v1", Kind: "Service"},
Name: "backendTwo"},
},
})
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
err = ra.ResolveVars()
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
c := getCommand(find("deploy1", ra.ResMap()))
if c != "myserver --somebackendService backendOne --yetAnother backendTwo" {
t.Fatalf("unexpected command: %s", c)
}
}
func TestResolveVarsOneUnused(t *testing.T) {
ra, _ := makeResAccumulator(t)
err := ra.MergeVars([]types.Var{
{
Name: "SERVICE_ONE",
ObjRef: types.Target{
Gvk: resid.Gvk{Version: "v1", Kind: "Service"},
Name: "backendOne"},
},
{
Name: "SERVICE_UNUSED",
ObjRef: types.Target{
Gvk: resid.Gvk{Version: "v1", Kind: "Service"},
Name: "backendTwo"},
},
})
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
var buf bytes.Buffer
log.SetOutput(&buf)
defer func() {
log.SetOutput(os.Stderr)
}()
err = ra.ResolveVars()
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
expectLog(t, buf, "well-defined vars that were never replaced: SERVICE_UNUSED")
c := getCommand(find("deploy1", ra.ResMap()))
if c != "myserver --somebackendService backendOne --yetAnother $(SERVICE_TWO)" {
t.Fatalf("unexpected command: %s", c)
}
}
func expectLog(t *testing.T, log bytes.Buffer, expect string) {
if !strings.Contains(log.String(), expect) {
t.Fatalf("expected log containing '%s', got '%s'", expect, log.String())
}
}
func TestResolveVarsVarNeedsDisambiguation(t *testing.T) {
ra, rf := makeResAccumulator(t)
rm0 := resmap.New()
err := rm0.Append(
rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "Service",
"metadata": map[string]interface{}{
"name": "backendOne",
"namespace": "fooNamespace",
},
}))
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
err = ra.AppendAll(rm0)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
err = ra.MergeVars([]types.Var{
{
Name: "SERVICE_ONE",
ObjRef: types.Target{
Gvk: resid.Gvk{Version: "v1", Kind: "Service"},
Name: "backendOne",
},
},
})
if err == nil {
t.Fatalf("expected error")
}
if !strings.Contains(
err.Error(), "unable to disambiguate") {
t.Fatalf("unexpected err: %v", err)
}
}
func makeNamespacedConfigMapWithDataProviderValue(
namespace string,
value string,
) map[string]interface{} {
return map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "environment",
"namespace": namespace,
},
"data": map[string]interface{}{
"provider": value,
},
}
}
func makeVarToNamepaceAndPath(
name string,
namespace string,
path string,
) types.Var {
return types.Var{
Name: name,
ObjRef: types.Target{
Gvk: resid.Gvk{Version: "v1", Kind: "ConfigMap"},
Name: "environment",
Namespace: namespace,
},
FieldRef: types.FieldSelector{FieldPath: path},
}
}
func TestResolveVarConflicts(t *testing.T) {
rf := resource.NewFactory(kunstruct.NewKunstructuredFactoryImpl())
// create configmaps in foo and bar namespaces with `data.provider` values.
fooAws := makeNamespacedConfigMapWithDataProviderValue("foo", "aws")
barAws := makeNamespacedConfigMapWithDataProviderValue("bar", "aws")
barGcp := makeNamespacedConfigMapWithDataProviderValue("bar", "gcp")
// create two variables with (apparently) conflicting names that point to
// fieldpaths that could be generalized.
varFoo := makeVarToNamepaceAndPath("PROVIDER", "foo", "data.provider")
varBar := makeVarToNamepaceAndPath("PROVIDER", "bar", "data.provider")
// create accumulators holding apparently conflicting vars that are not
// actually in conflict because they point to the same concrete value.
rm0 := resmap.New()
rm0.Append(rf.FromMap(fooAws))
ac0 := MakeEmptyAccumulator()
ac0.AppendAll(rm0)
ac0.MergeVars([]types.Var{varFoo})
rm1 := resmap.New()
rm1.Append(rf.FromMap(barAws))
ac1 := MakeEmptyAccumulator()
ac1.AppendAll(rm1)
ac1.MergeVars([]types.Var{varBar})
// validate that two vars of the same name which reference the same concrete
// value do not produce a conflict.
err := ac0.MergeAccumulator(ac1)
if err == nil {
t.Fatalf("see bug gh-1600")
}
// create an accumulator will have an actually conflicting value with the
// two above (because it contains a variable whose name is used in the other
// accumulators AND whose concrete values are different).
rm2 := resmap.New()
rm2.Append(rf.FromMap(barGcp))
ac2 := MakeEmptyAccumulator()
ac2.AppendAll(rm2)
ac2.MergeVars([]types.Var{varBar})
err = ac1.MergeAccumulator(ac2)
if err == nil {
t.Fatalf("dupe vars w/ different concrete values should conflict")
}
}
func TestResolveVarsGoodResIdBadField(t *testing.T) {
ra, _ := makeResAccumulator(t)
err := ra.MergeVars([]types.Var{
{
Name: "SERVICE_ONE",
ObjRef: types.Target{
Gvk: resid.Gvk{Version: "v1", Kind: "Service"},
Name: "backendOne"},
FieldRef: types.FieldSelector{FieldPath: "nope_nope_nope"},
},
})
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
err = ra.ResolveVars()
if err == nil {
t.Fatalf("expected error")
}
if !strings.Contains(
err.Error(),
"not found in corresponding resource") {
t.Fatalf("unexpected err: %v", err)
}
}
func TestResolveVarsUnmappableVar(t *testing.T) {
ra, _ := makeResAccumulator(t)
err := ra.MergeVars([]types.Var{
{
Name: "SERVICE_THREE",
ObjRef: types.Target{
Gvk: resid.Gvk{Version: "v1", Kind: "Service"},
Name: "doesNotExist"},
},
})
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
err = ra.ResolveVars()
if err == nil {
t.Fatalf("expected error")
}
if !strings.Contains(
err.Error(),
"cannot be mapped to a field in the set of known resources") {
t.Fatalf("unexpected err: %v", err)
}
}
func TestResolveVarsWithNoambiguation(t *testing.T) {
ra1, rf := makeResAccumulator(t)
err := ra1.MergeVars([]types.Var{
{
Name: "SERVICE_ONE",
ObjRef: types.Target{
Gvk: resid.Gvk{Version: "v1", Kind: "Service"},
Name: "backendOne",
},
},
})
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
// Create another accumulator having a resource with different prefix
ra2 := MakeEmptyAccumulator()
m := resmaptest_test.NewRmBuilder(t, rf).
Add(map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "deploy2",
},
"spec": map[string]interface{}{
"template": map[string]interface{}{
"spec": map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{
"command": []interface{}{
"myserver",
"--somebackendService $(SUB_SERVICE_ONE)",
},
},
},
},
},
}}).
Add(map[string]interface{}{
"apiVersion": "v1",
"kind": "Service",
"metadata": map[string]interface{}{
"name": "backendOne",
}}).ResMap()
// Make it seem like this resource
// went through a prefix transformer.
r := m.GetByIndex(1)
r.AddNamePrefix("sub-")
r.SetName("sub-backendOne") // original name remains "backendOne"
err = ra2.AppendAll(m)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
err = ra2.MergeVars([]types.Var{
{
Name: "SUB_SERVICE_ONE",
ObjRef: types.Target{
Gvk: resid.Gvk{Version: "v1", Kind: "Service"},
Name: "backendOne",
},
},
})
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
err = ra1.MergeAccumulator(ra2)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
err = ra1.ResolveVars()
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
}
func find(name string, resMap resmap.ResMap) *resource.Resource {
for _, r := range resMap.Resources() {
if r.GetName() == name {
return r
}
}
return nil
}
// Assumes arg is a deployment, returns the command of first container.
func getCommand(r *resource.Resource) string {
var m map[string]interface{}
var c []interface{}
m, _ = r.Map()["spec"].(map[string]interface{})
m, _ = m["template"].(map[string]interface{})
m, _ = m["spec"].(map[string]interface{})
c, _ = m["containers"].([]interface{})
m, _ = c[0].(map[string]interface{})
cmd, _ := m["command"].([]interface{})
n := make([]string, len(cmd))
for i, v := range cmd {
n[i] = v.(string)
}
return strings.Join(n, " ")
}

View File

@@ -4,9 +4,10 @@
package builtinconfig
import (
"strings"
"sigs.k8s.io/kustomize/v3/api/resid"
"sigs.k8s.io/kustomize/v3/api/types"
"strings"
)
// NameBackReferences is an association between a gvk.GVK and a list

View File

@@ -5,9 +5,10 @@ package builtinconfig
import (
"reflect"
"testing"
"sigs.k8s.io/kustomize/v3/api/resid"
"sigs.k8s.io/kustomize/v3/api/types"
"testing"
)
func TestMergeAll(t *testing.T) {

View File

@@ -0,0 +1,12 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package inventory
const (
// Annotation that contains the inventory content.
ContentAnnotation = "kustomize.config.k8s.io/Inventory"
// Annotation for inventory content hash.
HashAnnotation = "kustomize.config.k8s.io/InventoryHash"
)

235
api/inventory/inventory.go Normal file
View File

@@ -0,0 +1,235 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package inventory
import (
"encoding/json"
"sigs.k8s.io/kustomize/v3/api/resid"
)
//Refs is a reference map. Each key is the id
//of a k8s resource, and each value is a list of
//object ids that refer back to the object in the
//key.
//For example, the key could correspond to a
//ConfigMap, and the list of values might include
//several different Deployments that get data from
//that ConfigMap (and thus refer to it).
//References are important in inventory management
//because one may not delete an object before all
//objects referencing it have been removed.
type Refs map[resid.ResId][]resid.ResId
func NewRefs() Refs {
return Refs{}
}
// Merge merges a Refs into an existing Refs
func (rf Refs) Merge(b Refs) Refs {
for key, value := range b {
_, ok := rf[key]
if ok {
rf[key] = append(rf[key], value...)
} else {
rf[key] = value
}
}
return rf
}
// removeIfContains removes the reference relationship
// a --> b
// from the Refs if it exists
func (rf Refs) RemoveIfContains(a, b resid.ResId) {
refs, ok := rf[a]
if !ok {
return
}
for i, ref := range refs {
if ref.Equals(b) {
rf[a] = append(refs[:i], refs[i+1:]...)
break
}
}
}
//Inventory is a an object intended for
//serialization into the annotations of a so-called
//apply-root object (a ConfigMap, an Application,
//etc.) living in the cluster. This apply-root
//object is written as part of an apply operation as
//a means to record overall cluster state changes.
//At the end of a successful apply, the "current"
//field in Inventory will be a map whose keys all
//correspond to an object in the cluster, and
//"previous" will be the previous such set (an empty
//set on the first apply).
//An Inventory allows the Prune method to work.
type Inventory struct {
Current Refs `json:"current,omitempty"`
Previous Refs `json:"previous,omitempty"`
}
// NewInventory returns an Inventory object
func NewInventory() *Inventory {
return &Inventory{
Current: NewRefs(),
Previous: NewRefs(),
}
}
// UpdateCurrent updates the Inventory given a
// new current Refs
// The existing Current refs is merged into
// the Previous refs
func (a *Inventory) UpdateCurrent(curref Refs) *Inventory {
if len(a.Previous) > 0 {
a.Previous.Merge(a.Current)
} else {
a.Previous = a.Current
}
a.Current = curref
return a
}
func (a *Inventory) removeNewlyOrphanedItemsFromPrevious() []resid.ResId {
var results []resid.ResId
for item, refs := range a.Previous {
if _, ok := a.Current[item]; ok {
delete(a.Previous, item)
continue
}
var newRefs []resid.ResId
toDelete := true
for _, ref := range refs {
if _, ok := a.Current[ref]; ok {
toDelete = false
newRefs = append(newRefs, ref)
}
}
if toDelete {
results = append(results, item)
delete(a.Previous, item)
} else {
a.Previous[item] = newRefs
}
}
return results
}
func (a *Inventory) removeOrphanedItemsFromPreviousThatAreNotInCurrent() []resid.ResId {
var results []resid.ResId
for item, refs := range a.Previous {
if _, ok := a.Current[item]; ok {
continue
}
if len(refs) == 0 {
results = append(results, item)
delete(a.Previous, item)
}
}
return results
}
func (a *Inventory) removeOrphanedItemsFromPreviousThatAreInCurrent() {
//Remove references from Previous that are already in Current refs
for item, refs := range a.Current {
for _, ref := range refs {
a.Previous.RemoveIfContains(item, ref)
}
}
//Remove items from Previous that are already in Current refs
for item, refs := range a.Previous {
if len(refs) == 0 {
if _, ok := a.Current[item]; ok {
delete(a.Previous, item)
}
}
}
}
// Prune computes the diff of Current refs and Previous refs
// and returns a list of Items that can be pruned.
// An item that can be pruned shows up only in Previous refs.
// Prune also updates the Previous refs with those items removed
func (a *Inventory) Prune() []resid.ResId {
a.removeOrphanedItemsFromPreviousThatAreInCurrent()
// These are candidates for deletion from the cluster.
removable1 := a.removeOrphanedItemsFromPreviousThatAreNotInCurrent()
removable2 := a.removeNewlyOrphanedItemsFromPrevious()
return append(removable1, removable2...)
}
// inventory is the internal type used for serialization
type inventory struct {
Current map[string][]resid.ResId `json:"current,omitempty"`
Previous map[string][]resid.ResId `json:"previous,omitempty"`
}
func (a *Inventory) toInternalType() inventory {
prev := map[string][]resid.ResId{}
curr := map[string][]resid.ResId{}
for id, refs := range a.Current {
curr[id.String()] = refs
}
for id, refs := range a.Previous {
prev[id.String()] = refs
}
return inventory{
Current: curr,
Previous: prev,
}
}
func (a *Inventory) fromInternalType(i *inventory) {
for s, refs := range i.Previous {
a.Previous[resid.FromString(s)] = refs
}
for s, refs := range i.Current {
a.Current[resid.FromString(s)] = refs
}
}
func (a *Inventory) marshal() ([]byte, error) {
return json.Marshal(a.toInternalType())
}
func (a *Inventory) unMarshal(data []byte) error {
inv := &inventory{
Current: map[string][]resid.ResId{},
Previous: map[string][]resid.ResId{},
}
err := json.Unmarshal(data, inv)
if err != nil {
return err
}
a.fromInternalType(inv)
return nil
}
// UpdateAnnotations update the annotation map
func (a *Inventory) UpdateAnnotations(annot map[string]string) error {
data, err := a.marshal()
if err != nil {
return err
}
annot[ContentAnnotation] = string(data)
return nil
}
// LoadFromAnnotation loads the Inventory date from the annotation map
func (a *Inventory) LoadFromAnnotation(annot map[string]string) error {
value, ok := annot[ContentAnnotation]
if ok {
return a.unMarshal([]byte(value))
}
return nil
}

View File

@@ -0,0 +1,60 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package inventory_test
import (
"testing"
. "sigs.k8s.io/kustomize/v3/api/inventory"
"sigs.k8s.io/kustomize/v3/api/resid"
)
func makeRefs() (Refs, Refs) {
a := resid.FromString("G1_V1_K1|ns1|nm1")
b := resid.FromString("G2_V2_K2|ns2|nm2")
c := resid.FromString("G3_V3_K3|ns3|nm3")
current := NewRefs()
current[a] = []resid.ResId{b, c}
current[b] = []resid.ResId{}
current[c] = []resid.ResId{}
newRefs := NewRefs()
newRefs[a] = []resid.ResId{b}
newRefs[b] = []resid.ResId{}
return current, newRefs
}
func TestInventory(t *testing.T) {
inventory := NewInventory()
curref, _ := makeRefs()
inventory.UpdateCurrent(curref)
if len(inventory.Current) != 3 {
t.Fatalf("not getting the correct inventory %v", inventory)
}
curref, newref := makeRefs()
inventory.UpdateCurrent(curref)
if len(inventory.Current) != 3 {
t.Fatalf("not getting the corrent inventory %v", inventory)
}
if len(inventory.Previous) != 3 {
t.Fatalf("not getting the corrent inventory %v", inventory)
}
items := inventory.Prune()
if len(items) != 0 {
t.Fatalf("not getting the corrent items %v", items)
}
if len(inventory.Previous) != 0 {
t.Fatalf("not getting the corrent inventory %v", inventory)
}
inventory.UpdateCurrent(newref)
items = inventory.Prune()
if len(items) != 1 {
t.Fatalf("not getting the corrent items %v", items)
}
if len(inventory.Previous) != 0 {
t.Fatalf("not getting the corrent inventory %v", inventory.Previous)
}
}

View File

@@ -1,18 +1,5 @@
/*
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.
*/
/// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package loader
@@ -28,7 +15,7 @@ import (
"sigs.k8s.io/kustomize/v3/api/filesys"
"sigs.k8s.io/kustomize/v3/api/ifc"
"sigs.k8s.io/kustomize/v3/api/internal/git"
"sigs.k8s.io/kustomize/v3/pkg/pgmconfig"
"sigs.k8s.io/kustomize/v3/api/pgmconfig"
)
type testData struct {

View File

@@ -0,0 +1,41 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package pgmconfig
// RecognizedKustomizationFileNames is a list of file names
// that kustomize recognizes.
// To avoid ambiguity, a kustomization directory may not
// contain more than one match to this list.
func RecognizedKustomizationFileNames() []string {
return []string{
"kustomization.yaml",
"kustomization.yml",
"Kustomization",
}
}
func DefaultKustomizationFileName() string {
return RecognizedKustomizationFileNames()[0]
}
const (
// An environment variable to consult for kustomization
// configuration data. See:
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
XdgConfigHome = "XDG_CONFIG_HOME"
// Use this when XdgConfigHome not defined.
DefaultConfigSubdir = ".config"
// Program name, for help, finding the XDG_CONFIG_DIR, etc.
ProgramName = "kustomize"
// TODO: delete this. it's a copy of a const
// defined elsewhere but used by pluginator.
DomainName = "sigs.k8s.io"
// TODO: delete this. its a copy of a const
// defined elsewhere but used by pluginator.
PluginRoot = "plugin"
)

View File

@@ -0,0 +1,37 @@
// Code generated by "stringer -type=BuiltinPluginType"; DO NOT EDIT.
package builtins
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[Unknown-0]
_ = x[SecretGenerator-1]
_ = x[ConfigMapGenerator-2]
_ = x[ReplicaCountTransformer-3]
_ = x[NamespaceTransformer-4]
_ = x[PatchJson6902Transformer-5]
_ = x[PatchStrategicMergeTransformer-6]
_ = x[PatchTransformer-7]
_ = x[LabelTransformer-8]
_ = x[AnnotationsTransformer-9]
_ = x[PrefixSuffixTransformer-10]
_ = x[ImageTagTransformer-11]
_ = x[HashTransformer-12]
_ = x[InventoryTransformer-13]
_ = x[LegacyOrderTransformer-14]
}
const _BuiltinPluginType_name = "UnknownSecretGeneratorConfigMapGeneratorReplicaCountTransformerNamespaceTransformerPatchJson6902TransformerPatchStrategicMergeTransformerPatchTransformerLabelTransformerAnnotationsTransformerPrefixSuffixTransformerImageTagTransformerHashTransformerInventoryTransformerLegacyOrderTransformer"
var _BuiltinPluginType_index = [...]uint16{0, 7, 22, 40, 63, 83, 107, 137, 153, 169, 191, 214, 233, 248, 268, 290}
func (i BuiltinPluginType) String() string {
if i < 0 || i >= BuiltinPluginType(len(_BuiltinPluginType_index)-1) {
return "BuiltinPluginType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _BuiltinPluginType_name[_BuiltinPluginType_index[i]:_BuiltinPluginType_index[i+1]]
}

View File

@@ -0,0 +1,75 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package builtins
import (
"sigs.k8s.io/kustomize/v3/api/resmap"
"sigs.k8s.io/kustomize/v3/plugin/builtin"
)
//go:generate stringer -type=BuiltinPluginType
type BuiltinPluginType int
const (
Unknown BuiltinPluginType = iota
SecretGenerator
ConfigMapGenerator
ReplicaCountTransformer
NamespaceTransformer
PatchJson6902Transformer
PatchStrategicMergeTransformer
PatchTransformer
LabelTransformer
AnnotationsTransformer
PrefixSuffixTransformer
ImageTagTransformer
HashTransformer
InventoryTransformer
LegacyOrderTransformer
)
var stringToBuiltinPluginTypeMap map[string]BuiltinPluginType
func init() {
stringToBuiltinPluginTypeMap = makeStringToBuiltinPluginTypeMap()
}
func makeStringToBuiltinPluginTypeMap() (result map[string]BuiltinPluginType) {
result = make(map[string]BuiltinPluginType, 23)
for k := range GeneratorFactories {
result[k.String()] = k
}
for k := range TransformerFactories {
result[k.String()] = k
}
return
}
func GetBuiltinPluginType(n string) BuiltinPluginType {
result, ok := stringToBuiltinPluginTypeMap[n]
if ok {
return result
}
return Unknown
}
var GeneratorFactories = map[BuiltinPluginType]func() resmap.GeneratorPlugin{
SecretGenerator: builtin.NewSecretGeneratorPlugin,
ConfigMapGenerator: builtin.NewConfigMapGeneratorPlugin,
}
var TransformerFactories = map[BuiltinPluginType]func() resmap.TransformerPlugin{
NamespaceTransformer: builtin.NewNamespaceTransformerPlugin,
ReplicaCountTransformer: builtin.NewReplicaCountTransformerPlugin,
PatchJson6902Transformer: builtin.NewPatchJson6902TransformerPlugin,
PatchStrategicMergeTransformer: builtin.NewPatchStrategicMergeTransformerPlugin,
PatchTransformer: builtin.NewPatchTransformerPlugin,
LabelTransformer: builtin.NewLabelTransformerPlugin,
AnnotationsTransformer: builtin.NewAnnotationsTransformerPlugin,
PrefixSuffixTransformer: builtin.NewPrefixSuffixTransformerPlugin,
ImageTagTransformer: builtin.NewImageTagTransformerPlugin,
HashTransformer: builtin.NewHashTransformerPlugin,
InventoryTransformer: builtin.NewInventoryTransformerPlugin,
LegacyOrderTransformer: builtin.NewLegacyOrderTransformerPlugin,
}

View File

@@ -0,0 +1,146 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package compiler
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
"sigs.k8s.io/kustomize/v3/api/pgmconfig"
"sigs.k8s.io/kustomize/v3/api/plugins/config"
)
// Compiler creates Go plugin object files.
//
// Source code is read from
// ${srcRoot}/${g}/${v}/${k}.go
//
// Object code is written to
// ${objRoot}/${g}/${v}/${k}.so
type Compiler struct {
srcRoot string
objRoot string
}
// DefaultSrcRoot guesses where the user
// has her ${g}/${v}/$lower(${k})/${k}.go files.
func DefaultSrcRoot() (string, error) {
var nope []string
var root string
root = filepath.Join(
os.Getenv("GOPATH"), "src",
config.DomainName, pgmconfig.ProgramName, config.PluginRoot)
if FileExists(root) {
return root, nil
}
nope = append(nope, root)
root = config.DefaultPluginConfig().DirectoryPath
if FileExists(root) {
return root, nil
}
nope = append(nope, root)
root = filepath.Join(
config.HomeDir(), pgmconfig.ProgramName, config.PluginRoot)
if FileExists(root) {
return root, nil
}
nope = append(nope, root)
return "", fmt.Errorf(
"no default src root; tried %v", nope)
}
// NewCompiler returns a new compiler instance.
func NewCompiler(srcRoot, objRoot string) *Compiler {
return &Compiler{srcRoot: srcRoot, objRoot: objRoot}
}
// ObjRoot is root of compilation target tree.
func (b *Compiler) ObjRoot() string {
return b.objRoot
}
// SrcRoot is where to find src.
func (b *Compiler) SrcRoot() string {
return b.srcRoot
}
func goBin() string {
return filepath.Join(runtime.GOROOT(), "bin", "go")
}
// Compile reads ${srcRoot}/${g}/${v}/${k}.go
// and writes ${objRoot}/${g}/${v}/${k}.so
func (b *Compiler) Compile(g, v, k string) error {
lowK := strings.ToLower(k)
objDir := filepath.Join(b.objRoot, g, v, lowK)
objFile := filepath.Join(objDir, k) + ".so"
if RecentFileExists(objFile) {
// Skip rebuilding it.
return nil
}
err := os.MkdirAll(objDir, os.ModePerm)
if err != nil {
return err
}
srcFile := filepath.Join(b.srcRoot, g, v, lowK, k) + ".go"
if !FileExists(srcFile) {
// Handy for tests of lone plugins.
s := k + ".go"
if !FileExists(s) {
return fmt.Errorf(
"cannot find source at '%s' or '%s'", srcFile, s)
}
srcFile = s
}
commands := []string{
"build",
"-buildmode",
"plugin",
"-o", objFile, srcFile,
}
goBin := goBin()
if !FileExists(goBin) {
return fmt.Errorf(
"cannot find go compiler %s", goBin)
}
cmd := exec.Command(goBin, commands...)
cmd.Env = os.Environ()
if err := cmd.Run(); err != nil {
return fmt.Errorf(
"compiler error building %s: %v", srcFile, err)
}
return nil
}
// True if file less than 3 minutes old, i.e. not
// accidentally left over from some earlier build.
func RecentFileExists(path string) bool {
fi, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return false
}
}
age := time.Now().Sub(fi.ModTime())
return age.Minutes() < 3
}
func FileExists(name string) bool {
if _, err := os.Stat(name); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}

View File

@@ -0,0 +1,66 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package compiler_test
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
. "sigs.k8s.io/kustomize/v3/api/plugins/compiler"
)
// Regression coverage over compiler behavior.
func TestCompiler(t *testing.T) {
configRoot, err := ioutil.TempDir("", "kustomize-compiler-test")
if err != nil {
t.Errorf("failed to make temp dir: %v", err)
}
srcRoot, err := DefaultSrcRoot()
if err != nil {
t.Error(err)
}
c := NewCompiler(srcRoot, configRoot)
if configRoot != c.ObjRoot() {
t.Errorf("unexpected objRoot %s", c.ObjRoot())
}
expectObj := filepath.Join(
c.ObjRoot(),
"someteam.example.com", "v1", "dateprefixer", "DatePrefixer.so")
if FileExists(expectObj) {
t.Errorf("obj file should not exist yet: %s", expectObj)
}
err = c.Compile("someteam.example.com", "v1", "DatePrefixer")
if err != nil {
t.Error(err)
}
if !RecentFileExists(expectObj) {
t.Errorf("didn't find expected obj file %s", expectObj)
}
expectObj = filepath.Join(
c.ObjRoot(),
"builtin", "", "secretgenerator", "SecretGenerator.so")
if FileExists(expectObj) {
t.Errorf("obj file should not exist yet: %s", expectObj)
}
err = c.Compile("builtin", "", "SecretGenerator")
if err != nil {
t.Error(err)
}
if !RecentFileExists(expectObj) {
t.Errorf("didn't find expected obj file %s", expectObj)
}
err = os.RemoveAll(c.ObjRoot())
if err != nil {
t.Errorf(
"removing temp dir: %s %v", c.ObjRoot(), err)
}
if FileExists(expectObj) {
t.Errorf("cleanup failed; still see: %s", expectObj)
}
}

View File

@@ -0,0 +1,95 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package config
import (
"fmt"
"os"
"path/filepath"
"runtime"
"github.com/spf13/pflag"
"sigs.k8s.io/kustomize/v3/api/pgmconfig"
"sigs.k8s.io/kustomize/v3/api/types"
)
const (
// Used with Go plugins.
PluginSymbol = "KustomizePlugin"
// Location of builtins.
BuiltinPluginPackage = "builtin"
// ApiVersion of builtins.
BuiltinPluginApiVersion = BuiltinPluginPackage
// Domain from which kustomize code is imported, for locating
// plugin source code under $GOPATH.
DomainName = "sigs.k8s.io"
// Name of directory housing all plugins.
PluginRoot = "plugin"
flagEnablePluginsName = "enable_alpha_plugins"
flagEnablePluginsHelp = `enable plugins, an alpha feature.
See https://github.com/kubernetes-sigs/kustomize/blob/master/docs/plugins/README.md
`
flagErrorFmt = `
unable to load external plugin %s because plugins disabled
specify the flag
--%s
to %s`
)
func ActivePluginConfig() *types.PluginConfig {
pc := DefaultPluginConfig()
pc.Enabled = true
return pc
}
func DefaultPluginConfig() *types.PluginConfig {
return &types.PluginConfig{
Enabled: false,
DirectoryPath: filepath.Join(configRoot(), PluginRoot),
}
}
func NotEnabledErr(name string) error {
return fmt.Errorf(
flagErrorFmt,
name,
flagEnablePluginsName,
flagEnablePluginsHelp)
}
func AddFlagEnablePlugins(set *pflag.FlagSet, v *bool) {
set.BoolVar(
v, flagEnablePluginsName,
false, flagEnablePluginsHelp)
}
// Use https://github.com/kirsle/configdir instead?
func configRoot() string {
dir := os.Getenv(pgmconfig.XdgConfigHome)
if len(dir) == 0 {
dir = filepath.Join(
HomeDir(), pgmconfig.DefaultConfigSubdir)
}
return filepath.Join(dir, pgmconfig.ProgramName)
}
func HomeDir() string {
home := os.Getenv(homeEnv())
if len(home) > 0 {
return home
}
return "~"
}
func homeEnv() string {
if runtime.GOOS == "windows" {
return "USERPROFILE"
}
return "HOME"
}

View File

@@ -0,0 +1,45 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package config
import (
"os"
"path/filepath"
"strings"
"testing"
"sigs.k8s.io/kustomize/v3/api/pgmconfig"
)
func TestConfigDirNoXdg(t *testing.T) {
xdg, isSet := os.LookupEnv(pgmconfig.XdgConfigHome)
if isSet {
os.Unsetenv(pgmconfig.XdgConfigHome)
}
s := configRoot()
if isSet {
os.Setenv(pgmconfig.XdgConfigHome, xdg)
}
if !strings.HasSuffix(
s,
rootedPath(pgmconfig.DefaultConfigSubdir, pgmconfig.ProgramName)) {
t.Fatalf("unexpected config dir: %s", s)
}
}
func rootedPath(elem ...string) string {
return string(filepath.Separator) + filepath.Join(elem...)
}
func TestConfigDirWithXdg(t *testing.T) {
xdg, isSet := os.LookupEnv(pgmconfig.XdgConfigHome)
os.Setenv(pgmconfig.XdgConfigHome, rootedPath("blah"))
s := configRoot()
if isSet {
os.Setenv(pgmconfig.XdgConfigHome, xdg)
}
if s != rootedPath("blah", pgmconfig.ProgramName) {
t.Fatalf("unexpected config dir: %s", s)
}
}

View File

@@ -0,0 +1,268 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package execplugin
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"os/exec"
"strconv"
"strings"
"github.com/pkg/errors"
"sigs.k8s.io/kustomize/v3/api/resid"
"sigs.k8s.io/kustomize/v3/api/resmap"
"sigs.k8s.io/kustomize/v3/api/types"
"sigs.k8s.io/yaml"
)
const (
idAnnotation = "kustomize.config.k8s.io/id"
HashAnnotation = "kustomize.config.k8s.io/needs-hash"
BehaviorAnnotation = "kustomize.config.k8s.io/behavior"
tmpConfigFilePrefix = "kust-plugin-config-"
)
// ExecPlugin record the name and args of an executable
// It triggers the executable generator and transformer
type ExecPlugin struct {
// absolute path of the executable
path string
// Optional command line arguments to the executable
// pulled from specially named fields in cfg.
// This is for executables that don't want to parse YAML.
args []string
// Plugin configuration data.
cfg []byte
// PluginHelpers
h *resmap.PluginHelpers
}
func NewExecPlugin(p string) *ExecPlugin {
return &ExecPlugin{path: p}
}
func (p *ExecPlugin) Path() string {
return p.path
}
func (p *ExecPlugin) Args() []string {
return p.args
}
func (p *ExecPlugin) Cfg() []byte {
return p.cfg
}
// isAvailable checks to see if the plugin is available
func (p *ExecPlugin) IsAvailable() bool {
f, err := os.Stat(p.path)
if os.IsNotExist(err) {
return false
}
return f.Mode()&0111 != 0000
}
func (p *ExecPlugin) Config(h *resmap.PluginHelpers, config []byte) error {
p.h = h
p.cfg = config
return p.processOptionalArgsFields()
}
type argsConfig struct {
ArgsOneLiner string `json:"argsOneLiner,omitempty" yaml:"argsOneLiner,omitempty"`
ArgsFromFile string `json:"argsFromFile,omitempty" yaml:"argsFromFile,omitempty"`
}
func (p *ExecPlugin) processOptionalArgsFields() error {
var c argsConfig
yaml.Unmarshal(p.cfg, &c)
if c.ArgsOneLiner != "" {
p.args = strings.Split(c.ArgsOneLiner, " ")
}
if c.ArgsFromFile != "" {
content, err := p.h.Loader().Load(c.ArgsFromFile)
if err != nil {
return err
}
for _, x := range strings.Split(string(content), "\n") {
x := strings.TrimLeft(x, " ")
if x != "" {
p.args = append(p.args, x)
}
}
}
return nil
}
func (p *ExecPlugin) Generate() (resmap.ResMap, error) {
output, err := p.invokePlugin(nil)
if err != nil {
return nil, err
}
rm, err := p.h.ResmapFactory().NewResMapFromBytes(output)
if err != nil {
return nil, err
}
return p.UpdateResourceOptions(rm)
}
func (p *ExecPlugin) Transform(rm resmap.ResMap) error {
// add ResIds as annotations to all objects so that we can add them back
inputRM, err := p.getResMapWithIdAnnotation(rm)
if err != nil {
return err
}
// encode the ResMap so it can be fed to the plugin
resources, err := inputRM.AsYaml()
if err != nil {
return err
}
// invoke the plugin with resources as the input
output, err := p.invokePlugin(resources)
if err != nil {
return fmt.Errorf("%v %s", err, string(output))
}
// update the original ResMap based on the output
return p.updateResMapValues(output, rm)
}
// invokePlugin writes plugin config to a temp file, then
// passes the full temp file path as the first arg to a process
// running the plugin binary. Process output is returned.
func (p *ExecPlugin) invokePlugin(input []byte) ([]byte, error) {
f, err := ioutil.TempFile("", tmpConfigFilePrefix)
if err != nil {
return nil, errors.Wrap(
err, "creating tmp plugin config file")
}
_, err = f.Write(p.cfg)
if err != nil {
return nil, errors.Wrap(
err, "writing plugin config to "+f.Name())
}
err = f.Close()
if err != nil {
return nil, errors.Wrap(
err, "closing plugin config file "+f.Name())
}
cmd := exec.Command(
p.path, append([]string{f.Name()}, p.args...)...)
cmd.Env = p.getEnv()
cmd.Stdin = bytes.NewReader(input)
cmd.Stderr = os.Stderr
if _, err := os.Stat(p.h.Loader().Root()); err == nil {
cmd.Dir = p.h.Loader().Root()
}
result, err := cmd.Output()
if err != nil {
return nil, errors.Wrapf(
err, "failure in plugin configured via %s; %v",
f.Name(), err.Error())
}
return result, os.Remove(f.Name())
}
func (p *ExecPlugin) getEnv() []string {
env := os.Environ()
env = append(env,
"KUSTOMIZE_PLUGIN_CONFIG_STRING="+string(p.cfg),
"KUSTOMIZE_PLUGIN_CONFIG_ROOT="+p.h.Loader().Root())
return env
}
// Returns a new copy of the given ResMap with the ResIds annotated in each Resource
func (p *ExecPlugin) getResMapWithIdAnnotation(rm resmap.ResMap) (resmap.ResMap, error) {
inputRM := rm.DeepCopy()
for _, r := range inputRM.Resources() {
idString, err := yaml.Marshal(r.CurId())
if err != nil {
return nil, err
}
annotations := r.GetAnnotations()
if annotations == nil {
annotations = make(map[string]string)
}
annotations[idAnnotation] = string(idString)
r.SetAnnotations(annotations)
}
return inputRM, nil
}
// updateResMapValues updates the Resource value in the given ResMap
// with the emitted Resource values in output.
func (p *ExecPlugin) updateResMapValues(output []byte, rm resmap.ResMap) error {
outputRM, err := p.h.ResmapFactory().NewResMapFromBytes(output)
if err != nil {
return err
}
for _, r := range outputRM.Resources() {
// for each emitted Resource, find the matching Resource in the original ResMap
// using its id
annotations := r.GetAnnotations()
idString, ok := annotations[idAnnotation]
if !ok {
return fmt.Errorf("the transformer %s should not remove annotation %s",
p.path, idAnnotation)
}
id := resid.ResId{}
err := yaml.Unmarshal([]byte(idString), &id)
if err != nil {
return err
}
res, err := rm.GetByCurrentId(id)
if err != nil {
return fmt.Errorf("unable to find unique match to %s", id.String())
}
// remove the annotation set by Kustomize to track the resource
delete(annotations, idAnnotation)
if len(annotations) == 0 {
annotations = nil
}
r.SetAnnotations(annotations)
// update the ResMap resource value with the transformed object
res.Kunstructured = r.Kunstructured
}
return nil
}
// updateResourceOptions updates the generator options for each resource in the
// given ResMap based on plugin provided annotations.
func (p *ExecPlugin) UpdateResourceOptions(rm resmap.ResMap) (resmap.ResMap, error) {
for _, r := range rm.Resources() {
// Disable name hashing by default and require plugin to explicitly
// request it for each resource.
annotations := r.GetAnnotations()
behavior := annotations[BehaviorAnnotation]
var needsHash bool
if val, ok := annotations[HashAnnotation]; ok {
b, err := strconv.ParseBool(val)
if err != nil {
return nil, fmt.Errorf(
"the annotation %q contains an invalid value (%q)",
HashAnnotation, val)
}
needsHash = b
}
delete(annotations, HashAnnotation)
delete(annotations, BehaviorAnnotation)
if len(annotations) == 0 {
annotations = nil
}
r.SetAnnotations(annotations)
r.SetOptions(types.NewGenArgs(
&types.GeneratorArgs{Behavior: behavior},
&types.GeneratorOptions{DisableNameSuffixHash: !needsHash}))
}
return rm, nil
}

View File

@@ -0,0 +1,179 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package execplugin_test
import (
"fmt"
"strings"
"testing"
"sigs.k8s.io/kustomize/v3/api/plugins/config"
. "sigs.k8s.io/kustomize/v3/api/plugins/execplugin"
"sigs.k8s.io/kustomize/v3/api/plugins/loader"
"sigs.k8s.io/kustomize/v3/api/resmap"
"sigs.k8s.io/kustomize/v3/api/resource"
"sigs.k8s.io/kustomize/v3/api/testutils/valtest"
"sigs.k8s.io/kustomize/v3/api/types"
"sigs.k8s.io/kustomize/v3/internal/loadertest"
"sigs.k8s.io/kustomize/v3/k8sdeps/kunstruct"
)
func TestExecPluginConfig(t *testing.T) {
path := "/app"
rf := resmap.NewFactory(
resource.NewFactory(
kunstruct.NewKunstructuredFactoryImpl()), nil)
ldr := loadertest.NewFakeLoader(path)
v := valtest_test.MakeFakeValidator()
pluginConfig := rf.RF().FromMap(
map[string]interface{}{
"apiVersion": "someteam.example.com/v1",
"kind": "SedTransformer",
"metadata": map[string]interface{}{
"name": "some-random-name",
},
"argsOneLiner": "one two",
"argsFromFile": "sed-input.txt",
})
ldr.AddFile("/app/sed-input.txt", []byte(`
s/$FOO/foo/g
s/$BAR/bar/g
\ \ \
`))
p := NewExecPlugin(
loader.AbsolutePluginPath(
config.DefaultPluginConfig(),
pluginConfig.OrgId()))
yaml, err := pluginConfig.AsYAML()
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
p.Config(resmap.NewPluginHelpers(ldr, v, rf), yaml)
expected := "/kustomize/plugin/someteam.example.com/v1/sedtransformer/SedTransformer"
if !strings.HasSuffix(p.Path(), expected) {
t.Fatalf("expected suffix '%s', got '%s'", expected, p.Path())
}
expected = `apiVersion: someteam.example.com/v1
argsFromFile: sed-input.txt
argsOneLiner: one two
kind: SedTransformer
metadata:
name: some-random-name
`
if expected != string(p.Cfg()) {
t.Fatalf("expected cfg '%s', got '%s'", expected, string(p.Cfg()))
}
if len(p.Args()) != 5 {
t.Fatalf("unexpected arg len %d, %v", len(p.Args()), p.Args())
}
if p.Args()[0] != "one" ||
p.Args()[1] != "two" ||
p.Args()[2] != "s/$FOO/foo/g" ||
p.Args()[3] != "s/$BAR/bar/g" ||
p.Args()[4] != "\\ \\ \\ " {
t.Fatalf("unexpected arg array: %v", p.Args())
}
}
func makeConfigMap(rf *resource.Factory, name, behavior string, hashValue *string) *resource.Resource {
r := rf.FromMap(map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{"name": name},
})
annotations := map[string]string{}
if behavior != "" {
annotations[BehaviorAnnotation] = behavior
}
if hashValue != nil {
annotations[HashAnnotation] = *hashValue
}
if len(annotations) > 0 {
r.SetAnnotations(annotations)
}
return r
}
func makeConfigMapOptions(rf *resource.Factory, name, behavior string, disableHash bool) *resource.Resource {
return rf.FromMapAndOption(map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{"name": name},
}, &types.GeneratorArgs{Behavior: behavior}, &types.GeneratorOptions{DisableNameSuffixHash: disableHash})
}
func strptr(s string) *string {
return &s
}
func TestUpdateResourceOptions(t *testing.T) {
p := NewExecPlugin("")
rf := resource.NewFactory(kunstruct.NewKunstructuredFactoryImpl())
in := resmap.New()
expected := resmap.New()
cases := []struct {
behavior string
needsHash bool
hashValue *string
}{
{hashValue: strptr("false")},
{hashValue: strptr("true"), needsHash: true},
{behavior: "replace"},
{behavior: "merge"},
{behavior: "create"},
{behavior: "nonsense"},
{behavior: "merge", hashValue: strptr("false")},
{behavior: "merge", hashValue: strptr("true"), needsHash: true},
}
for i, c := range cases {
name := fmt.Sprintf("test%d", i)
in.Append(makeConfigMap(rf, name, c.behavior, c.hashValue))
expected.Append(makeConfigMapOptions(rf, name, c.behavior, !c.needsHash))
}
actual, err := p.UpdateResourceOptions(in)
if err != nil {
t.Fatalf("unexpected error: %v", err.Error())
}
for i, a := range expected.Resources() {
b := actual.GetByIndex(i)
if b == nil {
t.Fatalf("resource %d missing from processed map", i)
}
if !a.Equals(b) {
t.Errorf("expected %v got %v", a, b)
}
if a.NeedHashSuffix() != b.NeedHashSuffix() {
t.Errorf("")
}
if a.Behavior() != b.Behavior() {
t.Errorf("expected %v got %v", a.Behavior(), b.Behavior())
}
}
}
func TestUpdateResourceOptionsWithInvalidHashAnnotationValues(t *testing.T) {
p := NewExecPlugin("")
rf := resource.NewFactory(kunstruct.NewKunstructuredFactoryImpl())
cases := []string{
"",
"FaLsE",
"TrUe",
"potato",
}
for i, c := range cases {
name := fmt.Sprintf("test%d", i)
in := resmap.New()
in.Append(makeConfigMap(rf, name, "", &c))
_, err := p.UpdateResourceOptions(in)
if err == nil {
t.Errorf("expected error from value %q", c)
}
}
}

View File

@@ -0,0 +1,199 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package loader
import (
"fmt"
"path/filepath"
"plugin"
"reflect"
"strings"
"sigs.k8s.io/kustomize/v3/api/plugins/builtins"
"github.com/pkg/errors"
"sigs.k8s.io/kustomize/v3/api/ifc"
"sigs.k8s.io/kustomize/v3/api/plugins/config"
"sigs.k8s.io/kustomize/v3/api/plugins/execplugin"
"sigs.k8s.io/kustomize/v3/api/resid"
"sigs.k8s.io/kustomize/v3/api/resmap"
"sigs.k8s.io/kustomize/v3/api/resource"
"sigs.k8s.io/kustomize/v3/api/types"
"sigs.k8s.io/kustomize/v3/pkg/plugins"
)
type Loader struct {
pc *types.PluginConfig
rf *resmap.Factory
}
func NewLoader(
pc *types.PluginConfig, rf *resmap.Factory) *Loader {
return &Loader{pc: pc, rf: rf}
}
func (l *Loader) LoadGenerators(
ldr ifc.Loader, v ifc.Validator, rm resmap.ResMap) ([]resmap.Generator, error) {
var result []resmap.Generator
for _, res := range rm.Resources() {
g, err := l.LoadGenerator(ldr, v, res)
if err != nil {
return nil, err
}
result = append(result, g)
}
return result, nil
}
func (l *Loader) LoadGenerator(
ldr ifc.Loader, v ifc.Validator, res *resource.Resource) (resmap.Generator, error) {
c, err := l.loadAndConfigurePlugin(ldr, v, res)
if err != nil {
return nil, err
}
g, ok := c.(resmap.Generator)
if !ok {
return nil, fmt.Errorf("plugin %s not a generator", res.OrgId())
}
return g, nil
}
func (l *Loader) LoadTransformers(
ldr ifc.Loader, v ifc.Validator, rm resmap.ResMap) ([]resmap.Transformer, error) {
var result []resmap.Transformer
for _, res := range rm.Resources() {
t, err := l.LoadTransformer(ldr, v, res)
if err != nil {
return nil, err
}
result = append(result, t)
}
return result, nil
}
func (l *Loader) LoadTransformer(
ldr ifc.Loader, v ifc.Validator, res *resource.Resource) (resmap.Transformer, error) {
c, err := l.loadAndConfigurePlugin(ldr, v, res)
if err != nil {
return nil, err
}
t, ok := c.(resmap.Transformer)
if !ok {
return nil, fmt.Errorf("plugin %s not a transformer", res.OrgId())
}
return t, nil
}
func relativePluginPath(id resid.ResId) string {
return filepath.Join(
id.Group,
id.Version,
strings.ToLower(id.Kind))
}
func AbsolutePluginPath(pc *types.PluginConfig, id resid.ResId) string {
return filepath.Join(
pc.DirectoryPath, relativePluginPath(id), id.Kind)
}
func (l *Loader) absolutePluginPath(id resid.ResId) string {
return AbsolutePluginPath(l.pc, id)
}
func isBuiltinPlugin(res *resource.Resource) bool {
// TODO: the special string should appear in Group, not Version.
return res.GetGvk().Group == "" &&
res.GetGvk().Version == config.BuiltinPluginApiVersion
}
func (l *Loader) loadAndConfigurePlugin(
ldr ifc.Loader, v ifc.Validator, res *resource.Resource) (c resmap.Configurable, err error) {
if isBuiltinPlugin(res) {
// Instead of looking for and loading a .so file, just
// instantiate the plugin from a generated factory
// function (see "pluginator"). Being able to do this
// is what makes a plugin "builtin".
c, err = l.makeBuiltinPlugin(res.GetGvk())
} else if l.pc.Enabled {
c, err = l.loadPlugin(res.OrgId())
} else {
err = config.NotEnabledErr(res.OrgId().Kind)
}
if err != nil {
return nil, err
}
yaml, err := res.AsYAML()
if err != nil {
return nil, errors.Wrapf(err, "marshalling yaml from res %s", res.OrgId())
}
err = c.Config(resmap.NewPluginHelpers(ldr, v, l.rf), yaml)
if err != nil {
return nil, errors.Wrapf(
err, "plugin %s fails configuration", res.OrgId())
}
return c, nil
}
func (l *Loader) makeBuiltinPlugin(r resid.Gvk) (resmap.Configurable, error) {
bpt := builtins.GetBuiltinPluginType(r.Kind)
if f, ok := builtins.GeneratorFactories[bpt]; ok {
return f(), nil
}
if f, ok := builtins.TransformerFactories[bpt]; ok {
return f(), nil
}
return nil, errors.Errorf("unable to load builtin %s", r)
}
func (l *Loader) loadPlugin(resId resid.ResId) (resmap.Configurable, error) {
p := execplugin.NewExecPlugin(l.absolutePluginPath(resId))
if p.IsAvailable() {
return p, nil
}
c, err := l.loadGoPlugin(resId)
if err != nil {
return nil, err
}
return c, nil
}
// registry is a means to avoid trying to load the same .so file
// into memory more than once, which results in an error.
// Each test makes its own loader, and tries to load its own plugins,
// but the loaded .so files are in shared memory, so one will get
// "this plugin already loaded" errors if the registry is maintained
// as a Loader instance variable. So make it a package variable.
var registry = make(map[string]resmap.Configurable)
func (l *Loader) loadGoPlugin(id resid.ResId) (resmap.Configurable, error) {
regId := relativePluginPath(id)
if c, ok := registry[regId]; ok {
return copyPlugin(c), nil
}
absPath := l.absolutePluginPath(id)
p, err := plugin.Open(absPath + ".so")
if err != nil {
return nil, errors.Wrapf(err, "plugin %s fails to load", absPath)
}
symbol, err := p.Lookup(plugins.PluginSymbol)
if err != nil {
return nil, errors.Wrapf(
err, "plugin %s doesn't have symbol %s",
regId, plugins.PluginSymbol)
}
c, ok := symbol.(resmap.Configurable)
if !ok {
return nil, fmt.Errorf("plugin %s not configurable", regId)
}
registry[regId] = c
return copyPlugin(c), nil
}
func copyPlugin(c resmap.Configurable) resmap.Configurable {
indirect := reflect.Indirect(reflect.ValueOf(c))
newIndirect := reflect.New(indirect.Type())
newIndirect.Elem().Set(reflect.ValueOf(indirect.Interface()))
newNamed := newIndirect.Interface()
return newNamed.(resmap.Configurable)
}

View File

@@ -0,0 +1,75 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package loader_test
import (
"testing"
"sigs.k8s.io/kustomize/v3/api/plugins/config"
. "sigs.k8s.io/kustomize/v3/api/plugins/loader"
"sigs.k8s.io/kustomize/v3/api/resmap"
"sigs.k8s.io/kustomize/v3/api/resource"
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
"sigs.k8s.io/kustomize/v3/api/testutils/valtest"
"sigs.k8s.io/kustomize/v3/internal/loadertest"
"sigs.k8s.io/kustomize/v3/k8sdeps/kunstruct"
)
const (
secretGenerator = `
apiVersion: builtin
kind: SecretGenerator
metadata:
name: secretGenerator
name: mySecret
behavior: merge
envFiles:
- a.env
- b.env
valueFiles:
- longsecret.txt
literals:
- FRUIT=apple
- VEGETABLE=carrot
`
someServiceGenerator = `
apiVersion: someteam.example.com/v1
kind: SomeServiceGenerator
metadata:
name: myServiceGenerator
service: my-service
port: "12345"
`
)
func TestLoader(t *testing.T) {
tc := kusttest_test.NewPluginTestEnv(t).Set()
defer tc.Reset()
tc.BuildGoPlugin(
"builtin", "", "SecretGenerator")
tc.BuildGoPlugin(
"someteam.example.com", "v1", "SomeServiceGenerator")
rmF := resmap.NewFactory(resource.NewFactory(
kunstruct.NewKunstructuredFactoryImpl()), nil)
ldr := loadertest.NewFakeLoader("/foo")
pLdr := NewLoader(config.ActivePluginConfig(), rmF)
if pLdr == nil {
t.Fatal("expect non-nil loader")
}
m, err := rmF.NewResMapFromBytes([]byte(
someServiceGenerator + "---\n" + secretGenerator))
if err != nil {
t.Fatal(err)
}
_, err = pLdr.LoadGenerators(ldr, valtest_test.MakeFakeValidator(), m)
if err != nil {
t.Fatal(err)
}
}

View File

@@ -6,9 +6,10 @@ package resmap_test
import (
"encoding/base64"
"reflect"
"sigs.k8s.io/kustomize/v3/api/resid"
"testing"
"sigs.k8s.io/kustomize/v3/api/resid"
"sigs.k8s.io/kustomize/v3/api/filesys"
"sigs.k8s.io/kustomize/v3/api/ifc"
"sigs.k8s.io/kustomize/v3/api/kv"

View File

@@ -0,0 +1,313 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package target_test
import (
"testing"
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
)
func writeMediumBase(th *kusttest_test.KustTestHarness) {
th.WriteK("/app/base", `
namePrefix: baseprefix-
commonLabels:
foo: bar
commonAnnotations:
baseAnno: This is a base annotation
resources:
- deployment/deployment.yaml
- service/service.yaml
`)
th.WriteF("/app/base/service/service.yaml", `
apiVersion: v1
kind: Service
metadata:
name: mungebot-service
labels:
app: mungebot
spec:
ports:
- port: 7002
selector:
app: mungebot
`)
th.WriteF("/app/base/deployment/deployment.yaml", `
apiVersion: apps/v1
kind: Deployment
metadata:
name: mungebot
labels:
app: mungebot
spec:
replicas: 1
template:
metadata:
labels:
app: mungebot
spec:
containers:
- name: nginx
image: nginx
env:
- name: foo
value: bar
ports:
- containerPort: 80
`)
}
func TestMediumBase(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/base")
writeMediumBase(th)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
baseAnno: This is a base annotation
labels:
app: mungebot
foo: bar
name: baseprefix-mungebot
spec:
replicas: 1
selector:
matchLabels:
foo: bar
template:
metadata:
annotations:
baseAnno: This is a base annotation
labels:
app: mungebot
foo: bar
spec:
containers:
- env:
- name: foo
value: bar
image: nginx
name: nginx
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
annotations:
baseAnno: This is a base annotation
labels:
app: mungebot
foo: bar
name: baseprefix-mungebot-service
spec:
ports:
- port: 7002
selector:
app: mungebot
foo: bar
`)
}
func TestMediumOverlay(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/overlay")
writeMediumBase(th)
th.WriteK("/app/overlay", `
namePrefix: test-infra-
commonLabels:
app: mungebot
org: kubernetes
repo: test-infra
commonAnnotations:
note: This is a test annotation
resources:
- ../base
patchesStrategicMerge:
- deployment/deployment.yaml
configMapGenerator:
- name: app-env
envs:
- configmap/db.env
- configmap/units.ini
- configmap/food.ini
- name: app-config
files:
- nonsense=configmap/dummy.txt
images:
- name: nginx
newTag: 1.8.0`)
th.WriteF("/app/overlay/configmap/db.env", `
DB_USERNAME=admin
DB_PASSWORD=somepw
`)
th.WriteF("/app/overlay/configmap/units.ini", `
LENGTH=kilometer
ENERGY=electronvolt
`)
th.WriteF("/app/overlay/configmap/food.ini", `
FRUIT=banana
LEGUME=chickpea
`)
th.WriteF("/app/overlay/configmap/dummy.txt",
`Lorem ipsum dolor sit amet, consectetur
adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua.
`)
th.WriteF("/app/overlay/deployment/deployment.yaml", `
apiVersion: apps/v1
kind: Deployment
metadata:
name: mungebot
spec:
replicas: 2
template:
spec:
containers:
- name: nginx
image: nginx:1.7.9
env:
- name: FOO
valueFrom:
configMapKeyRef:
name: app-env
key: somekey
- name: busybox
image: busybox
envFrom:
- configMapRef:
name: someConfigMap
- configMapRef:
name: app-env
volumeMounts:
- mountPath: /tmp/env
name: app-env
volumes:
- configMap:
name: app-env
name: app-env
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
baseAnno: This is a base annotation
note: This is a test annotation
labels:
app: mungebot
foo: bar
org: kubernetes
repo: test-infra
name: test-infra-baseprefix-mungebot
spec:
replicas: 2
selector:
matchLabels:
app: mungebot
foo: bar
org: kubernetes
repo: test-infra
template:
metadata:
annotations:
baseAnno: This is a base annotation
note: This is a test annotation
labels:
app: mungebot
foo: bar
org: kubernetes
repo: test-infra
spec:
containers:
- env:
- name: FOO
valueFrom:
configMapKeyRef:
key: somekey
name: test-infra-app-env-ffmd9b969m
- name: foo
value: bar
image: nginx:1.8.0
name: nginx
ports:
- containerPort: 80
- envFrom:
- configMapRef:
name: someConfigMap
- configMapRef:
name: test-infra-app-env-ffmd9b969m
image: busybox
name: busybox
volumeMounts:
- mountPath: /tmp/env
name: app-env
volumes:
- configMap:
name: test-infra-app-env-ffmd9b969m
name: app-env
---
apiVersion: v1
kind: Service
metadata:
annotations:
baseAnno: This is a base annotation
note: This is a test annotation
labels:
app: mungebot
foo: bar
org: kubernetes
repo: test-infra
name: test-infra-baseprefix-mungebot-service
spec:
ports:
- port: 7002
selector:
app: mungebot
foo: bar
org: kubernetes
repo: test-infra
---
apiVersion: v1
data:
DB_PASSWORD: somepw
DB_USERNAME: admin
ENERGY: electronvolt
FRUIT: banana
LEGUME: chickpea
LENGTH: kilometer
kind: ConfigMap
metadata:
annotations:
note: This is a test annotation
labels:
app: mungebot
org: kubernetes
repo: test-infra
name: test-infra-app-env-ffmd9b969m
---
apiVersion: v1
data:
nonsense: "Lorem ipsum dolor sit amet, consectetur\nadipiscing elit, sed do eiusmod
tempor\nincididunt ut labore et dolore magna aliqua. \n"
kind: ConfigMap
metadata:
annotations:
note: This is a test annotation
labels:
app: mungebot
org: kubernetes
repo: test-infra
name: test-infra-app-config-f462h769f9
`)
}

View File

@@ -0,0 +1,456 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package target_test
import (
"strings"
"testing"
"sigs.k8s.io/kustomize/v3/api/loader"
"sigs.k8s.io/kustomize/v3/api/plugins/config"
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
)
func TestOrderPreserved(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/prod")
th.WriteK("/app/base", `
namePrefix: b-
resources:
- namespace.yaml
- role.yaml
- service.yaml
- deployment.yaml
`)
th.WriteF("/app/base/service.yaml", `
apiVersion: v1
kind: Service
metadata:
name: myService
`)
th.WriteF("/app/base/namespace.yaml", `
apiVersion: v1
kind: Namespace
metadata:
name: myNs
`)
th.WriteF("/app/base/role.yaml", `
apiVersion: v1
kind: Role
metadata:
name: myRole
`)
th.WriteF("/app/base/deployment.yaml", `
apiVersion: v1
kind: Deployment
metadata:
name: myDep
`)
th.WriteK("/app/prod", `
namePrefix: p-
resources:
- ../base
- service.yaml
- namespace.yaml
`)
th.WriteF("/app/prod/service.yaml", `
apiVersion: v1
kind: Service
metadata:
name: myService2
`)
th.WriteF("/app/prod/namespace.yaml", `
apiVersion: v1
kind: Namespace
metadata:
name: myNs2
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: v1
kind: Namespace
metadata:
name: p-b-myNs
---
apiVersion: v1
kind: Role
metadata:
name: p-b-myRole
---
apiVersion: v1
kind: Service
metadata:
name: p-b-myService
---
apiVersion: v1
kind: Deployment
metadata:
name: p-b-myDep
---
apiVersion: v1
kind: Service
metadata:
name: p-myService2
---
apiVersion: v1
kind: Namespace
metadata:
name: p-myNs2
`)
}
func TestBaseInResourceList(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/prod")
th.WriteK("/app/prod", `
namePrefix: b-
resources:
- ../base
`)
th.WriteK("/app/base", `
namePrefix: a-
resources:
- service.yaml
`)
th.WriteF("/app/base/service.yaml", `
apiVersion: v1
kind: Service
metadata:
name: myService
spec:
selector:
backend: bungie
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: v1
kind: Service
metadata:
name: b-a-myService
spec:
selector:
backend: bungie
`)
}
func writeSmallBase(th *kusttest_test.KustTestHarness) {
th.WriteK("/app/base", `
namePrefix: a-
commonLabels:
app: myApp
resources:
- deployment.yaml
- service.yaml
`)
th.WriteF("/app/base/service.yaml", `
apiVersion: v1
kind: Service
metadata:
name: myService
spec:
selector:
backend: bungie
ports:
- port: 7002
`)
th.WriteF("/app/base/deployment.yaml", `
apiVersion: apps/v1
kind: Deployment
metadata:
name: myDeployment
spec:
template:
metadata:
labels:
backend: awesome
spec:
containers:
- name: whatever
image: whatever
`)
}
func TestSmallBase(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/base")
writeSmallBase(th)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: myApp
name: a-myDeployment
spec:
selector:
matchLabels:
app: myApp
template:
metadata:
labels:
app: myApp
backend: awesome
spec:
containers:
- image: whatever
name: whatever
---
apiVersion: v1
kind: Service
metadata:
labels:
app: myApp
name: a-myService
spec:
ports:
- port: 7002
selector:
app: myApp
backend: bungie
`)
}
func TestSmallOverlay(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/overlay")
writeSmallBase(th)
th.WriteK("/app/overlay", `
namePrefix: b-
commonLabels:
env: prod
resources:
- ../base
patchesStrategicMerge:
- deployment/deployment.yaml
images:
- name: whatever
newTag: 1.8.0
`)
th.WriteF("/app/overlay/configmap/app.env", `
DB_USERNAME=admin
DB_PASSWORD=somepw
`)
th.WriteF("/app/overlay/configmap/app-init.ini", `
FOO=bar
BAR=baz
`)
th.WriteF("/app/overlay/deployment/deployment.yaml", `
apiVersion: apps/v1
kind: Deployment
metadata:
name: myDeployment
spec:
replicas: 1000
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: myApp
env: prod
name: b-a-myDeployment
spec:
replicas: 1000
selector:
matchLabels:
app: myApp
env: prod
template:
metadata:
labels:
app: myApp
backend: awesome
env: prod
spec:
containers:
- image: whatever:1.8.0
name: whatever
---
apiVersion: v1
kind: Service
metadata:
labels:
app: myApp
env: prod
name: b-a-myService
spec:
ports:
- port: 7002
selector:
app: myApp
backend: bungie
env: prod
`)
}
func TestSharedPatchDisAllowed(t *testing.T) {
th := kusttest_test.NewKustTestHarnessFull(
t, "/app/overlay",
loader.RestrictionRootOnly, config.DefaultPluginConfig())
writeSmallBase(th)
th.WriteK("/app/overlay", `
commonLabels:
env: prod
resources:
- ../base
patchesStrategicMerge:
- ../shared/deployment-patch.yaml
`)
th.WriteF("/app/shared/deployment-patch.yaml", `
apiVersion: apps/v1
kind: Deployment
metadata:
name: myDeployment
spec:
replicas: 1000
`)
_, err := th.MakeKustTarget().MakeCustomizedResMap()
if err == nil {
t.Fatalf("expected error")
}
if !strings.Contains(
err.Error(),
"security; file '/app/shared/deployment-patch.yaml' is not in or below '/app/overlay'") {
t.Fatalf("unexpected error: %s", err)
}
}
func TestSharedPatchAllowed(t *testing.T) {
th := kusttest_test.NewKustTestHarnessFull(
t, "/app/overlay",
loader.RestrictionNone, config.DefaultPluginConfig())
writeSmallBase(th)
th.WriteK("/app/overlay", `
commonLabels:
env: prod
resources:
- ../base
patchesStrategicMerge:
- ../shared/deployment-patch.yaml
`)
th.WriteF("/app/shared/deployment-patch.yaml", `
apiVersion: apps/v1
kind: Deployment
metadata:
name: myDeployment
spec:
replicas: 1000
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: myApp
env: prod
name: a-myDeployment
spec:
replicas: 1000
selector:
matchLabels:
app: myApp
env: prod
template:
metadata:
labels:
app: myApp
backend: awesome
env: prod
spec:
containers:
- image: whatever
name: whatever
---
apiVersion: v1
kind: Service
metadata:
labels:
app: myApp
env: prod
name: a-myService
spec:
ports:
- port: 7002
selector:
app: myApp
backend: bungie
env: prod
`)
}
func TestSmallOverlayJSONPatch(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/overlay")
writeSmallBase(th)
th.WriteK("/app/overlay", `
resources:
- ../base
patchesJson6902:
- target:
version: v1
kind: Service
name: a-myService
path: service-patch.yaml
`)
th.WriteF("/app/overlay/service-patch.yaml", `
- op: add
path: /spec/selector/backend
value: beagle
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: myApp
name: a-myDeployment
spec:
selector:
matchLabels:
app: myApp
template:
metadata:
labels:
app: myApp
backend: awesome
spec:
containers:
- image: whatever
name: whatever
---
apiVersion: v1
kind: Service
metadata:
labels:
app: myApp
name: a-myService
spec:
ports:
- port: 7002
selector:
app: myApp
backend: beagle
`)
}

View File

@@ -0,0 +1,199 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package target_test
import (
"testing"
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
)
// Here is a structure of a kustomization of two components, component1
// and component2, that both use a shared postgres definition, which
// they would individually adjust. This test case checks that the name
// prefix does not cause a name reference conflict.
//
// root
// / \
// component1/overlay component2/overlay
// | |
// component1/base component2/base
// \ /
// base
//
// This is the directory layout:
//
// ├── component1
// │ ├── base
// │ │ └── kustomization.yaml
// │ └── overlay
// │ └── kustomization.yaml
// ├── component2
// │ ├── base
// │ │ └── kustomization.yaml
// │ └── overlay
// │ └── kustomization.yaml
// ├── shared
// │ ├── kustomization.yaml
// │ └── resources.yaml
// ├── kustomization.yaml
func TestBaseReuseNameConflict(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app")
th.WriteK("/app/component1/base", `
resources:
- ../../shared
namePrefix: component1-
`)
th.WriteK("/app/component1/overlay", `
resources:
- ../base
namePrefix: overlay-
`)
th.WriteK("/app/component2/base", `
resources:
- ../../shared
namePrefix: component2-
`)
th.WriteK("/app/component2/overlay", `
resources:
- ../base
namePrefix: overlay-
`)
th.WriteK("/app/shared", `
resources:
- resources.yaml
`)
th.WriteF("/app/shared/resources.yaml", `
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: postgres
spec:
resources:
requests:
storage: 1Gi
accessModes:
- ReadWriteOnce
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
spec:
selector:
matchLabels: {}
strategy:
type: Recreate
template:
spec:
containers:
- name: postgres
image: postgres
imagePullPolicy: IfNotPresent
volumeMounts:
- mountPath: /var/lib/postgresql
name: data
ports:
- name: postgres
containerPort: 5432
volumes:
- name: data
persistentVolumeClaim:
claimName: postgres
`)
th.WriteK("/app", `
resources:
- component1/overlay
- component2/overlay
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: overlay-component1-postgres
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: overlay-component1-postgres
spec:
selector:
matchLabels: {}
strategy:
type: Recreate
template:
spec:
containers:
- image: postgres
imagePullPolicy: IfNotPresent
name: postgres
ports:
- containerPort: 5432
name: postgres
volumeMounts:
- mountPath: /var/lib/postgresql
name: data
volumes:
- name: data
persistentVolumeClaim:
claimName: overlay-component1-postgres
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: overlay-component2-postgres
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: overlay-component2-postgres
spec:
selector:
matchLabels: {}
strategy:
type: Recreate
template:
spec:
containers:
- image: postgres
imagePullPolicy: IfNotPresent
name: postgres
ports:
- containerPort: 5432
name: postgres
volumeMounts:
- mountPath: /var/lib/postgresql
name: data
volumes:
- name: data
persistentVolumeClaim:
claimName: overlay-component2-postgres
`)
}

View File

@@ -0,0 +1,111 @@
// +build notravis
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Disabled on travis, because don't want to install helm on travis.
package target_test
import (
"regexp"
"testing"
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
)
// This is an example of using a helm chart as a base,
// inflating it and then customizing it with a nameprefix
// applied to all its resources.
//
// The helm chart used is downloaded from
// https://github.com/helm/charts/tree/master/stable/minecraft
// with each test run, so it's a bit brittle as that
// chart could change obviously and break the test.
//
// This test requires having the helm binary on the PATH.
//
// TODO: Download and inflate the chart, and check that
// in for the test.
func TestChartInflatorPlugin(t *testing.T) {
tc := kusttest_test.NewPluginTestEnv(t).Set()
defer tc.Reset()
tc.BuildExecPlugin(
"someteam.example.com", "v1", "ChartInflator")
th := kusttest_test.NewKustTestHarnessAllowPlugins(t, "/app")
th.WriteK("/app", `
generators:
- chartInflator.yaml
namePrefix: LOOOOOOOONG-
`)
th.WriteF("/app/chartInflator.yaml", `
apiVersion: someteam.example.com/v1
kind: ChartInflator
metadata:
name: notImportantHere
chartName: minecraft
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
chartName := regexp.MustCompile("chart: minecraft-[0-9.]+")
th.AssertActualEqualsExpectedWithTweak(m,
func(x []byte) []byte {
return chartName.ReplaceAll(x, []byte("chart: minecraft-SOMEVERSION"))
}, `
apiVersion: v1
data:
rcon-password: Q0hBTkdFTUUh
kind: Secret
metadata:
labels:
app: release-name-minecraft
chart: minecraft-SOMEVERSION
heritage: Tiller
release: release-name
name: LOOOOOOOONG-release-name-minecraft
type: Opaque
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
annotations:
volume.alpha.kubernetes.io/storage-class: default
labels:
app: release-name-minecraft
chart: minecraft-SOMEVERSION
heritage: Tiller
release: release-name
name: LOOOOOOOONG-release-name-minecraft-datadir
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Service
metadata:
labels:
app: release-name-minecraft
chart: minecraft-SOMEVERSION
heritage: Tiller
release: release-name
name: LOOOOOOOONG-release-name-minecraft
spec:
ports:
- name: minecraft
port: 25565
protocol: TCP
targetPort: minecraft
selector:
app: release-name-minecraft
type: LoadBalancer
`)
}

View File

@@ -0,0 +1,557 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package target_test
import (
"strings"
"testing"
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
)
const httpsService = `
apiVersion: v1
kind: Service
metadata:
name: my-https-svc
spec:
ports:
- port: 443
protocol: TCP
name: https
selector:
app: my-app
`
func writeStatefulSetBase(th *kusttest_test.KustTestHarness) {
th.WriteK("/app/base", `
resources:
- statefulset.yaml
`)
th.WriteF("/app/base/statefulset.yaml", `
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: my-sts
spec:
serviceName: my-svc
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: app
image: my-image
volumeClaimTemplates:
- spec:
storageClassName: default
`)
}
func writeHTTPSOverlay(th *kusttest_test.KustTestHarness) {
th.WriteK("/app/https", `
resources:
- ../base
- https-svc.yaml
patchesStrategicMerge:
- sts-patch.yaml
`)
th.WriteF("/app/https/https-svc.yaml", httpsService)
th.WriteF("/app/https/sts-patch.yaml", `
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: my-sts
spec:
serviceName: my-https-svc
`)
}
func writeHTTPSTransformerRaw(th *kusttest_test.KustTestHarness) {
th.WriteF("/app/https/service/https-svc.yaml", httpsService)
th.WriteF("/app/https/transformer/transformer.yaml", `
apiVersion: builtin
kind: PatchTransformer
metadata:
name: svcNameTran
target:
group: apps
version: v1
kind: StatefulSet
name: my-sts
patch: |-
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: my-sts
spec:
serviceName: my-https-svc
`)
}
func writeHTTPSTransformerBase(th *kusttest_test.KustTestHarness) {
th.WriteK("/app/https/service", `
resources:
- https-svc.yaml
`)
th.WriteK("/app/https/transformer", `
resources:
- transformer.yaml
`)
writeHTTPSTransformerRaw(th)
}
func writeConfigFromEnvOverlay(th *kusttest_test.KustTestHarness) {
th.WriteK("/app/config", `
resources:
- ../base
configMapGenerator:
- name: my-config
literals:
- MY_ENV=foo
generatorOptions:
disableNameSuffixHash: true
patchesStrategicMerge:
- sts-patch.yaml
`)
th.WriteF("/app/config/sts-patch.yaml", `
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: my-sts
spec:
template:
spec:
containers:
- name: app
envFrom:
- configMapRef:
name: my-config
`)
}
func writeConfigFromEnvTransformerRaw(th *kusttest_test.KustTestHarness) {
th.WriteF("/app/config/map/generator.yaml", `
apiVersion: builtin
kind: ConfigMapGenerator
metadata:
name: my-config
disableNameSuffixHash: true
literals:
- MY_ENV=foo
`)
th.WriteF("/app/config/transformer/transformer.yaml", `
apiVersion: builtin
kind: PatchTransformer
metadata:
name: envFromConfigTrans
target:
group: apps
version: v1
kind: StatefulSet
name: my-sts
patch: |-
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: my-sts
spec:
template:
spec:
containers:
- name: app
envFrom:
- configMapRef:
name: my-config
`)
}
func writeConfigFromEnvTransformerBase(th *kusttest_test.KustTestHarness) {
th.WriteK("/app/config/map", `
resources:
- generator.yaml
`)
th.WriteK("/app/config/transformer", `
resources:
- transformer.yaml
`)
writeConfigFromEnvTransformerRaw(th)
}
func writeTolerationsOverlay(th *kusttest_test.KustTestHarness) {
th.WriteK("/app/tolerations", `
resources:
- ../base
patchesStrategicMerge:
- sts-patch.yaml
`)
th.WriteF("/app/tolerations/sts-patch.yaml", `
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: my-sts
spec:
template:
spec:
tolerations:
- effect: NoExecute
key: node.kubernetes.io/not-ready
tolerationSeconds: 30
`)
}
func writeTolerationsTransformerRaw(th *kusttest_test.KustTestHarness) {
th.WriteF("/app/tolerations/transformer.yaml", `
apiVersion: builtin
kind: PatchTransformer
metadata:
name: tolTrans
target:
group: apps
version: v1
kind: StatefulSet
name: my-sts
patch: |-
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: my-sts
spec:
template:
spec:
tolerations:
- effect: NoExecute
key: node.kubernetes.io/not-ready
tolerationSeconds: 30
`)
}
func writeTolerationsTransformerBase(th *kusttest_test.KustTestHarness) {
th.WriteK("/app/tolerations", `
resources:
- transformer.yaml
`)
writeTolerationsTransformerRaw(th)
}
func writeStorageOverlay(th *kusttest_test.KustTestHarness) {
th.WriteK("/app/storage", `
resources:
- ../base
patchesJson6902:
- target:
group: apps
version: v1
kind: StatefulSet
name: my-sts
path: sts-patch.json
`)
th.WriteF("/app/storage/sts-patch.json", `
[{"op": "replace", "path": "/spec/volumeClaimTemplates/0/spec/storageClassName", "value": "my-sc"}]
`)
}
func writeStorageTransformerRaw(th *kusttest_test.KustTestHarness) {
th.WriteF("/app/storage/transformer.yaml", `
apiVersion: builtin
kind: PatchTransformer
metadata:
name: storageTrans
target:
group: apps
version: v1
kind: StatefulSet
name: my-sts
patch: |-
[{"op": "replace", "path": "/spec/volumeClaimTemplates/0/spec/storageClassName", "value": "my-sc"}]
`)
}
func writeStorageTransformerBase(th *kusttest_test.KustTestHarness) {
th.WriteK("/app/storage", `
resources:
- transformer.yaml
`)
writeStorageTransformerRaw(th)
}
func writePatchingOverlays(th *kusttest_test.KustTestHarness) {
writeStorageOverlay(th)
writeConfigFromEnvOverlay(th)
writeTolerationsOverlay(th)
writeHTTPSOverlay(th)
}
func writePatchingTransformersRaw(th *kusttest_test.KustTestHarness) {
writeStorageTransformerRaw(th)
writeConfigFromEnvTransformerRaw(th)
writeTolerationsTransformerRaw(th)
writeHTTPSTransformerRaw(th)
}
// Similar to writePatchingTransformersRaw, except here the
// transformers and related artifacts are addressable as _bases_.
// They are listed in a kustomization file, and consumers of
// the plugin refer to the kustomization instead of to the local
// file in the "transformers:" field.
//
// Using bases makes the set of files relocatable with
// respect to the overlays, and avoids the need to relax load
// restrictions on file paths reaching outside the `dev` and
// `prod` kustomization roots. I.e. with bases tests can use
// NewKustTestHarness instead of NewKustTestHarnessNoLoadRestrictor.
//
// Using transformer plugins from _bases_ means the plugin config
// must be self-contained, i.e. the config may not have fields that
// refer to local files, since those files won't be present when
// the plugin is instantiated and used.
func writePatchingTransformerBases(th *kusttest_test.KustTestHarness) {
writeStorageTransformerBase(th)
writeConfigFromEnvTransformerBase(th)
writeTolerationsTransformerBase(th)
writeHTTPSTransformerBase(th)
}
// Here's a complex kustomization scenario that combines multiple overlays
// on a common base:
//
// dev prod
// | |
// | |
// + ------- + + ------------ + ------------- +
// | | | | |
// | | | | |
// v | v v v
// storage + -----> config tolerations https
// | | | |
// | | | |
// | + --- + + --- + |
// | | | |
// | v v |
// + -----------------------> base <------------------ +
//
// The base resource is a statefulset. Each intermediate overlay manages or
// generates new resources and patches different aspects of the same base
// resource, without using any of the `namePrefix`, `nameSuffix` or `namespace`
// kustomization keywords.
//
// Intermediate overlays:
// - storage: Changes the storage class of the stateful set with a JSON patch.
// - config: Generates a config map and adds a field as an environment
// variable.
// - tolerations: Adds a new tolerations field in the spec.
// - https: Adds a new service resource and changes the service name in the
// stateful set.
//
// Top overlays:
// - dev: Combines the storage and config intermediate overlays.
// - prod: Combines the config, tolerations and https intermediate overlays.
func TestComplexComposition_Dev_Failure(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/dev")
writeStatefulSetBase(th)
writePatchingOverlays(th)
th.WriteK("/app/dev", `
resources:
- ../storage
- ../config
`)
_, err := th.MakeKustTarget().MakeCustomizedResMap()
if err == nil {
t.Fatalf("Expected resource accumulation error")
}
if !strings.Contains(
err.Error(), "already registered id: apps_v1_StatefulSet|~X|my-sts") {
t.Fatalf("Unexpected err: %v", err)
}
}
const devDesiredResult = `
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: my-sts
spec:
selector:
matchLabels:
app: my-app
serviceName: my-svc
template:
metadata:
labels:
app: my-app
spec:
containers:
- envFrom:
- configMapRef:
name: my-config
image: my-image
name: app
volumeClaimTemplates:
- spec:
storageClassName: my-sc
---
apiVersion: v1
data:
MY_ENV: foo
kind: ConfigMap
metadata:
name: my-config
`
func TestComplexComposition_Dev_SuccessWithRawTransformers(t *testing.T) {
th := kusttest_test.NewKustTestHarnessNoLoadRestrictor(t, "/app/dev")
writeStatefulSetBase(th)
writePatchingTransformersRaw(th)
th.WriteK("/app/dev", `
resources:
- ../base
generators:
- ../config/map/generator.yaml
transformers:
- ../config/transformer/transformer.yaml
- ../storage/transformer.yaml
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Unexpected err: %v", err)
}
th.AssertActualEqualsExpected(m, devDesiredResult)
}
func TestComplexComposition_Dev_SuccessWithBaseTransformers(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/dev")
writeStatefulSetBase(th)
writePatchingTransformerBases(th)
th.WriteK("/app/dev", `
resources:
- ../base
generators:
- ../config/map
transformers:
- ../config/transformer
- ../storage
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Unexpected err: %v", err)
}
th.AssertActualEqualsExpected(m, devDesiredResult)
}
func TestComplexComposition_Prod_Failure(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/prod")
writeStatefulSetBase(th)
writePatchingOverlays(th)
th.WriteK("/app/prod", `
resources:
- ../config
- ../tolerations
- ../https
`)
_, err := th.MakeKustTarget().MakeCustomizedResMap()
if err == nil {
t.Fatalf("Expected resource accumulation error")
}
if !strings.Contains(
err.Error(), "already registered id: apps_v1_StatefulSet|~X|my-sts") {
t.Fatalf("Unexpected err: %v", err)
}
}
const prodDesiredResult = `
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: my-sts
spec:
selector:
matchLabels:
app: my-app
serviceName: my-https-svc
template:
metadata:
labels:
app: my-app
spec:
containers:
- envFrom:
- configMapRef:
name: my-config
image: my-image
name: app
tolerations:
- effect: NoExecute
key: node.kubernetes.io/not-ready
tolerationSeconds: 30
volumeClaimTemplates:
- spec:
storageClassName: default
---
apiVersion: v1
kind: Service
metadata:
name: my-https-svc
spec:
ports:
- name: https
port: 443
protocol: TCP
selector:
app: my-app
---
apiVersion: v1
data:
MY_ENV: foo
kind: ConfigMap
metadata:
name: my-config
`
func TestComplexComposition_Prod_SuccessWithRawTransformers(t *testing.T) {
th := kusttest_test.NewKustTestHarnessNoLoadRestrictor(t, "/app/prod")
writeStatefulSetBase(th)
writePatchingTransformersRaw(th)
th.WriteK("/app/prod", `
resources:
- ../base
- ../https/service/https-svc.yaml
generators:
- ../config/map/generator.yaml
transformers:
- ../config/transformer/transformer.yaml
- ../https/transformer/transformer.yaml
- ../tolerations/transformer.yaml
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Unexpected err: %v", err)
}
th.AssertActualEqualsExpected(m, prodDesiredResult)
}
func TestComplexComposition_Prod_SuccessWithBaseTransformers(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/prod")
writeStatefulSetBase(th)
writePatchingTransformerBases(th)
th.WriteK("/app/prod", `
resources:
- ../base
- ../https/service
generators:
- ../config/map
transformers:
- ../config/transformer
- ../https/transformer
- ../tolerations
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Unexpected err: %v", err)
}
th.AssertActualEqualsExpected(m, prodDesiredResult)
}

View File

@@ -0,0 +1,313 @@
/*
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 target_test
import (
"testing"
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
)
// Generate a Secret and a ConfigMap from the same data
// to compare the result.
func TestGeneratorBasics(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app")
th.WriteK("/app", `
namePrefix: blah-
configMapGenerator:
- name: bob
literals:
- fruit=apple
- vegetable=broccoli
envs:
- foo.env
files:
- passphrase=phrase.dat
- forces.txt
- name: json
literals:
- 'v2=[{"path": "var/druid/segment-cache"}]'
- >-
druid_segmentCache_locations=[{"path":
"var/druid/segment-cache",
"maxSize": 32000000000,
"freeSpacePercent": 1.0}]
secretGenerator:
- name: bob
literals:
- fruit=apple
- vegetable=broccoli
envs:
- foo.env
files:
- passphrase=phrase.dat
- forces.txt
`)
th.WriteF("/app/foo.env", `
MOUNTAIN=everest
OCEAN=pacific
`)
th.WriteF("/app/phrase.dat", `
Life is short.
But the years are long.
Not while the evil days come not.
`)
th.WriteF("/app/forces.txt", `
gravitational
electromagnetic
strong nuclear
weak nuclear
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: v1
data:
MOUNTAIN: everest
OCEAN: pacific
forces.txt: |2
gravitational
electromagnetic
strong nuclear
weak nuclear
fruit: apple
passphrase: |2
Life is short.
But the years are long.
Not while the evil days come not.
vegetable: broccoli
kind: ConfigMap
metadata:
name: blah-bob-k772g5db55
---
apiVersion: v1
data:
druid_segmentCache_locations: '[{"path": "var/druid/segment-cache", "maxSize":
32000000000, "freeSpacePercent": 1.0}]'
v2: '[{"path": "var/druid/segment-cache"}]'
kind: ConfigMap
metadata:
name: blah-json-9gtcc2fgb4
---
apiVersion: v1
data:
MOUNTAIN: ZXZlcmVzdA==
OCEAN: cGFjaWZpYw==
forces.txt: CmdyYXZpdGF0aW9uYWwKZWxlY3Ryb21hZ25ldGljCnN0cm9uZyBudWNsZWFyCndlYWsgbnVjbGVhcgo=
fruit: YXBwbGU=
passphrase: CkxpZmUgaXMgc2hvcnQuCkJ1dCB0aGUgeWVhcnMgYXJlIGxvbmcuCk5vdCB3aGlsZSB0aGUgZXZpbCBkYXlzIGNvbWUgbm90Lgo=
vegetable: YnJvY2NvbGk=
kind: Secret
metadata:
name: blah-bob-gmc2824f4b
type: Opaque
`)
}
// TODO: These should be errors instead.
func TestGeneratorRepeatsInKustomization(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app")
th.WriteK("/app", `
namePrefix: blah-
configMapGenerator:
- name: bob
behavior: create
literals:
- bean=pinto
- star=wolf-rayet
literals:
- fruit=apple
- vegetable=broccoli
files:
- forces.txt
files:
- nobles=nobility.txt
`)
th.WriteF("/app/forces.txt", `
gravitational
electromagnetic
strong nuclear
weak nuclear
`)
th.WriteF("/app/nobility.txt", `
helium
neon
argon
krypton
xenon
radon
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: v1
data:
fruit: apple
nobles: |2
helium
neon
argon
krypton
xenon
radon
vegetable: broccoli
kind: ConfigMap
metadata:
name: blah-bob-gfkcbk5ckf
`)
}
func TestGeneratorOverlays(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/overlay")
th.WriteK("/app/base1", `
namePrefix: p1-
configMapGenerator:
- name: com1
behavior: create
literals:
- from=base
`)
th.WriteK("/app/base2", `
namePrefix: p2-
configMapGenerator:
- name: com2
behavior: create
literals:
- from=base
`)
th.WriteK("/app/overlay/o1", `
resources:
- ../../base1
configMapGenerator:
- name: com1
behavior: merge
literals:
- from=overlay
`)
th.WriteK("/app/overlay/o2", `
resources:
- ../../base2
configMapGenerator:
- name: com2
behavior: merge
literals:
- from=overlay
`)
th.WriteK("/app/overlay", `
resources:
- o1
- o2
configMapGenerator:
- name: com1
behavior: merge
literals:
- foo=bar
- baz=qux
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: v1
data:
baz: qux
foo: bar
from: overlay
kind: ConfigMap
metadata:
annotations: {}
labels: {}
name: p1-com1-dhbbm922gd
---
apiVersion: v1
data:
from: overlay
kind: ConfigMap
metadata:
annotations: {}
labels: {}
name: p2-com2-c4b8md75k9
`)
}
func TestConfigMapGeneratorMergeNamePrefix(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app")
th.WriteK("/app/base", `
configMapGenerator:
- name: cm
behavior: create
literals:
- foo=bar
`)
th.WriteK("/app/o1", `
resources:
- ../base
namePrefix: o1-
`)
th.WriteK("/app/o2", `
resources:
- ../base
nameSuffix: -o2
`)
th.WriteK("/app", `
resources:
- o1
- o2
configMapGenerator:
- name: o1-cm
behavior: merge
literals:
- big=bang
- name: cm-o2
behavior: merge
literals:
- big=crunch
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: v1
data:
big: bang
foo: bar
kind: ConfigMap
metadata:
annotations: {}
labels: {}
name: o1-cm-28g596k77k
---
apiVersion: v1
data:
big: crunch
foo: bar
kind: ConfigMap
metadata:
annotations: {}
labels: {}
name: cm-o2-gfcc59fg5m
`)
}

384
api/target/crd_test.go Normal file
View File

@@ -0,0 +1,384 @@
/*
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 target_test
import (
"testing"
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
)
func writeBaseWithCrd(th *kusttest_test.KustTestHarness) {
th.WriteK("/app/base", `
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
crds:
- mycrd.json
resources:
- secret.yaml
- mykind.yaml
- bee.yaml
namePrefix: x-
`)
th.WriteF("/app/base/bee.yaml", `
apiVersion: v1beta1
kind: Bee
metadata:
name: bee
spec:
action: fly
`)
th.WriteF("/app/base/mykind.yaml", `
apiVersion: jingfang.example.com/v1beta1
kind: MyKind
metadata:
name: mykind
spec:
secretRef:
name: crdsecret
beeRef:
name: bee
`)
th.WriteF("/app/base/secret.yaml", `
apiVersion: v1
kind: Secret
metadata:
name: crdsecret
data:
PATH: yellowBrickRoad
`)
th.WriteF("/app/base/mycrd.json", `
{
"github.com/example/pkg/apis/jingfang/v1beta1.Bee": {
"Schema": {
"description": "Bee",
"properties": {
"apiVersion": {
"description": "APIVersion defines the versioned schema of this representation of an object.",
"type": "string"
},
"kind": {
"description": "Kind is a string value representing the REST resource this object represents.",
"type": "string"
},
"metadata": {
"$ref": "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"
},
"spec": {
"$ref": "github.com/example/pkg/apis/jingfang/v1beta1.BeeSpec"
},
"status": {
"$ref": "github.com/example/pkg/apis/jingfang/v1beta1.BeeStatus"
}
}
},
"Dependencies": [
"github.com/example/pkg/apis/jingfang/v1beta1.BeeSpec",
"github.com/example/pkg/apis/jingfang/v1beta1.BeeStatus",
"k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"
]
},
"github.com/example/pkg/apis/jingfang/v1beta1.BeeList": {
"Schema": {
"required": [
"items"
],
"properties": {
"apiVersion": {
"description": "APIVersion defines the versioned schema of this representation of an object.",
"type": "string"
},
"items": {
"type": "array",
"items": {
"$ref": "github.com/example/pkg/apis/jingfang/v1beta1.Bee"
}
},
"kind": {
"description": "Kind is a string value representing the REST resource this object represents.",
"type": "string"
},
"metadata": {
"$ref": "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"
}
}
},
"Dependencies": [
"github.com/example/pkg/apis/jingfang/v1beta1.Bee",
"k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"
]
},
"github.com/example/pkg/apis/jingfang/v1beta1.BeeObjectReference": {
"Schema": {
"properties": {
"name": {
"type": "string"
}
}
},
"Dependencies": []
},
"github.com/example/pkg/apis/jingfang/v1beta1.BeeSpec": {
"Schema": {
"description": "BeeSpec defines the desired state of Bee"
},
"Dependencies": []
},
"github.com/example/pkg/apis/jingfang/v1beta1.BeeStatus": {
"Schema": {
"description": "BeeStatus defines the observed state of Bee"
},
"Dependencies": []
},
"github.com/example/pkg/apis/jingfang/v1beta1.MyKind": {
"Schema": {
"description": "MyKind",
"properties": {
"apiVersion": {
"description": "APIVersion defines the versioned schema of this representation of an object.",
"type": "string"
},
"kind": {
"description": "Kind is a string value representing the REST resource this object represents.",
"type": "string"
},
"metadata": {
"$ref": "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"
},
"spec": {
"$ref": "github.com/example/pkg/apis/jingfang/v1beta1.MyKindSpec"
},
"status": {
"$ref": "github.com/example/pkg/apis/jingfang/v1beta1.MyKindStatus"
}
}
},
"Dependencies": [
"github.com/example/pkg/apis/jingfang/v1beta1.MyKindSpec",
"github.com/example/pkg/apis/jingfang/v1beta1.MyKindStatus",
"k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"
]
},
"github.com/example/pkg/apis/jingfang/v1beta1.MyKindList": {
"Schema": {
"required": [
"items"
],
"properties": {
"apiVersion": {
"description": "APIVersion defines the versioned schema of this representation of an object.",
"type": "string"
},
"items": {
"type": "array",
"items": {
"$ref": "github.com/example/pkg/apis/jingfang/v1beta1.MyKind"
}
},
"kind": {
"description": "Kind is a string value representing the REST resource this object represents.",
"type": "string"
},
"metadata": {
"$ref": "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"
}
}
},
"Dependencies": [
"github.com/example/pkg/apis/jingfang/v1beta1.MyKind",
"k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"
]
},
"github.com/example/pkg/apis/jingfang/v1beta1.MyKindSpec": {
"Schema": {
"description": "MyKindSpec defines the desired state of MyKind",
"properties": {
"beeRef": {
"x-kubernetes-object-ref-api-version": "v1beta1",
"x-kubernetes-object-ref-kind": "Bee",
"$ref": "github.com/example/pkg/apis/jingfang/v1beta1.BeeObjectReference"
},
"secretRef": {
"description": "If defined, use this secret for configuring the MYSQL_ROOT_PASSWORD",
"x-kubernetes-object-ref-api-version": "v1",
"x-kubernetes-object-ref-kind": "Secret",
"$ref": "k8s.io/api/core/v1.LocalObjectReference"
}
}
},
"Dependencies": [
"github.com/example/pkg/apis/jingfang/v1beta1.BeeObjectReference",
"k8s.io/api/core/v1.LocalObjectReference"
]
},
"github.com/example/pkg/apis/jingfang/v1beta1.MyKindStatus": {
"Schema": {
"description": "MyKindStatus defines the observed state of MyKind"
},
"Dependencies": []
}
}
`)
}
func TestCrdBase(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/base")
writeBaseWithCrd(th)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: v1
data:
PATH: yellowBrickRoad
kind: Secret
metadata:
name: x-crdsecret
---
apiVersion: jingfang.example.com/v1beta1
kind: MyKind
metadata:
name: x-mykind
spec:
beeRef:
name: x-bee
secretRef:
name: x-crdsecret
---
apiVersion: v1beta1
kind: Bee
metadata:
name: x-bee
spec:
action: fly
`)
}
func TestCrdWithOverlay(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/overlay")
writeBaseWithCrd(th)
th.WriteK("/app/overlay", `
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namePrefix: prod-
resources:
- ../base
patchesStrategicMerge:
- bee.yaml
`)
th.WriteF("/app/overlay/bee.yaml", `
apiVersion: v1beta1
kind: Bee
metadata:
name: bee
spec:
action: makehoney
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: v1
data:
PATH: yellowBrickRoad
kind: Secret
metadata:
name: prod-x-crdsecret
---
apiVersion: jingfang.example.com/v1beta1
kind: MyKind
metadata:
name: prod-x-mykind
spec:
beeRef:
name: prod-x-bee
secretRef:
name: prod-x-crdsecret
---
apiVersion: v1beta1
kind: Bee
metadata:
name: prod-x-bee
spec:
action: makehoney
`)
}
func TestCrdWithContainers(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/crd/containers")
th.WriteK("/app/crd/containers", `
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- crd.yaml
images:
- name: test/test
newName: registry.gitlab.com/test
newTag: latest
`)
th.WriteF("/app/crd/containers/crd.yaml", `
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: crontabs.stable.example.com
spec:
group: stable.example.com
scope: Namespaced
names:
plural: crontabs
singular: crontab
kind: CronTab
shortNames:
- ct
validation:
openAPIV3Schema:
properties:
spec:
containers:
description: Containers allows injecting additional containers
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: crontabs.stable.example.com
spec:
group: stable.example.com
names:
kind: CronTab
plural: crontabs
shortNames:
- ct
singular: crontab
scope: Namespaced
validation:
openAPIV3Schema:
properties:
spec:
containers:
description: Containers allows injecting additional containers
`)
}

View File

@@ -0,0 +1,332 @@
/*
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 target_test
import (
"testing"
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
)
func makeBaseReferencingCustomConfig(th *kusttest_test.KustTestHarness) {
th.WriteK("/app/base", `
namePrefix: x-
commonLabels:
app: myApp
vars:
- name: APRIL_DIET
objref:
kind: Giraffe
name: april
fieldref:
fieldpath: spec.diet
- name: KOKO_DIET
objref:
kind: Gorilla
name: koko
fieldref:
fieldpath: spec.diet
resources:
- animalPark.yaml
- giraffes.yaml
- gorilla.yaml
configurations:
- config/defaults.yaml
- config/custom.yaml
`)
th.WriteF("/app/base/giraffes.yaml", `
kind: Giraffe
metadata:
name: april
spec:
diet: mimosa
location: NE
---
kind: Giraffe
metadata:
name: may
spec:
diet: acacia
location: SE
`)
th.WriteF("/app/base/gorilla.yaml", `
kind: Gorilla
metadata:
name: koko
spec:
diet: bambooshoots
location: SW
`)
th.WriteF("/app/base/animalPark.yaml", `
kind: AnimalPark
metadata:
name: sandiego
spec:
gorillaRef:
name: koko
giraffeRef:
name: april
food:
- "$(APRIL_DIET)"
- "$(KOKO_DIET)"
`)
}
func TestCustomConfig(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/base")
makeBaseReferencingCustomConfig(th)
th.WriteDefaultConfigs("/app/base/config/defaults.yaml")
th.WriteF("/app/base/config/custom.yaml", `
nameReference:
- kind: Gorilla
fieldSpecs:
- kind: AnimalPark
path: spec/gorillaRef/name
- kind: Giraffe
fieldSpecs:
- kind: AnimalPark
path: spec/giraffeRef/name
varReference:
- path: spec/food
kind: AnimalPark
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
kind: AnimalPark
metadata:
labels:
app: myApp
name: x-sandiego
spec:
food:
- mimosa
- bambooshoots
giraffeRef:
name: x-april
gorillaRef:
name: x-koko
---
kind: Giraffe
metadata:
labels:
app: myApp
name: x-april
spec:
diet: mimosa
location: NE
---
kind: Giraffe
metadata:
labels:
app: myApp
name: x-may
spec:
diet: acacia
location: SE
---
kind: Gorilla
metadata:
labels:
app: myApp
name: x-koko
spec:
diet: bambooshoots
location: SW
`)
}
func TestCustomConfigWithDefaultOverspecification(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/base")
makeBaseReferencingCustomConfig(th)
th.WriteDefaultConfigs("/app/base/config/defaults.yaml")
// Specifying namePrefix here conflicts with (is the same as)
// the defaults written above. This is intentional in the
// test to assure duplicate config doesn't cause problems.
th.WriteF("/app/base/config/custom.yaml", `
namePrefix:
- path: metadata/name
nameReference:
- kind: Gorilla
fieldSpecs:
- kind: AnimalPark
path: spec/gorillaRef/name
- kind: Giraffe
fieldSpecs:
- kind: AnimalPark
path: spec/giraffeRef/name
varReference:
- path: spec/food
kind: AnimalPark
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
kind: AnimalPark
metadata:
labels:
app: myApp
name: x-sandiego
spec:
food:
- mimosa
- bambooshoots
giraffeRef:
name: x-april
gorillaRef:
name: x-koko
---
kind: Giraffe
metadata:
labels:
app: myApp
name: x-april
spec:
diet: mimosa
location: NE
---
kind: Giraffe
metadata:
labels:
app: myApp
name: x-may
spec:
diet: acacia
location: SE
---
kind: Gorilla
metadata:
labels:
app: myApp
name: x-koko
spec:
diet: bambooshoots
location: SW
`)
}
func TestFixedBug605_BaseCustomizationAvailableInOverlay(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/overlay")
makeBaseReferencingCustomConfig(th)
th.WriteDefaultConfigs("/app/base/config/defaults.yaml")
th.WriteF("/app/base/config/custom.yaml", `
nameReference:
- kind: Gorilla
fieldSpecs:
- kind: AnimalPark
path: spec/gorillaRef/name
- kind: Giraffe
fieldSpecs:
- kind: AnimalPark
path: spec/giraffeRef/name
varReference:
- path: spec/food
kind: AnimalPark
`)
th.WriteK("/app/overlay", `
namePrefix: o-
commonLabels:
movie: planetOfTheApes
patchesStrategicMerge:
- animalPark.yaml
resources:
- ../base
- ursus.yaml
`)
th.WriteF("/app/overlay/ursus.yaml", `
kind: Gorilla
metadata:
name: ursus
spec:
diet: heston
location: Arizona
`)
// The following replaces the gorillaRef in the AnimalPark.
th.WriteF("/app/overlay/animalPark.yaml", `
kind: AnimalPark
metadata:
name: sandiego
spec:
gorillaRef:
name: ursus
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
kind: AnimalPark
metadata:
labels:
app: myApp
movie: planetOfTheApes
name: o-x-sandiego
spec:
food:
- mimosa
- bambooshoots
giraffeRef:
name: o-x-april
gorillaRef:
name: o-ursus
---
kind: Giraffe
metadata:
labels:
app: myApp
movie: planetOfTheApes
name: o-x-april
spec:
diet: mimosa
location: NE
---
kind: Giraffe
metadata:
labels:
app: myApp
movie: planetOfTheApes
name: o-x-may
spec:
diet: acacia
location: SE
---
kind: Gorilla
metadata:
labels:
app: myApp
movie: planetOfTheApes
name: o-x-koko
spec:
diet: bambooshoots
location: SW
---
kind: Gorilla
metadata:
labels:
movie: planetOfTheApes
name: o-ursus
spec:
diet: heston
location: Arizona
`)
}

View File

@@ -0,0 +1,216 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package target_test
import (
"testing"
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
)
// Demo custom configuration of a builtin transformation.
// This is a NamePrefixer that only touches Deployments
// and Services.
func TestCustomNamePrefixer(t *testing.T) {
tc := kusttest_test.NewPluginTestEnv(t).Set()
defer tc.Reset()
tc.BuildGoPlugin(
"builtin", "", "PrefixSuffixTransformer")
th := kusttest_test.NewKustTestHarnessAllowPlugins(t, "/app")
th.WriteK("/app", `
resources:
- deployment.yaml
- role.yaml
- service.yaml
transformers:
- prefixer.yaml
`)
th.WriteF("/app/prefixer.yaml", `
apiVersion: builtin
kind: PrefixSuffixTransformer
metadata:
name: customPrefixer
prefix: zzz-
fieldSpecs:
- kind: Deployment
path: metadata/name
- kind: Service
path: metadata/name
`)
th.WriteF("/app/deployment.yaml", `
apiVersion: apps/v1
kind: Deployment
metadata:
name: myDeployment
spec:
template:
metadata:
labels:
backend: awesome
spec:
containers:
- name: whatever
image: whatever
`)
th.WriteF("/app/role.yaml", `
apiVersion: v1
kind: Role
metadata:
name: myRole
`)
th.WriteF("/app/service.yaml", `
apiVersion: v1
kind: Service
metadata:
name: myService
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1
kind: Deployment
metadata:
name: zzz-myDeployment
spec:
template:
metadata:
labels:
backend: awesome
spec:
containers:
- image: whatever
name: whatever
---
apiVersion: v1
kind: Role
metadata:
name: myRole
---
apiVersion: v1
kind: Service
metadata:
name: zzz-myService
`)
}
// Demo custom configuration as a base.
func TestReusableCustomNamePrefixer(t *testing.T) {
tc := kusttest_test.NewPluginTestEnv(t).Set()
defer tc.Reset()
tc.BuildGoPlugin(
"builtin", "", "PrefixSuffixTransformer")
tc.BuildGoPlugin(
"builtin", "", "LabelTransformer")
th := kusttest_test.NewKustTestHarnessAllowPlugins(t, "/app/foo")
// This kustomization file contains resources that
// all happen to be plugin configurations. This makes
// these plugins all available as part of a base,
// re-usable in any number of other kustomizations.
// Just specify the path (or URL) to this base in the
// "transformers:" field (not the "resources" field).
th.WriteK("/app/mytransformers", `
resources:
- prefixer.yaml
- labeller.yaml
`)
th.WriteF("/app/mytransformers/prefixer.yaml", `
apiVersion: builtin
kind: PrefixSuffixTransformer
metadata:
name: myPrefixer
prefix: zzz-
fieldSpecs:
- kind: Deployment
path: metadata/name
- kind: Service
path: metadata/name
`)
th.WriteF("/app/mytransformers/labeller.yaml", `
apiVersion: builtin
kind: LabelTransformer
metadata:
name: myLabeller
labels:
company: acmeCorp
fieldSpecs:
- path: spec/template/metadata/labels
kind: Deployment
`)
th.WriteK("/app/foo", `
resources:
- deployment.yaml
- role.yaml
- service.yaml
transformers:
- ../mytransformers
`)
th.WriteF("/app/foo/deployment.yaml", `
apiVersion: apps/v1
kind: Deployment
metadata:
name: myDeployment
spec:
template:
metadata:
labels:
backend: awesome
spec:
containers:
- name: whatever
image: whatever
`)
th.WriteF("/app/foo/role.yaml", `
apiVersion: v1
kind: Role
metadata:
name: myRole
`)
th.WriteF("/app/foo/service.yaml", `
apiVersion: v1
kind: Service
metadata:
name: myService
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1
kind: Deployment
metadata:
name: zzz-myDeployment
spec:
template:
metadata:
labels:
backend: awesome
company: acmeCorp
spec:
containers:
- image: whatever
name: whatever
---
apiVersion: v1
kind: Role
metadata:
name: myRole
---
apiVersion: v1
kind: Service
metadata:
name: zzz-myService
`)
}

View File

@@ -0,0 +1,490 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package target_test
import (
"fmt"
"path/filepath"
"strings"
"testing"
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
)
const patchAddProbe = `
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-deployment
spec:
template:
spec:
containers:
- name: my-deployment
livenessProbe:
httpGet:
path: /healthz
port: 8080
`
const container = `{ "image": "my-image", "livenessProbe": { "httpGet" : {"path": "/healthz", "port": 8080 } }, "name": "my-deployment"}`
const patchJsonAddProbe = `[{"op": "replace", "path": "/spec/template/spec/containers/0", "value": ` +
container + `}]`
const patchDnsPolicy = `
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-deployment
spec:
template:
spec:
dnsPolicy: ClusterFirst
`
const patchJsonDnsPolicy = `[{"op": "add", "path": "/spec/template/spec/dnsPolicy", "value": "ClusterFirst"}]`
const patchRestartPolicy = `
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-deployment
spec:
template:
spec:
restartPolicy: Always
`
const patchJsonRestartPolicy = `[{"op": "add", "path": "/spec/template/spec/restartPolicy", "value": "Always"}]`
func writeDeploymentBase(th *kusttest_test.KustTestHarness) {
th.WriteK("/app/base", `
resources:
- deployment.yaml
`)
th.WriteF("/app/base/deployment.yaml", `
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-deployment
spec:
template:
spec:
dnsPolicy: "None"
containers:
- name: my-deployment
image: my-image
`)
}
func writeProbeOverlay(th *kusttest_test.KustTestHarness) {
th.WriteK("/app/probe", `
resources:
- ../base
patchesStrategicMerge:
- dep-patch.yaml
`)
th.WriteF("/app/probe/dep-patch.yaml", patchAddProbe)
}
func writeDNSOverlay(th *kusttest_test.KustTestHarness) {
th.WriteK("/app/dns", `
resources:
- ../base
patchesStrategicMerge:
- dep-patch.yaml
`)
th.WriteF("/app/dns/dep-patch.yaml", patchDnsPolicy)
}
func writeRestartOverlay(th *kusttest_test.KustTestHarness) {
th.WriteK("/app/restart", `
resources:
- ../base
patchesStrategicMerge:
- dep-patch.yaml
`)
th.WriteF("/app/restart/dep-patch.yaml", patchRestartPolicy)
}
// Here's a composite kustomization, that combines multiple overlays
// (replicas, dns and metadata) which patch the same base resource.
//
// The base resource is a deployment and the overlays patch aspects
// of it, without using any of the `namePrefix`, `nameSuffix` or `namespace`
// kustomization keywords.
//
// composite
// / | \
// probe dns restart
// \ | /
// base
//
func TestIssue1251_CompositeDiamond_Failure(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/composite")
writeDeploymentBase(th)
writeProbeOverlay(th)
writeDNSOverlay(th)
writeRestartOverlay(th)
th.WriteK("/app/composite", `
resources:
- ../probe
- ../dns
- ../restart
`)
_, err := th.MakeKustTarget().MakeCustomizedResMap()
if err == nil {
t.Fatalf("Expected resource accumulation error")
}
if !strings.Contains(
err.Error(), "already registered id: apps_v1_Deployment|~X|my-deployment") {
t.Fatalf("Unexpected err: %v", err)
}
}
const expectedPatchedDeployment = `
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-deployment
spec:
template:
spec:
containers:
- image: my-image
livenessProbe:
httpGet:
path: /healthz
port: 8080
name: my-deployment
dnsPolicy: ClusterFirst
restartPolicy: Always
`
// This test reuses some methods from TestIssue1251_CompositeDiamond,
// but overwrites the kustomization files in the overlays.
func TestIssue1251_Patches_Overlayed(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/restart")
writeDeploymentBase(th)
// probe overlays base.
writeProbeOverlay(th)
// dns overlays probe.
writeDNSOverlay(th)
th.WriteK("/app/dns", `
resources:
- ../probe
patchesStrategicMerge:
- dep-patch.yaml
`)
// restart overlays dns.
writeRestartOverlay(th)
th.WriteK("/app/restart", `
resources:
- ../dns
patchesStrategicMerge:
- dep-patch.yaml
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, expectedPatchedDeployment)
}
func TestIssue1251_Patches_Local(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/composite")
writeDeploymentBase(th)
th.WriteK("/app/composite", `
resources:
- ../base
patchesStrategicMerge:
- patchAddProbe.yaml
- patchDnsPolicy.yaml
- patchRestartPolicy.yaml
`)
th.WriteF("/app/composite/patchRestartPolicy.yaml", patchRestartPolicy)
th.WriteF("/app/composite/patchDnsPolicy.yaml", patchDnsPolicy)
th.WriteF("/app/composite/patchAddProbe.yaml", patchAddProbe)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, expectedPatchedDeployment)
}
func definePatchDirStructure(th *kusttest_test.KustTestHarness) {
writeDeploymentBase(th)
th.WriteF("/app/patches/patchRestartPolicy.yaml", patchRestartPolicy)
th.WriteF("/app/patches/patchDnsPolicy.yaml", patchDnsPolicy)
th.WriteF("/app/patches/patchAddProbe.yaml", patchAddProbe)
}
// Fails due to file load restrictor.
func TestIssue1251_Patches_ProdVsDev_Failure(t *testing.T) {
th := kusttest_test.NewKustTestHarnessAllowPlugins(t, "/app/prod")
definePatchDirStructure(th)
th.WriteK("/app/prod", `
resources:
- ../base
patchesStrategicMerge:
- ../patches/patchAddProbe.yaml
- ../patches/patchDnsPolicy.yaml
`)
_, err := th.MakeKustTarget().MakeCustomizedResMap()
if err == nil {
t.Fatalf("expected error")
}
if !strings.Contains(
err.Error(),
"security; file '/app/patches/patchAddProbe.yaml' is not in or below '/app/prod'") {
t.Fatalf("unexpected error: %v", err)
}
}
const prodDevMergeResult1 = `
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-deployment
spec:
template:
spec:
containers:
- image: my-image
livenessProbe:
httpGet:
path: /healthz
port: 8080
name: my-deployment
dnsPolicy: ClusterFirst
`
const prodDevMergeResult2 = `
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-deployment
spec:
template:
spec:
containers:
- image: my-image
name: my-deployment
dnsPolicy: ClusterFirst
restartPolicy: Always
`
// This test does what
// TestIssue1251_Patches_ProdVsDev_Failure
// failed to do, because this test does the equivalent
// os specifying `--load_restrictor none` on the build.
//
// This allows the use patch files located outside the
// kustomization root, and not in a kustomization
// themselves.
//
// Doing so means the kustomization using them is no
// longer relocatable, not addressible via a git URL,
// and not git clonable. It's no longer self-contained.
//
// Likewise suppressing load restrictions happens for
// the entire build (i.e. everything can reach outside
// the kustomization root), opening the user to whatever
// threat the load restrictor was meant to address.
func TestIssue1251_Patches_ProdVsDev(t *testing.T) {
th := kusttest_test.NewKustTestHarnessNoLoadRestrictor(t, "/app/prod")
definePatchDirStructure(th)
th.WriteK("/app/prod", `
resources:
- ../base
patchesStrategicMerge:
- ../patches/patchAddProbe.yaml
- ../patches/patchDnsPolicy.yaml
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
th.AssertActualEqualsExpected(m, prodDevMergeResult1)
th = kusttest_test.NewKustTestHarnessNoLoadRestrictor(t, "/app/dev")
definePatchDirStructure(th)
th.WriteK("/app/dev", `
resources:
- ../base
patchesStrategicMerge:
- ../patches/patchDnsPolicy.yaml
- ../patches/patchRestartPolicy.yaml
`)
m, err = th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, prodDevMergeResult2)
}
func TestIssue1251_Plugins_ProdVsDev(t *testing.T) {
tc := kusttest_test.NewPluginTestEnv(t).Set()
defer tc.Reset()
tc.BuildGoPlugin(
"builtin", "", "PatchJson6902Transformer")
th := kusttest_test.NewKustTestHarnessAllowPlugins(t, "/app/prod")
defineTransformerDirStructure(th)
th.WriteK("/app/prod", `
resources:
- ../base
transformers:
- ../patches/addProbe
- ../patches/addDnsPolicy
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, prodDevMergeResult1)
th = kusttest_test.NewKustTestHarnessAllowPlugins(t, "/app/dev")
defineTransformerDirStructure(th)
th.WriteK("/app/dev", `
resources:
- ../base
transformers:
- ../patches/addRestartPolicy
- ../patches/addDnsPolicy
`)
m, err = th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, prodDevMergeResult2)
}
func TestIssue1251_Plugins_Local(t *testing.T) {
tc := kusttest_test.NewPluginTestEnv(t).Set()
defer tc.Reset()
tc.BuildGoPlugin(
"builtin", "", "PatchJson6902Transformer")
th := kusttest_test.NewKustTestHarnessAllowPlugins(t, "/app/composite")
writeDeploymentBase(th)
writeJsonTransformerPluginConfig(
th, "/app/composite", "addDnsPolicy", patchJsonDnsPolicy)
writeJsonTransformerPluginConfig(
th, "/app/composite", "addRestartPolicy", patchJsonRestartPolicy)
writeJsonTransformerPluginConfig(
th, "/app/composite", "addProbe", patchJsonAddProbe)
th.WriteK("/app/composite", `
resources:
- ../base
transformers:
- addDnsPolicyConfig.yaml
- addRestartPolicyConfig.yaml
- addProbeConfig.yaml
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, expectedPatchedDeployment)
}
func writeJsonTransformerPluginConfig(
th *kusttest_test.KustTestHarness, path, name, patch string) {
th.WriteF(filepath.Join(path, name+"Config.yaml"),
fmt.Sprintf(`
apiVersion: builtin
kind: PatchJson6902Transformer
metadata:
name: %s
target:
group: apps
version: v1
kind: Deployment
name: my-deployment
jsonOp: '%s'
`, name, patch))
}
// Remote in the sense that they are bundled in a different kustomization.
func TestIssue1251_Plugins_Bundled(t *testing.T) {
tc := kusttest_test.NewPluginTestEnv(t).Set()
defer tc.Reset()
tc.BuildGoPlugin(
"builtin", "", "PatchJson6902Transformer")
th := kusttest_test.NewKustTestHarnessAllowPlugins(t, "/app/composite")
writeDeploymentBase(th)
th.WriteK("/app/patches", `
resources:
- addDnsPolicyConfig.yaml
- addRestartPolicyConfig.yaml
- addProbeConfig.yaml
`)
writeJsonTransformerPluginConfig(
th, "/app/patches", "addDnsPolicy", patchJsonDnsPolicy)
writeJsonTransformerPluginConfig(
th, "/app/patches", "addRestartPolicy", patchJsonRestartPolicy)
writeJsonTransformerPluginConfig(
th, "/app/patches", "addProbe", patchJsonAddProbe)
th.WriteK("/app/composite", `
resources:
- ../base
transformers:
- ../patches
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, expectedPatchedDeployment)
}
func defineTransformerDirStructure(th *kusttest_test.KustTestHarness) {
writeDeploymentBase(th)
th.WriteK("/app/patches/addDnsPolicy", `
resources:
- addDnsPolicyConfig.yaml
`)
writeJsonTransformerPluginConfig(
th, "/app/patches/addDnsPolicy", "addDnsPolicy", patchJsonDnsPolicy)
th.WriteK("/app/patches/addRestartPolicy", `
resources:
- addRestartPolicyConfig.yaml
`)
writeJsonTransformerPluginConfig(
th, "/app/patches/addRestartPolicy", "addRestartPolicy", patchJsonRestartPolicy)
th.WriteK("/app/patches/addProbe", `
resources:
- addProbeConfig.yaml
`)
writeJsonTransformerPluginConfig(
th, "/app/patches/addProbe", "addProbe", patchJsonAddProbe)
}

225
api/target/diamonds_test.go Normal file
View File

@@ -0,0 +1,225 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package target_test
import (
"testing"
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
)
// Here's a structure of two kustomizations,
// `dev` and `prod`, individually deployable,
// that depend on a diamond that combines
// multiple tenants (kirk, spock and bones),
// each sharing a common base.
//
// The objects used are contrived to avoid
// clouding the example with authentic
// but verbose Deployment boilerplate.
//
// Patches are applied at various levels,
// requiring more specificity as needed.
//
// dev prod
// \ /
// tenants
// / | \
// kirk spock bones
// \ | /
// base
//
func writeDiamondBase(th *kusttest_test.KustTestHarness) {
th.WriteK("/app/base", `
resources:
- deploy.yaml
`)
th.WriteF("/app/base/deploy.yaml", `
apiVersion: v1
kind: Deployment
metadata:
name: storefront
spec:
numReplicas: 1
`)
}
func writeKirk(th *kusttest_test.KustTestHarness) {
th.WriteK("/app/kirk", `
namePrefix: kirk-
resources:
- ../base
- configmap.yaml
patchesStrategicMerge:
- dep-patch.yaml
`)
th.WriteF("/app/kirk/dep-patch.yaml", `
apiVersion: v1
kind: Deployment
metadata:
name: storefront
spec:
type: Confident
`)
th.WriteF("/app/kirk/configmap.yaml", `
apiVersion: v1
kind: ConfigMap
metadata:
name: settings
data:
phaser: caress
`)
}
func writeSpock(th *kusttest_test.KustTestHarness) {
th.WriteK("/app/spock", `
namePrefix: spock-
resources:
- ../base
patchesStrategicMerge:
- dep-patch.yaml
`)
th.WriteF("/app/spock/dep-patch.yaml", `
apiVersion: v1
kind: Deployment
metadata:
name: storefront
spec:
type: Logical
`)
}
func writeBones(th *kusttest_test.KustTestHarness) {
th.WriteK("/app/bones", `
namePrefix: bones-
resources:
- ../base
patchesStrategicMerge:
- dep-patch.yaml
`)
th.WriteF("/app/bones/dep-patch.yaml", `
apiVersion: v1
kind: Deployment
metadata:
name: storefront
spec:
type: Concerned
`)
}
func writeTenants(th *kusttest_test.KustTestHarness) {
th.WriteK("/app/tenants", `
namePrefix: t-
resources:
- ../kirk
- ../spock
- ../bones
- configMap.yaml
patchesStrategicMerge:
- bones-patch.yaml
`)
th.WriteF("/app/tenants/bones-patch.yaml", `
apiVersion: v1
kind: Deployment
metadata:
name: bones-storefront
spec:
mood: Cantankerous
`)
th.WriteF("/app/tenants/configMap.yaml", `
apiVersion: v1
kind: ConfigMap
metadata:
name: federation
data:
zone: neutral
guardian: forever
`)
}
func TestBasicDiamond(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/prod")
writeDiamondBase(th)
writeKirk(th)
writeSpock(th)
writeBones(th)
writeTenants(th)
th.WriteK("/app/prod", `
namePrefix: prod-
resources:
- ../tenants
patchesStrategicMerge:
- patches.yaml
`)
// The patch only has to be specific enough
// to match the item.
th.WriteF("/app/prod/patches.yaml", `
apiVersion: v1
kind: Deployment
metadata:
name: t-kirk-storefront
spec:
numReplicas: 10000
---
apiVersion: v1
kind: ConfigMap
metadata:
name: federation
data:
guardian: ofTheGalaxy
---
apiVersion: v1
kind: ConfigMap
metadata:
name: t-federation
data:
zone: twilight
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: v1
kind: Deployment
metadata:
name: prod-t-kirk-storefront
spec:
numReplicas: 10000
type: Confident
---
apiVersion: v1
data:
phaser: caress
kind: ConfigMap
metadata:
name: prod-t-kirk-settings
---
apiVersion: v1
kind: Deployment
metadata:
name: prod-t-spock-storefront
spec:
numReplicas: 1
type: Logical
---
apiVersion: v1
kind: Deployment
metadata:
name: prod-t-bones-storefront
spec:
mood: Cantankerous
numReplicas: 1
type: Concerned
---
apiVersion: v1
data:
guardian: ofTheGalaxy
zone: twilight
kind: ConfigMap
metadata:
name: prod-t-federation
`)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,597 @@
/*
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 target_test
import (
"strings"
"testing"
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
)
func TestSimpleBase(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/base")
th.WriteK("/app/base", `
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namePrefix: team-foo-
commonLabels:
app: mynginx
org: example.com
team: foo
commonAnnotations:
note: This is a test annotation
resources:
- service.yaml
- deployment.yaml
- networkpolicy.yaml
`)
th.WriteF("/app/base/service.yaml", `
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- port: 80
selector:
app: nginx
`)
th.WriteF("/app/base/networkpolicy.yaml", `
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: nginx
spec:
podSelector:
matchExpressions:
- {key: app, operator: In, values: [test]}
ingress:
- from:
- podSelector:
matchLabels:
app: nginx
`)
th.WriteF("/app/base/deployment.yaml", `
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: nginx
labels:
app: nginx
spec:
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: v1
kind: Service
metadata:
annotations:
note: This is a test annotation
labels:
app: mynginx
org: example.com
team: foo
name: team-foo-nginx
spec:
ports:
- port: 80
selector:
app: mynginx
org: example.com
team: foo
---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
annotations:
note: This is a test annotation
labels:
app: mynginx
org: example.com
team: foo
name: team-foo-nginx
spec:
selector:
matchLabels:
app: mynginx
org: example.com
team: foo
template:
metadata:
annotations:
note: This is a test annotation
labels:
app: mynginx
org: example.com
team: foo
spec:
containers:
- image: nginx
name: nginx
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
annotations:
note: This is a test annotation
labels:
app: mynginx
org: example.com
team: foo
name: team-foo-nginx
spec:
ingress:
- from:
- podSelector:
matchLabels:
app: mynginx
org: example.com
team: foo
podSelector:
matchExpressions:
- key: app
operator: In
values:
- test
`)
}
func makeBaseWithGenerators(th *kusttest_test.KustTestHarness) {
th.WriteK("/app", `
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namePrefix: team-foo-
commonLabels:
app: mynginx
org: example.com
team: foo
commonAnnotations:
note: This is a test annotation
resources:
- deployment.yaml
- service.yaml
configMapGenerator:
- name: configmap-in-base
literals:
- foo=bar
secretGenerator:
- name: secret-in-base
literals:
- username=admin
- password=somepw
`)
th.WriteF("/app/deployment.yaml", `
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: nginx
labels:
app: nginx
spec:
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
volumeMounts:
- name: nginx-persistent-storage
mountPath: /tmp/ps
volumes:
- name: nginx-persistent-storage
emptyDir: {}
- configMap:
name: configmap-in-base
name: configmap-in-base
`)
th.WriteF("/app/service.yaml", `
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- port: 80
selector:
app: nginx
`)
}
func TestBaseWithGeneratorsAlone(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app")
makeBaseWithGenerators(th)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1beta2
kind: Deployment
metadata:
annotations:
note: This is a test annotation
labels:
app: mynginx
org: example.com
team: foo
name: team-foo-nginx
spec:
selector:
matchLabels:
app: mynginx
org: example.com
team: foo
template:
metadata:
annotations:
note: This is a test annotation
labels:
app: mynginx
org: example.com
team: foo
spec:
containers:
- image: nginx
name: nginx
volumeMounts:
- mountPath: /tmp/ps
name: nginx-persistent-storage
volumes:
- emptyDir: {}
name: nginx-persistent-storage
- configMap:
name: team-foo-configmap-in-base-bbdmdh7m8t
name: configmap-in-base
---
apiVersion: v1
kind: Service
metadata:
annotations:
note: This is a test annotation
labels:
app: mynginx
org: example.com
team: foo
name: team-foo-nginx
spec:
ports:
- port: 80
selector:
app: mynginx
org: example.com
team: foo
---
apiVersion: v1
data:
foo: bar
kind: ConfigMap
metadata:
annotations:
note: This is a test annotation
labels:
app: mynginx
org: example.com
team: foo
name: team-foo-configmap-in-base-bbdmdh7m8t
---
apiVersion: v1
data:
password: c29tZXB3
username: YWRtaW4=
kind: Secret
metadata:
annotations:
note: This is a test annotation
labels:
app: mynginx
org: example.com
team: foo
name: team-foo-secret-in-base-tkm7hhtf8d
type: Opaque
`)
}
func TestMergeAndReplaceGenerators(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/overlay")
makeBaseWithGenerators(th)
th.WriteF("/overlay/deployment.yaml", `
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
volumes:
- name: nginx-persistent-storage
emptyDir: null
gcePersistentDisk:
pdName: nginx-persistent-storage
- configMap:
name: configmap-in-overlay
name: configmap-in-overlay
`)
th.WriteK("/overlay", `
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namePrefix: staging-
commonLabels:
env: staging
team: override-foo
patchesStrategicMerge:
- deployment.yaml
resources:
- ../app
configMapGenerator:
- name: configmap-in-overlay
literals:
- hello=world
- name: configmap-in-base
behavior: replace
literals:
- foo=override-bar
secretGenerator:
- name: secret-in-base
behavior: merge
literals:
- proxy=haproxy
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1beta2
kind: Deployment
metadata:
annotations:
note: This is a test annotation
labels:
app: mynginx
env: staging
org: example.com
team: override-foo
name: staging-team-foo-nginx
spec:
selector:
matchLabels:
app: mynginx
env: staging
org: example.com
team: override-foo
template:
metadata:
annotations:
note: This is a test annotation
labels:
app: mynginx
env: staging
org: example.com
team: override-foo
spec:
containers:
- image: nginx
name: nginx
volumeMounts:
- mountPath: /tmp/ps
name: nginx-persistent-storage
volumes:
- gcePersistentDisk:
pdName: nginx-persistent-storage
name: nginx-persistent-storage
- configMap:
name: staging-configmap-in-overlay-k7cbc75tg8
name: configmap-in-overlay
- configMap:
name: staging-team-foo-configmap-in-base-gh9d7t85gb
name: configmap-in-base
---
apiVersion: v1
kind: Service
metadata:
annotations:
note: This is a test annotation
labels:
app: mynginx
env: staging
org: example.com
team: override-foo
name: staging-team-foo-nginx
spec:
ports:
- port: 80
selector:
app: mynginx
env: staging
org: example.com
team: override-foo
---
apiVersion: v1
data:
foo: override-bar
kind: ConfigMap
metadata:
annotations:
note: This is a test annotation
labels:
app: mynginx
env: staging
org: example.com
team: override-foo
name: staging-team-foo-configmap-in-base-gh9d7t85gb
---
apiVersion: v1
data:
password: c29tZXB3
proxy: aGFwcm94eQ==
username: YWRtaW4=
kind: Secret
metadata:
annotations:
note: This is a test annotation
labels:
app: mynginx
env: staging
org: example.com
team: override-foo
name: staging-team-foo-secret-in-base-c8db7gk2m2
type: Opaque
---
apiVersion: v1
data:
hello: world
kind: ConfigMap
metadata:
labels:
env: staging
team: override-foo
name: staging-configmap-in-overlay-k7cbc75tg8
`)
}
func TestGeneratingIntoNamespaces(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app")
th.WriteK("/app", `
configMapGenerator:
- name: test
namespace: default
literals:
- key=value
- name: test
namespace: kube-system
literals:
- key=value
secretGenerator:
- name: test
namespace: default
literals:
- username=admin
- password=somepw
- name: test
namespace: kube-system
literals:
- username=admin
- password=somepw
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: v1
data:
key: value
kind: ConfigMap
metadata:
name: test-t5t4md8fdm
namespace: default
---
apiVersion: v1
data:
key: value
kind: ConfigMap
metadata:
name: test-t5t4md8fdm
namespace: kube-system
---
apiVersion: v1
data:
password: c29tZXB3
username: YWRtaW4=
kind: Secret
metadata:
name: test-h65t9hg6kc
namespace: default
type: Opaque
---
apiVersion: v1
data:
password: c29tZXB3
username: YWRtaW4=
kind: Secret
metadata:
name: test-h65t9hg6kc
namespace: kube-system
type: Opaque
`)
}
// Valid that conflict is detected is the name are identical
// and namespace left to default
func TestConfigMapGeneratingIntoSameNamespace(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app")
th.WriteK("/app", `
configMapGenerator:
- name: test
namespace: default
literals:
- key=value
- name: test
literals:
- key=value
`)
_, err := th.MakeKustTarget().MakeCustomizedResMap()
if err == nil {
t.Fatalf("expected error")
}
if !strings.Contains(err.Error(), "must merge or replace") {
t.Fatalf("unexpected error %v", err)
}
}
// Valid that conflict is detected is the name are identical
// and namespace left to default
func TestSecretGeneratingIntoSameNamespace(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app")
th.WriteK("/app", `
secretGenerator:
- name: test
namespace: default
literals:
- username=admin
- password=somepw
- name: test
literals:
- username=admin
- password=somepw
`)
_, err := th.MakeKustTarget().MakeCustomizedResMap()
if err == nil {
t.Fatalf("expected error")
}
if !strings.Contains(err.Error(), "must merge or replace") {
t.Fatalf("unexpected error %v", err)
}
}

View File

@@ -0,0 +1,115 @@
/*
Copyright 2019 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 target_test
import (
"testing"
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
)
func TestSecretGenerator(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app")
th.WriteK("/app", `
secretGenerator:
- name: bob
literals:
- FRUIT=apple
- VEGETABLE=carrot
files:
- foo.env
- passphrase=phrase.dat
envs:
- foo.env
`)
th.WriteF("/app/foo.env", `
MOUNTAIN=everest
OCEAN=pacific
`)
th.WriteF("/app/phrase.dat", "dat phrase")
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: v1
data:
FRUIT: YXBwbGU=
MOUNTAIN: ZXZlcmVzdA==
OCEAN: cGFjaWZpYw==
VEGETABLE: Y2Fycm90
foo.env: Ck1PVU5UQUlOPWV2ZXJlc3QKT0NFQU49cGFjaWZpYwo=
passphrase: ZGF0IHBocmFzZQ==
kind: Secret
metadata:
name: bob-kf5c9fccbt
type: Opaque
`)
}
func TestGeneratorOptionsWithBases(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/overlay")
th.WriteK("/app/base", `
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
generatorOptions:
disableNameSuffixHash: true
labels:
foo: bar
configMapGenerator:
- name: shouldNotHaveHash
literals:
- foo=bar
`)
th.WriteK("/app/overlay", `
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
generatorOptions:
disableNameSuffixHash: false
labels:
fruit: apple
configMapGenerator:
- name: shouldHaveHash
literals:
- fruit=apple
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: v1
data:
foo: bar
kind: ConfigMap
metadata:
labels:
foo: bar
name: shouldNotHaveHash
---
apiVersion: v1
data:
fruit: apple
kind: ConfigMap
metadata:
labels:
fruit: apple
name: shouldHaveHash-2k9hc848ff
`)
}

View File

@@ -0,0 +1,244 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package target_test
import (
"testing"
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
)
func makeResourcesForPatchTest(th *kusttest_test.KustTestHarness) {
th.WriteF("/app/base/deployment.yaml", `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
labels:
app: nginx
spec:
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
volumeMounts:
- name: nginx-persistent-storage
mountPath: /tmp/ps
volumes:
- name: nginx-persistent-storage
emptyDir: {}
- configMap:
name: configmap-in-base
name: configmap-in-base
`)
}
func TestStrategicMergePatchInline(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/base")
makeResourcesForPatchTest(th)
th.WriteK("/app/base", `
resources:
- deployment.yaml
patchesStrategicMerge:
- |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- name: nginx
image: image1
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx
name: nginx
spec:
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: image1
name: nginx
volumeMounts:
- mountPath: /tmp/ps
name: nginx-persistent-storage
volumes:
- emptyDir: {}
name: nginx-persistent-storage
- configMap:
name: configmap-in-base
name: configmap-in-base
`)
}
func TestJSONPatchInline(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/base")
makeResourcesForPatchTest(th)
th.WriteK("/app/base", `
resources:
- deployment.yaml
patchesJson6902:
- target:
group: apps
version: v1
kind: Deployment
name: nginx
patch: |-
- op: replace
path: /spec/template/spec/containers/0/image
value: image1
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx
name: nginx
spec:
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: image1
name: nginx
volumeMounts:
- mountPath: /tmp/ps
name: nginx-persistent-storage
volumes:
- emptyDir: {}
name: nginx-persistent-storage
- configMap:
name: configmap-in-base
name: configmap-in-base
`)
}
func TestExtendedPatchInlineJSON(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/base")
makeResourcesForPatchTest(th)
th.WriteK("/app/base", `
resources:
- deployment.yaml
patches:
- target:
kind: Deployment
name: nginx
patch: |-
- op: replace
path: /spec/template/spec/containers/0/image
value: image1
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx
name: nginx
spec:
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: image1
name: nginx
volumeMounts:
- mountPath: /tmp/ps
name: nginx-persistent-storage
volumes:
- emptyDir: {}
name: nginx-persistent-storage
- configMap:
name: configmap-in-base
name: configmap-in-base
`)
}
func TestExtendedPatchInlineYAML(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/base")
makeResourcesForPatchTest(th)
th.WriteK("/app/base", `
resources:
- deployment.yaml
patches:
- target:
kind: Deployment
name: nginx
patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- name: nginx
image: image1
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx
name: nginx
spec:
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: image1
name: nginx
volumeMounts:
- mountPath: /tmp/ps
name: nginx-persistent-storage
volumes:
- emptyDir: {}
name: nginx-persistent-storage
- configMap:
name: configmap-in-base
name: configmap-in-base
`)
}

389
api/target/kusttarget.go Normal file
View File

@@ -0,0 +1,389 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package target implements state for the set of all
// resources to customize.
package target
import (
"bytes"
"encoding/json"
"fmt"
"log"
"strings"
"github.com/pkg/errors"
"sigs.k8s.io/kustomize/v3/api/accumulator"
"sigs.k8s.io/kustomize/v3/api/builtinconfig"
"sigs.k8s.io/kustomize/v3/api/ifc"
"sigs.k8s.io/kustomize/v3/api/pgmconfig"
"sigs.k8s.io/kustomize/v3/api/plugins/builtins"
"sigs.k8s.io/kustomize/v3/api/plugins/loader"
"sigs.k8s.io/kustomize/v3/api/resmap"
"sigs.k8s.io/kustomize/v3/api/transform"
"sigs.k8s.io/kustomize/v3/api/types"
"sigs.k8s.io/kustomize/v3/plugin/builtin"
"sigs.k8s.io/yaml"
)
// KustTarget encapsulates the entirety of a kustomization build.
type KustTarget struct {
kustomization *types.Kustomization
ldr ifc.Loader
validator ifc.Validator
rFactory *resmap.Factory
tFactory resmap.PatchFactory
pLdr *loader.Loader
}
// NewKustTarget returns a new instance of KustTarget primed with a Loader.
func NewKustTarget(
ldr ifc.Loader,
validator ifc.Validator,
rFactory *resmap.Factory,
tFactory resmap.PatchFactory,
pLdr *loader.Loader) (*KustTarget, error) {
content, err := loadKustFile(ldr)
if err != nil {
return nil, err
}
content = types.FixKustomizationPreUnmarshalling(content)
var k types.Kustomization
err = unmarshal(content, &k)
if err != nil {
return nil, err
}
k.FixKustomizationPostUnmarshalling()
errs := k.EnforceFields()
if len(errs) > 0 {
return nil, fmt.Errorf(
"Failed to read kustomization file under %s:\n"+
strings.Join(errs, "\n"), ldr.Root())
}
return &KustTarget{
kustomization: &k,
ldr: ldr,
validator: validator,
rFactory: rFactory,
tFactory: tFactory,
pLdr: pLdr,
}, nil
}
func quoted(l []string) []string {
r := make([]string, len(l))
for i, v := range l {
r[i] = "'" + v + "'"
}
return r
}
func commaOr(q []string) string {
return strings.Join(q[:len(q)-1], ", ") + " or " + q[len(q)-1]
}
func loadKustFile(ldr ifc.Loader) ([]byte, error) {
var content []byte
match := 0
for _, kf := range pgmconfig.RecognizedKustomizationFileNames() {
c, err := ldr.Load(kf)
if err == nil {
match += 1
content = c
}
}
switch match {
case 0:
return nil, fmt.Errorf(
"unable to find one of %v in directory '%s'",
commaOr(quoted(pgmconfig.RecognizedKustomizationFileNames())),
ldr.Root())
case 1:
return content, nil
default:
return nil, fmt.Errorf(
"Found multiple kustomization files under: %s\n", ldr.Root())
}
}
func unmarshal(y []byte, o interface{}) error {
j, err := yaml.YAMLToJSON(y)
if err != nil {
return err
}
dec := json.NewDecoder(bytes.NewReader(j))
dec.DisallowUnknownFields()
return dec.Decode(o)
}
// MakeCustomizedResMap creates a ResMap per kustomization instructions.
// The Resources in the returned ResMap are fully customized.
func (kt *KustTarget) MakeCustomizedResMap() (resmap.ResMap, error) {
return kt.makeCustomizedResMap(types.GarbageIgnore)
}
func (kt *KustTarget) MakePruneConfigMap() (resmap.ResMap, error) {
return kt.makeCustomizedResMap(types.GarbageCollect)
}
func (kt *KustTarget) makeCustomizedResMap(
garbagePolicy types.GarbagePolicy) (resmap.ResMap, error) {
ra, err := kt.AccumulateTarget()
if err != nil {
return nil, err
}
// The following steps must be done last, not as part of
// the recursion implicit in AccumulateTarget.
err = kt.addHashesToNames(ra)
if err != nil {
return nil, err
}
// Given that names have changed (prefixs/suffixes added),
// fix all the back references to those names.
err = ra.FixBackReferences()
if err != nil {
return nil, err
}
// With all the back references fixed, it's OK to resolve Vars.
err = ra.ResolveVars()
if err != nil {
return nil, err
}
err = kt.computeInventory(ra, garbagePolicy)
if err != nil {
return nil, err
}
return ra.ResMap(), nil
}
func (kt *KustTarget) addHashesToNames(
ra *accumulator.ResAccumulator) error {
p := builtin.NewHashTransformerPlugin()
err := kt.configureBuiltinPlugin(p, nil, builtins.HashTransformer)
if err != nil {
return err
}
return ra.Transform(p)
}
func (kt *KustTarget) computeInventory(
ra *accumulator.ResAccumulator, garbagePolicy types.GarbagePolicy) error {
inv := kt.kustomization.Inventory
if inv == nil {
return nil
}
if inv.Type != "ConfigMap" {
return fmt.Errorf("don't know how to do that")
}
if inv.ConfigMap.Namespace != kt.kustomization.Namespace {
return fmt.Errorf("namespace mismatch")
}
var c struct {
Policy string
types.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
c.Name = inv.ConfigMap.Name
c.Namespace = inv.ConfigMap.Namespace
c.Policy = garbagePolicy.String()
p := builtin.NewInventoryTransformerPlugin()
err := kt.configureBuiltinPlugin(p, c, builtins.InventoryTransformer)
if err != nil {
return err
}
return ra.Transform(p)
}
func (kt *KustTarget) shouldAddHashSuffixesToGeneratedResources() bool {
return kt.kustomization.GeneratorOptions == nil ||
!kt.kustomization.GeneratorOptions.DisableNameSuffixHash
}
// AccumulateTarget returns a new ResAccumulator,
// holding customized resources and the data/rules used
// to do so. The name back references and vars are
// not yet fixed.
func (kt *KustTarget) AccumulateTarget() (
ra *accumulator.ResAccumulator, err error) {
ra = accumulator.MakeEmptyAccumulator()
err = kt.accumulateResources(ra, kt.kustomization.Resources)
if err != nil {
return nil, errors.Wrap(err, "accumulating resources")
}
tConfig, err := builtinconfig.MakeTransformerConfig(
kt.ldr, kt.kustomization.Configurations)
if err != nil {
return nil, err
}
err = ra.MergeConfig(tConfig)
if err != nil {
return nil, errors.Wrapf(
err, "merging config %v", tConfig)
}
crdTc, err := accumulator.LoadConfigFromCRDs(kt.ldr, kt.kustomization.Crds)
if err != nil {
return nil, errors.Wrapf(
err, "loading CRDs %v", kt.kustomization.Crds)
}
err = ra.MergeConfig(crdTc)
if err != nil {
return nil, errors.Wrapf(
err, "merging CRDs %v", crdTc)
}
err = kt.runGenerators(ra)
if err != nil {
return nil, err
}
err = kt.runTransformers(ra)
if err != nil {
return nil, err
}
err = ra.MergeVars(kt.kustomization.Vars)
if err != nil {
return nil, errors.Wrapf(
err, "merging vars %v", kt.kustomization.Vars)
}
return ra, nil
}
func (kt *KustTarget) runGenerators(
ra *accumulator.ResAccumulator) error {
var generators []resmap.Generator
gs, err := kt.configureBuiltinGenerators()
if err != nil {
return err
}
generators = append(generators, gs...)
gs, err = kt.configureExternalGenerators()
if err != nil {
return errors.Wrap(err, "loading generator plugins")
}
generators = append(generators, gs...)
for _, g := range generators {
resMap, err := g.Generate()
if err != nil {
return err
}
err = ra.AbsorbAll(resMap)
if err != nil {
return errors.Wrapf(err, "merging from generator %v", g)
}
}
return nil
}
func (kt *KustTarget) configureExternalGenerators() ([]resmap.Generator, error) {
ra := accumulator.MakeEmptyAccumulator()
err := kt.accumulateResources(ra, kt.kustomization.Generators)
if err != nil {
return nil, err
}
return kt.pLdr.LoadGenerators(kt.ldr, kt.validator, ra.ResMap())
}
func (kt *KustTarget) runTransformers(ra *accumulator.ResAccumulator) error {
var r []resmap.Transformer
tConfig := ra.GetTransformerConfig()
lts, err := kt.configureBuiltinTransformers(tConfig)
if err != nil {
return err
}
r = append(r, lts...)
lts, err = kt.configureExternalTransformers()
if err != nil {
return err
}
r = append(r, lts...)
t := transform.NewMultiTransformer(r)
return ra.Transform(t)
}
func (kt *KustTarget) configureExternalTransformers() ([]resmap.Transformer, error) {
ra := accumulator.MakeEmptyAccumulator()
err := kt.accumulateResources(ra, kt.kustomization.Transformers)
if err != nil {
return nil, err
}
return kt.pLdr.LoadTransformers(kt.ldr, kt.validator, ra.ResMap())
}
// accumulateResources fills the given resourceAccumulator
// with resources read from the given list of paths.
func (kt *KustTarget) accumulateResources(
ra *accumulator.ResAccumulator, paths []string) error {
for _, path := range paths {
ldr, err := kt.ldr.New(path)
if err == nil {
err = kt.accumulateDirectory(ra, ldr, path)
if err != nil {
return err
}
} else {
err2 := kt.accumulateFile(ra, path)
if err2 != nil {
// Log ldr.New() error to highlight git failures.
log.Print(err.Error())
return err2
}
}
}
return nil
}
func (kt *KustTarget) accumulateDirectory(
ra *accumulator.ResAccumulator, ldr ifc.Loader, path string) error {
defer ldr.Cleanup()
subKt, err := NewKustTarget(
ldr, kt.validator, kt.rFactory, kt.tFactory, kt.pLdr)
if err != nil {
return errors.Wrapf(err, "couldn't make target for path '%s'", path)
}
subRa, err := subKt.AccumulateTarget()
if err != nil {
return errors.Wrapf(
err, "recursed accumulation of path '%s'", path)
}
err = ra.MergeAccumulator(subRa)
if err != nil {
return errors.Wrapf(
err, "recursed merging from path '%s'", path)
}
return nil
}
func (kt *KustTarget) accumulateFile(
ra *accumulator.ResAccumulator, path string) error {
resources, err := kt.rFactory.FromFile(kt.ldr, path)
if err != nil {
return errors.Wrapf(err, "accumulating resources from '%s'", path)
}
err = ra.AppendAll(resources)
if err != nil {
return errors.Wrapf(err, "merging resources from '%s'", path)
}
return nil
}
func (kt *KustTarget) configureBuiltinPlugin(
p resmap.Configurable, c interface{}, bpt builtins.BuiltinPluginType) (err error) {
var y []byte
if c != nil {
y, err = yaml.Marshal(c)
if err != nil {
return errors.Wrapf(
err, "builtin %s marshal", bpt)
}
}
err = p.Config(resmap.NewPluginHelpers(kt.ldr, kt.validator, kt.rFactory), y)
if err != nil {
return errors.Wrapf(err, "builtin %s config: %v", bpt, y)
}
return nil
}

View File

@@ -0,0 +1,297 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package target
import (
"sigs.k8s.io/kustomize/v3/api/builtinconfig"
"sigs.k8s.io/kustomize/v3/api/plugins/builtins"
"sigs.k8s.io/kustomize/v3/api/resmap"
"sigs.k8s.io/kustomize/v3/api/types"
)
// Functions dedicated to configuring the builtin
// transformer and generator plugins using config data
// read from a kustomization file and from the
// config.TransformerConfig, whose data may be a
// mix of hardcoded values and data read from file.
//
// Non-builtin plugins will get their configuration
// from their own dedicated structs and YAML files.
//
// There are some loops in the functions below because
// the kustomization file would, say, allow someone to
// request multiple secrets be made, or run multiple
// image tag transforms. In these cases, we'll need
// N plugin instances with differing configurations.
func (kt *KustTarget) configureBuiltinGenerators() (
result []resmap.Generator, err error) {
for _, bpt := range []builtins.BuiltinPluginType{
builtins.ConfigMapGenerator,
builtins.SecretGenerator,
} {
r, err := generatorConfigurators[bpt](
kt, bpt, builtins.GeneratorFactories[bpt])
if err != nil {
return nil, err
}
result = append(result, r...)
}
return result, nil
}
func (kt *KustTarget) configureBuiltinTransformers(
tc *builtinconfig.TransformerConfig) (
result []resmap.Transformer, err error) {
for _, bpt := range []builtins.BuiltinPluginType{
builtins.PatchStrategicMergeTransformer,
builtins.PatchTransformer,
builtins.NamespaceTransformer,
builtins.PrefixSuffixTransformer,
builtins.LabelTransformer,
builtins.AnnotationsTransformer,
builtins.PatchJson6902Transformer,
builtins.ReplicaCountTransformer,
builtins.ImageTagTransformer,
} {
r, err := transformerConfigurators[bpt](
kt, bpt, builtins.TransformerFactories[bpt], tc)
if err != nil {
return nil, err
}
result = append(result, r...)
}
return result, nil
}
type gFactory func() resmap.GeneratorPlugin
var generatorConfigurators = map[builtins.BuiltinPluginType]func(
kt *KustTarget,
bpt builtins.BuiltinPluginType,
factory gFactory) (result []resmap.Generator, err error){
builtins.SecretGenerator: func(kt *KustTarget, bpt builtins.BuiltinPluginType, f gFactory) (
result []resmap.Generator, err error) {
var c struct {
types.GeneratorOptions
types.SecretArgs
}
if kt.kustomization.GeneratorOptions != nil {
c.GeneratorOptions = *kt.kustomization.GeneratorOptions
}
for _, args := range kt.kustomization.SecretGenerator {
c.SecretArgs = args
p := f()
err := kt.configureBuiltinPlugin(p, c, bpt)
if err != nil {
return nil, err
}
result = append(result, p)
}
return
},
builtins.ConfigMapGenerator: func(kt *KustTarget, bpt builtins.BuiltinPluginType, f gFactory) (
result []resmap.Generator, err error) {
var c struct {
types.GeneratorOptions
types.ConfigMapArgs
}
if kt.kustomization.GeneratorOptions != nil {
c.GeneratorOptions = *kt.kustomization.GeneratorOptions
}
for _, args := range kt.kustomization.ConfigMapGenerator {
c.ConfigMapArgs = args
p := f()
err := kt.configureBuiltinPlugin(p, c, bpt)
if err != nil {
return nil, err
}
result = append(result, p)
}
return
},
}
type tFactory func() resmap.TransformerPlugin
var transformerConfigurators = map[builtins.BuiltinPluginType]func(
kt *KustTarget,
bpt builtins.BuiltinPluginType,
f tFactory,
tc *builtinconfig.TransformerConfig) (result []resmap.Transformer, err error){
builtins.NamespaceTransformer: func(
kt *KustTarget, bpt builtins.BuiltinPluginType, f tFactory, tc *builtinconfig.TransformerConfig) (
result []resmap.Transformer, err error) {
var c struct {
types.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"`
FieldSpecs []types.FieldSpec
}
c.Namespace = kt.kustomization.Namespace
c.FieldSpecs = tc.NameSpace
p := f()
err = kt.configureBuiltinPlugin(p, c, bpt)
if err != nil {
return nil, err
}
result = append(result, p)
return
},
builtins.PatchJson6902Transformer: func(
kt *KustTarget, bpt builtins.BuiltinPluginType, f tFactory, _ *builtinconfig.TransformerConfig) (
result []resmap.Transformer, err error) {
var c struct {
Target types.PatchTarget `json:"target,omitempty" yaml:"target,omitempty"`
Path string `json:"path,omitempty" yaml:"path,omitempty"`
JsonOp string `json:"jsonOp,omitempty" yaml:"jsonOp,omitempty"`
}
for _, args := range kt.kustomization.PatchesJson6902 {
c.Target = *args.Target
c.Path = args.Path
c.JsonOp = args.Patch
p := f()
err = kt.configureBuiltinPlugin(p, c, bpt)
if err != nil {
return nil, err
}
result = append(result, p)
}
return
},
builtins.PatchStrategicMergeTransformer: func(
kt *KustTarget, bpt builtins.BuiltinPluginType, f tFactory, _ *builtinconfig.TransformerConfig) (
result []resmap.Transformer, err error) {
if len(kt.kustomization.PatchesStrategicMerge) == 0 {
return
}
var c struct {
Paths []types.PatchStrategicMerge `json:"paths,omitempty" yaml:"paths,omitempty"`
Patches string `json:"patches,omitempty" yaml:"patches,omitempty"`
}
c.Paths = kt.kustomization.PatchesStrategicMerge
p := f()
err = kt.configureBuiltinPlugin(p, c, bpt)
if err != nil {
return nil, err
}
result = append(result, p)
return
},
builtins.PatchTransformer: func(
kt *KustTarget, bpt builtins.BuiltinPluginType, f tFactory, _ *builtinconfig.TransformerConfig) (
result []resmap.Transformer, err error) {
if len(kt.kustomization.Patches) == 0 {
return
}
var c struct {
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"`
}
for _, pc := range kt.kustomization.Patches {
c.Target = pc.Target
c.Patch = pc.Patch
c.Path = pc.Path
p := f()
err = kt.configureBuiltinPlugin(p, c, bpt)
if err != nil {
return nil, err
}
result = append(result, p)
}
return
},
builtins.LabelTransformer: func(
kt *KustTarget, bpt builtins.BuiltinPluginType, f tFactory, tc *builtinconfig.TransformerConfig) (
result []resmap.Transformer, err error) {
var c struct {
Labels map[string]string
FieldSpecs []types.FieldSpec
}
c.Labels = kt.kustomization.CommonLabels
c.FieldSpecs = tc.CommonLabels
p := f()
err = kt.configureBuiltinPlugin(p, c, bpt)
if err != nil {
return nil, err
}
result = append(result, p)
return
},
builtins.AnnotationsTransformer: func(
kt *KustTarget, bpt builtins.BuiltinPluginType, f tFactory, tc *builtinconfig.TransformerConfig) (
result []resmap.Transformer, err error) {
var c struct {
Annotations map[string]string
FieldSpecs []types.FieldSpec
}
c.Annotations = kt.kustomization.CommonAnnotations
c.FieldSpecs = tc.CommonAnnotations
p := f()
err = kt.configureBuiltinPlugin(p, c, bpt)
if err != nil {
return nil, err
}
result = append(result, p)
return
},
builtins.PrefixSuffixTransformer: func(
kt *KustTarget, bpt builtins.BuiltinPluginType, f tFactory, tc *builtinconfig.TransformerConfig) (
result []resmap.Transformer, err error) {
var c struct {
Prefix string
Suffix string
FieldSpecs []types.FieldSpec
}
c.Prefix = kt.kustomization.NamePrefix
c.Suffix = kt.kustomization.NameSuffix
c.FieldSpecs = tc.NamePrefix
p := f()
err = kt.configureBuiltinPlugin(p, c, bpt)
if err != nil {
return nil, err
}
result = append(result, p)
return
},
builtins.ImageTagTransformer: func(
kt *KustTarget, bpt builtins.BuiltinPluginType, f tFactory, tc *builtinconfig.TransformerConfig) (
result []resmap.Transformer, err error) {
var c struct {
ImageTag types.Image
FieldSpecs []types.FieldSpec
}
for _, args := range kt.kustomization.Images {
c.ImageTag = args
c.FieldSpecs = tc.Images
p := f()
err = kt.configureBuiltinPlugin(p, c, bpt)
if err != nil {
return nil, err
}
result = append(result, p)
}
return
},
builtins.ReplicaCountTransformer: func(
kt *KustTarget, bpt builtins.BuiltinPluginType, f tFactory, tc *builtinconfig.TransformerConfig) (
result []resmap.Transformer, err error) {
var c struct {
Replica types.Replica
FieldSpecs []types.FieldSpec
}
for _, args := range kt.kustomization.Replicas {
c.Replica = args
c.FieldSpecs = tc.Replicas
p := f()
err = kt.configureBuiltinPlugin(p, c, bpt)
if err != nil {
return nil, err
}
result = append(result, p)
}
return
},
}

View File

@@ -0,0 +1,446 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package target_test
import (
"encoding/base64"
"fmt"
"strings"
"testing"
"sigs.k8s.io/kustomize/v3/api/ifc"
"sigs.k8s.io/kustomize/v3/api/resid"
"sigs.k8s.io/kustomize/v3/api/resmap"
"sigs.k8s.io/kustomize/v3/api/resource"
. "sigs.k8s.io/kustomize/v3/api/target"
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
"sigs.k8s.io/kustomize/v3/api/testutils/valtest"
"sigs.k8s.io/kustomize/v3/api/types"
"sigs.k8s.io/kustomize/v3/internal/loadertest"
)
const (
kustomizationContent = `
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namePrefix: foo-
nameSuffix: -bar
namespace: ns1
commonLabels:
app: nginx
commonAnnotations:
note: This is a test annotation
resources:
- deployment.yaml
- namespace.yaml
generatorOptions:
disableNameSuffixHash: false
configMapGenerator:
- name: literalConfigMap
literals:
- DB_USERNAME=admin
- DB_PASSWORD=somepw
secretGenerator:
- name: secret
literals:
- DB_USERNAME=admin
- DB_PASSWORD=somepw
type: Opaque
patchesJson6902:
- target:
group: apps
version: v1
kind: Deployment
name: dply1
path: jsonpatch.json
`
deploymentContent = `
apiVersion: apps/v1
metadata:
name: dply1
kind: Deployment
`
namespaceContent = `
apiVersion: v1
kind: Namespace
metadata:
name: ns1
`
jsonpatchContent = `[
{"op": "add", "path": "/spec/replica", "value": "3"}
]`
)
func TestResources(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/whatever")
th.WriteK("/whatever/", kustomizationContent)
th.WriteF("/whatever/deployment.yaml", deploymentContent)
th.WriteF("/whatever/namespace.yaml", namespaceContent)
th.WriteF("/whatever/jsonpatch.json", jsonpatchContent)
resources := []*resource.Resource{
th.RF().FromMapWithName("dply1", map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "foo-dply1-bar",
"namespace": "ns1",
"labels": map[string]interface{}{
"app": "nginx",
},
"annotations": map[string]interface{}{
"note": "This is a test annotation",
},
},
"spec": map[string]interface{}{
"replica": "3",
"selector": map[string]interface{}{
"matchLabels": map[string]interface{}{
"app": "nginx",
},
},
"template": map[string]interface{}{
"metadata": map[string]interface{}{
"annotations": map[string]interface{}{
"note": "This is a test annotation",
},
"labels": map[string]interface{}{
"app": "nginx",
},
},
},
},
}),
th.RF().FromMapWithName("ns1", map[string]interface{}{
"apiVersion": "v1",
"kind": "Namespace",
"metadata": map[string]interface{}{
"name": "foo-ns1-bar",
"labels": map[string]interface{}{
"app": "nginx",
},
"annotations": map[string]interface{}{
"note": "This is a test annotation",
},
},
}),
th.RF().FromMapWithName("literalConfigMap",
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "foo-literalConfigMap-bar-8d2dkb8k24",
"namespace": "ns1",
"labels": map[string]interface{}{
"app": "nginx",
},
"annotations": map[string]interface{}{
"note": "This is a test annotation",
},
},
"data": map[string]interface{}{
"DB_USERNAME": "admin",
"DB_PASSWORD": "somepw",
},
}),
th.RF().FromMapWithName("secret",
map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "foo-secret-bar-9btc7bt4kb",
"namespace": "ns1",
"labels": map[string]interface{}{
"app": "nginx",
},
"annotations": map[string]interface{}{
"note": "This is a test annotation",
},
},
"type": ifc.SecretTypeOpaque,
"data": map[string]interface{}{
"DB_USERNAME": base64.StdEncoding.EncodeToString([]byte("admin")),
"DB_PASSWORD": base64.StdEncoding.EncodeToString([]byte("somepw")),
},
}),
}
expected := resmap.New()
for _, r := range resources {
if err := expected.Append(r); err != nil {
t.Fatalf("unexpected error %v", err)
}
}
actual, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("unexpected Resources error %v", err)
}
if err = expected.ErrorIfNotEqualLists(actual); err != nil {
t.Fatalf("unexpected inequality: %v", err)
}
}
func TestKustomizationNotFound(t *testing.T) {
_, err := NewKustTarget(
loadertest.NewFakeLoader("/foo"),
valtest_test.MakeFakeValidator(), nil, nil, nil)
if err == nil {
t.Fatalf("expected an error")
}
if err.Error() !=
`unable to find one of 'kustomization.yaml', 'kustomization.yml' or 'Kustomization' in directory '/foo'` {
t.Fatalf("unexpected error: %q", err)
}
}
func TestResourceNotFound(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/whatever")
th.WriteK("/whatever", kustomizationContent)
_, err := th.MakeKustTarget().MakeCustomizedResMap()
if err == nil {
t.Fatalf("Didn't get the expected error for an unknown resource")
}
if !strings.Contains(err.Error(), `cannot read file`) {
t.Fatalf("unexpected error: %q", err)
}
}
func findSecret(m resmap.ResMap) *resource.Resource {
for _, r := range m.Resources() {
if r.OrgId().Kind == "Secret" {
return r
}
}
return nil
}
func TestDisableNameSuffixHash(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/whatever")
th.WriteK("/whatever/", kustomizationContent)
th.WriteF("/whatever/deployment.yaml", deploymentContent)
th.WriteF("/whatever/namespace.yaml", namespaceContent)
th.WriteF("/whatever/jsonpatch.json", jsonpatchContent)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("unexpected Resources error %v", err)
}
secret := findSecret(m)
if secret == nil {
t.Errorf("Expected to find a Secret")
}
if secret.GetName() != "foo-secret-bar-9btc7bt4kb" {
t.Errorf("unexpected secret resource name: %s", secret.GetName())
}
th.WriteK("/whatever/",
strings.Replace(kustomizationContent,
"disableNameSuffixHash: false",
"disableNameSuffixHash: true", -1))
m, err = th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("unexpected Resources error %v", err)
}
secret = findSecret(m)
if secret == nil {
t.Errorf("Expected to find a Secret")
}
if secret.GetName() != "foo-secret-bar" { // No hash at end.
t.Errorf("unexpected secret resource name: %s", secret.GetName())
}
}
func TestIssue596AllowDirectoriesThatAreSubstringsOfEachOther(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/overlays/aws-sandbox2.us-east-1")
th.WriteK("/app/base", "")
th.WriteK("/app/overlays/aws", `
resources:
- ../../base
`)
th.WriteK("/app/overlays/aws-nonprod", `
resources:
- ../aws
`)
th.WriteK("/app/overlays/aws-sandbox2.us-east-1", `
resources:
- ../aws-nonprod
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, "")
}
// To simplify tests, these vars specified in alphabetical order.
var someVars = []types.Var{
{
Name: "AWARD",
ObjRef: types.Target{
APIVersion: "v7",
Gvk: resid.Gvk{Kind: "Service"},
Name: "nobelPrize"},
FieldRef: types.FieldSelector{FieldPath: "some.arbitrary.path"},
},
{
Name: "BIRD",
ObjRef: types.Target{
APIVersion: "v300",
Gvk: resid.Gvk{Kind: "Service"},
Name: "heron"},
FieldRef: types.FieldSelector{FieldPath: "metadata.name"},
},
{
Name: "FRUIT",
ObjRef: types.Target{
Gvk: resid.Gvk{Kind: "Service"},
Name: "apple"},
FieldRef: types.FieldSelector{FieldPath: "metadata.name"},
},
{
Name: "VEGETABLE",
ObjRef: types.Target{
Gvk: resid.Gvk{Kind: "Leafy"},
Name: "kale"},
FieldRef: types.FieldSelector{FieldPath: "metadata.name"},
},
}
func TestGetAllVarsSimple(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app")
th.WriteK("/app", `
vars:
- name: AWARD
objref:
kind: Service
name: nobelPrize
apiVersion: v7
fieldref:
fieldpath: some.arbitrary.path
- name: BIRD
objref:
kind: Service
name: heron
apiVersion: v300
`)
ra, err := th.MakeKustTarget().AccumulateTarget()
if err != nil {
t.Fatalf("Err: %v", err)
}
vars := ra.Vars()
if len(vars) != 2 {
t.Fatalf("unexpected size %d", len(vars))
}
for i := range vars[:2] {
// By using Var.DeepEqual, we are protecting the code
// from a potential invocation of vars[i].ObjRef.GVK()
// during AccumulateTarget
if !vars[i].DeepEqual(someVars[i]) {
t.Fatalf("unexpected var[%d]:\n %v\n %v", i, vars[i], someVars[i])
}
}
}
func TestGetAllVarsNested(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/overlays/o2")
th.WriteK("/app/base", `
vars:
- name: AWARD
objref:
kind: Service
name: nobelPrize
apiVersion: v7
fieldref:
fieldpath: some.arbitrary.path
- name: BIRD
objref:
kind: Service
name: heron
apiVersion: v300
`)
th.WriteK("/app/overlays/o1", `
vars:
- name: FRUIT
objref:
kind: Service
name: apple
resources:
- ../../base
`)
th.WriteK("/app/overlays/o2", `
vars:
- name: VEGETABLE
objref:
kind: Leafy
name: kale
resources:
- ../o1
`)
ra, err := th.MakeKustTarget().AccumulateTarget()
if err != nil {
t.Fatalf("Err: %v", err)
}
vars := ra.Vars()
if len(vars) != 4 {
for i, v := range vars {
fmt.Printf("%v: %v\n", i, v)
}
t.Fatalf("expected 4 vars, got %d", len(vars))
}
for i := range vars {
// By using Var.DeepEqual, we are protecting the code
// from a potential invocation of vars[i].ObjRef.GVK()
// during AccumulateTarget
if !vars[i].DeepEqual(someVars[i]) {
t.Fatalf("unexpected var[%d]:\n %v\n %v", i, vars[i], someVars[i])
}
}
}
func TestVarCollisionsForbidden(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/overlays/o2")
th.WriteK("/app/base", `
vars:
- name: AWARD
objref:
kind: Service
name: nobelPrize
apiVersion: v7
fieldref:
fieldpath: some.arbitrary.path
- name: BIRD
objref:
kind: Service
name: heron
apiVersion: v300
`)
th.WriteK("/app/overlays/o1", `
vars:
- name: AWARD
objref:
kind: Service
name: academy
resources:
- ../../base
`)
th.WriteK("/app/overlays/o2", `
vars:
- name: VEGETABLE
objref:
kind: Leafy
name: kale
resources:
- ../o1
`)
_, err := th.MakeKustTarget().AccumulateTarget()
if err == nil {
t.Fatalf("expected var collision")
}
if !strings.Contains(err.Error(),
"var 'AWARD' already encountered") {
t.Fatalf("unexpected error: %v", err)
}
}

View File

@@ -0,0 +1,477 @@
/*
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 target_test
import (
"strings"
"testing"
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
)
func makeCommonFileForMultiplePatchTest(th *kusttest_test.KustTestHarness) {
th.WriteK("/app/base", `
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namePrefix: team-foo-
commonLabels:
app: mynginx
org: example.com
team: foo
commonAnnotations:
note: This is a test annotation
resources:
- deployment.yaml
- service.yaml
configMapGenerator:
- name: configmap-in-base
literals:
- foo=bar
`)
th.WriteF("/app/base/deployment.yaml", `
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: nginx
labels:
app: nginx
spec:
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
volumeMounts:
- name: nginx-persistent-storage
mountPath: /tmp/ps
- name: sidecar
image: sidecar:latest
volumes:
- name: nginx-persistent-storage
emptyDir: {}
- configMap:
name: configmap-in-base
name: configmap-in-base
`)
th.WriteF("/app/base/service.yaml", `
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- port: 80
selector:
app: nginx
`)
th.WriteK("/app/overlay/staging", `
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namePrefix: staging-
commonLabels:
env: staging
patchesStrategicMerge:
- deployment-patch1.yaml
- deployment-patch2.yaml
resources:
- ../../base
configMapGenerator:
- name: configmap-in-overlay
literals:
- hello=world
`)
}
func TestMultiplePatchesNoConflict(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/overlay/staging")
makeCommonFileForMultiplePatchTest(th)
th.WriteF("/app/overlay/staging/deployment-patch1.yaml", `
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- name: nginx
image: nginx:latest
env:
- name: ENVKEY
value: ENVVALUE
volumes:
- name: nginx-persistent-storage
emptyDir: null
gcePersistentDisk:
pdName: nginx-persistent-storage
- configMap:
name: configmap-in-overlay
name: configmap-in-overlay
`)
th.WriteF("/app/overlay/staging/deployment-patch2.yaml", `
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- name: nginx
env:
- name: ANOTHERENV
value: FOO
volumes:
- name: nginx-persistent-storage
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1beta2
kind: Deployment
metadata:
annotations:
note: This is a test annotation
labels:
app: mynginx
env: staging
org: example.com
team: foo
name: staging-team-foo-nginx
spec:
selector:
matchLabels:
app: mynginx
env: staging
org: example.com
team: foo
template:
metadata:
annotations:
note: This is a test annotation
labels:
app: mynginx
env: staging
org: example.com
team: foo
spec:
containers:
- env:
- name: ANOTHERENV
value: FOO
- name: ENVKEY
value: ENVVALUE
image: nginx:latest
name: nginx
volumeMounts:
- mountPath: /tmp/ps
name: nginx-persistent-storage
- image: sidecar:latest
name: sidecar
volumes:
- gcePersistentDisk:
pdName: nginx-persistent-storage
name: nginx-persistent-storage
- configMap:
name: staging-configmap-in-overlay-k7cbc75tg8
name: configmap-in-overlay
- configMap:
name: staging-team-foo-configmap-in-base-g7k6gt2889
name: configmap-in-base
---
apiVersion: v1
kind: Service
metadata:
annotations:
note: This is a test annotation
labels:
app: mynginx
env: staging
org: example.com
team: foo
name: staging-team-foo-nginx
spec:
ports:
- port: 80
selector:
app: mynginx
env: staging
org: example.com
team: foo
---
apiVersion: v1
data:
foo: bar
kind: ConfigMap
metadata:
annotations:
note: This is a test annotation
labels:
app: mynginx
env: staging
org: example.com
team: foo
name: staging-team-foo-configmap-in-base-g7k6gt2889
---
apiVersion: v1
data:
hello: world
kind: ConfigMap
metadata:
labels:
env: staging
name: staging-configmap-in-overlay-k7cbc75tg8
`)
}
func TestMultiplePatchesWithConflict(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/overlay/staging")
makeCommonFileForMultiplePatchTest(th)
th.WriteF("/app/overlay/staging/deployment-patch1.yaml", `
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- name: nginx
env:
- name: ENABLE_FEATURE_FOO
value: TRUE
volumes:
- name: nginx-persistent-storage
emptyDir: null
gcePersistentDisk:
pdName: nginx-persistent-storage
- configMap:
name: configmap-in-overlay
name: configmap-in-overlay
`)
th.WriteF("/app/overlay/staging/deployment-patch2.yaml", `
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- name: nginx
env:
- name: ENABLE_FEATURE_FOO
value: FALSE
`)
_, err := th.MakeKustTarget().MakeCustomizedResMap()
if err == nil {
t.Fatalf("expected conflict")
}
if !strings.Contains(
err.Error(), "conflict between ") {
t.Fatalf("Unexpected err: %v", err)
}
}
func TestMultiplePatchesWithOnePatchDeleteDirective(t *testing.T) {
additivePatch := `apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- name: nginx
env:
- name: SOME_NAME
value: somevalue
`
deletePatch := `apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- $patch: delete
name: sidecar
`
cases := []struct {
name string
patch1 string
patch2 string
expectError bool
}{
{
name: "Patch with delete directive first",
patch1: deletePatch,
patch2: additivePatch,
},
{
name: "Patch with delete directive second",
patch1: additivePatch,
patch2: deletePatch,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/overlay/staging")
makeCommonFileForMultiplePatchTest(th)
th.WriteF("/app/overlay/staging/deployment-patch1.yaml", c.patch1)
th.WriteF("/app/overlay/staging/deployment-patch2.yaml", c.patch2)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `apiVersion: apps/v1beta2
kind: Deployment
metadata:
annotations:
note: This is a test annotation
labels:
app: mynginx
env: staging
org: example.com
team: foo
name: staging-team-foo-nginx
spec:
selector:
matchLabels:
app: mynginx
env: staging
org: example.com
team: foo
template:
metadata:
annotations:
note: This is a test annotation
labels:
app: mynginx
env: staging
org: example.com
team: foo
spec:
containers:
- env:
- name: SOME_NAME
value: somevalue
image: nginx
name: nginx
volumeMounts:
- mountPath: /tmp/ps
name: nginx-persistent-storage
volumes:
- emptyDir: {}
name: nginx-persistent-storage
- configMap:
name: staging-team-foo-configmap-in-base-g7k6gt2889
name: configmap-in-base
---
apiVersion: v1
kind: Service
metadata:
annotations:
note: This is a test annotation
labels:
app: mynginx
env: staging
org: example.com
team: foo
name: staging-team-foo-nginx
spec:
ports:
- port: 80
selector:
app: mynginx
env: staging
org: example.com
team: foo
---
apiVersion: v1
data:
foo: bar
kind: ConfigMap
metadata:
annotations:
note: This is a test annotation
labels:
app: mynginx
env: staging
org: example.com
team: foo
name: staging-team-foo-configmap-in-base-g7k6gt2889
---
apiVersion: v1
data:
hello: world
kind: ConfigMap
metadata:
labels:
env: staging
name: staging-configmap-in-overlay-k7cbc75tg8
`)
})
}
}
func TestMultiplePatchesBothWithPatchDeleteDirective(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/overlay/staging")
makeCommonFileForMultiplePatchTest(th)
th.WriteF("/app/overlay/staging/deployment-patch1.yaml", `
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- $patch: delete
name: sidecar
`)
th.WriteF("/app/overlay/staging/deployment-patch2.yaml", `
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- $patch: delete
name: nginx
`)
_, err := th.MakeKustTarget().MakeCustomizedResMap()
if err == nil {
t.Fatalf("Expected error")
}
if !strings.Contains(
err.Error(), "both containing ") {
t.Fatalf("Unexpected err: %v", err)
}
return
}

View File

@@ -0,0 +1,129 @@
/*
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 target_test
import (
"testing"
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
)
func TestNamespacedGenerator(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app")
th.WriteK("/app", `
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
configMapGenerator:
- name: the-non-default-namespace-map
namespace: non-default
literals:
- altGreeting=Good Morning from non-default namespace!
- enableRisky="false"
- name: the-map
literals:
- altGreeting=Good Morning from default namespace!
- enableRisky="false"
secretGenerator:
- name: the-non-default-namespace-secret
namespace: non-default
literals:
- password.txt=verySecret
- name: the-secret
literals:
- password.txt=anotherSecret
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: v1
data:
altGreeting: Good Morning from non-default namespace!
enableRisky: "false"
kind: ConfigMap
metadata:
name: the-non-default-namespace-map-b6h49k7mt8
namespace: non-default
---
apiVersion: v1
data:
altGreeting: Good Morning from default namespace!
enableRisky: "false"
kind: ConfigMap
metadata:
name: the-map-4959m5tm6c
---
apiVersion: v1
data:
password.txt: dmVyeVNlY3JldA==
kind: Secret
metadata:
name: the-non-default-namespace-secret-h8d9hkgtb9
namespace: non-default
type: Opaque
---
apiVersion: v1
data:
password.txt: YW5vdGhlclNlY3JldA==
kind: Secret
metadata:
name: the-secret-fgb45h45bh
type: Opaque
`)
}
func TestNamespacedGeneratorWithOverlays(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/overlay")
th.WriteK("/app/base", `
namespace: base
configMapGenerator:
- name: testCase
literals:
- base=true
`)
th.WriteK("/app/overlay", `
resources:
- ../base
namespace: overlay
configMapGenerator:
- name: testCase
behavior: merge
literals:
- overlay=true
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: v1
data:
base: "true"
overlay: "true"
kind: ConfigMap
metadata:
annotations: {}
labels: {}
name: testCase-4g75kbk6gm
namespace: overlay
`)
}

View File

@@ -0,0 +1,602 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package target_test
import (
"strings"
"testing"
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
)
func TestNamespacedSecrets(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app")
th.WriteF("/app/secrets.yaml", `
apiVersion: v1
kind: Secret
metadata:
name: dummy
namespace: default
type: Opaque
data:
dummy: ""
---
apiVersion: v1
kind: Secret
metadata:
name: dummy
namespace: kube-system
type: Opaque
data:
dummy: ""
`)
// This should find the proper secret.
th.WriteF("/app/role.yaml", `
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: dummy
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["dummy"]
verbs: ["get"]
`)
th.WriteK("/app", `
resources:
- secrets.yaml
- role.yaml
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
// This validates Fix #1444. This should not be an error anymore -
// the secrets have the same name but are in different namespaces.
// The ClusterRole (by def) is not in a namespace,
// an in this case applies to *any* Secret resource
// named "dummy"
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: v1
data:
dummy: ""
kind: Secret
metadata:
name: dummy
namespace: default
type: Opaque
---
apiVersion: v1
data:
dummy: ""
kind: Secret
metadata:
name: dummy
namespace: kube-system
type: Opaque
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: dummy
rules:
- apiGroups:
- ""
resourceNames:
- dummy
resources:
- secrets
verbs:
- get
`)
}
// TestNameAndNsTransformation validates that NamespaceTransformer,
// PrefixSuffixTransformer and namereference transformers are
// able to deal with simultaneous change of namespace and name.
func TestNameAndNsTransformation(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/nameandns")
th.WriteK("/nameandns", `
namePrefix: p1-
nameSuffix: -s1
namespace: newnamespace
resources:
- resources.yaml
`)
th.WriteF("/nameandns/resources.yaml", `
apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
---
apiVersion: v1
kind: ConfigMap
metadata:
name: cm2
namespace: ns1
---
apiVersion: v1
kind: Service
metadata:
name: svc1
namespace: ns1
---
apiVersion: v1
kind: Service
metadata:
name: svc2
namespace: ns1
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: sa1
namespace: ns1
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: sa2
namespace: ns1
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: manager-rolebinding
subjects:
- kind: ServiceAccount
name: sa1
namespace: ns1
- kind: ServiceAccount
name: sa2
namespace: ns1
- kind: ServiceAccount
name: sa3
namespace: random
- kind: ServiceAccount
name: default
namespace: irrelevant
---
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
name: example
webhooks:
- name: example1
clientConfig:
service:
name: svc1
namespace: ns1
- name: example2
clientConfig:
service:
name: svc2
namespace: ns1
- name: example3
clientConfig:
service:
name: svc3
namespace: random
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: crds.my.org
---
kind: ClusterRole
metadata:
name: cr1
---
kind: ClusterRoleBinding
metadata:
name: crb1
subjects:
- kind: ServiceAccount
name: default
namespace: irrelevant
---
kind: PersistentVolume
metadata:
name: pv1
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: v1
kind: ConfigMap
metadata:
name: p1-cm1-s1
namespace: newnamespace
---
apiVersion: v1
kind: ConfigMap
metadata:
name: p1-cm2-s1
namespace: newnamespace
---
apiVersion: v1
kind: Service
metadata:
name: p1-svc1-s1
namespace: newnamespace
---
apiVersion: v1
kind: Service
metadata:
name: p1-svc2-s1
namespace: newnamespace
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: p1-sa1-s1
namespace: newnamespace
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: p1-sa2-s1
namespace: newnamespace
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: p1-manager-rolebinding-s1
subjects:
- kind: ServiceAccount
name: p1-sa1-s1
namespace: newnamespace
- kind: ServiceAccount
name: p1-sa2-s1
namespace: newnamespace
- kind: ServiceAccount
name: sa3
namespace: random
- kind: ServiceAccount
name: default
namespace: newnamespace
---
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
name: p1-example-s1
webhooks:
- clientConfig:
service:
name: p1-svc1-s1
namespace: newnamespace
name: example1
- clientConfig:
service:
name: p1-svc2-s1
namespace: newnamespace
name: example2
- clientConfig:
service:
name: svc3
namespace: random
name: example3
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: crds.my.org
---
kind: ClusterRole
metadata:
name: p1-cr1-s1
---
kind: ClusterRoleBinding
metadata:
name: p1-crb1-s1
subjects:
- kind: ServiceAccount
name: default
namespace: newnamespace
---
kind: PersistentVolume
metadata:
name: p1-pv1-s1
`)
}
// This serie of constants is used to prove the need of
// the namespace field in the objref field of the var declaration.
// The following tests demonstrate that it creates umbiguous variable
// declaration if two entities of the kind with the same name
// but in different namespaces are declared.
// This is tracking the following issue:
// https://github.com/kubernetes-sigs/kustomize/issues/1298
const namespaceNeedInVarMyApp string = `
resources:
- elasticsearch-dev-service.yaml
- elasticsearch-test-service.yaml
vars:
- name: elasticsearch-test-service-name
objref:
kind: Service
name: elasticsearch
apiVersion: v1
fieldref:
fieldpath: metadata.name
- name: elasticsearch-test-protocol
objref:
kind: Service
name: elasticsearch
apiVersion: v1
fieldref:
fieldpath: spec.ports[0].protocol
- name: elasticsearch-dev-service-name
objref:
kind: Service
name: elasticsearch
apiVersion: v1
fieldref:
fieldpath: metadata.name
- name: elasticsearch-dev-protocol
objref:
kind: Service
name: elasticsearch
apiVersion: v1
fieldref:
fieldpath: spec.ports[0].protocol
`
const namespaceNeedInVarDevResources string = `
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: elasticsearch
namespace: dev
spec:
template:
spec:
containers:
- name: elasticsearch
env:
- name: DISCOVERY_SERVICE
value: "$(elasticsearch-dev-service-name).monitoring.svc.cluster.local"
- name: DISCOVERY_PROTOCOL
value: "$(elasticsearch-dev-protocol)"
---
apiVersion: v1
kind: Service
metadata:
name: elasticsearch
namespace: dev
spec:
ports:
- name: transport
port: 9300
protocol: TCP
clusterIP: None
`
const namespaceNeedInVarTestResources string = `
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: elasticsearch
namespace: test
spec:
template:
spec:
containers:
- name: elasticsearch
env:
- name: DISCOVERY_SERVICE
value: "$(elasticsearch-test-service-name).monitoring.svc.cluster.local"
- name: DISCOVERY_PROTOCOL
value: "$(elasticsearch-test-protocol)"
---
apiVersion: v1
kind: Service
metadata:
name: elasticsearch
namespace: test
spec:
ports:
- name: transport
port: 9300
protocol: UDP
clusterIP: None
`
const namespaceNeedInVarExpectedOutput string = `
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: elasticsearch
namespace: dev
spec:
template:
spec:
containers:
- env:
- name: DISCOVERY_SERVICE
value: elasticsearch.monitoring.svc.cluster.local
- name: DISCOVERY_PROTOCOL
value: TCP
name: elasticsearch
---
apiVersion: v1
kind: Service
metadata:
name: elasticsearch
namespace: dev
spec:
clusterIP: None
ports:
- name: transport
port: 9300
protocol: TCP
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: elasticsearch
namespace: test
spec:
template:
spec:
containers:
- env:
- name: DISCOVERY_SERVICE
value: elasticsearch.monitoring.svc.cluster.local
- name: DISCOVERY_PROTOCOL
value: UDP
name: elasticsearch
---
apiVersion: v1
kind: Service
metadata:
name: elasticsearch
namespace: test
spec:
clusterIP: None
ports:
- name: transport
port: 9300
protocol: UDP
`
// TestVariablesAmbiguous demonstrates how two variables pointing at two different resources
// using the same name in different namespaces are treated as ambiguous if the namespace is
// not specified
func TestVariablesAmbiguous(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/namespaceNeedInVar/myapp")
th.WriteK("/namespaceNeedInVar/myapp", namespaceNeedInVarMyApp)
th.WriteF("/namespaceNeedInVar/myapp/elasticsearch-dev-service.yaml", namespaceNeedInVarDevResources)
th.WriteF("/namespaceNeedInVar/myapp/elasticsearch-test-service.yaml", namespaceNeedInVarTestResources)
_, err := th.MakeKustTarget().MakeCustomizedResMap()
if err == nil {
t.Fatalf("expected error")
}
if !strings.Contains(err.Error(), "unable to disambiguate") {
t.Fatalf("unexpected error %v", err)
}
}
const namespaceNeedInVarDevFolder string = `
resources:
- elasticsearch-dev-service.yaml
vars:
- name: elasticsearch-dev-service-name
objref:
kind: Service
name: elasticsearch
apiVersion: v1
fieldref:
fieldpath: metadata.name
- name: elasticsearch-dev-protocol
objref:
kind: Service
name: elasticsearch
apiVersion: v1
fieldref:
fieldpath: spec.ports[0].protocol
`
const namespaceNeedInVarTestFolder string = `
resources:
- elasticsearch-test-service.yaml
vars:
- name: elasticsearch-test-service-name
objref:
kind: Service
name: elasticsearch
apiVersion: v1
fieldref:
fieldpath: metadata.name
- name: elasticsearch-test-protocol
objref:
kind: Service
name: elasticsearch
apiVersion: v1
fieldref:
fieldpath: spec.ports[0].protocol
`
// TestVariablesAmbiguousWorkaround demonstrates a possible workaround
// to TestVariablesAmbiguous problem. It requires to separate the variables
// and resources into multiple kustomization context/folders instead of one.
func TestVariablesAmbiguousWorkaround(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/namespaceNeedInVar/workaround")
th.WriteK("/namespaceNeedInVar/dev", namespaceNeedInVarDevFolder)
th.WriteF("/namespaceNeedInVar/dev/elasticsearch-dev-service.yaml", namespaceNeedInVarDevResources)
th.WriteK("/namespaceNeedInVar/test", namespaceNeedInVarTestFolder)
th.WriteF("/namespaceNeedInVar/test/elasticsearch-test-service.yaml", namespaceNeedInVarTestResources)
th.WriteK("/namespaceNeedInVar/workaround", `
resources:
- ../dev
- ../test
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, namespaceNeedInVarExpectedOutput)
}
const namespaceNeedInVarMyAppWithNamespace string = `
resources:
- elasticsearch-dev-service.yaml
- elasticsearch-test-service.yaml
vars:
- name: elasticsearch-test-service-name
objref:
kind: Service
name: elasticsearch
namespace: test
apiVersion: v1
fieldref:
fieldpath: metadata.name
- name: elasticsearch-test-protocol
objref:
kind: Service
name: elasticsearch
namespace: test
apiVersion: v1
fieldref:
fieldpath: spec.ports[0].protocol
- name: elasticsearch-dev-service-name
objref:
kind: Service
name: elasticsearch
namespace: dev
apiVersion: v1
fieldref:
fieldpath: metadata.name
- name: elasticsearch-dev-protocol
objref:
kind: Service
name: elasticsearch
namespace: dev
apiVersion: v1
fieldref:
fieldpath: spec.ports[0].protocol
`
// TestVariablesDisambiguatedWithNamespace demonstrates that adding the namespace
// to the variable declarations allows to disambiguate the variables.
func TestVariablesDisambiguatedWithNamespace(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/namespaceNeedInVar/myapp")
th.WriteK("/namespaceNeedInVar/myapp", namespaceNeedInVarMyAppWithNamespace)
th.WriteF("/namespaceNeedInVar/myapp/elasticsearch-dev-service.yaml", namespaceNeedInVarDevResources)
th.WriteF("/namespaceNeedInVar/myapp/elasticsearch-test-service.yaml", namespaceNeedInVarTestResources)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, namespaceNeedInVarExpectedOutput)
}

View File

@@ -0,0 +1,80 @@
/*
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 target_test
import (
"testing"
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
)
func TestNullValues(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app")
th.WriteF("/app/deployment.yaml", `
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: example
name: example
spec:
selector:
matchLabels:
app: example
template:
metadata:
labels:
app: example
spec:
containers:
- args: null
image: image
name: example
`)
th.WriteF("/app/kustomization.yaml", `
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: example
name: example
spec:
selector:
matchLabels:
app: example
template:
metadata:
labels:
app: example
spec:
containers:
- args: null
image: image
name: example
`)
}

View File

@@ -0,0 +1,89 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package target_test
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"sigs.k8s.io/kustomize/v3/api/filesys"
fLdr "sigs.k8s.io/kustomize/v3/api/loader"
"sigs.k8s.io/kustomize/v3/api/plugins/config"
pLdr "sigs.k8s.io/kustomize/v3/api/plugins/loader"
"sigs.k8s.io/kustomize/v3/api/resmap"
"sigs.k8s.io/kustomize/v3/api/resource"
"sigs.k8s.io/kustomize/v3/api/target"
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
"sigs.k8s.io/kustomize/v3/api/testutils/valtest"
"sigs.k8s.io/kustomize/v3/k8sdeps/kunstruct"
"sigs.k8s.io/kustomize/v3/k8sdeps/transformer"
)
func TestPluginDir(t *testing.T) {
tc := kusttest_test.NewPluginTestEnv(t).Set()
defer tc.Reset()
tc.BuildExecPlugin(
"someteam.example.com", "v1", "PrintWorkDir")
base, err := os.Getwd()
if err != nil {
t.Fatalf("err %v", err)
}
dir, err := ioutil.TempDir(base, "kustomize-")
if err != nil {
t.Fatalf("err %v", err)
}
defer os.RemoveAll(dir)
fSys := filesys.MakeFsOnDisk()
err = fSys.WriteFile(filepath.Join(dir, "kustomization.yaml"), []byte(`
generators:
- config.yaml
`))
if err != nil {
t.Fatalf("err %v", err)
}
err = fSys.WriteFile(filepath.Join(dir, "config.yaml"), []byte(`
apiVersion: someteam.example.com/v1
kind: PrintWorkDir
metadata:
name: some-random-name
`))
if err != nil {
t.Fatalf("err %v", err)
}
ldr, err := fLdr.NewLoader(
fLdr.RestrictionRootOnly, dir, fSys)
if err != nil {
t.Fatalf("Err: %v", err)
}
rf := resmap.NewFactory(resource.NewFactory(
kunstruct.NewKunstructuredFactoryImpl()), nil)
pl := pLdr.NewLoader(config.ActivePluginConfig(), rf)
tg, err := target.NewKustTarget(
ldr, valtest_test.MakeFakeValidator(), rf, transformer.NewFactoryImpl(), pl)
if err != nil {
t.Fatalf("err %v", err)
}
m, err := tg.MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th := kusttest_test.NewKustTestHarness(t, ".")
th.AssertActualEqualsExpected(m, `
apiVersion: v1
kind: WorkDir
metadata:
name: `+dir+`
spec:
path: `+dir+`
`)
}

View File

@@ -0,0 +1,179 @@
/*
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 target_test
import (
"testing"
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
)
func TestPruneConfigMap(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/base")
th.WriteK("/app/base", `
resources:
- deployment.yaml
- service.yaml
- secret.yaml
inventory:
type: ConfigMap
configMap:
name: haha
namespace: default
namePrefix: my-
namespace: default
`)
th.WriteF("/app/base/deployment.yaml", `
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: mysql
labels:
app: mysql
spec:
selector:
matchLabels:
app: mysql
strategy:
type: Recreate
template:
metadata:
labels:
app: mysql
spec:
containers:
- image: mysql:5.6
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: pass
key: password
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: mysql-persistent-storage
mountPath: /var/lib/mysql
volumes:
- name: mysql-persistent-storage
emptyDir: {}
`)
th.WriteF("/app/base/service.yaml", `
apiVersion: v1
kind: Service
metadata:
name: mmmysql
labels:
app: mysql
spec:
ports:
- port: 3306
selector:
app: mysql
`)
th.WriteF("/app/base/secret.yaml", `
apiVersion: v1
kind: Secret
metadata:
name: pass
type: Opaque
data:
# Default password is "admin".
password: YWRtaW4=
username: jingfang
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
//nolint
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1beta2
kind: Deployment
metadata:
labels:
app: mysql
name: my-mysql
namespace: default
spec:
selector:
matchLabels:
app: mysql
strategy:
type: Recreate
template:
metadata:
labels:
app: mysql
spec:
containers:
- env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
key: password
name: my-pass
image: mysql:5.6
name: mysql
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- mountPath: /var/lib/mysql
name: mysql-persistent-storage
volumes:
- emptyDir: {}
name: mysql-persistent-storage
---
apiVersion: v1
kind: Service
metadata:
labels:
app: mysql
name: my-mmmysql
namespace: default
spec:
ports:
- port: 3306
selector:
app: mysql
---
apiVersion: v1
data:
password: YWRtaW4=
username: jingfang
kind: Secret
metadata:
name: my-pass
namespace: default
type: Opaque
---
apiVersion: v1
kind: ConfigMap
metadata:
annotations:
kustomize.config.k8s.io/Inventory: '{"current":{"apps_v1beta2_Deployment|default|my-mysql":null,"~G_v1_Secret|default|my-pass":[{"group":"apps","version":"v1beta2","kind":"Deployment","name":"my-mysql","namespace":"default"}],"~G_v1_Service|default|my-mmmysql":null}}'
kustomize.config.k8s.io/InventoryHash: kd67f7ht8t
name: haha
namespace: default
`)
}

View File

@@ -0,0 +1,380 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package target_test
import (
"strings"
"testing"
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
)
func writeBase(th *kusttest_test.KustTestHarness) {
th.WriteK("/app/base", `
resources:
- serviceaccount.yaml
- rolebinding.yaml
- clusterrolebinding.yaml
- clusterrole.yaml
namePrefix: pfx-
nameSuffix: -sfx
`)
th.WriteF("/app/base/serviceaccount.yaml", `
apiVersion: v1
kind: ServiceAccount
metadata:
name: serviceaccount
`)
th.WriteF("/app/base/rolebinding.yaml", `
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: role
subjects:
- kind: ServiceAccount
name: serviceaccount
`)
th.WriteF("/app/base/clusterrolebinding.yaml", `
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: role
subjects:
- kind: ServiceAccount
name: serviceaccount
`)
th.WriteF("/app/base/clusterrole.yaml", `
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: role
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "watch", "list"]
`)
}
func writeMidOverlays(th *kusttest_test.KustTestHarness) {
// Mid-level overlays
th.WriteK("/app/overlays/a", `
resources:
- ../../base
namePrefix: a-
nameSuffix: -suffixA
`)
th.WriteK("/app/overlays/b", `
resources:
- ../../base
namePrefix: b-
nameSuffix: -suffixB
`)
}
func writeTopOverlay(th *kusttest_test.KustTestHarness) {
// Top overlay, combining the mid-level overlays
th.WriteK("/app/combined", `
resources:
- ../overlays/a
- ../overlays/b
`)
}
func TestBase(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/base")
writeBase(th)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Unexpected err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: v1
kind: ServiceAccount
metadata:
name: pfx-serviceaccount-sfx
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: pfx-rolebinding-sfx
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: pfx-role-sfx
subjects:
- kind: ServiceAccount
name: pfx-serviceaccount-sfx
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: pfx-rolebinding-sfx
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: pfx-role-sfx
subjects:
- kind: ServiceAccount
name: pfx-serviceaccount-sfx
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: pfx-role-sfx
rules:
- apiGroups:
- ""
resources:
- secrets
verbs:
- get
- watch
- list
`)
}
func TestMidLevelA(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/overlays/a")
writeBase(th)
writeMidOverlays(th)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Unexpected err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: v1
kind: ServiceAccount
metadata:
name: a-pfx-serviceaccount-sfx-suffixA
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: a-pfx-rolebinding-sfx-suffixA
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: a-pfx-role-sfx-suffixA
subjects:
- kind: ServiceAccount
name: a-pfx-serviceaccount-sfx-suffixA
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: a-pfx-rolebinding-sfx-suffixA
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: a-pfx-role-sfx-suffixA
subjects:
- kind: ServiceAccount
name: a-pfx-serviceaccount-sfx-suffixA
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: a-pfx-role-sfx-suffixA
rules:
- apiGroups:
- ""
resources:
- secrets
verbs:
- get
- watch
- list
`)
}
func TestMidLevelB(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/overlays/b")
writeBase(th)
writeMidOverlays(th)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Unexpected err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: v1
kind: ServiceAccount
metadata:
name: b-pfx-serviceaccount-sfx-suffixB
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: b-pfx-rolebinding-sfx-suffixB
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: b-pfx-role-sfx-suffixB
subjects:
- kind: ServiceAccount
name: b-pfx-serviceaccount-sfx-suffixB
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: b-pfx-rolebinding-sfx-suffixB
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: b-pfx-role-sfx-suffixB
subjects:
- kind: ServiceAccount
name: b-pfx-serviceaccount-sfx-suffixB
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: b-pfx-role-sfx-suffixB
rules:
- apiGroups:
- ""
resources:
- secrets
verbs:
- get
- watch
- list
`)
}
func TestMultibasesNoConflict(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/combined")
writeBase(th)
writeMidOverlays(th)
writeTopOverlay(th)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Unexpected err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: v1
kind: ServiceAccount
metadata:
name: a-pfx-serviceaccount-sfx-suffixA
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: a-pfx-rolebinding-sfx-suffixA
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: a-pfx-role-sfx-suffixA
subjects:
- kind: ServiceAccount
name: a-pfx-serviceaccount-sfx-suffixA
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: a-pfx-rolebinding-sfx-suffixA
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: a-pfx-role-sfx-suffixA
subjects:
- kind: ServiceAccount
name: a-pfx-serviceaccount-sfx-suffixA
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: a-pfx-role-sfx-suffixA
rules:
- apiGroups:
- ""
resources:
- secrets
verbs:
- get
- watch
- list
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: b-pfx-serviceaccount-sfx-suffixB
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: b-pfx-rolebinding-sfx-suffixB
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: b-pfx-role-sfx-suffixB
subjects:
- kind: ServiceAccount
name: b-pfx-serviceaccount-sfx-suffixB
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: b-pfx-rolebinding-sfx-suffixB
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: b-pfx-role-sfx-suffixB
subjects:
- kind: ServiceAccount
name: b-pfx-serviceaccount-sfx-suffixB
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: b-pfx-role-sfx-suffixB
rules:
- apiGroups:
- ""
resources:
- secrets
verbs:
- get
- watch
- list
`)
}
func TestMultibasesWithConflict(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/combined")
writeBase(th)
writeMidOverlays(th)
writeTopOverlay(th)
th.WriteK("/app/overlays/a", `
namePrefix: a-
nameSuffix: -suffixA
resources:
- serviceaccount.yaml
- ../../base
`)
// Expect an error because this resource in the overlay
// matches a resource in the base.
th.WriteF("/app/overlays/a/serviceaccount.yaml", `
apiVersion: v1
kind: ServiceAccount
metadata:
name: serviceaccount
`)
_, err := th.MakeKustTarget().MakeCustomizedResMap()
if err == nil {
t.Fatalf("expected error")
}
if !strings.Contains(err.Error(), "multiple matches for ~G_v1_ServiceAccount|~X|serviceaccount") {
t.Fatalf("unexpected error %v", err)
}
}

View File

@@ -0,0 +1,236 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package target_test
import (
"strings"
"testing"
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
)
func writeDeployment(th *kusttest_test.KustTestHarness, path string) {
th.WriteF(path, `
apiVersion: apps/v1
kind: Deployment
metadata:
name: myDeployment
spec:
template:
metadata:
labels:
backend: awesome
spec:
containers:
- name: whatever
image: whatever
`)
}
func writeStringPrefixer(
th *kusttest_test.KustTestHarness, path, name string) {
th.WriteF(path, `
apiVersion: someteam.example.com/v1
kind: StringPrefixer
metadata:
name: `+name+`
`)
}
func writeDatePrefixer(
th *kusttest_test.KustTestHarness, path, name string) {
th.WriteF(path, `
apiVersion: someteam.example.com/v1
kind: DatePrefixer
metadata:
name: `+name+`
`)
}
func TestOrderedTransformers(t *testing.T) {
tc := kusttest_test.NewPluginTestEnv(t).Set()
defer tc.Reset()
tc.BuildGoPlugin(
"someteam.example.com", "v1", "StringPrefixer")
tc.BuildGoPlugin(
"someteam.example.com", "v1", "DatePrefixer")
th := kusttest_test.NewKustTestHarnessAllowPlugins(t, "/app")
th.WriteK("/app", `
resources:
- deployment.yaml
transformers:
- peachPrefixer.yaml
- date1Prefixer.yaml
- applePrefixer.yaml
- date2Prefixer.yaml
`)
writeDeployment(th, "/app/deployment.yaml")
writeStringPrefixer(th, "/app/applePrefixer.yaml", "apple")
writeStringPrefixer(th, "/app/peachPrefixer.yaml", "peach")
writeDatePrefixer(th, "/app/date1Prefixer.yaml", "date1")
writeDatePrefixer(th, "/app/date2Prefixer.yaml", "date2")
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1
kind: Deployment
metadata:
name: 2018-05-11-apple-2018-05-11-peach-myDeployment
spec:
template:
metadata:
labels:
backend: awesome
spec:
containers:
- image: whatever
name: whatever
`)
}
func TestPluginsNotEnabled(t *testing.T) {
tc := kusttest_test.NewPluginTestEnv(t).Set()
defer tc.Reset()
tc.BuildGoPlugin(
"someteam.example.com", "v1", "StringPrefixer")
th := kusttest_test.NewKustTestHarness(t, "/app")
th.WriteK("/app", `
transformers:
- stringPrefixer.yaml
`)
writeStringPrefixer(th, "/app/stringPrefixer.yaml", "apple")
_, err := th.MakeKustTarget().MakeCustomizedResMap()
if err == nil {
t.Fatalf("expected error")
}
if !strings.Contains(err.Error(), "unable to load external plugin StringPrefixer") {
t.Fatalf("unexpected err: %v", err)
}
}
func TestSedTransformer(t *testing.T) {
tc := kusttest_test.NewPluginTestEnv(t).Set()
defer tc.Reset()
tc.BuildExecPlugin(
"someteam.example.com", "v1", "SedTransformer")
th := kusttest_test.NewKustTestHarnessAllowPlugins(t, "/app")
th.WriteK("/app", `
resources:
- configmap.yaml
transformers:
- sed-transformer.yaml
configMapGenerator:
- name: test
literals:
- FOO=$FOO
- BAR=$BAR
`)
th.WriteF("/app/sed-transformer.yaml", `
apiVersion: someteam.example.com/v1
kind: SedTransformer
metadata:
name: some-random-name
argsFromFile: sed-input.txt
`)
th.WriteF("/app/sed-input.txt", `
s/$FOO/foo/g
s/$BAR/bar/g
`)
th.WriteF("/app/configmap.yaml", `
apiVersion: v1
kind: ConfigMap
metadata:
name: configmap-a
annotations:
kustomize.k8s.io/Generated: "false"
data:
foo: $FOO
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: v1
data:
foo: foo
kind: ConfigMap
metadata:
annotations:
kustomize.k8s.io/Generated: "false"
name: configmap-a
---
apiVersion: v1
data:
BAR: bar
FOO: foo
kind: ConfigMap
metadata:
name: test-k4bkhftttd
`)
}
func TestTransformedTransformers(t *testing.T) {
tc := kusttest_test.NewPluginTestEnv(t).Set()
defer tc.Reset()
tc.BuildGoPlugin(
"someteam.example.com", "v1", "StringPrefixer")
tc.BuildGoPlugin(
"someteam.example.com", "v1", "DatePrefixer")
th := kusttest_test.NewKustTestHarnessAllowPlugins(t, "/app/overlay")
th.WriteK("/app/base", `
resources:
- stringPrefixer.yaml
transformers:
- datePrefixer.yaml
`)
writeStringPrefixer(th, "/app/base/stringPrefixer.yaml", "apple")
writeDatePrefixer(th, "/app/base/datePrefixer.yaml", "date")
th.WriteK("/app/overlay", `
resources:
- deployment.yaml
transformers:
- ../base
`)
writeDeployment(th, "/app/overlay/deployment.yaml")
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1
kind: Deployment
metadata:
name: 2018-05-11-apple-myDeployment
spec:
template:
metadata:
labels:
backend: awesome
spec:
containers:
- image: whatever
name: whatever
`)
}

View File

@@ -0,0 +1,182 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package target_test
import (
"testing"
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
)
func makeStatefulSetKustomization(th *kusttest_test.KustTestHarness) {
th.WriteK("/app", `
commonLabels:
notIn: arrays
resources:
- statefulset.yaml
- statefulset-with-template.yaml
`)
th.WriteF("/app/statefulset.yaml", `
kind: StatefulSet
apiVersion: apps/v1
metadata:
name: test
labels:
notIn: arrays
spec:
serviceName: test
replicas: 1
selector:
matchLabels:
app: test
template:
metadata:
labels:
app: test
spec:
containers:
- name: nginx
image: k8s.gcr.io/nginx-slim:0.8
ports:
- containerPort: 80
name: web
`)
th.WriteF("/app/statefulset-with-template.yaml", `
kind: StatefulSet
apiVersion: apps/v1
metadata:
name: persisted-test
labels:
notIn: arrays
spec:
serviceName: test
replicas: 1
selector:
matchLabels:
app: test
template:
metadata:
labels:
app: test
spec:
containers:
- name: nginx
image: k8s.gcr.io/nginx-slim:0.8
ports:
- containerPort: 80
name: web
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
- name: data
mountPath: /usr/share/nginx/data
volumeClaimTemplates:
- metadata:
name: www
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: my-storage-class
resources:
requests:
storage: 1Gi
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: my-storage-class
resources:
requests:
storage: 100Gi
`)
}
func TestTransformersNoCreateArrays(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app")
makeStatefulSetKustomization(th)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1
kind: StatefulSet
metadata:
labels:
notIn: arrays
name: test
spec:
replicas: 1
selector:
matchLabels:
app: test
notIn: arrays
serviceName: test
template:
metadata:
labels:
app: test
notIn: arrays
spec:
containers:
- image: k8s.gcr.io/nginx-slim:0.8
name: nginx
ports:
- containerPort: 80
name: web
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
labels:
notIn: arrays
name: persisted-test
spec:
replicas: 1
selector:
matchLabels:
app: test
notIn: arrays
serviceName: test
template:
metadata:
labels:
app: test
notIn: arrays
spec:
containers:
- image: k8s.gcr.io/nginx-slim:0.8
name: nginx
ports:
- containerPort: 80
name: web
volumeMounts:
- mountPath: /usr/share/nginx/html
name: www
- mountPath: /usr/share/nginx/data
name: data
volumeClaimTemplates:
- metadata:
labels:
notIn: arrays
name: www
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: my-storage-class
- metadata:
labels:
notIn: arrays
name: data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
storageClassName: my-storage-class
`)
}

View File

@@ -0,0 +1,424 @@
/*
Copyright 2019 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 target_test
import (
"testing"
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
)
func makeTransfomersImageBase(th *kusttest_test.KustTestHarness) {
th.WriteK("/app/base", `
resources:
- deploy1.yaml
- random.yaml
images:
- name: nginx
newTag: v2
- name: my-nginx
newTag: previous
- name: myprivaterepohostname:1234/my/image
newTag: v1.0.1
- name: foobar
digest: sha256:24a0c4b4
- name: alpine
newName: myprivaterepohostname:1234/my/cool-alpine
- name: gcr.io:8080/my-project/my-cool-app
newName: my-cool-app
- name: postgres
newName: my-postgres
newTag: v3
- name: docker
newName: my-docker
digest: sha256:25a0d4b4
`)
th.WriteF("/app/base/deploy1.yaml", `
group: apps
apiVersion: v1
kind: Deployment
metadata:
name: deploy1
spec:
template:
spec:
initContainers:
- name: nginx2
image: my-nginx:1.8.0
- name: init-alpine
image: alpine:1.8.0
containers:
- name: ngnix
image: nginx:1.7.9
- name: repliaced-with-digest
image: foobar:1
- name: postgresdb
image: postgres:1.8.0
`)
th.WriteF("/app/base/random.yaml", `
kind: randomKind
metadata:
name: random
spec:
template:
spec:
containers:
- name: ngnix1
image: nginx
spec2:
template:
spec:
containers:
- name: nginx3
image: nginx:v1
- name: nginx4
image: my-nginx:latest
spec3:
template:
spec:
initContainers:
- name: postgresdb
image: postgres:alpine-9
- name: init-docker
image: docker:17-git
- name: myImage
image: myprivaterepohostname:1234/my/image:latest
- name: myImage2
image: myprivaterepohostname:1234/my/image
- name: my-app
image: my-app-image:v1
- name: my-cool-app
image: gcr.io:8080/my-project/my-cool-app:latest
`)
}
func TestIssue1281_JsonPatchAndImageTag(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app")
th.WriteK("/app", `
resources:
- deployment.yaml
images:
- name: abbott
newTag: v2
- name: costello
newTag: v8
patchesJson6902:
- target:
group: apps
version: v1
kind: Deployment
name: ben
path: patch.json
`)
th.WriteF("/app/deployment.yaml", `
apiVersion: apps/v1
kind: Deployment
metadata:
name: ben
spec:
template:
spec:
dnsPolicy: "None"
containers:
- name: awesome
image: abbott
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: alice
spec:
template:
spec:
containers:
- name: tomato
image: abbott
`)
th.WriteF("/app/patch.json", `
[ {"op": "add",
"path": "/spec/replica", "value": "3"},
{"op": "replace",
"path": "/spec/template/spec/containers/0",
"value": { "image": "costello" } } ]
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1
kind: Deployment
metadata:
name: ben
spec:
replica: "3"
template:
spec:
containers:
- image: costello:v8
dnsPolicy: None
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: alice
spec:
template:
spec:
containers:
- image: abbott:v2
name: tomato
`)
}
func TestTransfomersImageDefaultConfig(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/base")
makeTransfomersImageBase(th)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: v1
group: apps
kind: Deployment
metadata:
name: deploy1
spec:
template:
spec:
containers:
- image: nginx:v2
name: ngnix
- image: foobar@sha256:24a0c4b4
name: repliaced-with-digest
- image: my-postgres:v3
name: postgresdb
initContainers:
- image: my-nginx:previous
name: nginx2
- image: myprivaterepohostname:1234/my/cool-alpine:1.8.0
name: init-alpine
---
kind: randomKind
metadata:
name: random
spec:
template:
spec:
containers:
- image: nginx:v2
name: ngnix1
spec2:
template:
spec:
containers:
- image: nginx:v2
name: nginx3
- image: my-nginx:previous
name: nginx4
spec3:
template:
spec:
initContainers:
- image: my-postgres:v3
name: postgresdb
- image: my-docker@sha256:25a0d4b4
name: init-docker
- image: myprivaterepohostname:1234/my/image:v1.0.1
name: myImage
- image: myprivaterepohostname:1234/my/image:v1.0.1
name: myImage2
- image: my-app-image:v1
name: my-app
- image: my-cool-app:latest
name: my-cool-app
`)
}
func makeTransfomersImageCustomBase(th *kusttest_test.KustTestHarness) {
th.WriteK("/app/base", `
resources:
- custom.yaml
configurations:
- config/custom.yaml
images:
- name: nginx
newTag: v2
- name: my-nginx
newTag: previous
- name: myprivaterepohostname:1234/my/image
newTag: v1.0.1
- name: foobar
digest: sha256:24a0c4b4
- name: alpine
newName: myprivaterepohostname:1234/my/cool-alpine
- name: gcr.io:8080/my-project/my-cool-app
newName: my-cool-app
- name: postgres
newName: my-postgres
newTag: v3
- name: docker
newName: my-docker
digest: sha256:25a0d4b4
`)
th.WriteF("/app/base/custom.yaml", `
kind: customKind
metadata:
name: custom
spec:
template:
spec:
myContainers:
- name: ngnix1
image: nginx
spec2:
template:
spec:
myContainers:
- name: nginx3
image: nginx:v1
- name: nginx4
image: my-nginx:latest
spec3:
template:
spec:
myInitContainers:
- name: postgresdb
image: postgres:alpine-9
- name: init-docker
image: docker:17-git
- name: myImage
image: myprivaterepohostname:1234/my/image:latest
- name: myImage2
image: myprivaterepohostname:1234/my/image
- name: my-app
image: my-app-image:v1
- name: my-cool-app
image: gcr.io:8080/my-project/my-cool-app:latest
`)
th.WriteF("/app/base/config/custom.yaml", `
images:
- kind: Custom
path: spec/template/spec/myContainers/image
- kind: Custom
path: spec2/template/spec/myContainers/image
- kind: Custom
path: spec3/template/spec/myInitContainers/image
`)
}
func TestTransfomersImageCustomConfig(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/base")
makeTransfomersImageCustomBase(th)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
kind: customKind
metadata:
name: custom
spec:
template:
spec:
myContainers:
- image: nginx
name: ngnix1
spec2:
template:
spec:
myContainers:
- image: nginx:v1
name: nginx3
- image: my-nginx:latest
name: nginx4
spec3:
template:
spec:
myInitContainers:
- image: postgres:alpine-9
name: postgresdb
- image: docker:17-git
name: init-docker
- image: myprivaterepohostname:1234/my/image:latest
name: myImage
- image: myprivaterepohostname:1234/my/image
name: myImage2
- image: my-app-image:v1
name: my-app
- image: gcr.io:8080/my-project/my-cool-app:latest
name: my-cool-app
`)
}
func makeTransfomersImageKnativeBase(th *kusttest_test.KustTestHarness) {
th.WriteK("/app/base", `
resources:
- knative.yaml
configurations:
- config/knative.yaml
images:
- name: solsa-echo
newTag: foo
`)
th.WriteF("/app/base/knative.yaml", `
apiVersion: serving.knative.dev/v1alpha1
kind: Service
metadata:
name: echo
spec:
runLatest:
configuration:
revisionTemplate:
spec:
container:
image: solsa-echo
`)
th.WriteF("/app/base/config/knative.yaml", `
images:
- path: spec/runLatest/configuration/revisionTemplate/spec/container/image
apiVersion: serving.knative.dev/v1alpha1
kind: Service
`)
}
func TestTransfomersImageKnativeConfig(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/base")
makeTransfomersImageKnativeBase(th)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: serving.knative.dev/v1alpha1
kind: Service
metadata:
name: echo
spec:
runLatest:
configuration:
revisionTemplate:
spec:
container:
image: solsa-echo:foo
`)
}

File diff suppressed because it is too large Load Diff

View File

@@ -10,17 +10,18 @@ import (
"testing"
"sigs.k8s.io/kustomize/v3/api/builtinconfig/consts"
"sigs.k8s.io/kustomize/v3/api/loader"
fLdr "sigs.k8s.io/kustomize/v3/api/loader"
"sigs.k8s.io/kustomize/v3/api/pgmconfig"
"sigs.k8s.io/kustomize/v3/api/plugins/config"
pLdr "sigs.k8s.io/kustomize/v3/api/plugins/loader"
"sigs.k8s.io/kustomize/v3/api/resmap"
"sigs.k8s.io/kustomize/v3/api/resource"
"sigs.k8s.io/kustomize/v3/api/target"
"sigs.k8s.io/kustomize/v3/api/testutils/valtest"
"sigs.k8s.io/kustomize/v3/api/types"
"sigs.k8s.io/kustomize/v3/internal/loadertest"
"sigs.k8s.io/kustomize/v3/k8sdeps/kunstruct"
"sigs.k8s.io/kustomize/v3/k8sdeps/transformer"
"sigs.k8s.io/kustomize/v3/pkg/pgmconfig"
"sigs.k8s.io/kustomize/v3/pkg/plugins"
"sigs.k8s.io/kustomize/v3/pkg/target"
)
// KustTestHarness is an environment for running a kustomize build,
@@ -31,34 +32,34 @@ type KustTestHarness struct {
t *testing.T
rf *resmap.Factory
ldr loadertest.FakeLoader
pl *plugins.Loader
pl *pLdr.Loader
}
func NewKustTestHarness(t *testing.T, path string) *KustTestHarness {
return NewKustTestHarnessFull(
t, path, loader.RestrictionRootOnly, plugins.DefaultPluginConfig())
t, path, fLdr.RestrictionRootOnly, config.DefaultPluginConfig())
}
func NewKustTestHarnessAllowPlugins(t *testing.T, path string) *KustTestHarness {
return NewKustTestHarnessFull(
t, path, loader.RestrictionRootOnly, plugins.ActivePluginConfig())
t, path, fLdr.RestrictionRootOnly, config.ActivePluginConfig())
}
func NewKustTestHarnessNoLoadRestrictor(t *testing.T, path string) *KustTestHarness {
return NewKustTestHarnessFull(
t, path, loader.RestrictionNone, plugins.DefaultPluginConfig())
t, path, fLdr.RestrictionNone, config.DefaultPluginConfig())
}
func NewKustTestHarnessFull(
t *testing.T, path string,
lr loader.LoadRestrictorFunc, pc *types.PluginConfig) *KustTestHarness {
lr fLdr.LoadRestrictorFunc, pc *types.PluginConfig) *KustTestHarness {
rf := resmap.NewFactory(resource.NewFactory(
kunstruct.NewKunstructuredFactoryImpl()), transformer.NewFactoryImpl())
return &KustTestHarness{
t: t,
rf: rf,
ldr: loadertest.NewFakeLoaderWithRestrictor(lr, path),
pl: plugins.NewLoader(pc, rf)}
pl: pLdr.NewLoader(pc, rf)}
}
func (th *KustTestHarness) MakeKustTarget() *target.KustTarget {

View File

@@ -11,8 +11,9 @@ import (
"strings"
"testing"
"sigs.k8s.io/kustomize/v3/pkg/pgmconfig"
"sigs.k8s.io/kustomize/v3/pkg/plugins"
"sigs.k8s.io/kustomize/v3/api/pgmconfig"
"sigs.k8s.io/kustomize/v3/api/plugins/compiler"
"sigs.k8s.io/kustomize/v3/api/plugins/config"
)
// PluginTestEnv manages the plugin test environment.
@@ -20,7 +21,7 @@ import (
// manages a plugin compiler, etc.
type PluginTestEnv struct {
t *testing.T
compiler *plugins.Compiler
compiler *compiler.Compiler
workDir string
oldXdg string
wasSet bool
@@ -63,23 +64,23 @@ func (x *PluginTestEnv) BuildExecPlugin(g, v, k string) {
}
}
func (x *PluginTestEnv) makeCompiler() *plugins.Compiler {
func (x *PluginTestEnv) makeCompiler() *compiler.Compiler {
// The plugin loader wants to find object code under
// $XDG_CONFIG_HOME/kustomize/plugins
// and the compiler writes object code to
// $objRoot
// so set things up accordingly.
objRoot := filepath.Join(
x.workDir, pgmconfig.ProgramName, pgmconfig.PluginRoot)
x.workDir, pgmconfig.ProgramName, config.PluginRoot)
err := os.MkdirAll(objRoot, os.ModePerm)
if err != nil {
x.t.Error(err)
}
srcRoot, err := plugins.DefaultSrcRoot()
srcRoot, err := compiler.DefaultSrcRoot()
if err != nil {
x.t.Error(err)
}
return plugins.NewCompiler(srcRoot, objRoot)
return compiler.NewCompiler(srcRoot, objRoot)
}
func (x *PluginTestEnv) createWorkDir() {

View File

@@ -6,6 +6,7 @@ package transform
import (
"errors"
"fmt"
"sigs.k8s.io/kustomize/v3/api/resmap"
"sigs.k8s.io/kustomize/v3/api/types"
)

View File

@@ -5,10 +5,11 @@ package transform_test
import (
"fmt"
"testing"
"sigs.k8s.io/kustomize/v3/api/ifc"
. "sigs.k8s.io/kustomize/v3/api/transform"
"sigs.k8s.io/kustomize/v3/k8sdeps/kunstruct"
"testing"
)
type noopMutator struct {