From 51a9c2c9ba86d02fa55d2807c0d8e1866f8fcfd2 Mon Sep 17 00:00:00 2001 From: Paul Morie Date: Thu, 18 Feb 2016 21:24:21 -0500 Subject: [PATCH] Add kubectl create configmap --- hack/test-cmd.sh | 36 +++- pkg/kubectl/cmd/create.go | 1 + pkg/kubectl/cmd/create_configmap.go | 96 ++++++++++ pkg/kubectl/cmd/create_configmap_test.go | 54 ++++++ pkg/kubectl/cmd/create_secret.go | 4 +- pkg/kubectl/cmd/util/factory.go | 1 + pkg/kubectl/configmap.go | 212 +++++++++++++++++++++++ pkg/kubectl/configmap_test.go | 108 ++++++++++++ pkg/kubectl/kubectl.go | 36 ++++ pkg/kubectl/kubectl_test.go | 194 +++++++++++++++++++++ pkg/kubectl/secret.go | 31 ---- 11 files changed, 732 insertions(+), 41 deletions(-) create mode 100644 pkg/kubectl/cmd/create_configmap.go create mode 100644 pkg/kubectl/cmd/create_configmap_test.go create mode 100644 pkg/kubectl/configmap.go create mode 100644 pkg/kubectl/configmap_test.go create mode 100644 pkg/kubectl/kubectl_test.go diff --git a/hack/test-cmd.sh b/hack/test-cmd.sh index 1a0c527519e..565fafb3c6c 100755 --- a/hack/test-cmd.sh +++ b/hack/test-cmd.sh @@ -823,6 +823,34 @@ __EOF__ # Clean up kubectl delete namespace test-secrets + ###################### + # ConfigMap # + ###################### + + kubectl create -f docs/user-guide/configmap/config-map.yaml + kube::test::get_object_assert configmap "{{range.items}}{{$id_field}}{{end}}" 'test-configmap' + kubectl delete configmap test-configmap "${kube_flags[@]}" + + ### Create a new namespace + # Pre-condition: the test-configmaps namespace does not exist + kube::test::get_object_assert 'namespaces' '{{range.items}}{{ if eq $id_field \"test-configmaps\" }}found{{end}}{{end}}:' ':' + # Command + kubectl create namespace test-configmaps + # Post-condition: namespace 'test-configmaps' is created. + kube::test::get_object_assert 'namespaces/test-configmaps' "{{$id_field}}" 'test-configmaps' + + ### Create a generic configmap in a specific namespace + # Pre-condition: no configmaps namespace exists + kube::test::get_object_assert 'configmaps --namespace=test-configmaps' "{{range.items}}{{$id_field}}:{{end}}" '' + # Command + kubectl create configmap test-configmap --from-literal=key1=value1 --namespace=test-configmaps + # Post-condition: configmap exists and has expected values + kube::test::get_object_assert 'configmap/test-configmap --namespace=test-configmaps' "{{$id_field}}" 'test-configmap' + [[ "$(kubectl get configmap/test-configmap --namespace=test-configmaps -o yaml "${kube_flags[@]}" | grep 'key1: value1')" ]] + # Clean-up + kubectl delete configmap test-configmap --namespace=test-configmaps + kubectl delete namespace test-configmaps + ################# # Pod templates # ################# @@ -1271,14 +1299,6 @@ __EOF__ kube::test::get_object_assert rs "{{range.items}}{{$id_field}}:{{end}}" '' - ###################### - # ConfigMap # - ###################### - - kubectl create -f docs/user-guide/configmap/config-map.yaml - kube::test::get_object_assert configmap "{{range.items}}{{$id_field}}{{end}}" 'test-configmap' - kubectl delete configmap test-configmap "${kube_flags[@]}" - ###################### # Multiple Resources # ###################### diff --git a/pkg/kubectl/cmd/create.go b/pkg/kubectl/cmd/create.go index 6c4061ea04f..612ebd19194 100644 --- a/pkg/kubectl/cmd/create.go +++ b/pkg/kubectl/cmd/create.go @@ -78,6 +78,7 @@ func NewCmdCreate(f *cmdutil.Factory, out io.Writer) *cobra.Command { // create subcommands cmd.AddCommand(NewCmdCreateNamespace(f, out)) cmd.AddCommand(NewCmdCreateSecret(f, out)) + cmd.AddCommand(NewCmdCreateConfigMap(f, out)) return cmd } diff --git a/pkg/kubectl/cmd/create_configmap.go b/pkg/kubectl/cmd/create_configmap.go new file mode 100644 index 00000000000..99a7dcc2c7c --- /dev/null +++ b/pkg/kubectl/cmd/create_configmap.go @@ -0,0 +1,96 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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 cmd + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" + + "k8s.io/kubernetes/pkg/kubectl" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" +) + +const ( + configMapLong = `Create a configmap based on a file, directory, or specified literal value. + +A single configmap may package one or more key/value pairs. + +When creating a configmap based on a file, the key will default to the basename of the file, and the value will +default to the file content. If the basename is an invalid key, you may specify an alternate key. + +When creating a configmap based on a directory, each file whose basename is a valid key in the directory will be +packaged into the configmap. Any directory entries except regular files are ignored (e.g. subdirectories, +symlinks, devices, pipes, etc). +` + + configMapExample = ` # Create a new configmap named my-config with keys for each file in folder bar + $ kubectl create configmap generic my-config --from-file=path/to/bar + + # Create a new configmap named my-config with specified keys instead of names on disk + $ kubectl create configmap generic my-config --from-file=ssh-privatekey=~/.ssh/id_rsa --from-file=ssh-publickey=~/.ssh/id_rsa.pub + + # Create a new configMap named my-config with key1=config1 and key2=config2 + $ kubectl create configmap generic my-config --from-literal=key1=config1 --from-literal=key2=config2` +) + +// ConfigMap is a command to ease creating ConfigMaps. +func NewCmdCreateConfigMap(f *cmdutil.Factory, cmdOut io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "configmap NAME [--type=string] [--from-file=[key=]source] [--from-literal=key1=value1] [--dry-run]", + Short: "Create a configMap from a local file, directory or literal value.", + Long: configMapLong, + Example: configMapExample, + Run: func(cmd *cobra.Command, args []string) { + err := CreateConfigMap(f, cmdOut, cmd, args) + cmdutil.CheckErr(err) + }, + } + cmdutil.AddApplyAnnotationFlags(cmd) + cmdutil.AddValidateFlags(cmd) + cmdutil.AddPrinterFlags(cmd) + cmdutil.AddGeneratorFlags(cmd, cmdutil.ConfigMapV1GeneratorName) + cmd.Flags().StringSlice("from-file", []string{}, "Key files can be specified using their file path, in which case a default name will be given to them, or optionally with a name and file path, in which case the given name will be used. Specifying a directory will iterate each named file in the directory that is a valid configmap key.") + cmd.Flags().StringSlice("from-literal", []string{}, "Specify a key and literal value to insert in configmap (i.e. mykey=somevalue)") + return cmd +} + +// CreateConfigMap is the implementation of the create configmap generic command. +func CreateConfigMap(f *cmdutil.Factory, cmdOut io.Writer, cmd *cobra.Command, args []string) error { + name, err := NameFromCommandArgs(cmd, args) + if err != nil { + return err + } + var generator kubectl.StructuredGenerator + switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName { + case cmdutil.ConfigMapV1GeneratorName: + generator = &kubectl.ConfigMapGeneratorV1{ + Name: name, + FileSources: cmdutil.GetFlagStringSlice(cmd, "from-file"), + LiteralSources: cmdutil.GetFlagStringSlice(cmd, "from-literal"), + } + default: + return cmdutil.UsageError(cmd, fmt.Sprintf("Generator: %s not supported.", generatorName)) + } + return RunCreateSubcommand(f, cmd, cmdOut, &CreateSubcommandOptions{ + Name: name, + StructuredGenerator: generator, + DryRun: cmdutil.GetFlagBool(cmd, "dry-run"), + OutputFormat: cmdutil.GetFlagString(cmd, "output"), + }) +} diff --git a/pkg/kubectl/cmd/create_configmap_test.go b/pkg/kubectl/cmd/create_configmap_test.go new file mode 100644 index 00000000000..9c3971e36ac --- /dev/null +++ b/pkg/kubectl/cmd/create_configmap_test.go @@ -0,0 +1,54 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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 cmd + +import ( + "bytes" + "net/http" + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/client/unversioned/fake" +) + +func TestCreateConfigMap(t *testing.T) { + configMap := &api.ConfigMap{} + configMap.Name = "my-configmap" + f, tf, codec := NewAPIFactory() + tf.Printer = &testPrinter{} + tf.Client = &fake.RESTClient{ + Codec: codec, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + switch p, m := req.URL.Path, req.Method; { + case p == "/namespaces/test/configmaps" && m == "POST": + return &http.Response{StatusCode: 201, Body: objBody(codec, configMap)}, nil + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }), + } + tf.Namespace = "test" + buf := bytes.NewBuffer([]byte{}) + cmd := NewCmdCreateConfigMap(f, buf) + cmd.Flags().Set("output", "name") + cmd.Run(cmd, []string{configMap.Name}) + expectedOutput := "configmap/" + configMap.Name + "\n" + if buf.String() != expectedOutput { + t.Errorf("expected output: %s, but got: %s", buf.String(), expectedOutput) + } +} diff --git a/pkg/kubectl/cmd/create_secret.go b/pkg/kubectl/cmd/create_secret.go index 0a4c66b18a0..6cba3ba07b6 100644 --- a/pkg/kubectl/cmd/create_secret.go +++ b/pkg/kubectl/cmd/create_secret.go @@ -43,7 +43,7 @@ func NewCmdCreateSecret(f *cmdutil.Factory, cmdOut io.Writer) *cobra.Command { const ( secretLong = ` -Create a secret based on a file, directory, or specified literal value +Create a secret based on a file, directory, or specified literal value. A single secret may package one or more key/value pairs. @@ -87,7 +87,7 @@ func NewCmdCreateSecretGeneric(f *cmdutil.Factory, cmdOut io.Writer) *cobra.Comm return cmd } -// CreateSecretGeneric is the implementation the create secret generic command +// CreateSecretGeneric is the implementation of the create secret generic command func CreateSecretGeneric(f *cmdutil.Factory, cmdOut io.Writer, cmd *cobra.Command, args []string) error { name, err := NameFromCommandArgs(cmd, args) if err != nil { diff --git a/pkg/kubectl/cmd/util/factory.go b/pkg/kubectl/cmd/util/factory.go index 9fbd31a4a3f..ca6413b75d8 100644 --- a/pkg/kubectl/cmd/util/factory.go +++ b/pkg/kubectl/cmd/util/factory.go @@ -142,6 +142,7 @@ const ( NamespaceV1GeneratorName = "namespace/v1" SecretV1GeneratorName = "secret/v1" SecretForDockerRegistryV1GeneratorName = "secret-for-docker-registry/v1" + ConfigMapV1GeneratorName = "configmap/v1" ) // DefaultGenerators returns the set of default generators for use in Factory instances diff --git a/pkg/kubectl/configmap.go b/pkg/kubectl/configmap.go new file mode 100644 index 00000000000..04ed4aa6a45 --- /dev/null +++ b/pkg/kubectl/configmap.go @@ -0,0 +1,212 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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 kubectl + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "strings" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/validation" + "k8s.io/kubernetes/pkg/runtime" +) + +// ConfigMapGeneratorV1 supports stable generation of a configMap. +type ConfigMapGeneratorV1 struct { + // Name of configMap (required) + Name string + // Type of configMap (optional) + Type string + // FileSources to derive the configMap from (optional) + FileSources []string + // LiteralSources to derive the configMap from (optional) + LiteralSources []string +} + +// Ensure it supports the generator pattern that uses parameter injection. +var _ Generator = &ConfigMapGeneratorV1{} + +// Ensure it supports the generator pattern that uses parameters specified during construction. +var _ StructuredGenerator = &ConfigMapGeneratorV1{} + +// Generate returns a configMap using the specified parameters. +func (s ConfigMapGeneratorV1) Generate(genericParams map[string]interface{}) (runtime.Object, error) { + err := ValidateParams(s.ParamNames(), genericParams) + if err != nil { + return nil, err + } + delegate := &ConfigMapGeneratorV1{} + fromFileStrings, found := genericParams["from-file"] + if found { + fromFileArray, isArray := fromFileStrings.([]string) + if !isArray { + return nil, fmt.Errorf("expected []string, found :%v", fromFileStrings) + } + delegate.FileSources = fromFileArray + delete(genericParams, "from-file") + } + fromLiteralStrings, found := genericParams["from-literal"] + if found { + fromLiteralArray, isArray := fromLiteralStrings.([]string) + if !isArray { + return nil, fmt.Errorf("expected []string, found :%v", fromFileStrings) + } + delegate.LiteralSources = fromLiteralArray + delete(genericParams, "from-literal") + } + params := map[string]string{} + for key, value := range genericParams { + strVal, isString := value.(string) + if !isString { + return nil, fmt.Errorf("expected string, saw %v for '%s'", value, key) + } + params[key] = strVal + } + delegate.Name = params["name"] + delegate.Type = params["type"] + return delegate.StructuredGenerate() +} + +// ParamNames returns the set of supported input parameters when using the parameter injection generator pattern. +func (s ConfigMapGeneratorV1) ParamNames() []GeneratorParam { + return []GeneratorParam{ + {"name", true}, + {"type", false}, + {"from-file", false}, + {"from-literal", false}, + {"force", false}, + } +} + +// StructuredGenerate outputs a configMap object using the configured fields. +func (s ConfigMapGeneratorV1) StructuredGenerate() (runtime.Object, error) { + if err := s.validate(); err != nil { + return nil, err + } + configMap := &api.ConfigMap{} + configMap.Name = s.Name + configMap.Data = map[string]string{} + if len(s.FileSources) > 0 { + if err := handleConfigMapFromFileSources(configMap, s.FileSources); err != nil { + return nil, err + } + } + if len(s.LiteralSources) > 0 { + if err := handleConfigMapFromLiteralSources(configMap, s.LiteralSources); err != nil { + return nil, err + } + } + return configMap, nil +} + +// validate validates required fields are set to support structured generation. +func (s ConfigMapGeneratorV1) validate() error { + if len(s.Name) == 0 { + return fmt.Errorf("name must be specified") + } + return nil +} + +// handleConfigMapFromLiteralSources adds the specified literal source +// information into the provided configMap. +func handleConfigMapFromLiteralSources(configMap *api.ConfigMap, literalSources []string) error { + for _, literalSource := range literalSources { + keyName, value, err := parseLiteralSource(literalSource) + if err != nil { + return err + } + err = addKeyFromLiteralToConfigMap(configMap, keyName, value) + if err != nil { + return err + } + } + return nil +} + +// handleConfigMapFromFileSources adds the specified file source information +// into the provided configMap +func handleConfigMapFromFileSources(configMap *api.ConfigMap, fileSources []string) error { + for _, fileSource := range fileSources { + keyName, filePath, err := parseFileSource(fileSource) + if err != nil { + return err + } + info, err := os.Stat(filePath) + if err != nil { + switch err := err.(type) { + case *os.PathError: + return fmt.Errorf("error reading %s: %v", filePath, err.Err) + default: + return fmt.Errorf("error reading %s: %v", filePath, err) + } + } + if info.IsDir() { + if strings.Contains(fileSource, "=") { + return fmt.Errorf("cannot give a key name for a directory path.") + } + fileList, err := ioutil.ReadDir(filePath) + if err != nil { + return fmt.Errorf("error listing files in %s: %v", filePath, err) + } + for _, item := range fileList { + itemPath := path.Join(filePath, item.Name()) + if item.Mode().IsRegular() { + keyName = item.Name() + err = addKeyFromFileToConfigMap(configMap, keyName, itemPath) + if err != nil { + return err + } + } + } + } else { + err = addKeyFromFileToConfigMap(configMap, keyName, filePath) + if err != nil { + return err + } + } + } + + return nil +} + +// 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 *api.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 *api.ConfigMap, keyName, data string) error { + // Note, the rules for ConfigMap keys are the exact same as the ones for SecretKeys + // to be consistent; validation.IsSecretKey is used here intentionally. + if !validation.IsSecretKey(keyName) { + return fmt.Errorf("%v is not a valid key name for a configMap", keyName) + } + 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/kubectl/configmap_test.go b/pkg/kubectl/configmap_test.go new file mode 100644 index 00000000000..2fc936c9ec3 --- /dev/null +++ b/pkg/kubectl/configmap_test.go @@ -0,0 +1,108 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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 kubectl + +import ( + "reflect" + "testing" + + "k8s.io/kubernetes/pkg/api" +) + +func TestConfigMapGenerate(t *testing.T) { + tests := []struct { + params map[string]interface{} + expected *api.ConfigMap + expectErr bool + }{ + { + params: map[string]interface{}{ + "name": "foo", + }, + expected: &api.ConfigMap{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + }, + Data: map[string]string{}, + }, + expectErr: false, + }, + { + params: map[string]interface{}{ + "name": "foo", + "type": "my-type", + }, + expected: &api.ConfigMap{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + }, + Data: map[string]string{}, + }, + expectErr: false, + }, + { + params: map[string]interface{}{ + "name": "foo", + "from-literal": []string{"key1=value1", "key2=value2"}, + }, + expected: &api.ConfigMap{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + }, + Data: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + expectErr: false, + }, + { + params: map[string]interface{}{ + "name": "foo", + "from-literal": []string{"key1value1"}, + }, + expectErr: true, + }, + { + params: map[string]interface{}{ + "name": "foo", + "from-file": []string{"key1=/file=2"}, + }, + expectErr: true, + }, + { + params: map[string]interface{}{ + "name": "foo", + "from-file": []string{"key1==value"}, + }, + expectErr: true, + }, + } + generator := ConfigMapGeneratorV1{} + for _, test := range tests { + obj, err := generator.Generate(test.params) + if !test.expectErr && err != nil { + t.Errorf("unexpected error: %v", err) + } + if test.expectErr && err != nil { + continue + } + if !reflect.DeepEqual(obj.(*api.ConfigMap), test.expected) { + t.Errorf("\nexpected:\n%#v\nsaw:\n%#v", test.expected, obj.(*api.ConfigMap)) + } + } +} diff --git a/pkg/kubectl/kubectl.go b/pkg/kubectl/kubectl.go index 2f9c7c4e87f..60b280174a6 100644 --- a/pkg/kubectl/kubectl.go +++ b/pkg/kubectl/kubectl.go @@ -18,6 +18,9 @@ limitations under the License. package kubectl import ( + "errors" + "fmt" + "path" "strings" "k8s.io/kubernetes/pkg/api" @@ -120,3 +123,36 @@ func expandResourceShortcut(resource unversioned.GroupVersionResource) unversion } return resource } + +// 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 +func parseLiteralSource(source string) (keyName, value string, err error) { + items := strings.Split(source, "=") + 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/kubectl/kubectl_test.go b/pkg/kubectl/kubectl_test.go new file mode 100644 index 00000000000..6381b492067 --- /dev/null +++ b/pkg/kubectl/kubectl_test.go @@ -0,0 +1,194 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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 kubectl + +import ( + "testing" +) + +func TestParseFileSource(t *testing.T) { + cases := []struct { + name string + input string + key string + filepath string + err bool + }{ + { + name: "success 1", + input: "boo=zoo", + key: "boo", + filepath: "zoo", + err: false, + }, + { + name: "success 2", + input: "boo=/path/to/zoo", + key: "boo", + filepath: "/path/to/zoo", + err: false, + }, + { + name: "success 3", + input: "boo-2=/1/2/3/4/5/zab.txt", + key: "boo-2", + filepath: "/1/2/3/4/5/zab.txt", + err: false, + }, + { + name: "success 4", + input: "boo-=this/seems/weird.txt", + key: "boo-", + filepath: "this/seems/weird.txt", + err: false, + }, + { + name: "success 5", + input: "-key=some/path", + key: "-key", + filepath: "some/path", + err: false, + }, + { + name: "invalid 1", + input: "key==some/path", + err: true, + }, + { + name: "invalid 2", + input: "=key=some/path", + err: true, + }, + { + name: "invalid 3", + input: "==key=/some/other/path", + err: true, + }, + { + name: "invalid 4", + input: "=key", + err: true, + }, + { + name: "invalid 5", + input: "key=", + err: true, + }, + } + + for _, tc := range cases { + key, filepath, err := parseFileSource(tc.input) + if err != nil { + if tc.err { + continue + } + + t.Errorf("%v: unexpected error: %v", tc.name, err) + continue + } + + if tc.err { + t.Errorf("%v: unexpected success", tc.name) + continue + } + + if e, a := tc.key, key; e != a { + t.Errorf("%v: expected key %v; got %v", tc.name, e, a) + continue + } + + if e, a := tc.filepath, filepath; e != a { + t.Errorf("%v: expected filepath %v; got %v", tc.name, e, a) + } + } +} + +func TestParseLiteralSource(t *testing.T) { + cases := []struct { + name string + input string + key string + value string + err bool + }{ + { + name: "success 1", + input: "key=value", + key: "key", + value: "value", + err: false, + }, + { + name: "success 2", + input: "key=value/with/slashes", + key: "key", + value: "value/with/slashes", + err: false, + }, + { + name: "err 1", + input: "key==value", + err: true, + }, + { + name: "err 2", + input: "key=value=", + err: true, + }, + { + name: "err 3", + input: "key2=value==", + err: true, + }, + { + name: "err 4", + input: "==key", + err: true, + }, + { + name: "err 5", + input: "=key=", + err: true, + }, + } + + for _, tc := range cases { + key, value, err := parseLiteralSource(tc.input) + if err != nil { + if tc.err { + continue + } + + t.Errorf("%v: unexpected error: %v", tc.name, err) + continue + } + + if tc.err { + t.Errorf("%v: unexpected success", tc.name) + continue + } + + if e, a := tc.key, key; e != a { + t.Errorf("%v: expected key %v; got %v", tc.name, e, a) + continue + } + + if e, a := tc.value, value; e != a { + t.Errorf("%v: expected value %v; got %v", tc.name, e, a) + } + } +} diff --git a/pkg/kubectl/secret.go b/pkg/kubectl/secret.go index 62de7900874..e5b7cc33e78 100644 --- a/pkg/kubectl/secret.go +++ b/pkg/kubectl/secret.go @@ -17,7 +17,6 @@ limitations under the License. package kubectl import ( - "errors" "fmt" "io/ioutil" "os" @@ -206,33 +205,3 @@ func addKeyFromLiteralToSecret(secret *api.Secret, keyName string, data []byte) secret.Data[keyName] = data return nil } - -// parseFileSource parses the source given. Acceptable formats include: -// source-name=source-path, where source-name will become the key name and source-path is the path to the key file -// source-path, where source-path is a path to a file or directory, and key names will default to file names -// 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 -func parseLiteralSource(source string) (keyName, value string, err error) { - items := strings.Split(source, "=") - if len(items) != 2 { - return "", "", fmt.Errorf("invalid literal source %v, expected key=value", source) - } - return items[0], items[1], nil -}