mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-06-13 01:50:55 +00:00
Isolated content of pkg/kustomize
This commit is contained in:
57
pkg/resource/appresource.go
Normal file
57
pkg/resource/appresource.go
Normal 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 kustomization 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 kustomization 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
|
||||
}
|
||||
110
pkg/resource/appresource_test.go
Normal file
110
pkg/resource/appresource_test.go
Normal 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
pkg/resource/configmap.go
Normal file
144
pkg/resource/configmap.go
Normal 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"
|
||||
cutil "k8s.io/kubectl/pkg/kustomize/configmapandsecret/util"
|
||||
"k8s.io/kubectl/pkg/kustomize/types"
|
||||
"k8s.io/kubectl/pkg/loader"
|
||||
)
|
||||
|
||||
func newFromConfigMap(l loader.Loader, cm types.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 types.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 kustomization file.
|
||||
func NewFromConfigMaps(loader loader.Loader, cmList []types.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
pkg/resource/configmap_test.go
Normal file
158
pkg/resource/configmap_test.go
Normal 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"
|
||||
"k8s.io/kubectl/pkg/kustomize/resource"
|
||||
"k8s.io/kubectl/pkg/kustomize/types"
|
||||
"k8s.io/kubectl/pkg/loader/loadertest"
|
||||
)
|
||||
|
||||
func TestNewFromConfigMaps(t *testing.T) {
|
||||
type testCase struct {
|
||||
description string
|
||||
input []types.ConfigMapArgs
|
||||
filepath string
|
||||
content string
|
||||
expected resource.ResourceCollection
|
||||
}
|
||||
|
||||
l := loadertest.NewFakeLoader("/home/seans/project/")
|
||||
testCases := []testCase{
|
||||
{
|
||||
description: "construct config map from env",
|
||||
input: []types.ConfigMapArgs{
|
||||
{
|
||||
Name: "envConfigMap",
|
||||
DataSources: types.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: []types.ConfigMapArgs{{
|
||||
Name: "fileConfigMap",
|
||||
DataSources: types.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: []types.ConfigMapArgs{
|
||||
{
|
||||
Name: "literalConfigMap",
|
||||
DataSources: types.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
pkg/resource/kv.go
Normal file
102
pkg/resource/kv.go
Normal 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
pkg/resource/kv_test.go
Normal file
67
pkg/resource/kv_test.go
Normal 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
pkg/resource/resource.go
Normal file
55
pkg/resource/resource.go
Normal 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
pkg/resource/secret.go
Normal file
83
pkg/resource/secret.go
Normal 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"
|
||||
"k8s.io/kubectl/pkg/kustomize/types"
|
||||
)
|
||||
|
||||
func newFromSecretGenerator(p string, s types.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 []types.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
pkg/resource/secret_test.go
Normal file
72
pkg/resource/secret_test.go
Normal 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"
|
||||
"k8s.io/kubectl/pkg/kustomize/types"
|
||||
)
|
||||
|
||||
func TestNewFromSecretGenerators(t *testing.T) {
|
||||
secrets := []types.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
pkg/resource/util.go
Normal file
170
pkg/resource/util.go
Normal 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
pkg/resource/util_test.go
Normal file
151
pkg/resource/util_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user