mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-06-30 09:51:23 +00:00
Introduce ResId and ResMap.
This commit is contained in:
145
pkg/resmap/configmap.go
Normal file
145
pkg/resmap/configmap.go
Normal file
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
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 resmap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
cutil "github.com/kubernetes-sigs/kustomize/pkg/configmapandsecret/util"
|
||||
"github.com/kubernetes-sigs/kustomize/pkg/loader"
|
||||
"github.com/kubernetes-sigs/kustomize/pkg/resource"
|
||||
"github.com/kubernetes-sigs/kustomize/pkg/types"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
)
|
||||
|
||||
func newResourceFromConfigMap(l loader.Loader, cm types.ConfigMapArgs) (*resource.Resource, error) {
|
||||
corev1CM, err := makeConfigMap(l, cm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := newUnstructuredFromObject(corev1CM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resource.NewResource(data, 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
|
||||
}
|
||||
|
||||
// NewResMapFromConfigMapArgs returns a Resource slice given a configmap metadata slice from kustomization file.
|
||||
func NewResMapFromConfigMapArgs(loader loader.Loader, cmList []types.ConfigMapArgs) (ResMap, error) {
|
||||
allResources := []*resource.Resource{}
|
||||
for _, cm := range cmList {
|
||||
res, err := newResourceFromConfigMap(loader, cm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allResources = append(allResources, res)
|
||||
}
|
||||
return newResMapFromResourceSlice(allResources)
|
||||
}
|
||||
148
pkg/resmap/configmap_test.go
Normal file
148
pkg/resmap/configmap_test.go
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
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 resmap
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/kubernetes-sigs/kustomize/pkg/loader/loadertest"
|
||||
"github.com/kubernetes-sigs/kustomize/pkg/resource"
|
||||
"github.com/kubernetes-sigs/kustomize/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
var cmap = schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}
|
||||
|
||||
func TestNewFromConfigMaps(t *testing.T) {
|
||||
type testCase struct {
|
||||
description string
|
||||
input []types.ConfigMapArgs
|
||||
filepath string
|
||||
content string
|
||||
expected ResMap
|
||||
}
|
||||
|
||||
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: ResMap{
|
||||
resource.NewResId(cmap, "envConfigMap"): resource.NewBehaviorlessResource(
|
||||
&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: ResMap{
|
||||
resource.NewResId(cmap, "fileConfigMap"): resource.NewBehaviorlessResource(
|
||||
&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: ResMap{
|
||||
resource.NewResId(cmap, "literalConfigMap"): resource.NewBehaviorlessResource(
|
||||
&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 := NewResMapFromConfigMapArgs(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
37
pkg/resmap/idslice.go
Normal file
37
pkg/resmap/idslice.go
Normal 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 resmap
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/kubernetes-sigs/kustomize/pkg/resource"
|
||||
)
|
||||
|
||||
// IdSlice implements the sort interface.
|
||||
type IdSlice []resource.ResId
|
||||
|
||||
var _ sort.Interface = IdSlice{}
|
||||
|
||||
func (a IdSlice) Len() int { return len(a) }
|
||||
func (a IdSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a IdSlice) Less(i, j int) bool {
|
||||
if a[i].Gvk().String() != a[j].Gvk().String() {
|
||||
return a[i].Gvk().String() < a[j].Gvk().String()
|
||||
}
|
||||
return a[i].Name() < a[j].Name()
|
||||
}
|
||||
102
pkg/resmap/kv.go
Normal file
102
pkg/resmap/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 resmap
|
||||
|
||||
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/resmap/kv_test.go
Normal file
67
pkg/resmap/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 resmap
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
247
pkg/resmap/resmap.go
Normal file
247
pkg/resmap/resmap.go
Normal file
@@ -0,0 +1,247 @@
|
||||
package resmap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/golang/glog"
|
||||
"github.com/kubernetes-sigs/kustomize/pkg/loader"
|
||||
"github.com/kubernetes-sigs/kustomize/pkg/resource"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
|
||||
)
|
||||
|
||||
// ResMap is a map from ResId to Resource
|
||||
type ResMap map[resource.ResId]*resource.Resource
|
||||
|
||||
// EncodeAsYaml encodes a ResMap to YAML; encoded objects separated by `---`.
|
||||
func (m ResMap) EncodeAsYaml() ([]byte, error) {
|
||||
ids := []resource.ResId{}
|
||||
for gvkn := range m {
|
||||
ids = append(ids, gvkn)
|
||||
}
|
||||
sort.Sort(IdSlice(ids))
|
||||
|
||||
firstObj := true
|
||||
var b []byte
|
||||
buf := bytes.NewBuffer(b)
|
||||
for _, id := range ids {
|
||||
obj := m[id].Unstruct()
|
||||
out, err := yaml.Marshal(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if firstObj {
|
||||
firstObj = false
|
||||
} else {
|
||||
_, err = buf.WriteString("---\n")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
_, err = buf.Write(out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (m1 ResMap) ErrorIfNotEqual(m2 ResMap) error {
|
||||
if len(m1) != len(m2) {
|
||||
keySet1 := []resource.ResId{}
|
||||
keySet2 := []resource.ResId{}
|
||||
for id := range m1 {
|
||||
keySet1 = append(keySet1, id)
|
||||
}
|
||||
for id := range m2 {
|
||||
keySet2 = append(keySet2, id)
|
||||
}
|
||||
return fmt.Errorf("maps has different number of entries: %#v doesn't equals %#v", keySet1, keySet2)
|
||||
}
|
||||
for id, obj1 := range m1 {
|
||||
obj2, found := m2[id]
|
||||
if !found {
|
||||
return fmt.Errorf("%#v doesn't exist in %#v", id, m2)
|
||||
}
|
||||
if !reflect.DeepEqual(obj1.Unstruct(), obj2.Unstruct()) {
|
||||
return fmt.Errorf("%#v doesn't match %#v", obj1.Unstruct(), obj2.Unstruct())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m ResMap) insert(newName string, obj *unstructured.Unstructured) error {
|
||||
oldName := obj.GetName()
|
||||
gvk := obj.GroupVersionKind()
|
||||
id := resource.NewResId(gvk, oldName)
|
||||
|
||||
if _, found := m[id]; found {
|
||||
return fmt.Errorf("The <name: %q, GroupVersionKind: %v> already exists in the map", oldName, gvk)
|
||||
}
|
||||
obj.SetName(newName)
|
||||
m[id] = resource.NewBehaviorlessResource(obj)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewResourceSliceFromPatches returns a slice of Resources given a patch path slice from kustomization file.
|
||||
func NewResourceSliceFromPatches(
|
||||
loader loader.Loader, paths []string) ([]*resource.Resource, error) {
|
||||
result := []*resource.Resource{}
|
||||
for _, path := range paths {
|
||||
content, err := loader.Load(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := newResourceSliceFromBytes(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, res...)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// NewResMapFromFiles returns a ResMap given a resource path slice.
|
||||
func NewResMapFromFiles(loader loader.Loader, paths []string) (ResMap, error) {
|
||||
result := []ResMap{}
|
||||
for _, path := range paths {
|
||||
content, err := loader.Load(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := newResMapFromBytes(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, res)
|
||||
}
|
||||
return Merge(result...)
|
||||
}
|
||||
|
||||
// newResMapFromBytes decodes a list of objects in byte array format.
|
||||
func newResMapFromBytes(b []byte) (ResMap, error) {
|
||||
resources, err := newResourceSliceFromBytes(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := ResMap{}
|
||||
for _, res := range resources {
|
||||
gvkn := res.Id()
|
||||
if _, found := result[gvkn]; found {
|
||||
return result, fmt.Errorf("GroupVersionKindName: %#v already exists b the map", gvkn)
|
||||
}
|
||||
result[gvkn] = res
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func newResMapFromResourceSlice(resources []*resource.Resource) (ResMap, error) {
|
||||
result := ResMap{}
|
||||
for _, res := range resources {
|
||||
gvkn := res.Id()
|
||||
if _, found := result[gvkn]; found {
|
||||
return nil, fmt.Errorf("duplicated %#v is not allowed", gvkn)
|
||||
}
|
||||
result[gvkn] = res
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func newResourceSliceFromBytes(in []byte) ([]*resource.Resource, error) {
|
||||
decoder := k8syaml.NewYAMLOrJSONDecoder(bytes.NewReader(in), 1024)
|
||||
result := []*resource.Resource{}
|
||||
|
||||
var err error
|
||||
for {
|
||||
var out unstructured.Unstructured
|
||||
err = decoder.Decode(&out)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
result = append(result, resource.NewBehaviorlessResource(&out))
|
||||
}
|
||||
if err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Merge combines many maps to one.
|
||||
func Merge(maps ...ResMap) (ResMap, error) {
|
||||
result := ResMap{}
|
||||
for _, m := range maps {
|
||||
for gvkn, obj := range m {
|
||||
if _, found := result[gvkn]; found {
|
||||
return nil, fmt.Errorf("there is already an entry: %q", gvkn)
|
||||
}
|
||||
result[gvkn] = obj
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
const behaviorCreate = "create"
|
||||
const behaviorReplace = "replace"
|
||||
const behaviorMerge = "merge"
|
||||
|
||||
// MergeWithOverride merges the entries in the ResMap slice with Override.
|
||||
// If there is already an entry with the same Id , 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(maps ...ResMap) (ResMap, error) {
|
||||
result := ResMap{}
|
||||
for _, m := range maps {
|
||||
for gvkn, resource := range m {
|
||||
if _, found := result[gvkn]; found {
|
||||
switch resource.Behavior() {
|
||||
case "", behaviorCreate:
|
||||
return nil, fmt.Errorf("Create an existing gvkn %#v is not allowed", gvkn)
|
||||
case behaviorReplace:
|
||||
glog.V(4).Infof("Replace object %v by %v", result[gvkn].Unstruct().Object, resource.Unstruct().Object)
|
||||
resource.Replace(result[gvkn])
|
||||
result[gvkn] = resource
|
||||
case behaviorMerge:
|
||||
glog.V(4).Infof("Merge object %v with %v", result[gvkn].Unstruct().Object, resource.Unstruct().Object)
|
||||
resource.Merge(result[gvkn])
|
||||
result[gvkn] = resource
|
||||
glog.V(4).Infof("The merged object is %v", result[gvkn].Unstruct().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 resource.Behavior() {
|
||||
case "", behaviorCreate:
|
||||
result[gvkn] = resource
|
||||
case behaviorMerge, behaviorReplace:
|
||||
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 result, nil
|
||||
}
|
||||
|
||||
func newUnstructuredFromObject(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
|
||||
}
|
||||
222
pkg/resmap/resmap_test.go
Normal file
222
pkg/resmap/resmap_test.go
Normal file
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
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 resmap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/kubernetes-sigs/kustomize/pkg/loader/loadertest"
|
||||
"github.com/kubernetes-sigs/kustomize/pkg/resource"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
var deploy = schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}
|
||||
var statefulset = schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "StatefulSet"}
|
||||
|
||||
func TestEncodeAsYaml(t *testing.T) {
|
||||
encoded := []byte(`apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: cm1
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: cm2
|
||||
`)
|
||||
input := ResMap{
|
||||
resource.NewResId(cmap, "cm1"): resource.NewBehaviorlessResource(
|
||||
&unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "cm1",
|
||||
},
|
||||
},
|
||||
}),
|
||||
resource.NewResId(cmap, "cm2"): resource.NewBehaviorlessResource(
|
||||
&unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "cm2",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
out, err := input.EncodeAsYaml()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(out, encoded) {
|
||||
t.Fatalf("%s doesn't match expected %s", out, encoded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewMapFromFiles(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 := ResMap{resource.NewResId(deploy, "dply1"): resource.NewBehaviorlessResource(
|
||||
&unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "dply1",
|
||||
},
|
||||
},
|
||||
}),
|
||||
resource.NewResId(deploy, "dply2"): resource.NewBehaviorlessResource(
|
||||
&unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "dply2",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
m, _ := NewResMapFromFiles(l, []string{"/home/seans/project/deployment.yaml"})
|
||||
if len(m) != 2 {
|
||||
t.Fatalf("%#v should contain 2 appResource, but got %d", m, len(m))
|
||||
}
|
||||
|
||||
if err := expected.ErrorIfNotEqual(m); err != nil {
|
||||
t.Fatalf("actual doesn't match expected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewMapFromBytes(t *testing.T) {
|
||||
encoded := []byte(`apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: cm1
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: cm2
|
||||
`)
|
||||
expected := ResMap{
|
||||
resource.NewResId(cmap, "cm1"): resource.NewBehaviorlessResource(
|
||||
&unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "cm1",
|
||||
},
|
||||
},
|
||||
}),
|
||||
resource.NewResId(cmap, "cm2"): resource.NewBehaviorlessResource(
|
||||
&unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "cm2",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
m, err := newResMapFromBytes(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 := ResMap{
|
||||
resource.NewResId(deploy, "deploy1"): resource.NewBehaviorlessResource(
|
||||
&unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "foo-deploy1",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
input2 := ResMap{
|
||||
resource.NewResId(statefulset, "stateful1"): resource.NewBehaviorlessResource(
|
||||
&unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "StatefulSet",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "bar-stateful",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
input := []ResMap{input1, input2}
|
||||
expected := ResMap{
|
||||
resource.NewResId(deploy, "deploy1"): resource.NewBehaviorlessResource(
|
||||
&unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "foo-deploy1",
|
||||
},
|
||||
},
|
||||
}),
|
||||
resource.NewResId(statefulset, "stateful1"): resource.NewBehaviorlessResource(
|
||||
&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)
|
||||
}
|
||||
}
|
||||
84
pkg/resmap/secret.go
Normal file
84
pkg/resmap/secret.go
Normal file
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
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 resmap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/kubernetes-sigs/kustomize/pkg/resource"
|
||||
"github.com/kubernetes-sigs/kustomize/pkg/types"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
func newFromSecretGenerator(p string, s types.SecretArgs) (*resource.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 := newUnstructuredFromObject(corev1secret)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resource.NewResource(obj, 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()
|
||||
}
|
||||
|
||||
// NewResMapFromSecretArgs takes a SecretArgs slice and executes its command in directory p
|
||||
// then writes the output to a Resource slice and return it.
|
||||
func NewResMapFromSecretArgs(p string, secretList []types.SecretArgs) (ResMap, error) {
|
||||
allResources := []*resource.Resource{}
|
||||
for _, secret := range secretList {
|
||||
res, err := newFromSecretGenerator(p, secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allResources = append(allResources, res)
|
||||
}
|
||||
return newResMapFromResourceSlice(allResources)
|
||||
}
|
||||
72
pkg/resmap/secret_test.go
Normal file
72
pkg/resmap/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 resmap
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/kubernetes-sigs/kustomize/pkg/resource"
|
||||
"github.com/kubernetes-sigs/kustomize/pkg/types"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
var secret = schema.GroupVersionKind{Version: "v1", Kind: "Secret"}
|
||||
|
||||
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 := NewResMapFromSecretArgs(".", secrets)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
expected := ResMap{
|
||||
resource.NewResId(secret, "secret"): resource.NewResource(
|
||||
&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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user