diff --git a/pkg/kinflate/apps/apps_suite_test.go b/pkg/kinflate/apps/apps_suite_test.go new file mode 100644 index 0000000000000..8d0f7e5a1b03c --- /dev/null +++ b/pkg/kinflate/apps/apps_suite_test.go @@ -0,0 +1,49 @@ +/* +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 apps_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/config" + . "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" + + "fmt" + "testing" +) + +func TestApps(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecsWithDefaultAndCustomReporters(t, "Apps Suite", []Reporter{newlineReporter{}}) +} + +// Print a newline after the default newlineReporter due to issue +// https://github.com/jstemmer/go-junit-report/issues/31 +type newlineReporter struct{} + +func (newlineReporter) SpecSuiteWillBegin(config GinkgoConfigType, summary *SuiteSummary) {} + +func (newlineReporter) BeforeSuiteDidRun(setupSummary *SetupSummary) {} + +func (newlineReporter) AfterSuiteDidRun(setupSummary *SetupSummary) {} + +func (newlineReporter) SpecWillRun(specSummary *SpecSummary) {} + +func (newlineReporter) SpecDidComplete(specSummary *SpecSummary) {} + +// SpecSuiteDidEnd Prints a newline between "35 Passed | 0 Failed | 0 Pending | 0 Skipped" and "--- PASS:" +func (newlineReporter) SpecSuiteDidEnd(summary *SuiteSummary) { fmt.Printf("\n") } diff --git a/pkg/kinflate/apps/kind_visitor.go b/pkg/kinflate/apps/kind_visitor.go new file mode 100644 index 0000000000000..75657880791d6 --- /dev/null +++ b/pkg/kinflate/apps/kind_visitor.go @@ -0,0 +1,95 @@ +/* +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 apps + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// KindVisitor is used with GroupKindElement to call a particular function depending on the +// Kind of a schema.GroupKind +type KindVisitor interface { + VisitDaemonSet(kind GroupKindElement) + VisitDeployment(kind GroupKindElement) + VisitJob(kind GroupKindElement) + VisitPod(kind GroupKindElement) + VisitReplicaSet(kind GroupKindElement) + VisitReplicationController(kind GroupKindElement) + VisitStatefulSet(kind GroupKindElement) + VisitCronJob(kind GroupKindElement) +} + +// GroupKindElement defines a Kubernetes API group elem +type GroupKindElement struct { + schema.GroupKind + // If true, ignore the error when the kind is not a workload type. + IgnoreNonWorkloadError bool +} + +// Accept calls the Visit method on visitor that corresponds to elem's Kind +func (elem GroupKindElement) Accept(visitor KindVisitor) error { + switch { + case elem.GroupMatch("apps", "extensions") && elem.Kind == "DaemonSet": + visitor.VisitDaemonSet(elem) + case elem.GroupMatch("apps", "extensions") && elem.Kind == "Deployment": + visitor.VisitDeployment(elem) + case elem.GroupMatch("batch") && elem.Kind == "Job": + visitor.VisitJob(elem) + case elem.GroupMatch("", "core") && elem.Kind == "Pod": + visitor.VisitPod(elem) + case elem.GroupMatch("apps", "extensions") && elem.Kind == "ReplicaSet": + visitor.VisitReplicaSet(elem) + case elem.GroupMatch("", "core") && elem.Kind == "ReplicationController": + visitor.VisitReplicationController(elem) + case elem.GroupMatch("apps") && elem.Kind == "StatefulSet": + visitor.VisitStatefulSet(elem) + case elem.GroupMatch("batch") && elem.Kind == "CronJob": + visitor.VisitCronJob(elem) + default: + if !elem.IgnoreNonWorkloadError { + return fmt.Errorf("no visitor method exists for %v", elem) + } + } + return nil +} + +// GroupMatch returns true if and only if elem's group matches one +// of the group arguments +func (elem GroupKindElement) GroupMatch(groups ...string) bool { + for _, g := range groups { + if elem.Group == g { + return true + } + } + return false +} + +// NoOpKindVisitor implements KindVisitor with no-op functions. +type NoOpKindVisitor struct{} + +var _ KindVisitor = &NoOpKindVisitor{} + +func (*NoOpKindVisitor) VisitDaemonSet(kind GroupKindElement) {} +func (*NoOpKindVisitor) VisitDeployment(kind GroupKindElement) {} +func (*NoOpKindVisitor) VisitJob(kind GroupKindElement) {} +func (*NoOpKindVisitor) VisitPod(kind GroupKindElement) {} +func (*NoOpKindVisitor) VisitReplicaSet(kind GroupKindElement) {} +func (*NoOpKindVisitor) VisitReplicationController(kind GroupKindElement) {} +func (*NoOpKindVisitor) VisitStatefulSet(kind GroupKindElement) {} +func (*NoOpKindVisitor) VisitCronJob(kind GroupKindElement) {} diff --git a/pkg/kinflate/apps/kind_visitor_test.go b/pkg/kinflate/apps/kind_visitor_test.go new file mode 100644 index 0000000000000..e9d2e79642dde --- /dev/null +++ b/pkg/kinflate/apps/kind_visitor_test.go @@ -0,0 +1,212 @@ +/* +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 apps_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kubectl/pkg/kinflate/apps" +) + +var _ = Describe("When KindVisitor accepts a GroupKind", func() { + + var visitor *TestKindVisitor + + BeforeEach(func() { + visitor = &TestKindVisitor{map[string]int{}} + }) + + It("should Visit DaemonSet iff the Kind is a DaemonSet", func() { + kind := apps.GroupKindElement{ + GroupKind: schema.GroupKind{ + Kind: "DaemonSet", + Group: "apps", + }, + } + Expect(kind.Accept(visitor)).ShouldNot(HaveOccurred()) + Expect(visitor.visits).To(Equal(map[string]int{ + "DaemonSet": 1, + })) + + kind = apps.GroupKindElement{ + GroupKind: schema.GroupKind{ + Kind: "DaemonSet", + Group: "extensions", + }, + } + Expect(kind.Accept(visitor)).ShouldNot(HaveOccurred()) + Expect(visitor.visits).To(Equal(map[string]int{ + "DaemonSet": 2, + })) + }) + + It("should Visit Deployment iff the Kind is a Deployment", func() { + kind := apps.GroupKindElement{ + GroupKind: schema.GroupKind{ + Kind: "Deployment", + Group: "apps", + }, + } + Expect(kind.Accept(visitor)).ShouldNot(HaveOccurred()) + Expect(visitor.visits).To(Equal(map[string]int{ + "Deployment": 1, + })) + + kind = apps.GroupKindElement{ + GroupKind: schema.GroupKind{ + Kind: "Deployment", + Group: "extensions", + }, + } + Expect(kind.Accept(visitor)).ShouldNot(HaveOccurred()) + Expect(visitor.visits).To(Equal(map[string]int{ + "Deployment": 2, + })) + }) + + It("should Visit Job iff the Kind is a Job", func() { + kind := apps.GroupKindElement{ + GroupKind: schema.GroupKind{ + Kind: "Job", + Group: "batch", + }, + } + Expect(kind.Accept(visitor)).ShouldNot(HaveOccurred()) + Expect(visitor.visits).To(Equal(map[string]int{ + "Job": 1, + })) + + }) + + It("should Visit Pod iff the Kind is a Pod", func() { + kind := apps.GroupKindElement{ + GroupKind: schema.GroupKind{ + Kind: "Pod", + Group: "", + }, + } + Expect(kind.Accept(visitor)).ShouldNot(HaveOccurred()) + Expect(visitor.visits).To(Equal(map[string]int{ + "Pod": 1, + })) + + kind = apps.GroupKindElement{ + GroupKind: schema.GroupKind{ + Kind: "Pod", + Group: "core", + }, + } + Expect(kind.Accept(visitor)).ShouldNot(HaveOccurred()) + Expect(visitor.visits).To(Equal(map[string]int{ + "Pod": 2, + })) + }) + + It("should Visit ReplicationController iff the Kind is a ReplicationController", func() { + kind := apps.GroupKindElement{ + GroupKind: schema.GroupKind{ + Kind: "ReplicationController", + Group: "", + }, + } + Expect(kind.Accept(visitor)).ShouldNot(HaveOccurred()) + Expect(visitor.visits).To(Equal(map[string]int{ + "ReplicationController": 1, + })) + + kind = apps.GroupKindElement{ + GroupKind: schema.GroupKind{ + Kind: "ReplicationController", + Group: "core", + }, + } + Expect(kind.Accept(visitor)).ShouldNot(HaveOccurred()) + Expect(visitor.visits).To(Equal(map[string]int{ + "ReplicationController": 2, + })) + }) + + It("should Visit ReplicaSet iff the Kind is a ReplicaSet", func() { + kind := apps.GroupKindElement{ + GroupKind: schema.GroupKind{ + Kind: "ReplicaSet", + Group: "extensions", + }, + } + Expect(kind.Accept(visitor)).ShouldNot(HaveOccurred()) + Expect(visitor.visits).To(Equal(map[string]int{ + "ReplicaSet": 1, + })) + }) + + It("should Visit StatefulSet iff the Kind is a StatefulSet", func() { + kind := apps.GroupKindElement{ + GroupKind: schema.GroupKind{ + Kind: "StatefulSet", + Group: "apps", + }, + } + Expect(kind.Accept(visitor)).ShouldNot(HaveOccurred()) + Expect(visitor.visits).To(Equal(map[string]int{ + "StatefulSet": 1, + })) + }) + + It("should Visit CronJob iff the Kind is a CronJob", func() { + kind := apps.GroupKindElement{ + GroupKind: schema.GroupKind{ + Kind: "CronJob", + Group: "batch", + }, + } + Expect(kind.Accept(visitor)).ShouldNot(HaveOccurred()) + Expect(visitor.visits).To(Equal(map[string]int{ + "CronJob": 1, + })) + }) + + It("should give an error if the Kind is unknown", func() { + kind := apps.GroupKindElement{ + GroupKind: schema.GroupKind{ + Kind: "Unknown", + Group: "apps", + }, + } + Expect(kind.Accept(visitor)).Should(HaveOccurred()) + Expect(visitor.visits).To(Equal(map[string]int{})) + }) +}) + +// TestKindVisitor increments a value each time a Visit method was called +type TestKindVisitor struct { + visits map[string]int +} + +var _ apps.KindVisitor = &TestKindVisitor{} + +func (t *TestKindVisitor) Visit(kind apps.GroupKindElement) { t.visits[kind.Kind] += 1 } + +func (t *TestKindVisitor) VisitDaemonSet(kind apps.GroupKindElement) { t.Visit(kind) } +func (t *TestKindVisitor) VisitDeployment(kind apps.GroupKindElement) { t.Visit(kind) } +func (t *TestKindVisitor) VisitJob(kind apps.GroupKindElement) { t.Visit(kind) } +func (t *TestKindVisitor) VisitPod(kind apps.GroupKindElement) { t.Visit(kind) } +func (t *TestKindVisitor) VisitReplicaSet(kind apps.GroupKindElement) { t.Visit(kind) } +func (t *TestKindVisitor) VisitReplicationController(kind apps.GroupKindElement) { t.Visit(kind) } +func (t *TestKindVisitor) VisitStatefulSet(kind apps.GroupKindElement) { t.Visit(kind) } +func (t *TestKindVisitor) VisitCronJob(kind apps.GroupKindElement) { t.Visit(kind) } diff --git a/pkg/kinflate/configmapandsecret/configmap.go b/pkg/kinflate/configmapandsecret/configmap.go new file mode 100644 index 0000000000000..61c25a40a93bc --- /dev/null +++ b/pkg/kinflate/configmapandsecret/configmap.go @@ -0,0 +1,135 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package configmapandsecret + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "strings" + + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/validation" + "k8s.io/kubectl/pkg/kinflate/configmapandsecret/util" +) + +// handleConfigMapFromLiteralSources adds the specified literal source +// information into the provided configMap. +func HandleConfigMapFromLiteralSources(configMap *v1.ConfigMap, literalSources []string) error { + for _, literalSource := range literalSources { + keyName, value, err := util.ParseLiteralSource(literalSource) + if err != nil { + return err + } + err = addKeyFromLiteralToConfigMap(configMap, keyName, value) + if err != nil { + return err + } + } + return nil +} + +// handleConfigMapFromFileSources adds the specified file source information +// into the provided configMap +func HandleConfigMapFromFileSources(configMap *v1.ConfigMap, fileSources []string) error { + for _, fileSource := range fileSources { + keyName, filePath, err := util.ParseFileSource(fileSource) + if err != nil { + return err + } + info, err := os.Stat(filePath) + if err != nil { + switch err := err.(type) { + case *os.PathError: + return fmt.Errorf("error reading %s: %v", filePath, err.Err) + default: + return fmt.Errorf("error reading %s: %v", filePath, err) + } + } + if info.IsDir() { + if strings.Contains(fileSource, "=") { + return fmt.Errorf("cannot give a key name for a directory path.") + } + fileList, err := ioutil.ReadDir(filePath) + if err != nil { + return fmt.Errorf("error listing files in %s: %v", filePath, err) + } + for _, item := range fileList { + itemPath := path.Join(filePath, item.Name()) + if item.Mode().IsRegular() { + keyName = item.Name() + err = addKeyFromFileToConfigMap(configMap, keyName, itemPath) + if err != nil { + return err + } + } + } + } else { + if err := addKeyFromFileToConfigMap(configMap, keyName, filePath); err != nil { + return err + } + } + } + + return nil +} + +// handleConfigMapFromEnvFileSource adds the specified env file source information +// into the provided configMap +func HandleConfigMapFromEnvFileSource(configMap *v1.ConfigMap, envFileSource string) error { + info, err := os.Stat(envFileSource) + if err != nil { + switch err := err.(type) { + case *os.PathError: + return fmt.Errorf("error reading %s: %v", envFileSource, err.Err) + default: + return fmt.Errorf("error reading %s: %v", envFileSource, err) + } + } + if info.IsDir() { + return fmt.Errorf("env config file cannot be a directory") + } + + return addFromEnvFile(envFileSource, func(key, value string) error { + return addKeyFromLiteralToConfigMap(configMap, key, value) + }) +} + +// addKeyFromFileToConfigMap adds a key with the given name to a ConfigMap, populating +// the value with the content of the given file path, or returns an error. +func addKeyFromFileToConfigMap(configMap *v1.ConfigMap, keyName, filePath string) error { + data, err := ioutil.ReadFile(filePath) + if err != nil { + return err + } + return addKeyFromLiteralToConfigMap(configMap, keyName, string(data)) +} + +// addKeyFromLiteralToConfigMap adds the given key and data to the given config map, +// returning an error if the key is not valid or if the key already exists. +func addKeyFromLiteralToConfigMap(configMap *v1.ConfigMap, keyName, data string) error { + // Note, the rules for ConfigMap keys are the exact same as the ones for SecretKeys. + if errs := validation.IsConfigMapKey(keyName); len(errs) != 0 { + return fmt.Errorf("%q is not a valid key name for a ConfigMap: %s", keyName, strings.Join(errs, ";")) + } + if _, entryExists := configMap.Data[keyName]; entryExists { + return fmt.Errorf("cannot add key %s, another key by that name already exists: %v.", keyName, configMap.Data) + } + configMap.Data[keyName] = data + return nil +} diff --git a/pkg/kinflate/configmapandsecret/env_file.go b/pkg/kinflate/configmapandsecret/env_file.go new file mode 100644 index 0000000000000..e51467167a96f --- /dev/null +++ b/pkg/kinflate/configmapandsecret/env_file.go @@ -0,0 +1,103 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package configmapandsecret + +import ( + "bufio" + "bytes" + "fmt" + "os" + "strings" + "unicode" + "unicode/utf8" + + "k8s.io/apimachinery/pkg/util/validation" +) + +var utf8bom = []byte{0xEF, 0xBB, 0xBF} + +// proccessEnvFileLine returns a blank key if the line is empty or a comment. +// The value will be retrieved from the environment if necessary. +func proccessEnvFileLine(line []byte, filePath string, + currentLine int) (key, value string, err error) { + + if !utf8.Valid(line) { + return ``, ``, fmt.Errorf("env file %s contains invalid utf8 bytes at line %d: %v", + filePath, currentLine+1, line) + } + + // We trim UTF8 BOM from the first line of the file but no others + if currentLine == 0 { + line = bytes.TrimPrefix(line, utf8bom) + } + + // trim the line from all leading whitespace first + line = bytes.TrimLeftFunc(line, unicode.IsSpace) + + // If the line is empty or a comment, we return a blank key/value pair. + if len(line) == 0 || line[0] == '#' { + return ``, ``, nil + } + + data := strings.SplitN(string(line), "=", 2) + key = data[0] + if errs := validation.IsEnvVarName(key); len(errs) != 0 { + return ``, ``, fmt.Errorf("%q is not a valid key name: %s", key, strings.Join(errs, ";")) + } + + if len(data) == 2 { + value = data[1] + } else { + // No value (no `=` in the line) is a signal to obtain the value + // from the environment. + value = os.Getenv(key) + } + return +} + +// addFromEnvFile processes an env file allows a generic addTo to handle the +// collection of key value pairs or returns an error. +func addFromEnvFile(filePath string, addTo func(key, value string) error) error { + f, err := os.Open(filePath) + if err != nil { + return err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + currentLine := 0 + for scanner.Scan() { + // Proccess the current line, retrieving a key/value pair if + // possible. + scannedBytes := scanner.Bytes() + key, value, err := proccessEnvFileLine(scannedBytes, filePath, currentLine) + if err != nil { + return err + } + currentLine++ + + if len(key) == 0 { + // no key means line was empty or a comment + continue + } + + if err = addTo(key, value); err != nil { + return err + } + } + return nil +} diff --git a/pkg/kinflate/configmapandsecret/secret.go b/pkg/kinflate/configmapandsecret/secret.go new file mode 100644 index 0000000000000..f429188470f94 --- /dev/null +++ b/pkg/kinflate/configmapandsecret/secret.go @@ -0,0 +1,127 @@ +/* +Copyright 2015 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package configmapandsecret + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "strings" + + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/validation" + "k8s.io/kubectl/pkg/kinflate/configmapandsecret/util" +) + +// HandleFromLiteralSources adds the specified literal source information into the provided secret +func HandleFromLiteralSources(secret *v1.Secret, literalSources []string) error { + for _, literalSource := range literalSources { + keyName, value, err := util.ParseLiteralSource(literalSource) + if err != nil { + return err + } + if err = addKeyFromLiteralToSecret(secret, keyName, []byte(value)); err != nil { + return err + } + } + return nil +} + +// HandleFromFileSources adds the specified file source information into the provided secret +func HandleFromFileSources(secret *v1.Secret, fileSources []string) error { + for _, fileSource := range fileSources { + keyName, filePath, err := util.ParseFileSource(fileSource) + if err != nil { + return err + } + info, err := os.Stat(filePath) + if err != nil { + switch err := err.(type) { + case *os.PathError: + return fmt.Errorf("error reading %s: %v", filePath, err.Err) + default: + return fmt.Errorf("error reading %s: %v", filePath, err) + } + } + if info.IsDir() { + if strings.Contains(fileSource, "=") { + return fmt.Errorf("cannot give a key name for a directory path.") + } + fileList, err := ioutil.ReadDir(filePath) + if err != nil { + return fmt.Errorf("error listing files in %s: %v", filePath, err) + } + for _, item := range fileList { + itemPath := path.Join(filePath, item.Name()) + if item.Mode().IsRegular() { + keyName = item.Name() + if err = addKeyFromFileToSecret(secret, keyName, itemPath); err != nil { + return err + } + } + } + } else { + if err := addKeyFromFileToSecret(secret, keyName, filePath); err != nil { + return err + } + } + } + + return nil +} + +// HandleFromEnvFileSource adds the specified env file source information +// into the provided secret +func HandleFromEnvFileSource(secret *v1.Secret, envFileSource string) error { + info, err := os.Stat(envFileSource) + if err != nil { + switch err := err.(type) { + case *os.PathError: + return fmt.Errorf("error reading %s: %v", envFileSource, err.Err) + default: + return fmt.Errorf("error reading %s: %v", envFileSource, err) + } + } + if info.IsDir() { + return fmt.Errorf("env secret file cannot be a directory") + } + + return addFromEnvFile(envFileSource, func(key, value string) error { + return addKeyFromLiteralToSecret(secret, key, []byte(value)) + }) +} + +func addKeyFromFileToSecret(secret *v1.Secret, keyName, filePath string) error { + data, err := ioutil.ReadFile(filePath) + if err != nil { + return err + } + return addKeyFromLiteralToSecret(secret, keyName, data) +} + +func addKeyFromLiteralToSecret(secret *v1.Secret, keyName string, data []byte) error { + if errs := validation.IsConfigMapKey(keyName); len(errs) != 0 { + return fmt.Errorf("%q is not a valid key name for a Secret: %s", keyName, strings.Join(errs, ";")) + } + + if _, entryExists := secret.Data[keyName]; entryExists { + return fmt.Errorf("cannot add key %s, another key by that name already exists: %v.", keyName, secret.Data) + } + secret.Data[keyName] = data + return nil +} diff --git a/pkg/kinflate/configmapandsecret/util/util.go b/pkg/kinflate/configmapandsecret/util/util.go new file mode 100644 index 0000000000000..fd1bf13646896 --- /dev/null +++ b/pkg/kinflate/configmapandsecret/util/util.go @@ -0,0 +1,91 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "crypto/md5" + "errors" + "fmt" + "path" + "strings" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// ParseRFC3339 parses an RFC3339 date in either RFC3339Nano or RFC3339 format. +func ParseRFC3339(s string, nowFn func() metav1.Time) (metav1.Time, error) { + if t, timeErr := time.Parse(time.RFC3339Nano, s); timeErr == nil { + return metav1.Time{Time: t}, nil + } + t, err := time.Parse(time.RFC3339, s) + if err != nil { + return metav1.Time{}, err + } + return metav1.Time{Time: t}, nil +} + +func HashObject(obj runtime.Object, codec runtime.Codec) (string, error) { + data, err := runtime.Encode(codec, obj) + if err != nil { + return "", err + } + return fmt.Sprintf("%x", md5.Sum(data)), nil +} + +// ParseFileSource parses the source given. +// +// Acceptable formats include: +// 1. source-path: the basename will become the key name +// 2. source-name=source-path: the source-name will become the key name and +// source-path is the path to the key file. +// +// Key names cannot include '='. +func ParseFileSource(source string) (keyName, filePath string, err error) { + numSeparators := strings.Count(source, "=") + switch { + case numSeparators == 0: + return path.Base(source), source, nil + case numSeparators == 1 && strings.HasPrefix(source, "="): + return "", "", fmt.Errorf("key name for file path %v missing.", strings.TrimPrefix(source, "=")) + case numSeparators == 1 && strings.HasSuffix(source, "="): + return "", "", fmt.Errorf("file path for key name %v missing.", strings.TrimSuffix(source, "=")) + case numSeparators > 1: + return "", "", errors.New("Key names or file paths cannot contain '='.") + default: + components := strings.Split(source, "=") + return components[0], components[1], nil + } +} + +// ParseLiteralSource parses the source key=val pair into its component pieces. +// This functionality is distinguished from strings.SplitN(source, "=", 2) since +// it returns an error in the case of empty keys, values, or a missing equals sign. +func ParseLiteralSource(source string) (keyName, value string, err error) { + // leading equal is invalid + if strings.Index(source, "=") == 0 { + return "", "", fmt.Errorf("invalid literal source %v, expected key=value", source) + } + // split after the first equal (so values can have the = character) + items := strings.SplitN(source, "=", 2) + if len(items) != 2 { + return "", "", fmt.Errorf("invalid literal source %v, expected key=value", source) + } + + return items[0], items[1], nil +} diff --git a/pkg/kinflate/hash/hash.go b/pkg/kinflate/hash/hash.go new file mode 100644 index 0000000000000..d7afaab7d7cb1 --- /dev/null +++ b/pkg/kinflate/hash/hash.go @@ -0,0 +1,110 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package hash + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + + "k8s.io/api/core/v1" +) + +// ConfigMapHash returns a hash of the ConfigMap. +// The Data, Kind, and Name are taken into account. +func ConfigMapHash(cm *v1.ConfigMap) (string, error) { + encoded, err := encodeConfigMap(cm) + if err != nil { + return "", err + } + h, err := encodeHash(hash(encoded)) + if err != nil { + return "", err + } + return h, nil +} + +// SecretHash returns a hash of the Secret. +// The Data, Kind, Name, and Type are taken into account. +func SecretHash(sec *v1.Secret) (string, error) { + encoded, err := encodeSecret(sec) + if err != nil { + return "", err + } + h, err := encodeHash(hash(encoded)) + if err != nil { + return "", err + } + return h, nil +} + +// encodeConfigMap encodes a ConfigMap. +// Data, Kind, and Name are taken into account. +func encodeConfigMap(cm *v1.ConfigMap) (string, error) { + // json.Marshal sorts the keys in a stable order in the encoding + data, err := json.Marshal(map[string]interface{}{"kind": "ConfigMap", "name": cm.Name, "data": cm.Data}) + if err != nil { + return "", err + } + return string(data), nil +} + +// encodeSecret encodes a Secret. +// Data, Kind, Name, and Type are taken into account. +func encodeSecret(sec *v1.Secret) (string, error) { + // json.Marshal sorts the keys in a stable order in the encoding + data, err := json.Marshal(map[string]interface{}{"kind": "Secret", "type": sec.Type, "name": sec.Name, "data": sec.Data}) + if err != nil { + return "", err + } + return string(data), nil +} + +// encodeHash extracts the first 40 bits of the hash from the hex string +// (1 hex char represents 4 bits), and then maps vowels and vowel-like hex +// characters to consonants to prevent bad words from being formed (the theory +// is that no vowels makes it really hard to make bad words). Since the string +// is hex, the only vowels it can contain are 'a' and 'e'. +// We picked some arbitrary consonants to map to from the same character set as GenerateName. +// See: https://github.com/kubernetes/apimachinery/blob/dc1f89aff9a7509782bde3b68824c8043a3e58cc/pkg/util/rand/rand.go#L75 +// If the hex string contains fewer than ten characters, returns an error. +func encodeHash(hex string) (string, error) { + if len(hex) < 10 { + return "", fmt.Errorf("the hex string must contain at least 10 characters") + } + enc := []rune(hex[:10]) + for i := range enc { + switch enc[i] { + case '0': + enc[i] = 'g' + case '1': + enc[i] = 'h' + case '3': + enc[i] = 'k' + case 'a': + enc[i] = 'm' + case 'e': + enc[i] = 't' + } + } + return string(enc), nil +} + +// hash hashes `data` with sha256 and returns the hex string +func hash(data string) string { + return fmt.Sprintf("%x", sha256.Sum256([]byte(data))) +} diff --git a/pkg/kinflate/hash/hash_test.go b/pkg/kinflate/hash/hash_test.go new file mode 100644 index 0000000000000..15a0bd20e14d4 --- /dev/null +++ b/pkg/kinflate/hash/hash_test.go @@ -0,0 +1,178 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package hash + +import ( + "reflect" + "strings" + "testing" + + "k8s.io/api/core/v1" +) + +func TestConfigMapHash(t *testing.T) { + cases := []struct { + desc string + cm *v1.ConfigMap + hash string + err string + }{ + // empty map + {"empty data", &v1.ConfigMap{Data: map[string]string{}}, "42745tchd9", ""}, + // one key + {"one key", &v1.ConfigMap{Data: map[string]string{"one": ""}}, "9g67k2htb6", ""}, + // three keys (tests sorting order) + {"three keys", &v1.ConfigMap{Data: map[string]string{"two": "2", "one": "", "three": "3"}}, "f5h7t85m9b", ""}, + } + + for _, c := range cases { + h, err := ConfigMapHash(c.cm) + if SkipRest(t, c.desc, err, c.err) { + continue + } + if c.hash != h { + t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h) + } + } +} + +func TestSecretHash(t *testing.T) { + cases := []struct { + desc string + secret *v1.Secret + hash string + err string + }{ + // empty map + {"empty data", &v1.Secret{Type: "my-type", Data: map[string][]byte{}}, "t75bgf6ctb", ""}, + // one key + {"one key", &v1.Secret{Type: "my-type", Data: map[string][]byte{"one": []byte("")}}, "74bd68bm66", ""}, + // three keys (tests sorting order) + {"three keys", &v1.Secret{Type: "my-type", Data: map[string][]byte{"two": []byte("2"), "one": []byte(""), "three": []byte("3")}}, "dgcb6h9tmk", ""}, + } + + for _, c := range cases { + h, err := SecretHash(c.secret) + if SkipRest(t, c.desc, err, c.err) { + continue + } + if c.hash != h { + t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h) + } + } +} + +func TestEncodeConfigMap(t *testing.T) { + cases := []struct { + desc string + cm *v1.ConfigMap + expect string + err string + }{ + // empty map + {"empty data", &v1.ConfigMap{Data: map[string]string{}}, `{"data":{},"kind":"ConfigMap","name":""}`, ""}, + // one key + {"one key", &v1.ConfigMap{Data: map[string]string{"one": ""}}, `{"data":{"one":""},"kind":"ConfigMap","name":""}`, ""}, + // three keys (tests sorting order) + {"three keys", &v1.ConfigMap{Data: map[string]string{"two": "2", "one": "", "three": "3"}}, `{"data":{"one":"","three":"3","two":"2"},"kind":"ConfigMap","name":""}`, ""}, + } + for _, c := range cases { + s, err := encodeConfigMap(c.cm) + if SkipRest(t, c.desc, err, c.err) { + continue + } + if s != c.expect { + t.Errorf("case %q, expect %q but got %q from encode %#v", c.desc, c.expect, s, c.cm) + } + } +} + +func TestEncodeSecret(t *testing.T) { + cases := []struct { + desc string + secret *v1.Secret + expect string + err string + }{ + // empty map + {"empty data", &v1.Secret{Type: "my-type", Data: map[string][]byte{}}, `{"data":{},"kind":"Secret","name":"","type":"my-type"}`, ""}, + // one key + {"one key", &v1.Secret{Type: "my-type", Data: map[string][]byte{"one": []byte("")}}, `{"data":{"one":""},"kind":"Secret","name":"","type":"my-type"}`, ""}, + // three keys (tests sorting order) - note json.Marshal base64 encodes the values because they come in as []byte + {"three keys", &v1.Secret{Type: "my-type", Data: map[string][]byte{"two": []byte("2"), "one": []byte(""), "three": []byte("3")}}, `{"data":{"one":"","three":"Mw==","two":"Mg=="},"kind":"Secret","name":"","type":"my-type"}`, ""}, + } + for _, c := range cases { + s, err := encodeSecret(c.secret) + if SkipRest(t, c.desc, err, c.err) { + continue + } + if s != c.expect { + t.Errorf("case %q, expect %q but got %q from encode %#v", c.desc, c.expect, s, c.secret) + } + } +} + +func TestHash(t *testing.T) { + // hash the empty string to be sure that sha256 is being used + expect := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + sum := hash("") + if expect != sum { + t.Errorf("expected hash %q but got %q", expect, sum) + } +} + +// warn devs who change types that they might have to update a hash function +// not perfect, as it only checks the number of top-level fields +func TestTypeStability(t *testing.T) { + errfmt := `case %q, expected %d fields but got %d +Depending on the field(s) you added, you may need to modify the hash function for this type. +To guide you: the hash function targets fields that comprise the contents of objects, +not their metadata (e.g. the Data of a ConfigMap, but nothing in ObjectMeta). +` + cases := []struct { + typeName string + obj interface{} + expect int + }{ + {"ConfigMap", v1.ConfigMap{}, 3}, + {"Secret", v1.Secret{}, 5}, + } + for _, c := range cases { + val := reflect.ValueOf(c.obj) + if num := val.NumField(); c.expect != num { + t.Errorf(errfmt, c.typeName, c.expect, num) + } + } +} + +// SkipRest returns true if there was a non-nil error or if we expected an error that didn't happen, +// and logs the appropriate error on the test object. +// The return value indicates whether we should skip the rest of the test case due to the error result. +func SkipRest(t *testing.T, desc string, err error, contains string) bool { + if err != nil { + if len(contains) == 0 { + t.Errorf("case %q, expect nil error but got %q", desc, err.Error()) + } else if !strings.Contains(err.Error(), contains) { + t.Errorf("case %q, expect error to contain %q but got %q", desc, contains, err.Error()) + } + return true + } else if len(contains) > 0 { + t.Errorf("case %q, expect error to contain %q but got nil error", desc, contains) + return true + } + return false +}