From 696ec9b171ed65da61af09b8d2b69aa44a2d7468 Mon Sep 17 00:00:00 2001 From: Jingfang Liu Date: Tue, 10 Apr 2018 14:32:02 -0700 Subject: [PATCH] change kinflate to kustomize --- app/application.go | 255 +++++++ app/application_test.go | 237 +++++++ commands/addresource.go | 106 +++ commands/addresource_test.go | 94 +++ commands/build.go | 111 +++ commands/build_test.go | 150 ++++ commands/commands.go | 123 ++++ commands/configmap.go | 123 ++++ commands/configmap_test.go | 129 ++++ commands/data_config.go | 50 ++ commands/data_config_test.go | 83 +++ commands/diff.go | 124 ++++ commands/diff_test.go | 124 ++++ commands/init.go | 100 +++ commands/init_test.go | 62 ++ commands/set_name_prefix.go | 90 +++ commands/set_name_prefix_test.go | 66 ++ .../testdata/testcase-base-only/expected.diff | 58 ++ .../testdata/testcase-base-only/expected.yaml | 46 ++ .../testcase-base-only/in/deployment.yaml | 15 + .../testcase-base-only/in/kustomize.yaml | 14 + .../testcase-base-only/in/service.yaml | 11 + .../testdata/testcase-base-only/test.yaml | 5 + .../in/overlay/deployment-patch1.yaml | 20 + .../in/overlay/deployment-patch2.yaml | 12 + .../in/overlay/kustomize.yaml | 16 + .../in/package/deployment.yaml | 24 + .../in/package/kustomize.yaml | 18 + .../in/package/service.yaml | 11 + .../test.yaml | 4 + .../expected.diff | 99 +++ .../expected.yaml | 96 +++ .../in/overlay/deployment-patch1.yaml | 21 + .../in/overlay/deployment-patch2.yaml | 16 + .../in/overlay/kustomize.yaml | 16 + .../in/package/deployment.yaml | 24 + .../in/package/kustomize.yaml | 18 + .../in/package/service.yaml | 11 + .../test.yaml | 5 + .../testdata/testcase-simple/expected.diff | 154 +++++ .../testdata/testcase-simple/expected.yaml | 138 ++++ commands/testdata/testcase-simple/test.yaml | 5 + .../testcase-single-overlay/expected.diff | 128 ++++ .../testcase-single-overlay/expected.yaml | 108 +++ .../in/overlay/deployment.yaml | 15 + .../in/overlay/kustomize.yaml | 25 + .../in/package/deployment.yaml | 24 + .../in/package/kustomize.yaml | 23 + .../in/package/service.yaml | 11 + .../testcase-single-overlay/test.yaml | 5 + commands/util.go | 95 +++ commands/util_test.go | 87 +++ configmapandsecret/configmap_secret.go | 182 +++++ configmapandsecret/configmap_secret_test.go | 243 +++++++ configmapandsecret/util/configmap.go | 134 ++++ configmapandsecret/util/env_file.go | 103 +++ configmapandsecret/util/secret.go | 126 ++++ configmapandsecret/util/util.go | 91 +++ constants/constants.go | 26 + .../exampleinstance/configmap/app-init.ini | 2 + .../exampleinstance/configmap/app.env | 2 + .../deployment/deployment.yaml | 43 ++ .../instances/exampleinstance/kustomize.yaml | 35 + .../instances/exampleinstance/secret/tls.cert | 12 + .../instances/exampleinstance/secret/tls.key | 9 + examples/simple/package/Kube-descriptor.yaml | 14 + .../simple/package/deployment/deployment.yaml | 21 + examples/simple/package/kustomize.yaml | 19 + examples/simple/package/service/service.yaml | 11 + hash/hash.go | 110 +++ hash/hash_test.go | 178 +++++ internal/error/configmaperror.go | 28 + internal/error/configmaperror_test.go | 38 + internal/error/manifesterror.go | 57 ++ internal/error/manifesterror_test.go | 92 +++ internal/error/patcherror.go | 31 + internal/error/patcherror_test.go | 41 ++ internal/error/resourceerror.go | 30 + internal/error/resourceerror_test.go | 41 ++ internal/error/secreterror.go | 28 + internal/error/secreterror_test.go | 37 + resource/appresource.go | 57 ++ resource/appresource_test.go | 110 +++ resource/configmap.go | 144 ++++ resource/configmap_test.go | 158 +++++ resource/kv.go | 102 +++ resource/kv_test.go | 67 ++ resource/resource.go | 55 ++ resource/secret.go | 83 +++ resource/secret_test.go | 72 ++ resource/util.go | 170 +++++ resource/util_test.go | 151 ++++ transformers/labelsandannotations.go | 85 +++ transformers/labelsandannotations_test.go | 455 ++++++++++++ transformers/labelsandannotationsconfig.go | 159 +++++ transformers/multitransformer.go | 45 ++ transformers/namehash.go | 104 +++ transformers/namehash_test.go | 206 ++++++ transformers/namereference.go | 110 +++ transformers/namereference_test.go | 238 +++++++ transformers/namereferenceconfig.go | 342 +++++++++ transformers/nooptransformer.go | 34 + transformers/overlay.go | 164 +++++ transformers/overlay_test.go | 648 ++++++++++++++++++ transformers/overlayconflictdetector.go | 130 ++++ transformers/pathconfig.go | 55 ++ transformers/prefixname.go | 83 +++ transformers/prefixname_test.go | 102 +++ transformers/transformer.go | 25 + transformers/util.go | 70 ++ transformers/util_test.go | 49 ++ types/gvkn_sort.go | 35 + types/types.go | 29 + types/util.go | 59 ++ types/util_test.go | 131 ++++ util/diff.go | 104 +++ util/fs/fakefile.go | 74 ++ util/fs/fakefileinfo.go | 47 ++ util/fs/fakefs.go | 80 +++ util/fs/fakefs_test.go | 105 +++ util/fs/fs.go | 38 + util/fs/realfile.go | 49 ++ util/fs/realfs.go | 52 ++ util/util.go | 80 +++ util/util_test.go | 102 +++ 125 files changed, 10447 insertions(+) create mode 100644 app/application.go create mode 100644 app/application_test.go create mode 100644 commands/addresource.go create mode 100644 commands/addresource_test.go create mode 100644 commands/build.go create mode 100644 commands/build_test.go create mode 100644 commands/commands.go create mode 100644 commands/configmap.go create mode 100644 commands/configmap_test.go create mode 100644 commands/data_config.go create mode 100644 commands/data_config_test.go create mode 100644 commands/diff.go create mode 100644 commands/diff_test.go create mode 100644 commands/init.go create mode 100644 commands/init_test.go create mode 100644 commands/set_name_prefix.go create mode 100644 commands/set_name_prefix_test.go create mode 100644 commands/testdata/testcase-base-only/expected.diff create mode 100644 commands/testdata/testcase-base-only/expected.yaml create mode 100644 commands/testdata/testcase-base-only/in/deployment.yaml create mode 100644 commands/testdata/testcase-base-only/in/kustomize.yaml create mode 100644 commands/testdata/testcase-base-only/in/service.yaml create mode 100644 commands/testdata/testcase-base-only/test.yaml create mode 100644 commands/testdata/testcase-multiple-patches-conflict/in/overlay/deployment-patch1.yaml create mode 100644 commands/testdata/testcase-multiple-patches-conflict/in/overlay/deployment-patch2.yaml create mode 100644 commands/testdata/testcase-multiple-patches-conflict/in/overlay/kustomize.yaml create mode 100644 commands/testdata/testcase-multiple-patches-conflict/in/package/deployment.yaml create mode 100644 commands/testdata/testcase-multiple-patches-conflict/in/package/kustomize.yaml create mode 100644 commands/testdata/testcase-multiple-patches-conflict/in/package/service.yaml create mode 100644 commands/testdata/testcase-multiple-patches-conflict/test.yaml create mode 100644 commands/testdata/testcase-multiple-patches-noconflict/expected.diff create mode 100644 commands/testdata/testcase-multiple-patches-noconflict/expected.yaml create mode 100644 commands/testdata/testcase-multiple-patches-noconflict/in/overlay/deployment-patch1.yaml create mode 100644 commands/testdata/testcase-multiple-patches-noconflict/in/overlay/deployment-patch2.yaml create mode 100644 commands/testdata/testcase-multiple-patches-noconflict/in/overlay/kustomize.yaml create mode 100644 commands/testdata/testcase-multiple-patches-noconflict/in/package/deployment.yaml create mode 100644 commands/testdata/testcase-multiple-patches-noconflict/in/package/kustomize.yaml create mode 100644 commands/testdata/testcase-multiple-patches-noconflict/in/package/service.yaml create mode 100644 commands/testdata/testcase-multiple-patches-noconflict/test.yaml create mode 100644 commands/testdata/testcase-simple/expected.diff create mode 100644 commands/testdata/testcase-simple/expected.yaml create mode 100644 commands/testdata/testcase-simple/test.yaml create mode 100644 commands/testdata/testcase-single-overlay/expected.diff create mode 100644 commands/testdata/testcase-single-overlay/expected.yaml create mode 100644 commands/testdata/testcase-single-overlay/in/overlay/deployment.yaml create mode 100644 commands/testdata/testcase-single-overlay/in/overlay/kustomize.yaml create mode 100644 commands/testdata/testcase-single-overlay/in/package/deployment.yaml create mode 100644 commands/testdata/testcase-single-overlay/in/package/kustomize.yaml create mode 100644 commands/testdata/testcase-single-overlay/in/package/service.yaml create mode 100644 commands/testdata/testcase-single-overlay/test.yaml create mode 100644 commands/util.go create mode 100644 commands/util_test.go create mode 100644 configmapandsecret/configmap_secret.go create mode 100644 configmapandsecret/configmap_secret_test.go create mode 100644 configmapandsecret/util/configmap.go create mode 100644 configmapandsecret/util/env_file.go create mode 100644 configmapandsecret/util/secret.go create mode 100644 configmapandsecret/util/util.go create mode 100644 constants/constants.go create mode 100644 examples/simple/instances/exampleinstance/configmap/app-init.ini create mode 100644 examples/simple/instances/exampleinstance/configmap/app.env create mode 100644 examples/simple/instances/exampleinstance/deployment/deployment.yaml create mode 100644 examples/simple/instances/exampleinstance/kustomize.yaml create mode 100644 examples/simple/instances/exampleinstance/secret/tls.cert create mode 100644 examples/simple/instances/exampleinstance/secret/tls.key create mode 100644 examples/simple/package/Kube-descriptor.yaml create mode 100644 examples/simple/package/deployment/deployment.yaml create mode 100644 examples/simple/package/kustomize.yaml create mode 100644 examples/simple/package/service/service.yaml create mode 100644 hash/hash.go create mode 100644 hash/hash_test.go create mode 100644 internal/error/configmaperror.go create mode 100644 internal/error/configmaperror_test.go create mode 100644 internal/error/manifesterror.go create mode 100644 internal/error/manifesterror_test.go create mode 100644 internal/error/patcherror.go create mode 100644 internal/error/patcherror_test.go create mode 100644 internal/error/resourceerror.go create mode 100644 internal/error/resourceerror_test.go create mode 100644 internal/error/secreterror.go create mode 100644 internal/error/secreterror_test.go create mode 100644 resource/appresource.go create mode 100644 resource/appresource_test.go create mode 100644 resource/configmap.go create mode 100644 resource/configmap_test.go create mode 100644 resource/kv.go create mode 100644 resource/kv_test.go create mode 100644 resource/resource.go create mode 100644 resource/secret.go create mode 100644 resource/secret_test.go create mode 100644 resource/util.go create mode 100644 resource/util_test.go create mode 100644 transformers/labelsandannotations.go create mode 100644 transformers/labelsandannotations_test.go create mode 100644 transformers/labelsandannotationsconfig.go create mode 100644 transformers/multitransformer.go create mode 100644 transformers/namehash.go create mode 100644 transformers/namehash_test.go create mode 100644 transformers/namereference.go create mode 100644 transformers/namereference_test.go create mode 100644 transformers/namereferenceconfig.go create mode 100644 transformers/nooptransformer.go create mode 100644 transformers/overlay.go create mode 100644 transformers/overlay_test.go create mode 100644 transformers/overlayconflictdetector.go create mode 100644 transformers/pathconfig.go create mode 100644 transformers/prefixname.go create mode 100644 transformers/prefixname_test.go create mode 100644 transformers/transformer.go create mode 100644 transformers/util.go create mode 100644 transformers/util_test.go create mode 100644 types/gvkn_sort.go create mode 100644 types/types.go create mode 100644 types/util.go create mode 100644 types/util_test.go create mode 100644 util/diff.go create mode 100644 util/fs/fakefile.go create mode 100644 util/fs/fakefileinfo.go create mode 100644 util/fs/fakefs.go create mode 100644 util/fs/fakefs_test.go create mode 100644 util/fs/fs.go create mode 100644 util/fs/realfile.go create mode 100644 util/fs/realfs.go create mode 100644 util/util.go create mode 100644 util/util_test.go diff --git a/app/application.go b/app/application.go new file mode 100644 index 000000000..586d257fe --- /dev/null +++ b/app/application.go @@ -0,0 +1,255 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package app + +import ( + "bytes" + "encoding/json" + + "github.com/ghodss/yaml" + + manifest "k8s.io/kubectl/pkg/apis/manifest/v1alpha1" + "k8s.io/kubectl/pkg/kustomize/constants" + interror "k8s.io/kubectl/pkg/kustomize/internal/error" + "k8s.io/kubectl/pkg/kustomize/resource" + "k8s.io/kubectl/pkg/kustomize/transformers" + "k8s.io/kubectl/pkg/loader" +) + +type Application interface { + // Resources computes and returns the resources for the app. + Resources() (resource.ResourceCollection, error) + // SemiResources computes and returns the resources without name hash and name reference for the app + SemiResources() (resource.ResourceCollection, error) + // RawResources computes and returns the raw resources from the manifest. + // It contains resources from 1) untransformed resources from current manifest 2) transformed resources from sub packages + RawResources() (resource.ResourceCollection, error) +} + +var _ Application = &applicationImpl{} + +// Private implementation of the Application interface +type applicationImpl struct { + manifest *manifest.Manifest + loader loader.Loader +} + +// NewApp parses the manifest at the path using the loader. +func New(loader loader.Loader) (Application, error) { + // load the manifest using the loader + manifestBytes, err := loader.Load(constants.KustomizeFileName) + if err != nil { + return nil, err + } + + var m manifest.Manifest + err = unmarshal(manifestBytes, &m) + if err != nil { + return nil, err + } + return &applicationImpl{manifest: &m, loader: loader}, nil +} + +// Resources computes and returns the resources from the manifest. +// The namehashing for configmap/secrets and resolving name reference is only done +// in the most top overlay once at the end of getting resources. +func (a *applicationImpl) Resources() (resource.ResourceCollection, error) { + res, err := a.SemiResources() + if err != nil { + return nil, err + } + t, err := a.getHashAndReferenceTransformer() + if err != nil { + return nil, err + } + err = t.Transform(res) + if err != nil { + return nil, err + } + return res, nil +} + +// SemiResources computes and returns the resources without name hash and name reference for the app +func (a *applicationImpl) SemiResources() (resource.ResourceCollection, error) { + errs := &interror.ManifestErrors{} + raw, err := a.rawResources() + if err != nil { + errs.Append(err) + } + + cms, err := resource.NewFromConfigMaps(a.loader, a.manifest.ConfigMapGenerator) + if err != nil { + errs.Append(err) + } + secrets, err := resource.NewFromSecretGenerators(a.loader.Root(), a.manifest.SecretGenerator) + if err != nil { + errs.Append(err) + } + res, err := resource.Merge(cms, secrets) + if err != nil { + return nil, err + } + + allRes, err := resource.MergeWithOverride(raw, res) + if err != nil { + return nil, err + } + + patches, err := resource.NewFromPatches(a.loader, a.manifest.Patches) + if err != nil { + errs.Append(err) + } + + if len(errs.Get()) > 0 { + return nil, errs + } + + t, err := a.getTransformer(patches) + if err != nil { + return nil, err + } + err = t.Transform(allRes) + if err != nil { + return nil, err + } + + return allRes, nil +} + +// RawResources computes and returns the raw resources from the manifest. +// The namehashing for configmap/secrets and resolving name reference is only done +// in the most top overlay once at the end of getting resources. +func (a *applicationImpl) RawResources() (resource.ResourceCollection, error) { + res, err := a.rawResources() + if err != nil { + return nil, err + } + t, err := a.getHashAndReferenceTransformer() + if err != nil { + return nil, err + } + err = t.Transform(res) + if err != nil { + return nil, err + } + return res, nil +} + +func (a *applicationImpl) rawResources() (resource.ResourceCollection, error) { + subAppResources, errs := a.subAppResources() + resources, err := resource.NewFromResources(a.loader, a.manifest.Resources) + if err != nil { + errs.Append(err) + } + + if len(errs.Get()) > 0 { + return nil, errs + } + + return resource.Merge(resources, subAppResources) +} + +func (a *applicationImpl) subAppResources() (resource.ResourceCollection, *interror.ManifestErrors) { + sliceOfSubAppResources := []resource.ResourceCollection{} + errs := &interror.ManifestErrors{} + for _, pkgPath := range a.manifest.Bases { + subloader, err := a.loader.New(pkgPath) + if err != nil { + errs.Append(err) + continue + } + subapp, err := New(subloader) + if err != nil { + errs.Append(err) + continue + } + // Gather all transformed resources from subpackages. + subAppResources, err := subapp.SemiResources() + if err != nil { + errs.Append(err) + continue + } + sliceOfSubAppResources = append(sliceOfSubAppResources, subAppResources) + } + allResources, err := resource.Merge(sliceOfSubAppResources...) + if err != nil { + errs.Append(err) + } + return allResources, errs +} + +// getTransformer generates the following transformers: +// 1) apply overlay +// 2) name prefix +// 3) apply labels +// 4) apply annotations +func (a *applicationImpl) getTransformer(patches []*resource.Resource) (transformers.Transformer, error) { + ts := []transformers.Transformer{} + + ot, err := transformers.NewOverlayTransformer(patches) + if err != nil { + return nil, err + } + ts = append(ts, ot) + + npt, err := transformers.NewDefaultingNamePrefixTransformer(string(a.manifest.NamePrefix)) + if err != nil { + return nil, err + } + ts = append(ts, npt) + + lt, err := transformers.NewDefaultingLabelsMapTransformer(a.manifest.ObjectLabels) + if err != nil { + return nil, err + } + ts = append(ts, lt) + + at, err := transformers.NewDefaultingAnnotationsMapTransformer(a.manifest.ObjectAnnotations) + if err != nil { + return nil, err + } + ts = append(ts, at) + + return transformers.NewMultiTransformer(ts), nil +} + +// getHashAndReferenceTransformer generates the following transformers: +// 1) name hash for configmap and secrests +// 2) apply name reference +func (a *applicationImpl) getHashAndReferenceTransformer() (transformers.Transformer, error) { + ts := []transformers.Transformer{} + nht := transformers.NewNameHashTransformer() + ts = append(ts, nht) + + nrt, err := transformers.NewDefaultingNameReferenceTransformer() + if err != nil { + return nil, err + } + ts = append(ts, nrt) + return transformers.NewMultiTransformer(ts), nil +} + +func unmarshal(y []byte, o interface{}) error { + j, err := yaml.YAMLToJSON(y) + if err != nil { + return err + } + + dec := json.NewDecoder(bytes.NewReader(j)) + dec.DisallowUnknownFields() + return dec.Decode(o) +} diff --git a/app/application_test.go b/app/application_test.go new file mode 100644 index 000000000..3a49b923b --- /dev/null +++ b/app/application_test.go @@ -0,0 +1,237 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package app + +import ( + "encoding/base64" + "fmt" + "reflect" + "testing" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kubectl/pkg/kustomize/resource" + "k8s.io/kubectl/pkg/kustomize/types" + "k8s.io/kubectl/pkg/loader" + "k8s.io/kubectl/pkg/loader/loadertest" +) + +func setupTest(t *testing.T) loader.Loader { + manifestContent := []byte(`apiVersion: manifest.k8s.io/v1alpha1 +kind: Manifest +metadata: + name: nginx-app +namePrefix: foo- +objectLabels: + app: nginx +objectAnnotations: + note: This is a test annotation +resources: + - deployment.yaml +configMapGenerator: +- name: literalConfigMap + literals: + - DB_USERNAME=admin + - DB_PASSWORD=somepw +secretGenerator: +- name: secret + commands: + DB_USERNAME: "printf admin" + DB_PASSWORD: "printf somepw" + type: Opaque +`) + deploymentContent := []byte(`apiVersion: apps/v1 +kind: Deployment +metadata: + name: dply1 +`) + + loader := loadertest.NewFakeLoader("/testpath") + err := loader.AddFile("/testpath/kustomize.yaml", manifestContent) + if err != nil { + t.Fatalf("Failed to setup fake loader.") + } + err = loader.AddFile("/testpath/deployment.yaml", deploymentContent) + if err != nil { + t.Fatalf("Failed to setup fake loader.") + } + return loader +} + +func TestResources(t *testing.T) { + expected := resource.ResourceCollection{ + types.GroupVersionKindName{ + GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + Name: "dply1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "foo-dply1", + "labels": map[string]interface{}{ + "app": "nginx", + }, + "annotations": map[string]interface{}{ + "note": "This is a test annotation", + }, + }, + "spec": map[string]interface{}{ + "selector": map[string]interface{}{ + "matchLabels": map[string]interface{}{ + "app": "nginx", + }, + }, + "template": map[string]interface{}{ + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{ + "note": "This is a test annotation", + }, + "labels": map[string]interface{}{ + "app": "nginx", + }, + }, + }, + }, + }, + }, + }, + types.GroupVersionKindName{ + GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, + Name: "literalConfigMap", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "foo-literalConfigMap-mc92bgcbh5", + "labels": map[string]interface{}{ + "app": "nginx", + }, + "annotations": map[string]interface{}{ + "note": "This is a test annotation", + }, + "creationTimestamp": nil, + }, + "data": map[string]interface{}{ + "DB_USERNAME": "admin", + "DB_PASSWORD": "somepw", + }, + }, + }, + }, + types.GroupVersionKindName{ + GVK: schema.GroupVersionKind{Version: "v1", Kind: "Secret"}, + Name: "secret", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Secret", + "metadata": map[string]interface{}{ + "name": "foo-secret-877fcfhgt5", + "labels": map[string]interface{}{ + "app": "nginx", + }, + "annotations": map[string]interface{}{ + "note": "This is a test annotation", + }, + "creationTimestamp": nil, + }, + "type": string(corev1.SecretTypeOpaque), + "data": map[string]interface{}{ + "DB_USERNAME": base64.StdEncoding.EncodeToString([]byte("admin")), + "DB_PASSWORD": base64.StdEncoding.EncodeToString([]byte("somepw")), + }, + }, + }, + }, + } + l := setupTest(t) + app, err := New(l) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + actual, err := app.Resources() + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + + if !reflect.DeepEqual(actual, expected) { + err = compareMap(actual, expected) + t.Fatalf("unexpected error: %v", err) + } +} + +func TestRawResources(t *testing.T) { + expected := resource.ResourceCollection{ + types.GroupVersionKindName{ + GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + Name: "dply1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "dply1", + }, + }, + }, + }, + } + l := setupTest(t) + app, err := New(l) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + actual, err := app.RawResources() + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + + if err := compareMap(actual, expected); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func compareMap(m1, m2 resource.ResourceCollection) error { + if len(m1) != len(m2) { + keySet1 := []types.GroupVersionKindName{} + keySet2 := []types.GroupVersionKindName{} + for GVKn := range m1 { + keySet1 = append(keySet1, GVKn) + } + for GVKn := range m1 { + keySet2 = append(keySet2, GVKn) + } + return fmt.Errorf("maps has different number of entries: %#v doesn't equals %#v", keySet1, keySet2) + } + for GVKn, obj1 := range m1 { + obj2, found := m2[GVKn] + if !found { + return fmt.Errorf("%#v doesn't exist in %#v", GVKn, m2) + } + if !reflect.DeepEqual(obj1, obj2) { + return fmt.Errorf("%#v doesn't match %#v", obj1, obj2) + } + } + return nil +} diff --git a/commands/addresource.go b/commands/addresource.go new file mode 100644 index 000000000..414930599 --- /dev/null +++ b/commands/addresource.go @@ -0,0 +1,106 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package commands + +import ( + "errors" + "fmt" + "io" + + "github.com/spf13/cobra" + + "k8s.io/kubectl/pkg/kustomize/constants" + "k8s.io/kubectl/pkg/kustomize/util/fs" +) + +type addResourceOptions struct { + resourceFilePath string +} + +// newCmdAddResource adds the name of a file containing a resource to the manifest. +func newCmdAddResource(out, errOut io.Writer, fsys fs.FileSystem) *cobra.Command { + var o addResourceOptions + + cmd := &cobra.Command{ + Use: "resource", + Short: "Add the name of a file containing a resource to the manifest.", + Long: "Add the name of a file containing a resource to the manifest.", + Example: ` + add resource {filepath}`, + RunE: func(cmd *cobra.Command, args []string) error { + err := o.Validate(args) + if err != nil { + return err + } + err = o.Complete(cmd, args) + if err != nil { + return err + } + return o.RunAddResource(out, errOut, fsys) + }, + } + return cmd +} + +// Validate validates addResource command. +func (o *addResourceOptions) Validate(args []string) error { + if len(args) != 1 { + return errors.New("must specify a resource file") + } + o.resourceFilePath = args[0] + return nil +} + +// Complete completes addResource command. +func (o *addResourceOptions) Complete(cmd *cobra.Command, args []string) error { + return nil +} + +func stringInSlice(str string, list []string) bool { + for _, v := range list { + if v == str { + return true + } + } + return false +} + +// RunAddResource runs addResource command (do real work). +func (o *addResourceOptions) RunAddResource(out, errOut io.Writer, fsys fs.FileSystem) error { + _, err := fsys.Stat(o.resourceFilePath) + if err != nil { + return err + } + + mf, err := newManifestFile(constants.KustomizeFileName, fsys) + if err != nil { + return err + } + + m, err := mf.read() + if err != nil { + return err + } + + if stringInSlice(o.resourceFilePath, m.Resources) { + return fmt.Errorf("resource %s already in manifest", o.resourceFilePath) + } + + m.Resources = append(m.Resources, o.resourceFilePath) + + return mf.write(m) +} diff --git a/commands/addresource_test.go b/commands/addresource_test.go new file mode 100644 index 000000000..87c2eaa4c --- /dev/null +++ b/commands/addresource_test.go @@ -0,0 +1,94 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package commands + +import ( + "bytes" + "os" + "testing" + + "strings" + + "k8s.io/kubectl/pkg/kustomize/constants" + "k8s.io/kubectl/pkg/kustomize/util/fs" +) + +const ( + resourceFileName = "myWonderfulResource.yaml" + resourceFileContent = ` +Lorem ipsum dolor sit amet, consectetur adipiscing elit, +sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +` +) + +func TestAddResourceHappyPath(t *testing.T) { + buf := bytes.NewBuffer([]byte{}) + fakeFS := fs.MakeFakeFS() + fakeFS.WriteFile(resourceFileName, []byte(resourceFileContent)) + fakeFS.WriteFile(constants.KustomizeFileName, []byte(manifestTemplate)) + + cmd := newCmdAddResource(buf, os.Stderr, fakeFS) + args := []string{resourceFileName} + err := cmd.RunE(cmd, args) + if err != nil { + t.Errorf("unexpected cmd error: %v", err) + } + content, err := fakeFS.ReadFile(constants.KustomizeFileName) + if err != nil { + t.Errorf("unexpected read error: %v", err) + } + if !strings.Contains(string(content), resourceFileName) { + t.Errorf("expected resource name in manifest") + } +} + +func TestAddResourceAlreadyThere(t *testing.T) { + buf := bytes.NewBuffer([]byte{}) + fakeFS := fs.MakeFakeFS() + fakeFS.WriteFile(resourceFileName, []byte(resourceFileContent)) + fakeFS.WriteFile(constants.KustomizeFileName, []byte(manifestTemplate)) + + cmd := newCmdAddResource(buf, os.Stderr, fakeFS) + args := []string{resourceFileName} + err := cmd.RunE(cmd, args) + if err != nil { + t.Fatalf("unexpected cmd error: %v", err) + } + + // adding an existing resource should return an error + err = cmd.RunE(cmd, args) + if err == nil { + t.Errorf("expected already there problem") + } + if err.Error() != "resource "+resourceFileName+" already in manifest" { + t.Errorf("unexpected error %v", err) + } +} + +func TestAddResourceNoArgs(t *testing.T) { + buf := bytes.NewBuffer([]byte{}) + fakeFS := fs.MakeFakeFS() + + cmd := newCmdAddResource(buf, os.Stderr, fakeFS) + err := cmd.Execute() + if err == nil { + t.Errorf("expected error: %v", err) + } + if err.Error() != "must specify a resource file" { + t.Errorf("incorrect error: %v", err.Error()) + } +} diff --git a/commands/build.go b/commands/build.go new file mode 100644 index 000000000..7d780852b --- /dev/null +++ b/commands/build.go @@ -0,0 +1,111 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package commands + +import ( + "fmt" + "io" + "os" + "path/filepath" + + "github.com/spf13/cobra" + + "errors" + + "k8s.io/kubectl/pkg/kustomize/app" + "k8s.io/kubectl/pkg/kustomize/constants" + kutil "k8s.io/kubectl/pkg/kustomize/util" + "k8s.io/kubectl/pkg/kustomize/util/fs" + "k8s.io/kubectl/pkg/loader" +) + +type buildOptions struct { + manifestPath string +} + +// newCmdBuild creates a new build command. +func newCmdBuild(out, errOut io.Writer, fs fs.FileSystem) *cobra.Command { + var o buildOptions + + cmd := &cobra.Command{ + Use: "build [path]", + Short: "Print current configuration per contents of " + constants.KustomizeFileName, + Example: ` + # Use the kustomize.yaml file under somedir/ to generate a set of api resources. + build somedir/`, + Run: func(cmd *cobra.Command, args []string) { + err := o.Validate(args) + if err != nil { + fmt.Fprintf(errOut, "error: %v\n", err) + os.Exit(1) + } + err = o.RunBuild(out, errOut, fs) + if err != nil { + fmt.Fprintf(errOut, "error: %v\n", err) + os.Exit(1) + } + }, + } + return cmd +} + +// Validate validates build command. +func (o *buildOptions) Validate(args []string) error { + if len(args) > 1 { + return errors.New("specify one path to manifest") + } + if len(args) == 0 { + o.manifestPath = "./" + return nil + } + o.manifestPath = args[0] + return nil +} + +// RunBuild runs build command. +func (o *buildOptions) RunBuild(out, errOut io.Writer, fs fs.FileSystem) error { + l := loader.Init([]loader.SchemeLoader{loader.NewFileLoader(fs)}) + + absPath, err := filepath.Abs(o.manifestPath) + if err != nil { + return err + } + + rootLoader, err := l.New(absPath) + if err != nil { + return err + } + + application, err := app.New(rootLoader) + if err != nil { + return err + } + + allResources, err := application.Resources() + + if err != nil { + return err + } + + // Output the objects. + res, err := kutil.Encode(allResources) + if err != nil { + return err + } + _, err = out.Write(res) + return err +} diff --git a/commands/build_test.go b/commands/build_test.go new file mode 100644 index 000000000..2007a6795 --- /dev/null +++ b/commands/build_test.go @@ -0,0 +1,150 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package commands + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/ghodss/yaml" + + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/kubectl/pkg/kustomize/util/fs" +) + +type buildTestCase struct { + Description string `yaml:"description"` + Args []string `yaml:"args"` + Filename string `yaml:"filename"` + // path to the file that contains the expected output + ExpectedStdout string `yaml:"expectedStdout"` + ExpectedError string `yaml:"expectedError"` +} + +func TestBuildValidate(t *testing.T) { + var cases = []struct { + name string + args []string + path string + erMsg string + }{ + {"noargs", []string{}, "./", ""}, + {"file", []string{"beans"}, "beans", ""}, + {"path", []string{"a/b/c"}, "a/b/c", ""}, + {"path", []string{"too", "many"}, "", "specify one path to manifest"}, + } + for _, mycase := range cases { + opts := buildOptions{} + e := opts.Validate(mycase.args) + if len(mycase.erMsg) > 0 { + if e == nil { + t.Errorf("%s: Expected an error %v", mycase.name, mycase.erMsg) + } + if e.Error() != mycase.erMsg { + t.Errorf("%s: Expected error %s, but got %v", mycase.name, mycase.erMsg, e) + } + continue + } + if e != nil { + t.Errorf("%s: unknown error %v", mycase.name, e) + continue + } + if opts.manifestPath != mycase.path { + t.Errorf("%s: expected path '%s', got '%s'", mycase.name, mycase.path, opts.manifestPath) + } + } +} + +func TestBuild(t *testing.T) { + const updateEnvVar = "UPDATE_KUSTOMIZE_EXPECTED_DATA" + updateKustomizeExpected := os.Getenv(updateEnvVar) == "true" + fs := fs.MakeRealFS() + + testcases := sets.NewString() + filepath.Walk("testdata", func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if path == "testdata" { + return nil + } + name := filepath.Base(path) + if info.IsDir() { + if strings.HasPrefix(name, "testcase-") { + testcases.Insert(strings.TrimPrefix(name, "testcase-")) + } + return filepath.SkipDir + } + return nil + }) + // sanity check that we found the right folder + if !testcases.Has("simple") { + t.Fatalf("Error locating testcases") + } + + for _, testcaseName := range testcases.List() { + t.Run(testcaseName, func(t *testing.T) { + name := testcaseName + testcase := buildTestCase{} + testcaseDir := filepath.Join("testdata", "testcase-"+name) + testcaseData, err := ioutil.ReadFile(filepath.Join(testcaseDir, "test.yaml")) + if err != nil { + t.Fatalf("%s: %v", name, err) + } + if err := yaml.Unmarshal(testcaseData, &testcase); err != nil { + t.Fatalf("%s: %v", name, err) + } + + ops := &buildOptions{ + manifestPath: testcase.Filename, + } + buf := bytes.NewBuffer([]byte{}) + err = ops.RunBuild(buf, os.Stderr, fs) + switch { + case err != nil && len(testcase.ExpectedError) == 0: + t.Errorf("unexpected error: %v", err) + case err != nil && len(testcase.ExpectedError) != 0: + if !strings.Contains(err.Error(), testcase.ExpectedError) { + t.Errorf("expected error to contain %q but got: %v", testcase.ExpectedError, err) + } + return + case err == nil && len(testcase.ExpectedError) != 0: + t.Errorf("unexpected no error") + } + + actualBytes := buf.Bytes() + if !updateKustomizeExpected { + expectedBytes, err := ioutil.ReadFile(testcase.ExpectedStdout) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if !reflect.DeepEqual(actualBytes, expectedBytes) { + t.Errorf("%s\ndoesn't equal expected:\n%s\n", actualBytes, expectedBytes) + } + } else { + ioutil.WriteFile(testcase.ExpectedStdout, actualBytes, 0644) + } + + }) + } + +} diff --git a/commands/commands.go b/commands/commands.go new file mode 100644 index 000000000..c76f66816 --- /dev/null +++ b/commands/commands.go @@ -0,0 +1,123 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package commands + +import ( + "flag" + "io" + "os" + + "github.com/spf13/cobra" + "k8s.io/kubectl/cmd/kustomize/version" + "k8s.io/kubectl/pkg/kustomize/util/fs" +) + +// NewDefaultCommand returns the default (aka root) command for kustomize command. +func NewDefaultCommand() *cobra.Command { + fsys := fs.MakeRealFS() + stdOut, stdErr := os.Stdout, os.Stderr + + c := &cobra.Command{ + Use: "kustomize", + Short: "kustomize manages declarative configuration of Kubernetes", + Long: ` +kustomize manages declarative configuration of Kubernetes. + +More info at https://github.com/kubernetes/kubectl/tree/master/cmd/kustomize +`, + } + + c.AddCommand( + newCmdBuild(stdOut, stdErr, fsys), + newCmdDiff(stdOut, stdErr, fsys), + newCmdInit(stdOut, stdErr, fsys), + newCmdEdit(stdOut, stdErr, fsys), + version.NewCmdVersion(stdOut), + ) + c.PersistentFlags().AddGoFlagSet(flag.CommandLine) + + // Workaround for this issue: + // https://github.com/kubernetes/kubernetes/issues/17162 + flag.CommandLine.Parse([]string{}) + return c +} + +// newCmdEdit returns an instance of 'edit' subcommand. +func newCmdEdit(stdOut, stdErr io.Writer, fsys fs.FileSystem) *cobra.Command { + c := &cobra.Command{ + Use: "edit", + Short: "Edits a manifest file", + Long: "", + Example: ` + # Adds a configmap to the manifest + kustomize edit add configmap NAME --from-literal=k=v + + # Sets the nameprefix field + kustomize edit set nameprefix +`, + Args: cobra.MinimumNArgs(1), + } + c.AddCommand( + newCmdAdd(stdOut, stdErr, fsys), + newCmdSet(stdOut, stdErr, fsys), + ) + return c +} + +// newAddCommand returns an instance of 'add' subcommand. +func newCmdAdd(stdOut, stdErr io.Writer, fsys fs.FileSystem) *cobra.Command { + c := &cobra.Command{ + Use: "add", + Short: "Adds configmap/resource/secret to the manifest.", + Long: "", + Example: ` + # Adds a configmap to the manifest + kustomize edit add configmap NAME --from-literal=k=v + + # Adds a secret to the manifest + kustomize edit add secret NAME --from-literal=k=v + + # Adds a resource to the manifest + kustomize edit add resource +`, + Args: cobra.MinimumNArgs(1), + } + c.AddCommand( + newCmdAddResource(stdOut, stdErr, fsys), + newCmdAddConfigMap(stdErr, fsys), + ) + return c +} + +// newSetCommand returns an instance of 'set' subcommand. +func newCmdSet(stdOut, stdErr io.Writer, fsys fs.FileSystem) *cobra.Command { + c := &cobra.Command{ + Use: "set", + Short: "Sets the value of different fields in manifest.", + Long: "", + Example: ` + # Sets the nameprefix field + kustomize edit set nameprefix +`, + Args: cobra.MinimumNArgs(1), + } + + c.AddCommand( + newCmdSetNamePrefix(stdOut, stdErr, fsys), + ) + return c +} diff --git a/commands/configmap.go b/commands/configmap.go new file mode 100644 index 000000000..127adbc9c --- /dev/null +++ b/commands/configmap.go @@ -0,0 +1,123 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package commands + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" + + manifest "k8s.io/kubectl/pkg/apis/manifest/v1alpha1" + "k8s.io/kubectl/pkg/kustomize/configmapandsecret" + "k8s.io/kubectl/pkg/kustomize/constants" + "k8s.io/kubectl/pkg/kustomize/util/fs" +) + +func newCmdAddConfigMap(errOut io.Writer, fsys fs.FileSystem) *cobra.Command { + var config dataConfig + cmd := &cobra.Command{ + Use: "configmap NAME [--from-file=[key=]source] [--from-literal=key1=value1]", + Short: "Adds a configmap to the manifest.", + Long: "", + Example: ` + # Adds a configmap to the Manifest (with a specified key) + kustomize edit add configmap my-configmap --from-file=my-key=file/path --from-literal=my-literal=12345 + + # Adds a configmap to the Manifest (key is the filename) + kustomize edit add configmap my-configmap --from-file=file/path + + # Adds a configmap from env-file + kustomize edit add configmap my-configmap --from-env-file=env/path.env +`, + RunE: func(_ *cobra.Command, args []string) error { + err := config.Validate(args) + if err != nil { + return err + } + + // Load in the manifest file. + mf, err := newManifestFile(constants.KustomizeFileName, fsys) + if err != nil { + return err + } + + m, err := mf.read() + if err != nil { + return err + } + + // Add the config map to the manifest. + err = addConfigMap(m, config) + if err != nil { + return err + } + + // Write out the manifest with added configmap. + return mf.write(m) + }, + } + + cmd.Flags().StringSliceVar(&config.FileSources, "from-file", []string{}, "Key file can be specified using its file path, in which case file basename will be used as configmap key, or optionally with a key and file path, in which case the given key will be used. Specifying a directory will iterate each named file in the directory whose basename is a valid configmap key.") + cmd.Flags().StringArrayVar(&config.LiteralSources, "from-literal", []string{}, "Specify a key and literal value to insert in configmap (i.e. mykey=somevalue)") + cmd.Flags().StringVar(&config.EnvFileSource, "from-env-file", "", "Specify the path to a file to read lines of key=val pairs to create a configmap (i.e. a Docker .env file).") + + return cmd +} + +// addConfigMap updates a configmap within a manifest, using the data in config. +// Note: error may leave manifest in an undefined state. Suggest passing a copy +// of manifest. +func addConfigMap(m *manifest.Manifest, config dataConfig) error { + cm := getOrCreateConfigMap(m, config.Name) + + err := mergeData(&cm.DataSources, config) + if err != nil { + return err + } + + // Validate manifest's configmap by trying to create corev1.configmap. + _, _, err = configmapandsecret.MakeConfigmapAndGenerateName(*cm) + if err != nil { + return err + } + + return nil +} + +func getOrCreateConfigMap(m *manifest.Manifest, name string) *manifest.ConfigMapArgs { + for i, v := range m.ConfigMapGenerator { + if name == v.Name { + return &m.ConfigMapGenerator[i] + } + } + // config map not found, create new one and add it to the manifest. + cm := &manifest.ConfigMapArgs{Name: name} + m.ConfigMapGenerator = append(m.ConfigMapGenerator, *cm) + return &m.ConfigMapGenerator[len(m.ConfigMapGenerator)-1] +} + +func mergeData(src *manifest.DataSources, config dataConfig) error { + src.LiteralSources = append(src.LiteralSources, config.LiteralSources...) + src.FileSources = append(src.FileSources, config.FileSources...) + if src.EnvSource != "" && src.EnvSource != config.EnvFileSource { + return fmt.Errorf("updating existing env source '%s' not allowed.", src.EnvSource) + } + src.EnvSource = config.EnvFileSource + + return nil +} diff --git a/commands/configmap_test.go b/commands/configmap_test.go new file mode 100644 index 000000000..473c3982c --- /dev/null +++ b/commands/configmap_test.go @@ -0,0 +1,129 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package commands + +import ( + "testing" + + manifest "k8s.io/kubectl/pkg/apis/manifest/v1alpha1" + "k8s.io/kubectl/pkg/kustomize/util/fs" +) + +func TestNewAddConfigMapIsNotNil(t *testing.T) { + if newCmdAddConfigMap(nil, fs.MakeFakeFS()) == nil { + t.Fatal("newCmdAddConfigMap shouldn't be nil") + } +} + +func TestGetOrCreateConfigMap(t *testing.T) { + cmName := "test-config-name" + + manifest := &manifest.Manifest{ + NamePrefix: "test-name-prefix", + } + + if len(manifest.ConfigMapGenerator) != 0 { + t.Fatal("Initial manifest should not have any configmaps") + } + cm := getOrCreateConfigMap(manifest, cmName) + + if cm == nil { + t.Fatalf("ConfigMap should always be non-nil") + } + + if len(manifest.ConfigMapGenerator) != 1 { + t.Fatalf("Manifest should have newly created configmap") + } + + if &manifest.ConfigMapGenerator[len(manifest.ConfigMapGenerator)-1] != cm { + t.Fatalf("Pointer address for newly inserted configmap should be same") + } + + existingCM := getOrCreateConfigMap(manifest, cmName) + + if existingCM != cm { + t.Fatalf("should have returned an existing cm with name: %v", cmName) + } + + if len(manifest.ConfigMapGenerator) != 1 { + t.Fatalf("Should not insert configmap for an existing name: %v", cmName) + } +} + +func TestMergeData_LiteralSources(t *testing.T) { + ds := &manifest.DataSources{} + + err := mergeData(ds, dataConfig{LiteralSources: []string{"k1=v1"}}) + if err != nil { + t.Fatalf("Merge initial literal source should not return error") + } + + if len(ds.LiteralSources) != 1 { + t.Fatalf("Initial literal source should have been added") + } + + err = mergeData(ds, dataConfig{LiteralSources: []string{"k2=v2"}}) + if err != nil { + t.Fatalf("Merge second literal source should not return error") + } + + if len(ds.LiteralSources) != 2 { + t.Fatalf("Second literal source should have been added") + } +} + +func TestMergeData_FileSources(t *testing.T) { + ds := &manifest.DataSources{} + + err := mergeData(ds, dataConfig{FileSources: []string{"file1"}}) + if err != nil { + t.Fatalf("Merge initial file source should not return error") + } + + if len(ds.FileSources) != 1 { + t.Fatalf("Initial file source should have been added") + } + + err = mergeData(ds, dataConfig{FileSources: []string{"file2"}}) + if err != nil { + t.Fatalf("Merge second file source should not return error") + } + + if len(ds.FileSources) != 2 { + t.Fatalf("Second file source should have been added") + } +} + +func TestMergeData_EnvSource(t *testing.T) { + envFileName := "env1" + envFileName2 := "env2" + ds := &manifest.DataSources{} + + err := mergeData(ds, dataConfig{EnvFileSource: envFileName}) + if err != nil { + t.Fatalf("Merge initial env source should not return error") + } + + if ds.EnvSource != envFileName { + t.Fatalf("Initial env source filename should have been added") + } + + err = mergeData(ds, dataConfig{EnvFileSource: envFileName2}) + if err == nil { + t.Fatalf("Updating env source should return an error") + } +} diff --git a/commands/data_config.go b/commands/data_config.go new file mode 100644 index 000000000..9ee241b84 --- /dev/null +++ b/commands/data_config.go @@ -0,0 +1,50 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package commands + +import ( + "fmt" +) + +// dataConfig encapsulates the options for add configmap/Secret commands. +type dataConfig struct { + // Name of configMap/Secret (required) + Name string + // FileSources to derive the configMap/Secret from (optional) + FileSources []string + // LiteralSources to derive the configMap/Secret from (optional) + LiteralSources []string + // EnvFileSource to derive the configMap/Secret from (optional) + // TODO: Rationalize this name with Generic.EnvSource + EnvFileSource string +} + +// Validate validates required fields are set to support structured generation. +func (a *dataConfig) Validate(args []string) error { + if len(args) != 1 { + return fmt.Errorf("name must be specified once") + } + a.Name = args[0] + if len(a.EnvFileSource) == 0 && len(a.FileSources) == 0 && len(a.LiteralSources) == 0 { + return fmt.Errorf("at least from-env-file, or from-file or from-literal must be set") + } + if len(a.EnvFileSource) > 0 && (len(a.FileSources) > 0 || len(a.LiteralSources) > 0) { + return fmt.Errorf("from-env-file cannot be combined with from-file or from-literal") + } + // TODO: Should we check if the path exists? if it's valid, if it's within the same (sub-)directory? + return nil +} diff --git a/commands/data_config_test.go b/commands/data_config_test.go new file mode 100644 index 000000000..29154f4b0 --- /dev/null +++ b/commands/data_config_test.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 commands + +import ( + "testing" +) + +func TestDataConfigValidation_NoName(t *testing.T) { + config := dataConfig{} + + if config.Validate([]string{}) == nil { + t.Fatal("Validation should fail if no name is specified") + } +} + +func TestDataConfigValidation_MoreThanOneName(t *testing.T) { + config := dataConfig{} + + if config.Validate([]string{"name", "othername"}) == nil { + t.Fatal("Validation should fail if more than one name is specified") + } +} + +func TestDataConfigValidation_Flags(t *testing.T) { + tests := []struct { + name string + config dataConfig + shouldFail bool + }{ + { + name: "env-file-source and literal are both set", + config: dataConfig{ + LiteralSources: []string{"one", "two"}, + EnvFileSource: "three", + }, + shouldFail: true, + }, + { + name: "env-file-source and from-file are both set", + config: dataConfig{ + FileSources: []string{"one", "two"}, + EnvFileSource: "three", + }, + shouldFail: true, + }, + { + name: "we don't have any option set", + config: dataConfig{}, + shouldFail: true, + }, + { + name: "we have from-file and literal ", + config: dataConfig{ + LiteralSources: []string{"one", "two"}, + FileSources: []string{"three", "four"}, + }, + shouldFail: false, + }, + } + + for _, test := range tests { + if test.config.Validate([]string{"name"}) == nil && test.shouldFail { + t.Fatalf("Validation should fail if %s", test.name) + } else if test.config.Validate([]string{"name"}) != nil && !test.shouldFail { + t.Fatalf("Validation should succeed if %s", test.name) + } + } +} diff --git a/commands/diff.go b/commands/diff.go new file mode 100644 index 000000000..2a14ce524 --- /dev/null +++ b/commands/diff.go @@ -0,0 +1,124 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package commands + +import ( + "errors" + "io" + "path/filepath" + + "github.com/spf13/cobra" + + "k8s.io/kubectl/pkg/kustomize/app" + "k8s.io/kubectl/pkg/kustomize/util" + "k8s.io/kubectl/pkg/kustomize/util/fs" + "k8s.io/kubectl/pkg/loader" + "k8s.io/utils/exec" +) + +type diffOptions struct { + manifestPath string +} + +// newCmdDiff makes the diff command. +func newCmdDiff(out, errOut io.Writer, fs fs.FileSystem) *cobra.Command { + var o diffOptions + + cmd := &cobra.Command{ + Use: "diff", + Short: "diff between transformed resources and untransformed resources", + Long: "diff between transformed resources and untransformed resources and the subpackages are all transformed.", + Example: `diff -f .`, + RunE: func(cmd *cobra.Command, args []string) error { + err := o.Validate(cmd, args) + if err != nil { + return err + } + err = o.Complete(cmd, args) + if err != nil { + return err + } + return o.RunDiff(out, errOut, fs) + }, + } + + cmd.Flags().StringVarP(&o.manifestPath, "filename", "f", "", "Pass in a kustomize.yaml file or a directory that contains the file.") + cmd.MarkFlagRequired("filename") + return cmd +} + +// Validate validates diff command. +func (o *diffOptions) Validate(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + return errors.New("The diff command takes no arguments.") + } + return nil +} + +// Complete completes diff command. +func (o *diffOptions) Complete(cmd *cobra.Command, args []string) error { + return nil +} + +// RunDiff gets the differences between Application.Resources() and Application.RawResources(). +func (o *diffOptions) RunDiff(out, errOut io.Writer, fs fs.FileSystem) error { + printer := util.Printer{} + diff := util.DiffProgram{ + Exec: exec.New(), + Stdout: out, + Stderr: errOut, + } + + l := loader.Init([]loader.SchemeLoader{loader.NewFileLoader(fs)}) + + absPath, err := filepath.Abs(o.manifestPath) + if err != nil { + return err + } + + rootLoader, err := l.New(absPath) + if err != nil { + return err + } + + application, err := app.New(rootLoader) + if err != nil { + return err + } + resources, err := application.Resources() + if err != nil { + return err + } + rawResources, err := application.RawResources() + if err != nil { + return err + } + + transformedDir, err := util.WriteToDir(resources, "transformed", printer) + if err != nil { + return err + } + defer transformedDir.Delete() + + noopDir, err := util.WriteToDir(rawResources, "noop", printer) + if err != nil { + return err + } + defer noopDir.Delete() + + return diff.Run(noopDir.Name, transformedDir.Name) +} diff --git a/commands/diff_test.go b/commands/diff_test.go new file mode 100644 index 000000000..7732b12b5 --- /dev/null +++ b/commands/diff_test.go @@ -0,0 +1,124 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package commands + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "regexp" + "strings" + "testing" + + "github.com/ghodss/yaml" + + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/kubectl/pkg/kustomize/util/fs" +) + +type DiffTestCase struct { + Description string `yaml:"description"` + Args []string `yaml:"args"` + Filename string `yaml:"filename"` + // path to the file that contains the expected output + ExpectedDiff string `yaml:"expectedDiff"` + ExpectedError string `yaml:"expectedError"` +} + +func TestDiff(t *testing.T) { + const updateEnvVar = "UPDATE_KUSTOMIZE_EXPECTED_DATA" + updateKustomizeExpected := os.Getenv(updateEnvVar) == "true" + + noopDir, _ := regexp.Compile(`/tmp/noop-[0-9]*/`) + transformedDir, _ := regexp.Compile(`/tmp/transformed-[0-9]*/`) + timestamp, _ := regexp.Compile(`[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1]) (2[0-3]|[01][0-9]):[0-5][0-9]:[0-5][0-9].[0-9]* [+-]{1}[0-9]{4}`) + + fs := fs.MakeRealFS() + + testcases := sets.NewString() + filepath.Walk("testdata", func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if path == "testdata" { + return nil + } + name := filepath.Base(path) + if info.IsDir() { + if strings.HasPrefix(name, "testcase-") { + testcases.Insert(strings.TrimPrefix(name, "testcase-")) + } + return filepath.SkipDir + } + return nil + }) + // sanity check that we found the right folder + if !testcases.Has("simple") { + t.Fatalf("Error locating testcases") + } + + for _, testcaseName := range testcases.List() { + t.Run(testcaseName, func(t *testing.T) { + name := testcaseName + testcase := DiffTestCase{} + testcaseDir := filepath.Join("testdata", "testcase-"+name) + testcaseData, err := ioutil.ReadFile(filepath.Join(testcaseDir, "test.yaml")) + if err != nil { + t.Fatalf("%s: %v", name, err) + } + if err := yaml.Unmarshal(testcaseData, &testcase); err != nil { + t.Fatalf("%s: %v", name, err) + } + + diffOps := &diffOptions{ + manifestPath: testcase.Filename, + } + buf := bytes.NewBuffer([]byte{}) + err = diffOps.RunDiff(buf, os.Stderr, fs) + switch { + case err != nil && len(testcase.ExpectedError) == 0: + t.Errorf("unexpected error: %v", err) + case err != nil && len(testcase.ExpectedError) != 0: + if !strings.Contains(err.Error(), testcase.ExpectedError) { + t.Errorf("expected error to contain %q but got: %v", testcase.ExpectedError, err) + } + return + case err == nil && len(testcase.ExpectedError) != 0: + t.Errorf("unexpected no error") + } + + actualString := string(buf.Bytes()) + actualString = noopDir.ReplaceAllString(actualString, "/tmp/noop/") + actualString = transformedDir.ReplaceAllString(actualString, "/tmp/transformed/") + actualString = timestamp.ReplaceAllString(actualString, "YYYY-MM-DD HH:MM:SS") + actualBytes := []byte(actualString) + if !updateKustomizeExpected { + expectedBytes, err := ioutil.ReadFile(testcase.ExpectedDiff) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if !reflect.DeepEqual(actualBytes, expectedBytes) { + t.Errorf("%s\ndoesn't equal expected:\n%s\n", actualBytes, expectedBytes) + } + } else { + ioutil.WriteFile(testcase.ExpectedDiff, actualBytes, 0644) + } + + }) + } +} diff --git a/commands/init.go b/commands/init.go new file mode 100644 index 000000000..860dbc137 --- /dev/null +++ b/commands/init.go @@ -0,0 +1,100 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package commands + +import ( + "fmt" + "io" + + "errors" + + "github.com/spf13/cobra" + "k8s.io/kubectl/pkg/kustomize/constants" + "k8s.io/kubectl/pkg/kustomize/util/fs" +) + +const manifestTemplate = `apiVersion: manifest.k8s.io/v1alpha1 +kind: Manifest +metadata: + name: helloworld +description: helloworld does useful stuff. +namePrefix: some-prefix +# Labels to add to all objects and selectors. +# These labels would also be used to form the selector for apply --prune +# Named differently than “labels” to avoid confusion with metadata for this object +objectLabels: + app: helloworld +objectAnnotations: + note: This is an example annotation +resources: [] +#- service.yaml +#- ../some-dir/ +# There could also be configmaps in Base, which would make these overlays +configMapGenerator: [] +# There could be secrets in Base, if just using a fork/rebase workflow +secretGenerator: [] +` + +type initOptions struct { +} + +// NewCmdInit makes the init command. +func newCmdInit(out, errOut io.Writer, fs fs.FileSystem) *cobra.Command { + var o initOptions + + cmd := &cobra.Command{ + Use: "init", + Short: "Creates a file called \"" + constants.KustomizeFileName + "\" in the current directory", + Long: "Creates a file called \"" + + constants.KustomizeFileName + "\" in the current directory with example values.", + Example: `init`, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + err := o.Validate(cmd, args) + if err != nil { + return err + } + err = o.Complete(cmd, args) + if err != nil { + return err + } + return o.RunInit(out, errOut, fs) + }, + } + return cmd +} + +// Validate validates init command. +func (o *initOptions) Validate(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + return errors.New("The init command takes no arguments.") + } + return nil +} + +// Complete completes init command. +func (o *initOptions) Complete(cmd *cobra.Command, args []string) error { + return nil +} + +// RunInit writes a manifest file. +func (o *initOptions) RunInit(out, errOut io.Writer, fs fs.FileSystem) error { + if _, err := fs.Stat(constants.KustomizeFileName); err == nil { + return fmt.Errorf("%q already exists", constants.KustomizeFileName) + } + return fs.WriteFile(constants.KustomizeFileName, []byte(manifestTemplate)) +} diff --git a/commands/init_test.go b/commands/init_test.go new file mode 100644 index 000000000..c448f91aa --- /dev/null +++ b/commands/init_test.go @@ -0,0 +1,62 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package commands + +import ( + "bytes" + "os" + "testing" + + "k8s.io/kubectl/pkg/kustomize/constants" + "k8s.io/kubectl/pkg/kustomize/util/fs" +) + +func TestInitHappyPath(t *testing.T) { + buf := bytes.NewBuffer([]byte{}) + fakeFS := fs.MakeFakeFS() + cmd := newCmdInit(buf, os.Stderr, fakeFS) + err := cmd.Execute() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + f, err := fakeFS.Open(constants.KustomizeFileName) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + file := f.(*fs.FakeFile) + if !file.ContentMatches([]byte(manifestTemplate)) { + t.Fatalf("actual: %v doesn't match expected: %v", + string(file.GetContent()), manifestTemplate) + } +} + +func TestInitFileAlreadyExist(t *testing.T) { + content := "hey there" + fakeFS := fs.MakeFakeFS() + fakeFS.WriteFile(constants.KustomizeFileName, []byte(content)) + + buf := bytes.NewBuffer([]byte{}) + cmd := newCmdInit(buf, os.Stderr, fakeFS) + err := cmd.Execute() + if err == nil { + t.Fatalf("expected error") + } + if err.Error() != `"`+constants.KustomizeFileName+`" already exists` { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/commands/set_name_prefix.go b/commands/set_name_prefix.go new file mode 100644 index 000000000..c1c7b97d1 --- /dev/null +++ b/commands/set_name_prefix.go @@ -0,0 +1,90 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package commands + +import ( + "errors" + "io" + + "github.com/spf13/cobra" + + "k8s.io/kubectl/pkg/kustomize/constants" + "k8s.io/kubectl/pkg/kustomize/util/fs" +) + +type setNamePrefixOptions struct { + prefix string +} + +// newCmdSetNamePrefix sets the value of the namePrefix field in the manifest. +func newCmdSetNamePrefix(out, errOut io.Writer, fsys fs.FileSystem) *cobra.Command { + var o setNamePrefixOptions + + cmd := &cobra.Command{ + Use: "nameprefix", + Short: "Sets the value of the namePrefix field in the manifest.", + Long: "Sets the value of the namePrefix field in the manifest.", + // + Example: ` +The command + set nameprefix acme- +will add the field "namePrefix: acme-" to the manifest file if it doesn't exist, +and overwrite the value with "acme-" if the field does exist. +`, + RunE: func(cmd *cobra.Command, args []string) error { + err := o.Validate(args) + if err != nil { + return err + } + err = o.Complete(cmd, args) + if err != nil { + return err + } + return o.RunSetNamePrefix(out, errOut, fsys) + }, + } + return cmd +} + +// Validate validates setNamePrefix command. +func (o *setNamePrefixOptions) Validate(args []string) error { + if len(args) != 1 { + return errors.New("must specify exactly one prefix value") + } + // TODO: add further validation on the value. + o.prefix = args[0] + return nil +} + +// Complete completes setNamePrefix command. +func (o *setNamePrefixOptions) Complete(cmd *cobra.Command, args []string) error { + return nil +} + +// RunSetNamePrefix runs setNamePrefix command (does real work). +func (o *setNamePrefixOptions) RunSetNamePrefix(out, errOut io.Writer, fsys fs.FileSystem) error { + mf, err := newManifestFile(constants.KustomizeFileName, fsys) + if err != nil { + return err + } + m, err := mf.read() + if err != nil { + return err + } + m.NamePrefix = o.prefix + return mf.write(m) +} diff --git a/commands/set_name_prefix_test.go b/commands/set_name_prefix_test.go new file mode 100644 index 000000000..5361ad692 --- /dev/null +++ b/commands/set_name_prefix_test.go @@ -0,0 +1,66 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package commands + +import ( + "bytes" + "os" + "testing" + + "strings" + + "k8s.io/kubectl/pkg/kustomize/constants" + "k8s.io/kubectl/pkg/kustomize/util/fs" +) + +const ( + goodPrefixValue = "acme-" +) + +func TestSetNamePrefixHappyPath(t *testing.T) { + buf := bytes.NewBuffer([]byte{}) + fakeFS := fs.MakeFakeFS() + fakeFS.WriteFile(constants.KustomizeFileName, []byte(manifestTemplate)) + + cmd := newCmdSetNamePrefix(buf, os.Stderr, fakeFS) + args := []string{goodPrefixValue} + err := cmd.RunE(cmd, args) + if err != nil { + t.Errorf("unexpected cmd error: %v", err) + } + content, err := fakeFS.ReadFile(constants.KustomizeFileName) + if err != nil { + t.Errorf("unexpected read error: %v", err) + } + if !strings.Contains(string(content), goodPrefixValue) { + t.Errorf("expected prefix value in manifest") + } +} + +func TestSetNamePrefixNoArgs(t *testing.T) { + buf := bytes.NewBuffer([]byte{}) + fakeFS := fs.MakeFakeFS() + + cmd := newCmdSetNamePrefix(buf, os.Stderr, fakeFS) + err := cmd.Execute() + if err == nil { + t.Errorf("expected error: %v", err) + } + if err.Error() != "must specify exactly one prefix value" { + t.Errorf("incorrect error: %v", err.Error()) + } +} diff --git a/commands/testdata/testcase-base-only/expected.diff b/commands/testdata/testcase-base-only/expected.diff new file mode 100644 index 000000000..de911f805 --- /dev/null +++ b/commands/testdata/testcase-base-only/expected.diff @@ -0,0 +1,58 @@ +diff -u -N /tmp/noop/apps_v1beta2_Deployment_nginx.yaml /tmp/transformed/apps_v1beta2_Deployment_nginx.yaml +--- /tmp/noop/apps_v1beta2_Deployment_nginx.yaml YYYY-MM-DD HH:MM:SS ++++ /tmp/transformed/apps_v1beta2_Deployment_nginx.yaml YYYY-MM-DD HH:MM:SS +@@ -1,14 +1,27 @@ + apiVersion: apps/v1beta2 + kind: Deployment + metadata: ++ annotations: ++ note: This is a test annotation + labels: +- app: nginx +- name: nginx ++ app: mynginx ++ org: example.com ++ team: foo ++ name: team-foo-nginx + spec: ++ selector: ++ matchLabels: ++ app: mynginx ++ org: example.com ++ team: foo + template: + metadata: ++ annotations: ++ note: This is a test annotation + labels: +- app: nginx ++ app: mynginx ++ org: example.com ++ team: foo + spec: + containers: + - image: nginx +diff -u -N /tmp/noop/v1_Service_nginx.yaml /tmp/transformed/v1_Service_nginx.yaml +--- /tmp/noop/v1_Service_nginx.yaml YYYY-MM-DD HH:MM:SS ++++ /tmp/transformed/v1_Service_nginx.yaml YYYY-MM-DD HH:MM:SS +@@ -1,11 +1,17 @@ + apiVersion: v1 + kind: Service + metadata: ++ annotations: ++ note: This is a test annotation + labels: +- app: nginx +- name: nginx ++ app: mynginx ++ org: example.com ++ team: foo ++ name: team-foo-nginx + spec: + ports: + - port: 80 + selector: +- app: nginx ++ app: mynginx ++ org: example.com ++ team: foo diff --git a/commands/testdata/testcase-base-only/expected.yaml b/commands/testdata/testcase-base-only/expected.yaml new file mode 100644 index 000000000..886d804b8 --- /dev/null +++ b/commands/testdata/testcase-base-only/expected.yaml @@ -0,0 +1,46 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + note: This is a test annotation + labels: + app: mynginx + org: example.com + team: foo + name: team-foo-nginx +spec: + ports: + - port: 80 + selector: + app: mynginx + org: example.com + team: foo +--- +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + annotations: + note: This is a test annotation + labels: + app: mynginx + org: example.com + team: foo + name: team-foo-nginx +spec: + selector: + matchLabels: + app: mynginx + org: example.com + team: foo + template: + metadata: + annotations: + note: This is a test annotation + labels: + app: mynginx + org: example.com + team: foo + spec: + containers: + - image: nginx + name: nginx diff --git a/commands/testdata/testcase-base-only/in/deployment.yaml b/commands/testdata/testcase-base-only/in/deployment.yaml new file mode 100644 index 000000000..722bf87ce --- /dev/null +++ b/commands/testdata/testcase-base-only/in/deployment.yaml @@ -0,0 +1,15 @@ +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + name: nginx + labels: + app: nginx +spec: + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx diff --git a/commands/testdata/testcase-base-only/in/kustomize.yaml b/commands/testdata/testcase-base-only/in/kustomize.yaml new file mode 100644 index 000000000..bfd9e811e --- /dev/null +++ b/commands/testdata/testcase-base-only/in/kustomize.yaml @@ -0,0 +1,14 @@ +apiVersion: manifest.k8s.io/v1alpha1 +kind: Manifest +metadata: + name: nginx-app +namePrefix: team-foo- +objectLabels: + app: mynginx + org: example.com + team: foo +objectAnnotations: + note: This is a test annotation +resources: + - deployment.yaml + - service.yaml diff --git a/commands/testdata/testcase-base-only/in/service.yaml b/commands/testdata/testcase-base-only/in/service.yaml new file mode 100644 index 000000000..f6dd86909 --- /dev/null +++ b/commands/testdata/testcase-base-only/in/service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: nginx + labels: + app: nginx +spec: + ports: + - port: 80 + selector: + app: nginx diff --git a/commands/testdata/testcase-base-only/test.yaml b/commands/testdata/testcase-base-only/test.yaml new file mode 100644 index 000000000..61d6d0a07 --- /dev/null +++ b/commands/testdata/testcase-base-only/test.yaml @@ -0,0 +1,5 @@ +description: base only +args: [] +filename: testdata/testcase-base-only/in +expectedStdout: testdata/testcase-base-only/expected.yaml +expectedDiff: testdata/testcase-base-only/expected.diff diff --git a/commands/testdata/testcase-multiple-patches-conflict/in/overlay/deployment-patch1.yaml b/commands/testdata/testcase-multiple-patches-conflict/in/overlay/deployment-patch1.yaml new file mode 100644 index 000000000..c92c5090e --- /dev/null +++ b/commands/testdata/testcase-multiple-patches-conflict/in/overlay/deployment-patch1.yaml @@ -0,0 +1,20 @@ +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + name: nginx +spec: + template: + spec: + containers: + - name: nginx + env: + - name: ENABLE_FEATURE_FOO + value: TRUE + volumes: + - name: nginx-persistent-storage + emptyDir: null + gcePersistentDisk: + pdName: nginx-persistent-storage + - configMap: + name: configmap-in-overlay + name: configmap-in-overlay diff --git a/commands/testdata/testcase-multiple-patches-conflict/in/overlay/deployment-patch2.yaml b/commands/testdata/testcase-multiple-patches-conflict/in/overlay/deployment-patch2.yaml new file mode 100644 index 000000000..ab0bffbdf --- /dev/null +++ b/commands/testdata/testcase-multiple-patches-conflict/in/overlay/deployment-patch2.yaml @@ -0,0 +1,12 @@ +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + name: nginx +spec: + template: + spec: + containers: + - name: nginx + env: + - name: ENABLE_FEATURE_FOO + value: FALSE diff --git a/commands/testdata/testcase-multiple-patches-conflict/in/overlay/kustomize.yaml b/commands/testdata/testcase-multiple-patches-conflict/in/overlay/kustomize.yaml new file mode 100644 index 000000000..ecb0ef4d4 --- /dev/null +++ b/commands/testdata/testcase-multiple-patches-conflict/in/overlay/kustomize.yaml @@ -0,0 +1,16 @@ +apiVersion: manifest.k8s.io/v1alpha1 +kind: Manifest +metadata: + name: nginx-app +namePrefix: staging- +objectLabels: + env: staging +patches: + - deployment-patch2.yaml + - deployment-patch1.yaml +bases: + - ../package/ +configMapGenerator: + - name: configmap-in-overlay + literals: + - hello=world diff --git a/commands/testdata/testcase-multiple-patches-conflict/in/package/deployment.yaml b/commands/testdata/testcase-multiple-patches-conflict/in/package/deployment.yaml new file mode 100644 index 000000000..9f7d3cbb6 --- /dev/null +++ b/commands/testdata/testcase-multiple-patches-conflict/in/package/deployment.yaml @@ -0,0 +1,24 @@ +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + name: nginx + labels: + app: nginx +spec: + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx + volumeMounts: + - name: nginx-persistent-storage + mountPath: /tmp/ps + volumes: + - name: nginx-persistent-storage + emptyDir: {} + - configMap: + name: configmap-in-base + name: configmap-in-base diff --git a/commands/testdata/testcase-multiple-patches-conflict/in/package/kustomize.yaml b/commands/testdata/testcase-multiple-patches-conflict/in/package/kustomize.yaml new file mode 100644 index 000000000..e5e3ca206 --- /dev/null +++ b/commands/testdata/testcase-multiple-patches-conflict/in/package/kustomize.yaml @@ -0,0 +1,18 @@ +apiVersion: manifest.k8s.io/v1alpha1 +kind: Manifest +metadata: + name: nginx-app +namePrefix: team-foo- +objectLabels: + app: mynginx + org: example.com + team: foo +objectAnnotations: + note: This is a test annotation +resources: + - deployment.yaml + - service.yaml +configMapGenerator: + - name: configmap-in-base + literals: + - foo=bar diff --git a/commands/testdata/testcase-multiple-patches-conflict/in/package/service.yaml b/commands/testdata/testcase-multiple-patches-conflict/in/package/service.yaml new file mode 100644 index 000000000..f6dd86909 --- /dev/null +++ b/commands/testdata/testcase-multiple-patches-conflict/in/package/service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: nginx + labels: + app: nginx +spec: + ports: + - port: 80 + selector: + app: nginx diff --git a/commands/testdata/testcase-multiple-patches-conflict/test.yaml b/commands/testdata/testcase-multiple-patches-conflict/test.yaml new file mode 100644 index 000000000..a92e7f92d --- /dev/null +++ b/commands/testdata/testcase-multiple-patches-conflict/test.yaml @@ -0,0 +1,4 @@ +description: conflict between multiple patches +args: [] +filename: testdata/testcase-multiple-patches-conflict/in/overlay/ +expectedError: conflict diff --git a/commands/testdata/testcase-multiple-patches-noconflict/expected.diff b/commands/testdata/testcase-multiple-patches-noconflict/expected.diff new file mode 100644 index 000000000..c957e263b --- /dev/null +++ b/commands/testdata/testcase-multiple-patches-noconflict/expected.diff @@ -0,0 +1,99 @@ +diff -u -N /tmp/noop/apps_v1beta2_Deployment_nginx.yaml /tmp/transformed/apps_v1beta2_Deployment_nginx.yaml +--- /tmp/noop/apps_v1beta2_Deployment_nginx.yaml YYYY-MM-DD HH:MM:SS ++++ /tmp/transformed/apps_v1beta2_Deployment_nginx.yaml YYYY-MM-DD HH:MM:SS +@@ -5,13 +5,15 @@ + note: This is a test annotation + labels: + app: mynginx ++ env: staging + org: example.com + team: foo +- name: team-foo-nginx ++ name: staging-team-foo-nginx + spec: + selector: + matchLabels: + app: mynginx ++ env: staging + org: example.com + team: foo + template: +@@ -20,18 +22,30 @@ + note: This is a test annotation + labels: + app: mynginx ++ env: staging + org: example.com + team: foo + spec: + containers: +- - image: nginx ++ - env: ++ - name: ANOTHERENV ++ value: FOO ++ - name: ENVKEY ++ value: ENVVALUE ++ image: nginx:latest + name: nginx + volumeMounts: + - mountPath: /tmp/ps + name: nginx-persistent-storage ++ - image: sidecar ++ name: sidecar + volumes: +- - emptyDir: {} ++ - gcePersistentDisk: ++ pdName: nginx-persistent-storage + name: nginx-persistent-storage + - configMap: +- name: team-foo-configmap-in-base-bbdmdh7m8t ++ name: staging-configmap-in-overlay-k7cbc75tg8 ++ name: configmap-in-overlay ++ - configMap: ++ name: staging-team-foo-configmap-in-base-g7k6gt2889 + name: configmap-in-base +diff -u -N /tmp/noop/v1_ConfigMap_configmap-in-base.yaml /tmp/transformed/v1_ConfigMap_configmap-in-base.yaml +--- /tmp/noop/v1_ConfigMap_configmap-in-base.yaml YYYY-MM-DD HH:MM:SS ++++ /tmp/transformed/v1_ConfigMap_configmap-in-base.yaml YYYY-MM-DD HH:MM:SS +@@ -8,6 +8,7 @@ + creationTimestamp: null + labels: + app: mynginx ++ env: staging + org: example.com + team: foo +- name: team-foo-configmap-in-base-bbdmdh7m8t ++ name: staging-team-foo-configmap-in-base-g7k6gt2889 +diff -u -N /tmp/noop/v1_ConfigMap_configmap-in-overlay.yaml /tmp/transformed/v1_ConfigMap_configmap-in-overlay.yaml +--- /tmp/noop/v1_ConfigMap_configmap-in-overlay.yaml YYYY-MM-DD HH:MM:SS ++++ /tmp/transformed/v1_ConfigMap_configmap-in-overlay.yaml YYYY-MM-DD HH:MM:SS +@@ -0,0 +1,9 @@ ++apiVersion: v1 ++data: ++ hello: world ++kind: ConfigMap ++metadata: ++ creationTimestamp: null ++ labels: ++ env: staging ++ name: staging-configmap-in-overlay-k7cbc75tg8 +diff -u -N /tmp/noop/v1_Service_nginx.yaml /tmp/transformed/v1_Service_nginx.yaml +--- /tmp/noop/v1_Service_nginx.yaml YYYY-MM-DD HH:MM:SS ++++ /tmp/transformed/v1_Service_nginx.yaml YYYY-MM-DD HH:MM:SS +@@ -5,13 +5,15 @@ + note: This is a test annotation + labels: + app: mynginx ++ env: staging + org: example.com + team: foo +- name: team-foo-nginx ++ name: staging-team-foo-nginx + spec: + ports: + - port: 80 + selector: + app: mynginx ++ env: staging + org: example.com + team: foo diff --git a/commands/testdata/testcase-multiple-patches-noconflict/expected.yaml b/commands/testdata/testcase-multiple-patches-noconflict/expected.yaml new file mode 100644 index 000000000..743f93bde --- /dev/null +++ b/commands/testdata/testcase-multiple-patches-noconflict/expected.yaml @@ -0,0 +1,96 @@ +apiVersion: v1 +data: + foo: bar +kind: ConfigMap +metadata: + annotations: + note: This is a test annotation + creationTimestamp: null + labels: + app: mynginx + env: staging + org: example.com + team: foo + name: staging-team-foo-configmap-in-base-g7k6gt2889 +--- +apiVersion: v1 +data: + hello: world +kind: ConfigMap +metadata: + creationTimestamp: null + labels: + env: staging + name: staging-configmap-in-overlay-k7cbc75tg8 +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + note: This is a test annotation + labels: + app: mynginx + env: staging + org: example.com + team: foo + name: staging-team-foo-nginx +spec: + ports: + - port: 80 + selector: + app: mynginx + env: staging + org: example.com + team: foo +--- +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + annotations: + note: This is a test annotation + labels: + app: mynginx + env: staging + org: example.com + team: foo + name: staging-team-foo-nginx +spec: + selector: + matchLabels: + app: mynginx + env: staging + org: example.com + team: foo + template: + metadata: + annotations: + note: This is a test annotation + labels: + app: mynginx + env: staging + org: example.com + team: foo + spec: + containers: + - env: + - name: ANOTHERENV + value: FOO + - name: ENVKEY + value: ENVVALUE + image: nginx:latest + name: nginx + volumeMounts: + - mountPath: /tmp/ps + name: nginx-persistent-storage + - image: sidecar + name: sidecar + volumes: + - gcePersistentDisk: + pdName: nginx-persistent-storage + name: nginx-persistent-storage + - configMap: + name: staging-configmap-in-overlay-k7cbc75tg8 + name: configmap-in-overlay + - configMap: + name: staging-team-foo-configmap-in-base-g7k6gt2889 + name: configmap-in-base diff --git a/commands/testdata/testcase-multiple-patches-noconflict/in/overlay/deployment-patch1.yaml b/commands/testdata/testcase-multiple-patches-noconflict/in/overlay/deployment-patch1.yaml new file mode 100644 index 000000000..444e05b22 --- /dev/null +++ b/commands/testdata/testcase-multiple-patches-noconflict/in/overlay/deployment-patch1.yaml @@ -0,0 +1,21 @@ +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + name: nginx +spec: + template: + spec: + containers: + - name: nginx + image: nginx:latest + env: + - name: ENVKEY + value: ENVVALUE + volumes: + - name: nginx-persistent-storage + emptyDir: null + gcePersistentDisk: + pdName: nginx-persistent-storage + - configMap: + name: configmap-in-overlay + name: configmap-in-overlay diff --git a/commands/testdata/testcase-multiple-patches-noconflict/in/overlay/deployment-patch2.yaml b/commands/testdata/testcase-multiple-patches-noconflict/in/overlay/deployment-patch2.yaml new file mode 100644 index 000000000..f4006d1c7 --- /dev/null +++ b/commands/testdata/testcase-multiple-patches-noconflict/in/overlay/deployment-patch2.yaml @@ -0,0 +1,16 @@ +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + name: nginx +spec: + template: + spec: + containers: + - name: nginx + env: + - name: ANOTHERENV + value: FOO + - name: sidecar + image: sidecar + volumes: + - name: nginx-persistent-storage diff --git a/commands/testdata/testcase-multiple-patches-noconflict/in/overlay/kustomize.yaml b/commands/testdata/testcase-multiple-patches-noconflict/in/overlay/kustomize.yaml new file mode 100644 index 000000000..62e51a805 --- /dev/null +++ b/commands/testdata/testcase-multiple-patches-noconflict/in/overlay/kustomize.yaml @@ -0,0 +1,16 @@ +apiVersion: manifest.k8s.io/v1alpha1 +kind: Manifest +metadata: + name: nginx-app +namePrefix: staging- +objectLabels: + env: staging +patches: + - deployment-patch1.yaml + - deployment-patch2.yaml +bases: + - ../package/ +configMapGenerator: + - name: configmap-in-overlay + literals: + - hello=world diff --git a/commands/testdata/testcase-multiple-patches-noconflict/in/package/deployment.yaml b/commands/testdata/testcase-multiple-patches-noconflict/in/package/deployment.yaml new file mode 100644 index 000000000..9f7d3cbb6 --- /dev/null +++ b/commands/testdata/testcase-multiple-patches-noconflict/in/package/deployment.yaml @@ -0,0 +1,24 @@ +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + name: nginx + labels: + app: nginx +spec: + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx + volumeMounts: + - name: nginx-persistent-storage + mountPath: /tmp/ps + volumes: + - name: nginx-persistent-storage + emptyDir: {} + - configMap: + name: configmap-in-base + name: configmap-in-base diff --git a/commands/testdata/testcase-multiple-patches-noconflict/in/package/kustomize.yaml b/commands/testdata/testcase-multiple-patches-noconflict/in/package/kustomize.yaml new file mode 100644 index 000000000..e5e3ca206 --- /dev/null +++ b/commands/testdata/testcase-multiple-patches-noconflict/in/package/kustomize.yaml @@ -0,0 +1,18 @@ +apiVersion: manifest.k8s.io/v1alpha1 +kind: Manifest +metadata: + name: nginx-app +namePrefix: team-foo- +objectLabels: + app: mynginx + org: example.com + team: foo +objectAnnotations: + note: This is a test annotation +resources: + - deployment.yaml + - service.yaml +configMapGenerator: + - name: configmap-in-base + literals: + - foo=bar diff --git a/commands/testdata/testcase-multiple-patches-noconflict/in/package/service.yaml b/commands/testdata/testcase-multiple-patches-noconflict/in/package/service.yaml new file mode 100644 index 000000000..f6dd86909 --- /dev/null +++ b/commands/testdata/testcase-multiple-patches-noconflict/in/package/service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: nginx + labels: + app: nginx +spec: + ports: + - port: 80 + selector: + app: nginx diff --git a/commands/testdata/testcase-multiple-patches-noconflict/test.yaml b/commands/testdata/testcase-multiple-patches-noconflict/test.yaml new file mode 100644 index 000000000..0b9aaf1b5 --- /dev/null +++ b/commands/testdata/testcase-multiple-patches-noconflict/test.yaml @@ -0,0 +1,5 @@ +description: multiple patches no conflict +args: [] +filename: testdata/testcase-multiple-patches-noconflict/in/overlay/ +expectedStdout: testdata/testcase-multiple-patches-noconflict/expected.yaml +expectedDiff: testdata/testcase-multiple-patches-noconflict/expected.diff diff --git a/commands/testdata/testcase-simple/expected.diff b/commands/testdata/testcase-simple/expected.diff new file mode 100644 index 000000000..e3dc73552 --- /dev/null +++ b/commands/testdata/testcase-simple/expected.diff @@ -0,0 +1,154 @@ +diff -u -N /tmp/noop/extensions_v1beta1_Deployment_mungebot.yaml /tmp/transformed/extensions_v1beta1_Deployment_mungebot.yaml +--- /tmp/noop/extensions_v1beta1_Deployment_mungebot.yaml YYYY-MM-DD HH:MM:SS ++++ /tmp/transformed/extensions_v1beta1_Deployment_mungebot.yaml YYYY-MM-DD HH:MM:SS +@@ -3,28 +3,68 @@ + metadata: + annotations: + baseAnno: This is an base annotation ++ note: This is a test annotation + labels: + app: mungebot + foo: bar +- name: baseprefix-mungebot ++ org: kubernetes ++ repo: test-infra ++ name: test-infra-baseprefix-mungebot + spec: +- replicas: 1 ++ replicas: 2 + selector: + matchLabels: ++ app: mungebot + foo: bar ++ org: kubernetes ++ repo: test-infra + template: + metadata: + annotations: + baseAnno: This is an base annotation ++ note: This is a test annotation + labels: + app: mungebot + foo: bar ++ org: kubernetes ++ repo: test-infra + spec: + containers: + - env: ++ - name: FOO ++ valueFrom: ++ configMapKeyRef: ++ key: somekey ++ name: test-infra-app-env-bh449c299k ++ - name: BAR ++ valueFrom: ++ secretKeyRef: ++ key: somekey ++ name: test-infra-app-tls-6hkmhf2224 + - name: foo + value: bar +- image: nginx ++ image: nginx:1.7.9 + name: nginx + ports: + - containerPort: 80 ++ - envFrom: ++ - configMapRef: ++ name: someConfigMap ++ - configMapRef: ++ name: test-infra-app-env-bh449c299k ++ - secretRef: ++ name: test-infra-app-tls-6hkmhf2224 ++ image: busybox ++ name: busybox ++ volumeMounts: ++ - mountPath: /tmp/env ++ name: app-env ++ - mountPath: /tmp/tls ++ name: app-tls ++ volumes: ++ - configMap: ++ name: test-infra-app-env-bh449c299k ++ name: app-env ++ - name: app-tls ++ secret: ++ secretName: test-infra-app-tls-6hkmhf2224 +diff -u -N /tmp/noop/v1_ConfigMap_app-config.yaml /tmp/transformed/v1_ConfigMap_app-config.yaml +--- /tmp/noop/v1_ConfigMap_app-config.yaml YYYY-MM-DD HH:MM:SS ++++ /tmp/transformed/v1_ConfigMap_app-config.yaml YYYY-MM-DD HH:MM:SS +@@ -0,0 +1,15 @@ ++apiVersion: v1 ++data: ++ app-init.ini: | ++ FOO=bar ++ BAR=baz ++kind: ConfigMap ++metadata: ++ annotations: ++ note: This is a test annotation ++ creationTimestamp: null ++ labels: ++ app: mungebot ++ org: kubernetes ++ repo: test-infra ++ name: test-infra-app-config-hf5424hg8g +diff -u -N /tmp/noop/v1_ConfigMap_app-env.yaml /tmp/transformed/v1_ConfigMap_app-env.yaml +--- /tmp/noop/v1_ConfigMap_app-env.yaml YYYY-MM-DD HH:MM:SS ++++ /tmp/transformed/v1_ConfigMap_app-env.yaml YYYY-MM-DD HH:MM:SS +@@ -0,0 +1,14 @@ ++apiVersion: v1 ++data: ++ DB_PASSWORD: somepw ++ DB_USERNAME: admin ++kind: ConfigMap ++metadata: ++ annotations: ++ note: This is a test annotation ++ creationTimestamp: null ++ labels: ++ app: mungebot ++ org: kubernetes ++ repo: test-infra ++ name: test-infra-app-env-bh449c299k +diff -u -N /tmp/noop/v1_Secret_app-tls.yaml /tmp/transformed/v1_Secret_app-tls.yaml +--- /tmp/noop/v1_Secret_app-tls.yaml YYYY-MM-DD HH:MM:SS ++++ /tmp/transformed/v1_Secret_app-tls.yaml YYYY-MM-DD HH:MM:SS +@@ -0,0 +1,15 @@ ++apiVersion: v1 ++data: ++ tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUIwekNDQVgyZ0F3SUJBZ0lKQUkvTTdCWWp3Qit1TUEwR0NTcUdTSWIzRFFFQkJRVUFNRVV4Q3pBSkJnTlYKQkFZVEFrRlZNUk13RVFZRFZRUUlEQXBUYjIxbExWTjBZWFJsTVNFd0h3WURWUVFLREJoSmJuUmxjbTVsZENCWAphV1JuYVhSeklGQjBlU0JNZEdRd0hoY05NVEl3T1RFeU1qRTFNakF5V2hjTk1UVXdPVEV5TWpFMU1qQXlXakJGCk1Rc3dDUVlEVlFRR0V3SkJWVEVUTUJFR0ExVUVDQXdLVTI5dFpTMVRkR0YwWlRFaE1COEdBMVVFQ2d3WVNXNTAKWlhKdVpYUWdWMmxrWjJsMGN5QlFkSGtnVEhSa01Gd3dEUVlKS29aSWh2Y05BUUVCQlFBRFN3QXdTQUpCQU5MSgpoUEhoSVRxUWJQa2xHM2liQ1Z4d0dNUmZwL3Y0WHFoZmRRSGRjVmZIYXA2TlE1V29rLzR4SUErdWkzNS9NbU5hCnJ0TnVDK0JkWjF0TXVWQ1BGWmNDQXdFQUFhTlFNRTR3SFFZRFZSME9CQllFRkp2S3M4UmZKYVhUSDA4VytTR3YKelF5S24wSDhNQjhHQTFVZEl3UVlNQmFBRkp2S3M4UmZKYVhUSDA4VytTR3Z6UXlLbjBIOE1Bd0dBMVVkRXdRRgpNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUZCUUFEUVFCSmxmZkpIeWJqREd4Uk1xYVJtRGhYMCs2djAyVFVLWnNXCnI1UXVWYnBRaEg2dSswVWdjVzBqcDlRd3B4b1BUTFRXR1hFV0JCQnVyeEZ3aUNCaGtRK1YKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= ++ tls.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCT3dJQkFBSkJBTkxKaFBIaElUcVFiUGtsRzNpYkNWeHdHTVJmcC92NFhxaGZkUUhkY1ZmSGFwNk5RNVdvCmsvNHhJQSt1aTM1L01tTmFydE51QytCZFoxdE11VkNQRlpjQ0F3RUFBUUpBRUoyTit6c1IwWG44L1E2dHdhNEcKNk9CMU0xV08rayt6dG5YLzFTdk5lV3U4RDZHSW10dXBMVFlnalpjSHVmeWtqMDlqaUhtakh4OHU4WlpCL28xTgpNUUloQVBXK2V5Wm83YXkzbE16MVYwMVdWak5LSzlRU24xTUpsYjA2aC9MdVl2OUZBaUVBMjVXUGVkS2dWeUNXClNtVXdiUHc4Zm5UY3BxRFdFM3lUTzN2S2NlYnFNU3NDSUJGM1VtVnVlOFlVM2p5YkMzTnh1WHEzd05tMzRSOFQKeFZMSHdEWGgvNk5KQWlFQWwyb0hHR0x6NjRCdUFmaktycXd6N3FNWXI5SENMSWUvWXNvV3Evb2x6U2NDSVFEaQpEMmxXdXNvZTIvbkVxZkRWVldHV2x5Sjd5T21xYVZtL2lOVU45QjJOMmc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= ++kind: Secret ++metadata: ++ annotations: ++ note: This is a test annotation ++ creationTimestamp: null ++ labels: ++ app: mungebot ++ org: kubernetes ++ repo: test-infra ++ name: test-infra-app-tls-6hkmhf2224 ++type: kubernetes.io/tls +diff -u -N /tmp/noop/v1_Service_mungebot-service.yaml /tmp/transformed/v1_Service_mungebot-service.yaml +--- /tmp/noop/v1_Service_mungebot-service.yaml YYYY-MM-DD HH:MM:SS ++++ /tmp/transformed/v1_Service_mungebot-service.yaml YYYY-MM-DD HH:MM:SS +@@ -3,13 +3,18 @@ + metadata: + annotations: + baseAnno: This is an base annotation ++ note: This is a test annotation + labels: + app: mungebot + foo: bar +- name: baseprefix-mungebot-service ++ org: kubernetes ++ repo: test-infra ++ name: test-infra-baseprefix-mungebot-service + spec: + ports: + - port: 7002 + selector: + app: mungebot + foo: bar ++ org: kubernetes ++ repo: test-infra diff --git a/commands/testdata/testcase-simple/expected.yaml b/commands/testdata/testcase-simple/expected.yaml new file mode 100644 index 000000000..8b21a680e --- /dev/null +++ b/commands/testdata/testcase-simple/expected.yaml @@ -0,0 +1,138 @@ +apiVersion: v1 +data: + app-init.ini: | + FOO=bar + BAR=baz +kind: ConfigMap +metadata: + annotations: + note: This is a test annotation + creationTimestamp: null + labels: + app: mungebot + org: kubernetes + repo: test-infra + name: test-infra-app-config-hf5424hg8g +--- +apiVersion: v1 +data: + DB_PASSWORD: somepw + DB_USERNAME: admin +kind: ConfigMap +metadata: + annotations: + note: This is a test annotation + creationTimestamp: null + labels: + app: mungebot + org: kubernetes + repo: test-infra + name: test-infra-app-env-bh449c299k +--- +apiVersion: v1 +data: + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUIwekNDQVgyZ0F3SUJBZ0lKQUkvTTdCWWp3Qit1TUEwR0NTcUdTSWIzRFFFQkJRVUFNRVV4Q3pBSkJnTlYKQkFZVEFrRlZNUk13RVFZRFZRUUlEQXBUYjIxbExWTjBZWFJsTVNFd0h3WURWUVFLREJoSmJuUmxjbTVsZENCWAphV1JuYVhSeklGQjBlU0JNZEdRd0hoY05NVEl3T1RFeU1qRTFNakF5V2hjTk1UVXdPVEV5TWpFMU1qQXlXakJGCk1Rc3dDUVlEVlFRR0V3SkJWVEVUTUJFR0ExVUVDQXdLVTI5dFpTMVRkR0YwWlRFaE1COEdBMVVFQ2d3WVNXNTAKWlhKdVpYUWdWMmxrWjJsMGN5QlFkSGtnVEhSa01Gd3dEUVlKS29aSWh2Y05BUUVCQlFBRFN3QXdTQUpCQU5MSgpoUEhoSVRxUWJQa2xHM2liQ1Z4d0dNUmZwL3Y0WHFoZmRRSGRjVmZIYXA2TlE1V29rLzR4SUErdWkzNS9NbU5hCnJ0TnVDK0JkWjF0TXVWQ1BGWmNDQXdFQUFhTlFNRTR3SFFZRFZSME9CQllFRkp2S3M4UmZKYVhUSDA4VytTR3YKelF5S24wSDhNQjhHQTFVZEl3UVlNQmFBRkp2S3M4UmZKYVhUSDA4VytTR3Z6UXlLbjBIOE1Bd0dBMVVkRXdRRgpNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUZCUUFEUVFCSmxmZkpIeWJqREd4Uk1xYVJtRGhYMCs2djAyVFVLWnNXCnI1UXVWYnBRaEg2dSswVWdjVzBqcDlRd3B4b1BUTFRXR1hFV0JCQnVyeEZ3aUNCaGtRK1YKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + tls.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCT3dJQkFBSkJBTkxKaFBIaElUcVFiUGtsRzNpYkNWeHdHTVJmcC92NFhxaGZkUUhkY1ZmSGFwNk5RNVdvCmsvNHhJQSt1aTM1L01tTmFydE51QytCZFoxdE11VkNQRlpjQ0F3RUFBUUpBRUoyTit6c1IwWG44L1E2dHdhNEcKNk9CMU0xV08rayt6dG5YLzFTdk5lV3U4RDZHSW10dXBMVFlnalpjSHVmeWtqMDlqaUhtakh4OHU4WlpCL28xTgpNUUloQVBXK2V5Wm83YXkzbE16MVYwMVdWak5LSzlRU24xTUpsYjA2aC9MdVl2OUZBaUVBMjVXUGVkS2dWeUNXClNtVXdiUHc4Zm5UY3BxRFdFM3lUTzN2S2NlYnFNU3NDSUJGM1VtVnVlOFlVM2p5YkMzTnh1WHEzd05tMzRSOFQKeFZMSHdEWGgvNk5KQWlFQWwyb0hHR0x6NjRCdUFmaktycXd6N3FNWXI5SENMSWUvWXNvV3Evb2x6U2NDSVFEaQpEMmxXdXNvZTIvbkVxZkRWVldHV2x5Sjd5T21xYVZtL2lOVU45QjJOMmc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= +kind: Secret +metadata: + annotations: + note: This is a test annotation + creationTimestamp: null + labels: + app: mungebot + org: kubernetes + repo: test-infra + name: test-infra-app-tls-6hkmhf2224 +type: kubernetes.io/tls +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + baseAnno: This is an base annotation + note: This is a test annotation + labels: + app: mungebot + foo: bar + org: kubernetes + repo: test-infra + name: test-infra-baseprefix-mungebot-service +spec: + ports: + - port: 7002 + selector: + app: mungebot + foo: bar + org: kubernetes + repo: test-infra +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + annotations: + baseAnno: This is an base annotation + note: This is a test annotation + labels: + app: mungebot + foo: bar + org: kubernetes + repo: test-infra + name: test-infra-baseprefix-mungebot +spec: + replicas: 2 + selector: + matchLabels: + app: mungebot + foo: bar + org: kubernetes + repo: test-infra + template: + metadata: + annotations: + baseAnno: This is an base annotation + note: This is a test annotation + labels: + app: mungebot + foo: bar + org: kubernetes + repo: test-infra + spec: + containers: + - env: + - name: FOO + valueFrom: + configMapKeyRef: + key: somekey + name: test-infra-app-env-bh449c299k + - name: BAR + valueFrom: + secretKeyRef: + key: somekey + name: test-infra-app-tls-6hkmhf2224 + - name: foo + value: bar + image: nginx:1.7.9 + name: nginx + ports: + - containerPort: 80 + - envFrom: + - configMapRef: + name: someConfigMap + - configMapRef: + name: test-infra-app-env-bh449c299k + - secretRef: + name: test-infra-app-tls-6hkmhf2224 + image: busybox + name: busybox + volumeMounts: + - mountPath: /tmp/env + name: app-env + - mountPath: /tmp/tls + name: app-tls + volumes: + - configMap: + name: test-infra-app-env-bh449c299k + name: app-env + - name: app-tls + secret: + secretName: test-infra-app-tls-6hkmhf2224 diff --git a/commands/testdata/testcase-simple/test.yaml b/commands/testdata/testcase-simple/test.yaml new file mode 100644 index 000000000..e9185c012 --- /dev/null +++ b/commands/testdata/testcase-simple/test.yaml @@ -0,0 +1,5 @@ +description: simple +args: [] +filename: ../examples/simple/instances/exampleinstance/ +expectedStdout: testdata/testcase-simple/expected.yaml +expectedDiff: testdata/testcase-simple/expected.diff diff --git a/commands/testdata/testcase-single-overlay/expected.diff b/commands/testdata/testcase-single-overlay/expected.diff new file mode 100644 index 000000000..d27e1da5f --- /dev/null +++ b/commands/testdata/testcase-single-overlay/expected.diff @@ -0,0 +1,128 @@ +diff -u -N /tmp/noop/apps_v1beta2_Deployment_nginx.yaml /tmp/transformed/apps_v1beta2_Deployment_nginx.yaml +--- /tmp/noop/apps_v1beta2_Deployment_nginx.yaml YYYY-MM-DD HH:MM:SS ++++ /tmp/transformed/apps_v1beta2_Deployment_nginx.yaml YYYY-MM-DD HH:MM:SS +@@ -5,23 +5,26 @@ + note: This is a test annotation + labels: + app: mynginx ++ env: staging + org: example.com +- team: foo +- name: team-foo-nginx ++ team: override-foo ++ name: staging-team-foo-nginx + spec: + selector: + matchLabels: + app: mynginx ++ env: staging + org: example.com +- team: foo ++ team: override-foo + template: + metadata: + annotations: + note: This is a test annotation + labels: + app: mynginx ++ env: staging + org: example.com +- team: foo ++ team: override-foo + spec: + containers: + - image: nginx +@@ -30,8 +33,12 @@ + - mountPath: /tmp/ps + name: nginx-persistent-storage + volumes: +- - emptyDir: {} ++ - gcePersistentDisk: ++ pdName: nginx-persistent-storage + name: nginx-persistent-storage + - configMap: +- name: team-foo-configmap-in-base-bbdmdh7m8t ++ name: staging-configmap-in-overlay-k7cbc75tg8 ++ name: configmap-in-overlay ++ - configMap: ++ name: staging-team-foo-configmap-in-base-gh9d7t85gb + name: configmap-in-base +diff -u -N /tmp/noop/v1_ConfigMap_configmap-in-base.yaml /tmp/transformed/v1_ConfigMap_configmap-in-base.yaml +--- /tmp/noop/v1_ConfigMap_configmap-in-base.yaml YYYY-MM-DD HH:MM:SS ++++ /tmp/transformed/v1_ConfigMap_configmap-in-base.yaml YYYY-MM-DD HH:MM:SS +@@ -1,6 +1,6 @@ + apiVersion: v1 + data: +- foo: bar ++ foo: override-bar + kind: ConfigMap + metadata: + annotations: +@@ -8,6 +8,7 @@ + creationTimestamp: null + labels: + app: mynginx ++ env: staging + org: example.com +- team: foo +- name: team-foo-configmap-in-base-bbdmdh7m8t ++ team: override-foo ++ name: staging-team-foo-configmap-in-base-gh9d7t85gb +diff -u -N /tmp/noop/v1_ConfigMap_configmap-in-overlay.yaml /tmp/transformed/v1_ConfigMap_configmap-in-overlay.yaml +--- /tmp/noop/v1_ConfigMap_configmap-in-overlay.yaml YYYY-MM-DD HH:MM:SS ++++ /tmp/transformed/v1_ConfigMap_configmap-in-overlay.yaml YYYY-MM-DD HH:MM:SS +@@ -0,0 +1,10 @@ ++apiVersion: v1 ++data: ++ hello: world ++kind: ConfigMap ++metadata: ++ creationTimestamp: null ++ labels: ++ env: staging ++ team: override-foo ++ name: staging-configmap-in-overlay-k7cbc75tg8 +diff -u -N /tmp/noop/v1_Secret_secret-in-base.yaml /tmp/transformed/v1_Secret_secret-in-base.yaml +--- /tmp/noop/v1_Secret_secret-in-base.yaml YYYY-MM-DD HH:MM:SS ++++ /tmp/transformed/v1_Secret_secret-in-base.yaml YYYY-MM-DD HH:MM:SS +@@ -1,6 +1,7 @@ + apiVersion: v1 + data: + password: c29tZXB3 ++ proxy: aGFwcm94eQ== + username: YWRtaW4= + kind: Secret + metadata: +@@ -9,7 +10,8 @@ + creationTimestamp: null + labels: + app: mynginx ++ env: staging + org: example.com +- team: foo +- name: team-foo-secret-in-base-tkm7hhtf8d ++ team: override-foo ++ name: staging-team-foo-secret-in-base-c8db7gk2m2 + type: Opaque +diff -u -N /tmp/noop/v1_Service_nginx.yaml /tmp/transformed/v1_Service_nginx.yaml +--- /tmp/noop/v1_Service_nginx.yaml YYYY-MM-DD HH:MM:SS ++++ /tmp/transformed/v1_Service_nginx.yaml YYYY-MM-DD HH:MM:SS +@@ -5,13 +5,15 @@ + note: This is a test annotation + labels: + app: mynginx ++ env: staging + org: example.com +- team: foo +- name: team-foo-nginx ++ team: override-foo ++ name: staging-team-foo-nginx + spec: + ports: + - port: 80 + selector: + app: mynginx ++ env: staging + org: example.com +- team: foo ++ team: override-foo diff --git a/commands/testdata/testcase-single-overlay/expected.yaml b/commands/testdata/testcase-single-overlay/expected.yaml new file mode 100644 index 000000000..91f209111 --- /dev/null +++ b/commands/testdata/testcase-single-overlay/expected.yaml @@ -0,0 +1,108 @@ +apiVersion: v1 +data: + foo: override-bar +kind: ConfigMap +metadata: + annotations: + note: This is a test annotation + creationTimestamp: null + labels: + app: mynginx + env: staging + org: example.com + team: override-foo + name: staging-team-foo-configmap-in-base-gh9d7t85gb +--- +apiVersion: v1 +data: + hello: world +kind: ConfigMap +metadata: + creationTimestamp: null + labels: + env: staging + team: override-foo + name: staging-configmap-in-overlay-k7cbc75tg8 +--- +apiVersion: v1 +data: + password: c29tZXB3 + proxy: aGFwcm94eQ== + username: YWRtaW4= +kind: Secret +metadata: + annotations: + note: This is a test annotation + creationTimestamp: null + labels: + app: mynginx + env: staging + org: example.com + team: override-foo + name: staging-team-foo-secret-in-base-c8db7gk2m2 +type: Opaque +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + note: This is a test annotation + labels: + app: mynginx + env: staging + org: example.com + team: override-foo + name: staging-team-foo-nginx +spec: + ports: + - port: 80 + selector: + app: mynginx + env: staging + org: example.com + team: override-foo +--- +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + annotations: + note: This is a test annotation + labels: + app: mynginx + env: staging + org: example.com + team: override-foo + name: staging-team-foo-nginx +spec: + selector: + matchLabels: + app: mynginx + env: staging + org: example.com + team: override-foo + template: + metadata: + annotations: + note: This is a test annotation + labels: + app: mynginx + env: staging + org: example.com + team: override-foo + spec: + containers: + - image: nginx + name: nginx + volumeMounts: + - mountPath: /tmp/ps + name: nginx-persistent-storage + volumes: + - gcePersistentDisk: + pdName: nginx-persistent-storage + name: nginx-persistent-storage + - configMap: + name: staging-configmap-in-overlay-k7cbc75tg8 + name: configmap-in-overlay + - configMap: + name: staging-team-foo-configmap-in-base-gh9d7t85gb + name: configmap-in-base diff --git a/commands/testdata/testcase-single-overlay/in/overlay/deployment.yaml b/commands/testdata/testcase-single-overlay/in/overlay/deployment.yaml new file mode 100644 index 000000000..ae8bc1280 --- /dev/null +++ b/commands/testdata/testcase-single-overlay/in/overlay/deployment.yaml @@ -0,0 +1,15 @@ +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + name: nginx +spec: + template: + spec: + volumes: + - name: nginx-persistent-storage + emptyDir: null + gcePersistentDisk: + pdName: nginx-persistent-storage + - configMap: + name: configmap-in-overlay + name: configmap-in-overlay diff --git a/commands/testdata/testcase-single-overlay/in/overlay/kustomize.yaml b/commands/testdata/testcase-single-overlay/in/overlay/kustomize.yaml new file mode 100644 index 000000000..765e47bff --- /dev/null +++ b/commands/testdata/testcase-single-overlay/in/overlay/kustomize.yaml @@ -0,0 +1,25 @@ +apiVersion: manifest.k8s.io/v1alpha1 +kind: Manifest +metadata: + name: nginx-app +namePrefix: staging- +objectLabels: + env: staging + team: override-foo +patches: + - deployment.yaml +bases: + - ../package/ +configMapGenerator: + - name: configmap-in-overlay + literals: + - hello=world + - name: configmap-in-base + behavior: replace + literals: + - foo=override-bar +secretGenerator: +- name: secret-in-base + behavior: merge + commands: + proxy: "printf haproxy" diff --git a/commands/testdata/testcase-single-overlay/in/package/deployment.yaml b/commands/testdata/testcase-single-overlay/in/package/deployment.yaml new file mode 100644 index 000000000..9f7d3cbb6 --- /dev/null +++ b/commands/testdata/testcase-single-overlay/in/package/deployment.yaml @@ -0,0 +1,24 @@ +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + name: nginx + labels: + app: nginx +spec: + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx + volumeMounts: + - name: nginx-persistent-storage + mountPath: /tmp/ps + volumes: + - name: nginx-persistent-storage + emptyDir: {} + - configMap: + name: configmap-in-base + name: configmap-in-base diff --git a/commands/testdata/testcase-single-overlay/in/package/kustomize.yaml b/commands/testdata/testcase-single-overlay/in/package/kustomize.yaml new file mode 100644 index 000000000..fb0bf362c --- /dev/null +++ b/commands/testdata/testcase-single-overlay/in/package/kustomize.yaml @@ -0,0 +1,23 @@ +apiVersion: manifest.k8s.io/v1alpha1 +kind: Manifest +metadata: + name: nginx-app +namePrefix: team-foo- +objectLabels: + app: mynginx + org: example.com + team: foo +objectAnnotations: + note: This is a test annotation +resources: + - deployment.yaml + - service.yaml +configMapGenerator: + - name: configmap-in-base + literals: + - foo=bar +secretGenerator: +- name: secret-in-base + commands: + username: "printf admin" + password: "printf somepw" diff --git a/commands/testdata/testcase-single-overlay/in/package/service.yaml b/commands/testdata/testcase-single-overlay/in/package/service.yaml new file mode 100644 index 000000000..f6dd86909 --- /dev/null +++ b/commands/testdata/testcase-single-overlay/in/package/service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: nginx + labels: + app: nginx +spec: + ports: + - port: 80 + selector: + app: nginx diff --git a/commands/testdata/testcase-single-overlay/test.yaml b/commands/testdata/testcase-single-overlay/test.yaml new file mode 100644 index 000000000..1af80f52c --- /dev/null +++ b/commands/testdata/testcase-single-overlay/test.yaml @@ -0,0 +1,5 @@ +description: single overlay +args: [] +filename: testdata/testcase-single-overlay/in/overlay/ +expectedStdout: testdata/testcase-single-overlay/expected.yaml +expectedDiff: testdata/testcase-single-overlay/expected.diff diff --git a/commands/util.go b/commands/util.go new file mode 100644 index 000000000..fb035b87b --- /dev/null +++ b/commands/util.go @@ -0,0 +1,95 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package commands + +import ( + "errors" + "fmt" + "path" + "strings" + + "github.com/ghodss/yaml" + + manifest "k8s.io/kubectl/pkg/apis/manifest/v1alpha1" + "k8s.io/kubectl/pkg/kustomize/constants" + interror "k8s.io/kubectl/pkg/kustomize/internal/error" + "k8s.io/kubectl/pkg/kustomize/util/fs" +) + +type manifestFile struct { + mPath string + fsys fs.FileSystem +} + +func newManifestFile(mPath string, fsys fs.FileSystem) (*manifestFile, error) { + mf := &manifestFile{mPath: mPath, fsys: fsys} + err := mf.validate() + if err != nil { + return nil, err + } + return mf, nil +} + +func (mf *manifestFile) validate() error { + f, err := mf.fsys.Stat(mf.mPath) + if err != nil { + errorMsg := fmt.Sprintf("Manifest (%s) missing\nRun `kustomize init` first", mf.mPath) + merr := interror.ManifestError{ManifestFilepath: mf.mPath, ErrorMsg: errorMsg} + return merr + } + if f.IsDir() { + mf.mPath = path.Join(mf.mPath, constants.KustomizeFileName) + _, err = mf.fsys.Stat(mf.mPath) + if err != nil { + errorMsg := fmt.Sprintf("Manifest (%s) missing\nRun `kustomize init` first", mf.mPath) + merr := interror.ManifestError{ManifestFilepath: mf.mPath, ErrorMsg: errorMsg} + return merr + } + } else { + if !strings.HasSuffix(mf.mPath, constants.KustomizeFileName) { + errorMsg := fmt.Sprintf("Manifest file (%s) should have %s suffix\n", mf.mPath, constants.KustomizeSuffix) + merr := interror.ManifestError{ManifestFilepath: mf.mPath, ErrorMsg: errorMsg} + return merr + } + } + return nil +} + +func (mf *manifestFile) read() (*manifest.Manifest, error) { + bytes, err := mf.fsys.ReadFile(mf.mPath) + if err != nil { + return nil, err + } + var manifest manifest.Manifest + err = yaml.Unmarshal(bytes, &manifest) + if err != nil { + return nil, err + } + return &manifest, err +} + +func (mf *manifestFile) write(manifest *manifest.Manifest) error { + if manifest == nil { + return errors.New("util: failed to write passed-in nil manifest") + } + bytes, err := yaml.Marshal(manifest) + if err != nil { + return err + } + + return mf.fsys.WriteFile(mf.mPath, bytes) +} diff --git a/commands/util_test.go b/commands/util_test.go new file mode 100644 index 000000000..696d77da9 --- /dev/null +++ b/commands/util_test.go @@ -0,0 +1,87 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package commands + +import ( + "reflect" + "strings" + "testing" + + manifest "k8s.io/kubectl/pkg/apis/manifest/v1alpha1" + "k8s.io/kubectl/pkg/kustomize/util/fs" +) + +func TestWriteAndRead(t *testing.T) { + manifest := &manifest.Manifest{ + NamePrefix: "prefix", + } + + fsys := fs.MakeFakeFS() + fsys.Create("kustomize.yaml") + mf, err := newManifestFile("kustomize.yaml", fsys) + if err != nil { + t.Fatalf("Unexpected Error: %v", err) + } + + if err := mf.write(manifest); err != nil { + t.Fatalf("Couldn't write manifest file: %v\n", err) + } + + readManifest, err := mf.read() + if err != nil { + t.Fatalf("Couldn't read manifest file: %v\n", err) + } + if !reflect.DeepEqual(manifest, readManifest) { + t.Fatal("Read manifest is different from written manifest") + } +} + +func TestEmptyFile(t *testing.T) { + fsys := fs.MakeFakeFS() + _, err := newManifestFile("", fsys) + if err == nil { + t.Fatalf("Creat manifestFile from empty filename should fail") + } +} + +func TestNewNotExist(t *testing.T) { + badSuffix := "foo.bar" + fakeFS := fs.MakeFakeFS() + fakeFS.Mkdir(".", 0644) + fakeFS.Create(badSuffix) + _, err := newManifestFile("kustomize.yaml", fakeFS) + if err == nil { + t.Fatalf("expect an error") + } + if !strings.Contains(err.Error(), "Run `kustomize init` first") { + t.Fatalf("expect an error contains %q, but got %v", "does not exist", err) + } + _, err = newManifestFile("kustomize.yaml", fakeFS) + if err == nil { + t.Fatalf("expect an error") + } + if !strings.Contains(err.Error(), "Run `kustomize init` first") { + t.Fatalf("expect an error contains %q, but got %v", "does not exist", err) + } + _, err = newManifestFile(badSuffix, fakeFS) + if err == nil { + t.Fatalf("expect an error") + } + if !strings.Contains(err.Error(), "should have .yaml suffix") { + t.Fatalf("expect an error contains %q, but got %v", "does not exist", err) + } +} diff --git a/configmapandsecret/configmap_secret.go b/configmapandsecret/configmap_secret.go new file mode 100644 index 000000000..d7087e0d9 --- /dev/null +++ b/configmapandsecret/configmap_secret.go @@ -0,0 +1,182 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package configmapandsecret + +import ( + "context" + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "time" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + manifest "k8s.io/kubectl/pkg/apis/manifest/v1alpha1" + cutil "k8s.io/kubectl/pkg/kustomize/configmapandsecret/util" + "k8s.io/kubectl/pkg/kustomize/hash" + "k8s.io/kubectl/pkg/kustomize/resource" + "k8s.io/kubectl/pkg/kustomize/types" +) + +// MakeConfigmapAndGenerateName makes a configmap and returns the configmap and the name appended with a hash. +func MakeConfigmapAndGenerateName(cm manifest.ConfigMapArgs) (*unstructured.Unstructured, string, error) { + corev1CM, err := makeConfigMap(cm) + if err != nil { + return nil, "", err + } + h, err := hash.ConfigMapHash(corev1CM) + if err != nil { + return nil, "", err + } + nameWithHash := fmt.Sprintf("%s-%s", corev1CM.GetName(), h) + unstructuredCM, err := objectToUnstructured(corev1CM) + return unstructuredCM, nameWithHash, err +} + +// MakeSecretAndGenerateName returns a secret with the name appended with a hash. +func MakeSecretAndGenerateName(secret manifest.SecretArgs, path string) (*unstructured.Unstructured, string, error) { + corev1Secret, err := makeSecret(secret, path) + if err != nil { + return nil, "", err + } + h, err := hash.SecretHash(corev1Secret) + if err != nil { + return nil, "", err + } + nameWithHash := fmt.Sprintf("%s-%s", secret.Name, h) + unstructuredCM, err := objectToUnstructured(corev1Secret) + return unstructuredCM, nameWithHash, err +} + +func objectToUnstructured(in runtime.Object) (*unstructured.Unstructured, error) { + marshaled, err := json.Marshal(in) + if err != nil { + return nil, err + } + var out unstructured.Unstructured + err = out.UnmarshalJSON(marshaled) + return &out, err +} + +func makeConfigMap(cm manifest.ConfigMapArgs) (*corev1.ConfigMap, error) { + corev1cm := &corev1.ConfigMap{} + corev1cm.APIVersion = "v1" + corev1cm.Kind = "ConfigMap" + corev1cm.Name = cm.Name + corev1cm.Data = map[string]string{} + + if cm.EnvSource != "" { + if err := cutil.HandleConfigMapFromEnvFileSource(corev1cm, cm.EnvSource); err != nil { + return nil, err + } + } + if cm.FileSources != nil { + if err := cutil.HandleConfigMapFromFileSources(corev1cm, cm.FileSources); err != nil { + return nil, err + } + } + if cm.LiteralSources != nil { + if err := cutil.HandleConfigMapFromLiteralSources(corev1cm, cm.LiteralSources); err != nil { + return nil, err + } + } + + return corev1cm, nil +} + +func makeSecret(secret manifest.SecretArgs, path string) (*corev1.Secret, error) { + corev1secret := &corev1.Secret{} + corev1secret.APIVersion = "v1" + corev1secret.Kind = "Secret" + corev1secret.Name = secret.Name + corev1secret.Type = corev1.SecretType(secret.Type) + if corev1secret.Type == "" { + corev1secret.Type = corev1.SecretTypeOpaque + } + corev1secret.Data = map[string][]byte{} + + for k, v := range secret.Commands { + out, err := createSecretKey(path, v) + if err != nil { + return nil, err + } + corev1secret.Data[k] = out + } + + return corev1secret, nil +} + +func populateMap(m resource.ResourceCollection, obj *unstructured.Unstructured, newName string) error { + oldName := obj.GetName() + gvk := obj.GroupVersionKind() + gvkn := types.GroupVersionKindName{GVK: gvk, Name: oldName} + + if _, found := m[gvkn]; found { + return fmt.Errorf("The already exists in the map", oldName, gvk) + } + obj.SetName(newName) + m[gvkn] = &resource.Resource{Data: obj} + return nil +} + +// MakeConfigMapsResourceCollection returns a map of -> unstructured object. +func MakeConfigMapsResourceCollection(maps []manifest.ConfigMapArgs) (resource.ResourceCollection, error) { + m := resource.ResourceCollection{} + for _, cm := range maps { + unstructuredConfigMap, nameWithHash, err := MakeConfigmapAndGenerateName(cm) + if err != nil { + return nil, err + } + err = populateMap(m, unstructuredConfigMap, nameWithHash) + if err != nil { + return nil, err + } + } + return m, nil +} + +// MakeSecretsResourceCollection returns a map of -> unstructured object. +func MakeSecretsResourceCollection(secrets []manifest.SecretArgs, path string) (resource.ResourceCollection, error) { + m := resource.ResourceCollection{} + for _, secret := range secrets { + unstructuredSecret, nameWithHash, err := MakeSecretAndGenerateName(secret, path) + if err != nil { + return nil, err + } + err = populateMap(m, unstructuredSecret, nameWithHash) + if err != nil { + return nil, err + } + } + return m, nil +} + +func createSecretKey(wd string, command string) ([]byte, error) { + fi, err := os.Stat(wd) + if err != nil || !fi.IsDir() { + wd = filepath.Dir(wd) + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, "sh", "-c", command) + cmd.Dir = wd + + return cmd.Output() +} diff --git a/configmapandsecret/configmap_secret_test.go b/configmapandsecret/configmap_secret_test.go new file mode 100644 index 000000000..7665ab8b9 --- /dev/null +++ b/configmapandsecret/configmap_secret_test.go @@ -0,0 +1,243 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package configmapandsecret + +import ( + "encoding/base64" + "reflect" + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + manifest "k8s.io/kubectl/pkg/apis/manifest/v1alpha1" +) + +func makeEnvConfigMap(name string) *corev1.ConfigMap { + return &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Data: map[string]string{ + "DB_USERNAME": "admin", + "DB_PASSWORD": "somepw", + }, + } +} + +func makeUnstructuredEnvConfigMap(name string) *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": name, + "creationTimestamp": nil, + }, + "data": map[string]interface{}{ + "DB_USERNAME": "admin", + "DB_PASSWORD": "somepw", + }, + }, + } +} + +func makeFileConfigMap(name string) *corev1.ConfigMap { + return &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Data: map[string]string{ + "app-init.ini": `FOO=bar +BAR=baz +`, + }, + } +} + +func makeLiteralConfigMap(name string) *corev1.ConfigMap { + return &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Data: map[string]string{ + "a": "x", + "b": "y", + }, + } +} + +func makeTestSecret(name string) *corev1.Secret { + return &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Data: map[string][]byte{ + "DB_USERNAME": []byte("admin"), + "DB_PASSWORD": []byte("somepw"), + }, + Type: corev1.SecretTypeOpaque, + } +} + +func makeUnstructuredSecret(name string) *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Secret", + "metadata": map[string]interface{}{ + "name": name, + "creationTimestamp": nil, + }, + "type": string(corev1.SecretTypeOpaque), + "data": map[string]interface{}{ + "DB_USERNAME": base64.StdEncoding.EncodeToString([]byte("admin")), + "DB_PASSWORD": base64.StdEncoding.EncodeToString([]byte("somepw")), + }, + }, + } +} + +func TestConstructConfigMap(t *testing.T) { + type testCase struct { + description string + input manifest.ConfigMapArgs + expected *corev1.ConfigMap + } + + testCases := []testCase{ + { + description: "construct config map from env", + input: manifest.ConfigMapArgs{ + Name: "envConfigMap", + DataSources: manifest.DataSources{ + EnvSource: "../examples/simple/instances/exampleinstance/configmap/app.env", + }, + }, + expected: makeEnvConfigMap("envConfigMap"), + }, + { + description: "construct config map from file", + input: manifest.ConfigMapArgs{ + Name: "fileConfigMap", + DataSources: manifest.DataSources{ + FileSources: []string{"../examples/simple/instances/exampleinstance/configmap/app-init.ini"}, + }, + }, + expected: makeFileConfigMap("fileConfigMap"), + }, + { + description: "construct config map from literal", + input: manifest.ConfigMapArgs{ + Name: "literalConfigMap", + DataSources: manifest.DataSources{ + LiteralSources: []string{"a=x", "b=y"}, + }, + }, + expected: makeLiteralConfigMap("literalConfigMap"), + }, + } + + for _, tc := range testCases { + cm, err := makeConfigMap(tc.input) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(*cm, *tc.expected) { + t.Fatalf("in testcase: %q updated:\n%#v\ndoesn't match expected:\n%#v\n", tc.description, *cm, tc.expected) + } + } +} + +func TestConstructSecret(t *testing.T) { + secret := manifest.SecretArgs{ + Name: "secret", + Commands: map[string]string{ + "DB_USERNAME": "printf admin", + "DB_PASSWORD": "printf somepw", + }, + Type: "Opaque", + } + cm, err := makeSecret(secret, ".") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + expected := makeTestSecret("secret") + if !reflect.DeepEqual(*cm, *expected) { + t.Fatalf("%#v\ndoesn't match expected:\n%#v", *cm, *expected) + } +} + +func TestFailConstructSecret(t *testing.T) { + secret := manifest.SecretArgs{ + Name: "secret", + Commands: map[string]string{ + "FAILURE": "false", // This will fail. + }, + Type: "Opaque", + } + _, err := makeSecret(secret, ".") + if err == nil { + t.Fatalf("Expected failure.") + } +} + +func TestObjectConvertToUnstructured(t *testing.T) { + type testCase struct { + description string + input *corev1.ConfigMap + expected *unstructured.Unstructured + } + + testCases := []testCase{ + { + description: "convert config map", + input: makeEnvConfigMap("envConfigMap"), + expected: makeUnstructuredEnvConfigMap("envConfigMap"), + }, + { + description: "convert secret", + input: makeEnvConfigMap("envSecret"), + expected: makeUnstructuredEnvConfigMap("envSecret"), + }, + } + for _, tc := range testCases { + actual, err := objectToUnstructured(tc.input) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(actual, tc.expected) { + t.Fatalf("%#v\ndoesn't match expected\n%#v\n", actual, tc.expected) + } + } +} diff --git a/configmapandsecret/util/configmap.go b/configmapandsecret/util/configmap.go new file mode 100644 index 000000000..a8dc2ebdd --- /dev/null +++ b/configmapandsecret/util/configmap.go @@ -0,0 +1,134 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "strings" + + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/validation" +) + +// handleConfigMapFromLiteralSources adds the specified literal source +// information into the provided configMap. +func HandleConfigMapFromLiteralSources(configMap *v1.ConfigMap, literalSources []string) error { + for _, literalSource := range literalSources { + keyName, value, err := ParseLiteralSource(literalSource) + if err != nil { + return err + } + err = addKeyFromLiteralToConfigMap(configMap, keyName, value) + if err != nil { + return err + } + } + return nil +} + +// handleConfigMapFromFileSources adds the specified file source information +// into the provided configMap +func HandleConfigMapFromFileSources(configMap *v1.ConfigMap, fileSources []string) error { + for _, fileSource := range fileSources { + keyName, filePath, err := ParseFileSource(fileSource) + if err != nil { + return err + } + info, err := os.Stat(filePath) + if err != nil { + switch err := err.(type) { + case *os.PathError: + return fmt.Errorf("error reading %s: %v", filePath, err.Err) + default: + return fmt.Errorf("error reading %s: %v", filePath, err) + } + } + if info.IsDir() { + if strings.Contains(fileSource, "=") { + return fmt.Errorf("cannot give a key name for a directory path.") + } + fileList, err := ioutil.ReadDir(filePath) + if err != nil { + return fmt.Errorf("error listing files in %s: %v", filePath, err) + } + for _, item := range fileList { + itemPath := path.Join(filePath, item.Name()) + if item.Mode().IsRegular() { + keyName = item.Name() + err = addKeyFromFileToConfigMap(configMap, keyName, itemPath) + if err != nil { + return err + } + } + } + } else { + if err := addKeyFromFileToConfigMap(configMap, keyName, filePath); err != nil { + return err + } + } + } + + return nil +} + +// handleConfigMapFromEnvFileSource adds the specified env file source information +// into the provided configMap +func HandleConfigMapFromEnvFileSource(configMap *v1.ConfigMap, envFileSource string) error { + info, err := os.Stat(envFileSource) + if err != nil { + switch err := err.(type) { + case *os.PathError: + return fmt.Errorf("error reading %s: %v", envFileSource, err.Err) + default: + return fmt.Errorf("error reading %s: %v", envFileSource, err) + } + } + if info.IsDir() { + return fmt.Errorf("env config file cannot be a directory") + } + + return addFromEnvFile(envFileSource, func(key, value string) error { + return addKeyFromLiteralToConfigMap(configMap, key, value) + }) +} + +// addKeyFromFileToConfigMap adds a key with the given name to a ConfigMap, populating +// the value with the content of the given file path, or returns an error. +func addKeyFromFileToConfigMap(configMap *v1.ConfigMap, keyName, filePath string) error { + data, err := ioutil.ReadFile(filePath) + if err != nil { + return err + } + return addKeyFromLiteralToConfigMap(configMap, keyName, string(data)) +} + +// addKeyFromLiteralToConfigMap adds the given key and data to the given config map, +// returning an error if the key is not valid or if the key already exists. +func addKeyFromLiteralToConfigMap(configMap *v1.ConfigMap, keyName, data string) error { + // Note, the rules for ConfigMap keys are the exact same as the ones for SecretKeys. + if errs := validation.IsConfigMapKey(keyName); len(errs) != 0 { + return fmt.Errorf("%q is not a valid key name for a ConfigMap: %s", keyName, strings.Join(errs, ";")) + } + if _, entryExists := configMap.Data[keyName]; entryExists { + return fmt.Errorf("cannot add key %s, another key by that name already exists: %v.", keyName, configMap.Data) + } + configMap.Data[keyName] = data + return nil +} diff --git a/configmapandsecret/util/env_file.go b/configmapandsecret/util/env_file.go new file mode 100644 index 000000000..5f687202c --- /dev/null +++ b/configmapandsecret/util/env_file.go @@ -0,0 +1,103 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "bufio" + "bytes" + "fmt" + "os" + "strings" + "unicode" + "unicode/utf8" + + "k8s.io/apimachinery/pkg/util/validation" +) + +var utf8bom = []byte{0xEF, 0xBB, 0xBF} + +// processEnvFileLine returns a blank key if the line is empty or a comment. +// The value will be retrieved from the environment if necessary. +func processEnvFileLine(line []byte, filePath string, + currentLine int) (key, value string, err error) { + + if !utf8.Valid(line) { + return ``, ``, fmt.Errorf("env file %s contains invalid utf8 bytes at line %d: %v", + filePath, currentLine+1, line) + } + + // We trim UTF8 BOM from the first line of the file but no others + if currentLine == 0 { + line = bytes.TrimPrefix(line, utf8bom) + } + + // trim the line from all leading whitespace first + line = bytes.TrimLeftFunc(line, unicode.IsSpace) + + // If the line is empty or a comment, we return a blank key/value pair. + if len(line) == 0 || line[0] == '#' { + return ``, ``, nil + } + + data := strings.SplitN(string(line), "=", 2) + key = data[0] + if errs := validation.IsEnvVarName(key); len(errs) != 0 { + return ``, ``, fmt.Errorf("%q is not a valid key name: %s", key, strings.Join(errs, ";")) + } + + if len(data) == 2 { + value = data[1] + } else { + // No value (no `=` in the line) is a signal to obtain the value + // from the environment. + value = os.Getenv(key) + } + return +} + +// addFromEnvFile processes an env file allows a generic addTo to handle the +// collection of key value pairs or returns an error. +func addFromEnvFile(filePath string, addTo func(key, value string) error) error { + f, err := os.Open(filePath) + if err != nil { + return err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + currentLine := 0 + for scanner.Scan() { + // Process the current line, retrieving a key/value pair if + // possible. + scannedBytes := scanner.Bytes() + key, value, err := processEnvFileLine(scannedBytes, filePath, currentLine) + if err != nil { + return err + } + currentLine++ + + if len(key) == 0 { + // no key means line was empty or a comment + continue + } + + if err = addTo(key, value); err != nil { + return err + } + } + return nil +} diff --git a/configmapandsecret/util/secret.go b/configmapandsecret/util/secret.go new file mode 100644 index 000000000..80be6c3ff --- /dev/null +++ b/configmapandsecret/util/secret.go @@ -0,0 +1,126 @@ +/* +Copyright 2015 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "strings" + + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/validation" +) + +// HandleFromLiteralSources adds the specified literal source information into the provided secret +func HandleFromLiteralSources(secret *v1.Secret, literalSources []string) error { + for _, literalSource := range literalSources { + keyName, value, err := ParseLiteralSource(literalSource) + if err != nil { + return err + } + if err = addKeyFromLiteralToSecret(secret, keyName, []byte(value)); err != nil { + return err + } + } + return nil +} + +// HandleFromFileSources adds the specified file source information into the provided secret +func HandleFromFileSources(secret *v1.Secret, fileSources []string) error { + for _, fileSource := range fileSources { + keyName, filePath, err := ParseFileSource(fileSource) + if err != nil { + return err + } + info, err := os.Stat(filePath) + if err != nil { + switch err := err.(type) { + case *os.PathError: + return fmt.Errorf("error reading %s: %v", filePath, err.Err) + default: + return fmt.Errorf("error reading %s: %v", filePath, err) + } + } + if info.IsDir() { + if strings.Contains(fileSource, "=") { + return fmt.Errorf("cannot give a key name for a directory path.") + } + fileList, err := ioutil.ReadDir(filePath) + if err != nil { + return fmt.Errorf("error listing files in %s: %v", filePath, err) + } + for _, item := range fileList { + itemPath := path.Join(filePath, item.Name()) + if item.Mode().IsRegular() { + keyName = item.Name() + if err = addKeyFromFileToSecret(secret, keyName, itemPath); err != nil { + return err + } + } + } + } else { + if err := addKeyFromFileToSecret(secret, keyName, filePath); err != nil { + return err + } + } + } + + return nil +} + +// HandleFromEnvFileSource adds the specified env file source information +// into the provided secret +func HandleFromEnvFileSource(secret *v1.Secret, envFileSource string) error { + info, err := os.Stat(envFileSource) + if err != nil { + switch err := err.(type) { + case *os.PathError: + return fmt.Errorf("error reading %s: %v", envFileSource, err.Err) + default: + return fmt.Errorf("error reading %s: %v", envFileSource, err) + } + } + if info.IsDir() { + return fmt.Errorf("env secret file cannot be a directory") + } + + return addFromEnvFile(envFileSource, func(key, value string) error { + return addKeyFromLiteralToSecret(secret, key, []byte(value)) + }) +} + +func addKeyFromFileToSecret(secret *v1.Secret, keyName, filePath string) error { + data, err := ioutil.ReadFile(filePath) + if err != nil { + return err + } + return addKeyFromLiteralToSecret(secret, keyName, data) +} + +func addKeyFromLiteralToSecret(secret *v1.Secret, keyName string, data []byte) error { + if errs := validation.IsConfigMapKey(keyName); len(errs) != 0 { + return fmt.Errorf("%q is not a valid key name for a Secret: %s", keyName, strings.Join(errs, ";")) + } + + if _, entryExists := secret.Data[keyName]; entryExists { + return fmt.Errorf("cannot add key %s, another key by that name already exists: %v.", keyName, secret.Data) + } + secret.Data[keyName] = data + return nil +} diff --git a/configmapandsecret/util/util.go b/configmapandsecret/util/util.go new file mode 100644 index 000000000..fd1bf1364 --- /dev/null +++ b/configmapandsecret/util/util.go @@ -0,0 +1,91 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "crypto/md5" + "errors" + "fmt" + "path" + "strings" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// ParseRFC3339 parses an RFC3339 date in either RFC3339Nano or RFC3339 format. +func ParseRFC3339(s string, nowFn func() metav1.Time) (metav1.Time, error) { + if t, timeErr := time.Parse(time.RFC3339Nano, s); timeErr == nil { + return metav1.Time{Time: t}, nil + } + t, err := time.Parse(time.RFC3339, s) + if err != nil { + return metav1.Time{}, err + } + return metav1.Time{Time: t}, nil +} + +func HashObject(obj runtime.Object, codec runtime.Codec) (string, error) { + data, err := runtime.Encode(codec, obj) + if err != nil { + return "", err + } + return fmt.Sprintf("%x", md5.Sum(data)), nil +} + +// ParseFileSource parses the source given. +// +// Acceptable formats include: +// 1. source-path: the basename will become the key name +// 2. source-name=source-path: the source-name will become the key name and +// source-path is the path to the key file. +// +// Key names cannot include '='. +func ParseFileSource(source string) (keyName, filePath string, err error) { + numSeparators := strings.Count(source, "=") + switch { + case numSeparators == 0: + return path.Base(source), source, nil + case numSeparators == 1 && strings.HasPrefix(source, "="): + return "", "", fmt.Errorf("key name for file path %v missing.", strings.TrimPrefix(source, "=")) + case numSeparators == 1 && strings.HasSuffix(source, "="): + return "", "", fmt.Errorf("file path for key name %v missing.", strings.TrimSuffix(source, "=")) + case numSeparators > 1: + return "", "", errors.New("Key names or file paths cannot contain '='.") + default: + components := strings.Split(source, "=") + return components[0], components[1], nil + } +} + +// ParseLiteralSource parses the source key=val pair into its component pieces. +// This functionality is distinguished from strings.SplitN(source, "=", 2) since +// it returns an error in the case of empty keys, values, or a missing equals sign. +func ParseLiteralSource(source string) (keyName, value string, err error) { + // leading equal is invalid + if strings.Index(source, "=") == 0 { + return "", "", fmt.Errorf("invalid literal source %v, expected key=value", source) + } + // split after the first equal (so values can have the = character) + items := strings.SplitN(source, "=", 2) + if len(items) != 2 { + return "", "", fmt.Errorf("invalid literal source %v, expected key=value", source) + } + + return items[0], items[1], nil +} diff --git a/constants/constants.go b/constants/constants.go new file mode 100644 index 000000000..3a351104b --- /dev/null +++ b/constants/constants.go @@ -0,0 +1,26 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package constants + +// KustomizeFileName is the Well-Known File Name for a kubernetes app manifest. +const KustomizeSuffix = ".yaml" +const KustomizeFileName = "kustomize" + KustomizeSuffix + +// Configmap behaviors +const CreateBehavior = "create" +const ReplaceBehavior = "replace" +const MergeBehavior = "merge" diff --git a/examples/simple/instances/exampleinstance/configmap/app-init.ini b/examples/simple/instances/exampleinstance/configmap/app-init.ini new file mode 100644 index 000000000..8ebb8fcb3 --- /dev/null +++ b/examples/simple/instances/exampleinstance/configmap/app-init.ini @@ -0,0 +1,2 @@ +FOO=bar +BAR=baz diff --git a/examples/simple/instances/exampleinstance/configmap/app.env b/examples/simple/instances/exampleinstance/configmap/app.env new file mode 100644 index 000000000..c4032090a --- /dev/null +++ b/examples/simple/instances/exampleinstance/configmap/app.env @@ -0,0 +1,2 @@ +DB_USERNAME=admin +DB_PASSWORD=somepw diff --git a/examples/simple/instances/exampleinstance/deployment/deployment.yaml b/examples/simple/instances/exampleinstance/deployment/deployment.yaml new file mode 100644 index 000000000..c6a060d55 --- /dev/null +++ b/examples/simple/instances/exampleinstance/deployment/deployment.yaml @@ -0,0 +1,43 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: mungebot +spec: + replicas: 2 + template: + spec: + containers: + - name: nginx + image: nginx:1.7.9 + env: + - name: FOO + valueFrom: + configMapKeyRef: + name: app-env + key: somekey + - name: BAR + valueFrom: + secretKeyRef: + name: app-tls + key: somekey + - name: busybox + image: busybox + envFrom: + - configMapRef: + name: someConfigMap + - configMapRef: + name: app-env + - secretRef: + name: app-tls + volumeMounts: + - mountPath: /tmp/env + name: app-env + - mountPath: /tmp/tls + name: app-tls + volumes: + - configMap: + name: app-env + name: app-env + - secret: + secretName: app-tls + name: app-tls diff --git a/examples/simple/instances/exampleinstance/kustomize.yaml b/examples/simple/instances/exampleinstance/kustomize.yaml new file mode 100644 index 000000000..c9892e080 --- /dev/null +++ b/examples/simple/instances/exampleinstance/kustomize.yaml @@ -0,0 +1,35 @@ +# This example is from https://docs.google.com/document/d/1cLPGweVEYrVqQvBLJg6sxV-TrE5Rm2MNOBA_cxZP2WU/edit#heading=h.dr88tktf0e99 + +apiVersion: manifest.k8s.io/v1alpha1 +kind: Manifest +metadata: + name: test-infra-mungebot +namePrefix: test-infra- +# Labels to add to all objects and selectors. +# These labels would also be used to form the selector for apply --prune +# Named differently than “labels” to avoid confusion with metadata for this object +objectLabels: + app: mungebot + org: kubernetes + repo: test-infra +objectAnnotations: + note: This is a test annotation +bases: +- ../../package/ +#These are strategic merge patch overlays in the form of API resources +patches: +- deployment/deployment.yaml +#There could also be configmaps in Base, which would make these overlays +configMapGenerator: +- name: app-env + env: configmap/app.env +- name: app-config + files: + - configmap/app-init.ini +#There could be secrets in Base, if just using a fork/rebase workflow +secretGenerator: +- name: app-tls + commands: + tls.crt: "cat secret/tls.cert" + tls.key: "cat secret/tls.key" + type: "kubernetes.io/tls" diff --git a/examples/simple/instances/exampleinstance/secret/tls.cert b/examples/simple/instances/exampleinstance/secret/tls.cert new file mode 100644 index 000000000..039a9cadf --- /dev/null +++ b/examples/simple/instances/exampleinstance/secret/tls.cert @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIB0zCCAX2gAwIBAgIJAI/M7BYjwB+uMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTIwOTEyMjE1MjAyWhcNMTUwOTEyMjE1MjAyWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANLJ +hPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wok/4xIA+ui35/MmNa +rtNuC+BdZ1tMuVCPFZcCAwEAAaNQME4wHQYDVR0OBBYEFJvKs8RfJaXTH08W+SGv +zQyKn0H8MB8GA1UdIwQYMBaAFJvKs8RfJaXTH08W+SGvzQyKn0H8MAwGA1UdEwQF +MAMBAf8wDQYJKoZIhvcNAQEFBQADQQBJlffJHybjDGxRMqaRmDhX0+6v02TUKZsW +r5QuVbpQhH6u+0UgcW0jp9QwpxoPTLTWGXEWBBBurxFwiCBhkQ+V +-----END CERTIFICATE----- diff --git a/examples/simple/instances/exampleinstance/secret/tls.key b/examples/simple/instances/exampleinstance/secret/tls.key new file mode 100644 index 000000000..a748bb8ae --- /dev/null +++ b/examples/simple/instances/exampleinstance/secret/tls.key @@ -0,0 +1,9 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIBOwIBAAJBANLJhPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wo +k/4xIA+ui35/MmNartNuC+BdZ1tMuVCPFZcCAwEAAQJAEJ2N+zsR0Xn8/Q6twa4G +6OB1M1WO+k+ztnX/1SvNeWu8D6GImtupLTYgjZcHufykj09jiHmjHx8u8ZZB/o1N +MQIhAPW+eyZo7ay3lMz1V01WVjNKK9QSn1MJlb06h/LuYv9FAiEA25WPedKgVyCW +SmUwbPw8fnTcpqDWE3yTO3vKcebqMSsCIBF3UmVue8YU3jybC3NxuXq3wNm34R8T +xVLHwDXh/6NJAiEAl2oHGGLz64BuAfjKrqwz7qMYr9HCLIe/YsoWq/olzScCIQDi +D2lWusoe2/nEqfDVVWGWlyJ7yOmqaVm/iNUN9B2N2g== +-----END RSA PRIVATE KEY----- diff --git a/examples/simple/package/Kube-descriptor.yaml b/examples/simple/package/Kube-descriptor.yaml new file mode 100644 index 000000000..a26036374 --- /dev/null +++ b/examples/simple/package/Kube-descriptor.yaml @@ -0,0 +1,14 @@ +# This example is from https://docs.google.com/document/d/1cLPGweVEYrVqQvBLJg6sxV-TrE5Rm2MNOBA_cxZP2WU/edit#heading=h.dr88tktf0e99 + +# Inspired by https://github.com/kubernetes/helm/blob/master/docs/charts.md +# But Kubernetes API style +apiVersion: manifest.k8s.io/v1alpha1 +kind: Descriptor +metadata: + name: mungebot +description: Mungegithub package +# These are search keywords +keywords: [github, bot, kubernetes] +home: https://github.com/bgrant0607/mungebot-pkg/blob/master/README.md +sources: https://github.com/bgrant0607/mungebot-pkg +icon: https://github.com/bgrant0607/mungebot-pkg/blob/master/icon.png diff --git a/examples/simple/package/deployment/deployment.yaml b/examples/simple/package/deployment/deployment.yaml new file mode 100644 index 000000000..9582b0056 --- /dev/null +++ b/examples/simple/package/deployment/deployment.yaml @@ -0,0 +1,21 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: mungebot + labels: + app: mungebot +spec: + replicas: 1 + template: + metadata: + labels: + app: mungebot + spec: + containers: + - name: nginx + image: nginx + env: + - name: foo + value: bar + ports: + - containerPort: 80 diff --git a/examples/simple/package/kustomize.yaml b/examples/simple/package/kustomize.yaml new file mode 100644 index 000000000..e5ab6d507 --- /dev/null +++ b/examples/simple/package/kustomize.yaml @@ -0,0 +1,19 @@ +# This example is from https://docs.google.com/document/d/1cLPGweVEYrVqQvBLJg6sxV-TrE5Rm2MNOBA_cxZP2WU/edit#heading=h.dr88tktf0e99 + +# Inspired by https://github.com/kubernetes/helm/blob/master/docs/charts.md +# But Kubernetes API style +apiVersion: manifest.k8s.io/v1alpha1 +kind: Manifest +metadata: + name: mungebot +namePrefix: baseprefix- +# Labels to add to all objects and selectors. +# These labels would also be used to form the selector for apply --prune +# Named differently than “labels” to avoid confusion with metadata for this object +objectLabels: + foo: bar +objectAnnotations: + baseAnno: This is an base annotation +resources: +- deployment/deployment.yaml +- service/service.yaml diff --git a/examples/simple/package/service/service.yaml b/examples/simple/package/service/service.yaml new file mode 100644 index 000000000..e8fb0c806 --- /dev/null +++ b/examples/simple/package/service/service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: mungebot-service + labels: + app: mungebot +spec: + ports: + - port: 7002 + selector: + app: mungebot diff --git a/hash/hash.go b/hash/hash.go new file mode 100644 index 000000000..e5685280b --- /dev/null +++ b/hash/hash.go @@ -0,0 +1,110 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package hash + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + + "k8s.io/api/core/v1" +) + +// ConfigMapHash returns a hash of the ConfigMap. +// The data, Kind, and Name are taken into account. +func ConfigMapHash(cm *v1.ConfigMap) (string, error) { + encoded, err := encodeConfigMap(cm) + if err != nil { + return "", err + } + h, err := encodeHash(hash(encoded)) + if err != nil { + return "", err + } + return h, nil +} + +// SecretHash returns a hash of the Secret. +// The data, Kind, Name, and Type are taken into account. +func SecretHash(sec *v1.Secret) (string, error) { + encoded, err := encodeSecret(sec) + if err != nil { + return "", err + } + h, err := encodeHash(hash(encoded)) + if err != nil { + return "", err + } + return h, nil +} + +// encodeConfigMap encodes a ConfigMap. +// data, Kind, and Name are taken into account. +func encodeConfigMap(cm *v1.ConfigMap) (string, error) { + // json.Marshal sorts the keys in a stable order in the encoding + data, err := json.Marshal(map[string]interface{}{"kind": "ConfigMap", "name": cm.Name, "data": cm.Data}) + if err != nil { + return "", err + } + return string(data), nil +} + +// encodeSecret encodes a Secret. +// data, Kind, Name, and Type are taken into account. +func encodeSecret(sec *v1.Secret) (string, error) { + // json.Marshal sorts the keys in a stable order in the encoding + data, err := json.Marshal(map[string]interface{}{"kind": "Secret", "type": sec.Type, "name": sec.Name, "data": sec.Data}) + if err != nil { + return "", err + } + return string(data), nil +} + +// encodeHash extracts the first 40 bits of the hash from the hex string +// (1 hex char represents 4 bits), and then maps vowels and vowel-like hex +// characters to consonants to prevent bad words from being formed (the theory +// is that no vowels makes it really hard to make bad words). Since the string +// is hex, the only vowels it can contain are 'a' and 'e'. +// We picked some arbitrary consonants to map to from the same character set as GenerateName. +// See: https://github.com/kubernetes/apimachinery/blob/dc1f89aff9a7509782bde3b68824c8043a3e58cc/pkg/util/rand/rand.go#L75 +// If the hex string contains fewer than ten characters, returns an error. +func encodeHash(hex string) (string, error) { + if len(hex) < 10 { + return "", fmt.Errorf("the hex string must contain at least 10 characters") + } + enc := []rune(hex[:10]) + for i := range enc { + switch enc[i] { + case '0': + enc[i] = 'g' + case '1': + enc[i] = 'h' + case '3': + enc[i] = 'k' + case 'a': + enc[i] = 'm' + case 'e': + enc[i] = 't' + } + } + return string(enc), nil +} + +// hash hashes `data` with sha256 and returns the hex string +func hash(data string) string { + return fmt.Sprintf("%x", sha256.Sum256([]byte(data))) +} diff --git a/hash/hash_test.go b/hash/hash_test.go new file mode 100644 index 000000000..aa2d085c2 --- /dev/null +++ b/hash/hash_test.go @@ -0,0 +1,178 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package hash + +import ( + "reflect" + "strings" + "testing" + + "k8s.io/api/core/v1" +) + +func TestConfigMapHash(t *testing.T) { + cases := []struct { + desc string + cm *v1.ConfigMap + hash string + err string + }{ + // empty map + {"empty data", &v1.ConfigMap{Data: map[string]string{}}, "42745tchd9", ""}, + // one key + {"one key", &v1.ConfigMap{Data: map[string]string{"one": ""}}, "9g67k2htb6", ""}, + // three keys (tests sorting order) + {"three keys", &v1.ConfigMap{Data: map[string]string{"two": "2", "one": "", "three": "3"}}, "f5h7t85m9b", ""}, + } + + for _, c := range cases { + h, err := ConfigMapHash(c.cm) + if SkipRest(t, c.desc, err, c.err) { + continue + } + if c.hash != h { + t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h) + } + } +} + +func TestSecretHash(t *testing.T) { + cases := []struct { + desc string + secret *v1.Secret + hash string + err string + }{ + // empty map + {"empty data", &v1.Secret{Type: "my-type", Data: map[string][]byte{}}, "t75bgf6ctb", ""}, + // one key + {"one key", &v1.Secret{Type: "my-type", Data: map[string][]byte{"one": []byte("")}}, "74bd68bm66", ""}, + // three keys (tests sorting order) + {"three keys", &v1.Secret{Type: "my-type", Data: map[string][]byte{"two": []byte("2"), "one": []byte(""), "three": []byte("3")}}, "dgcb6h9tmk", ""}, + } + + for _, c := range cases { + h, err := SecretHash(c.secret) + if SkipRest(t, c.desc, err, c.err) { + continue + } + if c.hash != h { + t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h) + } + } +} + +func TestEncodeConfigMap(t *testing.T) { + cases := []struct { + desc string + cm *v1.ConfigMap + expect string + err string + }{ + // empty map + {"empty data", &v1.ConfigMap{Data: map[string]string{}}, `{"data":{},"kind":"ConfigMap","name":""}`, ""}, + // one key + {"one key", &v1.ConfigMap{Data: map[string]string{"one": ""}}, `{"data":{"one":""},"kind":"ConfigMap","name":""}`, ""}, + // three keys (tests sorting order) + {"three keys", &v1.ConfigMap{Data: map[string]string{"two": "2", "one": "", "three": "3"}}, `{"data":{"one":"","three":"3","two":"2"},"kind":"ConfigMap","name":""}`, ""}, + } + for _, c := range cases { + s, err := encodeConfigMap(c.cm) + if SkipRest(t, c.desc, err, c.err) { + continue + } + if s != c.expect { + t.Errorf("case %q, expect %q but got %q from encode %#v", c.desc, c.expect, s, c.cm) + } + } +} + +func TestEncodeSecret(t *testing.T) { + cases := []struct { + desc string + secret *v1.Secret + expect string + err string + }{ + // empty map + {"empty data", &v1.Secret{Type: "my-type", Data: map[string][]byte{}}, `{"data":{},"kind":"Secret","name":"","type":"my-type"}`, ""}, + // one key + {"one key", &v1.Secret{Type: "my-type", Data: map[string][]byte{"one": []byte("")}}, `{"data":{"one":""},"kind":"Secret","name":"","type":"my-type"}`, ""}, + // three keys (tests sorting order) - note json.Marshal base64 encodes the values because they come in as []byte + {"three keys", &v1.Secret{Type: "my-type", Data: map[string][]byte{"two": []byte("2"), "one": []byte(""), "three": []byte("3")}}, `{"data":{"one":"","three":"Mw==","two":"Mg=="},"kind":"Secret","name":"","type":"my-type"}`, ""}, + } + for _, c := range cases { + s, err := encodeSecret(c.secret) + if SkipRest(t, c.desc, err, c.err) { + continue + } + if s != c.expect { + t.Errorf("case %q, expect %q but got %q from encode %#v", c.desc, c.expect, s, c.secret) + } + } +} + +func TestHash(t *testing.T) { + // hash the empty string to be sure that sha256 is being used + expect := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + sum := hash("") + if expect != sum { + t.Errorf("expected hash %q but got %q", expect, sum) + } +} + +// warn devs who change types that they might have to update a hash function +// not perfect, as it only checks the number of top-level fields +func TestTypeStability(t *testing.T) { + errfmt := `case %q, expected %d fields but got %d +Depending on the field(s) you added, you may need to modify the hash function for this type. +To guide you: the hash function targets fields that comprise the contents of objects, +not their metadata (e.g. the data of a ConfigMap, but nothing in ObjectMeta). +` + cases := []struct { + typeName string + obj interface{} + expect int + }{ + {"ConfigMap", v1.ConfigMap{}, 3}, + {"Secret", v1.Secret{}, 5}, + } + for _, c := range cases { + val := reflect.ValueOf(c.obj) + if num := val.NumField(); c.expect != num { + t.Errorf(errfmt, c.typeName, c.expect, num) + } + } +} + +// SkipRest returns true if there was a non-nil error or if we expected an error that didn't happen, +// and logs the appropriate error on the test object. +// The return value indicates whether we should skip the rest of the test case due to the error result. +func SkipRest(t *testing.T, desc string, err error, contains string) bool { + if err != nil { + if len(contains) == 0 { + t.Errorf("case %q, expect nil error but got %q", desc, err.Error()) + } else if !strings.Contains(err.Error(), contains) { + t.Errorf("case %q, expect error to contain %q but got %q", desc, contains, err.Error()) + } + return true + } else if len(contains) > 0 { + t.Errorf("case %q, expect error to contain %q but got nil error", desc, contains) + return true + } + return false +} diff --git a/internal/error/configmaperror.go b/internal/error/configmaperror.go new file mode 100644 index 000000000..4d713bac8 --- /dev/null +++ b/internal/error/configmaperror.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 error + +import "fmt" + +type ConfigmapError struct { + ManifestFilepath string + ErrorMsg string +} + +func (e ConfigmapError) Error() string { + return fmt.Sprintf("Manifest file [%s] encounters a configmap error: %s\n", e.ManifestFilepath, e.ErrorMsg) +} diff --git a/internal/error/configmaperror_test.go b/internal/error/configmaperror_test.go new file mode 100644 index 000000000..8700efbf5 --- /dev/null +++ b/internal/error/configmaperror_test.go @@ -0,0 +1,38 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package error + +import ( + "strings" + "testing" +) + +func TestConfigmapError_Error(t *testing.T) { + filepath := "/path/to/kustomize.yaml" + errorMsg := "configmap name is missing" + me := ConfigmapError{ManifestFilepath: filepath, ErrorMsg: errorMsg} + + if !strings.Contains(me.Error(), filepath) { + t.Errorf("Incorrect ConfigmapError.Error() message \n") + t.Errorf("Expected filepath %s, but unfound\n", filepath) + } + + if !strings.Contains(me.Error(), errorMsg) { + t.Errorf("Incorrect ConfigmapError.Error() message \n") + t.Errorf("Expected errorMsg %s, but unfound\n", errorMsg) + } +} diff --git a/internal/error/manifesterror.go b/internal/error/manifesterror.go new file mode 100644 index 000000000..db6dde9b5 --- /dev/null +++ b/internal/error/manifesterror.go @@ -0,0 +1,57 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package error + +import ( + "fmt" +) + +// First pass to encapsulate fields for more informative error messages. +type ManifestError struct { + ManifestFilepath string + ErrorMsg string +} + +func (me ManifestError) Error() string { + return fmt.Sprintf("Manifest File [%s]: %s\n", me.ManifestFilepath, me.ErrorMsg) +} + +type ManifestErrors struct { + merrors []error +} + +func (me *ManifestErrors) Error() string { + errormsg := "" + for _, e := range me.merrors { + errormsg += e.Error() + "\n" + } + return errormsg +} + +func (me *ManifestErrors) Append(e error) { + me.merrors = append(me.merrors, e) +} + +func (me *ManifestErrors) Get() []error { + return me.merrors +} + +func (me *ManifestErrors) BatchAppend(e ManifestErrors) { + for _, err := range e.Get() { + me.merrors = append(me.merrors, err) + } +} diff --git a/internal/error/manifesterror_test.go b/internal/error/manifesterror_test.go new file mode 100644 index 000000000..a85ef006a --- /dev/null +++ b/internal/error/manifesterror_test.go @@ -0,0 +1,92 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package error + +import ( + "fmt" + "strings" + "testing" +) + +func TestManifestError_Error(t *testing.T) { + filepath := "/path/to/kustomize.yaml" + errorMsg := "Manifest not found" + + me := ManifestError{ManifestFilepath: filepath, ErrorMsg: errorMsg} + + if !strings.Contains(me.Error(), filepath) { + t.Errorf("Incorrect ManifestError.Error() message \n") + t.Errorf("Expected filepath %s, but unfound\n", filepath) + } + + if !strings.Contains(me.Error(), errorMsg) { + t.Errorf("Incorrect ManifestError.Error() message \n") + t.Errorf("Expected errorMsg %s, but unfound\n", errorMsg) + } + +} + +func TestManifestErrors_Error(t *testing.T) { + filepath := "/path/to/kustomize" + me := ManifestError{ManifestFilepath: filepath, ErrorMsg: "Manifest not found"} + ce := ConfigmapError{ManifestFilepath: filepath, ErrorMsg: "can't find configmap name"} + pe := PatchError{ManifestFilepath: filepath, PatchFilepath: filepath, ErrorMsg: "can't find patch file"} + re := ResourceError{ManifestFilepath: filepath, ResourceFilepath: filepath, ErrorMsg: "can't find resource file"} + se := SecretError{ManifestFilepath: filepath, ErrorMsg: "can't find secret name"} + mes := ManifestErrors{merrors: []error{me, ce, pe, re, se}} + expectedErrorMsg := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n", me.Error(), ce.Error(), pe.Error(), re.Error(), se.Error()) + if mes.Error() != expectedErrorMsg { + t.Errorf("Incorrect ManifestErrors.Error() message\n") + t.Errorf(" Expected: %s\n", expectedErrorMsg) + t.Errorf(" Got: %s\n", mes.Error()) + } +} + +func TestManifestErrors_Get(t *testing.T) { + ce := ConfigmapError{ManifestFilepath: "manifest/filepath", ErrorMsg: "can't find configmap name"} + mes := ManifestErrors{merrors: []error{ce}} + if len(mes.Get()) != 1 { + t.Errorf("Incorrect ManifestErrors.Get()\n") + t.Errorf(" Expected: %v\n", []error{ce}) + t.Errorf(" Got: %s\n", mes.Get()) + } +} + +func TestManifestErrors_Append(t *testing.T) { + ce := ConfigmapError{ManifestFilepath: "manifest/filepath", ErrorMsg: "can't find configmap name"} + pe := PatchError{ManifestFilepath: "manifest/filepath", PatchFilepath: "patch/path", ErrorMsg: "can't find patch file"} + mes := ManifestErrors{merrors: []error{ce}} + mes.Append(pe) + if len(mes.Get()) != 2 { + t.Errorf("Incorrect ManifestErrors.Append()\n") + t.Errorf(" Expected: %d error\n%v/n", 2, []error{ce, pe}) + t.Errorf(" Got: %d error\n%v\n", len(mes.Get()), mes.Get()) + } +} + +func TestManifestErrors_BatchAppend(t *testing.T) { + ce := ConfigmapError{ManifestFilepath: "manifest/filepath", ErrorMsg: "can't find configmap name"} + pe := PatchError{ManifestFilepath: "manifest/filepath", PatchFilepath: "patch/path", ErrorMsg: "can't find patch file"} + mes := ManifestErrors{merrors: []error{ce}} + me := ManifestErrors{merrors: []error{pe}} + mes.BatchAppend(me) + if len(mes.Get()) != 2 { + t.Errorf("Incorrect ManifestErrors.Append()\n") + t.Errorf(" Expected: %d error\n%v/n", 2, []error{ce, pe}) + t.Errorf(" Got: %d error\n%v\n", len(mes.Get()), mes.Get()) + } +} diff --git a/internal/error/patcherror.go b/internal/error/patcherror.go new file mode 100644 index 000000000..0b1c5a34d --- /dev/null +++ b/internal/error/patcherror.go @@ -0,0 +1,31 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package error + +import ( + "fmt" +) + +type PatchError struct { + ManifestFilepath string + PatchFilepath string + ErrorMsg string +} + +func (e PatchError) Error() string { + return fmt.Sprintf("Manifest file [%s] encounters a patch error for [%s]: %s\n", e.ManifestFilepath, e.PatchFilepath, e.ErrorMsg) +} diff --git a/internal/error/patcherror_test.go b/internal/error/patcherror_test.go new file mode 100644 index 000000000..00b5164ed --- /dev/null +++ b/internal/error/patcherror_test.go @@ -0,0 +1,41 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package error + +import ( + "strings" + "testing" +) + +func TestPatchError_Error(t *testing.T) { + filepath := "/path/to/kustomize.yaml" + patchfilepath := "/path/to/patch/patch.yaml" + errorMsg := "file not found" + me := PatchError{ManifestFilepath: filepath, PatchFilepath: patchfilepath, ErrorMsg: errorMsg} + if !strings.Contains(me.Error(), filepath) { + t.Errorf("Incorrect PatchError.Error() message \n") + t.Errorf("Expected filepath %s, but unfound\n", filepath) + } + if !strings.Contains(me.Error(), patchfilepath) { + t.Errorf("Incorrect PatchError.Error() message \n") + t.Errorf("Expected patchfilepath %s, but unfound\n", patchfilepath) + } + if !strings.Contains(me.Error(), errorMsg) { + t.Errorf("Incorrect PatchError.Error() message \n") + t.Errorf("Expected errorMsg %s, but unfound\n", errorMsg) + } +} diff --git a/internal/error/resourceerror.go b/internal/error/resourceerror.go new file mode 100644 index 000000000..0b111794b --- /dev/null +++ b/internal/error/resourceerror.go @@ -0,0 +1,30 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package error + +import "fmt" + +// First pass to encapsulate fields for more informative error messages. +type ResourceError struct { + ManifestFilepath string + ResourceFilepath string + ErrorMsg string +} + +func (e ResourceError) Error() string { + return fmt.Sprintf("Manifest file [%s] encounters a resource error for [%s]: %s\n", e.ManifestFilepath, e.ResourceFilepath, e.ErrorMsg) +} diff --git a/internal/error/resourceerror_test.go b/internal/error/resourceerror_test.go new file mode 100644 index 000000000..695b7f438 --- /dev/null +++ b/internal/error/resourceerror_test.go @@ -0,0 +1,41 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package error + +import ( + "strings" + "testing" +) + +func TestResourceError_Error(t *testing.T) { + filepath := "/path/to/kustomize.yaml" + resourcefilepath := "/path/to/resource/deployment.yaml" + errorMsg := "file not found" + me := ResourceError{ManifestFilepath: filepath, ResourceFilepath: resourcefilepath, ErrorMsg: errorMsg} + if !strings.Contains(me.Error(), filepath) { + t.Errorf("Incorrect ResourceError.Error() message \n") + t.Errorf("Expected filepath %s, but unfound\n", filepath) + } + if !strings.Contains(me.Error(), resourcefilepath) { + t.Errorf("Incorrect ResourceError.Error() message \n") + t.Errorf("Expected resourcefilepath %s, but unfound\n", resourcefilepath) + } + if !strings.Contains(me.Error(), errorMsg) { + t.Errorf("Incorrect ResourceError.Error() message \n") + t.Errorf("Expected errorMsg %s, but unfound\n", errorMsg) + } +} diff --git a/internal/error/secreterror.go b/internal/error/secreterror.go new file mode 100644 index 000000000..edcf0d4f6 --- /dev/null +++ b/internal/error/secreterror.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 error + +import "fmt" + +type SecretError struct { + ManifestFilepath string + ErrorMsg string +} + +func (e SecretError) Error() string { + return fmt.Sprintf("Manifest file [%s] encounters a secret error: %s\n", e.ManifestFilepath, e.ErrorMsg) +} diff --git a/internal/error/secreterror_test.go b/internal/error/secreterror_test.go new file mode 100644 index 000000000..b98ef7baa --- /dev/null +++ b/internal/error/secreterror_test.go @@ -0,0 +1,37 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package error + +import ( + "strings" + "testing" +) + +func TestSecretError_Error(t *testing.T) { + filepath := "/path/to/secret.yaml" + errorMsg := "missing a command" + me := SecretError{ManifestFilepath: filepath, ErrorMsg: errorMsg} + if !strings.Contains(me.Error(), filepath) { + t.Errorf("Incorrect SecretError.Error() message \n") + t.Errorf("Expected filepath %s, but unfound\n", filepath) + } + + if !strings.Contains(me.Error(), errorMsg) { + t.Errorf("Incorrect SecretError.Error() message \n") + t.Errorf("Expected errorMsg %s, but unfound\n", errorMsg) + } +} diff --git a/resource/appresource.go b/resource/appresource.go new file mode 100644 index 000000000..65f3fd456 --- /dev/null +++ b/resource/appresource.go @@ -0,0 +1,57 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resource + +import ( + "k8s.io/kubectl/pkg/loader" +) + +// NewFromResources returns a ResourceCollection given a resource path slice from manifest file. +func NewFromResources(loader loader.Loader, paths []string) (ResourceCollection, error) { + allResources := []ResourceCollection{} + for _, path := range paths { + content, err := loader.Load(path) + if err != nil { + return nil, err + } + + res, err := decodeToResourceCollection(content) + if err != nil { + return nil, err + } + allResources = append(allResources, res) + } + return Merge(allResources...) +} + +// NewFromPatches returns a slice of Resources given a patch path slice from manifest file. +func NewFromPatches(loader loader.Loader, paths []string) ([]*Resource, error) { + allResources := []*Resource{} + for _, path := range paths { + content, err := loader.Load(path) + if err != nil { + return nil, err + } + + res, err := decode(content) + if err != nil { + return nil, err + } + allResources = append(allResources, res...) + } + return allResources, nil +} diff --git a/resource/appresource_test.go b/resource/appresource_test.go new file mode 100644 index 000000000..df10756a9 --- /dev/null +++ b/resource/appresource_test.go @@ -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 +} diff --git a/resource/configmap.go b/resource/configmap.go new file mode 100644 index 000000000..53d00da10 --- /dev/null +++ b/resource/configmap.go @@ -0,0 +1,144 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resource + +import ( + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/validation" + manifest "k8s.io/kubectl/pkg/apis/manifest/v1alpha1" + cutil "k8s.io/kubectl/pkg/kustomize/configmapandsecret/util" + "k8s.io/kubectl/pkg/loader" +) + +func newFromConfigMap(l loader.Loader, cm manifest.ConfigMapArgs) (*Resource, error) { + corev1CM, err := makeConfigMap(l, cm) + if err != nil { + return nil, err + } + + data, err := objectToUnstructured(corev1CM) + if err != nil { + return nil, err + } + return &Resource{Data: data, Behavior: cm.Behavior}, nil +} + +func makeConfigMap(l loader.Loader, cm manifest.ConfigMapArgs) (*corev1.ConfigMap, error) { + var envPairs, literalPairs, filePairs []kvPair + var err error + + corev1cm := &corev1.ConfigMap{} + corev1cm.APIVersion = "v1" + corev1cm.Kind = "ConfigMap" + corev1cm.Name = cm.Name + corev1cm.Data = map[string]string{} + + if cm.EnvSource != "" { + envPairs, err = keyValuesFromEnvFile(l, cm.EnvSource) + if err != nil { + return nil, fmt.Errorf("error reading keys from env source file: %s %v", cm.EnvSource, err) + } + } + + literalPairs, err = keyValuesFromLiteralSources(cm.LiteralSources) + if err != nil { + return nil, fmt.Errorf("error reading key values from literal sources: %v", err) + } + + filePairs, err = keyValuesFromFileSources(l, cm.FileSources) + if err != nil { + return nil, fmt.Errorf("error reading key values from file sources: %v", err) + } + + allPairs := append(append(envPairs, literalPairs...), filePairs...) + + // merge key value pairs from all the sources + for _, kv := range allPairs { + err = addKV(corev1cm.Data, kv) + if err != nil { + return nil, fmt.Errorf("error adding key in configmap: %v", err) + } + } + + return corev1cm, nil +} + +func keyValuesFromEnvFile(l loader.Loader, path string) ([]kvPair, error) { + content, err := l.Load(path) + if err != nil { + return nil, err + } + return keyValuesFromLines(content) +} + +func keyValuesFromLiteralSources(sources []string) ([]kvPair, error) { + var kvs []kvPair + for _, s := range sources { + // TODO: move ParseLiteralSource in this file + k, v, err := cutil.ParseLiteralSource(s) + if err != nil { + return nil, err + } + kvs = append(kvs, kvPair{key: k, value: v}) + } + return kvs, nil +} + +func keyValuesFromFileSources(l loader.Loader, sources []string) ([]kvPair, error) { + var kvs []kvPair + + for _, s := range sources { + key, path, err := cutil.ParseFileSource(s) + if err != nil { + return nil, err + } + fileContent, err := l.Load(path) + if err != nil { + return nil, err + } + kvs = append(kvs, kvPair{key: key, value: string(fileContent)}) + } + return kvs, nil +} + +// addKV adds key-value pair to the provided map. +func addKV(m map[string]string, kv kvPair) error { + if errs := validation.IsConfigMapKey(kv.key); len(errs) != 0 { + return fmt.Errorf("%q is not a valid key name: %s", kv.key, strings.Join(errs, ";")) + } + if _, exists := m[kv.key]; exists { + return fmt.Errorf("key %s already exists: %v.", kv.key, m) + } + m[kv.key] = kv.value + return nil +} + +// NewFromConfigMaps returns a Resource slice given a configmap metadata slice from manifest file. +func NewFromConfigMaps(loader loader.Loader, cmList []manifest.ConfigMapArgs) (ResourceCollection, error) { + allResources := []*Resource{} + for _, cm := range cmList { + res, err := newFromConfigMap(loader, cm) + if err != nil { + return nil, err + } + allResources = append(allResources, res) + } + return resourceCollectionFromResources(allResources) +} diff --git a/resource/configmap_test.go b/resource/configmap_test.go new file mode 100644 index 000000000..4fbc53480 --- /dev/null +++ b/resource/configmap_test.go @@ -0,0 +1,158 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resource_test + +import ( + "reflect" + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + manifest "k8s.io/kubectl/pkg/apis/manifest/v1alpha1" + "k8s.io/kubectl/pkg/kustomize/resource" + "k8s.io/kubectl/pkg/loader/loadertest" +) + +func TestNewFromConfigMaps(t *testing.T) { + type testCase struct { + description string + input []manifest.ConfigMapArgs + filepath string + content string + expected resource.ResourceCollection + } + + l := loadertest.NewFakeLoader("/home/seans/project/") + testCases := []testCase{ + { + description: "construct config map from env", + input: []manifest.ConfigMapArgs{ + { + Name: "envConfigMap", + DataSources: manifest.DataSources{ + EnvSource: "app.env", + }, + }, + }, + filepath: "/home/seans/project/app.env", + content: "DB_USERNAME=admin\nDB_PASSWORD=somepw", + expected: resource.ResourceCollection{ + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, + Name: "envConfigMap", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "envConfigMap", + "creationTimestamp": nil, + }, + "data": map[string]interface{}{ + "DB_USERNAME": "admin", + "DB_PASSWORD": "somepw", + }, + }, + }, + }, + }, + }, + { + description: "construct config map from file", + input: []manifest.ConfigMapArgs{{ + Name: "fileConfigMap", + DataSources: manifest.DataSources{ + FileSources: []string{"app-init.ini"}, + }, + }, + }, + filepath: "/home/seans/project/app-init.ini", + content: "FOO=bar\nBAR=baz\n", + expected: resource.ResourceCollection{ + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, + Name: "fileConfigMap", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "fileConfigMap", + "creationTimestamp": nil, + }, + "data": map[string]interface{}{ + "app-init.ini": `FOO=bar +BAR=baz +`, + }, + }, + }, + }, + }, + }, + { + description: "construct config map from literal", + input: []manifest.ConfigMapArgs{ + { + Name: "literalConfigMap", + DataSources: manifest.DataSources{ + LiteralSources: []string{"a=x", "b=y"}, + }, + }, + }, + expected: resource.ResourceCollection{ + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, + Name: "literalConfigMap", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "literalConfigMap", + "creationTimestamp": nil, + }, + "data": map[string]interface{}{ + "a": "x", + "b": "y", + }, + }, + }, + }, + }, + }, + // TODO: add testcase for data coming from multiple sources like + // files/literal/env etc. + } + + for _, tc := range testCases { + + if ferr := l.AddFile(tc.filepath, []byte(tc.content)); ferr != nil { + t.Fatalf("Error adding fake file: %v\n", ferr) + } + r, err := resource.NewFromConfigMaps(l, tc.input) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(r, tc.expected) { + t.Fatalf("in testcase: %q got:\n%+v\n expected:\n%+v\n", tc.description, r, tc.expected) + } + } +} diff --git a/resource/kv.go b/resource/kv.go new file mode 100644 index 000000000..25c546816 --- /dev/null +++ b/resource/kv.go @@ -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 +} diff --git a/resource/kv_test.go b/resource/kv_test.go new file mode 100644 index 000000000..dd423b0e6 --- /dev/null +++ b/resource/kv_test.go @@ -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) + } + + } +} diff --git a/resource/resource.go b/resource/resource.go new file mode 100644 index 000000000..8be7552f5 --- /dev/null +++ b/resource/resource.go @@ -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 +} diff --git a/resource/secret.go b/resource/secret.go new file mode 100644 index 000000000..f97229751 --- /dev/null +++ b/resource/secret.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 resource + +import ( + "context" + "os" + "os/exec" + "path/filepath" + "time" + + corev1 "k8s.io/api/core/v1" + manifest "k8s.io/kubectl/pkg/apis/manifest/v1alpha1" +) + +func newFromSecretGenerator(p string, s manifest.SecretArgs) (*Resource, error) { + corev1secret := &corev1.Secret{} + corev1secret.APIVersion = "v1" + corev1secret.Kind = "Secret" + corev1secret.Name = s.Name + corev1secret.Type = corev1.SecretType(s.Type) + if corev1secret.Type == "" { + corev1secret.Type = corev1.SecretTypeOpaque + } + corev1secret.Data = map[string][]byte{} + + for k, v := range s.Commands { + out, err := createSecretKey(p, v) + if err != nil { + return nil, err + } + corev1secret.Data[k] = out + } + + obj, err := objectToUnstructured(corev1secret) + + if err != nil { + return nil, err + } + + return &Resource{Data: obj, Behavior: s.Behavior}, nil +} + +func createSecretKey(wd string, command string) ([]byte, error) { + fi, err := os.Stat(wd) + if err != nil || !fi.IsDir() { + wd = filepath.Dir(wd) + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, "sh", "-c", command) + cmd.Dir = wd + + return cmd.Output() +} + +// NewFromSecretGenerators takes a SecretGenerator slice and executes its command in directory p +// then writes the output to a Resource slice and return it. +func NewFromSecretGenerators(p string, secretList []manifest.SecretArgs) (ResourceCollection, error) { + allResources := []*Resource{} + for _, secret := range secretList { + res, err := newFromSecretGenerator(p, secret) + if err != nil { + return nil, err + } + allResources = append(allResources, res) + } + return resourceCollectionFromResources(allResources) +} diff --git a/resource/secret_test.go b/resource/secret_test.go new file mode 100644 index 000000000..50b8b001a --- /dev/null +++ b/resource/secret_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 resource + +import ( + "encoding/base64" + "reflect" + "testing" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + manifest "k8s.io/kubectl/pkg/apis/manifest/v1alpha1" +) + +func TestNewFromSecretGenerators(t *testing.T) { + secrets := []manifest.SecretArgs{ + { + Name: "secret", + Commands: map[string]string{ + "DB_USERNAME": "printf admin", + "DB_PASSWORD": "printf somepw", + }, + Type: "Opaque", + }, + } + re, err := NewFromSecretGenerators(".", secrets) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + expected := ResourceCollection{ + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "Secret"}, + Name: "secret", + }: &Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Secret", + "metadata": map[string]interface{}{ + "name": "secret", + "creationTimestamp": nil, + }, + "type": string(corev1.SecretTypeOpaque), + "data": map[string]interface{}{ + "DB_USERNAME": base64.StdEncoding.EncodeToString([]byte("admin")), + "DB_PASSWORD": base64.StdEncoding.EncodeToString([]byte("somepw")), + }, + }, + }, + }, + } + + if !reflect.DeepEqual(re, expected) { + t.Fatalf("%#v\ndoesn't match expected:\n%#v", re, expected) + } +} diff --git a/resource/util.go b/resource/util.go new file mode 100644 index 000000000..9ee7fd6b5 --- /dev/null +++ b/resource/util.go @@ -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 +} diff --git a/resource/util_test.go b/resource/util_test.go new file mode 100644 index 000000000..8968fde41 --- /dev/null +++ b/resource/util_test.go @@ -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) + } +} diff --git a/transformers/labelsandannotations.go b/transformers/labelsandannotations.go new file mode 100644 index 000000000..d392a0f87 --- /dev/null +++ b/transformers/labelsandannotations.go @@ -0,0 +1,85 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package transformers + +import ( + "errors" + "fmt" + + "k8s.io/kubectl/pkg/kustomize/resource" + "k8s.io/kubectl/pkg/kustomize/types" +) + +// mapTransformer contains a map string->string and path configs +// The map will be applied to the fields specified in path configs. +type mapTransformer struct { + m map[string]string + pathConfigs []PathConfig +} + +var _ Transformer = &mapTransformer{} + +// NewDefaultingLabelsMapTransformer construct a mapTransformer with defaultLabelsPathConfigs. +func NewDefaultingLabelsMapTransformer(m map[string]string) (Transformer, error) { + return NewMapTransformer(defaultLabelsPathConfigs, m) +} + +// NewDefaultingAnnotationsMapTransformer construct a mapTransformer with defaultAnnotationsPathConfigs. +func NewDefaultingAnnotationsMapTransformer(m map[string]string) (Transformer, error) { + return NewMapTransformer(defaultAnnotationsPathConfigs, m) +} + +// NewMapTransformer construct a mapTransformer. +func NewMapTransformer(pc []PathConfig, m map[string]string) (Transformer, error) { + if m == nil { + return NewNoOpTransformer(), nil + } + if pc == nil { + return nil, errors.New("pathConfigs is not expected to be nil") + } + return &mapTransformer{pathConfigs: pc, m: m}, nil +} + +// Transform apply each pair in the mapTransformer to the +// fields specified in mapTransformer. +func (o *mapTransformer) Transform(m resource.ResourceCollection) error { + for gvkn := range m { + obj := m[gvkn].Data + objMap := obj.UnstructuredContent() + for _, path := range o.pathConfigs { + if !types.SelectByGVK(gvkn.GVK, path.GroupVersionKind) { + continue + } + err := mutateField(objMap, path.Path, path.CreateIfNotPresent, o.addMap) + if err != nil { + return err + } + } + } + return nil +} + +func (o *mapTransformer) addMap(in interface{}) (interface{}, error) { + m, ok := in.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("%#v is expectd to be %T", in, m) + } + for k, v := range o.m { + m[k] = v + } + return m, nil +} diff --git a/transformers/labelsandannotations_test.go b/transformers/labelsandannotations_test.go new file mode 100644 index 000000000..dc972f9df --- /dev/null +++ b/transformers/labelsandannotations_test.go @@ -0,0 +1,455 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package transformers + +import ( + "reflect" + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kubectl/pkg/kustomize/resource" +) + +func TestLabelsRun(t *testing.T) { + m := resource.ResourceCollection{ + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, + Name: "cm1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "cm1", + }, + }, + }, + }, + { + GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + Name: "deploy1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "group": "apps", + "apiVersion": "v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "deploy1", + }, + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "old-label": "old-value", + }, + }, + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "nginx", + "image": "nginx:1.7.9", + }, + }, + }, + }, + }, + }, + }, + }, + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "Service"}, + Name: "svc1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Service", + "metadata": map[string]interface{}{ + "name": "svc1", + }, + "spec": map[string]interface{}{ + "ports": []interface{}{ + map[string]interface{}{ + "name": "port1", + "port": "12345", + }, + }, + }, + }, + }, + }, + } + expected := resource.ResourceCollection{ + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, + Name: "cm1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "cm1", + "labels": map[string]interface{}{ + "label-key1": "label-value1", + "label-key2": "label-value2", + }, + }, + }, + }, + }, + { + GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + Name: "deploy1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "group": "apps", + "apiVersion": "v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "deploy1", + "labels": map[string]interface{}{ + "label-key1": "label-value1", + "label-key2": "label-value2", + }, + }, + "spec": map[string]interface{}{ + "selector": map[string]interface{}{ + "matchLabels": map[string]interface{}{ + "label-key1": "label-value1", + "label-key2": "label-value2", + }, + }, + "template": map[string]interface{}{ + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "old-label": "old-value", + "label-key1": "label-value1", + "label-key2": "label-value2", + }, + }, + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "nginx", + "image": "nginx:1.7.9", + }, + }, + }, + }, + }, + }, + }, + }, + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "Service"}, + Name: "svc1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Service", + "metadata": map[string]interface{}{ + "name": "svc1", + "labels": map[string]interface{}{ + "label-key1": "label-value1", + "label-key2": "label-value2", + }, + }, + "spec": map[string]interface{}{ + "ports": []interface{}{ + map[string]interface{}{ + "name": "port1", + "port": "12345", + }, + }, + "selector": map[string]interface{}{ + "label-key1": "label-value1", + "label-key2": "label-value2", + }, + }, + }, + }, + }, + } + + lt, err := NewDefaultingLabelsMapTransformer(map[string]string{"label-key1": "label-value1", "label-key2": "label-value2"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + err = lt.Transform(m) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(m, expected) { + err = compareMap(m, expected) + t.Fatalf("actual doesn't match expected: %v", err) + } +} + +func makeAnnotatededConfigMap() *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "cm1", + "annotations": map[string]interface{}{ + "anno-key1": "anno-value1", + "anno-key2": "anno-value2", + }, + }, + }, + } +} + +func makeAnnotatededDeployment() *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "group": "apps", + "apiVersion": "v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "deploy1", + "annotations": map[string]interface{}{ + "anno-key1": "anno-value1", + "anno-key2": "anno-value2", + }, + }, + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{ + "anno-key1": "anno-value1", + "anno-key2": "anno-value2", + }, + "labels": map[string]interface{}{ + "old-label": "old-value", + }, + }, + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "nginx", + "image": "nginx:1.7.9", + }, + }, + }, + }, + }, + }, + } +} + +func makeAnnotatededService() *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Service", + "metadata": map[string]interface{}{ + "name": "svc1", + "annotations": map[string]interface{}{ + "anno-key1": "anno-value1", + "anno-key2": "anno-value2", + }, + }, + "spec": map[string]interface{}{ + "ports": []interface{}{ + map[string]interface{}{ + "name": "port1", + "port": "12345", + }, + }, + }, + }, + } +} + +func TestAnnotationsRun(t *testing.T) { + m := resource.ResourceCollection{ + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, + Name: "cm1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "cm1", + }, + }, + }, + }, + { + GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + Name: "deploy1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "group": "apps", + "apiVersion": "v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "deploy1", + }, + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "old-label": "old-value", + }, + }, + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "nginx", + "image": "nginx:1.7.9", + }, + }, + }, + }, + }, + }, + }, + }, + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "Service"}, + Name: "svc1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Service", + "metadata": map[string]interface{}{ + "name": "svc1", + }, + "spec": map[string]interface{}{ + "ports": []interface{}{ + map[string]interface{}{ + "name": "port1", + "port": "12345", + }, + }, + }, + }, + }, + }, + } + expected := resource.ResourceCollection{ + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, + Name: "cm1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "cm1", + "annotations": map[string]interface{}{ + "anno-key1": "anno-value1", + "anno-key2": "anno-value2", + }, + }, + }, + }, + }, + { + GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + Name: "deploy1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "group": "apps", + "apiVersion": "v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "deploy1", + "annotations": map[string]interface{}{ + "anno-key1": "anno-value1", + "anno-key2": "anno-value2", + }, + }, + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{ + "anno-key1": "anno-value1", + "anno-key2": "anno-value2", + }, + "labels": map[string]interface{}{ + "old-label": "old-value", + }, + }, + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "nginx", + "image": "nginx:1.7.9", + }, + }, + }, + }, + }, + }, + }, + }, + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "Service"}, + Name: "svc1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Service", + "metadata": map[string]interface{}{ + "name": "svc1", + "annotations": map[string]interface{}{ + "anno-key1": "anno-value1", + "anno-key2": "anno-value2", + }, + }, + "spec": map[string]interface{}{ + "ports": []interface{}{ + map[string]interface{}{ + "name": "port1", + "port": "12345", + }, + }, + }, + }, + }, + }, + } + at, err := NewDefaultingAnnotationsMapTransformer(map[string]string{"anno-key1": "anno-value1", "anno-key2": "anno-value2"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + err = at.Transform(m) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(m, expected) { + err = compareMap(m, expected) + t.Fatalf("actual doesn't match expected: %v", err) + } +} diff --git a/transformers/labelsandannotationsconfig.go b/transformers/labelsandannotationsconfig.go new file mode 100644 index 000000000..d64d7e041 --- /dev/null +++ b/transformers/labelsandannotationsconfig.go @@ -0,0 +1,159 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package transformers + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// defaultLabelsPathConfigs is the default configuration for mutating labels and +// selector fields for native k8s APIs. +var defaultLabelsPathConfigs = []PathConfig{ + { + Path: []string{"metadata", "labels"}, + CreateIfNotPresent: true, + }, + { + GroupVersionKind: &schema.GroupVersionKind{Version: "v1", Kind: "Service"}, + Path: []string{"spec", "selector"}, + CreateIfNotPresent: true, + }, + { + GroupVersionKind: &schema.GroupVersionKind{Version: "v1", Kind: "ReplicationController"}, + Path: []string{"spec", "selector"}, + CreateIfNotPresent: true, + }, + { + GroupVersionKind: &schema.GroupVersionKind{Version: "v1", Kind: "ReplicationController"}, + Path: []string{"spec", "template", "metadata", "labels"}, + CreateIfNotPresent: true, + }, + { + GroupVersionKind: &schema.GroupVersionKind{Kind: "Deployment"}, + Path: []string{"spec", "selector", "matchLabels"}, + CreateIfNotPresent: true, + }, + { + GroupVersionKind: &schema.GroupVersionKind{Kind: "Deployment"}, + Path: []string{"spec", "template", "metadata", "labels"}, + CreateIfNotPresent: true, + }, + { + GroupVersionKind: &schema.GroupVersionKind{Kind: "ReplicaSet"}, + Path: []string{"spec", "selector", "matchLabels"}, + CreateIfNotPresent: true, + }, + { + GroupVersionKind: &schema.GroupVersionKind{Kind: "ReplicaSet"}, + Path: []string{"spec", "template", "metadata", "labels"}, + CreateIfNotPresent: true, + }, + { + GroupVersionKind: &schema.GroupVersionKind{Kind: "DaemonSet"}, + Path: []string{"spec", "selector", "matchLabels"}, + CreateIfNotPresent: true, + }, + { + GroupVersionKind: &schema.GroupVersionKind{Kind: "DaemonSet"}, + Path: []string{"spec", "template", "metadata", "labels"}, + CreateIfNotPresent: true, + }, + { + GroupVersionKind: &schema.GroupVersionKind{Group: "apps", Kind: "StatefulSet"}, + Path: []string{"spec", "selector", "matchLabels"}, + CreateIfNotPresent: true, + }, + { + GroupVersionKind: &schema.GroupVersionKind{Group: "apps", Kind: "StatefulSet"}, + Path: []string{"spec", "template", "metadata", "labels"}, + CreateIfNotPresent: true, + }, + { + GroupVersionKind: &schema.GroupVersionKind{Group: "batch", Kind: "Job"}, + Path: []string{"spec", "selector", "matchLabels"}, + CreateIfNotPresent: true, + }, + { + GroupVersionKind: &schema.GroupVersionKind{Group: "batch", Kind: "Job"}, + Path: []string{"spec", "template", "metadata", "labels"}, + CreateIfNotPresent: true, + }, + { + GroupVersionKind: &schema.GroupVersionKind{Group: "batch", Kind: "CronJob"}, + Path: []string{"spec", "jobTemplate", "spec", "selector", "matchLabels"}, + CreateIfNotPresent: true, + }, + { + GroupVersionKind: &schema.GroupVersionKind{Group: "batch", Kind: "CronJob"}, + Path: []string{"spec", "jobTemplate", "spec", "metadata", "labels"}, + CreateIfNotPresent: true, + }, + { + GroupVersionKind: &schema.GroupVersionKind{Group: "batch", Kind: "CronJob"}, + Path: []string{"spec", "jobTemplate", "spec", "template", "metadata", "labels"}, + CreateIfNotPresent: true, + }, +} + +// defaultLabelsPathConfigs is the default configuration for mutating annotations +// fields for native k8s APIs. +var defaultAnnotationsPathConfigs = []PathConfig{ + { + Path: []string{"metadata", "annotations"}, + CreateIfNotPresent: true, + }, + { + GroupVersionKind: &schema.GroupVersionKind{Version: "v1", Kind: "ReplicationController"}, + Path: []string{"spec", "template", "metadata", "annotations"}, + CreateIfNotPresent: true, + }, + { + GroupVersionKind: &schema.GroupVersionKind{Kind: "Deployment"}, + Path: []string{"spec", "template", "metadata", "annotations"}, + CreateIfNotPresent: true, + }, + { + GroupVersionKind: &schema.GroupVersionKind{Kind: "ReplicaSet"}, + Path: []string{"spec", "template", "metadata", "annotations"}, + CreateIfNotPresent: true, + }, + { + GroupVersionKind: &schema.GroupVersionKind{Kind: "DaemonSet"}, + Path: []string{"spec", "template", "metadata", "annotations"}, + CreateIfNotPresent: true, + }, + { + GroupVersionKind: &schema.GroupVersionKind{Group: "apps", Kind: "StatefulSet"}, + Path: []string{"spec", "template", "metadata", "annotations"}, + CreateIfNotPresent: true, + }, + { + GroupVersionKind: &schema.GroupVersionKind{Group: "batch", Kind: "Job"}, + Path: []string{"spec", "template", "metadata", "annotations"}, + CreateIfNotPresent: true, + }, + { + GroupVersionKind: &schema.GroupVersionKind{Group: "batch", Kind: "CronJob"}, + Path: []string{"spec", "jobTemplate", "metadata", "annotations"}, + CreateIfNotPresent: true, + }, + { + GroupVersionKind: &schema.GroupVersionKind{Group: "batch", Kind: "CronJob"}, + Path: []string{"spec", "jobTemplate", "spec", "template", "metadata", "annotations"}, + CreateIfNotPresent: true, + }, +} diff --git a/transformers/multitransformer.go b/transformers/multitransformer.go new file mode 100644 index 000000000..03e646749 --- /dev/null +++ b/transformers/multitransformer.go @@ -0,0 +1,45 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package transformers + +import "k8s.io/kubectl/pkg/kustomize/resource" + +// multiTransformer contains a list of transformers. +type multiTransformer struct { + transformers []Transformer +} + +var _ Transformer = &multiTransformer{} + +// NewMultiTransformer constructs a multiTransformer. +func NewMultiTransformer(t []Transformer) Transformer { + r := &multiTransformer{ + transformers: make([]Transformer, len(t))} + copy(r.transformers, t) + return r +} + +// Transform prepends the name prefix. +func (o *multiTransformer) Transform(m resource.ResourceCollection) error { + for _, t := range o.transformers { + err := t.Transform(m) + if err != nil { + return err + } + } + return nil +} diff --git a/transformers/namehash.go b/transformers/namehash.go new file mode 100644 index 000000000..a783558fa --- /dev/null +++ b/transformers/namehash.go @@ -0,0 +1,104 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package transformers + +import ( + "encoding/json" + "fmt" + + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kubectl/pkg/kustomize/hash" + "k8s.io/kubectl/pkg/kustomize/resource" + "k8s.io/kubectl/pkg/kustomize/types" +) + +// nameHashTransformer contains the prefix and the path config for each field that +// the name prefix will be applied. +type nameHashTransformer struct{} + +var _ Transformer = &nameHashTransformer{} + +// NewNameHashTransformer construct a nameHashTransformer. +func NewNameHashTransformer() Transformer { + return &nameHashTransformer{} +} + +// Transform appends hash to configmaps and secrets. +func (o *nameHashTransformer) Transform(m resource.ResourceCollection) error { + for gvkn, obj := range m { + switch { + case types.SelectByGVK(gvkn.GVK, &schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}): + appendHashForConfigMap(obj.Data) + + case types.SelectByGVK(gvkn.GVK, &schema.GroupVersionKind{Version: "v1", Kind: "Secret"}): + appendHashForSecret(obj.Data) + } + } + return nil +} + +func appendHashForConfigMap(obj *unstructured.Unstructured) error { + cm, err := unstructuredToConfigmap(obj) + if err != nil { + return err + } + h, err := hash.ConfigMapHash(cm) + if err != nil { + return err + } + nameWithHash := fmt.Sprintf("%s-%s", obj.GetName(), h) + obj.SetName(nameWithHash) + return nil +} + +// TODO: Remove this function after we support hash unstructured objects +func unstructuredToConfigmap(in *unstructured.Unstructured) (*v1.ConfigMap, error) { + marshaled, err := json.Marshal(in) + if err != nil { + return nil, err + } + var out v1.ConfigMap + err = json.Unmarshal(marshaled, &out) + return &out, err +} + +func appendHashForSecret(obj *unstructured.Unstructured) error { + secret, err := unstructuredToSecret(obj) + if err != nil { + return err + } + h, err := hash.SecretHash(secret) + if err != nil { + return err + } + nameWithHash := fmt.Sprintf("%s-%s", obj.GetName(), h) + obj.SetName(nameWithHash) + return nil +} + +// TODO: Remove this function after we support hash unstructured objects +func unstructuredToSecret(in *unstructured.Unstructured) (*v1.Secret, error) { + marshaled, err := json.Marshal(in) + if err != nil { + return nil, err + } + var out v1.Secret + err = json.Unmarshal(marshaled, &out) + return &out, err +} diff --git a/transformers/namehash_test.go b/transformers/namehash_test.go new file mode 100644 index 000000000..83a2cc21a --- /dev/null +++ b/transformers/namehash_test.go @@ -0,0 +1,206 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package transformers + +import ( + "reflect" + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kubectl/pkg/kustomize/resource" +) + +func TestNameHashTransformer(t *testing.T) { + objs := resource.ResourceCollection{ + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, + Name: "cm1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "cm1", + }, + }, + }, + }, + { + GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + Name: "deploy1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "group": "apps", + "apiVersion": "v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "deploy1", + }, + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "old-label": "old-value", + }, + }, + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "nginx", + "image": "nginx:1.7.9", + }, + }, + }, + }, + }, + }, + }, + }, + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "Service"}, + Name: "svc1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Service", + "metadata": map[string]interface{}{ + "name": "svc1", + }, + "spec": map[string]interface{}{ + "ports": []interface{}{ + map[string]interface{}{ + "name": "port1", + "port": "12345", + }, + }, + }, + }, + }, + }, + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "Secret"}, + Name: "secret1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Secret", + "metadata": map[string]interface{}{ + "name": "secret1", + }, + }, + }, + }, + } + + expected := resource.ResourceCollection{ + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, + Name: "cm1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "cm1-m462kdfb68", + }, + }, + }, + }, + { + GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + Name: "deploy1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "group": "apps", + "apiVersion": "v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "deploy1", + }, + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "old-label": "old-value", + }, + }, + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "nginx", + "image": "nginx:1.7.9", + }, + }, + }, + }, + }, + }, + }, + }, + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "Service"}, + Name: "svc1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Service", + "metadata": map[string]interface{}{ + "name": "svc1", + }, + "spec": map[string]interface{}{ + "ports": []interface{}{ + map[string]interface{}{ + "name": "port1", + "port": "12345", + }, + }, + }, + }, + }, + }, + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "Secret"}, + Name: "secret1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Secret", + "metadata": map[string]interface{}{ + "name": "secret1-7kc45hd5f7", + }, + }, + }, + }, + } + + tran := NewNameHashTransformer() + tran.Transform(objs) + + if !reflect.DeepEqual(objs, expected) { + err := compareMap(objs, expected) + t.Fatalf("actual doesn't match expected: %v", err) + } +} diff --git a/transformers/namereference.go b/transformers/namereference.go new file mode 100644 index 000000000..870112f87 --- /dev/null +++ b/transformers/namereference.go @@ -0,0 +1,110 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package transformers + +import ( + "errors" + "fmt" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kubectl/pkg/kustomize/resource" + "k8s.io/kubectl/pkg/kustomize/types" +) + +// nameReferenceTransformer contains the referencing info between 2 GroupVersionKinds +type nameReferenceTransformer struct { + pathConfigs []referencePathConfig +} + +var _ Transformer = &nameReferenceTransformer{} + +// NewDefaultingNameReferenceTransformer constructs a nameReferenceTransformer +// with defaultNameReferencepathConfigs. +func NewDefaultingNameReferenceTransformer() (Transformer, error) { + return NewNameReferenceTransformer(defaultNameReferencePathConfigs) +} + +// NewNameReferenceTransformer construct a nameReferenceTransformer. +func NewNameReferenceTransformer(pc []referencePathConfig) (Transformer, error) { + if pc == nil { + return nil, errors.New("pathConfigs is not expected to be nil") + } + return &nameReferenceTransformer{pathConfigs: pc}, nil +} + +// Transform does the fields update according to pathConfigs. +// The old name is in the key in the map and the new name is in the object +// associated with the key. e.g. if is one of the key-value pair in the map, +// then the old name is k.Name and the new name is v.GetName() +func (o *nameReferenceTransformer) Transform( + m resource.ResourceCollection) error { + for GVKn := range m { + obj := m[GVKn].Data + objMap := obj.UnstructuredContent() + for _, referencePathConfig := range o.pathConfigs { + for _, path := range referencePathConfig.pathConfigs { + if !types.SelectByGVK(GVKn.GVK, path.GroupVersionKind) { + continue + } + err := mutateField(objMap, path.Path, path.CreateIfNotPresent, + o.updateNameReference(referencePathConfig.referencedGVK, m)) + if err != nil { + return err + } + } + } + } + return nil +} + +// noMatchingGVKNError indicates failing to find a gvkn.GroupVersionKindName. +type noMatchingGVKNError struct { + message string +} + +// newNoMatchingGVKNError constructs an instance of noMatchingGVKNError with +// a given error message. +func newNoMatchingGVKNError(errMsg string) noMatchingGVKNError { + return noMatchingGVKNError{errMsg} +} + +// Error returns the error in string format. +func (err noMatchingGVKNError) Error() string { + return err.message +} + +func (o *nameReferenceTransformer) updateNameReference( + GVK schema.GroupVersionKind, + m resource.ResourceCollection, +) func(in interface{}) (interface{}, error) { + return func(in interface{}) (interface{}, error) { + s, ok := in.(string) + if !ok { + return nil, fmt.Errorf("%#v is expectd to be %T", in, s) + } + + for GVKn, obj := range m { + if !types.SelectByGVK(GVKn.GVK, &GVK) { + continue + } + if GVKn.Name == s { + return obj.Data.GetName(), nil + } + } + return in, nil + } +} diff --git a/transformers/namereference_test.go b/transformers/namereference_test.go new file mode 100644 index 000000000..97b284a97 --- /dev/null +++ b/transformers/namereference_test.go @@ -0,0 +1,238 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package transformers + +import ( + "reflect" + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kubectl/pkg/kustomize/resource" +) + +func TestNameReferenceRun(t *testing.T) { + m := resource.ResourceCollection{ + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, + Name: "cm1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "someprefix-cm1-somehash", + }, + }, + }, + }, + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "Secret"}, + Name: "secret1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Secret", + "metadata": map[string]interface{}{ + "name": "someprefix-secret1-somehash", + }, + }, + }, + }, + { + GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + Name: "deploy1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "group": "apps", + "apiVersion": "v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "deploy1", + }, + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "nginx", + "image": "nginx:1.7.9", + "env": []interface{}{ + map[string]interface{}{ + "name": "CM_FOO", + "valueFrom": map[string]interface{}{ + "configMapKeyRef": map[string]interface{}{ + "name": "cm1", + "key": "somekey", + }, + }, + }, + map[string]interface{}{ + "name": "SECRET_FOO", + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "name": "secret1", + "key": "somekey", + }, + }, + }, + }, + "envFrom": []interface{}{ + map[string]interface{}{ + "configMapRef": map[string]interface{}{ + "name": "cm1", + "key": "somekey", + }, + }, + map[string]interface{}{ + "secretRef": map[string]interface{}{ + "name": "secret1", + "key": "somekey", + }, + }, + }, + }, + }, + "volumes": map[string]interface{}{ + "configMap": map[string]interface{}{ + "name": "cm1", + }, + "secret": map[string]interface{}{ + "secretName": "secret1", + }, + }, + }, + }, + }, + }, + }, + }, + } + + expected := resource.ResourceCollection{ + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, + Name: "cm1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "someprefix-cm1-somehash", + }, + }, + }, + }, + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "Secret"}, + Name: "secret1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Secret", + "metadata": map[string]interface{}{ + "name": "someprefix-secret1-somehash", + }, + }, + }, + }, + { + GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + Name: "deploy1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "group": "apps", + "apiVersion": "v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "deploy1", + }, + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "nginx", + "image": "nginx:1.7.9", + "env": []interface{}{ + map[string]interface{}{ + "name": "CM_FOO", + "valueFrom": map[string]interface{}{ + "configMapKeyRef": map[string]interface{}{ + "name": "someprefix-cm1-somehash", + "key": "somekey", + }, + }, + }, + map[string]interface{}{ + "name": "SECRET_FOO", + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "name": "someprefix-secret1-somehash", + "key": "somekey", + }, + }, + }, + }, + "envFrom": []interface{}{ + map[string]interface{}{ + "configMapRef": map[string]interface{}{ + "name": "someprefix-cm1-somehash", + "key": "somekey", + }, + }, + map[string]interface{}{ + "secretRef": map[string]interface{}{ + "name": "someprefix-secret1-somehash", + "key": "somekey", + }, + }, + }, + }, + }, + "volumes": map[string]interface{}{ + "configMap": map[string]interface{}{ + "name": "someprefix-cm1-somehash", + }, + "secret": map[string]interface{}{ + "secretName": "someprefix-secret1-somehash", + }, + }, + }, + }, + }, + }, + }, + }, + } + + nrt, err := NewDefaultingNameReferenceTransformer() + err = nrt.Transform(m) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(m, expected) { + err = compareMap(m, expected) + t.Fatalf("actual doesn't match expected: %v", err) + } +} diff --git a/transformers/namereferenceconfig.go b/transformers/namereferenceconfig.go new file mode 100644 index 000000000..3e1ff44b3 --- /dev/null +++ b/transformers/namereferenceconfig.go @@ -0,0 +1,342 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package transformers + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// defaultNameReferencePathConfigs is the default configuration for updating +// the fields reference the name of other resources. +var defaultNameReferencePathConfigs = []referencePathConfig{ + { + referencedGVK: schema.GroupVersionKind{ + Version: "v1", + Kind: "ConfigMap", + }, + pathConfigs: []PathConfig{ + { + GroupVersionKind: &schema.GroupVersionKind{ + Version: "v1", + Kind: "Pod", + }, + Path: []string{"spec", "volumes", "configMap", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Version: "v1", + Kind: "Pod", + }, + Path: []string{"spec", "containers", "env", "valueFrom", "configMapKeyRef", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Version: "v1", + Kind: "Pod", + }, + Path: []string{"spec", "containers", "envFrom", "configMapRef", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "Deployment", + }, + Path: []string{"spec", "template", "spec", "volumes", "configMap", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "Deployment", + }, + Path: []string{"spec", "template", "spec", "containers", "env", "valueFrom", "configMapKeyRef", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "Deployment", + }, + Path: []string{"spec", "template", "spec", "containers", "envFrom", "configMapRef", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "ReplicaSet", + }, + Path: []string{"spec", "template", "spec", "volumes", "configMap", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "ReplicaSet", + }, + Path: []string{"spec", "template", "spec", "containers", "env", "valueFrom", "configMapKeyRef", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "ReplicaSet", + }, + Path: []string{"spec", "template", "spec", "containers", "envFrom", "configMapRef", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "DaemonSet", + }, + Path: []string{"spec", "template", "spec", "volumes", "configMap", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "DaemonSet", + }, + Path: []string{"spec", "template", "spec", "containers", "env", "valueFrom", "configMapKeyRef", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "DaemonSet", + }, + Path: []string{"spec", "template", "spec", "containers", "envFrom", "configMapRef", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "StatefulSet", + }, + Path: []string{"spec", "template", "spec", "volumes", "configMap", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "StatefulSet", + }, + Path: []string{"spec", "template", "spec", "containers", "env", "valueFrom", "configMapKeyRef", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "StatefulSet", + }, + Path: []string{"spec", "template", "spec", "containers", "envFrom", "configMapRef", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "Job", + }, + Path: []string{"spec", "template", "spec", "volumes", "configMap", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "Job", + }, + Path: []string{"spec", "template", "spec", "containers", "env", "valueFrom", "configMapKeyRef", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "Job", + }, + Path: []string{"spec", "template", "spec", "containers", "envFrom", "configMapRef", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "CronJob", + }, + Path: []string{"spec", "jobTemplate", "spec", "template", "spec", "volumes", "configMap", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "CronJob", + }, + Path: []string{"spec", "jobTemplate", "spec", "template", "spec", "containers", "env", "valueFrom", "configMapKeyRef", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "CronJob", + }, + Path: []string{"spec", "jobTemplate", "spec", "template", "spec", "containers", "envFrom", "configMapRef", "name"}, + CreateIfNotPresent: false, + }, + }, + }, + { + referencedGVK: schema.GroupVersionKind{ + Version: "v1", + Kind: "Secret", + }, + pathConfigs: []PathConfig{ + { + GroupVersionKind: &schema.GroupVersionKind{ + Version: "v1", + Kind: "Pod", + }, + Path: []string{"spec", "volumes", "secret", "secretName"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Version: "v1", + Kind: "Pod", + }, + Path: []string{"spec", "containers", "env", "valueFrom", "secretKeyRef", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Version: "v1", + Kind: "Pod", + }, + Path: []string{"spec", "containers", "envFrom", "secretRef", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "Deployment", + }, + Path: []string{"spec", "template", "spec", "volumes", "secret", "secretName"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "Deployment", + }, + Path: []string{"spec", "template", "spec", "containers", "env", "valueFrom", "secretKeyRef", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "Deployment", + }, + Path: []string{"spec", "template", "spec", "containers", "envFrom", "secretRef", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "ReplicaSet", + }, + Path: []string{"spec", "template", "spec", "volumes", "secret", "secretName"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "ReplicaSet", + }, + Path: []string{"spec", "template", "spec", "containers", "env", "valueFrom", "secretKeyRef", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "ReplicaSet", + }, + Path: []string{"spec", "template", "spec", "containers", "envFrom", "secretRef", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "DaemonSet", + }, + Path: []string{"spec", "template", "spec", "volumes", "secret", "secretName"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "DaemonSet", + }, + Path: []string{"spec", "template", "spec", "containers", "env", "valueFrom", "secretKeyRef", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "DaemonSet", + }, + Path: []string{"spec", "template", "spec", "containers", "envFrom", "secretRef", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "StatefulSet", + }, + Path: []string{"spec", "template", "spec", "volumes", "secret", "secretName"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "StatefulSet", + }, + Path: []string{"spec", "template", "spec", "containers", "env", "valueFrom", "secretKeyRef", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "StatefulSet", + }, + Path: []string{"spec", "template", "spec", "containers", "envFrom", "secretRef", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "Job", + }, + Path: []string{"spec", "template", "spec", "volumes", "secret", "secretName"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "Job", + }, + Path: []string{"spec", "template", "spec", "containers", "env", "valueFrom", "secretKeyRef", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "Job", + }, + Path: []string{"spec", "template", "spec", "containers", "envFrom", "secretRef", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "CronJob", + }, + Path: []string{"spec", "jobTemplate", "spec", "template", "spec", "volumes", "secret", "secretName"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "CronJob", + }, + Path: []string{"spec", "jobTemplate", "spec", "template", "spec", "containers", "env", "valueFrom", "secretKeyRef", "name"}, + CreateIfNotPresent: false, + }, + { + GroupVersionKind: &schema.GroupVersionKind{ + Kind: "CronJob", + }, + Path: []string{"spec", "jobTemplate", "spec", "template", "spec", "containers", "envFrom", "secretRef", "name"}, + CreateIfNotPresent: false, + }, + }, + }, +} diff --git a/transformers/nooptransformer.go b/transformers/nooptransformer.go new file mode 100644 index 000000000..78210177e --- /dev/null +++ b/transformers/nooptransformer.go @@ -0,0 +1,34 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package transformers + +import "k8s.io/kubectl/pkg/kustomize/resource" + +// noOpTransformer contains a no-op transformer. +type noOpTransformer struct{} + +var _ Transformer = &noOpTransformer{} + +// NewNoOpTransformer constructs a noOpTransformer. +func NewNoOpTransformer() Transformer { + return &noOpTransformer{} +} + +// Transform does nothing. +func (o *noOpTransformer) Transform(_ resource.ResourceCollection) error { + return nil +} diff --git a/transformers/overlay.go b/transformers/overlay.go new file mode 100644 index 000000000..415eaebee --- /dev/null +++ b/transformers/overlay.go @@ -0,0 +1,164 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package transformers + +import ( + "encoding/json" + "fmt" + + jsonpatch "github.com/evanphx/json-patch" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/strategicpatch" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/kubectl/pkg/kustomize/resource" +) + +// overlayTransformer contains a map of overlay objects +type overlayTransformer struct { + overlay []*resource.Resource +} + +var _ Transformer = &overlayTransformer{} + +// NewOverlayTransformer constructs a overlayTransformer. +func NewOverlayTransformer(overlay []*resource.Resource) (Transformer, error) { + if len(overlay) == 0 { + return NewNoOpTransformer(), nil + } + return &overlayTransformer{overlay}, nil +} + +// Transform apply the overlay on top of the base resources. +func (o *overlayTransformer) Transform(baseResourceMap resource.ResourceCollection) error { + // Merge and then index the patches by GVKN. + overlays, err := o.mergePatches() + if err != nil { + return err + } + + // Strategic merge the resources exist in both base and overlay. + for _, overlay := range overlays { + // Merge overlay with base resource. + gvkn := overlay.GVKN() + base, found := baseResourceMap[gvkn] + if !found { + return fmt.Errorf("failed to find an object with %#v to apply the patch", gvkn.GVK) + } + merged := map[string]interface{}{} + versionedObj, err := scheme.Scheme.New(gvkn.GVK) + baseName := base.Data.GetName() + switch { + case runtime.IsNotRegisteredError(err): + // Use JSON merge patch to handle types w/o schema + baseBytes, err := json.Marshal(base.Data) + if err != nil { + return err + } + patchBytes, err := json.Marshal(overlay.Data) + if err != nil { + return err + } + mergedBytes, err := jsonpatch.MergePatch(baseBytes, patchBytes) + if err != nil { + return err + } + err = json.Unmarshal(mergedBytes, &merged) + if err != nil { + return err + } + case err != nil: + return err + default: + // Use Strategic Merge Patch to handle types w/ schema + // TODO: Change this to use the new Merge package. + // Store the name of the base object, because this name may have been munged. + // Apply this name to the StrategicMergePatched object. + lookupPatchMeta, err := strategicpatch.NewPatchMetaFromStruct(versionedObj) + if err != nil { + return err + } + merged, err = strategicpatch.StrategicMergeMapPatchUsingLookupPatchMeta( + base.Data.Object, + overlay.Data.Object, + lookupPatchMeta) + if err != nil { + return err + } + } + base.Data.SetName(baseName) + baseResourceMap[gvkn].Data.Object = merged + } + return nil +} + +// mergePatches merge and index patches by GVKN. +// It errors out if there is conflict between patches. +func (o *overlayTransformer) mergePatches() (resource.ResourceCollection, error) { + rc := resource.ResourceCollection{} + patches := resourcesToObjects(o.overlay) + for ix, patch := range o.overlay { + gvkn := patch.GVKN() + existing, found := rc[gvkn] + if !found { + rc[gvkn] = patch + continue + } + + versionedObj, err := scheme.Scheme.New(gvkn.GVK) + if err != nil && !runtime.IsNotRegisteredError(err) { + return nil, err + } + var cd conflictDetector + if err != nil { + cd = newJMPConflictDetector() + } else { + cd, err = newSMPConflictDetector(versionedObj) + if err != nil { + return nil, err + } + } + + conflict, err := cd.hasConflict(existing.Data, patch.Data) + if err != nil { + return nil, err + } + if conflict { + conflictingPatch, err := cd.findConflict(ix, patches) + if err != nil { + return nil, err + } + return nil, fmt.Errorf("there is conflict between %#v and %#v", conflictingPatch.Object, patch.Data.Object) + } else { + merged, err := cd.mergePatches(existing.Data, patch.Data) + if err != nil { + return nil, err + } + existing.Data = merged + } + } + return rc, nil +} + +func resourcesToObjects(rs []*resource.Resource) []*unstructured.Unstructured { + objectList := make([]*unstructured.Unstructured, len(rs)) + for i := range rs { + objectList[i] = rs[i].Data + } + return objectList +} diff --git a/transformers/overlay_test.go b/transformers/overlay_test.go new file mode 100644 index 000000000..8dc8b75c4 --- /dev/null +++ b/transformers/overlay_test.go @@ -0,0 +1,648 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package transformers + +import ( + "reflect" + "strings" + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kubectl/pkg/kustomize/resource" +) + +func TestOverlayRun(t *testing.T) { + base := resource.ResourceCollection{ + { + GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + Name: "deploy1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/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", + }, + }, + }, + }, + }, + }, + }, + }, + } + overlay := []*resource.Resource{ + { + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/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{}{ + "another-label": "foo", + }, + }, + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "nginx", + "image": "nginx:latest", + "env": []interface{}{ + map[string]interface{}{ + "name": "SOMEENV", + "value": "BAR", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + expected := resource.ResourceCollection{ + { + GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + Name: "deploy1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/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", + "another-label": "foo", + }, + }, + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "nginx", + "image": "nginx:latest", + "env": []interface{}{ + map[string]interface{}{ + "name": "SOMEENV", + "value": "BAR", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + lt, err := NewOverlayTransformer(overlay) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + err = lt.Transform(base) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(base, expected) { + err = compareMap(base, expected) + t.Fatalf("actual doesn't match expected: %v", err) + } +} + +func TestMultiplePatches(t *testing.T) { + base := resource.ResourceCollection{ + { + GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + Name: "deploy1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "deploy1", + }, + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "nginx", + "image": "nginx", + }, + }, + }, + }, + }, + }, + }, + }, + } + overlay := []*resource.Resource{ + { + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "deploy1", + }, + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "nginx", + "image": "nginx:latest", + "env": []interface{}{ + map[string]interface{}{ + "name": "SOMEENV", + "value": "BAR", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "deploy1", + }, + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "nginx", + "env": []interface{}{ + map[string]interface{}{ + "name": "ANOTHERENV", + "value": "HELLO", + }, + }, + }, + map[string]interface{}{ + "name": "busybox", + "image": "busybox", + }, + }, + }, + }, + }, + }, + }, + }, + } + expected := resource.ResourceCollection{ + { + GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + Name: "deploy1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "deploy1", + }, + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "nginx", + "image": "nginx:latest", + "env": []interface{}{ + map[string]interface{}{ + "name": "ANOTHERENV", + "value": "HELLO", + }, + map[string]interface{}{ + "name": "SOMEENV", + "value": "BAR", + }, + }, + }, + map[string]interface{}{ + "name": "busybox", + "image": "busybox", + }, + }, + }, + }, + }, + }, + }, + }, + } + lt, err := NewOverlayTransformer(overlay) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + err = lt.Transform(base) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(base, expected) { + err = compareMap(base, expected) + t.Fatalf("actual doesn't match expected: %v", err) + } +} + +func TestMultiplePatchesWithConflict(t *testing.T) { + base := resource.ResourceCollection{ + { + GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + Name: "deploy1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "deploy1", + }, + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "nginx", + "image": "nginx", + }, + }, + }, + }, + }, + }, + }, + }, + } + overlay := []*resource.Resource{ + { + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "deploy1", + }, + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "nginx", + "image": "nginx:latest", + "env": []interface{}{ + map[string]interface{}{ + "name": "SOMEENV", + "value": "BAR", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "deploy1", + }, + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "nginx", + "image": "nginx:1.7.9", + }, + }, + }, + }, + }, + }, + }, + }, + } + + lt, err := NewOverlayTransformer(overlay) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + err = lt.Transform(base) + if err == nil { + t.Fatalf("did not get expected error") + } + if !strings.Contains(err.Error(), "conflict") { + t.Fatalf("expected error to contain %q but get %v", "conflict", err) + } +} + +func TestNoSchemaOverlayRun(t *testing.T) { + base := resource.ResourceCollection{ + { + GVK: schema.GroupVersionKind{Group: "example.com", Version: "v1", Kind: "Foo"}, + Name: "my-foo", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "example.com/v1", + "kind": "Foo", + "metadata": map[string]interface{}{ + "name": "my-foo", + }, + "spec": map[string]interface{}{ + "bar": map[string]interface{}{ + "A": "X", + "B": "Y", + }, + }, + }, + }, + }, + } + overlay := []*resource.Resource{ + { + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "example.com/v1", + "kind": "Foo", + "metadata": map[string]interface{}{ + "name": "my-foo", + }, + "spec": map[string]interface{}{ + "bar": map[string]interface{}{ + "B": nil, + "C": "Z", + }, + }, + }, + }, + }, + } + expected := resource.ResourceCollection{ + { + GVK: schema.GroupVersionKind{Group: "example.com", Version: "v1", Kind: "Foo"}, + Name: "my-foo", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "example.com/v1", + "kind": "Foo", + "metadata": map[string]interface{}{ + "name": "my-foo", + }, + "spec": map[string]interface{}{ + "bar": map[string]interface{}{ + "A": "X", + "C": "Z", + }, + }, + }, + }, + }, + } + + lt, err := NewOverlayTransformer(overlay) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + err = lt.Transform(base) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err = compareMap(base, expected); err != nil { + t.Fatalf("actual doesn't match expected: %v", err) + } +} + +func TestNoSchemaMultiplePatches(t *testing.T) { + base := resource.ResourceCollection{ + { + GVK: schema.GroupVersionKind{Group: "example.com", Version: "v1", Kind: "Foo"}, + Name: "my-foo", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "example.com/v1", + "kind": "Foo", + "metadata": map[string]interface{}{ + "name": "my-foo", + }, + "spec": map[string]interface{}{ + "bar": map[string]interface{}{ + "A": "X", + "B": "Y", + }, + }, + }, + }, + }, + } + overlay := []*resource.Resource{ + { + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "example.com/v1", + "kind": "Foo", + "metadata": map[string]interface{}{ + "name": "my-foo", + }, + "spec": map[string]interface{}{ + "bar": map[string]interface{}{ + "B": nil, + "C": "Z", + }, + }, + }, + }, + }, + { + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "example.com/v1", + "kind": "Foo", + "metadata": map[string]interface{}{ + "name": "my-foo", + }, + "spec": map[string]interface{}{ + "bar": map[string]interface{}{ + "C": "Z", + "D": "W", + }, + "baz": map[string]interface{}{ + "hello": "world", + }, + }, + }, + }, + }, + } + expected := resource.ResourceCollection{ + { + GVK: schema.GroupVersionKind{Group: "example.com", Version: "v1", Kind: "Foo"}, + Name: "my-foo", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "example.com/v1", + "kind": "Foo", + "metadata": map[string]interface{}{ + "name": "my-foo", + }, + "spec": map[string]interface{}{ + "bar": map[string]interface{}{ + "A": "X", + "C": "Z", + "D": "W", + }, + "baz": map[string]interface{}{ + "hello": "world", + }, + }, + }, + }, + }, + } + + lt, err := NewOverlayTransformer(overlay) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + err = lt.Transform(base) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err = compareMap(base, expected); err != nil { + t.Fatalf("actual doesn't match expected: %v", err) + } +} + +func TestNoSchemaMultiplePatchesWithConflict(t *testing.T) { + base := resource.ResourceCollection{ + { + GVK: schema.GroupVersionKind{Group: "example.com", Version: "v1", Kind: "Foo"}, + Name: "my-foo", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "example.com/v1", + "kind": "Foo", + "metadata": map[string]interface{}{ + "name": "my-foo", + }, + "spec": map[string]interface{}{ + "bar": map[string]interface{}{ + "A": "X", + "B": "Y", + }, + }, + }, + }, + }, + } + overlay := []*resource.Resource{ + { + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "example.com/v1", + "kind": "Foo", + "metadata": map[string]interface{}{ + "name": "my-foo", + }, + "spec": map[string]interface{}{ + "bar": map[string]interface{}{ + "B": nil, + "C": "Z", + }, + }, + }, + }, + }, + { + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "example.com/v1", + "kind": "Foo", + "metadata": map[string]interface{}{ + "name": "my-foo", + }, + "spec": map[string]interface{}{ + "bar": map[string]interface{}{ + "C": "NOT_Z", + }, + }, + }, + }, + }, + } + + lt, err := NewOverlayTransformer(overlay) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + err = lt.Transform(base) + if err == nil { + t.Fatalf("did not get expected error") + } + if !strings.Contains(err.Error(), "conflict") { + t.Fatalf("expected error to contain %q but get %v", "conflict", err) + } +} diff --git a/transformers/overlayconflictdetector.go b/transformers/overlayconflictdetector.go new file mode 100644 index 000000000..6a0d30587 --- /dev/null +++ b/transformers/overlayconflictdetector.go @@ -0,0 +1,130 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package transformers + +import ( + "encoding/json" + + jsonpatch "github.com/evanphx/json-patch" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/mergepatch" + "k8s.io/apimachinery/pkg/util/strategicpatch" +) + +type conflictDetector interface { + hasConflict(patch1, patch2 *unstructured.Unstructured) (bool, error) + findConflict(conflictingPatchIdx int, patches []*unstructured.Unstructured) (*unstructured.Unstructured, error) + mergePatches(patch1, patch2 *unstructured.Unstructured) (*unstructured.Unstructured, error) +} + +type jsonMergePatch struct{} + +var _ conflictDetector = &jsonMergePatch{} + +func newJMPConflictDetector() conflictDetector { + return &jsonMergePatch{} +} + +func (jmp *jsonMergePatch) hasConflict(patch1, patch2 *unstructured.Unstructured) (bool, error) { + return mergepatch.HasConflicts(patch1.Object, patch2.Object) +} + +func (jmp *jsonMergePatch) findConflict(conflictingPatchIdx int, patches []*unstructured.Unstructured) (*unstructured.Unstructured, error) { + for i, patch := range patches { + if i == conflictingPatchIdx { + continue + } + if patches[conflictingPatchIdx].GroupVersionKind() != patch.GroupVersionKind() || + patches[conflictingPatchIdx].GetName() != patch.GetName() { + continue + } + conflict, err := mergepatch.HasConflicts(patch.Object, patches[conflictingPatchIdx].Object) + if err != nil { + return nil, err + } + if conflict { + return patch, nil + } + } + return nil, nil +} + +func (jmp *jsonMergePatch) mergePatches(patch1, patch2 *unstructured.Unstructured) (*unstructured.Unstructured, error) { + var merged unstructured.Unstructured + var mergedMap map[string]interface{} + baseBytes, err := json.Marshal(patch1.Object) + if err != nil { + return nil, err + } + patchBytes, err := json.Marshal(patch2.Object) + if err != nil { + return nil, err + } + mergedBytes, err := jsonpatch.MergeMergePatches(baseBytes, patchBytes) + if err != nil { + return nil, err + } + err = json.Unmarshal(mergedBytes, &mergedMap) + merged.SetUnstructuredContent(mergedMap) + return &merged, err +} + +type strategicMergePatch struct { + lookupPatchMeta strategicpatch.LookupPatchMeta +} + +var _ conflictDetector = &strategicMergePatch{} + +func newSMPConflictDetector(versionedObj runtime.Object) (conflictDetector, error) { + lookupPatchMeta, err := strategicpatch.NewPatchMetaFromStruct(versionedObj) + return &strategicMergePatch{lookupPatchMeta: lookupPatchMeta}, err +} + +func (smp *strategicMergePatch) hasConflict(patch1, patch2 *unstructured.Unstructured) (bool, error) { + return strategicpatch.MergingMapsHaveConflicts(patch1.Object, patch2.Object, smp.lookupPatchMeta) +} + +func (smp *strategicMergePatch) findConflict(conflictingPatchIdx int, patches []*unstructured.Unstructured) (*unstructured.Unstructured, error) { + for i, patch := range patches { + if i == conflictingPatchIdx { + continue + } + if patches[conflictingPatchIdx].GroupVersionKind() != patch.GroupVersionKind() || + patches[conflictingPatchIdx].GetName() != patch.GetName() { + continue + } + conflict, err := strategicpatch.MergingMapsHaveConflicts( + patch.Object, patches[conflictingPatchIdx].Object, smp.lookupPatchMeta) + if err != nil { + return nil, err + } + if conflict { + return patch, nil + } + } + return nil, nil +} + +func (smp *strategicMergePatch) mergePatches(patch1, patch2 *unstructured.Unstructured) (*unstructured.Unstructured, error) { + merged := unstructured.Unstructured{} + mergeJsonMap, err := strategicpatch.MergeStrategicMergeMapPatchUsingLookupPatchMeta( + smp.lookupPatchMeta, patch1.Object, patch2.Object) + merged.SetUnstructuredContent(mergeJsonMap) + return &merged, err +} diff --git a/transformers/pathconfig.go b/transformers/pathconfig.go new file mode 100644 index 000000000..aad2569d5 --- /dev/null +++ b/transformers/pathconfig.go @@ -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 transformers + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// PathConfig contains the configuration of a field, including the GVK it ties to, +// path to the field, etc. +type PathConfig struct { + // 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 +} + +// referencePathConfig contains the configuration of a field that references +// the name of another resource whose GroupVersionKind is specified in referencedGVK. +// e.g. pod.spec.template.volumes.configMap.name references the name of a configmap +// Its corresponding referencePathConfig will look like: +// +// referencePathConfig{ +// referencedGVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, +// pathConfigs: []PathConfig{ +// { +// GroupVersionKind: &schema.GroupVersionKind{Version: "v1", Kind: "Pod"}, +// Path: []string{"spec", "volumes", "configMap", "name"}, +// }, +// } +type referencePathConfig struct { + // referencedGVK is the GroupVersionKind that is referenced by + // the PathConfig's GVK in the path of PathConfig.Path. + referencedGVK schema.GroupVersionKind + // PathConfig is the GVK that is referencing the referencedGVK object's name. + pathConfigs []PathConfig +} diff --git a/transformers/prefixname.go b/transformers/prefixname.go new file mode 100644 index 000000000..98898fe55 --- /dev/null +++ b/transformers/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 transformers + +import ( + "errors" + "fmt" + + "k8s.io/kubectl/pkg/kustomize/resource" + "k8s.io/kubectl/pkg/kustomize/types" +) + +// namePrefixTransformer contains the prefix and the path config for each field that +// the name prefix will be applied. +type namePrefixTransformer struct { + prefix string + pathConfigs []PathConfig +} + +var _ Transformer = &namePrefixTransformer{} + +var defaultNamePrefixPathConfigs = []PathConfig{ + { + Path: []string{"metadata", "name"}, + CreateIfNotPresent: false, + }, +} + +// NewDefaultingNamePrefixTransformer construct a namePrefixTransformer with defaultNamePrefixPathConfigs. +func NewDefaultingNamePrefixTransformer(nameprefix string) (Transformer, error) { + return NewNamePrefixTransformer(defaultNamePrefixPathConfigs, nameprefix) +} + +// NewNamePrefixTransformer construct a namePrefixTransformer. +func NewNamePrefixTransformer(pc []PathConfig, np string) (Transformer, error) { + if len(np) == 0 { + return NewNoOpTransformer(), nil + } + if pc == nil { + return nil, errors.New("pathConfigs is not expected to be nil") + } + return &namePrefixTransformer{pathConfigs: pc, prefix: np}, nil +} + +// Transform prepends the name prefix. +func (o *namePrefixTransformer) Transform(m resource.ResourceCollection) error { + for gvkn := range m { + obj := m[gvkn].Data + objMap := obj.UnstructuredContent() + for _, path := range o.pathConfigs { + if !types.SelectByGVK(gvkn.GVK, path.GroupVersionKind) { + continue + } + err := mutateField(objMap, path.Path, path.CreateIfNotPresent, o.addPrefix) + if err != nil { + return err + } + } + } + return nil +} + +func (o *namePrefixTransformer) 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/transformers/prefixname_test.go b/transformers/prefixname_test.go new file mode 100644 index 000000000..3d73d2d1c --- /dev/null +++ b/transformers/prefixname_test.go @@ -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 transformers + +import ( + "reflect" + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kubectl/pkg/kustomize/resource" +) + +func TestPrefixNameRun(t *testing.T) { + m := resource.ResourceCollection{ + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, + Name: "cm1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "cm1", + }, + }, + }, + }, + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, + Name: "cm2", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "cm2", + }, + }, + }, + }, + } + expected := resource.ResourceCollection{ + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, + Name: "cm1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "someprefix-cm1", + }, + }, + }, + }, + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, + Name: "cm2", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "someprefix-cm2", + }, + }, + }, + }, + } + + npt, err := NewDefaultingNamePrefixTransformer("someprefix-") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + err = npt.Transform(m) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(m, expected) { + err = compareMap(m, expected) + t.Fatalf("actual doesn't match expected: %v", err) + } +} diff --git a/transformers/transformer.go b/transformers/transformer.go new file mode 100644 index 000000000..4c7c8961a --- /dev/null +++ b/transformers/transformer.go @@ -0,0 +1,25 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package transformers + +import "k8s.io/kubectl/pkg/kustomize/resource" + +// Transformer can transform objects. +type Transformer interface { + // Transform modifies objects in a map, e.g. add prefixes or additional labels. + Transform(m resource.ResourceCollection) error +} diff --git a/transformers/util.go b/transformers/util.go new file mode 100644 index 000000000..d0786c9a1 --- /dev/null +++ b/transformers/util.go @@ -0,0 +1,70 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package transformers + +import ( + "fmt" +) + +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/transformers/util_test.go b/transformers/util_test.go new file mode 100644 index 000000000..5af0f62c2 --- /dev/null +++ b/transformers/util_test.go @@ -0,0 +1,49 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package transformers + +import ( + "fmt" + "reflect" + + "k8s.io/kubectl/pkg/kustomize/resource" + "k8s.io/kubectl/pkg/kustomize/types" +) + +func compareMap(m1, m2 resource.ResourceCollection) error { + if len(m1) != len(m2) { + keySet1 := []types.GroupVersionKindName{} + keySet2 := []types.GroupVersionKindName{} + for GVKn := range m1 { + keySet1 = append(keySet1, GVKn) + } + for GVKn := range m1 { + keySet2 = append(keySet2, GVKn) + } + return fmt.Errorf("maps has different number of entries: %#v doesn't equals %#v", keySet1, keySet2) + } + for GVKn, obj1 := range m1 { + obj2, found := m2[GVKn] + if !found { + return fmt.Errorf("%#v doesn't exist in %#v", GVKn, m2) + } + if !reflect.DeepEqual(obj1.Data, obj2.Data) { + return fmt.Errorf("%#v doesn't match %#v", obj1.Data, obj2.Data) + } + } + return nil +} diff --git a/types/gvkn_sort.go b/types/gvkn_sort.go new file mode 100644 index 000000000..df3baf1ab --- /dev/null +++ b/types/gvkn_sort.go @@ -0,0 +1,35 @@ +/* +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 types + +import ( + "sort" +) + +// ByGVKN implements the sort interface. +type ByGVKN []GroupVersionKindName + +var _ sort.Interface = ByGVKN{} + +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/types/types.go b/types/types.go new file mode 100644 index 000000000..230a22969 --- /dev/null +++ b/types/types.go @@ -0,0 +1,29 @@ +/* +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 types + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// GroupVersionKindName contains GroupVersionKind and original name of the resource. +type GroupVersionKindName struct { + // GroupVersionKind of the resource. + GVK schema.GroupVersionKind + // original name of the resource before transformation. + Name string +} diff --git a/types/util.go b/types/util.go new file mode 100644 index 000000000..6a156aadb --- /dev/null +++ b/types/util.go @@ -0,0 +1,59 @@ +/* +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 types + +import ( + "strings" + + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func (gvkn GroupVersionKindName) String() string { + if gvkn.GVK.Group == "" { + return strings.Join([]string{gvkn.GVK.Version, gvkn.GVK.Kind, gvkn.Name}, "_") + ".yaml" + } + return strings.Join([]string{gvkn.GVK.Group, gvkn.GVK.Version, gvkn.GVK.Kind, gvkn.Name}, "_") + ".yaml" +} + +// SelectByGVK returns true if `selector` selects `in`; otherwise, false. +// If `selector` and `in` are the same, return true. +// If `selector` is nil, it is considered as a wildcard and always return true. +// e.g. selector CAN select +// . +// selector CANNOT select +// . +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 +} diff --git a/types/util_test.go b/types/util_test.go new file mode 100644 index 000000000..1bc2606d7 --- /dev/null +++ b/types/util_test.go @@ -0,0 +1,131 @@ +/* +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 types + +import ( + "testing" + + "k8s.io/apimachinery/pkg/runtime/schema" +) + +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) + } + } +} diff --git a/util/diff.go b/util/diff.go new file mode 100644 index 000000000..dbcb0a351 --- /dev/null +++ b/util/diff.go @@ -0,0 +1,104 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "io" + "io/ioutil" + "os" + "path/filepath" + + "github.com/ghodss/yaml" + + "k8s.io/utils/exec" +) + +// DiffProgram finds and run the diff program. The value of +// KUBERNETES_EXTERNAL_DIFF environment variable will be used a diff +// program. By default, `diff(1)` will be used. +type DiffProgram struct { + Exec exec.Interface + Stdout io.Writer + Stderr io.Writer +} + +func (d *DiffProgram) getCommand(args ...string) exec.Cmd { + diff := "" + if envDiff := os.Getenv("KUBERNETES_EXTERNAL_DIFF"); envDiff != "" { + diff = envDiff + } else { + diff = "diff" + args = append([]string{"-u", "-N"}, args...) + } + + cmd := d.Exec.Command(diff, args...) + cmd.SetStdout(d.Stdout) + cmd.SetStderr(d.Stderr) + + return cmd +} + +// Run runs the detected diff program. `from` and `to` are the directory to diff. +func (d *DiffProgram) Run(from, to string) error { + d.getCommand(from, to).Run() // Ignore diff return code + return nil +} + +// Printer is used to print an object. +type Printer struct{} + +// Print the object inside the writer w. +func (p *Printer) Print(obj interface{}, w io.Writer) error { + if obj == nil { + return nil + } + data, err := yaml.Marshal(obj) + if err != nil { + return err + } + _, err = w.Write(data) + return err + +} + +// Directory creates a new temp directory, and allows to easily create new files. +type Directory struct { + Name string +} + +// CreateDirectory does create the actual disk directory, and return a +// new representation of it. +func CreateDirectory(prefix string) (*Directory, error) { + name, err := ioutil.TempDir("", prefix+"-") + if err != nil { + return nil, err + } + + return &Directory{ + Name: name, + }, nil +} + +// NewFile creates a new file in the directory. +func (d *Directory) NewFile(name string) (*os.File, error) { + return os.OpenFile(filepath.Join(d.Name, name), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0700) +} + +// Delete removes the directory recursively. +func (d *Directory) Delete() error { + return os.RemoveAll(d.Name) +} diff --git a/util/fs/fakefile.go b/util/fs/fakefile.go new file mode 100644 index 000000000..a40bf7a55 --- /dev/null +++ b/util/fs/fakefile.go @@ -0,0 +1,74 @@ +/* +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 fs + +import ( + "bytes" + "os" +) + +var _ File = &FakeFile{} + +// FakeFile implements File in-memory for tests. +type FakeFile struct { + name string + content []byte + dir bool + open bool +} + +// makeFile makes a fake file. +func makeFile() *FakeFile { + return &FakeFile{} +} + +// makeDir makes a fake directory. +func makeDir(name string) *FakeFile { + return &FakeFile{name: name, dir: true} +} + +// Close marks the fake file closed. +func (f *FakeFile) Close() error { + f.open = false + return nil +} + +// Read never fails, and doesn't mutate p. +func (f *FakeFile) Read(p []byte) (n int, err error) { + return len(p), nil +} + +// Write saves the contents of the argument to memory. +func (f *FakeFile) Write(p []byte) (n int, err error) { + f.content = p + return len(p), nil +} + +// ContentMatches returns true if v matches fake file's content. +func (f *FakeFile) ContentMatches(v []byte) bool { + return bytes.Equal(v, f.content) +} + +// GetContent the content of a fake file. +func (f *FakeFile) GetContent() []byte { + return f.content +} + +// Stat returns nil. +func (f *FakeFile) Stat() (os.FileInfo, error) { + return nil, nil +} diff --git a/util/fs/fakefileinfo.go b/util/fs/fakefileinfo.go new file mode 100644 index 000000000..6ccca9150 --- /dev/null +++ b/util/fs/fakefileinfo.go @@ -0,0 +1,47 @@ +/* +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 fs + +import ( + "os" + "time" +) + +var _ os.FileInfo = &Fakefileinfo{} + +// Fakefileinfo implements Fakefileinfo using a fake in-memory filesystem. +type Fakefileinfo struct { + *FakeFile +} + +// Name returns the name of the file +func (fi *Fakefileinfo) Name() string { return fi.name } + +// Size returns the size of the file +func (fi *Fakefileinfo) Size() int64 { return int64(len(fi.content)) } + +// Mode returns the file mode +func (fi *Fakefileinfo) Mode() os.FileMode { return 0777 } + +// ModTime returns the modification time +func (fi *Fakefileinfo) ModTime() time.Time { return time.Time{} } + +// IsDir returns if it is a directory +func (fi *Fakefileinfo) IsDir() bool { return fi.dir } + +// Sys should return underlying data source, but it now returns nil +func (fi *Fakefileinfo) Sys() interface{} { return nil } diff --git a/util/fs/fakefs.go b/util/fs/fakefs.go new file mode 100644 index 000000000..a3ca4ee01 --- /dev/null +++ b/util/fs/fakefs.go @@ -0,0 +1,80 @@ +/* +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 fs + +import ( + "fmt" + "os" +) + +var _ FileSystem = &FakeFS{} + +// FakeFS implements FileSystem using a fake in-memory filesystem. +type FakeFS struct { + m map[string]*FakeFile +} + +// MakeFakeFS returns an instance of FakeFS with no files in it. +func MakeFakeFS() *FakeFS { + return &FakeFS{m: map[string]*FakeFile{}} +} + +// Create assures a fake file appears in the in-memory file system. +func (fs *FakeFS) Create(name string) (File, error) { + f := &FakeFile{} + f.open = true + fs.m[name] = f + return fs.m[name], nil +} + +// Mkdir assures a fake directory appears in the in-memory file system. +func (fs *FakeFS) Mkdir(name string, perm os.FileMode) error { + fs.m[name] = makeDir(name) + return nil +} + +// Open returns a fake file in the open state. +func (fs *FakeFS) Open(name string) (File, error) { + if _, found := fs.m[name]; !found { + return nil, fmt.Errorf("file %q cannot be opened", name) + } + return fs.m[name], nil +} + +// Stat always returns nil FileInfo, and returns an error if file does not exist. +func (fs *FakeFS) Stat(name string) (os.FileInfo, error) { + if f, found := fs.m[name]; found { + return &Fakefileinfo{f}, nil + } + return nil, fmt.Errorf("file %q does not exist", name) +} + +// ReadFile always returns an empty bytes and error depending on content of m. +func (fs *FakeFS) ReadFile(name string) ([]byte, error) { + if ff, found := fs.m[name]; found { + return ff.content, nil + } + return nil, fmt.Errorf("cannot read file %q", name) +} + +// WriteFile always succeeds and does nothing. +func (fs *FakeFS) WriteFile(name string, c []byte) error { + ff := &FakeFile{} + ff.Write(c) + fs.m[name] = ff + return nil +} diff --git a/util/fs/fakefs_test.go b/util/fs/fakefs_test.go new file mode 100644 index 000000000..c7c7b8d05 --- /dev/null +++ b/util/fs/fakefs_test.go @@ -0,0 +1,105 @@ +/* +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 fs + +import ( + "bytes" + "testing" +) + +func TestStatNotExist(t *testing.T) { + x := MakeFakeFS() + info, err := x.Stat("foo") + if info != nil { + t.Fatalf("expected nil info") + } + if err == nil { + t.Fatalf("expected error") + } +} + +func TestStat(t *testing.T) { + x := MakeFakeFS() + expectedName := "my-dir" + err := x.Mkdir(expectedName, 0666) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + info, err := x.Stat(expectedName) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + name := info.Name() + if name != expectedName { + t.Fatalf("expected %v but got %v", expectedName, name) + } + if !info.IsDir() { + t.Fatalf("expected IsDir() return true") + } +} + +func TestCreate(t *testing.T) { + x := MakeFakeFS() + f, err := x.Create("foo") + if f == nil { + t.Fatalf("expected file") + } + if err != nil { + t.Fatalf("unexpected error") + } + info, err := x.Stat("foo") + if info == nil { + t.Fatalf("expected non-nil info") + } + if err != nil { + t.Fatalf("expected no error") + } +} + +func TestReadFile(t *testing.T) { + x := MakeFakeFS() + f, err := x.Create("foo") + if f == nil { + t.Fatalf("expected file") + } + if err != nil { + t.Fatalf("unexpected error") + } + content, err := x.ReadFile("foo") + if len(content) != 0 { + t.Fatalf("expected no content") + } + if err != nil { + t.Fatalf("expected no error") + } +} + +func TestWriteFile(t *testing.T) { + x := MakeFakeFS() + c := []byte("heybuddy") + err := x.WriteFile("foo", c) + if err != nil { + t.Fatalf("expected no error") + } + content, err := x.ReadFile("foo") + if err != nil { + t.Fatalf("expected read to work: %v", err) + } + if bytes.Compare(c, content) != 0 { + t.Fatalf("incorrect content: %v", content) + } +} diff --git a/util/fs/fs.go b/util/fs/fs.go new file mode 100644 index 000000000..2d6aedf5a --- /dev/null +++ b/util/fs/fs.go @@ -0,0 +1,38 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fs + +import ( + "io" + "os" +) + +// FileSystem groups basic os filesystem methods. +type FileSystem interface { + Create(name string) (File, error) + Mkdir(name string, perm os.FileMode) error + Open(name string) (File, error) + Stat(name string) (os.FileInfo, error) + ReadFile(name string) ([]byte, error) + WriteFile(name string, data []byte) error +} + +// File groups the basic os.File methods. +type File interface { + io.ReadWriteCloser + Stat() (os.FileInfo, error) +} diff --git a/util/fs/realfile.go b/util/fs/realfile.go new file mode 100644 index 000000000..d6bd21046 --- /dev/null +++ b/util/fs/realfile.go @@ -0,0 +1,49 @@ +/* +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 fs + +import ( + "errors" + "os" +) + +var _ File = &realFile{} + +// realFile implements File using the local filesystem. +type realFile struct { + file *os.File +} + +// MakeRealFile makes an instance of realFile. +func MakeRealFile(f *os.File) (File, error) { + if f == nil { + return nil, errors.New("file argument may not be nil") + } + return &realFile{file: f}, nil +} + +// Close closes a file. +func (f *realFile) Close() error { return f.file.Close() } + +// Read reads a file's content. +func (f *realFile) Read(p []byte) (n int, err error) { return f.file.Read(p) } + +// Write writes bytes to a file +func (f *realFile) Write(p []byte) (n int, err error) { return f.file.Write(p) } + +// Stat returns an interface which has all the information regarding the file. +func (f *realFile) Stat() (os.FileInfo, error) { return f.file.Stat() } diff --git a/util/fs/realfs.go b/util/fs/realfs.go new file mode 100644 index 000000000..f2c3237b1 --- /dev/null +++ b/util/fs/realfs.go @@ -0,0 +1,52 @@ +/* +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 fs + +import ( + "io/ioutil" + "os" +) + +var _ FileSystem = realFS{} + +// realFS implements FileSystem using the local filesystem. +type realFS struct{} + +// MakeRealFS makes an instance of realFS. +func MakeRealFS() FileSystem { + return realFS{} +} + +// Create delegates to os.Create. +func (realFS) Create(name string) (File, error) { return os.Create(name) } + +// Mkdir delegates to os.Mkdir. +func (realFS) Mkdir(name string, perm os.FileMode) error { return os.Mkdir(name, perm) } + +// Open delegates to os.Open. +func (realFS) Open(name string) (File, error) { return os.Open(name) } + +// Stat delegates to os.Stat. +func (realFS) Stat(name string) (os.FileInfo, error) { return os.Stat(name) } + +// ReadFile delegates to ioutil.ReadFile. +func (realFS) ReadFile(name string) ([]byte, error) { return ioutil.ReadFile(name) } + +// WriteFile delegates to ioutil.WriteFile with read/write permissions. +func (realFS) WriteFile(name string, c []byte) error { + return ioutil.WriteFile(name, c, 0666) +} diff --git a/util/util.go b/util/util.go new file mode 100644 index 000000000..95a693b82 --- /dev/null +++ b/util/util.go @@ -0,0 +1,80 @@ +/* +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" + "sort" + + "github.com/ghodss/yaml" + + "k8s.io/kubectl/pkg/kustomize/resource" + "k8s.io/kubectl/pkg/kustomize/types" +) + +// Encode encodes the map `in` and output the encoded objects separated by `---`. +func Encode(in resource.ResourceCollection) ([]byte, error) { + gvknList := []types.GroupVersionKindName{} + for gvkn := range in { + gvknList = append(gvknList, gvkn) + } + sort.Sort(types.ByGVKN(gvknList)) + + firstObj := true + var b []byte + buf := bytes.NewBuffer(b) + for _, gvkn := range gvknList { + obj := in[gvkn].Data + 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 +} + +// WriteToDir write each object in ResourceCollection to a file named with GroupVersionKindName. +func WriteToDir(in resource.ResourceCollection, dirName string, printer Printer) (*Directory, error) { + dir, err := CreateDirectory(dirName) + if err != nil { + return &Directory{}, err + } + + for gvkn, obj := range in { + f, err := dir.NewFile(gvkn.String()) + if err != nil { + return &Directory{}, err + } + defer f.Close() + err = printer.Print(obj.Data, f) + if err != nil { + return &Directory{}, err + } + } + return dir, nil +} diff --git a/util/util_test.go b/util/util_test.go new file mode 100644 index 000000000..0d84d9c02 --- /dev/null +++ b/util/util_test.go @@ -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 util + +import ( + "fmt" + "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" +) + +func TestEncode(t *testing.T) { + encoded := []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm2 +`) + input := resource.ResourceCollection{ + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, + Name: "cm1", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "cm1", + }, + }, + }, + }, + { + GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, + Name: "cm2", + }: &resource.Resource{ + Data: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "cm2", + }, + }, + }, + }, + } + out, err := Encode(input) + 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 compareMap(m1, m2 resource.ResourceCollection) error { + if len(m1) != len(m2) { + keySet1 := []types.GroupVersionKindName{} + keySet2 := []types.GroupVersionKindName{} + for GVKn := range m1 { + keySet1 = append(keySet1, GVKn) + } + for GVKn := range m1 { + keySet2 = append(keySet2, GVKn) + } + return fmt.Errorf("maps has different number of entries: %#v doesn't equals %#v", keySet1, keySet2) + } + for GVKn, obj1 := range m1 { + obj2, found := m2[GVKn] + if !found { + return fmt.Errorf("%#v doesn't exist in %#v", GVKn, m2) + } + if !reflect.DeepEqual(obj1, obj2) { + return fmt.Errorf("%#v doesn't match %#v", obj1, obj2) + } + } + return nil +}