change kinflate to kustomize

This commit is contained in:
Jingfang Liu
2018-04-10 14:32:02 -07:00
committed by Sunil Arora
commit 696ec9b171
125 changed files with 10447 additions and 0 deletions

255
app/application.go Normal file
View File

@@ -0,0 +1,255 @@
/*
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 app
import (
"bytes"
"encoding/json"
"github.com/ghodss/yaml"
manifest "k8s.io/kubectl/pkg/apis/manifest/v1alpha1"
"k8s.io/kubectl/pkg/kustomize/constants"
interror "k8s.io/kubectl/pkg/kustomize/internal/error"
"k8s.io/kubectl/pkg/kustomize/resource"
"k8s.io/kubectl/pkg/kustomize/transformers"
"k8s.io/kubectl/pkg/loader"
)
type Application interface {
// Resources computes and returns the resources for the app.
Resources() (resource.ResourceCollection, error)
// SemiResources computes and returns the resources without name hash and name reference for the app
SemiResources() (resource.ResourceCollection, error)
// RawResources computes and returns the raw resources from the manifest.
// It contains resources from 1) untransformed resources from current manifest 2) transformed resources from sub packages
RawResources() (resource.ResourceCollection, error)
}
var _ Application = &applicationImpl{}
// Private implementation of the Application interface
type applicationImpl struct {
manifest *manifest.Manifest
loader loader.Loader
}
// NewApp parses the manifest at the path using the loader.
func New(loader loader.Loader) (Application, error) {
// load the manifest using the loader
manifestBytes, err := loader.Load(constants.KustomizeFileName)
if err != nil {
return nil, err
}
var m manifest.Manifest
err = unmarshal(manifestBytes, &m)
if err != nil {
return nil, err
}
return &applicationImpl{manifest: &m, loader: loader}, nil
}
// Resources computes and returns the resources from the manifest.
// The namehashing for configmap/secrets and resolving name reference is only done
// in the most top overlay once at the end of getting resources.
func (a *applicationImpl) Resources() (resource.ResourceCollection, error) {
res, err := a.SemiResources()
if err != nil {
return nil, err
}
t, err := a.getHashAndReferenceTransformer()
if err != nil {
return nil, err
}
err = t.Transform(res)
if err != nil {
return nil, err
}
return res, nil
}
// SemiResources computes and returns the resources without name hash and name reference for the app
func (a *applicationImpl) SemiResources() (resource.ResourceCollection, error) {
errs := &interror.ManifestErrors{}
raw, err := a.rawResources()
if err != nil {
errs.Append(err)
}
cms, err := resource.NewFromConfigMaps(a.loader, a.manifest.ConfigMapGenerator)
if err != nil {
errs.Append(err)
}
secrets, err := resource.NewFromSecretGenerators(a.loader.Root(), a.manifest.SecretGenerator)
if err != nil {
errs.Append(err)
}
res, err := resource.Merge(cms, secrets)
if err != nil {
return nil, err
}
allRes, err := resource.MergeWithOverride(raw, res)
if err != nil {
return nil, err
}
patches, err := resource.NewFromPatches(a.loader, a.manifest.Patches)
if err != nil {
errs.Append(err)
}
if len(errs.Get()) > 0 {
return nil, errs
}
t, err := a.getTransformer(patches)
if err != nil {
return nil, err
}
err = t.Transform(allRes)
if err != nil {
return nil, err
}
return allRes, nil
}
// RawResources computes and returns the raw resources from the manifest.
// The namehashing for configmap/secrets and resolving name reference is only done
// in the most top overlay once at the end of getting resources.
func (a *applicationImpl) RawResources() (resource.ResourceCollection, error) {
res, err := a.rawResources()
if err != nil {
return nil, err
}
t, err := a.getHashAndReferenceTransformer()
if err != nil {
return nil, err
}
err = t.Transform(res)
if err != nil {
return nil, err
}
return res, nil
}
func (a *applicationImpl) rawResources() (resource.ResourceCollection, error) {
subAppResources, errs := a.subAppResources()
resources, err := resource.NewFromResources(a.loader, a.manifest.Resources)
if err != nil {
errs.Append(err)
}
if len(errs.Get()) > 0 {
return nil, errs
}
return resource.Merge(resources, subAppResources)
}
func (a *applicationImpl) subAppResources() (resource.ResourceCollection, *interror.ManifestErrors) {
sliceOfSubAppResources := []resource.ResourceCollection{}
errs := &interror.ManifestErrors{}
for _, pkgPath := range a.manifest.Bases {
subloader, err := a.loader.New(pkgPath)
if err != nil {
errs.Append(err)
continue
}
subapp, err := New(subloader)
if err != nil {
errs.Append(err)
continue
}
// Gather all transformed resources from subpackages.
subAppResources, err := subapp.SemiResources()
if err != nil {
errs.Append(err)
continue
}
sliceOfSubAppResources = append(sliceOfSubAppResources, subAppResources)
}
allResources, err := resource.Merge(sliceOfSubAppResources...)
if err != nil {
errs.Append(err)
}
return allResources, errs
}
// getTransformer generates the following transformers:
// 1) apply overlay
// 2) name prefix
// 3) apply labels
// 4) apply annotations
func (a *applicationImpl) getTransformer(patches []*resource.Resource) (transformers.Transformer, error) {
ts := []transformers.Transformer{}
ot, err := transformers.NewOverlayTransformer(patches)
if err != nil {
return nil, err
}
ts = append(ts, ot)
npt, err := transformers.NewDefaultingNamePrefixTransformer(string(a.manifest.NamePrefix))
if err != nil {
return nil, err
}
ts = append(ts, npt)
lt, err := transformers.NewDefaultingLabelsMapTransformer(a.manifest.ObjectLabels)
if err != nil {
return nil, err
}
ts = append(ts, lt)
at, err := transformers.NewDefaultingAnnotationsMapTransformer(a.manifest.ObjectAnnotations)
if err != nil {
return nil, err
}
ts = append(ts, at)
return transformers.NewMultiTransformer(ts), nil
}
// getHashAndReferenceTransformer generates the following transformers:
// 1) name hash for configmap and secrests
// 2) apply name reference
func (a *applicationImpl) getHashAndReferenceTransformer() (transformers.Transformer, error) {
ts := []transformers.Transformer{}
nht := transformers.NewNameHashTransformer()
ts = append(ts, nht)
nrt, err := transformers.NewDefaultingNameReferenceTransformer()
if err != nil {
return nil, err
}
ts = append(ts, nrt)
return transformers.NewMultiTransformer(ts), nil
}
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)
}

237
app/application_test.go Normal file
View File

@@ -0,0 +1,237 @@
/*
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 app
import (
"encoding/base64"
"fmt"
"reflect"
"testing"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kubectl/pkg/kustomize/resource"
"k8s.io/kubectl/pkg/kustomize/types"
"k8s.io/kubectl/pkg/loader"
"k8s.io/kubectl/pkg/loader/loadertest"
)
func setupTest(t *testing.T) loader.Loader {
manifestContent := []byte(`apiVersion: manifest.k8s.io/v1alpha1
kind: Manifest
metadata:
name: nginx-app
namePrefix: foo-
objectLabels:
app: nginx
objectAnnotations:
note: This is a test annotation
resources:
- deployment.yaml
configMapGenerator:
- name: literalConfigMap
literals:
- DB_USERNAME=admin
- DB_PASSWORD=somepw
secretGenerator:
- name: secret
commands:
DB_USERNAME: "printf admin"
DB_PASSWORD: "printf somepw"
type: Opaque
`)
deploymentContent := []byte(`apiVersion: apps/v1
kind: Deployment
metadata:
name: dply1
`)
loader := loadertest.NewFakeLoader("/testpath")
err := loader.AddFile("/testpath/kustomize.yaml", manifestContent)
if err != nil {
t.Fatalf("Failed to setup fake loader.")
}
err = loader.AddFile("/testpath/deployment.yaml", deploymentContent)
if err != nil {
t.Fatalf("Failed to setup fake loader.")
}
return loader
}
func TestResources(t *testing.T) {
expected := resource.ResourceCollection{
types.GroupVersionKindName{
GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
Name: "dply1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "foo-dply1",
"labels": map[string]interface{}{
"app": "nginx",
},
"annotations": map[string]interface{}{
"note": "This is a test annotation",
},
},
"spec": map[string]interface{}{
"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",
},
},
},
},
},
},
},
types.GroupVersionKindName{
GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"},
Name: "literalConfigMap",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "foo-literalConfigMap-mc92bgcbh5",
"labels": map[string]interface{}{
"app": "nginx",
},
"annotations": map[string]interface{}{
"note": "This is a test annotation",
},
"creationTimestamp": nil,
},
"data": map[string]interface{}{
"DB_USERNAME": "admin",
"DB_PASSWORD": "somepw",
},
},
},
},
types.GroupVersionKindName{
GVK: schema.GroupVersionKind{Version: "v1", Kind: "Secret"},
Name: "secret",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "foo-secret-877fcfhgt5",
"labels": map[string]interface{}{
"app": "nginx",
},
"annotations": map[string]interface{}{
"note": "This is a test annotation",
},
"creationTimestamp": nil,
},
"type": string(corev1.SecretTypeOpaque),
"data": map[string]interface{}{
"DB_USERNAME": base64.StdEncoding.EncodeToString([]byte("admin")),
"DB_PASSWORD": base64.StdEncoding.EncodeToString([]byte("somepw")),
},
},
},
},
}
l := setupTest(t)
app, err := New(l)
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
actual, err := app.Resources()
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
if !reflect.DeepEqual(actual, expected) {
err = compareMap(actual, expected)
t.Fatalf("unexpected error: %v", err)
}
}
func TestRawResources(t *testing.T) {
expected := resource.ResourceCollection{
types.GroupVersionKindName{
GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
Name: "dply1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "dply1",
},
},
},
},
}
l := setupTest(t)
app, err := New(l)
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
actual, err := app.RawResources()
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
if err := compareMap(actual, expected); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func compareMap(m1, m2 resource.ResourceCollection) error {
if len(m1) != len(m2) {
keySet1 := []types.GroupVersionKindName{}
keySet2 := []types.GroupVersionKindName{}
for GVKn := range m1 {
keySet1 = append(keySet1, GVKn)
}
for GVKn := range m1 {
keySet2 = append(keySet2, GVKn)
}
return fmt.Errorf("maps has different number of entries: %#v doesn't equals %#v", keySet1, keySet2)
}
for GVKn, obj1 := range m1 {
obj2, found := m2[GVKn]
if !found {
return fmt.Errorf("%#v doesn't exist in %#v", GVKn, m2)
}
if !reflect.DeepEqual(obj1, obj2) {
return fmt.Errorf("%#v doesn't match %#v", obj1, obj2)
}
}
return nil
}

106
commands/addresource.go Normal file
View File

@@ -0,0 +1,106 @@
/*
Copyright 2017 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 commands
import (
"errors"
"fmt"
"io"
"github.com/spf13/cobra"
"k8s.io/kubectl/pkg/kustomize/constants"
"k8s.io/kubectl/pkg/kustomize/util/fs"
)
type addResourceOptions struct {
resourceFilePath string
}
// newCmdAddResource adds the name of a file containing a resource to the manifest.
func newCmdAddResource(out, errOut io.Writer, fsys fs.FileSystem) *cobra.Command {
var o addResourceOptions
cmd := &cobra.Command{
Use: "resource",
Short: "Add the name of a file containing a resource to the manifest.",
Long: "Add the name of a file containing a resource to the manifest.",
Example: `
add resource {filepath}`,
RunE: func(cmd *cobra.Command, args []string) error {
err := o.Validate(args)
if err != nil {
return err
}
err = o.Complete(cmd, args)
if err != nil {
return err
}
return o.RunAddResource(out, errOut, fsys)
},
}
return cmd
}
// Validate validates addResource command.
func (o *addResourceOptions) Validate(args []string) error {
if len(args) != 1 {
return errors.New("must specify a resource file")
}
o.resourceFilePath = args[0]
return nil
}
// Complete completes addResource command.
func (o *addResourceOptions) Complete(cmd *cobra.Command, args []string) error {
return nil
}
func stringInSlice(str string, list []string) bool {
for _, v := range list {
if v == str {
return true
}
}
return false
}
// RunAddResource runs addResource command (do real work).
func (o *addResourceOptions) RunAddResource(out, errOut io.Writer, fsys fs.FileSystem) error {
_, err := fsys.Stat(o.resourceFilePath)
if err != nil {
return err
}
mf, err := newManifestFile(constants.KustomizeFileName, fsys)
if err != nil {
return err
}
m, err := mf.read()
if err != nil {
return err
}
if stringInSlice(o.resourceFilePath, m.Resources) {
return fmt.Errorf("resource %s already in manifest", o.resourceFilePath)
}
m.Resources = append(m.Resources, o.resourceFilePath)
return mf.write(m)
}

View File

@@ -0,0 +1,94 @@
/*
Copyright 2017 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 commands
import (
"bytes"
"os"
"testing"
"strings"
"k8s.io/kubectl/pkg/kustomize/constants"
"k8s.io/kubectl/pkg/kustomize/util/fs"
)
const (
resourceFileName = "myWonderfulResource.yaml"
resourceFileContent = `
Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
`
)
func TestAddResourceHappyPath(t *testing.T) {
buf := bytes.NewBuffer([]byte{})
fakeFS := fs.MakeFakeFS()
fakeFS.WriteFile(resourceFileName, []byte(resourceFileContent))
fakeFS.WriteFile(constants.KustomizeFileName, []byte(manifestTemplate))
cmd := newCmdAddResource(buf, os.Stderr, fakeFS)
args := []string{resourceFileName}
err := cmd.RunE(cmd, args)
if err != nil {
t.Errorf("unexpected cmd error: %v", err)
}
content, err := fakeFS.ReadFile(constants.KustomizeFileName)
if err != nil {
t.Errorf("unexpected read error: %v", err)
}
if !strings.Contains(string(content), resourceFileName) {
t.Errorf("expected resource name in manifest")
}
}
func TestAddResourceAlreadyThere(t *testing.T) {
buf := bytes.NewBuffer([]byte{})
fakeFS := fs.MakeFakeFS()
fakeFS.WriteFile(resourceFileName, []byte(resourceFileContent))
fakeFS.WriteFile(constants.KustomizeFileName, []byte(manifestTemplate))
cmd := newCmdAddResource(buf, os.Stderr, fakeFS)
args := []string{resourceFileName}
err := cmd.RunE(cmd, args)
if err != nil {
t.Fatalf("unexpected cmd error: %v", err)
}
// adding an existing resource should return an error
err = cmd.RunE(cmd, args)
if err == nil {
t.Errorf("expected already there problem")
}
if err.Error() != "resource "+resourceFileName+" already in manifest" {
t.Errorf("unexpected error %v", err)
}
}
func TestAddResourceNoArgs(t *testing.T) {
buf := bytes.NewBuffer([]byte{})
fakeFS := fs.MakeFakeFS()
cmd := newCmdAddResource(buf, os.Stderr, fakeFS)
err := cmd.Execute()
if err == nil {
t.Errorf("expected error: %v", err)
}
if err.Error() != "must specify a resource file" {
t.Errorf("incorrect error: %v", err.Error())
}
}

111
commands/build.go Normal file
View File

@@ -0,0 +1,111 @@
/*
Copyright 2017 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 commands
import (
"fmt"
"io"
"os"
"path/filepath"
"github.com/spf13/cobra"
"errors"
"k8s.io/kubectl/pkg/kustomize/app"
"k8s.io/kubectl/pkg/kustomize/constants"
kutil "k8s.io/kubectl/pkg/kustomize/util"
"k8s.io/kubectl/pkg/kustomize/util/fs"
"k8s.io/kubectl/pkg/loader"
)
type buildOptions struct {
manifestPath string
}
// newCmdBuild creates a new build command.
func newCmdBuild(out, errOut io.Writer, fs fs.FileSystem) *cobra.Command {
var o buildOptions
cmd := &cobra.Command{
Use: "build [path]",
Short: "Print current configuration per contents of " + constants.KustomizeFileName,
Example: `
# Use the kustomize.yaml file under somedir/ to generate a set of api resources.
build somedir/`,
Run: func(cmd *cobra.Command, args []string) {
err := o.Validate(args)
if err != nil {
fmt.Fprintf(errOut, "error: %v\n", err)
os.Exit(1)
}
err = o.RunBuild(out, errOut, fs)
if err != nil {
fmt.Fprintf(errOut, "error: %v\n", err)
os.Exit(1)
}
},
}
return cmd
}
// Validate validates build command.
func (o *buildOptions) Validate(args []string) error {
if len(args) > 1 {
return errors.New("specify one path to manifest")
}
if len(args) == 0 {
o.manifestPath = "./"
return nil
}
o.manifestPath = args[0]
return nil
}
// RunBuild runs build command.
func (o *buildOptions) RunBuild(out, errOut io.Writer, fs fs.FileSystem) error {
l := loader.Init([]loader.SchemeLoader{loader.NewFileLoader(fs)})
absPath, err := filepath.Abs(o.manifestPath)
if err != nil {
return err
}
rootLoader, err := l.New(absPath)
if err != nil {
return err
}
application, err := app.New(rootLoader)
if err != nil {
return err
}
allResources, err := application.Resources()
if err != nil {
return err
}
// Output the objects.
res, err := kutil.Encode(allResources)
if err != nil {
return err
}
_, err = out.Write(res)
return err
}

150
commands/build_test.go Normal file
View File

@@ -0,0 +1,150 @@
/*
Copyright 2017 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 commands
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"github.com/ghodss/yaml"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/kubectl/pkg/kustomize/util/fs"
)
type buildTestCase struct {
Description string `yaml:"description"`
Args []string `yaml:"args"`
Filename string `yaml:"filename"`
// path to the file that contains the expected output
ExpectedStdout string `yaml:"expectedStdout"`
ExpectedError string `yaml:"expectedError"`
}
func TestBuildValidate(t *testing.T) {
var cases = []struct {
name string
args []string
path string
erMsg string
}{
{"noargs", []string{}, "./", ""},
{"file", []string{"beans"}, "beans", ""},
{"path", []string{"a/b/c"}, "a/b/c", ""},
{"path", []string{"too", "many"}, "", "specify one path to manifest"},
}
for _, mycase := range cases {
opts := buildOptions{}
e := opts.Validate(mycase.args)
if len(mycase.erMsg) > 0 {
if e == nil {
t.Errorf("%s: Expected an error %v", mycase.name, mycase.erMsg)
}
if e.Error() != mycase.erMsg {
t.Errorf("%s: Expected error %s, but got %v", mycase.name, mycase.erMsg, e)
}
continue
}
if e != nil {
t.Errorf("%s: unknown error %v", mycase.name, e)
continue
}
if opts.manifestPath != mycase.path {
t.Errorf("%s: expected path '%s', got '%s'", mycase.name, mycase.path, opts.manifestPath)
}
}
}
func TestBuild(t *testing.T) {
const updateEnvVar = "UPDATE_KUSTOMIZE_EXPECTED_DATA"
updateKustomizeExpected := os.Getenv(updateEnvVar) == "true"
fs := fs.MakeRealFS()
testcases := sets.NewString()
filepath.Walk("testdata", func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if path == "testdata" {
return nil
}
name := filepath.Base(path)
if info.IsDir() {
if strings.HasPrefix(name, "testcase-") {
testcases.Insert(strings.TrimPrefix(name, "testcase-"))
}
return filepath.SkipDir
}
return nil
})
// sanity check that we found the right folder
if !testcases.Has("simple") {
t.Fatalf("Error locating testcases")
}
for _, testcaseName := range testcases.List() {
t.Run(testcaseName, func(t *testing.T) {
name := testcaseName
testcase := buildTestCase{}
testcaseDir := filepath.Join("testdata", "testcase-"+name)
testcaseData, err := ioutil.ReadFile(filepath.Join(testcaseDir, "test.yaml"))
if err != nil {
t.Fatalf("%s: %v", name, err)
}
if err := yaml.Unmarshal(testcaseData, &testcase); err != nil {
t.Fatalf("%s: %v", name, err)
}
ops := &buildOptions{
manifestPath: testcase.Filename,
}
buf := bytes.NewBuffer([]byte{})
err = ops.RunBuild(buf, os.Stderr, fs)
switch {
case err != nil && len(testcase.ExpectedError) == 0:
t.Errorf("unexpected error: %v", err)
case err != nil && len(testcase.ExpectedError) != 0:
if !strings.Contains(err.Error(), testcase.ExpectedError) {
t.Errorf("expected error to contain %q but got: %v", testcase.ExpectedError, err)
}
return
case err == nil && len(testcase.ExpectedError) != 0:
t.Errorf("unexpected no error")
}
actualBytes := buf.Bytes()
if !updateKustomizeExpected {
expectedBytes, err := ioutil.ReadFile(testcase.ExpectedStdout)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if !reflect.DeepEqual(actualBytes, expectedBytes) {
t.Errorf("%s\ndoesn't equal expected:\n%s\n", actualBytes, expectedBytes)
}
} else {
ioutil.WriteFile(testcase.ExpectedStdout, actualBytes, 0644)
}
})
}
}

123
commands/commands.go Normal file
View File

@@ -0,0 +1,123 @@
/*
Copyright 2017 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 commands
import (
"flag"
"io"
"os"
"github.com/spf13/cobra"
"k8s.io/kubectl/cmd/kustomize/version"
"k8s.io/kubectl/pkg/kustomize/util/fs"
)
// NewDefaultCommand returns the default (aka root) command for kustomize command.
func NewDefaultCommand() *cobra.Command {
fsys := fs.MakeRealFS()
stdOut, stdErr := os.Stdout, os.Stderr
c := &cobra.Command{
Use: "kustomize",
Short: "kustomize manages declarative configuration of Kubernetes",
Long: `
kustomize manages declarative configuration of Kubernetes.
More info at https://github.com/kubernetes/kubectl/tree/master/cmd/kustomize
`,
}
c.AddCommand(
newCmdBuild(stdOut, stdErr, fsys),
newCmdDiff(stdOut, stdErr, fsys),
newCmdInit(stdOut, stdErr, fsys),
newCmdEdit(stdOut, stdErr, fsys),
version.NewCmdVersion(stdOut),
)
c.PersistentFlags().AddGoFlagSet(flag.CommandLine)
// Workaround for this issue:
// https://github.com/kubernetes/kubernetes/issues/17162
flag.CommandLine.Parse([]string{})
return c
}
// newCmdEdit returns an instance of 'edit' subcommand.
func newCmdEdit(stdOut, stdErr io.Writer, fsys fs.FileSystem) *cobra.Command {
c := &cobra.Command{
Use: "edit",
Short: "Edits a manifest file",
Long: "",
Example: `
# Adds a configmap to the manifest
kustomize edit add configmap NAME --from-literal=k=v
# Sets the nameprefix field
kustomize edit set nameprefix <prefix-value>
`,
Args: cobra.MinimumNArgs(1),
}
c.AddCommand(
newCmdAdd(stdOut, stdErr, fsys),
newCmdSet(stdOut, stdErr, fsys),
)
return c
}
// newAddCommand returns an instance of 'add' subcommand.
func newCmdAdd(stdOut, stdErr io.Writer, fsys fs.FileSystem) *cobra.Command {
c := &cobra.Command{
Use: "add",
Short: "Adds configmap/resource/secret to the manifest.",
Long: "",
Example: `
# Adds a configmap to the manifest
kustomize edit add configmap NAME --from-literal=k=v
# Adds a secret to the manifest
kustomize edit add secret NAME --from-literal=k=v
# Adds a resource to the manifest
kustomize edit add resource <filepath>
`,
Args: cobra.MinimumNArgs(1),
}
c.AddCommand(
newCmdAddResource(stdOut, stdErr, fsys),
newCmdAddConfigMap(stdErr, fsys),
)
return c
}
// newSetCommand returns an instance of 'set' subcommand.
func newCmdSet(stdOut, stdErr io.Writer, fsys fs.FileSystem) *cobra.Command {
c := &cobra.Command{
Use: "set",
Short: "Sets the value of different fields in manifest.",
Long: "",
Example: `
# Sets the nameprefix field
kustomize edit set nameprefix <prefix-value>
`,
Args: cobra.MinimumNArgs(1),
}
c.AddCommand(
newCmdSetNamePrefix(stdOut, stdErr, fsys),
)
return c
}

123
commands/configmap.go Normal file
View File

@@ -0,0 +1,123 @@
/*
Copyright 2017 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 commands
import (
"fmt"
"io"
"github.com/spf13/cobra"
manifest "k8s.io/kubectl/pkg/apis/manifest/v1alpha1"
"k8s.io/kubectl/pkg/kustomize/configmapandsecret"
"k8s.io/kubectl/pkg/kustomize/constants"
"k8s.io/kubectl/pkg/kustomize/util/fs"
)
func newCmdAddConfigMap(errOut io.Writer, fsys fs.FileSystem) *cobra.Command {
var config dataConfig
cmd := &cobra.Command{
Use: "configmap NAME [--from-file=[key=]source] [--from-literal=key1=value1]",
Short: "Adds a configmap to the manifest.",
Long: "",
Example: `
# Adds a configmap to the Manifest (with a specified key)
kustomize edit add configmap my-configmap --from-file=my-key=file/path --from-literal=my-literal=12345
# Adds a configmap to the Manifest (key is the filename)
kustomize edit add configmap my-configmap --from-file=file/path
# Adds a configmap from env-file
kustomize edit add configmap my-configmap --from-env-file=env/path.env
`,
RunE: func(_ *cobra.Command, args []string) error {
err := config.Validate(args)
if err != nil {
return err
}
// Load in the manifest file.
mf, err := newManifestFile(constants.KustomizeFileName, fsys)
if err != nil {
return err
}
m, err := mf.read()
if err != nil {
return err
}
// Add the config map to the manifest.
err = addConfigMap(m, config)
if err != nil {
return err
}
// Write out the manifest with added configmap.
return mf.write(m)
},
}
cmd.Flags().StringSliceVar(&config.FileSources, "from-file", []string{}, "Key file can be specified using its file path, in which case file basename will be used as configmap key, or optionally with a key and file path, in which case the given key will be used. Specifying a directory will iterate each named file in the directory whose basename is a valid configmap key.")
cmd.Flags().StringArrayVar(&config.LiteralSources, "from-literal", []string{}, "Specify a key and literal value to insert in configmap (i.e. mykey=somevalue)")
cmd.Flags().StringVar(&config.EnvFileSource, "from-env-file", "", "Specify the path to a file to read lines of key=val pairs to create a configmap (i.e. a Docker .env file).")
return cmd
}
// addConfigMap updates a configmap within a manifest, using the data in config.
// Note: error may leave manifest in an undefined state. Suggest passing a copy
// of manifest.
func addConfigMap(m *manifest.Manifest, config dataConfig) error {
cm := getOrCreateConfigMap(m, config.Name)
err := mergeData(&cm.DataSources, config)
if err != nil {
return err
}
// Validate manifest's configmap by trying to create corev1.configmap.
_, _, err = configmapandsecret.MakeConfigmapAndGenerateName(*cm)
if err != nil {
return err
}
return nil
}
func getOrCreateConfigMap(m *manifest.Manifest, name string) *manifest.ConfigMapArgs {
for i, v := range m.ConfigMapGenerator {
if name == v.Name {
return &m.ConfigMapGenerator[i]
}
}
// config map not found, create new one and add it to the manifest.
cm := &manifest.ConfigMapArgs{Name: name}
m.ConfigMapGenerator = append(m.ConfigMapGenerator, *cm)
return &m.ConfigMapGenerator[len(m.ConfigMapGenerator)-1]
}
func mergeData(src *manifest.DataSources, config dataConfig) error {
src.LiteralSources = append(src.LiteralSources, config.LiteralSources...)
src.FileSources = append(src.FileSources, config.FileSources...)
if src.EnvSource != "" && src.EnvSource != config.EnvFileSource {
return fmt.Errorf("updating existing env source '%s' not allowed.", src.EnvSource)
}
src.EnvSource = config.EnvFileSource
return nil
}

129
commands/configmap_test.go Normal file
View File

@@ -0,0 +1,129 @@
/*
Copyright 2017 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 commands
import (
"testing"
manifest "k8s.io/kubectl/pkg/apis/manifest/v1alpha1"
"k8s.io/kubectl/pkg/kustomize/util/fs"
)
func TestNewAddConfigMapIsNotNil(t *testing.T) {
if newCmdAddConfigMap(nil, fs.MakeFakeFS()) == nil {
t.Fatal("newCmdAddConfigMap shouldn't be nil")
}
}
func TestGetOrCreateConfigMap(t *testing.T) {
cmName := "test-config-name"
manifest := &manifest.Manifest{
NamePrefix: "test-name-prefix",
}
if len(manifest.ConfigMapGenerator) != 0 {
t.Fatal("Initial manifest should not have any configmaps")
}
cm := getOrCreateConfigMap(manifest, cmName)
if cm == nil {
t.Fatalf("ConfigMap should always be non-nil")
}
if len(manifest.ConfigMapGenerator) != 1 {
t.Fatalf("Manifest should have newly created configmap")
}
if &manifest.ConfigMapGenerator[len(manifest.ConfigMapGenerator)-1] != cm {
t.Fatalf("Pointer address for newly inserted configmap should be same")
}
existingCM := getOrCreateConfigMap(manifest, cmName)
if existingCM != cm {
t.Fatalf("should have returned an existing cm with name: %v", cmName)
}
if len(manifest.ConfigMapGenerator) != 1 {
t.Fatalf("Should not insert configmap for an existing name: %v", cmName)
}
}
func TestMergeData_LiteralSources(t *testing.T) {
ds := &manifest.DataSources{}
err := mergeData(ds, dataConfig{LiteralSources: []string{"k1=v1"}})
if err != nil {
t.Fatalf("Merge initial literal source should not return error")
}
if len(ds.LiteralSources) != 1 {
t.Fatalf("Initial literal source should have been added")
}
err = mergeData(ds, dataConfig{LiteralSources: []string{"k2=v2"}})
if err != nil {
t.Fatalf("Merge second literal source should not return error")
}
if len(ds.LiteralSources) != 2 {
t.Fatalf("Second literal source should have been added")
}
}
func TestMergeData_FileSources(t *testing.T) {
ds := &manifest.DataSources{}
err := mergeData(ds, dataConfig{FileSources: []string{"file1"}})
if err != nil {
t.Fatalf("Merge initial file source should not return error")
}
if len(ds.FileSources) != 1 {
t.Fatalf("Initial file source should have been added")
}
err = mergeData(ds, dataConfig{FileSources: []string{"file2"}})
if err != nil {
t.Fatalf("Merge second file source should not return error")
}
if len(ds.FileSources) != 2 {
t.Fatalf("Second file source should have been added")
}
}
func TestMergeData_EnvSource(t *testing.T) {
envFileName := "env1"
envFileName2 := "env2"
ds := &manifest.DataSources{}
err := mergeData(ds, dataConfig{EnvFileSource: envFileName})
if err != nil {
t.Fatalf("Merge initial env source should not return error")
}
if ds.EnvSource != envFileName {
t.Fatalf("Initial env source filename should have been added")
}
err = mergeData(ds, dataConfig{EnvFileSource: envFileName2})
if err == nil {
t.Fatalf("Updating env source should return an error")
}
}

50
commands/data_config.go Normal file
View File

@@ -0,0 +1,50 @@
/*
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 commands
import (
"fmt"
)
// dataConfig encapsulates the options for add configmap/Secret commands.
type dataConfig struct {
// Name of configMap/Secret (required)
Name string
// FileSources to derive the configMap/Secret from (optional)
FileSources []string
// LiteralSources to derive the configMap/Secret from (optional)
LiteralSources []string
// EnvFileSource to derive the configMap/Secret from (optional)
// TODO: Rationalize this name with Generic.EnvSource
EnvFileSource string
}
// Validate validates required fields are set to support structured generation.
func (a *dataConfig) Validate(args []string) error {
if len(args) != 1 {
return fmt.Errorf("name must be specified once")
}
a.Name = args[0]
if len(a.EnvFileSource) == 0 && len(a.FileSources) == 0 && len(a.LiteralSources) == 0 {
return fmt.Errorf("at least from-env-file, or from-file or from-literal must be set")
}
if len(a.EnvFileSource) > 0 && (len(a.FileSources) > 0 || len(a.LiteralSources) > 0) {
return fmt.Errorf("from-env-file cannot be combined with from-file or from-literal")
}
// TODO: Should we check if the path exists? if it's valid, if it's within the same (sub-)directory?
return nil
}

View File

@@ -0,0 +1,83 @@
/*
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 commands
import (
"testing"
)
func TestDataConfigValidation_NoName(t *testing.T) {
config := dataConfig{}
if config.Validate([]string{}) == nil {
t.Fatal("Validation should fail if no name is specified")
}
}
func TestDataConfigValidation_MoreThanOneName(t *testing.T) {
config := dataConfig{}
if config.Validate([]string{"name", "othername"}) == nil {
t.Fatal("Validation should fail if more than one name is specified")
}
}
func TestDataConfigValidation_Flags(t *testing.T) {
tests := []struct {
name string
config dataConfig
shouldFail bool
}{
{
name: "env-file-source and literal are both set",
config: dataConfig{
LiteralSources: []string{"one", "two"},
EnvFileSource: "three",
},
shouldFail: true,
},
{
name: "env-file-source and from-file are both set",
config: dataConfig{
FileSources: []string{"one", "two"},
EnvFileSource: "three",
},
shouldFail: true,
},
{
name: "we don't have any option set",
config: dataConfig{},
shouldFail: true,
},
{
name: "we have from-file and literal ",
config: dataConfig{
LiteralSources: []string{"one", "two"},
FileSources: []string{"three", "four"},
},
shouldFail: false,
},
}
for _, test := range tests {
if test.config.Validate([]string{"name"}) == nil && test.shouldFail {
t.Fatalf("Validation should fail if %s", test.name)
} else if test.config.Validate([]string{"name"}) != nil && !test.shouldFail {
t.Fatalf("Validation should succeed if %s", test.name)
}
}
}

124
commands/diff.go Normal file
View File

@@ -0,0 +1,124 @@
/*
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 commands
import (
"errors"
"io"
"path/filepath"
"github.com/spf13/cobra"
"k8s.io/kubectl/pkg/kustomize/app"
"k8s.io/kubectl/pkg/kustomize/util"
"k8s.io/kubectl/pkg/kustomize/util/fs"
"k8s.io/kubectl/pkg/loader"
"k8s.io/utils/exec"
)
type diffOptions struct {
manifestPath string
}
// newCmdDiff makes the diff command.
func newCmdDiff(out, errOut io.Writer, fs fs.FileSystem) *cobra.Command {
var o diffOptions
cmd := &cobra.Command{
Use: "diff",
Short: "diff between transformed resources and untransformed resources",
Long: "diff between transformed resources and untransformed resources and the subpackages are all transformed.",
Example: `diff -f .`,
RunE: func(cmd *cobra.Command, args []string) error {
err := o.Validate(cmd, args)
if err != nil {
return err
}
err = o.Complete(cmd, args)
if err != nil {
return err
}
return o.RunDiff(out, errOut, fs)
},
}
cmd.Flags().StringVarP(&o.manifestPath, "filename", "f", "", "Pass in a kustomize.yaml file or a directory that contains the file.")
cmd.MarkFlagRequired("filename")
return cmd
}
// Validate validates diff command.
func (o *diffOptions) Validate(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
return errors.New("The diff command takes no arguments.")
}
return nil
}
// Complete completes diff command.
func (o *diffOptions) Complete(cmd *cobra.Command, args []string) error {
return nil
}
// RunDiff gets the differences between Application.Resources() and Application.RawResources().
func (o *diffOptions) RunDiff(out, errOut io.Writer, fs fs.FileSystem) error {
printer := util.Printer{}
diff := util.DiffProgram{
Exec: exec.New(),
Stdout: out,
Stderr: errOut,
}
l := loader.Init([]loader.SchemeLoader{loader.NewFileLoader(fs)})
absPath, err := filepath.Abs(o.manifestPath)
if err != nil {
return err
}
rootLoader, err := l.New(absPath)
if err != nil {
return err
}
application, err := app.New(rootLoader)
if err != nil {
return err
}
resources, err := application.Resources()
if err != nil {
return err
}
rawResources, err := application.RawResources()
if err != nil {
return err
}
transformedDir, err := util.WriteToDir(resources, "transformed", printer)
if err != nil {
return err
}
defer transformedDir.Delete()
noopDir, err := util.WriteToDir(rawResources, "noop", printer)
if err != nil {
return err
}
defer noopDir.Delete()
return diff.Run(noopDir.Name, transformedDir.Name)
}

124
commands/diff_test.go Normal file
View File

@@ -0,0 +1,124 @@
/*
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 commands
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"regexp"
"strings"
"testing"
"github.com/ghodss/yaml"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/kubectl/pkg/kustomize/util/fs"
)
type DiffTestCase struct {
Description string `yaml:"description"`
Args []string `yaml:"args"`
Filename string `yaml:"filename"`
// path to the file that contains the expected output
ExpectedDiff string `yaml:"expectedDiff"`
ExpectedError string `yaml:"expectedError"`
}
func TestDiff(t *testing.T) {
const updateEnvVar = "UPDATE_KUSTOMIZE_EXPECTED_DATA"
updateKustomizeExpected := os.Getenv(updateEnvVar) == "true"
noopDir, _ := regexp.Compile(`/tmp/noop-[0-9]*/`)
transformedDir, _ := regexp.Compile(`/tmp/transformed-[0-9]*/`)
timestamp, _ := regexp.Compile(`[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1]) (2[0-3]|[01][0-9]):[0-5][0-9]:[0-5][0-9].[0-9]* [+-]{1}[0-9]{4}`)
fs := fs.MakeRealFS()
testcases := sets.NewString()
filepath.Walk("testdata", func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if path == "testdata" {
return nil
}
name := filepath.Base(path)
if info.IsDir() {
if strings.HasPrefix(name, "testcase-") {
testcases.Insert(strings.TrimPrefix(name, "testcase-"))
}
return filepath.SkipDir
}
return nil
})
// sanity check that we found the right folder
if !testcases.Has("simple") {
t.Fatalf("Error locating testcases")
}
for _, testcaseName := range testcases.List() {
t.Run(testcaseName, func(t *testing.T) {
name := testcaseName
testcase := DiffTestCase{}
testcaseDir := filepath.Join("testdata", "testcase-"+name)
testcaseData, err := ioutil.ReadFile(filepath.Join(testcaseDir, "test.yaml"))
if err != nil {
t.Fatalf("%s: %v", name, err)
}
if err := yaml.Unmarshal(testcaseData, &testcase); err != nil {
t.Fatalf("%s: %v", name, err)
}
diffOps := &diffOptions{
manifestPath: testcase.Filename,
}
buf := bytes.NewBuffer([]byte{})
err = diffOps.RunDiff(buf, os.Stderr, fs)
switch {
case err != nil && len(testcase.ExpectedError) == 0:
t.Errorf("unexpected error: %v", err)
case err != nil && len(testcase.ExpectedError) != 0:
if !strings.Contains(err.Error(), testcase.ExpectedError) {
t.Errorf("expected error to contain %q but got: %v", testcase.ExpectedError, err)
}
return
case err == nil && len(testcase.ExpectedError) != 0:
t.Errorf("unexpected no error")
}
actualString := string(buf.Bytes())
actualString = noopDir.ReplaceAllString(actualString, "/tmp/noop/")
actualString = transformedDir.ReplaceAllString(actualString, "/tmp/transformed/")
actualString = timestamp.ReplaceAllString(actualString, "YYYY-MM-DD HH:MM:SS")
actualBytes := []byte(actualString)
if !updateKustomizeExpected {
expectedBytes, err := ioutil.ReadFile(testcase.ExpectedDiff)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if !reflect.DeepEqual(actualBytes, expectedBytes) {
t.Errorf("%s\ndoesn't equal expected:\n%s\n", actualBytes, expectedBytes)
}
} else {
ioutil.WriteFile(testcase.ExpectedDiff, actualBytes, 0644)
}
})
}
}

100
commands/init.go Normal file
View File

@@ -0,0 +1,100 @@
/*
Copyright 2017 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 commands
import (
"fmt"
"io"
"errors"
"github.com/spf13/cobra"
"k8s.io/kubectl/pkg/kustomize/constants"
"k8s.io/kubectl/pkg/kustomize/util/fs"
)
const manifestTemplate = `apiVersion: manifest.k8s.io/v1alpha1
kind: Manifest
metadata:
name: helloworld
description: helloworld does useful stuff.
namePrefix: some-prefix
# Labels to add to all objects and selectors.
# These labels would also be used to form the selector for apply --prune
# Named differently than “labels” to avoid confusion with metadata for this object
objectLabels:
app: helloworld
objectAnnotations:
note: This is an example annotation
resources: []
#- service.yaml
#- ../some-dir/
# There could also be configmaps in Base, which would make these overlays
configMapGenerator: []
# There could be secrets in Base, if just using a fork/rebase workflow
secretGenerator: []
`
type initOptions struct {
}
// NewCmdInit makes the init command.
func newCmdInit(out, errOut io.Writer, fs fs.FileSystem) *cobra.Command {
var o initOptions
cmd := &cobra.Command{
Use: "init",
Short: "Creates a file called \"" + constants.KustomizeFileName + "\" in the current directory",
Long: "Creates a file called \"" +
constants.KustomizeFileName + "\" in the current directory with example values.",
Example: `init`,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
err := o.Validate(cmd, args)
if err != nil {
return err
}
err = o.Complete(cmd, args)
if err != nil {
return err
}
return o.RunInit(out, errOut, fs)
},
}
return cmd
}
// Validate validates init command.
func (o *initOptions) Validate(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
return errors.New("The init command takes no arguments.")
}
return nil
}
// Complete completes init command.
func (o *initOptions) Complete(cmd *cobra.Command, args []string) error {
return nil
}
// RunInit writes a manifest file.
func (o *initOptions) RunInit(out, errOut io.Writer, fs fs.FileSystem) error {
if _, err := fs.Stat(constants.KustomizeFileName); err == nil {
return fmt.Errorf("%q already exists", constants.KustomizeFileName)
}
return fs.WriteFile(constants.KustomizeFileName, []byte(manifestTemplate))
}

62
commands/init_test.go Normal file
View File

@@ -0,0 +1,62 @@
/*
Copyright 2017 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 commands
import (
"bytes"
"os"
"testing"
"k8s.io/kubectl/pkg/kustomize/constants"
"k8s.io/kubectl/pkg/kustomize/util/fs"
)
func TestInitHappyPath(t *testing.T) {
buf := bytes.NewBuffer([]byte{})
fakeFS := fs.MakeFakeFS()
cmd := newCmdInit(buf, os.Stderr, fakeFS)
err := cmd.Execute()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
f, err := fakeFS.Open(constants.KustomizeFileName)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
file := f.(*fs.FakeFile)
if !file.ContentMatches([]byte(manifestTemplate)) {
t.Fatalf("actual: %v doesn't match expected: %v",
string(file.GetContent()), manifestTemplate)
}
}
func TestInitFileAlreadyExist(t *testing.T) {
content := "hey there"
fakeFS := fs.MakeFakeFS()
fakeFS.WriteFile(constants.KustomizeFileName, []byte(content))
buf := bytes.NewBuffer([]byte{})
cmd := newCmdInit(buf, os.Stderr, fakeFS)
err := cmd.Execute()
if err == nil {
t.Fatalf("expected error")
}
if err.Error() != `"`+constants.KustomizeFileName+`" already exists` {
t.Fatalf("unexpected error: %v", err)
}
}

View File

@@ -0,0 +1,90 @@
/*
Copyright 2017 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 commands
import (
"errors"
"io"
"github.com/spf13/cobra"
"k8s.io/kubectl/pkg/kustomize/constants"
"k8s.io/kubectl/pkg/kustomize/util/fs"
)
type setNamePrefixOptions struct {
prefix string
}
// newCmdSetNamePrefix sets the value of the namePrefix field in the manifest.
func newCmdSetNamePrefix(out, errOut io.Writer, fsys fs.FileSystem) *cobra.Command {
var o setNamePrefixOptions
cmd := &cobra.Command{
Use: "nameprefix",
Short: "Sets the value of the namePrefix field in the manifest.",
Long: "Sets the value of the namePrefix field in the manifest.",
//
Example: `
The command
set nameprefix acme-
will add the field "namePrefix: acme-" to the manifest file if it doesn't exist,
and overwrite the value with "acme-" if the field does exist.
`,
RunE: func(cmd *cobra.Command, args []string) error {
err := o.Validate(args)
if err != nil {
return err
}
err = o.Complete(cmd, args)
if err != nil {
return err
}
return o.RunSetNamePrefix(out, errOut, fsys)
},
}
return cmd
}
// Validate validates setNamePrefix command.
func (o *setNamePrefixOptions) Validate(args []string) error {
if len(args) != 1 {
return errors.New("must specify exactly one prefix value")
}
// TODO: add further validation on the value.
o.prefix = args[0]
return nil
}
// Complete completes setNamePrefix command.
func (o *setNamePrefixOptions) Complete(cmd *cobra.Command, args []string) error {
return nil
}
// RunSetNamePrefix runs setNamePrefix command (does real work).
func (o *setNamePrefixOptions) RunSetNamePrefix(out, errOut io.Writer, fsys fs.FileSystem) error {
mf, err := newManifestFile(constants.KustomizeFileName, fsys)
if err != nil {
return err
}
m, err := mf.read()
if err != nil {
return err
}
m.NamePrefix = o.prefix
return mf.write(m)
}

View File

@@ -0,0 +1,66 @@
/*
Copyright 2017 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 commands
import (
"bytes"
"os"
"testing"
"strings"
"k8s.io/kubectl/pkg/kustomize/constants"
"k8s.io/kubectl/pkg/kustomize/util/fs"
)
const (
goodPrefixValue = "acme-"
)
func TestSetNamePrefixHappyPath(t *testing.T) {
buf := bytes.NewBuffer([]byte{})
fakeFS := fs.MakeFakeFS()
fakeFS.WriteFile(constants.KustomizeFileName, []byte(manifestTemplate))
cmd := newCmdSetNamePrefix(buf, os.Stderr, fakeFS)
args := []string{goodPrefixValue}
err := cmd.RunE(cmd, args)
if err != nil {
t.Errorf("unexpected cmd error: %v", err)
}
content, err := fakeFS.ReadFile(constants.KustomizeFileName)
if err != nil {
t.Errorf("unexpected read error: %v", err)
}
if !strings.Contains(string(content), goodPrefixValue) {
t.Errorf("expected prefix value in manifest")
}
}
func TestSetNamePrefixNoArgs(t *testing.T) {
buf := bytes.NewBuffer([]byte{})
fakeFS := fs.MakeFakeFS()
cmd := newCmdSetNamePrefix(buf, os.Stderr, fakeFS)
err := cmd.Execute()
if err == nil {
t.Errorf("expected error: %v", err)
}
if err.Error() != "must specify exactly one prefix value" {
t.Errorf("incorrect error: %v", err.Error())
}
}

View File

@@ -0,0 +1,58 @@
diff -u -N /tmp/noop/apps_v1beta2_Deployment_nginx.yaml /tmp/transformed/apps_v1beta2_Deployment_nginx.yaml
--- /tmp/noop/apps_v1beta2_Deployment_nginx.yaml YYYY-MM-DD HH:MM:SS
+++ /tmp/transformed/apps_v1beta2_Deployment_nginx.yaml YYYY-MM-DD HH:MM:SS
@@ -1,14 +1,27 @@
apiVersion: apps/v1beta2
kind: Deployment
metadata:
+ annotations:
+ note: This is a test annotation
labels:
- app: nginx
- name: nginx
+ 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: nginx
+ app: mynginx
+ org: example.com
+ team: foo
spec:
containers:
- image: nginx
diff -u -N /tmp/noop/v1_Service_nginx.yaml /tmp/transformed/v1_Service_nginx.yaml
--- /tmp/noop/v1_Service_nginx.yaml YYYY-MM-DD HH:MM:SS
+++ /tmp/transformed/v1_Service_nginx.yaml YYYY-MM-DD HH:MM:SS
@@ -1,11 +1,17 @@
apiVersion: v1
kind: Service
metadata:
+ annotations:
+ note: This is a test annotation
labels:
- app: nginx
- name: nginx
+ app: mynginx
+ org: example.com
+ team: foo
+ name: team-foo-nginx
spec:
ports:
- port: 80
selector:
- app: nginx
+ app: mynginx
+ org: example.com
+ team: foo

View File

@@ -0,0 +1,46 @@
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

View File

@@ -0,0 +1,15 @@
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: nginx
labels:
app: nginx
spec:
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx

View File

@@ -0,0 +1,14 @@
apiVersion: manifest.k8s.io/v1alpha1
kind: Manifest
metadata:
name: nginx-app
namePrefix: team-foo-
objectLabels:
app: mynginx
org: example.com
team: foo
objectAnnotations:
note: This is a test annotation
resources:
- deployment.yaml
- service.yaml

View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- port: 80
selector:
app: nginx

View File

@@ -0,0 +1,5 @@
description: base only
args: []
filename: testdata/testcase-base-only/in
expectedStdout: testdata/testcase-base-only/expected.yaml
expectedDiff: testdata/testcase-base-only/expected.diff

View File

@@ -0,0 +1,20 @@
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

View File

@@ -0,0 +1,12 @@
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- name: nginx
env:
- name: ENABLE_FEATURE_FOO
value: FALSE

View File

@@ -0,0 +1,16 @@
apiVersion: manifest.k8s.io/v1alpha1
kind: Manifest
metadata:
name: nginx-app
namePrefix: staging-
objectLabels:
env: staging
patches:
- deployment-patch2.yaml
- deployment-patch1.yaml
bases:
- ../package/
configMapGenerator:
- name: configmap-in-overlay
literals:
- hello=world

View File

@@ -0,0 +1,24 @@
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

View File

@@ -0,0 +1,18 @@
apiVersion: manifest.k8s.io/v1alpha1
kind: Manifest
metadata:
name: nginx-app
namePrefix: team-foo-
objectLabels:
app: mynginx
org: example.com
team: foo
objectAnnotations:
note: This is a test annotation
resources:
- deployment.yaml
- service.yaml
configMapGenerator:
- name: configmap-in-base
literals:
- foo=bar

View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- port: 80
selector:
app: nginx

View File

@@ -0,0 +1,4 @@
description: conflict between multiple patches
args: []
filename: testdata/testcase-multiple-patches-conflict/in/overlay/
expectedError: conflict

View File

@@ -0,0 +1,99 @@
diff -u -N /tmp/noop/apps_v1beta2_Deployment_nginx.yaml /tmp/transformed/apps_v1beta2_Deployment_nginx.yaml
--- /tmp/noop/apps_v1beta2_Deployment_nginx.yaml YYYY-MM-DD HH:MM:SS
+++ /tmp/transformed/apps_v1beta2_Deployment_nginx.yaml YYYY-MM-DD HH:MM:SS
@@ -5,13 +5,15 @@
note: This is a test annotation
labels:
app: mynginx
+ env: staging
org: example.com
team: foo
- name: team-foo-nginx
+ name: staging-team-foo-nginx
spec:
selector:
matchLabels:
app: mynginx
+ env: staging
org: example.com
team: foo
template:
@@ -20,18 +22,30 @@
note: This is a test annotation
labels:
app: mynginx
+ env: staging
org: example.com
team: foo
spec:
containers:
- - image: nginx
+ - env:
+ - name: ANOTHERENV
+ value: FOO
+ - name: ENVKEY
+ value: ENVVALUE
+ image: nginx:latest
name: nginx
volumeMounts:
- mountPath: /tmp/ps
name: nginx-persistent-storage
+ - image: sidecar
+ name: sidecar
volumes:
- - emptyDir: {}
+ - gcePersistentDisk:
+ pdName: nginx-persistent-storage
name: nginx-persistent-storage
- configMap:
- name: team-foo-configmap-in-base-bbdmdh7m8t
+ name: staging-configmap-in-overlay-k7cbc75tg8
+ name: configmap-in-overlay
+ - configMap:
+ name: staging-team-foo-configmap-in-base-g7k6gt2889
name: configmap-in-base
diff -u -N /tmp/noop/v1_ConfigMap_configmap-in-base.yaml /tmp/transformed/v1_ConfigMap_configmap-in-base.yaml
--- /tmp/noop/v1_ConfigMap_configmap-in-base.yaml YYYY-MM-DD HH:MM:SS
+++ /tmp/transformed/v1_ConfigMap_configmap-in-base.yaml YYYY-MM-DD HH:MM:SS
@@ -8,6 +8,7 @@
creationTimestamp: null
labels:
app: mynginx
+ env: staging
org: example.com
team: foo
- name: team-foo-configmap-in-base-bbdmdh7m8t
+ name: staging-team-foo-configmap-in-base-g7k6gt2889
diff -u -N /tmp/noop/v1_ConfigMap_configmap-in-overlay.yaml /tmp/transformed/v1_ConfigMap_configmap-in-overlay.yaml
--- /tmp/noop/v1_ConfigMap_configmap-in-overlay.yaml YYYY-MM-DD HH:MM:SS
+++ /tmp/transformed/v1_ConfigMap_configmap-in-overlay.yaml YYYY-MM-DD HH:MM:SS
@@ -0,0 +1,9 @@
+apiVersion: v1
+data:
+ hello: world
+kind: ConfigMap
+metadata:
+ creationTimestamp: null
+ labels:
+ env: staging
+ name: staging-configmap-in-overlay-k7cbc75tg8
diff -u -N /tmp/noop/v1_Service_nginx.yaml /tmp/transformed/v1_Service_nginx.yaml
--- /tmp/noop/v1_Service_nginx.yaml YYYY-MM-DD HH:MM:SS
+++ /tmp/transformed/v1_Service_nginx.yaml YYYY-MM-DD HH:MM:SS
@@ -5,13 +5,15 @@
note: This is a test annotation
labels:
app: mynginx
+ env: staging
org: example.com
team: foo
- name: team-foo-nginx
+ name: staging-team-foo-nginx
spec:
ports:
- port: 80
selector:
app: mynginx
+ env: staging
org: example.com
team: foo

View File

@@ -0,0 +1,96 @@
apiVersion: v1
data:
foo: bar
kind: ConfigMap
metadata:
annotations:
note: This is a test annotation
creationTimestamp: null
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:
creationTimestamp: null
labels:
env: staging
name: staging-configmap-in-overlay-k7cbc75tg8
---
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: 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
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

View File

@@ -0,0 +1,21 @@
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

View File

@@ -0,0 +1,16 @@
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- name: nginx
env:
- name: ANOTHERENV
value: FOO
- name: sidecar
image: sidecar
volumes:
- name: nginx-persistent-storage

View File

@@ -0,0 +1,16 @@
apiVersion: manifest.k8s.io/v1alpha1
kind: Manifest
metadata:
name: nginx-app
namePrefix: staging-
objectLabels:
env: staging
patches:
- deployment-patch1.yaml
- deployment-patch2.yaml
bases:
- ../package/
configMapGenerator:
- name: configmap-in-overlay
literals:
- hello=world

View File

@@ -0,0 +1,24 @@
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

View File

@@ -0,0 +1,18 @@
apiVersion: manifest.k8s.io/v1alpha1
kind: Manifest
metadata:
name: nginx-app
namePrefix: team-foo-
objectLabels:
app: mynginx
org: example.com
team: foo
objectAnnotations:
note: This is a test annotation
resources:
- deployment.yaml
- service.yaml
configMapGenerator:
- name: configmap-in-base
literals:
- foo=bar

View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- port: 80
selector:
app: nginx

View File

@@ -0,0 +1,5 @@
description: multiple patches no conflict
args: []
filename: testdata/testcase-multiple-patches-noconflict/in/overlay/
expectedStdout: testdata/testcase-multiple-patches-noconflict/expected.yaml
expectedDiff: testdata/testcase-multiple-patches-noconflict/expected.diff

View File

@@ -0,0 +1,154 @@
diff -u -N /tmp/noop/extensions_v1beta1_Deployment_mungebot.yaml /tmp/transformed/extensions_v1beta1_Deployment_mungebot.yaml
--- /tmp/noop/extensions_v1beta1_Deployment_mungebot.yaml YYYY-MM-DD HH:MM:SS
+++ /tmp/transformed/extensions_v1beta1_Deployment_mungebot.yaml YYYY-MM-DD HH:MM:SS
@@ -3,28 +3,68 @@
metadata:
annotations:
baseAnno: This is an base annotation
+ note: This is a test annotation
labels:
app: mungebot
foo: bar
- name: baseprefix-mungebot
+ org: kubernetes
+ repo: test-infra
+ name: test-infra-baseprefix-mungebot
spec:
- replicas: 1
+ replicas: 2
selector:
matchLabels:
+ app: mungebot
foo: bar
+ org: kubernetes
+ repo: test-infra
template:
metadata:
annotations:
baseAnno: This is an 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-bh449c299k
+ - name: BAR
+ valueFrom:
+ secretKeyRef:
+ key: somekey
+ name: test-infra-app-tls-6hkmhf2224
- name: foo
value: bar
- image: nginx
+ image: nginx:1.7.9
name: nginx
ports:
- containerPort: 80
+ - envFrom:
+ - configMapRef:
+ name: someConfigMap
+ - configMapRef:
+ name: test-infra-app-env-bh449c299k
+ - secretRef:
+ name: test-infra-app-tls-6hkmhf2224
+ image: busybox
+ name: busybox
+ volumeMounts:
+ - mountPath: /tmp/env
+ name: app-env
+ - mountPath: /tmp/tls
+ name: app-tls
+ volumes:
+ - configMap:
+ name: test-infra-app-env-bh449c299k
+ name: app-env
+ - name: app-tls
+ secret:
+ secretName: test-infra-app-tls-6hkmhf2224
diff -u -N /tmp/noop/v1_ConfigMap_app-config.yaml /tmp/transformed/v1_ConfigMap_app-config.yaml
--- /tmp/noop/v1_ConfigMap_app-config.yaml YYYY-MM-DD HH:MM:SS
+++ /tmp/transformed/v1_ConfigMap_app-config.yaml YYYY-MM-DD HH:MM:SS
@@ -0,0 +1,15 @@
+apiVersion: v1
+data:
+ app-init.ini: |
+ FOO=bar
+ BAR=baz
+kind: ConfigMap
+metadata:
+ annotations:
+ note: This is a test annotation
+ creationTimestamp: null
+ labels:
+ app: mungebot
+ org: kubernetes
+ repo: test-infra
+ name: test-infra-app-config-hf5424hg8g
diff -u -N /tmp/noop/v1_ConfigMap_app-env.yaml /tmp/transformed/v1_ConfigMap_app-env.yaml
--- /tmp/noop/v1_ConfigMap_app-env.yaml YYYY-MM-DD HH:MM:SS
+++ /tmp/transformed/v1_ConfigMap_app-env.yaml YYYY-MM-DD HH:MM:SS
@@ -0,0 +1,14 @@
+apiVersion: v1
+data:
+ DB_PASSWORD: somepw
+ DB_USERNAME: admin
+kind: ConfigMap
+metadata:
+ annotations:
+ note: This is a test annotation
+ creationTimestamp: null
+ labels:
+ app: mungebot
+ org: kubernetes
+ repo: test-infra
+ name: test-infra-app-env-bh449c299k
diff -u -N /tmp/noop/v1_Secret_app-tls.yaml /tmp/transformed/v1_Secret_app-tls.yaml
--- /tmp/noop/v1_Secret_app-tls.yaml YYYY-MM-DD HH:MM:SS
+++ /tmp/transformed/v1_Secret_app-tls.yaml YYYY-MM-DD HH:MM:SS
@@ -0,0 +1,15 @@
+apiVersion: v1
+data:
+ tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUIwekNDQVgyZ0F3SUJBZ0lKQUkvTTdCWWp3Qit1TUEwR0NTcUdTSWIzRFFFQkJRVUFNRVV4Q3pBSkJnTlYKQkFZVEFrRlZNUk13RVFZRFZRUUlEQXBUYjIxbExWTjBZWFJsTVNFd0h3WURWUVFLREJoSmJuUmxjbTVsZENCWAphV1JuYVhSeklGQjBlU0JNZEdRd0hoY05NVEl3T1RFeU1qRTFNakF5V2hjTk1UVXdPVEV5TWpFMU1qQXlXakJGCk1Rc3dDUVlEVlFRR0V3SkJWVEVUTUJFR0ExVUVDQXdLVTI5dFpTMVRkR0YwWlRFaE1COEdBMVVFQ2d3WVNXNTAKWlhKdVpYUWdWMmxrWjJsMGN5QlFkSGtnVEhSa01Gd3dEUVlKS29aSWh2Y05BUUVCQlFBRFN3QXdTQUpCQU5MSgpoUEhoSVRxUWJQa2xHM2liQ1Z4d0dNUmZwL3Y0WHFoZmRRSGRjVmZIYXA2TlE1V29rLzR4SUErdWkzNS9NbU5hCnJ0TnVDK0JkWjF0TXVWQ1BGWmNDQXdFQUFhTlFNRTR3SFFZRFZSME9CQllFRkp2S3M4UmZKYVhUSDA4VytTR3YKelF5S24wSDhNQjhHQTFVZEl3UVlNQmFBRkp2S3M4UmZKYVhUSDA4VytTR3Z6UXlLbjBIOE1Bd0dBMVVkRXdRRgpNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUZCUUFEUVFCSmxmZkpIeWJqREd4Uk1xYVJtRGhYMCs2djAyVFVLWnNXCnI1UXVWYnBRaEg2dSswVWdjVzBqcDlRd3B4b1BUTFRXR1hFV0JCQnVyeEZ3aUNCaGtRK1YKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
+ tls.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCT3dJQkFBSkJBTkxKaFBIaElUcVFiUGtsRzNpYkNWeHdHTVJmcC92NFhxaGZkUUhkY1ZmSGFwNk5RNVdvCmsvNHhJQSt1aTM1L01tTmFydE51QytCZFoxdE11VkNQRlpjQ0F3RUFBUUpBRUoyTit6c1IwWG44L1E2dHdhNEcKNk9CMU0xV08rayt6dG5YLzFTdk5lV3U4RDZHSW10dXBMVFlnalpjSHVmeWtqMDlqaUhtakh4OHU4WlpCL28xTgpNUUloQVBXK2V5Wm83YXkzbE16MVYwMVdWak5LSzlRU24xTUpsYjA2aC9MdVl2OUZBaUVBMjVXUGVkS2dWeUNXClNtVXdiUHc4Zm5UY3BxRFdFM3lUTzN2S2NlYnFNU3NDSUJGM1VtVnVlOFlVM2p5YkMzTnh1WHEzd05tMzRSOFQKeFZMSHdEWGgvNk5KQWlFQWwyb0hHR0x6NjRCdUFmaktycXd6N3FNWXI5SENMSWUvWXNvV3Evb2x6U2NDSVFEaQpEMmxXdXNvZTIvbkVxZkRWVldHV2x5Sjd5T21xYVZtL2lOVU45QjJOMmc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
+kind: Secret
+metadata:
+ annotations:
+ note: This is a test annotation
+ creationTimestamp: null
+ labels:
+ app: mungebot
+ org: kubernetes
+ repo: test-infra
+ name: test-infra-app-tls-6hkmhf2224
+type: kubernetes.io/tls
diff -u -N /tmp/noop/v1_Service_mungebot-service.yaml /tmp/transformed/v1_Service_mungebot-service.yaml
--- /tmp/noop/v1_Service_mungebot-service.yaml YYYY-MM-DD HH:MM:SS
+++ /tmp/transformed/v1_Service_mungebot-service.yaml YYYY-MM-DD HH:MM:SS
@@ -3,13 +3,18 @@
metadata:
annotations:
baseAnno: This is an base annotation
+ note: This is a test annotation
labels:
app: mungebot
foo: bar
- name: baseprefix-mungebot-service
+ 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

View File

@@ -0,0 +1,138 @@
apiVersion: v1
data:
app-init.ini: |
FOO=bar
BAR=baz
kind: ConfigMap
metadata:
annotations:
note: This is a test annotation
creationTimestamp: null
labels:
app: mungebot
org: kubernetes
repo: test-infra
name: test-infra-app-config-hf5424hg8g
---
apiVersion: v1
data:
DB_PASSWORD: somepw
DB_USERNAME: admin
kind: ConfigMap
metadata:
annotations:
note: This is a test annotation
creationTimestamp: null
labels:
app: mungebot
org: kubernetes
repo: test-infra
name: test-infra-app-env-bh449c299k
---
apiVersion: v1
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUIwekNDQVgyZ0F3SUJBZ0lKQUkvTTdCWWp3Qit1TUEwR0NTcUdTSWIzRFFFQkJRVUFNRVV4Q3pBSkJnTlYKQkFZVEFrRlZNUk13RVFZRFZRUUlEQXBUYjIxbExWTjBZWFJsTVNFd0h3WURWUVFLREJoSmJuUmxjbTVsZENCWAphV1JuYVhSeklGQjBlU0JNZEdRd0hoY05NVEl3T1RFeU1qRTFNakF5V2hjTk1UVXdPVEV5TWpFMU1qQXlXakJGCk1Rc3dDUVlEVlFRR0V3SkJWVEVUTUJFR0ExVUVDQXdLVTI5dFpTMVRkR0YwWlRFaE1COEdBMVVFQ2d3WVNXNTAKWlhKdVpYUWdWMmxrWjJsMGN5QlFkSGtnVEhSa01Gd3dEUVlKS29aSWh2Y05BUUVCQlFBRFN3QXdTQUpCQU5MSgpoUEhoSVRxUWJQa2xHM2liQ1Z4d0dNUmZwL3Y0WHFoZmRRSGRjVmZIYXA2TlE1V29rLzR4SUErdWkzNS9NbU5hCnJ0TnVDK0JkWjF0TXVWQ1BGWmNDQXdFQUFhTlFNRTR3SFFZRFZSME9CQllFRkp2S3M4UmZKYVhUSDA4VytTR3YKelF5S24wSDhNQjhHQTFVZEl3UVlNQmFBRkp2S3M4UmZKYVhUSDA4VytTR3Z6UXlLbjBIOE1Bd0dBMVVkRXdRRgpNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUZCUUFEUVFCSmxmZkpIeWJqREd4Uk1xYVJtRGhYMCs2djAyVFVLWnNXCnI1UXVWYnBRaEg2dSswVWdjVzBqcDlRd3B4b1BUTFRXR1hFV0JCQnVyeEZ3aUNCaGtRK1YKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
tls.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCT3dJQkFBSkJBTkxKaFBIaElUcVFiUGtsRzNpYkNWeHdHTVJmcC92NFhxaGZkUUhkY1ZmSGFwNk5RNVdvCmsvNHhJQSt1aTM1L01tTmFydE51QytCZFoxdE11VkNQRlpjQ0F3RUFBUUpBRUoyTit6c1IwWG44L1E2dHdhNEcKNk9CMU0xV08rayt6dG5YLzFTdk5lV3U4RDZHSW10dXBMVFlnalpjSHVmeWtqMDlqaUhtakh4OHU4WlpCL28xTgpNUUloQVBXK2V5Wm83YXkzbE16MVYwMVdWak5LSzlRU24xTUpsYjA2aC9MdVl2OUZBaUVBMjVXUGVkS2dWeUNXClNtVXdiUHc4Zm5UY3BxRFdFM3lUTzN2S2NlYnFNU3NDSUJGM1VtVnVlOFlVM2p5YkMzTnh1WHEzd05tMzRSOFQKeFZMSHdEWGgvNk5KQWlFQWwyb0hHR0x6NjRCdUFmaktycXd6N3FNWXI5SENMSWUvWXNvV3Evb2x6U2NDSVFEaQpEMmxXdXNvZTIvbkVxZkRWVldHV2x5Sjd5T21xYVZtL2lOVU45QjJOMmc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
kind: Secret
metadata:
annotations:
note: This is a test annotation
creationTimestamp: null
labels:
app: mungebot
org: kubernetes
repo: test-infra
name: test-infra-app-tls-6hkmhf2224
type: kubernetes.io/tls
---
apiVersion: v1
kind: Service
metadata:
annotations:
baseAnno: This is an 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: extensions/v1beta1
kind: Deployment
metadata:
annotations:
baseAnno: This is an 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 an 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-bh449c299k
- name: BAR
valueFrom:
secretKeyRef:
key: somekey
name: test-infra-app-tls-6hkmhf2224
- name: foo
value: bar
image: nginx:1.7.9
name: nginx
ports:
- containerPort: 80
- envFrom:
- configMapRef:
name: someConfigMap
- configMapRef:
name: test-infra-app-env-bh449c299k
- secretRef:
name: test-infra-app-tls-6hkmhf2224
image: busybox
name: busybox
volumeMounts:
- mountPath: /tmp/env
name: app-env
- mountPath: /tmp/tls
name: app-tls
volumes:
- configMap:
name: test-infra-app-env-bh449c299k
name: app-env
- name: app-tls
secret:
secretName: test-infra-app-tls-6hkmhf2224

View File

@@ -0,0 +1,5 @@
description: simple
args: []
filename: ../examples/simple/instances/exampleinstance/
expectedStdout: testdata/testcase-simple/expected.yaml
expectedDiff: testdata/testcase-simple/expected.diff

View File

@@ -0,0 +1,128 @@
diff -u -N /tmp/noop/apps_v1beta2_Deployment_nginx.yaml /tmp/transformed/apps_v1beta2_Deployment_nginx.yaml
--- /tmp/noop/apps_v1beta2_Deployment_nginx.yaml YYYY-MM-DD HH:MM:SS
+++ /tmp/transformed/apps_v1beta2_Deployment_nginx.yaml YYYY-MM-DD HH:MM:SS
@@ -5,23 +5,26 @@
note: This is a test annotation
labels:
app: mynginx
+ env: staging
org: example.com
- team: foo
- name: team-foo-nginx
+ team: override-foo
+ name: staging-team-foo-nginx
spec:
selector:
matchLabels:
app: mynginx
+ env: staging
org: example.com
- team: foo
+ team: override-foo
template:
metadata:
annotations:
note: This is a test annotation
labels:
app: mynginx
+ env: staging
org: example.com
- team: foo
+ team: override-foo
spec:
containers:
- image: nginx
@@ -30,8 +33,12 @@
- mountPath: /tmp/ps
name: nginx-persistent-storage
volumes:
- - emptyDir: {}
+ - gcePersistentDisk:
+ pdName: nginx-persistent-storage
name: nginx-persistent-storage
- configMap:
- name: team-foo-configmap-in-base-bbdmdh7m8t
+ name: staging-configmap-in-overlay-k7cbc75tg8
+ name: configmap-in-overlay
+ - configMap:
+ name: staging-team-foo-configmap-in-base-gh9d7t85gb
name: configmap-in-base
diff -u -N /tmp/noop/v1_ConfigMap_configmap-in-base.yaml /tmp/transformed/v1_ConfigMap_configmap-in-base.yaml
--- /tmp/noop/v1_ConfigMap_configmap-in-base.yaml YYYY-MM-DD HH:MM:SS
+++ /tmp/transformed/v1_ConfigMap_configmap-in-base.yaml YYYY-MM-DD HH:MM:SS
@@ -1,6 +1,6 @@
apiVersion: v1
data:
- foo: bar
+ foo: override-bar
kind: ConfigMap
metadata:
annotations:
@@ -8,6 +8,7 @@
creationTimestamp: null
labels:
app: mynginx
+ env: staging
org: example.com
- team: foo
- name: team-foo-configmap-in-base-bbdmdh7m8t
+ team: override-foo
+ name: staging-team-foo-configmap-in-base-gh9d7t85gb
diff -u -N /tmp/noop/v1_ConfigMap_configmap-in-overlay.yaml /tmp/transformed/v1_ConfigMap_configmap-in-overlay.yaml
--- /tmp/noop/v1_ConfigMap_configmap-in-overlay.yaml YYYY-MM-DD HH:MM:SS
+++ /tmp/transformed/v1_ConfigMap_configmap-in-overlay.yaml YYYY-MM-DD HH:MM:SS
@@ -0,0 +1,10 @@
+apiVersion: v1
+data:
+ hello: world
+kind: ConfigMap
+metadata:
+ creationTimestamp: null
+ labels:
+ env: staging
+ team: override-foo
+ name: staging-configmap-in-overlay-k7cbc75tg8
diff -u -N /tmp/noop/v1_Secret_secret-in-base.yaml /tmp/transformed/v1_Secret_secret-in-base.yaml
--- /tmp/noop/v1_Secret_secret-in-base.yaml YYYY-MM-DD HH:MM:SS
+++ /tmp/transformed/v1_Secret_secret-in-base.yaml YYYY-MM-DD HH:MM:SS
@@ -1,6 +1,7 @@
apiVersion: v1
data:
password: c29tZXB3
+ proxy: aGFwcm94eQ==
username: YWRtaW4=
kind: Secret
metadata:
@@ -9,7 +10,8 @@
creationTimestamp: null
labels:
app: mynginx
+ env: staging
org: example.com
- team: foo
- name: team-foo-secret-in-base-tkm7hhtf8d
+ team: override-foo
+ name: staging-team-foo-secret-in-base-c8db7gk2m2
type: Opaque
diff -u -N /tmp/noop/v1_Service_nginx.yaml /tmp/transformed/v1_Service_nginx.yaml
--- /tmp/noop/v1_Service_nginx.yaml YYYY-MM-DD HH:MM:SS
+++ /tmp/transformed/v1_Service_nginx.yaml YYYY-MM-DD HH:MM:SS
@@ -5,13 +5,15 @@
note: This is a test annotation
labels:
app: mynginx
+ env: staging
org: example.com
- team: foo
- name: team-foo-nginx
+ team: override-foo
+ name: staging-team-foo-nginx
spec:
ports:
- port: 80
selector:
app: mynginx
+ env: staging
org: example.com
- team: foo
+ team: override-foo

View File

@@ -0,0 +1,108 @@
apiVersion: v1
data:
foo: override-bar
kind: ConfigMap
metadata:
annotations:
note: This is a test annotation
creationTimestamp: null
labels:
app: mynginx
env: staging
org: example.com
team: override-foo
name: staging-team-foo-configmap-in-base-gh9d7t85gb
---
apiVersion: v1
data:
hello: world
kind: ConfigMap
metadata:
creationTimestamp: null
labels:
env: staging
team: override-foo
name: staging-configmap-in-overlay-k7cbc75tg8
---
apiVersion: v1
data:
password: c29tZXB3
proxy: aGFwcm94eQ==
username: YWRtaW4=
kind: Secret
metadata:
annotations:
note: This is a test annotation
creationTimestamp: null
labels:
app: mynginx
env: staging
org: example.com
team: override-foo
name: staging-team-foo-secret-in-base-c8db7gk2m2
type: Opaque
---
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: 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

View File

@@ -0,0 +1,15 @@
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

View File

@@ -0,0 +1,25 @@
apiVersion: manifest.k8s.io/v1alpha1
kind: Manifest
metadata:
name: nginx-app
namePrefix: staging-
objectLabels:
env: staging
team: override-foo
patches:
- deployment.yaml
bases:
- ../package/
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
commands:
proxy: "printf haproxy"

View File

@@ -0,0 +1,24 @@
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

View File

@@ -0,0 +1,23 @@
apiVersion: manifest.k8s.io/v1alpha1
kind: Manifest
metadata:
name: nginx-app
namePrefix: team-foo-
objectLabels:
app: mynginx
org: example.com
team: foo
objectAnnotations:
note: This is a test annotation
resources:
- deployment.yaml
- service.yaml
configMapGenerator:
- name: configmap-in-base
literals:
- foo=bar
secretGenerator:
- name: secret-in-base
commands:
username: "printf admin"
password: "printf somepw"

View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- port: 80
selector:
app: nginx

View File

@@ -0,0 +1,5 @@
description: single overlay
args: []
filename: testdata/testcase-single-overlay/in/overlay/
expectedStdout: testdata/testcase-single-overlay/expected.yaml
expectedDiff: testdata/testcase-single-overlay/expected.diff

95
commands/util.go Normal file
View File

@@ -0,0 +1,95 @@
/*
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 commands
import (
"errors"
"fmt"
"path"
"strings"
"github.com/ghodss/yaml"
manifest "k8s.io/kubectl/pkg/apis/manifest/v1alpha1"
"k8s.io/kubectl/pkg/kustomize/constants"
interror "k8s.io/kubectl/pkg/kustomize/internal/error"
"k8s.io/kubectl/pkg/kustomize/util/fs"
)
type manifestFile struct {
mPath string
fsys fs.FileSystem
}
func newManifestFile(mPath string, fsys fs.FileSystem) (*manifestFile, error) {
mf := &manifestFile{mPath: mPath, fsys: fsys}
err := mf.validate()
if err != nil {
return nil, err
}
return mf, nil
}
func (mf *manifestFile) validate() error {
f, err := mf.fsys.Stat(mf.mPath)
if err != nil {
errorMsg := fmt.Sprintf("Manifest (%s) missing\nRun `kustomize init` first", mf.mPath)
merr := interror.ManifestError{ManifestFilepath: mf.mPath, ErrorMsg: errorMsg}
return merr
}
if f.IsDir() {
mf.mPath = path.Join(mf.mPath, constants.KustomizeFileName)
_, err = mf.fsys.Stat(mf.mPath)
if err != nil {
errorMsg := fmt.Sprintf("Manifest (%s) missing\nRun `kustomize init` first", mf.mPath)
merr := interror.ManifestError{ManifestFilepath: mf.mPath, ErrorMsg: errorMsg}
return merr
}
} else {
if !strings.HasSuffix(mf.mPath, constants.KustomizeFileName) {
errorMsg := fmt.Sprintf("Manifest file (%s) should have %s suffix\n", mf.mPath, constants.KustomizeSuffix)
merr := interror.ManifestError{ManifestFilepath: mf.mPath, ErrorMsg: errorMsg}
return merr
}
}
return nil
}
func (mf *manifestFile) read() (*manifest.Manifest, error) {
bytes, err := mf.fsys.ReadFile(mf.mPath)
if err != nil {
return nil, err
}
var manifest manifest.Manifest
err = yaml.Unmarshal(bytes, &manifest)
if err != nil {
return nil, err
}
return &manifest, err
}
func (mf *manifestFile) write(manifest *manifest.Manifest) error {
if manifest == nil {
return errors.New("util: failed to write passed-in nil manifest")
}
bytes, err := yaml.Marshal(manifest)
if err != nil {
return err
}
return mf.fsys.WriteFile(mf.mPath, bytes)
}

87
commands/util_test.go Normal file
View File

@@ -0,0 +1,87 @@
/*
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 commands
import (
"reflect"
"strings"
"testing"
manifest "k8s.io/kubectl/pkg/apis/manifest/v1alpha1"
"k8s.io/kubectl/pkg/kustomize/util/fs"
)
func TestWriteAndRead(t *testing.T) {
manifest := &manifest.Manifest{
NamePrefix: "prefix",
}
fsys := fs.MakeFakeFS()
fsys.Create("kustomize.yaml")
mf, err := newManifestFile("kustomize.yaml", fsys)
if err != nil {
t.Fatalf("Unexpected Error: %v", err)
}
if err := mf.write(manifest); err != nil {
t.Fatalf("Couldn't write manifest file: %v\n", err)
}
readManifest, err := mf.read()
if err != nil {
t.Fatalf("Couldn't read manifest file: %v\n", err)
}
if !reflect.DeepEqual(manifest, readManifest) {
t.Fatal("Read manifest is different from written manifest")
}
}
func TestEmptyFile(t *testing.T) {
fsys := fs.MakeFakeFS()
_, err := newManifestFile("", fsys)
if err == nil {
t.Fatalf("Creat manifestFile from empty filename should fail")
}
}
func TestNewNotExist(t *testing.T) {
badSuffix := "foo.bar"
fakeFS := fs.MakeFakeFS()
fakeFS.Mkdir(".", 0644)
fakeFS.Create(badSuffix)
_, err := newManifestFile("kustomize.yaml", fakeFS)
if err == nil {
t.Fatalf("expect an error")
}
if !strings.Contains(err.Error(), "Run `kustomize init` first") {
t.Fatalf("expect an error contains %q, but got %v", "does not exist", err)
}
_, err = newManifestFile("kustomize.yaml", fakeFS)
if err == nil {
t.Fatalf("expect an error")
}
if !strings.Contains(err.Error(), "Run `kustomize init` first") {
t.Fatalf("expect an error contains %q, but got %v", "does not exist", err)
}
_, err = newManifestFile(badSuffix, fakeFS)
if err == nil {
t.Fatalf("expect an error")
}
if !strings.Contains(err.Error(), "should have .yaml suffix") {
t.Fatalf("expect an error contains %q, but got %v", "does not exist", err)
}
}

View File

@@ -0,0 +1,182 @@
/*
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 configmapandsecret
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"time"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
manifest "k8s.io/kubectl/pkg/apis/manifest/v1alpha1"
cutil "k8s.io/kubectl/pkg/kustomize/configmapandsecret/util"
"k8s.io/kubectl/pkg/kustomize/hash"
"k8s.io/kubectl/pkg/kustomize/resource"
"k8s.io/kubectl/pkg/kustomize/types"
)
// MakeConfigmapAndGenerateName makes a configmap and returns the configmap and the name appended with a hash.
func MakeConfigmapAndGenerateName(cm manifest.ConfigMapArgs) (*unstructured.Unstructured, string, error) {
corev1CM, err := makeConfigMap(cm)
if err != nil {
return nil, "", err
}
h, err := hash.ConfigMapHash(corev1CM)
if err != nil {
return nil, "", err
}
nameWithHash := fmt.Sprintf("%s-%s", corev1CM.GetName(), h)
unstructuredCM, err := objectToUnstructured(corev1CM)
return unstructuredCM, nameWithHash, err
}
// MakeSecretAndGenerateName returns a secret with the name appended with a hash.
func MakeSecretAndGenerateName(secret manifest.SecretArgs, path string) (*unstructured.Unstructured, string, error) {
corev1Secret, err := makeSecret(secret, path)
if err != nil {
return nil, "", err
}
h, err := hash.SecretHash(corev1Secret)
if err != nil {
return nil, "", err
}
nameWithHash := fmt.Sprintf("%s-%s", secret.Name, h)
unstructuredCM, err := objectToUnstructured(corev1Secret)
return unstructuredCM, nameWithHash, err
}
func objectToUnstructured(in runtime.Object) (*unstructured.Unstructured, error) {
marshaled, err := json.Marshal(in)
if err != nil {
return nil, err
}
var out unstructured.Unstructured
err = out.UnmarshalJSON(marshaled)
return &out, err
}
func makeConfigMap(cm manifest.ConfigMapArgs) (*corev1.ConfigMap, error) {
corev1cm := &corev1.ConfigMap{}
corev1cm.APIVersion = "v1"
corev1cm.Kind = "ConfigMap"
corev1cm.Name = cm.Name
corev1cm.Data = map[string]string{}
if cm.EnvSource != "" {
if err := cutil.HandleConfigMapFromEnvFileSource(corev1cm, cm.EnvSource); err != nil {
return nil, err
}
}
if cm.FileSources != nil {
if err := cutil.HandleConfigMapFromFileSources(corev1cm, cm.FileSources); err != nil {
return nil, err
}
}
if cm.LiteralSources != nil {
if err := cutil.HandleConfigMapFromLiteralSources(corev1cm, cm.LiteralSources); err != nil {
return nil, err
}
}
return corev1cm, nil
}
func makeSecret(secret manifest.SecretArgs, path string) (*corev1.Secret, error) {
corev1secret := &corev1.Secret{}
corev1secret.APIVersion = "v1"
corev1secret.Kind = "Secret"
corev1secret.Name = secret.Name
corev1secret.Type = corev1.SecretType(secret.Type)
if corev1secret.Type == "" {
corev1secret.Type = corev1.SecretTypeOpaque
}
corev1secret.Data = map[string][]byte{}
for k, v := range secret.Commands {
out, err := createSecretKey(path, v)
if err != nil {
return nil, err
}
corev1secret.Data[k] = out
}
return corev1secret, nil
}
func populateMap(m resource.ResourceCollection, obj *unstructured.Unstructured, newName string) error {
oldName := obj.GetName()
gvk := obj.GroupVersionKind()
gvkn := types.GroupVersionKindName{GVK: gvk, Name: oldName}
if _, found := m[gvkn]; found {
return fmt.Errorf("The <name: %q, GroupVersionKind: %v> already exists in the map", oldName, gvk)
}
obj.SetName(newName)
m[gvkn] = &resource.Resource{Data: obj}
return nil
}
// MakeConfigMapsResourceCollection returns a map of <GVK, oldName> -> unstructured object.
func MakeConfigMapsResourceCollection(maps []manifest.ConfigMapArgs) (resource.ResourceCollection, error) {
m := resource.ResourceCollection{}
for _, cm := range maps {
unstructuredConfigMap, nameWithHash, err := MakeConfigmapAndGenerateName(cm)
if err != nil {
return nil, err
}
err = populateMap(m, unstructuredConfigMap, nameWithHash)
if err != nil {
return nil, err
}
}
return m, nil
}
// MakeSecretsResourceCollection returns a map of <GVK, oldName> -> unstructured object.
func MakeSecretsResourceCollection(secrets []manifest.SecretArgs, path string) (resource.ResourceCollection, error) {
m := resource.ResourceCollection{}
for _, secret := range secrets {
unstructuredSecret, nameWithHash, err := MakeSecretAndGenerateName(secret, path)
if err != nil {
return nil, err
}
err = populateMap(m, unstructuredSecret, nameWithHash)
if err != nil {
return nil, err
}
}
return m, nil
}
func createSecretKey(wd string, command string) ([]byte, error) {
fi, err := os.Stat(wd)
if err != nil || !fi.IsDir() {
wd = filepath.Dir(wd)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "sh", "-c", command)
cmd.Dir = wd
return cmd.Output()
}

View File

@@ -0,0 +1,243 @@
/*
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 configmapandsecret
import (
"encoding/base64"
"reflect"
"testing"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
manifest "k8s.io/kubectl/pkg/apis/manifest/v1alpha1"
)
func makeEnvConfigMap(name string) *corev1.ConfigMap {
return &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "ConfigMap",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Data: map[string]string{
"DB_USERNAME": "admin",
"DB_PASSWORD": "somepw",
},
}
}
func makeUnstructuredEnvConfigMap(name string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": name,
"creationTimestamp": nil,
},
"data": map[string]interface{}{
"DB_USERNAME": "admin",
"DB_PASSWORD": "somepw",
},
},
}
}
func makeFileConfigMap(name string) *corev1.ConfigMap {
return &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "ConfigMap",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Data: map[string]string{
"app-init.ini": `FOO=bar
BAR=baz
`,
},
}
}
func makeLiteralConfigMap(name string) *corev1.ConfigMap {
return &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "ConfigMap",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Data: map[string]string{
"a": "x",
"b": "y",
},
}
}
func makeTestSecret(name string) *corev1.Secret {
return &corev1.Secret{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Secret",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Data: map[string][]byte{
"DB_USERNAME": []byte("admin"),
"DB_PASSWORD": []byte("somepw"),
},
Type: corev1.SecretTypeOpaque,
}
}
func makeUnstructuredSecret(name string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": name,
"creationTimestamp": nil,
},
"type": string(corev1.SecretTypeOpaque),
"data": map[string]interface{}{
"DB_USERNAME": base64.StdEncoding.EncodeToString([]byte("admin")),
"DB_PASSWORD": base64.StdEncoding.EncodeToString([]byte("somepw")),
},
},
}
}
func TestConstructConfigMap(t *testing.T) {
type testCase struct {
description string
input manifest.ConfigMapArgs
expected *corev1.ConfigMap
}
testCases := []testCase{
{
description: "construct config map from env",
input: manifest.ConfigMapArgs{
Name: "envConfigMap",
DataSources: manifest.DataSources{
EnvSource: "../examples/simple/instances/exampleinstance/configmap/app.env",
},
},
expected: makeEnvConfigMap("envConfigMap"),
},
{
description: "construct config map from file",
input: manifest.ConfigMapArgs{
Name: "fileConfigMap",
DataSources: manifest.DataSources{
FileSources: []string{"../examples/simple/instances/exampleinstance/configmap/app-init.ini"},
},
},
expected: makeFileConfigMap("fileConfigMap"),
},
{
description: "construct config map from literal",
input: manifest.ConfigMapArgs{
Name: "literalConfigMap",
DataSources: manifest.DataSources{
LiteralSources: []string{"a=x", "b=y"},
},
},
expected: makeLiteralConfigMap("literalConfigMap"),
},
}
for _, tc := range testCases {
cm, err := makeConfigMap(tc.input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !reflect.DeepEqual(*cm, *tc.expected) {
t.Fatalf("in testcase: %q updated:\n%#v\ndoesn't match expected:\n%#v\n", tc.description, *cm, tc.expected)
}
}
}
func TestConstructSecret(t *testing.T) {
secret := manifest.SecretArgs{
Name: "secret",
Commands: map[string]string{
"DB_USERNAME": "printf admin",
"DB_PASSWORD": "printf somepw",
},
Type: "Opaque",
}
cm, err := makeSecret(secret, ".")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expected := makeTestSecret("secret")
if !reflect.DeepEqual(*cm, *expected) {
t.Fatalf("%#v\ndoesn't match expected:\n%#v", *cm, *expected)
}
}
func TestFailConstructSecret(t *testing.T) {
secret := manifest.SecretArgs{
Name: "secret",
Commands: map[string]string{
"FAILURE": "false", // This will fail.
},
Type: "Opaque",
}
_, err := makeSecret(secret, ".")
if err == nil {
t.Fatalf("Expected failure.")
}
}
func TestObjectConvertToUnstructured(t *testing.T) {
type testCase struct {
description string
input *corev1.ConfigMap
expected *unstructured.Unstructured
}
testCases := []testCase{
{
description: "convert config map",
input: makeEnvConfigMap("envConfigMap"),
expected: makeUnstructuredEnvConfigMap("envConfigMap"),
},
{
description: "convert secret",
input: makeEnvConfigMap("envSecret"),
expected: makeUnstructuredEnvConfigMap("envSecret"),
},
}
for _, tc := range testCases {
actual, err := objectToUnstructured(tc.input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !reflect.DeepEqual(actual, tc.expected) {
t.Fatalf("%#v\ndoesn't match expected\n%#v\n", actual, tc.expected)
}
}
}

View File

@@ -0,0 +1,134 @@
/*
Copyright 2016 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 util
import (
"fmt"
"io/ioutil"
"os"
"path"
"strings"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/validation"
)
// handleConfigMapFromLiteralSources adds the specified literal source
// information into the provided configMap.
func HandleConfigMapFromLiteralSources(configMap *v1.ConfigMap, literalSources []string) error {
for _, literalSource := range literalSources {
keyName, value, err := ParseLiteralSource(literalSource)
if err != nil {
return err
}
err = addKeyFromLiteralToConfigMap(configMap, keyName, value)
if err != nil {
return err
}
}
return nil
}
// handleConfigMapFromFileSources adds the specified file source information
// into the provided configMap
func HandleConfigMapFromFileSources(configMap *v1.ConfigMap, fileSources []string) error {
for _, fileSource := range fileSources {
keyName, filePath, err := ParseFileSource(fileSource)
if err != nil {
return err
}
info, err := os.Stat(filePath)
if err != nil {
switch err := err.(type) {
case *os.PathError:
return fmt.Errorf("error reading %s: %v", filePath, err.Err)
default:
return fmt.Errorf("error reading %s: %v", filePath, err)
}
}
if info.IsDir() {
if strings.Contains(fileSource, "=") {
return fmt.Errorf("cannot give a key name for a directory path.")
}
fileList, err := ioutil.ReadDir(filePath)
if err != nil {
return fmt.Errorf("error listing files in %s: %v", filePath, err)
}
for _, item := range fileList {
itemPath := path.Join(filePath, item.Name())
if item.Mode().IsRegular() {
keyName = item.Name()
err = addKeyFromFileToConfigMap(configMap, keyName, itemPath)
if err != nil {
return err
}
}
}
} else {
if err := addKeyFromFileToConfigMap(configMap, keyName, filePath); err != nil {
return err
}
}
}
return nil
}
// handleConfigMapFromEnvFileSource adds the specified env file source information
// into the provided configMap
func HandleConfigMapFromEnvFileSource(configMap *v1.ConfigMap, envFileSource string) error {
info, err := os.Stat(envFileSource)
if err != nil {
switch err := err.(type) {
case *os.PathError:
return fmt.Errorf("error reading %s: %v", envFileSource, err.Err)
default:
return fmt.Errorf("error reading %s: %v", envFileSource, err)
}
}
if info.IsDir() {
return fmt.Errorf("env config file cannot be a directory")
}
return addFromEnvFile(envFileSource, func(key, value string) error {
return addKeyFromLiteralToConfigMap(configMap, key, value)
})
}
// addKeyFromFileToConfigMap adds a key with the given name to a ConfigMap, populating
// the value with the content of the given file path, or returns an error.
func addKeyFromFileToConfigMap(configMap *v1.ConfigMap, keyName, filePath string) error {
data, err := ioutil.ReadFile(filePath)
if err != nil {
return err
}
return addKeyFromLiteralToConfigMap(configMap, keyName, string(data))
}
// addKeyFromLiteralToConfigMap adds the given key and data to the given config map,
// returning an error if the key is not valid or if the key already exists.
func addKeyFromLiteralToConfigMap(configMap *v1.ConfigMap, keyName, data string) error {
// Note, the rules for ConfigMap keys are the exact same as the ones for SecretKeys.
if errs := validation.IsConfigMapKey(keyName); len(errs) != 0 {
return fmt.Errorf("%q is not a valid key name for a ConfigMap: %s", keyName, strings.Join(errs, ";"))
}
if _, entryExists := configMap.Data[keyName]; entryExists {
return fmt.Errorf("cannot add key %s, another key by that name already exists: %v.", keyName, configMap.Data)
}
configMap.Data[keyName] = data
return nil
}

View File

@@ -0,0 +1,103 @@
/*
Copyright 2017 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 util
import (
"bufio"
"bytes"
"fmt"
"os"
"strings"
"unicode"
"unicode/utf8"
"k8s.io/apimachinery/pkg/util/validation"
)
var utf8bom = []byte{0xEF, 0xBB, 0xBF}
// processEnvFileLine returns a blank key if the line is empty or a comment.
// The value will be retrieved from the environment if necessary.
func processEnvFileLine(line []byte, filePath string,
currentLine int) (key, value string, err error) {
if !utf8.Valid(line) {
return ``, ``, fmt.Errorf("env file %s contains invalid utf8 bytes at line %d: %v",
filePath, currentLine+1, line)
}
// We trim UTF8 BOM from the first line of the file but no others
if currentLine == 0 {
line = bytes.TrimPrefix(line, utf8bom)
}
// trim the line from all leading whitespace first
line = bytes.TrimLeftFunc(line, unicode.IsSpace)
// If the line is empty or a comment, we return a blank key/value pair.
if len(line) == 0 || line[0] == '#' {
return ``, ``, nil
}
data := strings.SplitN(string(line), "=", 2)
key = data[0]
if errs := validation.IsEnvVarName(key); len(errs) != 0 {
return ``, ``, fmt.Errorf("%q is not a valid key name: %s", key, strings.Join(errs, ";"))
}
if len(data) == 2 {
value = data[1]
} else {
// No value (no `=` in the line) is a signal to obtain the value
// from the environment.
value = os.Getenv(key)
}
return
}
// addFromEnvFile processes an env file allows a generic addTo to handle the
// collection of key value pairs or returns an error.
func addFromEnvFile(filePath string, addTo func(key, value string) error) error {
f, err := os.Open(filePath)
if err != nil {
return err
}
defer f.Close()
scanner := bufio.NewScanner(f)
currentLine := 0
for scanner.Scan() {
// Process the current line, retrieving a key/value pair if
// possible.
scannedBytes := scanner.Bytes()
key, value, err := processEnvFileLine(scannedBytes, filePath, currentLine)
if err != nil {
return err
}
currentLine++
if len(key) == 0 {
// no key means line was empty or a comment
continue
}
if err = addTo(key, value); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,126 @@
/*
Copyright 2015 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 util
import (
"fmt"
"io/ioutil"
"os"
"path"
"strings"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/validation"
)
// HandleFromLiteralSources adds the specified literal source information into the provided secret
func HandleFromLiteralSources(secret *v1.Secret, literalSources []string) error {
for _, literalSource := range literalSources {
keyName, value, err := ParseLiteralSource(literalSource)
if err != nil {
return err
}
if err = addKeyFromLiteralToSecret(secret, keyName, []byte(value)); err != nil {
return err
}
}
return nil
}
// HandleFromFileSources adds the specified file source information into the provided secret
func HandleFromFileSources(secret *v1.Secret, fileSources []string) error {
for _, fileSource := range fileSources {
keyName, filePath, err := ParseFileSource(fileSource)
if err != nil {
return err
}
info, err := os.Stat(filePath)
if err != nil {
switch err := err.(type) {
case *os.PathError:
return fmt.Errorf("error reading %s: %v", filePath, err.Err)
default:
return fmt.Errorf("error reading %s: %v", filePath, err)
}
}
if info.IsDir() {
if strings.Contains(fileSource, "=") {
return fmt.Errorf("cannot give a key name for a directory path.")
}
fileList, err := ioutil.ReadDir(filePath)
if err != nil {
return fmt.Errorf("error listing files in %s: %v", filePath, err)
}
for _, item := range fileList {
itemPath := path.Join(filePath, item.Name())
if item.Mode().IsRegular() {
keyName = item.Name()
if err = addKeyFromFileToSecret(secret, keyName, itemPath); err != nil {
return err
}
}
}
} else {
if err := addKeyFromFileToSecret(secret, keyName, filePath); err != nil {
return err
}
}
}
return nil
}
// HandleFromEnvFileSource adds the specified env file source information
// into the provided secret
func HandleFromEnvFileSource(secret *v1.Secret, envFileSource string) error {
info, err := os.Stat(envFileSource)
if err != nil {
switch err := err.(type) {
case *os.PathError:
return fmt.Errorf("error reading %s: %v", envFileSource, err.Err)
default:
return fmt.Errorf("error reading %s: %v", envFileSource, err)
}
}
if info.IsDir() {
return fmt.Errorf("env secret file cannot be a directory")
}
return addFromEnvFile(envFileSource, func(key, value string) error {
return addKeyFromLiteralToSecret(secret, key, []byte(value))
})
}
func addKeyFromFileToSecret(secret *v1.Secret, keyName, filePath string) error {
data, err := ioutil.ReadFile(filePath)
if err != nil {
return err
}
return addKeyFromLiteralToSecret(secret, keyName, data)
}
func addKeyFromLiteralToSecret(secret *v1.Secret, keyName string, data []byte) error {
if errs := validation.IsConfigMapKey(keyName); len(errs) != 0 {
return fmt.Errorf("%q is not a valid key name for a Secret: %s", keyName, strings.Join(errs, ";"))
}
if _, entryExists := secret.Data[keyName]; entryExists {
return fmt.Errorf("cannot add key %s, another key by that name already exists: %v.", keyName, secret.Data)
}
secret.Data[keyName] = data
return nil
}

View File

@@ -0,0 +1,91 @@
/*
Copyright 2017 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 util
import (
"crypto/md5"
"errors"
"fmt"
"path"
"strings"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// ParseRFC3339 parses an RFC3339 date in either RFC3339Nano or RFC3339 format.
func ParseRFC3339(s string, nowFn func() metav1.Time) (metav1.Time, error) {
if t, timeErr := time.Parse(time.RFC3339Nano, s); timeErr == nil {
return metav1.Time{Time: t}, nil
}
t, err := time.Parse(time.RFC3339, s)
if err != nil {
return metav1.Time{}, err
}
return metav1.Time{Time: t}, nil
}
func HashObject(obj runtime.Object, codec runtime.Codec) (string, error) {
data, err := runtime.Encode(codec, obj)
if err != nil {
return "", err
}
return fmt.Sprintf("%x", md5.Sum(data)), nil
}
// ParseFileSource parses the source given.
//
// Acceptable formats include:
// 1. source-path: the basename will become the key name
// 2. source-name=source-path: the source-name will become the key name and
// source-path is the path to the key file.
//
// Key names cannot include '='.
func ParseFileSource(source string) (keyName, filePath string, err error) {
numSeparators := strings.Count(source, "=")
switch {
case numSeparators == 0:
return path.Base(source), source, nil
case numSeparators == 1 && strings.HasPrefix(source, "="):
return "", "", fmt.Errorf("key name for file path %v missing.", strings.TrimPrefix(source, "="))
case numSeparators == 1 && strings.HasSuffix(source, "="):
return "", "", fmt.Errorf("file path for key name %v missing.", strings.TrimSuffix(source, "="))
case numSeparators > 1:
return "", "", errors.New("Key names or file paths cannot contain '='.")
default:
components := strings.Split(source, "=")
return components[0], components[1], nil
}
}
// ParseLiteralSource parses the source key=val pair into its component pieces.
// This functionality is distinguished from strings.SplitN(source, "=", 2) since
// it returns an error in the case of empty keys, values, or a missing equals sign.
func ParseLiteralSource(source string) (keyName, value string, err error) {
// leading equal is invalid
if strings.Index(source, "=") == 0 {
return "", "", fmt.Errorf("invalid literal source %v, expected key=value", source)
}
// split after the first equal (so values can have the = character)
items := strings.SplitN(source, "=", 2)
if len(items) != 2 {
return "", "", fmt.Errorf("invalid literal source %v, expected key=value", source)
}
return items[0], items[1], nil
}

26
constants/constants.go Normal file
View File

@@ -0,0 +1,26 @@
/*
Copyright 2017 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 constants
// KustomizeFileName is the Well-Known File Name for a kubernetes app manifest.
const KustomizeSuffix = ".yaml"
const KustomizeFileName = "kustomize" + KustomizeSuffix
// Configmap behaviors
const CreateBehavior = "create"
const ReplaceBehavior = "replace"
const MergeBehavior = "merge"

View File

@@ -0,0 +1,2 @@
FOO=bar
BAR=baz

View File

@@ -0,0 +1,2 @@
DB_USERNAME=admin
DB_PASSWORD=somepw

View File

@@ -0,0 +1,43 @@
apiVersion: extensions/v1beta1
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: BAR
valueFrom:
secretKeyRef:
name: app-tls
key: somekey
- name: busybox
image: busybox
envFrom:
- configMapRef:
name: someConfigMap
- configMapRef:
name: app-env
- secretRef:
name: app-tls
volumeMounts:
- mountPath: /tmp/env
name: app-env
- mountPath: /tmp/tls
name: app-tls
volumes:
- configMap:
name: app-env
name: app-env
- secret:
secretName: app-tls
name: app-tls

View File

@@ -0,0 +1,35 @@
# This example is from https://docs.google.com/document/d/1cLPGweVEYrVqQvBLJg6sxV-TrE5Rm2MNOBA_cxZP2WU/edit#heading=h.dr88tktf0e99
apiVersion: manifest.k8s.io/v1alpha1
kind: Manifest
metadata:
name: test-infra-mungebot
namePrefix: test-infra-
# Labels to add to all objects and selectors.
# These labels would also be used to form the selector for apply --prune
# Named differently than “labels” to avoid confusion with metadata for this object
objectLabels:
app: mungebot
org: kubernetes
repo: test-infra
objectAnnotations:
note: This is a test annotation
bases:
- ../../package/
#These are strategic merge patch overlays in the form of API resources
patches:
- deployment/deployment.yaml
#There could also be configmaps in Base, which would make these overlays
configMapGenerator:
- name: app-env
env: configmap/app.env
- name: app-config
files:
- configmap/app-init.ini
#There could be secrets in Base, if just using a fork/rebase workflow
secretGenerator:
- name: app-tls
commands:
tls.crt: "cat secret/tls.cert"
tls.key: "cat secret/tls.key"
type: "kubernetes.io/tls"

View File

@@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE-----
MIIB0zCCAX2gAwIBAgIJAI/M7BYjwB+uMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTIwOTEyMjE1MjAyWhcNMTUwOTEyMjE1MjAyWjBF
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANLJ
hPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wok/4xIA+ui35/MmNa
rtNuC+BdZ1tMuVCPFZcCAwEAAaNQME4wHQYDVR0OBBYEFJvKs8RfJaXTH08W+SGv
zQyKn0H8MB8GA1UdIwQYMBaAFJvKs8RfJaXTH08W+SGvzQyKn0H8MAwGA1UdEwQF
MAMBAf8wDQYJKoZIhvcNAQEFBQADQQBJlffJHybjDGxRMqaRmDhX0+6v02TUKZsW
r5QuVbpQhH6u+0UgcW0jp9QwpxoPTLTWGXEWBBBurxFwiCBhkQ+V
-----END CERTIFICATE-----

View File

@@ -0,0 +1,9 @@
-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBANLJhPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wo
k/4xIA+ui35/MmNartNuC+BdZ1tMuVCPFZcCAwEAAQJAEJ2N+zsR0Xn8/Q6twa4G
6OB1M1WO+k+ztnX/1SvNeWu8D6GImtupLTYgjZcHufykj09jiHmjHx8u8ZZB/o1N
MQIhAPW+eyZo7ay3lMz1V01WVjNKK9QSn1MJlb06h/LuYv9FAiEA25WPedKgVyCW
SmUwbPw8fnTcpqDWE3yTO3vKcebqMSsCIBF3UmVue8YU3jybC3NxuXq3wNm34R8T
xVLHwDXh/6NJAiEAl2oHGGLz64BuAfjKrqwz7qMYr9HCLIe/YsoWq/olzScCIQDi
D2lWusoe2/nEqfDVVWGWlyJ7yOmqaVm/iNUN9B2N2g==
-----END RSA PRIVATE KEY-----

View File

@@ -0,0 +1,14 @@
# This example is from https://docs.google.com/document/d/1cLPGweVEYrVqQvBLJg6sxV-TrE5Rm2MNOBA_cxZP2WU/edit#heading=h.dr88tktf0e99
# Inspired by https://github.com/kubernetes/helm/blob/master/docs/charts.md
# But Kubernetes API style
apiVersion: manifest.k8s.io/v1alpha1
kind: Descriptor
metadata:
name: mungebot
description: Mungegithub package
# These are search keywords
keywords: [github, bot, kubernetes]
home: https://github.com/bgrant0607/mungebot-pkg/blob/master/README.md
sources: https://github.com/bgrant0607/mungebot-pkg
icon: https://github.com/bgrant0607/mungebot-pkg/blob/master/icon.png

View File

@@ -0,0 +1,21 @@
apiVersion: extensions/v1beta1
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

View File

@@ -0,0 +1,19 @@
# This example is from https://docs.google.com/document/d/1cLPGweVEYrVqQvBLJg6sxV-TrE5Rm2MNOBA_cxZP2WU/edit#heading=h.dr88tktf0e99
# Inspired by https://github.com/kubernetes/helm/blob/master/docs/charts.md
# But Kubernetes API style
apiVersion: manifest.k8s.io/v1alpha1
kind: Manifest
metadata:
name: mungebot
namePrefix: baseprefix-
# Labels to add to all objects and selectors.
# These labels would also be used to form the selector for apply --prune
# Named differently than “labels” to avoid confusion with metadata for this object
objectLabels:
foo: bar
objectAnnotations:
baseAnno: This is an base annotation
resources:
- deployment/deployment.yaml
- service/service.yaml

View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: mungebot-service
labels:
app: mungebot
spec:
ports:
- port: 7002
selector:
app: mungebot

110
hash/hash.go Normal file
View File

@@ -0,0 +1,110 @@
/*
Copyright 2017 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 hash
import (
"crypto/sha256"
"encoding/json"
"fmt"
"k8s.io/api/core/v1"
)
// ConfigMapHash returns a hash of the ConfigMap.
// The data, Kind, and Name are taken into account.
func ConfigMapHash(cm *v1.ConfigMap) (string, error) {
encoded, err := encodeConfigMap(cm)
if err != nil {
return "", err
}
h, err := encodeHash(hash(encoded))
if err != nil {
return "", err
}
return h, nil
}
// SecretHash returns a hash of the Secret.
// The data, Kind, Name, and Type are taken into account.
func SecretHash(sec *v1.Secret) (string, error) {
encoded, err := encodeSecret(sec)
if err != nil {
return "", err
}
h, err := encodeHash(hash(encoded))
if err != nil {
return "", err
}
return h, nil
}
// encodeConfigMap encodes a ConfigMap.
// data, Kind, and Name are taken into account.
func encodeConfigMap(cm *v1.ConfigMap) (string, error) {
// json.Marshal sorts the keys in a stable order in the encoding
data, err := json.Marshal(map[string]interface{}{"kind": "ConfigMap", "name": cm.Name, "data": cm.Data})
if err != nil {
return "", err
}
return string(data), nil
}
// encodeSecret encodes a Secret.
// data, Kind, Name, and Type are taken into account.
func encodeSecret(sec *v1.Secret) (string, error) {
// json.Marshal sorts the keys in a stable order in the encoding
data, err := json.Marshal(map[string]interface{}{"kind": "Secret", "type": sec.Type, "name": sec.Name, "data": sec.Data})
if err != nil {
return "", err
}
return string(data), nil
}
// encodeHash extracts the first 40 bits of the hash from the hex string
// (1 hex char represents 4 bits), and then maps vowels and vowel-like hex
// characters to consonants to prevent bad words from being formed (the theory
// is that no vowels makes it really hard to make bad words). Since the string
// is hex, the only vowels it can contain are 'a' and 'e'.
// We picked some arbitrary consonants to map to from the same character set as GenerateName.
// See: https://github.com/kubernetes/apimachinery/blob/dc1f89aff9a7509782bde3b68824c8043a3e58cc/pkg/util/rand/rand.go#L75
// If the hex string contains fewer than ten characters, returns an error.
func encodeHash(hex string) (string, error) {
if len(hex) < 10 {
return "", fmt.Errorf("the hex string must contain at least 10 characters")
}
enc := []rune(hex[:10])
for i := range enc {
switch enc[i] {
case '0':
enc[i] = 'g'
case '1':
enc[i] = 'h'
case '3':
enc[i] = 'k'
case 'a':
enc[i] = 'm'
case 'e':
enc[i] = 't'
}
}
return string(enc), nil
}
// hash hashes `data` with sha256 and returns the hex string
func hash(data string) string {
return fmt.Sprintf("%x", sha256.Sum256([]byte(data)))
}

178
hash/hash_test.go Normal file
View File

@@ -0,0 +1,178 @@
/*
Copyright 2017 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 hash
import (
"reflect"
"strings"
"testing"
"k8s.io/api/core/v1"
)
func TestConfigMapHash(t *testing.T) {
cases := []struct {
desc string
cm *v1.ConfigMap
hash string
err string
}{
// empty map
{"empty data", &v1.ConfigMap{Data: map[string]string{}}, "42745tchd9", ""},
// one key
{"one key", &v1.ConfigMap{Data: map[string]string{"one": ""}}, "9g67k2htb6", ""},
// three keys (tests sorting order)
{"three keys", &v1.ConfigMap{Data: map[string]string{"two": "2", "one": "", "three": "3"}}, "f5h7t85m9b", ""},
}
for _, c := range cases {
h, err := ConfigMapHash(c.cm)
if SkipRest(t, c.desc, err, c.err) {
continue
}
if c.hash != h {
t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h)
}
}
}
func TestSecretHash(t *testing.T) {
cases := []struct {
desc string
secret *v1.Secret
hash string
err string
}{
// empty map
{"empty data", &v1.Secret{Type: "my-type", Data: map[string][]byte{}}, "t75bgf6ctb", ""},
// one key
{"one key", &v1.Secret{Type: "my-type", Data: map[string][]byte{"one": []byte("")}}, "74bd68bm66", ""},
// three keys (tests sorting order)
{"three keys", &v1.Secret{Type: "my-type", Data: map[string][]byte{"two": []byte("2"), "one": []byte(""), "three": []byte("3")}}, "dgcb6h9tmk", ""},
}
for _, c := range cases {
h, err := SecretHash(c.secret)
if SkipRest(t, c.desc, err, c.err) {
continue
}
if c.hash != h {
t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h)
}
}
}
func TestEncodeConfigMap(t *testing.T) {
cases := []struct {
desc string
cm *v1.ConfigMap
expect string
err string
}{
// empty map
{"empty data", &v1.ConfigMap{Data: map[string]string{}}, `{"data":{},"kind":"ConfigMap","name":""}`, ""},
// one key
{"one key", &v1.ConfigMap{Data: map[string]string{"one": ""}}, `{"data":{"one":""},"kind":"ConfigMap","name":""}`, ""},
// three keys (tests sorting order)
{"three keys", &v1.ConfigMap{Data: map[string]string{"two": "2", "one": "", "three": "3"}}, `{"data":{"one":"","three":"3","two":"2"},"kind":"ConfigMap","name":""}`, ""},
}
for _, c := range cases {
s, err := encodeConfigMap(c.cm)
if SkipRest(t, c.desc, err, c.err) {
continue
}
if s != c.expect {
t.Errorf("case %q, expect %q but got %q from encode %#v", c.desc, c.expect, s, c.cm)
}
}
}
func TestEncodeSecret(t *testing.T) {
cases := []struct {
desc string
secret *v1.Secret
expect string
err string
}{
// empty map
{"empty data", &v1.Secret{Type: "my-type", Data: map[string][]byte{}}, `{"data":{},"kind":"Secret","name":"","type":"my-type"}`, ""},
// one key
{"one key", &v1.Secret{Type: "my-type", Data: map[string][]byte{"one": []byte("")}}, `{"data":{"one":""},"kind":"Secret","name":"","type":"my-type"}`, ""},
// three keys (tests sorting order) - note json.Marshal base64 encodes the values because they come in as []byte
{"three keys", &v1.Secret{Type: "my-type", Data: map[string][]byte{"two": []byte("2"), "one": []byte(""), "three": []byte("3")}}, `{"data":{"one":"","three":"Mw==","two":"Mg=="},"kind":"Secret","name":"","type":"my-type"}`, ""},
}
for _, c := range cases {
s, err := encodeSecret(c.secret)
if SkipRest(t, c.desc, err, c.err) {
continue
}
if s != c.expect {
t.Errorf("case %q, expect %q but got %q from encode %#v", c.desc, c.expect, s, c.secret)
}
}
}
func TestHash(t *testing.T) {
// hash the empty string to be sure that sha256 is being used
expect := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
sum := hash("")
if expect != sum {
t.Errorf("expected hash %q but got %q", expect, sum)
}
}
// warn devs who change types that they might have to update a hash function
// not perfect, as it only checks the number of top-level fields
func TestTypeStability(t *testing.T) {
errfmt := `case %q, expected %d fields but got %d
Depending on the field(s) you added, you may need to modify the hash function for this type.
To guide you: the hash function targets fields that comprise the contents of objects,
not their metadata (e.g. the data of a ConfigMap, but nothing in ObjectMeta).
`
cases := []struct {
typeName string
obj interface{}
expect int
}{
{"ConfigMap", v1.ConfigMap{}, 3},
{"Secret", v1.Secret{}, 5},
}
for _, c := range cases {
val := reflect.ValueOf(c.obj)
if num := val.NumField(); c.expect != num {
t.Errorf(errfmt, c.typeName, c.expect, num)
}
}
}
// SkipRest returns true if there was a non-nil error or if we expected an error that didn't happen,
// and logs the appropriate error on the test object.
// The return value indicates whether we should skip the rest of the test case due to the error result.
func SkipRest(t *testing.T, desc string, err error, contains string) bool {
if err != nil {
if len(contains) == 0 {
t.Errorf("case %q, expect nil error but got %q", desc, err.Error())
} else if !strings.Contains(err.Error(), contains) {
t.Errorf("case %q, expect error to contain %q but got %q", desc, contains, err.Error())
}
return true
} else if len(contains) > 0 {
t.Errorf("case %q, expect error to contain %q but got nil error", desc, contains)
return true
}
return false
}

View File

@@ -0,0 +1,28 @@
/*
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 error
import "fmt"
type ConfigmapError struct {
ManifestFilepath string
ErrorMsg string
}
func (e ConfigmapError) Error() string {
return fmt.Sprintf("Manifest file [%s] encounters a configmap error: %s\n", e.ManifestFilepath, e.ErrorMsg)
}

View File

@@ -0,0 +1,38 @@
/*
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 error
import (
"strings"
"testing"
)
func TestConfigmapError_Error(t *testing.T) {
filepath := "/path/to/kustomize.yaml"
errorMsg := "configmap name is missing"
me := ConfigmapError{ManifestFilepath: filepath, ErrorMsg: errorMsg}
if !strings.Contains(me.Error(), filepath) {
t.Errorf("Incorrect ConfigmapError.Error() message \n")
t.Errorf("Expected filepath %s, but unfound\n", filepath)
}
if !strings.Contains(me.Error(), errorMsg) {
t.Errorf("Incorrect ConfigmapError.Error() message \n")
t.Errorf("Expected errorMsg %s, but unfound\n", errorMsg)
}
}

View File

@@ -0,0 +1,57 @@
/*
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 error
import (
"fmt"
)
// First pass to encapsulate fields for more informative error messages.
type ManifestError struct {
ManifestFilepath string
ErrorMsg string
}
func (me ManifestError) Error() string {
return fmt.Sprintf("Manifest File [%s]: %s\n", me.ManifestFilepath, me.ErrorMsg)
}
type ManifestErrors struct {
merrors []error
}
func (me *ManifestErrors) Error() string {
errormsg := ""
for _, e := range me.merrors {
errormsg += e.Error() + "\n"
}
return errormsg
}
func (me *ManifestErrors) Append(e error) {
me.merrors = append(me.merrors, e)
}
func (me *ManifestErrors) Get() []error {
return me.merrors
}
func (me *ManifestErrors) BatchAppend(e ManifestErrors) {
for _, err := range e.Get() {
me.merrors = append(me.merrors, err)
}
}

View File

@@ -0,0 +1,92 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package error
import (
"fmt"
"strings"
"testing"
)
func TestManifestError_Error(t *testing.T) {
filepath := "/path/to/kustomize.yaml"
errorMsg := "Manifest not found"
me := ManifestError{ManifestFilepath: filepath, ErrorMsg: errorMsg}
if !strings.Contains(me.Error(), filepath) {
t.Errorf("Incorrect ManifestError.Error() message \n")
t.Errorf("Expected filepath %s, but unfound\n", filepath)
}
if !strings.Contains(me.Error(), errorMsg) {
t.Errorf("Incorrect ManifestError.Error() message \n")
t.Errorf("Expected errorMsg %s, but unfound\n", errorMsg)
}
}
func TestManifestErrors_Error(t *testing.T) {
filepath := "/path/to/kustomize"
me := ManifestError{ManifestFilepath: filepath, ErrorMsg: "Manifest not found"}
ce := ConfigmapError{ManifestFilepath: filepath, ErrorMsg: "can't find configmap name"}
pe := PatchError{ManifestFilepath: filepath, PatchFilepath: filepath, ErrorMsg: "can't find patch file"}
re := ResourceError{ManifestFilepath: filepath, ResourceFilepath: filepath, ErrorMsg: "can't find resource file"}
se := SecretError{ManifestFilepath: filepath, ErrorMsg: "can't find secret name"}
mes := ManifestErrors{merrors: []error{me, ce, pe, re, se}}
expectedErrorMsg := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n", me.Error(), ce.Error(), pe.Error(), re.Error(), se.Error())
if mes.Error() != expectedErrorMsg {
t.Errorf("Incorrect ManifestErrors.Error() message\n")
t.Errorf(" Expected: %s\n", expectedErrorMsg)
t.Errorf(" Got: %s\n", mes.Error())
}
}
func TestManifestErrors_Get(t *testing.T) {
ce := ConfigmapError{ManifestFilepath: "manifest/filepath", ErrorMsg: "can't find configmap name"}
mes := ManifestErrors{merrors: []error{ce}}
if len(mes.Get()) != 1 {
t.Errorf("Incorrect ManifestErrors.Get()\n")
t.Errorf(" Expected: %v\n", []error{ce})
t.Errorf(" Got: %s\n", mes.Get())
}
}
func TestManifestErrors_Append(t *testing.T) {
ce := ConfigmapError{ManifestFilepath: "manifest/filepath", ErrorMsg: "can't find configmap name"}
pe := PatchError{ManifestFilepath: "manifest/filepath", PatchFilepath: "patch/path", ErrorMsg: "can't find patch file"}
mes := ManifestErrors{merrors: []error{ce}}
mes.Append(pe)
if len(mes.Get()) != 2 {
t.Errorf("Incorrect ManifestErrors.Append()\n")
t.Errorf(" Expected: %d error\n%v/n", 2, []error{ce, pe})
t.Errorf(" Got: %d error\n%v\n", len(mes.Get()), mes.Get())
}
}
func TestManifestErrors_BatchAppend(t *testing.T) {
ce := ConfigmapError{ManifestFilepath: "manifest/filepath", ErrorMsg: "can't find configmap name"}
pe := PatchError{ManifestFilepath: "manifest/filepath", PatchFilepath: "patch/path", ErrorMsg: "can't find patch file"}
mes := ManifestErrors{merrors: []error{ce}}
me := ManifestErrors{merrors: []error{pe}}
mes.BatchAppend(me)
if len(mes.Get()) != 2 {
t.Errorf("Incorrect ManifestErrors.Append()\n")
t.Errorf(" Expected: %d error\n%v/n", 2, []error{ce, pe})
t.Errorf(" Got: %d error\n%v\n", len(mes.Get()), mes.Get())
}
}

View File

@@ -0,0 +1,31 @@
/*
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 error
import (
"fmt"
)
type PatchError struct {
ManifestFilepath string
PatchFilepath string
ErrorMsg string
}
func (e PatchError) Error() string {
return fmt.Sprintf("Manifest file [%s] encounters a patch error for [%s]: %s\n", e.ManifestFilepath, e.PatchFilepath, e.ErrorMsg)
}

View File

@@ -0,0 +1,41 @@
/*
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 error
import (
"strings"
"testing"
)
func TestPatchError_Error(t *testing.T) {
filepath := "/path/to/kustomize.yaml"
patchfilepath := "/path/to/patch/patch.yaml"
errorMsg := "file not found"
me := PatchError{ManifestFilepath: filepath, PatchFilepath: patchfilepath, ErrorMsg: errorMsg}
if !strings.Contains(me.Error(), filepath) {
t.Errorf("Incorrect PatchError.Error() message \n")
t.Errorf("Expected filepath %s, but unfound\n", filepath)
}
if !strings.Contains(me.Error(), patchfilepath) {
t.Errorf("Incorrect PatchError.Error() message \n")
t.Errorf("Expected patchfilepath %s, but unfound\n", patchfilepath)
}
if !strings.Contains(me.Error(), errorMsg) {
t.Errorf("Incorrect PatchError.Error() message \n")
t.Errorf("Expected errorMsg %s, but unfound\n", errorMsg)
}
}

View File

@@ -0,0 +1,30 @@
/*
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 error
import "fmt"
// First pass to encapsulate fields for more informative error messages.
type ResourceError struct {
ManifestFilepath string
ResourceFilepath string
ErrorMsg string
}
func (e ResourceError) Error() string {
return fmt.Sprintf("Manifest file [%s] encounters a resource error for [%s]: %s\n", e.ManifestFilepath, e.ResourceFilepath, e.ErrorMsg)
}

View File

@@ -0,0 +1,41 @@
/*
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 error
import (
"strings"
"testing"
)
func TestResourceError_Error(t *testing.T) {
filepath := "/path/to/kustomize.yaml"
resourcefilepath := "/path/to/resource/deployment.yaml"
errorMsg := "file not found"
me := ResourceError{ManifestFilepath: filepath, ResourceFilepath: resourcefilepath, ErrorMsg: errorMsg}
if !strings.Contains(me.Error(), filepath) {
t.Errorf("Incorrect ResourceError.Error() message \n")
t.Errorf("Expected filepath %s, but unfound\n", filepath)
}
if !strings.Contains(me.Error(), resourcefilepath) {
t.Errorf("Incorrect ResourceError.Error() message \n")
t.Errorf("Expected resourcefilepath %s, but unfound\n", resourcefilepath)
}
if !strings.Contains(me.Error(), errorMsg) {
t.Errorf("Incorrect ResourceError.Error() message \n")
t.Errorf("Expected errorMsg %s, but unfound\n", errorMsg)
}
}

View File

@@ -0,0 +1,28 @@
/*
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 error
import "fmt"
type SecretError struct {
ManifestFilepath string
ErrorMsg string
}
func (e SecretError) Error() string {
return fmt.Sprintf("Manifest file [%s] encounters a secret error: %s\n", e.ManifestFilepath, e.ErrorMsg)
}

View File

@@ -0,0 +1,37 @@
/*
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 error
import (
"strings"
"testing"
)
func TestSecretError_Error(t *testing.T) {
filepath := "/path/to/secret.yaml"
errorMsg := "missing a command"
me := SecretError{ManifestFilepath: filepath, ErrorMsg: errorMsg}
if !strings.Contains(me.Error(), filepath) {
t.Errorf("Incorrect SecretError.Error() message \n")
t.Errorf("Expected filepath %s, but unfound\n", filepath)
}
if !strings.Contains(me.Error(), errorMsg) {
t.Errorf("Incorrect SecretError.Error() message \n")
t.Errorf("Expected errorMsg %s, but unfound\n", errorMsg)
}
}

57
resource/appresource.go Normal file
View File

@@ -0,0 +1,57 @@
/*
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 resource
import (
"k8s.io/kubectl/pkg/loader"
)
// NewFromResources returns a ResourceCollection given a resource path slice from manifest file.
func NewFromResources(loader loader.Loader, paths []string) (ResourceCollection, error) {
allResources := []ResourceCollection{}
for _, path := range paths {
content, err := loader.Load(path)
if err != nil {
return nil, err
}
res, err := decodeToResourceCollection(content)
if err != nil {
return nil, err
}
allResources = append(allResources, res)
}
return Merge(allResources...)
}
// NewFromPatches returns a slice of Resources given a patch path slice from manifest file.
func NewFromPatches(loader loader.Loader, paths []string) ([]*Resource, error) {
allResources := []*Resource{}
for _, path := range paths {
content, err := loader.Load(path)
if err != nil {
return nil, err
}
res, err := decode(content)
if err != nil {
return nil, err
}
allResources = append(allResources, res...)
}
return allResources, nil
}

View File

@@ -0,0 +1,110 @@
/*
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 resource
import (
"fmt"
"reflect"
"testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kubectl/pkg/kustomize/types"
"k8s.io/kubectl/pkg/loader/loadertest"
)
func TestNewFromPaths(t *testing.T) {
resourceStr := `apiVersion: apps/v1
kind: Deployment
metadata:
name: dply1
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: dply2
`
l := loadertest.NewFakeLoader("/home/seans/project")
if ferr := l.AddFile("/home/seans/project/deployment.yaml", []byte(resourceStr)); ferr != nil {
t.Fatalf("Error adding fake file: %v\n", ferr)
}
expected := ResourceCollection{
{
GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
Name: "dply1",
}: &Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "dply1",
},
},
},
},
{
GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
Name: "dply2",
}: &Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "dply2",
},
},
},
},
}
resources, _ := NewFromResources(l, []string{"/home/seans/project/deployment.yaml"})
if len(resources) != 2 {
t.Fatalf("%#v should contain 2 appResource, but got %d", resources, len(resources))
}
if err := compareMap(resources, expected); err != nil {
t.Fatalf("actual doesn't match expected: %v", err)
}
}
func compareMap(m1, m2 ResourceCollection) error {
if len(m1) != len(m2) {
keySet1 := []types.GroupVersionKindName{}
keySet2 := []types.GroupVersionKindName{}
for GVKn := range m1 {
keySet1 = append(keySet1, GVKn)
}
for GVKn := range m1 {
keySet2 = append(keySet2, GVKn)
}
return fmt.Errorf("maps has different number of entries: %#v doesn't equals %#v", keySet1, keySet2)
}
for GVKn, obj1 := range m1 {
obj2, found := m2[GVKn]
if !found {
return fmt.Errorf("%#v doesn't exist in %#v", GVKn, m2)
}
if !reflect.DeepEqual(obj1.Data, obj2.Data) {
return fmt.Errorf("%#v doesn't match %#v", obj1.Data, obj2.Data)
}
}
return nil
}

144
resource/configmap.go Normal file
View File

@@ -0,0 +1,144 @@
/*
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 resource
import (
"fmt"
"strings"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/validation"
manifest "k8s.io/kubectl/pkg/apis/manifest/v1alpha1"
cutil "k8s.io/kubectl/pkg/kustomize/configmapandsecret/util"
"k8s.io/kubectl/pkg/loader"
)
func newFromConfigMap(l loader.Loader, cm manifest.ConfigMapArgs) (*Resource, error) {
corev1CM, err := makeConfigMap(l, cm)
if err != nil {
return nil, err
}
data, err := objectToUnstructured(corev1CM)
if err != nil {
return nil, err
}
return &Resource{Data: data, Behavior: cm.Behavior}, nil
}
func makeConfigMap(l loader.Loader, cm manifest.ConfigMapArgs) (*corev1.ConfigMap, error) {
var envPairs, literalPairs, filePairs []kvPair
var err error
corev1cm := &corev1.ConfigMap{}
corev1cm.APIVersion = "v1"
corev1cm.Kind = "ConfigMap"
corev1cm.Name = cm.Name
corev1cm.Data = map[string]string{}
if cm.EnvSource != "" {
envPairs, err = keyValuesFromEnvFile(l, cm.EnvSource)
if err != nil {
return nil, fmt.Errorf("error reading keys from env source file: %s %v", cm.EnvSource, err)
}
}
literalPairs, err = keyValuesFromLiteralSources(cm.LiteralSources)
if err != nil {
return nil, fmt.Errorf("error reading key values from literal sources: %v", err)
}
filePairs, err = keyValuesFromFileSources(l, cm.FileSources)
if err != nil {
return nil, fmt.Errorf("error reading key values from file sources: %v", err)
}
allPairs := append(append(envPairs, literalPairs...), filePairs...)
// merge key value pairs from all the sources
for _, kv := range allPairs {
err = addKV(corev1cm.Data, kv)
if err != nil {
return nil, fmt.Errorf("error adding key in configmap: %v", err)
}
}
return corev1cm, nil
}
func keyValuesFromEnvFile(l loader.Loader, path string) ([]kvPair, error) {
content, err := l.Load(path)
if err != nil {
return nil, err
}
return keyValuesFromLines(content)
}
func keyValuesFromLiteralSources(sources []string) ([]kvPair, error) {
var kvs []kvPair
for _, s := range sources {
// TODO: move ParseLiteralSource in this file
k, v, err := cutil.ParseLiteralSource(s)
if err != nil {
return nil, err
}
kvs = append(kvs, kvPair{key: k, value: v})
}
return kvs, nil
}
func keyValuesFromFileSources(l loader.Loader, sources []string) ([]kvPair, error) {
var kvs []kvPair
for _, s := range sources {
key, path, err := cutil.ParseFileSource(s)
if err != nil {
return nil, err
}
fileContent, err := l.Load(path)
if err != nil {
return nil, err
}
kvs = append(kvs, kvPair{key: key, value: string(fileContent)})
}
return kvs, nil
}
// addKV adds key-value pair to the provided map.
func addKV(m map[string]string, kv kvPair) error {
if errs := validation.IsConfigMapKey(kv.key); len(errs) != 0 {
return fmt.Errorf("%q is not a valid key name: %s", kv.key, strings.Join(errs, ";"))
}
if _, exists := m[kv.key]; exists {
return fmt.Errorf("key %s already exists: %v.", kv.key, m)
}
m[kv.key] = kv.value
return nil
}
// NewFromConfigMaps returns a Resource slice given a configmap metadata slice from manifest file.
func NewFromConfigMaps(loader loader.Loader, cmList []manifest.ConfigMapArgs) (ResourceCollection, error) {
allResources := []*Resource{}
for _, cm := range cmList {
res, err := newFromConfigMap(loader, cm)
if err != nil {
return nil, err
}
allResources = append(allResources, res)
}
return resourceCollectionFromResources(allResources)
}

158
resource/configmap_test.go Normal file
View File

@@ -0,0 +1,158 @@
/*
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 resource_test
import (
"reflect"
"testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
manifest "k8s.io/kubectl/pkg/apis/manifest/v1alpha1"
"k8s.io/kubectl/pkg/kustomize/resource"
"k8s.io/kubectl/pkg/loader/loadertest"
)
func TestNewFromConfigMaps(t *testing.T) {
type testCase struct {
description string
input []manifest.ConfigMapArgs
filepath string
content string
expected resource.ResourceCollection
}
l := loadertest.NewFakeLoader("/home/seans/project/")
testCases := []testCase{
{
description: "construct config map from env",
input: []manifest.ConfigMapArgs{
{
Name: "envConfigMap",
DataSources: manifest.DataSources{
EnvSource: "app.env",
},
},
},
filepath: "/home/seans/project/app.env",
content: "DB_USERNAME=admin\nDB_PASSWORD=somepw",
expected: resource.ResourceCollection{
{
GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"},
Name: "envConfigMap",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "envConfigMap",
"creationTimestamp": nil,
},
"data": map[string]interface{}{
"DB_USERNAME": "admin",
"DB_PASSWORD": "somepw",
},
},
},
},
},
},
{
description: "construct config map from file",
input: []manifest.ConfigMapArgs{{
Name: "fileConfigMap",
DataSources: manifest.DataSources{
FileSources: []string{"app-init.ini"},
},
},
},
filepath: "/home/seans/project/app-init.ini",
content: "FOO=bar\nBAR=baz\n",
expected: resource.ResourceCollection{
{
GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"},
Name: "fileConfigMap",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "fileConfigMap",
"creationTimestamp": nil,
},
"data": map[string]interface{}{
"app-init.ini": `FOO=bar
BAR=baz
`,
},
},
},
},
},
},
{
description: "construct config map from literal",
input: []manifest.ConfigMapArgs{
{
Name: "literalConfigMap",
DataSources: manifest.DataSources{
LiteralSources: []string{"a=x", "b=y"},
},
},
},
expected: resource.ResourceCollection{
{
GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"},
Name: "literalConfigMap",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "literalConfigMap",
"creationTimestamp": nil,
},
"data": map[string]interface{}{
"a": "x",
"b": "y",
},
},
},
},
},
},
// TODO: add testcase for data coming from multiple sources like
// files/literal/env etc.
}
for _, tc := range testCases {
if ferr := l.AddFile(tc.filepath, []byte(tc.content)); ferr != nil {
t.Fatalf("Error adding fake file: %v\n", ferr)
}
r, err := resource.NewFromConfigMaps(l, tc.input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !reflect.DeepEqual(r, tc.expected) {
t.Fatalf("in testcase: %q got:\n%+v\n expected:\n%+v\n", tc.description, r, tc.expected)
}
}
}

102
resource/kv.go Normal file
View File

@@ -0,0 +1,102 @@
/*
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 resource
import (
"bufio"
"bytes"
"fmt"
"os"
"strings"
"unicode"
"unicode/utf8"
"k8s.io/apimachinery/pkg/util/validation"
)
var utf8bom = []byte{0xEF, 0xBB, 0xBF}
// kvPair represents a key value pair.
type kvPair struct {
key string
value string
}
// keyValuesFromLines parses given content in to a list of key-value pairs.
func keyValuesFromLines(content []byte) ([]kvPair, error) {
var kvs []kvPair
scanner := bufio.NewScanner(bytes.NewReader(content))
currentLine := 0
for scanner.Scan() {
// Process the current line, retrieving a key/value pair if
// possible.
scannedBytes := scanner.Bytes()
kv, err := kvFromLine(scannedBytes, currentLine)
if err != nil {
return nil, err
}
currentLine++
if len(kv.key) == 0 {
// no key means line was empty or a comment
continue
}
kvs = append(kvs, kv)
}
return kvs, nil
}
// kvFromLine returns a kv with blank key if the line is empty or a comment.
// The value will be retrieved from the environment if necessary.
func kvFromLine(line []byte, currentLine int) (kvPair, error) {
kv := kvPair{}
if !utf8.Valid(line) {
return kv, fmt.Errorf("line %d has invalid utf8 bytes : %v", line, string(line))
}
// We trim UTF8 BOM from the first line of the file but no others
if currentLine == 0 {
line = bytes.TrimPrefix(line, utf8bom)
}
// trim the line from all leading whitespace first
line = bytes.TrimLeftFunc(line, unicode.IsSpace)
// If the line is empty or a comment, we return a blank key/value pair.
if len(line) == 0 || line[0] == '#' {
return kv, nil
}
data := strings.SplitN(string(line), "=", 2)
key := data[0]
if errs := validation.IsEnvVarName(key); len(errs) != 0 {
return kv, fmt.Errorf("%q is not a valid key name: %s", key, strings.Join(errs, ";"))
}
if len(data) == 2 {
kv.value = data[1]
} else {
// No value (no `=` in the line) is a signal to obtain the value
// from the environment.
kv.value = os.Getenv(key)
}
kv.key = key
return kv, nil
}

67
resource/kv_test.go Normal file
View File

@@ -0,0 +1,67 @@
/*
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 resource
import (
"reflect"
"testing"
)
func TestKeyValuesFromLines(t *testing.T) {
tests := []struct {
desc string
content string
expectedPairs []kvPair
expectedErr bool
}{
{
desc: "valid kv content parse",
content: `
k1=v1
k2=v2
`,
expectedPairs: []kvPair{
{key: "k1", value: "v1"},
{key: "k2", value: "v2"},
},
expectedErr: false,
},
{
desc: "content with comments",
content: `
k1=v1
#k2=v2
`,
expectedPairs: []kvPair{
{key: "k1", value: "v1"},
},
expectedErr: false,
},
// TODO: add negative testcases
}
for _, test := range tests {
pairs, err := keyValuesFromLines([]byte(test.content))
if test.expectedErr && err == nil {
t.Fatalf("%s should not return error", test.desc)
}
if !reflect.DeepEqual(pairs, test.expectedPairs) {
t.Errorf("%s should succeed, got:%v exptected:%v", test.desc, pairs, test.expectedPairs)
}
}
}

55
resource/resource.go Normal file
View File

@@ -0,0 +1,55 @@
/*
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 resource
import (
"encoding/json"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/kubectl/pkg/kustomize/types"
)
// Resource represents a Kubernetes Resource Object for ex. Deployment, Server
// ConfigMap etc.
type Resource struct {
Data *unstructured.Unstructured
Behavior string
}
// GVKN returns Group/Version/Kind/Name for the resource.
func (r *Resource) GVKN() types.GroupVersionKindName {
var emptyZVKN types.GroupVersionKindName
if r.Data == nil {
return emptyZVKN
}
gvk := r.Data.GroupVersionKind()
return types.GroupVersionKindName{GVK: gvk, Name: r.Data.GetName()}
}
// ResourceCollection is a map from GroupVersionKindName to Resource
type ResourceCollection map[types.GroupVersionKindName]*Resource
func objectToUnstructured(in runtime.Object) (*unstructured.Unstructured, error) {
marshaled, err := json.Marshal(in)
if err != nil {
return nil, err
}
var out unstructured.Unstructured
err = out.UnmarshalJSON(marshaled)
return &out, err
}

83
resource/secret.go Normal file
View File

@@ -0,0 +1,83 @@
/*
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 resource
import (
"context"
"os"
"os/exec"
"path/filepath"
"time"
corev1 "k8s.io/api/core/v1"
manifest "k8s.io/kubectl/pkg/apis/manifest/v1alpha1"
)
func newFromSecretGenerator(p string, s manifest.SecretArgs) (*Resource, error) {
corev1secret := &corev1.Secret{}
corev1secret.APIVersion = "v1"
corev1secret.Kind = "Secret"
corev1secret.Name = s.Name
corev1secret.Type = corev1.SecretType(s.Type)
if corev1secret.Type == "" {
corev1secret.Type = corev1.SecretTypeOpaque
}
corev1secret.Data = map[string][]byte{}
for k, v := range s.Commands {
out, err := createSecretKey(p, v)
if err != nil {
return nil, err
}
corev1secret.Data[k] = out
}
obj, err := objectToUnstructured(corev1secret)
if err != nil {
return nil, err
}
return &Resource{Data: obj, Behavior: s.Behavior}, nil
}
func createSecretKey(wd string, command string) ([]byte, error) {
fi, err := os.Stat(wd)
if err != nil || !fi.IsDir() {
wd = filepath.Dir(wd)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "sh", "-c", command)
cmd.Dir = wd
return cmd.Output()
}
// NewFromSecretGenerators takes a SecretGenerator slice and executes its command in directory p
// then writes the output to a Resource slice and return it.
func NewFromSecretGenerators(p string, secretList []manifest.SecretArgs) (ResourceCollection, error) {
allResources := []*Resource{}
for _, secret := range secretList {
res, err := newFromSecretGenerator(p, secret)
if err != nil {
return nil, err
}
allResources = append(allResources, res)
}
return resourceCollectionFromResources(allResources)
}

72
resource/secret_test.go Normal file
View File

@@ -0,0 +1,72 @@
/*
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 resource
import (
"encoding/base64"
"reflect"
"testing"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
manifest "k8s.io/kubectl/pkg/apis/manifest/v1alpha1"
)
func TestNewFromSecretGenerators(t *testing.T) {
secrets := []manifest.SecretArgs{
{
Name: "secret",
Commands: map[string]string{
"DB_USERNAME": "printf admin",
"DB_PASSWORD": "printf somepw",
},
Type: "Opaque",
},
}
re, err := NewFromSecretGenerators(".", secrets)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expected := ResourceCollection{
{
GVK: schema.GroupVersionKind{Version: "v1", Kind: "Secret"},
Name: "secret",
}: &Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "secret",
"creationTimestamp": nil,
},
"type": string(corev1.SecretTypeOpaque),
"data": map[string]interface{}{
"DB_USERNAME": base64.StdEncoding.EncodeToString([]byte("admin")),
"DB_PASSWORD": base64.StdEncoding.EncodeToString([]byte("somepw")),
},
},
},
},
}
if !reflect.DeepEqual(re, expected) {
t.Fatalf("%#v\ndoesn't match expected:\n%#v", re, expected)
}
}

170
resource/util.go Normal file
View File

@@ -0,0 +1,170 @@
/*
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 resource
import (
"bytes"
"fmt"
"io"
"github.com/golang/glog"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/kubectl/pkg/kustomize/constants"
)
// decode decodes a list of objects in byte array format
func decode(in []byte) ([]*Resource, error) {
decoder := k8syaml.NewYAMLOrJSONDecoder(bytes.NewReader(in), 1024)
resources := []*Resource{}
var err error
for {
var out unstructured.Unstructured
err = decoder.Decode(&out)
if err != nil {
break
}
resources = append(resources, &Resource{Data: &out})
}
if err != io.EOF {
return nil, err
}
return resources, nil
}
// decodeToResourceCollection decodes a list of objects in byte array format.
// it will return a ResourceCollection.
func decodeToResourceCollection(in []byte) (ResourceCollection, error) {
resources, err := decode(in)
if err != nil {
return nil, err
}
into := ResourceCollection{}
for _, res := range resources {
gvkn := res.GVKN()
if _, found := into[gvkn]; found {
return into, fmt.Errorf("GroupVersionKindName: %#v already exists in the map", gvkn)
}
into[gvkn] = res
}
return into, nil
}
func resourceCollectionFromResources(resources []*Resource) (ResourceCollection, error) {
out := ResourceCollection{}
for _, res := range resources {
gvkn := res.GVKN()
if _, found := out[gvkn]; found {
return nil, fmt.Errorf("duplicated %#v is not allowed", gvkn)
}
out[gvkn] = res
}
return out, nil
}
// Merge will merge all of the entries in the slice of ResourceCollection.
func Merge(rcs ...ResourceCollection) (ResourceCollection, error) {
all := ResourceCollection{}
for _, rc := range rcs {
for gvkn, obj := range rc {
if _, found := all[gvkn]; found {
return nil, fmt.Errorf("there is already an entry: %q", gvkn)
}
all[gvkn] = obj
}
}
return all, nil
}
// MergeWithOverride will merge all of the entries in the slice of ResourceCollection with Override
// If there is already an entry with the same GVKN exists, different actions are performed according to value of Behavior field
// 'create': create a new one;
// 'replace': replace the data only; keep the labels and annotations
// 'merge': merge the data; keep the labels and annotations
func MergeWithOverride(rcs ...ResourceCollection) (ResourceCollection, error) {
all := ResourceCollection{}
for _, rc := range rcs {
for gvkn, obj := range rc {
if _, found := all[gvkn]; found {
switch obj.Behavior {
case "", constants.CreateBehavior:
return nil, fmt.Errorf("Create an existing gvkn %#v is not allowed", gvkn)
case constants.ReplaceBehavior:
glog.V(4).Infof("Replace object %v by %v", all[gvkn].Data.Object, obj.Data.Object)
obj.replace(all[gvkn])
all[gvkn] = obj
case constants.MergeBehavior:
glog.V(4).Infof("Merge object %v with %v", all[gvkn].Data.Object, obj.Data.Object)
obj.merge(all[gvkn])
all[gvkn] = obj
glog.V(4).Infof("The merged object is %v", all[gvkn].Data.Object)
default:
return nil, fmt.Errorf("The behavior of %#v must be one of merge and replace since it already exists in the base", gvkn)
}
} else {
switch obj.Behavior {
case "", constants.CreateBehavior:
all[gvkn] = obj
case constants.MergeBehavior, constants.ReplaceBehavior:
return nil, fmt.Errorf("No merge or replace is allowed for non existing gvkn %#v", gvkn)
default:
return nil, fmt.Errorf("The behavior of %#v must be create since it doesn't exist", gvkn)
}
}
}
}
return all, nil
}
func (r *Resource) replace(other *Resource) {
r.Data.SetLabels(mergeMap(other.Data.GetLabels(), r.Data.GetLabels()))
r.Data.SetAnnotations(mergeMap(other.Data.GetAnnotations(), r.Data.GetAnnotations()))
r.Data.SetName(other.Data.GetName())
}
func (r *Resource) merge(other *Resource) {
r.replace(other)
mergeConfigmap(r.Data.Object, other.Data.Object, r.Data.Object)
}
func mergeMap(maps ...map[string]string) map[string]string {
mergedMap := map[string]string{}
for _, m := range maps {
for key, value := range m {
mergedMap[key] = value
}
}
return mergedMap
}
// TODO: Add BinaryData once we sync to new k8s.io/api
func mergeConfigmap(mergedTo map[string]interface{}, maps ...map[string]interface{}) {
mergedMap := map[string]interface{}{}
for _, m := range maps {
datamap, ok := m["data"].(map[string]interface{})
if ok {
for key, value := range datamap {
mergedMap[key] = value
}
}
}
mergedTo["data"] = mergedMap
}

151
resource/util_test.go Normal file
View File

@@ -0,0 +1,151 @@
/*
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 resource
import (
"fmt"
"reflect"
"testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kubectl/pkg/kustomize/types"
)
func TestDecodeToResourceCollection(t *testing.T) {
encoded := []byte(`apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
---
apiVersion: v1
kind: ConfigMap
metadata:
name: cm2
`)
expected := ResourceCollection{
{
GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"},
Name: "cm1",
}: &Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm1",
},
},
},
},
{
GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"},
Name: "cm2",
}: &Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm2",
},
},
},
},
}
m, err := decodeToResourceCollection(encoded)
fmt.Printf("%v\n", m)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !reflect.DeepEqual(m, expected) {
t.Fatalf("%#v doesn't match expected %#v", m, expected)
}
}
func TestMerge(t *testing.T) {
input1 := ResourceCollection{
types.GroupVersionKindName{
GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
Name: "deploy1",
}: &Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "foo-deploy1",
},
},
},
},
}
input2 := ResourceCollection{
types.GroupVersionKindName{
GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "StatefulSet"},
Name: "stateful1",
}: &Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "StatefulSet",
"metadata": map[string]interface{}{
"name": "bar-stateful",
},
},
},
},
}
input := []ResourceCollection{input1, input2}
expected := ResourceCollection{
types.GroupVersionKindName{
GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
Name: "deploy1",
}: &Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "foo-deploy1",
},
},
},
},
types.GroupVersionKindName{
GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "StatefulSet"},
Name: "stateful1",
}: &Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "StatefulSet",
"metadata": map[string]interface{}{
"name": "bar-stateful",
},
},
},
},
}
merged, err := Merge(input...)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !reflect.DeepEqual(merged, expected) {
t.Fatalf("%#v doesn't equal expected %#v", merged, expected)
}
}

View File

@@ -0,0 +1,85 @@
/*
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 transformers
import (
"errors"
"fmt"
"k8s.io/kubectl/pkg/kustomize/resource"
"k8s.io/kubectl/pkg/kustomize/types"
)
// mapTransformer contains a map string->string and path configs
// The map will be applied to the fields specified in path configs.
type mapTransformer struct {
m map[string]string
pathConfigs []PathConfig
}
var _ Transformer = &mapTransformer{}
// NewDefaultingLabelsMapTransformer construct a mapTransformer with defaultLabelsPathConfigs.
func NewDefaultingLabelsMapTransformer(m map[string]string) (Transformer, error) {
return NewMapTransformer(defaultLabelsPathConfigs, m)
}
// NewDefaultingAnnotationsMapTransformer construct a mapTransformer with defaultAnnotationsPathConfigs.
func NewDefaultingAnnotationsMapTransformer(m map[string]string) (Transformer, error) {
return NewMapTransformer(defaultAnnotationsPathConfigs, m)
}
// NewMapTransformer construct a mapTransformer.
func NewMapTransformer(pc []PathConfig, m map[string]string) (Transformer, error) {
if m == nil {
return NewNoOpTransformer(), nil
}
if pc == nil {
return nil, errors.New("pathConfigs is not expected to be nil")
}
return &mapTransformer{pathConfigs: pc, m: m}, nil
}
// Transform apply each <key, value> pair in the mapTransformer to the
// fields specified in mapTransformer.
func (o *mapTransformer) Transform(m resource.ResourceCollection) error {
for gvkn := range m {
obj := m[gvkn].Data
objMap := obj.UnstructuredContent()
for _, path := range o.pathConfigs {
if !types.SelectByGVK(gvkn.GVK, path.GroupVersionKind) {
continue
}
err := mutateField(objMap, path.Path, path.CreateIfNotPresent, o.addMap)
if err != nil {
return err
}
}
}
return nil
}
func (o *mapTransformer) addMap(in interface{}) (interface{}, error) {
m, ok := in.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("%#v is expectd to be %T", in, m)
}
for k, v := range o.m {
m[k] = v
}
return m, nil
}

View File

@@ -0,0 +1,455 @@
/*
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 transformers
import (
"reflect"
"testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kubectl/pkg/kustomize/resource"
)
func TestLabelsRun(t *testing.T) {
m := resource.ResourceCollection{
{
GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"},
Name: "cm1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm1",
},
},
},
},
{
GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
Name: "deploy1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"group": "apps",
"apiVersion": "v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "deploy1",
},
"spec": map[string]interface{}{
"template": map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
"old-label": "old-value",
},
},
"spec": map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{
"name": "nginx",
"image": "nginx:1.7.9",
},
},
},
},
},
},
},
},
{
GVK: schema.GroupVersionKind{Version: "v1", Kind: "Service"},
Name: "svc1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Service",
"metadata": map[string]interface{}{
"name": "svc1",
},
"spec": map[string]interface{}{
"ports": []interface{}{
map[string]interface{}{
"name": "port1",
"port": "12345",
},
},
},
},
},
},
}
expected := resource.ResourceCollection{
{
GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"},
Name: "cm1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm1",
"labels": map[string]interface{}{
"label-key1": "label-value1",
"label-key2": "label-value2",
},
},
},
},
},
{
GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
Name: "deploy1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"group": "apps",
"apiVersion": "v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "deploy1",
"labels": map[string]interface{}{
"label-key1": "label-value1",
"label-key2": "label-value2",
},
},
"spec": map[string]interface{}{
"selector": map[string]interface{}{
"matchLabels": map[string]interface{}{
"label-key1": "label-value1",
"label-key2": "label-value2",
},
},
"template": map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
"old-label": "old-value",
"label-key1": "label-value1",
"label-key2": "label-value2",
},
},
"spec": map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{
"name": "nginx",
"image": "nginx:1.7.9",
},
},
},
},
},
},
},
},
{
GVK: schema.GroupVersionKind{Version: "v1", Kind: "Service"},
Name: "svc1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Service",
"metadata": map[string]interface{}{
"name": "svc1",
"labels": map[string]interface{}{
"label-key1": "label-value1",
"label-key2": "label-value2",
},
},
"spec": map[string]interface{}{
"ports": []interface{}{
map[string]interface{}{
"name": "port1",
"port": "12345",
},
},
"selector": map[string]interface{}{
"label-key1": "label-value1",
"label-key2": "label-value2",
},
},
},
},
},
}
lt, err := NewDefaultingLabelsMapTransformer(map[string]string{"label-key1": "label-value1", "label-key2": "label-value2"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
err = lt.Transform(m)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !reflect.DeepEqual(m, expected) {
err = compareMap(m, expected)
t.Fatalf("actual doesn't match expected: %v", err)
}
}
func makeAnnotatededConfigMap() *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm1",
"annotations": map[string]interface{}{
"anno-key1": "anno-value1",
"anno-key2": "anno-value2",
},
},
},
}
}
func makeAnnotatededDeployment() *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"group": "apps",
"apiVersion": "v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "deploy1",
"annotations": map[string]interface{}{
"anno-key1": "anno-value1",
"anno-key2": "anno-value2",
},
},
"spec": map[string]interface{}{
"template": map[string]interface{}{
"metadata": map[string]interface{}{
"annotations": map[string]interface{}{
"anno-key1": "anno-value1",
"anno-key2": "anno-value2",
},
"labels": map[string]interface{}{
"old-label": "old-value",
},
},
"spec": map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{
"name": "nginx",
"image": "nginx:1.7.9",
},
},
},
},
},
},
}
}
func makeAnnotatededService() *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Service",
"metadata": map[string]interface{}{
"name": "svc1",
"annotations": map[string]interface{}{
"anno-key1": "anno-value1",
"anno-key2": "anno-value2",
},
},
"spec": map[string]interface{}{
"ports": []interface{}{
map[string]interface{}{
"name": "port1",
"port": "12345",
},
},
},
},
}
}
func TestAnnotationsRun(t *testing.T) {
m := resource.ResourceCollection{
{
GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"},
Name: "cm1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm1",
},
},
},
},
{
GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
Name: "deploy1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"group": "apps",
"apiVersion": "v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "deploy1",
},
"spec": map[string]interface{}{
"template": map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
"old-label": "old-value",
},
},
"spec": map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{
"name": "nginx",
"image": "nginx:1.7.9",
},
},
},
},
},
},
},
},
{
GVK: schema.GroupVersionKind{Version: "v1", Kind: "Service"},
Name: "svc1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Service",
"metadata": map[string]interface{}{
"name": "svc1",
},
"spec": map[string]interface{}{
"ports": []interface{}{
map[string]interface{}{
"name": "port1",
"port": "12345",
},
},
},
},
},
},
}
expected := resource.ResourceCollection{
{
GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"},
Name: "cm1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm1",
"annotations": map[string]interface{}{
"anno-key1": "anno-value1",
"anno-key2": "anno-value2",
},
},
},
},
},
{
GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
Name: "deploy1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"group": "apps",
"apiVersion": "v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "deploy1",
"annotations": map[string]interface{}{
"anno-key1": "anno-value1",
"anno-key2": "anno-value2",
},
},
"spec": map[string]interface{}{
"template": map[string]interface{}{
"metadata": map[string]interface{}{
"annotations": map[string]interface{}{
"anno-key1": "anno-value1",
"anno-key2": "anno-value2",
},
"labels": map[string]interface{}{
"old-label": "old-value",
},
},
"spec": map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{
"name": "nginx",
"image": "nginx:1.7.9",
},
},
},
},
},
},
},
},
{
GVK: schema.GroupVersionKind{Version: "v1", Kind: "Service"},
Name: "svc1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Service",
"metadata": map[string]interface{}{
"name": "svc1",
"annotations": map[string]interface{}{
"anno-key1": "anno-value1",
"anno-key2": "anno-value2",
},
},
"spec": map[string]interface{}{
"ports": []interface{}{
map[string]interface{}{
"name": "port1",
"port": "12345",
},
},
},
},
},
},
}
at, err := NewDefaultingAnnotationsMapTransformer(map[string]string{"anno-key1": "anno-value1", "anno-key2": "anno-value2"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
err = at.Transform(m)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !reflect.DeepEqual(m, expected) {
err = compareMap(m, expected)
t.Fatalf("actual doesn't match expected: %v", err)
}
}

View File

@@ -0,0 +1,159 @@
/*
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 transformers
import (
"k8s.io/apimachinery/pkg/runtime/schema"
)
// defaultLabelsPathConfigs is the default configuration for mutating labels and
// selector fields for native k8s APIs.
var defaultLabelsPathConfigs = []PathConfig{
{
Path: []string{"metadata", "labels"},
CreateIfNotPresent: true,
},
{
GroupVersionKind: &schema.GroupVersionKind{Version: "v1", Kind: "Service"},
Path: []string{"spec", "selector"},
CreateIfNotPresent: true,
},
{
GroupVersionKind: &schema.GroupVersionKind{Version: "v1", Kind: "ReplicationController"},
Path: []string{"spec", "selector"},
CreateIfNotPresent: true,
},
{
GroupVersionKind: &schema.GroupVersionKind{Version: "v1", Kind: "ReplicationController"},
Path: []string{"spec", "template", "metadata", "labels"},
CreateIfNotPresent: true,
},
{
GroupVersionKind: &schema.GroupVersionKind{Kind: "Deployment"},
Path: []string{"spec", "selector", "matchLabels"},
CreateIfNotPresent: true,
},
{
GroupVersionKind: &schema.GroupVersionKind{Kind: "Deployment"},
Path: []string{"spec", "template", "metadata", "labels"},
CreateIfNotPresent: true,
},
{
GroupVersionKind: &schema.GroupVersionKind{Kind: "ReplicaSet"},
Path: []string{"spec", "selector", "matchLabels"},
CreateIfNotPresent: true,
},
{
GroupVersionKind: &schema.GroupVersionKind{Kind: "ReplicaSet"},
Path: []string{"spec", "template", "metadata", "labels"},
CreateIfNotPresent: true,
},
{
GroupVersionKind: &schema.GroupVersionKind{Kind: "DaemonSet"},
Path: []string{"spec", "selector", "matchLabels"},
CreateIfNotPresent: true,
},
{
GroupVersionKind: &schema.GroupVersionKind{Kind: "DaemonSet"},
Path: []string{"spec", "template", "metadata", "labels"},
CreateIfNotPresent: true,
},
{
GroupVersionKind: &schema.GroupVersionKind{Group: "apps", Kind: "StatefulSet"},
Path: []string{"spec", "selector", "matchLabels"},
CreateIfNotPresent: true,
},
{
GroupVersionKind: &schema.GroupVersionKind{Group: "apps", Kind: "StatefulSet"},
Path: []string{"spec", "template", "metadata", "labels"},
CreateIfNotPresent: true,
},
{
GroupVersionKind: &schema.GroupVersionKind{Group: "batch", Kind: "Job"},
Path: []string{"spec", "selector", "matchLabels"},
CreateIfNotPresent: true,
},
{
GroupVersionKind: &schema.GroupVersionKind{Group: "batch", Kind: "Job"},
Path: []string{"spec", "template", "metadata", "labels"},
CreateIfNotPresent: true,
},
{
GroupVersionKind: &schema.GroupVersionKind{Group: "batch", Kind: "CronJob"},
Path: []string{"spec", "jobTemplate", "spec", "selector", "matchLabels"},
CreateIfNotPresent: true,
},
{
GroupVersionKind: &schema.GroupVersionKind{Group: "batch", Kind: "CronJob"},
Path: []string{"spec", "jobTemplate", "spec", "metadata", "labels"},
CreateIfNotPresent: true,
},
{
GroupVersionKind: &schema.GroupVersionKind{Group: "batch", Kind: "CronJob"},
Path: []string{"spec", "jobTemplate", "spec", "template", "metadata", "labels"},
CreateIfNotPresent: true,
},
}
// defaultLabelsPathConfigs is the default configuration for mutating annotations
// fields for native k8s APIs.
var defaultAnnotationsPathConfigs = []PathConfig{
{
Path: []string{"metadata", "annotations"},
CreateIfNotPresent: true,
},
{
GroupVersionKind: &schema.GroupVersionKind{Version: "v1", Kind: "ReplicationController"},
Path: []string{"spec", "template", "metadata", "annotations"},
CreateIfNotPresent: true,
},
{
GroupVersionKind: &schema.GroupVersionKind{Kind: "Deployment"},
Path: []string{"spec", "template", "metadata", "annotations"},
CreateIfNotPresent: true,
},
{
GroupVersionKind: &schema.GroupVersionKind{Kind: "ReplicaSet"},
Path: []string{"spec", "template", "metadata", "annotations"},
CreateIfNotPresent: true,
},
{
GroupVersionKind: &schema.GroupVersionKind{Kind: "DaemonSet"},
Path: []string{"spec", "template", "metadata", "annotations"},
CreateIfNotPresent: true,
},
{
GroupVersionKind: &schema.GroupVersionKind{Group: "apps", Kind: "StatefulSet"},
Path: []string{"spec", "template", "metadata", "annotations"},
CreateIfNotPresent: true,
},
{
GroupVersionKind: &schema.GroupVersionKind{Group: "batch", Kind: "Job"},
Path: []string{"spec", "template", "metadata", "annotations"},
CreateIfNotPresent: true,
},
{
GroupVersionKind: &schema.GroupVersionKind{Group: "batch", Kind: "CronJob"},
Path: []string{"spec", "jobTemplate", "metadata", "annotations"},
CreateIfNotPresent: true,
},
{
GroupVersionKind: &schema.GroupVersionKind{Group: "batch", Kind: "CronJob"},
Path: []string{"spec", "jobTemplate", "spec", "template", "metadata", "annotations"},
CreateIfNotPresent: true,
},
}

View File

@@ -0,0 +1,45 @@
/*
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 transformers
import "k8s.io/kubectl/pkg/kustomize/resource"
// multiTransformer contains a list of transformers.
type multiTransformer struct {
transformers []Transformer
}
var _ Transformer = &multiTransformer{}
// NewMultiTransformer constructs a multiTransformer.
func NewMultiTransformer(t []Transformer) Transformer {
r := &multiTransformer{
transformers: make([]Transformer, len(t))}
copy(r.transformers, t)
return r
}
// Transform prepends the name prefix.
func (o *multiTransformer) Transform(m resource.ResourceCollection) error {
for _, t := range o.transformers {
err := t.Transform(m)
if err != nil {
return err
}
}
return nil
}

104
transformers/namehash.go Normal file
View File

@@ -0,0 +1,104 @@
/*
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 transformers
import (
"encoding/json"
"fmt"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kubectl/pkg/kustomize/hash"
"k8s.io/kubectl/pkg/kustomize/resource"
"k8s.io/kubectl/pkg/kustomize/types"
)
// nameHashTransformer contains the prefix and the path config for each field that
// the name prefix will be applied.
type nameHashTransformer struct{}
var _ Transformer = &nameHashTransformer{}
// NewNameHashTransformer construct a nameHashTransformer.
func NewNameHashTransformer() Transformer {
return &nameHashTransformer{}
}
// Transform appends hash to configmaps and secrets.
func (o *nameHashTransformer) Transform(m resource.ResourceCollection) error {
for gvkn, obj := range m {
switch {
case types.SelectByGVK(gvkn.GVK, &schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}):
appendHashForConfigMap(obj.Data)
case types.SelectByGVK(gvkn.GVK, &schema.GroupVersionKind{Version: "v1", Kind: "Secret"}):
appendHashForSecret(obj.Data)
}
}
return nil
}
func appendHashForConfigMap(obj *unstructured.Unstructured) error {
cm, err := unstructuredToConfigmap(obj)
if err != nil {
return err
}
h, err := hash.ConfigMapHash(cm)
if err != nil {
return err
}
nameWithHash := fmt.Sprintf("%s-%s", obj.GetName(), h)
obj.SetName(nameWithHash)
return nil
}
// TODO: Remove this function after we support hash unstructured objects
func unstructuredToConfigmap(in *unstructured.Unstructured) (*v1.ConfigMap, error) {
marshaled, err := json.Marshal(in)
if err != nil {
return nil, err
}
var out v1.ConfigMap
err = json.Unmarshal(marshaled, &out)
return &out, err
}
func appendHashForSecret(obj *unstructured.Unstructured) error {
secret, err := unstructuredToSecret(obj)
if err != nil {
return err
}
h, err := hash.SecretHash(secret)
if err != nil {
return err
}
nameWithHash := fmt.Sprintf("%s-%s", obj.GetName(), h)
obj.SetName(nameWithHash)
return nil
}
// TODO: Remove this function after we support hash unstructured objects
func unstructuredToSecret(in *unstructured.Unstructured) (*v1.Secret, error) {
marshaled, err := json.Marshal(in)
if err != nil {
return nil, err
}
var out v1.Secret
err = json.Unmarshal(marshaled, &out)
return &out, err
}

View File

@@ -0,0 +1,206 @@
/*
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 transformers
import (
"reflect"
"testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kubectl/pkg/kustomize/resource"
)
func TestNameHashTransformer(t *testing.T) {
objs := resource.ResourceCollection{
{
GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"},
Name: "cm1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm1",
},
},
},
},
{
GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
Name: "deploy1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"group": "apps",
"apiVersion": "v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "deploy1",
},
"spec": map[string]interface{}{
"template": map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
"old-label": "old-value",
},
},
"spec": map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{
"name": "nginx",
"image": "nginx:1.7.9",
},
},
},
},
},
},
},
},
{
GVK: schema.GroupVersionKind{Version: "v1", Kind: "Service"},
Name: "svc1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Service",
"metadata": map[string]interface{}{
"name": "svc1",
},
"spec": map[string]interface{}{
"ports": []interface{}{
map[string]interface{}{
"name": "port1",
"port": "12345",
},
},
},
},
},
},
{
GVK: schema.GroupVersionKind{Version: "v1", Kind: "Secret"},
Name: "secret1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "secret1",
},
},
},
},
}
expected := resource.ResourceCollection{
{
GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"},
Name: "cm1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm1-m462kdfb68",
},
},
},
},
{
GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
Name: "deploy1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"group": "apps",
"apiVersion": "v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "deploy1",
},
"spec": map[string]interface{}{
"template": map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
"old-label": "old-value",
},
},
"spec": map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{
"name": "nginx",
"image": "nginx:1.7.9",
},
},
},
},
},
},
},
},
{
GVK: schema.GroupVersionKind{Version: "v1", Kind: "Service"},
Name: "svc1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Service",
"metadata": map[string]interface{}{
"name": "svc1",
},
"spec": map[string]interface{}{
"ports": []interface{}{
map[string]interface{}{
"name": "port1",
"port": "12345",
},
},
},
},
},
},
{
GVK: schema.GroupVersionKind{Version: "v1", Kind: "Secret"},
Name: "secret1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "secret1-7kc45hd5f7",
},
},
},
},
}
tran := NewNameHashTransformer()
tran.Transform(objs)
if !reflect.DeepEqual(objs, expected) {
err := compareMap(objs, expected)
t.Fatalf("actual doesn't match expected: %v", err)
}
}

View File

@@ -0,0 +1,110 @@
/*
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 transformers
import (
"errors"
"fmt"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kubectl/pkg/kustomize/resource"
"k8s.io/kubectl/pkg/kustomize/types"
)
// nameReferenceTransformer contains the referencing info between 2 GroupVersionKinds
type nameReferenceTransformer struct {
pathConfigs []referencePathConfig
}
var _ Transformer = &nameReferenceTransformer{}
// NewDefaultingNameReferenceTransformer constructs a nameReferenceTransformer
// with defaultNameReferencepathConfigs.
func NewDefaultingNameReferenceTransformer() (Transformer, error) {
return NewNameReferenceTransformer(defaultNameReferencePathConfigs)
}
// NewNameReferenceTransformer construct a nameReferenceTransformer.
func NewNameReferenceTransformer(pc []referencePathConfig) (Transformer, error) {
if pc == nil {
return nil, errors.New("pathConfigs is not expected to be nil")
}
return &nameReferenceTransformer{pathConfigs: pc}, nil
}
// Transform does the fields update according to pathConfigs.
// The old name is in the key in the map and the new name is in the object
// associated with the key. e.g. if <k, v> is one of the key-value pair in the map,
// then the old name is k.Name and the new name is v.GetName()
func (o *nameReferenceTransformer) Transform(
m resource.ResourceCollection) error {
for GVKn := range m {
obj := m[GVKn].Data
objMap := obj.UnstructuredContent()
for _, referencePathConfig := range o.pathConfigs {
for _, path := range referencePathConfig.pathConfigs {
if !types.SelectByGVK(GVKn.GVK, path.GroupVersionKind) {
continue
}
err := mutateField(objMap, path.Path, path.CreateIfNotPresent,
o.updateNameReference(referencePathConfig.referencedGVK, m))
if err != nil {
return err
}
}
}
}
return nil
}
// noMatchingGVKNError indicates failing to find a gvkn.GroupVersionKindName.
type noMatchingGVKNError struct {
message string
}
// newNoMatchingGVKNError constructs an instance of noMatchingGVKNError with
// a given error message.
func newNoMatchingGVKNError(errMsg string) noMatchingGVKNError {
return noMatchingGVKNError{errMsg}
}
// Error returns the error in string format.
func (err noMatchingGVKNError) Error() string {
return err.message
}
func (o *nameReferenceTransformer) updateNameReference(
GVK schema.GroupVersionKind,
m resource.ResourceCollection,
) func(in interface{}) (interface{}, error) {
return func(in interface{}) (interface{}, error) {
s, ok := in.(string)
if !ok {
return nil, fmt.Errorf("%#v is expectd to be %T", in, s)
}
for GVKn, obj := range m {
if !types.SelectByGVK(GVKn.GVK, &GVK) {
continue
}
if GVKn.Name == s {
return obj.Data.GetName(), nil
}
}
return in, nil
}
}

View File

@@ -0,0 +1,238 @@
/*
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 transformers
import (
"reflect"
"testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kubectl/pkg/kustomize/resource"
)
func TestNameReferenceRun(t *testing.T) {
m := resource.ResourceCollection{
{
GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"},
Name: "cm1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "someprefix-cm1-somehash",
},
},
},
},
{
GVK: schema.GroupVersionKind{Version: "v1", Kind: "Secret"},
Name: "secret1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "someprefix-secret1-somehash",
},
},
},
},
{
GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
Name: "deploy1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"group": "apps",
"apiVersion": "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{}{
"name": "nginx",
"image": "nginx:1.7.9",
"env": []interface{}{
map[string]interface{}{
"name": "CM_FOO",
"valueFrom": map[string]interface{}{
"configMapKeyRef": map[string]interface{}{
"name": "cm1",
"key": "somekey",
},
},
},
map[string]interface{}{
"name": "SECRET_FOO",
"valueFrom": map[string]interface{}{
"secretKeyRef": map[string]interface{}{
"name": "secret1",
"key": "somekey",
},
},
},
},
"envFrom": []interface{}{
map[string]interface{}{
"configMapRef": map[string]interface{}{
"name": "cm1",
"key": "somekey",
},
},
map[string]interface{}{
"secretRef": map[string]interface{}{
"name": "secret1",
"key": "somekey",
},
},
},
},
},
"volumes": map[string]interface{}{
"configMap": map[string]interface{}{
"name": "cm1",
},
"secret": map[string]interface{}{
"secretName": "secret1",
},
},
},
},
},
},
},
},
}
expected := resource.ResourceCollection{
{
GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"},
Name: "cm1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "someprefix-cm1-somehash",
},
},
},
},
{
GVK: schema.GroupVersionKind{Version: "v1", Kind: "Secret"},
Name: "secret1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "someprefix-secret1-somehash",
},
},
},
},
{
GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
Name: "deploy1",
}: &resource.Resource{
Data: &unstructured.Unstructured{
Object: map[string]interface{}{
"group": "apps",
"apiVersion": "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{}{
"name": "nginx",
"image": "nginx:1.7.9",
"env": []interface{}{
map[string]interface{}{
"name": "CM_FOO",
"valueFrom": map[string]interface{}{
"configMapKeyRef": map[string]interface{}{
"name": "someprefix-cm1-somehash",
"key": "somekey",
},
},
},
map[string]interface{}{
"name": "SECRET_FOO",
"valueFrom": map[string]interface{}{
"secretKeyRef": map[string]interface{}{
"name": "someprefix-secret1-somehash",
"key": "somekey",
},
},
},
},
"envFrom": []interface{}{
map[string]interface{}{
"configMapRef": map[string]interface{}{
"name": "someprefix-cm1-somehash",
"key": "somekey",
},
},
map[string]interface{}{
"secretRef": map[string]interface{}{
"name": "someprefix-secret1-somehash",
"key": "somekey",
},
},
},
},
},
"volumes": map[string]interface{}{
"configMap": map[string]interface{}{
"name": "someprefix-cm1-somehash",
},
"secret": map[string]interface{}{
"secretName": "someprefix-secret1-somehash",
},
},
},
},
},
},
},
},
}
nrt, err := NewDefaultingNameReferenceTransformer()
err = nrt.Transform(m)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !reflect.DeepEqual(m, expected) {
err = compareMap(m, expected)
t.Fatalf("actual doesn't match expected: %v", err)
}
}

Some files were not shown because too many files have changed in this diff Show More