From 74a64a9f3de0b6caee6a815684605266ecfb8c31 Mon Sep 17 00:00:00 2001 From: Jingfang Liu Date: Mon, 11 Feb 2019 14:16:43 -0800 Subject: [PATCH] copy kustomize/k8sdeps into cli-runtime --- .../src/k8s.io/cli-runtime/Godeps/Godeps.json | 48 +- .../k8s.io/cli-runtime/pkg/kustomize/BUILD | 38 ++ .../cli-runtime/pkg/kustomize/k8sdeps/BUILD | 39 ++ .../k8sdeps/configmapandsecret/BUILD | 53 ++ .../configmapandsecret/configmapfactory.go | 126 ++++ .../configmapfactory_test.go | 154 +++++ .../k8sdeps/configmapandsecret/kv.go | 107 ++++ .../k8sdeps/configmapandsecret/kv_test.go | 57 ++ .../configmapandsecret/secretfactory.go | 106 ++++ .../configmapandsecret/secretfactory_test.go | 151 +++++ .../cli-runtime/pkg/kustomize/k8sdeps/doc.go | 76 +++ .../pkg/kustomize/k8sdeps/factory.go | 34 ++ .../pkg/kustomize/k8sdeps/kunstruct/BUILD | 47 ++ .../kustomize/k8sdeps/kunstruct/factory.go | 118 ++++ .../k8sdeps/kunstruct/factory_test.go | 175 ++++++ .../pkg/kustomize/k8sdeps/kunstruct/helper.go | 71 +++ .../kustomize/k8sdeps/kunstruct/kunstruct.go | 92 +++ .../k8sdeps/kunstruct/kunstruct_test.go | 148 +++++ .../pkg/kustomize/k8sdeps/kv/BUILD | 30 + .../pkg/kustomize/k8sdeps/kv/kv.go | 102 ++++ .../pkg/kustomize/k8sdeps/kv/kv_test.go | 68 +++ .../pkg/kustomize/k8sdeps/transformer/BUILD | 33 + .../kustomize/k8sdeps/transformer/factory.go | 43 ++ .../kustomize/k8sdeps/transformer/hash/BUILD | 50 ++ .../k8sdeps/transformer/hash/hash.go | 168 ++++++ .../k8sdeps/transformer/hash/hash_test.go | 208 +++++++ .../k8sdeps/transformer/hash/namehash.go | 47 ++ .../k8sdeps/transformer/hash/namehash_test.go | 162 +++++ .../kustomize/k8sdeps/transformer/patch/BUILD | 51 ++ .../k8sdeps/transformer/patch/patch.go | 174 ++++++ .../k8sdeps/transformer/patch/patch_test.go | 563 ++++++++++++++++++ .../patch/patchconflictdetector.go | 137 +++++ .../pkg/kustomize/k8sdeps/validator/BUILD | 29 + .../kustomize/k8sdeps/validator/validators.go | 61 ++ 34 files changed, 3542 insertions(+), 24 deletions(-) create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/BUILD create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/BUILD create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/BUILD create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/configmapfactory.go create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/configmapfactory_test.go create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/kv.go create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/kv_test.go create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/secretfactory.go create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/secretfactory_test.go create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/doc.go create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/factory.go create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/BUILD create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/factory.go create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/factory_test.go create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/helper.go create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/kunstruct.go create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/kunstruct_test.go create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kv/BUILD create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kv/kv.go create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kv/kv_test.go create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/BUILD create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/factory.go create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash/BUILD create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash/hash.go create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash/hash_test.go create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash/namehash.go create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash/namehash_test.go create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/patch/BUILD create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/patch/patch.go create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/patch/patch_test.go create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/patch/patchconflictdetector.go create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/validator/BUILD create mode 100644 staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/validator/validators.go diff --git a/staging/src/k8s.io/cli-runtime/Godeps/Godeps.json b/staging/src/k8s.io/cli-runtime/Godeps/Godeps.json index 972c2f0bed..ee914bf748 100644 --- a/staging/src/k8s.io/cli-runtime/Godeps/Godeps.json +++ b/staging/src/k8s.io/cli-runtime/Godeps/Godeps.json @@ -644,11 +644,11 @@ }, { "ImportPath": "k8s.io/kube-openapi/pkg/common", - "Rev": "ced9eb3070a5f1c548ef46e8dfe2a97c208d9f03" + "Rev": "d7c86cdc46e3a4fcf892b32dd7bc3aa775e0870e" }, { "ImportPath": "k8s.io/kube-openapi/pkg/util/proto", - "Rev": "ced9eb3070a5f1c548ef46e8dfe2a97c208d9f03" + "Rev": "d7c86cdc46e3a4fcf892b32dd7bc3aa775e0870e" }, { "ImportPath": "k8s.io/client-go/discovery", @@ -700,91 +700,91 @@ }, { "ImportPath": "sigs.k8s.io/kustomize/pkg/commands/build", - "Rev": "0184d5b69700607a1b9280f303152e2959bc19e7" + "Rev": "ce7e5ee2c30cc5856fea01fe423cf167f2a2d0c3" }, { "ImportPath": "sigs.k8s.io/kustomize/pkg/constants", - "Rev": "0184d5b69700607a1b9280f303152e2959bc19e7" + "Rev": "ce7e5ee2c30cc5856fea01fe423cf167f2a2d0c3" }, { "ImportPath": "sigs.k8s.io/kustomize/pkg/expansion", - "Rev": "0184d5b69700607a1b9280f303152e2959bc19e7" + "Rev": "ce7e5ee2c30cc5856fea01fe423cf167f2a2d0c3" }, { "ImportPath": "sigs.k8s.io/kustomize/pkg/factory", - "Rev": "0184d5b69700607a1b9280f303152e2959bc19e7" + "Rev": "ce7e5ee2c30cc5856fea01fe423cf167f2a2d0c3" }, { "ImportPath": "sigs.k8s.io/kustomize/pkg/fs", - "Rev": "0184d5b69700607a1b9280f303152e2959bc19e7" + "Rev": "ce7e5ee2c30cc5856fea01fe423cf167f2a2d0c3" }, { "ImportPath": "sigs.k8s.io/kustomize/pkg/git", - "Rev": "0184d5b69700607a1b9280f303152e2959bc19e7" + "Rev": "ce7e5ee2c30cc5856fea01fe423cf167f2a2d0c3" }, { "ImportPath": "sigs.k8s.io/kustomize/pkg/gvk", - "Rev": "0184d5b69700607a1b9280f303152e2959bc19e7" + "Rev": "ce7e5ee2c30cc5856fea01fe423cf167f2a2d0c3" }, { "ImportPath": "sigs.k8s.io/kustomize/pkg/ifc", - "Rev": "0184d5b69700607a1b9280f303152e2959bc19e7" + "Rev": "ce7e5ee2c30cc5856fea01fe423cf167f2a2d0c3" }, { "ImportPath": "sigs.k8s.io/kustomize/pkg/ifc/transformer", - "Rev": "0184d5b69700607a1b9280f303152e2959bc19e7" + "Rev": "ce7e5ee2c30cc5856fea01fe423cf167f2a2d0c3" }, { "ImportPath": "sigs.k8s.io/kustomize/pkg/image", - "Rev": "0184d5b69700607a1b9280f303152e2959bc19e7" + "Rev": "ce7e5ee2c30cc5856fea01fe423cf167f2a2d0c3" }, { "ImportPath": "sigs.k8s.io/kustomize/pkg/internal/error", - "Rev": "0184d5b69700607a1b9280f303152e2959bc19e7" + "Rev": "ce7e5ee2c30cc5856fea01fe423cf167f2a2d0c3" }, { "ImportPath": "sigs.k8s.io/kustomize/pkg/loader", - "Rev": "0184d5b69700607a1b9280f303152e2959bc19e7" + "Rev": "ce7e5ee2c30cc5856fea01fe423cf167f2a2d0c3" }, { "ImportPath": "sigs.k8s.io/kustomize/pkg/patch", - "Rev": "0184d5b69700607a1b9280f303152e2959bc19e7" + "Rev": "ce7e5ee2c30cc5856fea01fe423cf167f2a2d0c3" }, { "ImportPath": "sigs.k8s.io/kustomize/pkg/patch/transformer", - "Rev": "0184d5b69700607a1b9280f303152e2959bc19e7" + "Rev": "ce7e5ee2c30cc5856fea01fe423cf167f2a2d0c3" }, { "ImportPath": "sigs.k8s.io/kustomize/pkg/resid", - "Rev": "0184d5b69700607a1b9280f303152e2959bc19e7" + "Rev": "ce7e5ee2c30cc5856fea01fe423cf167f2a2d0c3" }, { "ImportPath": "sigs.k8s.io/kustomize/pkg/resmap", - "Rev": "0184d5b69700607a1b9280f303152e2959bc19e7" + "Rev": "ce7e5ee2c30cc5856fea01fe423cf167f2a2d0c3" }, { "ImportPath": "sigs.k8s.io/kustomize/pkg/resource", - "Rev": "0184d5b69700607a1b9280f303152e2959bc19e7" + "Rev": "ce7e5ee2c30cc5856fea01fe423cf167f2a2d0c3" }, { "ImportPath": "sigs.k8s.io/kustomize/pkg/target", - "Rev": "0184d5b69700607a1b9280f303152e2959bc19e7" + "Rev": "ce7e5ee2c30cc5856fea01fe423cf167f2a2d0c3" }, { "ImportPath": "sigs.k8s.io/kustomize/pkg/transformers", - "Rev": "0184d5b69700607a1b9280f303152e2959bc19e7" + "Rev": "ce7e5ee2c30cc5856fea01fe423cf167f2a2d0c3" }, { "ImportPath": "sigs.k8s.io/kustomize/pkg/transformers/config", - "Rev": "0184d5b69700607a1b9280f303152e2959bc19e7" + "Rev": "ce7e5ee2c30cc5856fea01fe423cf167f2a2d0c3" }, { "ImportPath": "sigs.k8s.io/kustomize/pkg/transformers/config/defaultconfig", - "Rev": "0184d5b69700607a1b9280f303152e2959bc19e7" + "Rev": "ce7e5ee2c30cc5856fea01fe423cf167f2a2d0c3" }, { "ImportPath": "sigs.k8s.io/kustomize/pkg/types", - "Rev": "0184d5b69700607a1b9280f303152e2959bc19e7" + "Rev": "ce7e5ee2c30cc5856fea01fe423cf167f2a2d0c3" }, { "ImportPath": "sigs.k8s.io/structured-merge-diff/fieldpath", diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/BUILD b/staging/src/k8s.io/cli-runtime/pkg/kustomize/BUILD new file mode 100644 index 0000000000..8780b6b06f --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/BUILD @@ -0,0 +1,38 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["builder.go"], + importmap = "k8s.io/kubernetes/vendor/k8s.io/cli-runtime/pkg/kustomize", + importpath = "k8s.io/cli-runtime/pkg/kustomize", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/commands/build:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/fs:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [ + ":package-srcs", + "//staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps:all-srcs", + ], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) + +go_test( + name = "go_default_test", + srcs = ["builder_test.go"], + embed = [":go_default_library"], + deps = ["//vendor/sigs.k8s.io/kustomize/pkg/fs:go_default_library"], +) diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/BUILD b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/BUILD new file mode 100644 index 0000000000..1ff850ef37 --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/BUILD @@ -0,0 +1,39 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "doc.go", + "factory.go", + ], + importmap = "k8s.io/kubernetes/vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps", + importpath = "k8s.io/cli-runtime/pkg/kustomize/k8sdeps", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct:go_default_library", + "//staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer:go_default_library", + "//staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/validator:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/factory:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [ + ":package-srcs", + "//staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret:all-srcs", + "//staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct:all-srcs", + "//staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kv:all-srcs", + "//staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer:all-srcs", + "//staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/validator:all-srcs", + ], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/BUILD b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/BUILD new file mode 100644 index 0000000000..67c6bae6b5 --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/BUILD @@ -0,0 +1,53 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "configmapfactory.go", + "kv.go", + "secretfactory.go", + ], + importmap = "k8s.io/kubernetes/vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret", + importpath = "k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/api/core/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/validation:go_default_library", + "//staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kv:go_default_library", + "//vendor/github.com/pkg/errors:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/ifc:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/types:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = [ + "configmapfactory_test.go", + "kv_test.go", + "secretfactory_test.go", + ], + embed = [":go_default_library"], + deps = [ + "//staging/src/k8s.io/api/core/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kv:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/fs:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/loader:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/types:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/configmapfactory.go b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/configmapfactory.go new file mode 100644 index 0000000000..4525537364 --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/configmapfactory.go @@ -0,0 +1,126 @@ +/* +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 generates configmaps and secrets per generator rules. +package configmapandsecret + +import ( + "fmt" + "strings" + "unicode/utf8" + + "github.com/pkg/errors" + "k8s.io/api/core/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/validation" + "k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kv" + "sigs.k8s.io/kustomize/pkg/ifc" + "sigs.k8s.io/kustomize/pkg/types" +) + +// ConfigMapFactory makes ConfigMaps. +type ConfigMapFactory struct { + ldr ifc.Loader +} + +// NewConfigMapFactory returns a new ConfigMapFactory. +func NewConfigMapFactory(l ifc.Loader) *ConfigMapFactory { + return &ConfigMapFactory{ldr: l} +} + +func (f *ConfigMapFactory) makeFreshConfigMap( + args *types.ConfigMapArgs) *corev1.ConfigMap { + cm := &corev1.ConfigMap{} + cm.APIVersion = "v1" + cm.Kind = "ConfigMap" + cm.Name = args.Name + cm.Namespace = args.Namespace + cm.Data = map[string]string{} + return cm +} + +// MakeConfigMap returns a new ConfigMap, or nil and an error. +func (f *ConfigMapFactory) MakeConfigMap( + args *types.ConfigMapArgs, options *types.GeneratorOptions) (*corev1.ConfigMap, error) { + var all []kv.Pair + var err error + cm := f.makeFreshConfigMap(args) + + pairs, err := keyValuesFromEnvFile(f.ldr, args.EnvSource) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf( + "env source file: %s", + args.EnvSource)) + } + all = append(all, pairs...) + + pairs, err = keyValuesFromLiteralSources(args.LiteralSources) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf( + "literal sources %v", args.LiteralSources)) + } + all = append(all, pairs...) + + pairs, err = keyValuesFromFileSources(f.ldr, args.FileSources) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf( + "file sources: %v", args.FileSources)) + } + all = append(all, pairs...) + + for _, kv := range all { + err = addKvToConfigMap(cm, kv.Key, kv.Value) + if err != nil { + return nil, err + } + } + if options != nil { + cm.SetLabels(options.Labels) + cm.SetAnnotations(options.Annotations) + } + return cm, nil +} + +// addKvToConfigMap adds the given key and data to the given config map. +// Error if key invalid, or already exists. +func addKvToConfigMap(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, ";")) + } + + keyExistsErrorMsg := "cannot add key %s, another key by that name already exists: %v" + + // If the configmap data contains byte sequences that are all in the UTF-8 + // range, we will write it to .Data + if utf8.Valid([]byte(data)) { + if _, entryExists := configMap.Data[keyName]; entryExists { + return fmt.Errorf(keyExistsErrorMsg, keyName, configMap.Data) + } + configMap.Data[keyName] = data + return nil + } + + // otherwise, it's BinaryData + if configMap.BinaryData == nil { + configMap.BinaryData = map[string][]byte{} + } + if _, entryExists := configMap.BinaryData[keyName]; entryExists { + return fmt.Errorf(keyExistsErrorMsg, keyName, configMap.BinaryData) + } + configMap.BinaryData[keyName] = []byte(data) + return nil +} diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/configmapfactory_test.go b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/configmapfactory_test.go new file mode 100644 index 0000000000..3524a7bfdf --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/configmapfactory_test.go @@ -0,0 +1,154 @@ +/* +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 ( + "reflect" + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/kustomize/pkg/fs" + "sigs.k8s.io/kustomize/pkg/loader" + "sigs.k8s.io/kustomize/pkg/types" +) + +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 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 +`, + }, + BinaryData: map[string][]byte{ + "app.bin": {0xff, 0xfd}, + }, + } +} + +func makeLiteralConfigMap(name string) *corev1.ConfigMap { + cm := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Data: map[string]string{ + "a": "x", + "b": "y", + "c": "Hello World", + "d": "true", + }, + } + cm.SetLabels(map[string]string{"foo": "bar"}) + return cm +} + +func TestConstructConfigMap(t *testing.T) { + type testCase struct { + description string + input types.ConfigMapArgs + options *types.GeneratorOptions + expected *corev1.ConfigMap + } + + testCases := []testCase{ + { + description: "construct config map from env", + input: types.ConfigMapArgs{ + GeneratorArgs: types.GeneratorArgs{ + Name: "envConfigMap", + DataSources: types.DataSources{ + EnvSource: "configmap/app.env", + }, + }, + }, + options: nil, + expected: makeEnvConfigMap("envConfigMap"), + }, + { + description: "construct config map from file", + input: types.ConfigMapArgs{ + GeneratorArgs: types.GeneratorArgs{ + Name: "fileConfigMap", + DataSources: types.DataSources{ + FileSources: []string{"configmap/app-init.ini", "configmap/app.bin"}, + }, + }, + }, + options: nil, + expected: makeFileConfigMap("fileConfigMap"), + }, + { + description: "construct config map from literal", + input: types.ConfigMapArgs{ + GeneratorArgs: types.GeneratorArgs{ + Name: "literalConfigMap", + DataSources: types.DataSources{ + LiteralSources: []string{"a=x", "b=y", "c=\"Hello World\"", "d='true'"}, + }, + }, + }, + options: &types.GeneratorOptions{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + expected: makeLiteralConfigMap("literalConfigMap"), + }, + } + + fSys := fs.MakeFakeFS() + fSys.WriteFile("/configmap/app.env", []byte("DB_USERNAME=admin\nDB_PASSWORD=somepw\n")) + fSys.WriteFile("/configmap/app-init.ini", []byte("FOO=bar\nBAR=baz\n")) + fSys.WriteFile("/configmap/app.bin", []byte{0xff, 0xfd}) + f := NewConfigMapFactory(loader.NewFileLoaderAtRoot(fSys)) + for _, tc := range testCases { + cm, err := f.MakeConfigMap(&tc.input, tc.options) + 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) + } + } +} diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/kv.go b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/kv.go new file mode 100644 index 0000000000..893dfefc9c --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/kv.go @@ -0,0 +1,107 @@ +/* +Copyright 2019 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 ( + "fmt" + "path" + "strings" + + "github.com/pkg/errors" + "k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kv" + "sigs.k8s.io/kustomize/pkg/ifc" +) + +func keyValuesFromLiteralSources(sources []string) ([]kv.Pair, error) { + var kvs []kv.Pair + for _, s := range sources { + k, v, err := parseLiteralSource(s) + if err != nil { + return nil, err + } + kvs = append(kvs, kv.Pair{Key: k, Value: v}) + } + return kvs, nil +} + +func keyValuesFromFileSources(ldr ifc.Loader, sources []string) ([]kv.Pair, error) { + var kvs []kv.Pair + for _, s := range sources { + k, fPath, err := parseFileSource(s) + if err != nil { + return nil, err + } + content, err := ldr.Load(fPath) + if err != nil { + return nil, err + } + kvs = append(kvs, kv.Pair{Key: k, Value: string(content)}) + } + return kvs, nil +} + +func keyValuesFromEnvFile(l ifc.Loader, path string) ([]kv.Pair, error) { + if path == "" { + return nil, nil + } + content, err := l.Load(path) + if err != nil { + return nil, err + } + return kv.KeyValuesFromLines(content) +} + +// 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], strings.Trim(items[1], "\"'"), nil +} diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/kv_test.go b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/kv_test.go new file mode 100644 index 0000000000..236af02643 --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/kv_test.go @@ -0,0 +1,57 @@ +/* +Copyright 2019 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 ( + "reflect" + "testing" + + "k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kv" + "sigs.k8s.io/kustomize/pkg/fs" + "sigs.k8s.io/kustomize/pkg/loader" +) + +func TestKeyValuesFromFileSources(t *testing.T) { + tests := []struct { + description string + sources []string + expected []kv.Pair + }{ + { + description: "create kvs from file sources", + sources: []string{"files/app-init.ini"}, + expected: []kv.Pair{ + { + Key: "app-init.ini", + Value: "FOO=bar", + }, + }, + }, + } + + fSys := fs.MakeFakeFS() + fSys.WriteFile("/files/app-init.ini", []byte("FOO=bar")) + for _, tc := range tests { + kvs, err := keyValuesFromFileSources(loader.NewFileLoaderAtRoot(fSys), tc.sources) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(kvs, tc.expected) { + t.Fatalf("in testcase: %q updated:\n%#v\ndoesn't match expected:\n%#v\n", tc.description, kvs, tc.expected) + } + } +} diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/secretfactory.go b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/secretfactory.go new file mode 100644 index 0000000000..6e431a7e3e --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/secretfactory.go @@ -0,0 +1,106 @@ +/* +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 ( + "fmt" + "strings" + + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/validation" + "k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kv" + "sigs.k8s.io/kustomize/pkg/ifc" + "sigs.k8s.io/kustomize/pkg/types" +) + +// SecretFactory makes Secrets. +type SecretFactory struct { + ldr ifc.Loader +} + +// NewSecretFactory returns a new SecretFactory. +func NewSecretFactory(ldr ifc.Loader) *SecretFactory { + return &SecretFactory{ldr: ldr} +} + +func (f *SecretFactory) makeFreshSecret(args *types.SecretArgs) *corev1.Secret { + s := &corev1.Secret{} + s.APIVersion = "v1" + s.Kind = "Secret" + s.Name = args.Name + s.Namespace = args.Namespace + s.Type = corev1.SecretType(args.Type) + if s.Type == "" { + s.Type = corev1.SecretTypeOpaque + } + s.Data = map[string][]byte{} + return s +} + +// MakeSecret returns a new secret. +func (f *SecretFactory) MakeSecret(args *types.SecretArgs, options *types.GeneratorOptions) (*corev1.Secret, error) { + var all []kv.Pair + var err error + s := f.makeFreshSecret(args) + + pairs, err := keyValuesFromEnvFile(f.ldr, args.EnvSource) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf( + "env source file: %s", + args.EnvSource)) + } + all = append(all, pairs...) + + pairs, err = keyValuesFromLiteralSources(args.LiteralSources) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf( + "literal sources %v", args.LiteralSources)) + } + all = append(all, pairs...) + + pairs, err = keyValuesFromFileSources(f.ldr, args.FileSources) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf( + "file sources: %v", args.FileSources)) + } + all = append(all, pairs...) + + for _, kv := range all { + err = addKvToSecret(s, kv.Key, kv.Value) + if err != nil { + return nil, err + } + } + if options != nil { + s.SetLabels(options.Labels) + s.SetAnnotations(options.Annotations) + } + return s, nil +} + +func addKvToSecret(secret *corev1.Secret, keyName, data string) error { + // Note, the rules for SecretKeys keys are the exact same as the ones for ConfigMap. + 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", keyName) + } + secret.Data[keyName] = []byte(data) + return nil +} diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/secretfactory_test.go b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/secretfactory_test.go new file mode 100644 index 0000000000..da405a27f8 --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/secretfactory_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 configmapandsecret + +import ( + "reflect" + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/kustomize/pkg/fs" + "sigs.k8s.io/kustomize/pkg/loader" + "sigs.k8s.io/kustomize/pkg/types" +) + +func makeEnvSecret(name string) *corev1.Secret { + return &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Data: map[string][]byte{ + "DB_PASSWORD": []byte("somepw"), + "DB_USERNAME": []byte("admin"), + }, + Type: "Opaque", + } +} + +func makeFileSecret(name string) *corev1.Secret { + return &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Data: map[string][]byte{ + "app-init.ini": []byte(`FOO=bar +BAR=baz +`), + }, + Type: "Opaque", + } +} + +func makeLiteralSecret(name string) *corev1.Secret { + s := &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Data: map[string][]byte{ + "a": []byte("x"), + "b": []byte("y"), + }, + Type: "Opaque", + } + s.SetLabels(map[string]string{"foo": "bar"}) + return s +} + +func TestConstructSecret(t *testing.T) { + type testCase struct { + description string + input types.SecretArgs + options *types.GeneratorOptions + expected *corev1.Secret + } + + testCases := []testCase{ + { + description: "construct secret from env", + input: types.SecretArgs{ + GeneratorArgs: types.GeneratorArgs{ + Name: "envSecret", + DataSources: types.DataSources{ + EnvSource: "secret/app.env", + }, + }, + }, + options: nil, + expected: makeEnvSecret("envSecret"), + }, + { + description: "construct secret from file", + input: types.SecretArgs{ + GeneratorArgs: types.GeneratorArgs{ + Name: "fileSecret", + DataSources: types.DataSources{ + FileSources: []string{"secret/app-init.ini"}, + }, + }, + }, + options: nil, + expected: makeFileSecret("fileSecret"), + }, + { + description: "construct secret from literal", + input: types.SecretArgs{ + GeneratorArgs: types.GeneratorArgs{ + Name: "literalSecret", + DataSources: types.DataSources{ + LiteralSources: []string{"a=x", "b=y"}, + }, + }, + }, + options: &types.GeneratorOptions{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + expected: makeLiteralSecret("literalSecret"), + }, + } + + fSys := fs.MakeFakeFS() + fSys.WriteFile("/secret/app.env", []byte("DB_USERNAME=admin\nDB_PASSWORD=somepw\n")) + fSys.WriteFile("/secret/app-init.ini", []byte("FOO=bar\nBAR=baz\n")) + f := NewSecretFactory(loader.NewFileLoaderAtRoot(fSys)) + for _, tc := range testCases { + cm, err := f.MakeSecret(&tc.input, tc.options) + 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) + } + } +} diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/doc.go b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/doc.go new file mode 100644 index 0000000000..c98cb8d680 --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/doc.go @@ -0,0 +1,76 @@ +/* +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. +*/ + +// It's possible that kustomize's features will be vendored into +// the kubernetes/kubernetes repo and made available to kubectl +// commands, while at the same time the kustomize program will +// continue to exist as an independent CLI. Vendoring snapshots +// would be taken just before a kubectl release. +// +// This creates a problem in that freestanding-kustomize depends on +// (for example): +// +// https://github.com/kubernetes/apimachinery/ +// tree/master/pkg/util/yaml +// +// It vendors that package into +// sigs.k8s.io/kustomize/vendor/k8s.io/apimachinery/ +// +// Whereas kubectl-kustomize would have to depend on the "staging" +// version of this code, located at +// +// https://github.com/kubernetes/kubernetes/ +// blob/master/staging/src/k8s.io/apimachinery/pkg/util/yaml +// +// which is "vendored" via symlinks: +// k8s.io/kubernetes/vendor/k8s.io/apimachinery +// is a symlink to +// ../../staging/src/k8s.io/apimachinery +// +// The staging version is the canonical, under-development +// version of the code that kubectl depends on, whereas the packages +// at kubernetes/apimachinery are periodic snapshots of staging made +// for outside tools to depend on. +// +// apimachinery isn't the only package that poses this problem, just +// using it as a specific example. +// +// The kubectl binary cannot vendor in kustomize code that in +// turn vendors in the non-staging packages. +// +// One way to fix some of this would be to copy code - a hard fork. +// This has all the problems associated with a hard forking. +// +// Another way would be to break the kustomize repo into three: +// +// (1) kustomize - repo with the main() function, +// vendoring (2) and (3). +// +// (2) kustomize-libs - packages used by (1) with no +// apimachinery dependence. +// +// (3) kustomize-k8sdeps - A thin code layer that depends +// on (vendors) apimachinery to provide thin implementations +// to interfaces used in (2). +// +// The kubectl repo would then vendor from (2) only, and have +// a local implementation of (3). With that in mind, it's clear +// that (3) doesn't have to be a repo; the kustomize version of +// the thin layer can live directly in (1). +// +// This package is the code in (3), meant for kustomize. + +package k8sdeps diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/factory.go b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/factory.go new file mode 100644 index 0000000000..a83b4bdaae --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/factory.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 k8sdeps provides kustomize factory with k8s dependencies +package k8sdeps + +import ( + "k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct" + "k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer" + "k8s.io/cli-runtime/pkg/kustomize/k8sdeps/validator" + "sigs.k8s.io/kustomize/pkg/factory" +) + +// NewFactory creates an instance of KustFactory using k8sdeps factories +func NewFactory() *factory.KustFactory { + return factory.NewKustFactory( + kunstruct.NewKunstructuredFactoryImpl(), + validator.NewKustValidator(), + transformer.NewFactoryImpl(), + ) +} diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/BUILD b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/BUILD new file mode 100644 index 0000000000..92105687e4 --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/BUILD @@ -0,0 +1,47 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "factory.go", + "helper.go", + "kunstruct.go", + ], + importmap = "k8s.io/kubernetes/vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct", + importpath = "k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/yaml:go_default_library", + "//staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/gvk:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/ifc:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/types:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = [ + "factory_test.go", + "kunstruct_test.go", + ], + embed = [":go_default_library"], + deps = ["//vendor/sigs.k8s.io/kustomize/pkg/ifc:go_default_library"], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/factory.go b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/factory.go new file mode 100644 index 0000000000..4e001aaa05 --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/factory.go @@ -0,0 +1,118 @@ +/* +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 kunstruct + +import ( + "bytes" + "fmt" + "io" + "strings" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret" + "sigs.k8s.io/kustomize/pkg/ifc" + "sigs.k8s.io/kustomize/pkg/types" +) + +// KunstructuredFactoryImpl hides construction using apimachinery types. +type KunstructuredFactoryImpl struct { + cmFactory *configmapandsecret.ConfigMapFactory + secretFactory *configmapandsecret.SecretFactory +} + +var _ ifc.KunstructuredFactory = &KunstructuredFactoryImpl{} + +// NewKunstructuredFactoryImpl returns a factory. +func NewKunstructuredFactoryImpl() ifc.KunstructuredFactory { + return &KunstructuredFactoryImpl{} +} + +// SliceFromBytes returns a slice of Kunstructured. +func (kf *KunstructuredFactoryImpl) SliceFromBytes( + in []byte) ([]ifc.Kunstructured, error) { + decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(in), 1024) + var result []ifc.Kunstructured + var err error + for err == nil || isEmptyYamlError(err) { + var out unstructured.Unstructured + err = decoder.Decode(&out) + if err == nil { + if len(out.Object) == 0 { + continue + } + err = kf.validate(out) + if err != nil { + return nil, err + } + result = append(result, &UnstructAdapter{Unstructured: out}) + } + } + if err != io.EOF { + return nil, err + } + return result, nil +} + +func isEmptyYamlError(err error) bool { + return strings.Contains(err.Error(), "is missing in 'null'") +} + +// FromMap returns an instance of Kunstructured. +func (kf *KunstructuredFactoryImpl) FromMap( + m map[string]interface{}) ifc.Kunstructured { + return &UnstructAdapter{Unstructured: unstructured.Unstructured{Object: m}} +} + +// MakeConfigMap returns an instance of Kunstructured for ConfigMap +func (kf *KunstructuredFactoryImpl) MakeConfigMap(args *types.ConfigMapArgs, options *types.GeneratorOptions) (ifc.Kunstructured, error) { + cm, err := kf.cmFactory.MakeConfigMap(args, options) + if err != nil { + return nil, err + } + return NewKunstructuredFromObject(cm) +} + +// MakeSecret returns an instance of Kunstructured for Secret +func (kf *KunstructuredFactoryImpl) MakeSecret(args *types.SecretArgs, options *types.GeneratorOptions) (ifc.Kunstructured, error) { + sec, err := kf.secretFactory.MakeSecret(args, options) + if err != nil { + return nil, err + } + return NewKunstructuredFromObject(sec) +} + +// Set sets loader +func (kf *KunstructuredFactoryImpl) Set(ldr ifc.Loader) { + kf.cmFactory = configmapandsecret.NewConfigMapFactory(ldr) + kf.secretFactory = configmapandsecret.NewSecretFactory(ldr) +} + +// validate validates that u has kind and name +// except for kind `List`, which doesn't require a name +func (kf *KunstructuredFactoryImpl) validate(u unstructured.Unstructured) error { + kind := u.GetKind() + if kind == "" { + return fmt.Errorf("missing kind in object %v", u) + } else if kind == "List" { + return nil + } + if u.GetName() == "" { + return fmt.Errorf("missing metadata.name in object %v", u) + } + return nil +} diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/factory_test.go b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/factory_test.go new file mode 100644 index 0000000000..51e2be793c --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/factory_test.go @@ -0,0 +1,175 @@ +/* +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 kunstruct + +import ( + "reflect" + "testing" + + "sigs.k8s.io/kustomize/pkg/ifc" +) + +func TestSliceFromBytes(t *testing.T) { + factory := NewKunstructuredFactoryImpl() + testConfigMap := factory.FromMap( + map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "winnie", + }, + }) + testList := factory.FromMap( + map[string]interface{}{ + "apiVersion": "v1", + "kind": "List", + "items": []interface{}{ + testConfigMap.Map(), + testConfigMap.Map(), + }, + }) + + tests := []struct { + name string + input []byte + expectedOut []ifc.Kunstructured + expectedErr bool + }{ + { + name: "garbage", + input: []byte("garbageIn: garbageOut"), + expectedOut: []ifc.Kunstructured{}, + expectedErr: true, + }, + { + name: "noBytes", + input: []byte{}, + expectedOut: []ifc.Kunstructured{}, + expectedErr: false, + }, + { + name: "goodJson", + input: []byte(` +{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"winnie"}} +`), + expectedOut: []ifc.Kunstructured{testConfigMap}, + expectedErr: false, + }, + { + name: "goodYaml1", + input: []byte(` +apiVersion: v1 +kind: ConfigMap +metadata: + name: winnie +`), + expectedOut: []ifc.Kunstructured{testConfigMap}, + expectedErr: false, + }, + { + name: "goodYaml2", + input: []byte(` +apiVersion: v1 +kind: ConfigMap +metadata: + name: winnie +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: winnie +`), + expectedOut: []ifc.Kunstructured{testConfigMap, testConfigMap}, + expectedErr: false, + }, + { + name: "garbageInOneOfTwoObjects", + input: []byte(` +apiVersion: v1 +kind: ConfigMap +metadata: + name: winnie +--- +WOOOOOOOOOOOOOOOOOOOOOOOOT: woot +`), + expectedOut: []ifc.Kunstructured{}, + expectedErr: true, + }, + { + name: "emptyObjects", + input: []byte(` +--- +#a comment + +--- + +`), + expectedOut: []ifc.Kunstructured{}, + expectedErr: false, + }, + { + name: "Missing .metadata.name in object", + input: []byte(` +apiVersion: v1 +kind: Namespace +metadata: + annotations: + foo: bar +`), + expectedOut: nil, + expectedErr: true, + }, + { + name: "List", + input: []byte(` +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: ConfigMap + metadata: + name: winnie +- apiVersion: v1 + kind: ConfigMap + metadata: + name: winnie +`), + expectedOut: []ifc.Kunstructured{testList}, + expectedErr: false, + }, + } + + for _, test := range tests { + rs, err := factory.SliceFromBytes(test.input) + if test.expectedErr && err == nil { + t.Fatalf("%v: should return error", test.name) + } + if !test.expectedErr && err != nil { + t.Fatalf("%v: unexpected error: %s", test.name, err) + } + if len(rs) != len(test.expectedOut) { + t.Fatalf("%s: length mismatch %d != %d", + test.name, len(rs), len(test.expectedOut)) + } + for i := range rs { + if !reflect.DeepEqual(test.expectedOut[i], rs[i]) { + t.Fatalf("%s: Got: %v\nexpected:%v", + test.name, test.expectedOut[i], rs[i]) + } + } + } +} diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/helper.go b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/helper.go new file mode 100644 index 0000000000..0675b961de --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/helper.go @@ -0,0 +1,71 @@ +/* +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 kunstruct provides unstructured from api machinery and factory for creating unstructured +package kunstruct + +import ( + "fmt" + "strings" +) + +func parseFields(path string) ([]string, error) { + if !strings.Contains(path, "[") { + return strings.Split(path, "."), nil + } + + var fields []string + start := 0 + insideParentheses := false + for i := range path { + switch path[i] { + case '.': + if !insideParentheses { + fields = append(fields, path[start:i]) + start = i + 1 + } + case '[': + if !insideParentheses { + if i == start { + start = i + 1 + } else { + fields = append(fields, path[start:i]) + start = i + 1 + } + insideParentheses = true + } else { + return nil, fmt.Errorf("nested parentheses are not allowed: %s", path) + } + case ']': + if insideParentheses { + fields = append(fields, path[start:i]) + start = i + 1 + insideParentheses = false + } else { + return nil, fmt.Errorf("invalid field path %s", path) + } + } + } + if start < len(path)-1 { + fields = append(fields, path[start:]) + } + for i, f := range fields { + if strings.HasPrefix(f, "\"") || strings.HasPrefix(f, "'") { + fields[i] = strings.Trim(f, "\"'") + } + } + return fields, nil +} diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/kunstruct.go b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/kunstruct.go new file mode 100644 index 0000000000..5ad306bf56 --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/kunstruct.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 kunstruct provides unstructured from api machinery and factory for creating unstructured +package kunstruct + +import ( + "encoding/json" + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/kustomize/pkg/gvk" + "sigs.k8s.io/kustomize/pkg/ifc" +) + +var _ ifc.Kunstructured = &UnstructAdapter{} + +// UnstructAdapter wraps unstructured.Unstructured from +// https://github.com/kubernetes/apimachinery/blob/master/ +// pkg/apis/meta/v1/unstructured/unstructured.go +// to isolate dependence on apimachinery. +type UnstructAdapter struct { + unstructured.Unstructured +} + +// NewKunstructuredFromObject returns a new instance of Kunstructured. +func NewKunstructuredFromObject(obj runtime.Object) (ifc.Kunstructured, error) { + // Convert obj to a byte stream, then convert that to JSON (Unstructured). + marshaled, err := json.Marshal(obj) + if err != nil { + return &UnstructAdapter{}, err + } + var u unstructured.Unstructured + err = u.UnmarshalJSON(marshaled) + // creationTimestamp always 'null', remove it + u.SetCreationTimestamp(metav1.Time{}) + return &UnstructAdapter{Unstructured: u}, err +} + +// GetGvk returns the Gvk name of the object. +func (fs *UnstructAdapter) GetGvk() gvk.Gvk { + x := fs.GroupVersionKind() + return gvk.Gvk{ + Group: x.Group, + Version: x.Version, + Kind: x.Kind, + } +} + +// Copy provides a copy behind an interface. +func (fs *UnstructAdapter) Copy() ifc.Kunstructured { + return &UnstructAdapter{*fs.DeepCopy()} +} + +// Map returns the unstructured content map. +func (fs *UnstructAdapter) Map() map[string]interface{} { + return fs.Object +} + +// SetMap overrides the unstructured content map. +func (fs *UnstructAdapter) SetMap(m map[string]interface{}) { + fs.Object = m +} + +// GetFieldValue returns value at the given fieldpath. +func (fs *UnstructAdapter) GetFieldValue(path string) (string, error) { + fields, err := parseFields(path) + if err != nil { + return "", err + } + s, found, err := unstructured.NestedString( + fs.UnstructuredContent(), fields...) + if found || err != nil { + return s, err + } + return "", fmt.Errorf("no field named '%s'", path) +} diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/kunstruct_test.go b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/kunstruct_test.go new file mode 100644 index 0000000000..dc7eb8bb2a --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/kunstruct_test.go @@ -0,0 +1,148 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kunstruct + +import ( + "testing" +) + +func TestGetFieldValue(t *testing.T) { + factory := NewKunstructuredFactoryImpl() + kunstructured := factory.FromMap(map[string]interface{}{ + "Kind": "Service", + "metadata": map[string]interface{}{ + "labels": map[string]string{ + "app": "application-name", + }, + "name": "service-name", + }, + "spec": map[string]interface{}{ + "ports": map[string]interface{}{ + "port": "80", + }, + }, + "this": map[string]interface{}{ + "is": map[string]interface{}{ + "aNumber": 1000, + "aNilValue": nil, + "anEmptyMap": map[string]interface{}{}, + "unrecognizable": testing.InternalExample{ + Name: "fooBar", + }, + }, + }, + }) + + tests := []struct { + name string + pathToField string + expectedValue string + errorExpected bool + errorMsg string + }{ + { + name: "oneField", + pathToField: "Kind", + expectedValue: "Service", + errorExpected: false, + }, + { + name: "twoFields", + pathToField: "metadata.name", + expectedValue: "service-name", + errorExpected: false, + }, + { + name: "threeFields", + pathToField: "spec.ports.port", + expectedValue: "80", + errorExpected: false, + }, + { + name: "empty", + pathToField: "", + errorExpected: true, + errorMsg: "no field named ''", + }, + { + name: "emptyDotEmpty", + pathToField: ".", + errorExpected: true, + errorMsg: "no field named '.'", + }, + { + name: "twoFieldsOneMissing", + pathToField: "metadata.banana", + errorExpected: true, + errorMsg: "no field named 'metadata.banana'", + }, + { + name: "deeperMissingField", + pathToField: "this.is.aDeep.field.that.does.not.exist", + errorExpected: true, + errorMsg: "no field named 'this.is.aDeep.field.that.does.not.exist'", + }, + { + name: "emptyMap", + pathToField: "this.is.anEmptyMap", + errorExpected: true, + errorMsg: ".this.is.anEmptyMap accessor error: map[] is of the type map[string]interface {}, expected string", + }, + { + name: "numberAsValue", + pathToField: "this.is.aNumber", + errorExpected: true, + errorMsg: ".this.is.aNumber accessor error: 1000 is of the type int, expected string", + }, + { + name: "nilAsValue", + pathToField: "this.is.aNilValue", + errorExpected: true, + errorMsg: ".this.is.aNilValue accessor error: is of the type , expected string", + }, + { + name: "unrecognizable", + pathToField: "this.is.unrecognizable.Name", + errorExpected: true, + errorMsg: ".this.is.unrecognizable.Name accessor error: {fooBar false} is of the type testing.InternalExample, expected map[string]interface{}", + }, + } + + for _, test := range tests { + s, err := kunstructured.GetFieldValue(test.pathToField) + if test.errorExpected { + if err == nil { + t.Fatalf("%q; path %q - should return error, but no error returned", + test.name, test.pathToField) + } + if test.errorMsg != err.Error() { + t.Fatalf("%q; path %q - expected error: \"%s\", got error: \"%v\"", + test.name, test.pathToField, test.errorMsg, err.Error()) + } + continue + } + if err != nil { + t.Fatalf("%q; path %q - unexpected error %v", + test.name, test.pathToField, err) + } + if test.expectedValue != s { + t.Fatalf("%q; Got: %s expected: %s", + test.name, s, test.expectedValue) + } + + } +} diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kv/BUILD b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kv/BUILD new file mode 100644 index 0000000000..896f3c73e8 --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kv/BUILD @@ -0,0 +1,30 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["kv.go"], + importmap = "k8s.io/kubernetes/vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kv", + importpath = "k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kv", + visibility = ["//visibility:public"], + deps = ["//staging/src/k8s.io/apimachinery/pkg/util/validation:go_default_library"], +) + +go_test( + name = "go_default_test", + srcs = ["kv_test.go"], + embed = [":go_default_library"], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kv/kv.go b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kv/kv.go new file mode 100644 index 0000000000..27b8b3431e --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kv/kv.go @@ -0,0 +1,102 @@ +/* +Copyright 2019 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 kv + +import ( + "bufio" + "bytes" + "fmt" + "os" + "strings" + "unicode" + "unicode/utf8" + + "k8s.io/apimachinery/pkg/util/validation" +) + +// Pair represents a pair. +type Pair struct { + Key string + Value string +} + +var utf8bom = []byte{0xEF, 0xBB, 0xBF} + +// KeyValuesFromLines parses given content in to a list of key-value pairs. +func KeyValuesFromLines(content []byte) ([]Pair, error) { + var kvs []Pair + + 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 := KeyValuesFromLine(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 +} + +// KeyValuesFromLine 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 KeyValuesFromLine(line []byte, currentLine int) (Pair, error) { + kv := Pair{} + + 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/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kv/kv_test.go b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kv/kv_test.go new file mode 100644 index 0000000000..f854a917f5 --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kv/kv_test.go @@ -0,0 +1,68 @@ +/* +Copyright 2019 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 kv + +import ( + "reflect" + "testing" +) + +func TestKeyValuesFromLines(t *testing.T) { + tests := []struct { + desc string + content string + expectedPairs []Pair + expectedErr bool + }{ + { + desc: "valid kv content parse", + content: ` + k1=v1 + k2=v2 + `, + expectedPairs: []Pair{ + {Key: "k1", Value: "v1"}, + {Key: "k2", Value: "v2"}, + }, + expectedErr: false, + }, + { + desc: "content with comments", + content: ` + k1=v1 + #k2=v2 + `, + expectedPairs: []Pair{ + {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/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/BUILD b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/BUILD new file mode 100644 index 0000000000..b6ab75a107 --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/BUILD @@ -0,0 +1,33 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["factory.go"], + importmap = "k8s.io/kubernetes/vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer", + importpath = "k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash:go_default_library", + "//staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/patch:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/resource:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/transformers:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [ + ":package-srcs", + "//staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash:all-srcs", + "//staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/patch:all-srcs", + ], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/factory.go b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/factory.go new file mode 100644 index 0000000000..bc435b3797 --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/factory.go @@ -0,0 +1,43 @@ +/* +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 transformer provides transformer factory +package transformer + +import ( + "k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash" + "k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/patch" + "sigs.k8s.io/kustomize/pkg/resource" + "sigs.k8s.io/kustomize/pkg/transformers" +) + +// FactoryImpl makes patch transformer and name hash transformer +type FactoryImpl struct{} + +// NewFactoryImpl makes a new factoryImpl instance +func NewFactoryImpl() *FactoryImpl { + return &FactoryImpl{} +} + +// MakePatchTransformer makes a new patch transformer +func (p *FactoryImpl) MakePatchTransformer(slice []*resource.Resource, rf *resource.Factory) (transformers.Transformer, error) { + return patch.NewPatchTransformer(slice, rf) +} + +// MakeHashTransformer makes a new name hash transformer +func (p *FactoryImpl) MakeHashTransformer() transformers.Transformer { + return hash.NewNameHashTransformer() +} diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash/BUILD b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash/BUILD new file mode 100644 index 0000000000..1b2a78eb67 --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash/BUILD @@ -0,0 +1,50 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "hash.go", + "namehash.go", + ], + importmap = "k8s.io/kubernetes/vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash", + importpath = "k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/api/core/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/resmap:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/transformers:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = [ + "hash_test.go", + "namehash_test.go", + ], + embed = [":go_default_library"], + deps = [ + "//staging/src/k8s.io/api/core/v1:go_default_library", + "//staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/gvk:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/resid:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/resmap:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/resource:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/types:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash/hash.go b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash/hash.go new file mode 100644 index 0000000000..17e24ff3e6 --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash/hash.go @@ -0,0 +1,168 @@ +/* +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" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// KustHash compute hash for unstructured objects +type KustHash struct{} + +// NewKustHash returns a KustHash object +func NewKustHash() *KustHash { + return &KustHash{} +} + +// Hash returns a hash of either a ConfigMap or a Secret +func (h *KustHash) Hash(m map[string]interface{}) (string, error) { + u := unstructured.Unstructured{ + Object: m, + } + kind := u.GetKind() + switch kind { + case "ConfigMap": + cm, err := unstructuredToConfigmap(u) + if err != nil { + return "", err + } + return ConfigMapHash(cm) + case "Secret": + sec, err := unstructuredToSecret(u) + + if err != nil { + return "", err + } + return SecretHash(sec) + default: + return "", fmt.Errorf("type %s is supported for hashing in %v", kind, m) + } +} + +// 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 + m := map[string]interface{}{"kind": "ConfigMap", "name": cm.Name, "data": cm.Data} + if len(cm.BinaryData) > 0 { + m["binaryData"] = cm.BinaryData + } + data, err := json.Marshal(m) + 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))) +} + +func unstructuredToConfigmap(u unstructured.Unstructured) (*v1.ConfigMap, error) { + marshaled, err := json.Marshal(u.Object) + if err != nil { + return nil, err + } + var out v1.ConfigMap + err = json.Unmarshal(marshaled, &out) + return &out, err +} + +func unstructuredToSecret(u unstructured.Unstructured) (*v1.Secret, error) { + marshaled, err := json.Marshal(u.Object) + if err != nil { + return nil, err + } + var out v1.Secret + err = json.Unmarshal(marshaled, &out) + return &out, err +} diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash/hash_test.go b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash/hash_test.go new file mode 100644 index 0000000000..2d336f35a8 --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash/hash_test.go @@ -0,0 +1,208 @@ +/* +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" + "sigs.k8s.io/kustomize/pkg/gvk" +) + +var service = gvk.Gvk{Version: "v1", Kind: "Service"} +var secret = gvk.Gvk{Version: "v1", Kind: "Secret"} +var cmap = gvk.Gvk{Version: "v1", Kind: "ConfigMap"} +var deploy = gvk.Gvk{Group: "apps", Version: "v1", Kind: "Deployment"} + +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{}, BinaryData: map[string][]byte{}}, "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", ""}, + // empty binary data map + {"empty binary data", &v1.ConfigMap{BinaryData: map[string][]byte{}}, "dk855m5d49", ""}, + // one key with binary data + {"one key with binary data", &v1.ConfigMap{BinaryData: map[string][]byte{"one": []byte("")}}, "mk79584b8c", ""}, + // three keys with binary data (tests sorting order) + {"three keys with binary data", &v1.ConfigMap{BinaryData: map[string][]byte{"two": []byte("2"), "one": []byte(""), "three": []byte("3")}}, "t458mc6db2", ""}, + // two keys, one with string and another with binary data + {"two keys with one each", &v1.ConfigMap{Data: map[string]string{"one": ""}, BinaryData: map[string][]byte{"two": []byte("")}}, "698h7c7t9m", ""}, + } + + 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":""}`, ""}, + // empty binary map + {"empty data", &v1.ConfigMap{BinaryData: map[string][]byte{}}, `{"data":null,"kind":"ConfigMap","name":""}`, ""}, + // one key with binary data + {"one key", &v1.ConfigMap{BinaryData: map[string][]byte{"one": []byte("")}}, + `{"binaryData":{"one":""},"data":null,"kind":"ConfigMap","name":""}`, ""}, + // three keys with binary data (tests sorting order) + {"three keys", &v1.ConfigMap{BinaryData: map[string][]byte{"two": []byte("2"), "one": []byte(""), "three": []byte("3")}}, + `{"binaryData":{"one":"","three":"Mw==","two":"Mg=="},"data":null,"kind":"ConfigMap","name":""}`, ""}, + // two keys, one string and one binary values + {"two keys with one each", &v1.ConfigMap{Data: map[string]string{"one": ""}, BinaryData: map[string][]byte{"two": []byte("")}}, + `{"binaryData":{"two":""},"data":{"one":""},"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{}, 4}, + {"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/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash/namehash.go b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash/namehash.go new file mode 100644 index 0000000000..a52072e8ad --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash/namehash.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 hash + +import ( + "fmt" + + "sigs.k8s.io/kustomize/pkg/resmap" + "sigs.k8s.io/kustomize/pkg/transformers" +) + +type nameHashTransformer struct{} + +var _ transformers.Transformer = &nameHashTransformer{} + +// NewNameHashTransformer construct a nameHashTransformer. +func NewNameHashTransformer() transformers.Transformer { + return &nameHashTransformer{} +} + +// Transform appends hash to generated resources. +func (o *nameHashTransformer) Transform(m resmap.ResMap) error { + for _, res := range m { + if res.NeedHashSuffix() { + h, err := NewKustHash().Hash(res.Map()) + if err != nil { + return err + } + res.SetName(fmt.Sprintf("%s-%s", res.GetName(), h)) + } + } + return nil +} diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash/namehash_test.go b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash/namehash_test.go new file mode 100644 index 0000000000..b97e4a2e2a --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash/namehash_test.go @@ -0,0 +1,162 @@ +/* +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 hash + +import ( + "reflect" + "testing" + + "k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct" + "sigs.k8s.io/kustomize/pkg/resid" + "sigs.k8s.io/kustomize/pkg/resmap" + "sigs.k8s.io/kustomize/pkg/resource" + "sigs.k8s.io/kustomize/pkg/types" +) + +func TestNameHashTransformer(t *testing.T) { + rf := resource.NewFactory( + kunstruct.NewKunstructuredFactoryImpl()) + objs := resmap.ResMap{ + resid.NewResId(cmap, "cm1"): rf.FromMap( + map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "cm1", + }, + }), + resid.NewResId(deploy, "deploy1"): rf.FromMap( + 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", + }, + }, + }, + }, + }, + }), + resid.NewResId(service, "svc1"): rf.FromMap( + 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", + }, + }, + }, + }), + resid.NewResId(secret, "secret1"): rf.FromMapAndOption( + map[string]interface{}{ + "apiVersion": "v1", + "kind": "Secret", + "metadata": map[string]interface{}{ + "name": "secret1", + }, + }, &types.GeneratorArgs{Behavior: "create"}, &types.GeneratorOptions{DisableNameSuffixHash: false}), + } + + expected := resmap.ResMap{ + resid.NewResId(cmap, "cm1"): rf.FromMap( + map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "cm1", + }, + }), + resid.NewResId(deploy, "deploy1"): rf.FromMap( + 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", + }, + }, + }, + }, + }, + }), + resid.NewResId(service, "svc1"): rf.FromMap( + 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", + }, + }, + }, + }), + resid.NewResId(secret, "secret1"): rf.FromMapAndOption( + map[string]interface{}{ + "apiVersion": "v1", + "kind": "Secret", + "metadata": map[string]interface{}{ + "name": "secret1-7kc45hd5f7", + }, + }, &types.GeneratorArgs{Behavior: "create"}, &types.GeneratorOptions{DisableNameSuffixHash: false}), + } + + tran := NewNameHashTransformer() + tran.Transform(objs) + + if !reflect.DeepEqual(objs, expected) { + err := expected.ErrorIfNotEqual(objs) + t.Fatalf("actual doesn't match expected: %v", err) + } +} diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/patch/BUILD b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/patch/BUILD new file mode 100644 index 0000000000..0cb7910dce --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/patch/BUILD @@ -0,0 +1,51 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "patch.go", + "patchconflictdetector.go", + ], + importmap = "k8s.io/kubernetes/vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/patch", + importpath = "k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/patch", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/mergepatch:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/strategicpatch:go_default_library", + "//staging/src/k8s.io/client-go/kubernetes/scheme:go_default_library", + "//vendor/github.com/evanphx/json-patch:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/gvk:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/resmap:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/resource:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/transformers:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["patch_test.go"], + embed = [":go_default_library"], + deps = [ + "//staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/gvk:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/resid:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/resmap:go_default_library", + "//vendor/sigs.k8s.io/kustomize/pkg/resource:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/patch/patch.go b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/patch/patch.go new file mode 100644 index 0000000000..4a2764bfbd --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/patch/patch.go @@ -0,0 +1,174 @@ +/* +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 patch + +import ( + "encoding/json" + "fmt" + + "github.com/evanphx/json-patch" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/strategicpatch" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/kustomize/pkg/gvk" + "sigs.k8s.io/kustomize/pkg/resmap" + "sigs.k8s.io/kustomize/pkg/resource" + "sigs.k8s.io/kustomize/pkg/transformers" +) + +// patchTransformer applies patches. +type patchTransformer struct { + patches []*resource.Resource + rf *resource.Factory +} + +var _ transformers.Transformer = &patchTransformer{} + +// NewPatchTransformer constructs a patchTransformer. +func NewPatchTransformer( + slice []*resource.Resource, rf *resource.Factory) (transformers.Transformer, error) { + if len(slice) == 0 { + return transformers.NewNoOpTransformer(), nil + } + return &patchTransformer{patches: slice, rf: rf}, nil +} + +// Transform apply the patches on top of the base resources. +func (pt *patchTransformer) Transform(baseResourceMap resmap.ResMap) error { + // Merge and then index the patches by Id. + patches, err := pt.mergePatches() + if err != nil { + return err + } + + // Strategic merge the resources exist in both base and patches. + for _, patch := range patches { + // Merge patches with base resource. + id := patch.Id() + matchedIds := baseResourceMap.FindByGVKN(id) + if len(matchedIds) == 0 { + return fmt.Errorf("failed to find an object with %s to apply the patch", id.GvknString()) + } + if len(matchedIds) > 1 { + return fmt.Errorf("found multiple objects %#v targeted by patch %#v (ambiguous)", matchedIds, id) + } + id = matchedIds[0] + base := baseResourceMap[id] + merged := map[string]interface{}{} + versionedObj, err := scheme.Scheme.New(toSchemaGvk(id.Gvk())) + baseName := base.GetName() + switch { + case runtime.IsNotRegisteredError(err): + // Use JSON merge patch to handle types w/o schema + baseBytes, err := json.Marshal(base.Map()) + if err != nil { + return err + } + patchBytes, err := json.Marshal(patch.Map()) + 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 patched object. + lookupPatchMeta, err := strategicpatch.NewPatchMetaFromStruct(versionedObj) + if err != nil { + return err + } + merged, err = strategicpatch.StrategicMergeMapPatchUsingLookupPatchMeta( + base.Map(), + patch.Map(), + lookupPatchMeta) + if err != nil { + return err + } + } + base.SetName(baseName) + baseResourceMap[id].SetMap(merged) + } + return nil +} + +// mergePatches merge and index patches by Id. +// It errors out if there is conflict between patches. +func (pt *patchTransformer) mergePatches() (resmap.ResMap, error) { + rc := resmap.ResMap{} + for ix, patch := range pt.patches { + id := patch.Id() + existing, found := rc[id] + if !found { + rc[id] = patch + continue + } + + versionedObj, err := scheme.Scheme.New(toSchemaGvk(id.Gvk())) + if err != nil && !runtime.IsNotRegisteredError(err) { + return nil, err + } + var cd conflictDetector + if err != nil { + cd = newJMPConflictDetector(pt.rf) + } else { + cd, err = newSMPConflictDetector(versionedObj, pt.rf) + if err != nil { + return nil, err + } + } + + conflict, err := cd.hasConflict(existing, patch) + if err != nil { + return nil, err + } + if conflict { + conflictingPatch, err := cd.findConflict(ix, pt.patches) + if err != nil { + return nil, err + } + return nil, fmt.Errorf( + "conflict between %#v and %#v", + conflictingPatch.Map(), patch.Map()) + } + merged, err := cd.mergePatches(existing, patch) + if err != nil { + return nil, err + } + rc[id] = merged + } + return rc, nil +} + +// toSchemaGvk converts to a schema.GroupVersionKind. +func toSchemaGvk(x gvk.Gvk) schema.GroupVersionKind { + return schema.GroupVersionKind{ + Group: x.Group, + Version: x.Version, + Kind: x.Kind, + } +} diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/patch/patch_test.go b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/patch/patch_test.go new file mode 100644 index 0000000000..1505abe271 --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/patch/patch_test.go @@ -0,0 +1,563 @@ +/* +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 patch + +import ( + "reflect" + "strings" + "testing" + + "k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct" + "sigs.k8s.io/kustomize/pkg/gvk" + "sigs.k8s.io/kustomize/pkg/resid" + "sigs.k8s.io/kustomize/pkg/resmap" + "sigs.k8s.io/kustomize/pkg/resource" +) + +var rf = resource.NewFactory( + kunstruct.NewKunstructuredFactoryImpl()) +var deploy = gvk.Gvk{Group: "apps", Version: "v1", Kind: "Deployment"} +var foo = gvk.Gvk{Group: "example.com", Version: "v1", Kind: "Foo"} + +func TestOverlayRun(t *testing.T) { + base := resmap.ResMap{ + resid.NewResId(deploy, "deploy1"): rf.FromMap( + 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", + }, + }, + }, + }, + }, + }), + } + patch := []*resource.Resource{ + rf.FromMap(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 := resmap.ResMap{ + resid.NewResId(deploy, "deploy1"): rf.FromMap( + 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 := NewPatchTransformer(patch, rf) + 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 = expected.ErrorIfNotEqual(base) + t.Fatalf("actual doesn't match expected: %v", err) + } +} + +func TestMultiplePatches(t *testing.T) { + base := resmap.ResMap{ + resid.NewResId(deploy, "deploy1"): rf.FromMap( + 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", + }, + }, + }, + }, + }, + }), + } + patch := []*resource.Resource{ + rf.FromMap(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", + }, + }, + }, + }, + }, + }, + }, + }, + ), + rf.FromMap(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 := resmap.ResMap{ + resid.NewResId(deploy, "deploy1"): rf.FromMap( + 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 := NewPatchTransformer(patch, rf) + 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 = expected.ErrorIfNotEqual(base) + t.Fatalf("actual doesn't match expected: %v", err) + } +} + +func TestMultiplePatchesWithConflict(t *testing.T) { + base := resmap.ResMap{ + resid.NewResId(deploy, "deploy1"): rf.FromMap( + 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", + }, + }, + }, + }, + }, + }), + } + patch := []*resource.Resource{ + rf.FromMap(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", + }, + }, + }, + }, + }, + }, + }, + }, + ), + rf.FromMap(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 := NewPatchTransformer(patch, rf) + 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 := resmap.ResMap{ + resid.NewResId(foo, "my-foo"): rf.FromMap( + 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", + }, + }, + }), + } + patch := []*resource.Resource{ + rf.FromMap(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 := resmap.ResMap{ + resid.NewResId(foo, "my-foo"): rf.FromMap( + 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 := NewPatchTransformer(patch, rf) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + err = lt.Transform(base) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err = expected.ErrorIfNotEqual(base); err != nil { + t.Fatalf("actual doesn't match expected: %v", err) + } +} + +func TestNoSchemaMultiplePatches(t *testing.T) { + base := resmap.ResMap{ + resid.NewResId(foo, "my-foo"): rf.FromMap( + 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", + }, + }, + }), + } + patch := []*resource.Resource{ + rf.FromMap(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", + }, + }, + }, + ), + rf.FromMap(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 := resmap.ResMap{ + resid.NewResId(foo, "my-foo"): rf.FromMap( + 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 := NewPatchTransformer(patch, rf) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + err = lt.Transform(base) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err = expected.ErrorIfNotEqual(base); err != nil { + t.Fatalf("actual doesn't match expected: %v", err) + } +} + +func TestNoSchemaMultiplePatchesWithConflict(t *testing.T) { + base := resmap.ResMap{ + resid.NewResId(foo, "my-foo"): rf.FromMap( + 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", + }, + }, + }), + } + patch := []*resource.Resource{ + rf.FromMap(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", + }, + }, + }), + rf.FromMap(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 := NewPatchTransformer(patch, rf) + 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/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/patch/patchconflictdetector.go b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/patch/patchconflictdetector.go new file mode 100644 index 0000000000..10353c77ff --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/patch/patchconflictdetector.go @@ -0,0 +1,137 @@ +/* +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 patch + +import ( + "encoding/json" + + "github.com/evanphx/json-patch" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/mergepatch" + "k8s.io/apimachinery/pkg/util/strategicpatch" + "sigs.k8s.io/kustomize/pkg/resource" +) + +type conflictDetector interface { + hasConflict(patch1, patch2 *resource.Resource) (bool, error) + findConflict(conflictingPatchIdx int, patches []*resource.Resource) (*resource.Resource, error) + mergePatches(patch1, patch2 *resource.Resource) (*resource.Resource, error) +} + +type jsonMergePatch struct { + rf *resource.Factory +} + +var _ conflictDetector = &jsonMergePatch{} + +func newJMPConflictDetector(rf *resource.Factory) conflictDetector { + return &jsonMergePatch{rf: rf} +} + +func (jmp *jsonMergePatch) hasConflict( + patch1, patch2 *resource.Resource) (bool, error) { + return mergepatch.HasConflicts(patch1.Map(), patch2.Map()) +} + +func (jmp *jsonMergePatch) findConflict( + conflictingPatchIdx int, patches []*resource.Resource) (*resource.Resource, error) { + for i, patch := range patches { + if i == conflictingPatchIdx { + continue + } + if !patches[conflictingPatchIdx].Id().GvknEquals(patch.Id()) { + continue + } + conflict, err := mergepatch.HasConflicts( + patch.Map(), + patches[conflictingPatchIdx].Map()) + if err != nil { + return nil, err + } + if conflict { + return patch, nil + } + } + return nil, nil +} + +func (jmp *jsonMergePatch) mergePatches( + patch1, patch2 *resource.Resource) (*resource.Resource, error) { + baseBytes, err := json.Marshal(patch1.Map()) + if err != nil { + return nil, err + } + patchBytes, err := json.Marshal(patch2.Map()) + if err != nil { + return nil, err + } + mergedBytes, err := jsonpatch.MergeMergePatches(baseBytes, patchBytes) + if err != nil { + return nil, err + } + mergedMap := make(map[string]interface{}) + err = json.Unmarshal(mergedBytes, &mergedMap) + return jmp.rf.FromMap(mergedMap), err +} + +type strategicMergePatch struct { + lookupPatchMeta strategicpatch.LookupPatchMeta + rf *resource.Factory +} + +var _ conflictDetector = &strategicMergePatch{} + +func newSMPConflictDetector( + versionedObj runtime.Object, + rf *resource.Factory) (conflictDetector, error) { + lookupPatchMeta, err := strategicpatch.NewPatchMetaFromStruct(versionedObj) + return &strategicMergePatch{lookupPatchMeta: lookupPatchMeta, rf: rf}, err +} + +func (smp *strategicMergePatch) hasConflict(p1, p2 *resource.Resource) (bool, error) { + return strategicpatch.MergingMapsHaveConflicts( + p1.Map(), p2.Map(), smp.lookupPatchMeta) +} + +func (smp *strategicMergePatch) findConflict( + conflictingPatchIdx int, patches []*resource.Resource) (*resource.Resource, error) { + for i, patch := range patches { + if i == conflictingPatchIdx { + continue + } + if !patches[conflictingPatchIdx].Id().GvknEquals(patch.Id()) { + continue + } + conflict, err := strategicpatch.MergingMapsHaveConflicts( + patch.Map(), + patches[conflictingPatchIdx].Map(), + smp.lookupPatchMeta) + if err != nil { + return nil, err + } + if conflict { + return patch, nil + } + } + return nil, nil +} + +func (smp *strategicMergePatch) mergePatches(patch1, patch2 *resource.Resource) (*resource.Resource, error) { + mergeJSONMap, err := strategicpatch.MergeStrategicMergeMapPatchUsingLookupPatchMeta( + smp.lookupPatchMeta, patch1.Map(), patch2.Map()) + return smp.rf.FromMap(mergeJSONMap), err +} diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/validator/BUILD b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/validator/BUILD new file mode 100644 index 0000000000..b42649ce92 --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/validator/BUILD @@ -0,0 +1,29 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["validators.go"], + importmap = "k8s.io/kubernetes/vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/validator", + importpath = "k8s.io/cli-runtime/pkg/kustomize/k8sdeps/validator", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/api/validation:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/validation:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/validator/validators.go b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/validator/validators.go new file mode 100644 index 0000000000..563e8d6b9c --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/validator/validators.go @@ -0,0 +1,61 @@ +/* +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 validator provides functions to validate labels, annotations, namespace using apimachinery +package validator + +import ( + "errors" + apivalidation "k8s.io/apimachinery/pkg/api/validation" + v1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" + "k8s.io/apimachinery/pkg/util/validation" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +// KustValidator validates Labels and annotations by apimachinery +type KustValidator struct{} + +// NewKustValidator returns a KustValidator object +func NewKustValidator() *KustValidator { + return &KustValidator{} +} + +// MakeAnnotationValidator returns a MapValidatorFunc using apimachinery. +func (v *KustValidator) MakeAnnotationValidator() func(map[string]string) error { + return func(x map[string]string) error { + errs := apivalidation.ValidateAnnotations(x, field.NewPath("field")) + if len(errs) > 0 { + return errors.New(errs.ToAggregate().Error()) + } + return nil + } +} + +// MakeLabelValidator returns a MapValidatorFunc using apimachinery. +func (v *KustValidator) MakeLabelValidator() func(map[string]string) error { + return func(x map[string]string) error { + errs := v1validation.ValidateLabels(x, field.NewPath("field")) + if len(errs) > 0 { + return errors.New(errs.ToAggregate().Error()) + } + return nil + } +} + +// ValidateNamespace validates a string is a valid namespace using apimachinery. +func (v *KustValidator) ValidateNamespace(s string) []string { + return validation.IsDNS1123Label(s) +}