diff --git a/pkg/kubectl/BUILD b/pkg/kubectl/BUILD index 98923897872..e1a70141f6c 100644 --- a/pkg/kubectl/BUILD +++ b/pkg/kubectl/BUILD @@ -19,6 +19,7 @@ go_library( "configmap.go", "deployment.go", "doc.go", + "env_file.go", "explain.go", "generate.go", "history.go", diff --git a/pkg/kubectl/cmd/create_secret.go b/pkg/kubectl/cmd/create_secret.go index 176703c9891..e79af190838 100644 --- a/pkg/kubectl/cmd/create_secret.go +++ b/pkg/kubectl/cmd/create_secret.go @@ -64,7 +64,10 @@ var ( kubectl create secret generic my-secret --from-file=ssh-privatekey=~/.ssh/id_rsa --from-file=ssh-publickey=~/.ssh/id_rsa.pub # Create a new secret named my-secret with key1=supersecret and key2=topsecret - kubectl create secret generic my-secret --from-literal=key1=supersecret --from-literal=key2=topsecret`) + kubectl create secret generic my-secret --from-literal=key1=supersecret --from-literal=key2=topsecret + + # Create a new secret named my-secret from an env file + kubectl create secret generic my-secret --from-env-file=path/to/bar.env`) ) // NewCmdCreateSecretGeneric is a command to create generic secrets from files, directories, or literal values @@ -85,6 +88,7 @@ func NewCmdCreateSecretGeneric(f cmdutil.Factory, cmdOut io.Writer) *cobra.Comma cmdutil.AddGeneratorFlags(cmd, cmdutil.SecretV1GeneratorName) 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 secret key.") cmd.Flags().StringArray("from-literal", []string{}, "Specify a key and literal value to insert in secret (i.e. mykey=somevalue)") + cmd.Flags().String("from-env-file", "", "Specify the path to a file to read lines of key=val pairs to create a secret (i.e. a Docker .env file).") cmd.Flags().String("type", "", i18n.T("The type of secret to create")) return cmd } @@ -103,6 +107,7 @@ func CreateSecretGeneric(f cmdutil.Factory, cmdOut io.Writer, cmd *cobra.Command Type: cmdutil.GetFlagString(cmd, "type"), FileSources: cmdutil.GetFlagStringSlice(cmd, "from-file"), LiteralSources: cmdutil.GetFlagStringArray(cmd, "from-literal"), + EnvFileSource: cmdutil.GetFlagString(cmd, "from-env-file"), } default: return cmdutil.UsageError(cmd, fmt.Sprintf("Generator: %s not supported.", generatorName)) diff --git a/pkg/kubectl/configmap.go b/pkg/kubectl/configmap.go index 71e8c0c3b85..794903dd8e1 100644 --- a/pkg/kubectl/configmap.go +++ b/pkg/kubectl/configmap.go @@ -221,7 +221,10 @@ func handleConfigMapFromEnvFileSource(configMap *api.ConfigMap, envFileSource st if info.IsDir() { return fmt.Errorf("must be a file") } - return addFromEnvFileToConfigMap(configMap, envFileSource) + + 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 diff --git a/pkg/kubectl/secret.go b/pkg/kubectl/secret.go index 70d436b05bc..1882adf4588 100644 --- a/pkg/kubectl/secret.go +++ b/pkg/kubectl/secret.go @@ -38,6 +38,8 @@ type SecretGeneratorV1 struct { FileSources []string // LiteralSources to derive the secret from (optional) LiteralSources []string + // EnvFileSource to derive the secret from (optional) + EnvFileSource string } // Ensure it supports the generator pattern that uses parameter injection @@ -71,6 +73,15 @@ func (s SecretGeneratorV1) Generate(genericParams map[string]interface{}) (runti delegate.LiteralSources = fromLiteralArray delete(genericParams, "from-literal") } + fromEnvFileString, found := genericParams["from-env-file"] + if found { + fromEnvFile, isString := fromEnvFileString.(string) + if !isString { + return nil, fmt.Errorf("expected string, found :%v", fromEnvFileString) + } + delegate.EnvFileSource = fromEnvFile + delete(genericParams, "from-env-file") + } params := map[string]string{} for key, value := range genericParams { strVal, isString := value.(string) @@ -91,6 +102,7 @@ func (s SecretGeneratorV1) ParamNames() []GeneratorParam { {"type", false}, {"from-file", false}, {"from-literal", false}, + {"from-env-file", false}, {"force", false}, } } @@ -116,6 +128,11 @@ func (s SecretGeneratorV1) StructuredGenerate() (runtime.Object, error) { return nil, err } } + if len(s.EnvFileSource) > 0 { + if err := handleFromEnvFileSource(secret, s.EnvFileSource); err != nil { + return nil, err + } + } return secret, nil } @@ -124,6 +141,9 @@ func (s SecretGeneratorV1) validate() error { if len(s.Name) == 0 { return fmt.Errorf("name must be specified") } + if len(s.EnvFileSource) > 0 && (len(s.FileSources) > 0 || len(s.LiteralSources) > 0) { + return fmt.Errorf("from-env-file cannot be combined with from-file or from-literal") + } return nil } @@ -187,6 +207,27 @@ func handleFromFileSources(secret *api.Secret, fileSources []string) error { return nil } +// handleFromEnvFileSource adds the specified env file source information +// into the provided secret +func handleFromEnvFileSource(secret *api.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("must be a file") + } + + return addFromEnvFile(envFileSource, func(key, value string) error { + return addKeyFromLiteralToSecret(secret, key, []byte(value)) + }) +} + func addKeyFromFileToSecret(secret *api.Secret, keyName, filePath string) error { data, err := ioutil.ReadFile(filePath) if err != nil { diff --git a/pkg/kubectl/secret_test.go b/pkg/kubectl/secret_test.go index cb9856d72ae..309c7848d65 100644 --- a/pkg/kubectl/secret_test.go +++ b/pkg/kubectl/secret_test.go @@ -17,6 +17,7 @@ limitations under the License. package kubectl import ( + "os" "reflect" "testing" @@ -26,6 +27,7 @@ import ( func TestSecretGenerate(t *testing.T) { tests := []struct { + setup func(t *testing.T, params map[string]interface{}) func() params map[string]interface{} expected *api.Secret expectErr bool @@ -108,9 +110,84 @@ func TestSecretGenerate(t *testing.T) { }, expectErr: false, }, + { + setup: setupEnvFile("key1=value1", "#", "", "key2=value2"), + params: map[string]interface{}{ + "name": "valid_env", + "from-env-file": "file.env", + }, + expected: &api.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "valid_env", + }, + Data: map[string][]byte{ + "key1": []byte("value1"), + "key2": []byte("value2"), + }, + }, + expectErr: false, + }, + { + setup: func() func(t *testing.T, params map[string]interface{}) func() { + os.Setenv("g_key1", "1") + os.Setenv("g_key2", "2") + return setupEnvFile("g_key1", "g_key2=") + }(), + params: map[string]interface{}{ + "name": "getenv", + "from-env-file": "file.env", + }, + expected: &api.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "getenv", + }, + Data: map[string][]byte{ + "g_key1": []byte("1"), + "g_key2": []byte(""), + }, + }, + expectErr: false, + }, + { + params: map[string]interface{}{ + "name": "too_many_args", + "from-literal": []string{"key1=value1"}, + "from-env-file": "file.env", + }, + expectErr: true, + }, + { + setup: setupEnvFile("key.1=value1"), + params: map[string]interface{}{ + "name": "invalid_key", + "from-env-file": "file.env", + }, + expectErr: true, + }, + { + setup: setupEnvFile(" key1= value1"), + params: map[string]interface{}{ + "name": "with_spaces", + "from-env-file": "file.env", + }, + expected: &api.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "with_spaces", + }, + Data: map[string][]byte{ + "key1": []byte(" value1"), + }, + }, + expectErr: false, + }, } generator := SecretGeneratorV1{} for _, test := range tests { + if test.setup != nil { + if teardown := test.setup(t, test.params); teardown != nil { + defer teardown() + } + } obj, err := generator.Generate(test.params) if !test.expectErr && err != nil { t.Errorf("unexpected error: %v", err)