diff --git a/pkg/kinflate/util/gvkn_sort.go b/pkg/kinflate/util/gvkn_sort.go new file mode 100644 index 000000000..5e6bb374b --- /dev/null +++ b/pkg/kinflate/util/gvkn_sort.go @@ -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 util + +type ByGVKN []GroupVersionKindName + +func (a ByGVKN) Len() int { return len(a) } +func (a ByGVKN) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a ByGVKN) 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 +} diff --git a/pkg/kinflate/util/pathconfig.go b/pkg/kinflate/util/pathconfig.go new file mode 100644 index 000000000..d5c901c60 --- /dev/null +++ b/pkg/kinflate/util/pathconfig.go @@ -0,0 +1,22 @@ +/* +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 util + +type PathConfig struct { + Path []string + CreateIfNotPresent bool +} diff --git a/pkg/kinflate/util/prefixname.go b/pkg/kinflate/util/prefixname.go new file mode 100644 index 000000000..9ae7e8e2a --- /dev/null +++ b/pkg/kinflate/util/prefixname.go @@ -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 util + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// PrefixNameOptions contains the prefix and the path config for each field that +// the name prefix will be applied. +type PrefixNameOptions struct { + prefix string + pathConfigs []PathConfig +} + +var _ Transformer = &PrefixNameOptions{} + +var DefaultNamePrefixPathConfigs = []PathConfig{ + { + Path: []string{"metadata", "name"}, + CreateIfNotPresent: false, + }, +} + +func (o *PrefixNameOptions) Complete(prefix string, pathConfigs []PathConfig) { + o.prefix = prefix + if pathConfigs == nil { + pathConfigs = DefaultNamePrefixPathConfigs + } + o.pathConfigs = pathConfigs +} + +func (o *PrefixNameOptions) Transform(m map[GroupVersionKindName]*unstructured.Unstructured) error { + for gvkn := range m { + obj := m[gvkn] + objMap := obj.UnstructuredContent() + for _, path := range o.pathConfigs { + err := mutateField(objMap, path.Path, path.CreateIfNotPresent, o.addPrefix) + if err != nil { + return err + } + } + } + return nil +} + +func (o *PrefixNameOptions) TransformBytes(in []byte) ([]byte, error) { + m, err := Decode(in) + if err != nil { + return nil, err + } + + err = o.Transform(m) + if err != nil { + return nil, err + } + + return Encode(m) +} + +func (o *PrefixNameOptions) addPrefix(in interface{}) (interface{}, error) { + s, ok := in.(string) + if !ok { + return nil, fmt.Errorf("%#v is expectd to be %T", in, s) + } + return o.prefix + s, nil +} diff --git a/pkg/kinflate/util/prefixname_test.go b/pkg/kinflate/util/prefixname_test.go new file mode 100644 index 000000000..aee95618a --- /dev/null +++ b/pkg/kinflate/util/prefixname_test.go @@ -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 util + +import ( + "reflect" + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var prefixNameOps = PrefixNameOptions{ + prefix: "someprefix-", + pathConfigs: DefaultNamePrefixPathConfigs, +} + +var namePrefixedCm1 = unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "someprefix-cm1", + }, + }, +} + +var namePrefixedCm2 = unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "someprefix-cm2", + }, + }, +} + +var namePrefixedM = map[GroupVersionKindName]*unstructured.Unstructured{ + { + gvk: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, + name: "cm1", + }: &namePrefixedCm1, + { + gvk: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, + name: "cm2", + }: &namePrefixedCm2, +} + +func TestPrefixNameRun(t *testing.T) { + m := createMap() + err := prefixNameOps.Transform(m) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(m, namePrefixedM) { + t.Fatalf("%s doesn't match expected %s", m, namePrefixedM) + } +} diff --git a/pkg/kinflate/util/transformer.go b/pkg/kinflate/util/transformer.go new file mode 100644 index 000000000..0b86b0961 --- /dev/null +++ b/pkg/kinflate/util/transformer.go @@ -0,0 +1,26 @@ +/* +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 util + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +type Transformer interface { + // Transform modifies objects in a map, e.g. add prefixes or additional labels. + Transform(m map[GroupVersionKindName]*unstructured.Unstructured) error +} diff --git a/pkg/kinflate/util/util.go b/pkg/kinflate/util/util.go new file mode 100644 index 000000000..c2f07c130 --- /dev/null +++ b/pkg/kinflate/util/util.go @@ -0,0 +1,161 @@ +/* +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 util + +import ( + "bytes" + "fmt" + "io" + "sort" + + "github.com/ghodss/yaml" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + k8syaml "k8s.io/apimachinery/pkg/util/yaml" +) + +type GroupVersionKindName struct { + gvk schema.GroupVersionKind + // name of the resource. + name string +} + +func Decode(in []byte) (map[GroupVersionKindName]*unstructured.Unstructured, error) { + decoder := k8syaml.NewYAMLOrJSONDecoder(bytes.NewReader(in), 1024) + objs := []*unstructured.Unstructured{} + + var err error + for { + var out unstructured.Unstructured + err = decoder.Decode(&out) + if err != nil { + break + } + objs = append(objs, &out) + } + if err != io.EOF { + return nil, err + } + + m := map[GroupVersionKindName]*unstructured.Unstructured{} + for i := range objs { + metaAccessor, err := meta.Accessor(objs[i]) + if err != nil { + return nil, err + } + name := metaAccessor.GetName() + typeAccessor, err := meta.TypeAccessor(objs[i]) + if err != nil { + return nil, err + } + apiVersion := typeAccessor.GetAPIVersion() + kind := typeAccessor.GetKind() + gv, err := schema.ParseGroupVersion(apiVersion) + if err != nil { + return nil, err + } + gvk := gv.WithKind(kind) + gvkn := GroupVersionKindName{ + gvk: gvk, + name: name, + } + m[gvkn] = objs[i] + } + return m, nil +} + +func Encode(in map[GroupVersionKindName]*unstructured.Unstructured) ([]byte, error) { + gvknList := []GroupVersionKindName{} + for gvkn := range in { + gvknList = append(gvknList, gvkn) + } + sort.Sort(ByGVKN(gvknList)) + + firstObj := true + var b []byte + buf := bytes.NewBuffer(b) + for _, gvkn := range gvknList { + obj := in[gvkn] + out, err := yaml.Marshal(obj) + if err != nil { + return nil, err + } + if !firstObj { + _, err = buf.WriteString("---\n") + if err != nil { + return nil, err + } + } + _, err = buf.Write(out) + if err != nil { + return nil, err + } + firstObj = false + } + return buf.Bytes(), nil +} + +type mutateFunc func(interface{}) (interface{}, error) + +func mutateField(m map[string]interface{}, pathToField []string, createIfNotPresent bool, fns ...mutateFunc) error { + if len(pathToField) == 0 { + return nil + } + + _, found := m[pathToField[0]] + if !found { + if !createIfNotPresent { + return nil + } + m[pathToField[0]] = map[string]interface{}{} + } + + if len(pathToField) == 1 { + var err error + for _, fn := range fns { + m[pathToField[0]], err = fn(m[pathToField[0]]) + if err != nil { + return err + } + } + return nil + } + + v := m[pathToField[0]] + newPathToField := pathToField[1:] + switch typedV := v.(type) { + case map[string]interface{}: + return mutateField(typedV, newPathToField, createIfNotPresent, fns...) + case []interface{}: + for i := range typedV { + item := typedV[i] + typedItem, ok := item.(map[string]interface{}) + if !ok { + return fmt.Errorf("%#v is expectd to be %T", item, typedItem) + } + err := mutateField(typedItem, newPathToField, createIfNotPresent, fns...) + if err != nil { + return err + } + } + return nil + default: + return fmt.Errorf("%#v is not expected to be a primitive type", typedV) + } +} diff --git a/pkg/kinflate/util/util_test.go b/pkg/kinflate/util/util_test.go new file mode 100644 index 000000000..ed04dec3a --- /dev/null +++ b/pkg/kinflate/util/util_test.go @@ -0,0 +1,89 @@ +/* +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 util + +import ( + "reflect" + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var encoded = []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm2 +`) + +func createMap() map[GroupVersionKindName]*unstructured.Unstructured { + cm1 := unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "cm1", + }, + }, + } + + cm2 := unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "cm2", + }, + }, + } + return map[GroupVersionKindName]*unstructured.Unstructured{ + { + gvk: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, + name: "cm1", + }: &cm1, + { + gvk: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, + name: "cm2", + }: &cm2, + } +} + +func TestDecode(t *testing.T) { + expected := createMap() + m, err := Decode(encoded) + 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 TestEncode(t *testing.T) { + out, err := Encode(createMap()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(out, encoded) { + t.Fatalf("%s doesn't match expected %s", out, encoded) + } +}