diff --git a/pkg/kinflate/util/labelsandannotations.go b/pkg/kinflate/util/labelsandannotations.go new file mode 100644 index 000000000..581a127d1 --- /dev/null +++ b/pkg/kinflate/util/labelsandannotations.go @@ -0,0 +1,88 @@ +/* +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" +) + +type ApplyAdditionalMapOptions struct { + additionalMap map[string]string + pathConfigs []PathConfig +} + +var _ Transformer = &ApplyAdditionalMapOptions{} + +func (o *ApplyAdditionalMapOptions) CompleteForLabels(m map[string]string, pathConfigs []PathConfig) { + o.additionalMap = m + if pathConfigs == nil { + pathConfigs = DefaultLabelsPathConfigs + } + o.pathConfigs = pathConfigs +} + +func (o *ApplyAdditionalMapOptions) CompleteForAnnotations(m map[string]string, pathConfigs []PathConfig) { + o.additionalMap = m + if pathConfigs == nil { + pathConfigs = DefaultAnnotationsPathConfigs + } + o.pathConfigs = pathConfigs +} + +func (o *ApplyAdditionalMapOptions) Transform(m map[GroupVersionKindName]*unstructured.Unstructured) error { + for gvkn := range m { + obj := m[gvkn] + objMap := obj.UnstructuredContent() + for _, path := range o.pathConfigs { + if !SelectByGVK(gvkn.gvk, path.GroupVersionKind) { + continue + } + err := mutateField(objMap, path.Path, path.CreateIfNotPresent, o.addMap) + if err != nil { + return err + } + } + } + return nil +} + +func (o *ApplyAdditionalMapOptions) 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 *ApplyAdditionalMapOptions) 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.additionalMap { + m[k] = v + } + return m, nil +} diff --git a/pkg/kinflate/util/labelsandannotations_test.go b/pkg/kinflate/util/labelsandannotations_test.go new file mode 100644 index 000000000..a060b3583 --- /dev/null +++ b/pkg/kinflate/util/labelsandannotations_test.go @@ -0,0 +1,320 @@ +/* +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 labelsOps = ApplyAdditionalMapOptions{ + additionalMap: map[string]string{"label-key1": "label-value1", "label-key2": "label-value2"}, + pathConfigs: DefaultLabelsPathConfigs, +} + +var annotationsOps = ApplyAdditionalMapOptions{ + additionalMap: map[string]string{"anno-key1": "anno-value1", "anno-key2": "anno-value2"}, + pathConfigs: DefaultAnnotationsPathConfigs, +} + +func getConfigmap() *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "cm1", + }, + }, + } +} + +func getDeployment() *unstructured.Unstructured { + return &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", + }, + }, + }, + }, + }, + }, + } +} + +func getService() *unstructured.Unstructured { + return &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", + }, + }, + }, + }, + } +} + +func getTestMap() map[GroupVersionKindName]*unstructured.Unstructured { + return map[GroupVersionKindName]*unstructured.Unstructured{ + { + gvk: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, + name: "cm1", + }: getConfigmap(), + { + gvk: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + name: "deploy1", + }: getDeployment(), + { + gvk: schema.GroupVersionKind{Version: "v1", Kind: "Service"}, + name: "svc1", + }: getService(), + } +} + +var labeledObj1 = 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", + }, + }, + }, +} + +var labeledObj2 = 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", + }, + }, + }, + }, + }, + }, +} + +var labeledObj3 = 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", + }, + }, + }, +} + +var labeledM = map[GroupVersionKindName]*unstructured.Unstructured{ + { + gvk: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, + name: "cm1", + }: &labeledObj1, + { + gvk: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + name: "deploy1", + }: &labeledObj2, + { + gvk: schema.GroupVersionKind{Version: "v1", Kind: "Service"}, + name: "svc1", + }: &labeledObj3, +} + +func TestLabelsRun(t *testing.T) { + m := getTestMap() + err := labelsOps.Transform(m) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(m, labeledM) { + err = CompareMap(m, labeledM) + t.Fatalf("actual doesn't match expected: %v", err) + } +} + +var annotatedObj1 = 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", + }, + }, + }, +} + +var annotatedObj2 = 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", + }, + }, + }, + }, + }, + }, +} + +var annotatedObj3 = 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", + }, + }, + }, + }, +} + +var annotatedM = map[GroupVersionKindName]*unstructured.Unstructured{ + { + gvk: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, + name: "cm1", + }: &annotatedObj1, + { + gvk: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + name: "deploy1", + }: &annotatedObj2, + { + gvk: schema.GroupVersionKind{Version: "v1", Kind: "Service"}, + name: "svc1", + }: &annotatedObj3, +} + +func TestAnnotationsRun(t *testing.T) { + m := getTestMap() + err := annotationsOps.Transform(m) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(m, annotatedM) { + err = CompareMap(m, annotatedM) + t.Fatalf("actual doesn't match expected: %v", err) + } +} diff --git a/pkg/kinflate/util/labelsandannotationsconfig.go b/pkg/kinflate/util/labelsandannotationsconfig.go new file mode 100644 index 000000000..0861cc2b2 --- /dev/null +++ b/pkg/kinflate/util/labelsandannotationsconfig.go @@ -0,0 +1,155 @@ +/* +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/runtime/schema" +) + +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, + }, +} + +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, + }, +} diff --git a/pkg/kinflate/util/pathconfig.go b/pkg/kinflate/util/pathconfig.go index d5c901c60..f50d44d34 100644 --- a/pkg/kinflate/util/pathconfig.go +++ b/pkg/kinflate/util/pathconfig.go @@ -16,7 +16,17 @@ limitations under the License. package util +import ( + "k8s.io/apimachinery/pkg/runtime/schema" +) + type PathConfig struct { - Path []string + // If true, it will create the path if it is not found. CreateIfNotPresent bool + // The GVK that this path tied to. + // If unset, it applied to any GVK + // If some fields are set, it applies to all matching GVK. + GroupVersionKind *schema.GroupVersionKind + // Path to the field that will be munged. + Path []string } diff --git a/pkg/kinflate/util/prefixname.go b/pkg/kinflate/util/prefixname.go index 9ae7e8e2a..16ef2f80a 100644 --- a/pkg/kinflate/util/prefixname.go +++ b/pkg/kinflate/util/prefixname.go @@ -51,6 +51,9 @@ func (o *PrefixNameOptions) Transform(m map[GroupVersionKindName]*unstructured.U obj := m[gvkn] objMap := obj.UnstructuredContent() for _, path := range o.pathConfigs { + if !SelectByGVK(gvkn.gvk, path.GroupVersionKind) { + continue + } err := mutateField(objMap, path.Path, path.CreateIfNotPresent, o.addPrefix) if err != nil { return err diff --git a/pkg/kinflate/util/util.go b/pkg/kinflate/util/util.go index c2f07c130..cc4075d5a 100644 --- a/pkg/kinflate/util/util.go +++ b/pkg/kinflate/util/util.go @@ -20,6 +20,7 @@ import ( "bytes" "fmt" "io" + "reflect" "sort" "github.com/ghodss/yaml" @@ -111,6 +112,53 @@ func Encode(in map[GroupVersionKindName]*unstructured.Unstructured) ([]byte, err return buf.Bytes(), nil } +// SelectByGVK returns true if selector is a superset of in; otherwise, false. +func SelectByGVK(in schema.GroupVersionKind, selector *schema.GroupVersionKind) bool { + if selector == nil { + return true + } + if len(selector.Group) > 0 { + if in.Group != selector.Group { + return false + } + } + if len(selector.Version) > 0 { + if in.Version != selector.Version { + return false + } + } + if len(selector.Kind) > 0 { + if in.Kind != selector.Kind { + return false + } + } + return true +} + +func CompareMap(m1, m2 map[GroupVersionKindName]*unstructured.Unstructured) error { + if len(m1) != len(m2) { + keySet1 := []GroupVersionKindName{} + keySet2 := []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 +} + type mutateFunc func(interface{}) (interface{}, error) func mutateField(m map[string]interface{}, pathToField []string, createIfNotPresent bool, fns ...mutateFunc) error { diff --git a/pkg/kinflate/util/util_test.go b/pkg/kinflate/util/util_test.go index ed04dec3a..25539473e 100644 --- a/pkg/kinflate/util/util_test.go +++ b/pkg/kinflate/util/util_test.go @@ -87,3 +87,111 @@ func TestEncode(t *testing.T) { t.Fatalf("%s doesn't match expected %s", out, encoded) } } + +func TestFilterByGVK(t *testing.T) { + type testCase struct { + description string + in schema.GroupVersionKind + filter *schema.GroupVersionKind + expected bool + } + testCases := []testCase{ + { + description: "nil filter", + in: schema.GroupVersionKind{}, + filter: nil, + expected: true, + }, + { + description: "gvk matches", + in: schema.GroupVersionKind{ + Group: "group1", + Version: "version1", + Kind: "kind1", + }, + filter: &schema.GroupVersionKind{ + Group: "group1", + Version: "version1", + Kind: "kind1", + }, + expected: true, + }, + { + description: "group doesn't matches", + in: schema.GroupVersionKind{ + Group: "group1", + Version: "version1", + Kind: "kind1", + }, + filter: &schema.GroupVersionKind{ + Group: "group2", + Version: "version1", + Kind: "kind1", + }, + expected: false, + }, + { + description: "version doesn't matches", + in: schema.GroupVersionKind{ + Group: "group1", + Version: "version1", + Kind: "kind1", + }, + filter: &schema.GroupVersionKind{ + Group: "group1", + Version: "version2", + Kind: "kind1", + }, + expected: false, + }, + { + description: "kind doesn't matches", + in: schema.GroupVersionKind{ + Group: "group1", + Version: "version1", + Kind: "kind1", + }, + filter: &schema.GroupVersionKind{ + Group: "group1", + Version: "version1", + Kind: "kind2", + }, + expected: false, + }, + { + description: "no version in filter", + in: schema.GroupVersionKind{ + Group: "group1", + Version: "version1", + Kind: "kind1", + }, + filter: &schema.GroupVersionKind{ + Group: "group1", + Version: "", + Kind: "kind1", + }, + expected: true, + }, + { + description: "only kind is set in filter", + in: schema.GroupVersionKind{ + Group: "group1", + Version: "version1", + Kind: "kind1", + }, + filter: &schema.GroupVersionKind{ + Group: "", + Version: "", + Kind: "kind1", + }, + expected: true, + }, + } + + for _, tc := range testCases { + filtered := SelectByGVK(tc.in, tc.filter) + if filtered != tc.expected { + t.Fatalf("unexpected filter result for test case: %v", tc.description) + } + } +}