diff --git a/pkg/kubectl/BUILD b/pkg/kubectl/BUILD index d8e26502ab..8c6005bf62 100644 --- a/pkg/kubectl/BUILD +++ b/pkg/kubectl/BUILD @@ -132,6 +132,7 @@ go_library( "//pkg/credentialprovider:go_default_library", "//pkg/kubectl/resource:go_default_library", "//pkg/kubectl/util:go_default_library", + "//pkg/kubectl/util/hash:go_default_library", "//pkg/kubectl/util/slice:go_default_library", "//pkg/printers:go_default_library", "//pkg/printers/internalversion:go_default_library", diff --git a/pkg/kubectl/cmd/create.go b/pkg/kubectl/cmd/create.go index 7447d50c3a..33dadb10e3 100644 --- a/pkg/kubectl/cmd/create.go +++ b/pkg/kubectl/cmd/create.go @@ -291,7 +291,7 @@ func RunCreateSubcommand(f cmdutil.Factory, cmd *cobra.Command, out io.Writer, o } if useShortOutput := options.OutputFormat == "name"; useShortOutput || len(options.OutputFormat) == 0 { - cmdutil.PrintSuccess(mapper, useShortOutput, out, mapping.Resource, options.Name, options.DryRun, "created") + cmdutil.PrintSuccess(mapper, useShortOutput, out, mapping.Resource, info.Name, options.DryRun, "created") return nil } diff --git a/pkg/kubectl/cmd/create_configmap.go b/pkg/kubectl/cmd/create_configmap.go index b1b841eddf..b9f17adede 100644 --- a/pkg/kubectl/cmd/create_configmap.go +++ b/pkg/kubectl/cmd/create_configmap.go @@ -77,6 +77,7 @@ func NewCmdCreateConfigMap(f cmdutil.Factory, cmdOut io.Writer) *cobra.Command { cmd.Flags().StringSlice("from-file", []string{}, "Key file can be specified using its file path, in which case file basename will be used as configmap key, or optionally with a key and file path, in which case the given key will be used. Specifying a directory will iterate each named file in the directory whose basename is a valid configmap key.") cmd.Flags().StringArray("from-literal", []string{}, "Specify a key and literal value to insert in configmap (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 configmap (i.e. a Docker .env file).") + cmd.Flags().Bool("append-hash", false, "Append a hash of the configmap to its name.") return cmd } @@ -94,6 +95,7 @@ func CreateConfigMap(f cmdutil.Factory, cmdOut io.Writer, cmd *cobra.Command, ar FileSources: cmdutil.GetFlagStringSlice(cmd, "from-file"), LiteralSources: cmdutil.GetFlagStringArray(cmd, "from-literal"), EnvFileSource: cmdutil.GetFlagString(cmd, "from-env-file"), + AppendHash: cmdutil.GetFlagBool(cmd, "append-hash"), } default: return errUnsupportedGenerator(cmd, generatorName) diff --git a/pkg/kubectl/cmd/create_secret.go b/pkg/kubectl/cmd/create_secret.go index 8d2ba2cec5..9a762cb5ae 100644 --- a/pkg/kubectl/cmd/create_secret.go +++ b/pkg/kubectl/cmd/create_secret.go @@ -89,6 +89,7 @@ func NewCmdCreateSecretGeneric(f cmdutil.Factory, cmdOut io.Writer) *cobra.Comma 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")) + cmd.Flags().Bool("append-hash", false, "Append a hash of the secret to its name.") return cmd } @@ -107,6 +108,7 @@ func CreateSecretGeneric(f cmdutil.Factory, cmdOut io.Writer, cmd *cobra.Command FileSources: cmdutil.GetFlagStringSlice(cmd, "from-file"), LiteralSources: cmdutil.GetFlagStringArray(cmd, "from-literal"), EnvFileSource: cmdutil.GetFlagString(cmd, "from-env-file"), + AppendHash: cmdutil.GetFlagBool(cmd, "append-hash"), } default: return errUnsupportedGenerator(cmd, generatorName) @@ -163,6 +165,7 @@ func NewCmdCreateSecretDockerRegistry(f cmdutil.Factory, cmdOut io.Writer) *cobr cmd.MarkFlagRequired("docker-password") cmd.Flags().String("docker-email", "", i18n.T("Email for Docker registry")) cmd.Flags().String("docker-server", "https://index.docker.io/v1/", i18n.T("Server location for Docker registry")) + cmd.Flags().Bool("append-hash", false, "Append a hash of the secret to its name.") cmdutil.AddInclude3rdPartyFlags(cmd) return cmd } @@ -183,11 +186,12 @@ func CreateSecretDockerRegistry(f cmdutil.Factory, cmdOut io.Writer, cmd *cobra. switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName { case cmdutil.SecretForDockerRegistryV1GeneratorName: generator = &kubectl.SecretForDockerRegistryGeneratorV1{ - Name: name, - Username: cmdutil.GetFlagString(cmd, "docker-username"), - Email: cmdutil.GetFlagString(cmd, "docker-email"), - Password: cmdutil.GetFlagString(cmd, "docker-password"), - Server: cmdutil.GetFlagString(cmd, "docker-server"), + Name: name, + Username: cmdutil.GetFlagString(cmd, "docker-username"), + Email: cmdutil.GetFlagString(cmd, "docker-email"), + Password: cmdutil.GetFlagString(cmd, "docker-password"), + Server: cmdutil.GetFlagString(cmd, "docker-server"), + AppendHash: cmdutil.GetFlagBool(cmd, "append-hash"), } default: return errUnsupportedGenerator(cmd, generatorName) @@ -229,6 +233,7 @@ func NewCmdCreateSecretTLS(f cmdutil.Factory, cmdOut io.Writer) *cobra.Command { cmdutil.AddGeneratorFlags(cmd, cmdutil.SecretForTLSV1GeneratorName) cmd.Flags().String("cert", "", i18n.T("Path to PEM encoded public key certificate.")) cmd.Flags().String("key", "", i18n.T("Path to private key associated with given certificate.")) + cmd.Flags().Bool("append-hash", false, "Append a hash of the secret to its name.") return cmd } @@ -248,9 +253,10 @@ func CreateSecretTLS(f cmdutil.Factory, cmdOut io.Writer, cmd *cobra.Command, ar switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName { case cmdutil.SecretForTLSV1GeneratorName: generator = &kubectl.SecretForTLSGeneratorV1{ - Name: name, - Key: cmdutil.GetFlagString(cmd, "key"), - Cert: cmdutil.GetFlagString(cmd, "cert"), + Name: name, + Key: cmdutil.GetFlagString(cmd, "key"), + Cert: cmdutil.GetFlagString(cmd, "cert"), + AppendHash: cmdutil.GetFlagBool(cmd, "append-hash"), } default: return errUnsupportedGenerator(cmd, generatorName) diff --git a/pkg/kubectl/configmap.go b/pkg/kubectl/configmap.go index c6cb5963f3..a8161906c2 100644 --- a/pkg/kubectl/configmap.go +++ b/pkg/kubectl/configmap.go @@ -26,6 +26,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/kubectl/util/hash" ) // ConfigMapGeneratorV1 supports stable generation of a configMap. @@ -40,6 +41,8 @@ type ConfigMapGeneratorV1 struct { LiteralSources []string // EnvFileSource to derive the configMap from (optional) EnvFileSource string + // AppendHash; if true, derive a hash from the ConfigMap and append it to the name + AppendHash bool } // Ensure it supports the generator pattern that uses parameter injection. @@ -73,14 +76,6 @@ func (s ConfigMapGeneratorV1) Generate(genericParams map[string]interface{}) (ru 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 - } fromEnvFileString, found := genericParams["from-env-file"] if found { fromEnvFile, isString := fromEnvFileString.(string) @@ -90,8 +85,26 @@ func (s ConfigMapGeneratorV1) Generate(genericParams map[string]interface{}) (ru delegate.EnvFileSource = fromEnvFile delete(genericParams, "from-env-file") } + hashParam, found := genericParams["append-hash"] + if found { + hashBool, isBool := hashParam.(bool) + if !isBool { + return nil, fmt.Errorf("expected bool, found :%v", hashParam) + } + delegate.AppendHash = hashBool + delete(genericParams, "append-hash") + } + 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() } @@ -104,6 +117,7 @@ func (s ConfigMapGeneratorV1) ParamNames() []GeneratorParam { {"from-literal", false}, {"from-env-file", false}, {"force", false}, + {"hash", false}, } } @@ -130,6 +144,13 @@ func (s ConfigMapGeneratorV1) StructuredGenerate() (runtime.Object, error) { return nil, err } } + if s.AppendHash { + h, err := hash.ConfigMapHash(configMap) + if err != nil { + return nil, err + } + configMap.Name = fmt.Sprintf("%s-%s", configMap.Name, h) + } return configMap, nil } diff --git a/pkg/kubectl/configmap_test.go b/pkg/kubectl/configmap_test.go index b5c98b58f4..74a85a89eb 100644 --- a/pkg/kubectl/configmap_test.go +++ b/pkg/kubectl/configmap_test.go @@ -45,6 +45,19 @@ func TestConfigMapGenerate(t *testing.T) { }, expectErr: false, }, + { + params: map[string]interface{}{ + "name": "foo", + "append-hash": true, + }, + expected: &api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-867km9574f", + }, + Data: map[string]string{}, + }, + expectErr: false, + }, { params: map[string]interface{}{ "name": "foo", @@ -58,6 +71,20 @@ func TestConfigMapGenerate(t *testing.T) { }, expectErr: false, }, + { + params: map[string]interface{}{ + "name": "foo", + "type": "my-type", + "append-hash": true, + }, + expected: &api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-867km9574f", + }, + Data: map[string]string{}, + }, + expectErr: false, + }, { params: map[string]interface{}{ "name": "foo", @@ -74,6 +101,23 @@ func TestConfigMapGenerate(t *testing.T) { }, expectErr: false, }, + { + params: map[string]interface{}{ + "name": "foo", + "from-literal": []string{"key1=value1", "key2=value2"}, + "append-hash": true, + }, + expected: &api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-gcb75dd9gb", + }, + Data: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + expectErr: false, + }, { params: map[string]interface{}{ "name": "foo", @@ -110,6 +154,22 @@ func TestConfigMapGenerate(t *testing.T) { }, expectErr: false, }, + { + params: map[string]interface{}{ + "name": "foo", + "from-literal": []string{"key1==value1"}, + "append-hash": true, + }, + expected: &api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-bdgk9ttt7m", + }, + Data: map[string]string{ + "key1": "=value1", + }, + }, + expectErr: false, + }, { setup: setupEnvFile("key1=value1", "#", "", "key2=value2"), params: map[string]interface{}{ @@ -127,6 +187,24 @@ func TestConfigMapGenerate(t *testing.T) { }, expectErr: false, }, + { + setup: setupEnvFile("key1=value1", "#", "", "key2=value2"), + params: map[string]interface{}{ + "name": "valid_env", + "from-env-file": "file.env", + "append-hash": true, + }, + expected: &api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "valid_env-2cgh8552ch", + }, + Data: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + expectErr: false, + }, { setup: func() func(t *testing.T, params map[string]interface{}) func() { os.Setenv("g_key1", "1") @@ -148,6 +226,28 @@ func TestConfigMapGenerate(t *testing.T) { }, 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", + "append-hash": true, + }, + expected: &api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "getenv-b4hh92hgdk", + }, + Data: map[string]string{ + "g_key1": "1", + "g_key2": "", + }, + }, + expectErr: false, + }, { params: map[string]interface{}{ "name": "too_many_args", @@ -180,9 +280,26 @@ func TestConfigMapGenerate(t *testing.T) { }, expectErr: false, }, + { + setup: setupEnvFile(" key1= value1"), + params: map[string]interface{}{ + "name": "with_spaces", + "from-env-file": "file.env", + "append-hash": true, + }, + expected: &api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "with_spaces-bfc558b4ct", + }, + Data: map[string]string{ + "key1": " value1", + }, + }, + expectErr: false, + }, } generator := ConfigMapGeneratorV1{} - for _, test := range tests { + for i, test := range tests { if test.setup != nil { if teardown := test.setup(t, test.params); teardown != nil { defer teardown() @@ -190,13 +307,13 @@ func TestConfigMapGenerate(t *testing.T) { } obj, err := generator.Generate(test.params) if !test.expectErr && err != nil { - t.Errorf("unexpected error: %v", err) + t.Errorf("case %d, unexpected error: %v", i, 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)) + t.Errorf("\ncase %d, expected:\n%#v\nsaw:\n%#v", i, test.expected, obj.(*api.ConfigMap)) } } } diff --git a/pkg/kubectl/secret.go b/pkg/kubectl/secret.go index ecb2f34e95..856d1d86f9 100644 --- a/pkg/kubectl/secret.go +++ b/pkg/kubectl/secret.go @@ -26,6 +26,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/kubectl/util/hash" ) // SecretGeneratorV1 supports stable generation of an opaque secret @@ -40,6 +41,8 @@ type SecretGeneratorV1 struct { LiteralSources []string // EnvFileSource to derive the secret from (optional) EnvFileSource string + // AppendHash; if true, derive a hash from the Secret data and type and append it to the name + AppendHash bool } // Ensure it supports the generator pattern that uses parameter injection @@ -82,6 +85,17 @@ func (s SecretGeneratorV1) Generate(genericParams map[string]interface{}) (runti delegate.EnvFileSource = fromEnvFile delete(genericParams, "from-env-file") } + + hashParam, found := genericParams["append-hash"] + if found { + hashBool, isBool := hashParam.(bool) + if !isBool { + return nil, fmt.Errorf("expected bool, found :%v", hashParam) + } + delegate.AppendHash = hashBool + delete(genericParams, "append-hash") + } + params := map[string]string{} for key, value := range genericParams { strVal, isString := value.(string) @@ -92,6 +106,7 @@ func (s SecretGeneratorV1) Generate(genericParams map[string]interface{}) (runti } delegate.Name = params["name"] delegate.Type = params["type"] + return delegate.StructuredGenerate() } @@ -104,6 +119,7 @@ func (s SecretGeneratorV1) ParamNames() []GeneratorParam { {"from-literal", false}, {"from-env-file", false}, {"force", false}, + {"append-hash", false}, } } @@ -133,6 +149,13 @@ func (s SecretGeneratorV1) StructuredGenerate() (runtime.Object, error) { return nil, err } } + if s.AppendHash { + h, err := hash.SecretHash(secret) + if err != nil { + return nil, err + } + secret.Name = fmt.Sprintf("%s-%s", secret.Name, h) + } return secret, nil } diff --git a/pkg/kubectl/secret_for_docker_registry.go b/pkg/kubectl/secret_for_docker_registry.go index 72e91fb82e..d39ebb5b32 100644 --- a/pkg/kubectl/secret_for_docker_registry.go +++ b/pkg/kubectl/secret_for_docker_registry.go @@ -23,6 +23,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/credentialprovider" + "k8s.io/kubernetes/pkg/kubectl/util/hash" ) // SecretForDockerRegistryGeneratorV1 supports stable generation of a docker registry secret @@ -37,6 +38,8 @@ type SecretForDockerRegistryGeneratorV1 struct { Password string // Server for registry (required) Server string + // AppendHash; if true, derive a hash from the Secret and append it to the name + AppendHash bool } // Ensure it supports the generator pattern that uses parameter injection @@ -51,6 +54,16 @@ func (s SecretForDockerRegistryGeneratorV1) Generate(genericParams map[string]in if err != nil { return nil, err } + delegate := &SecretForDockerRegistryGeneratorV1{} + hashParam, found := genericParams["append-hash"] + if found { + hashBool, isBool := hashParam.(bool) + if !isBool { + return nil, fmt.Errorf("expected bool, found :%v", hashParam) + } + delegate.AppendHash = hashBool + delete(genericParams, "append-hash") + } params := map[string]string{} for key, value := range genericParams { strVal, isString := value.(string) @@ -59,13 +72,11 @@ func (s SecretForDockerRegistryGeneratorV1) Generate(genericParams map[string]in } params[key] = strVal } - delegate := &SecretForDockerRegistryGeneratorV1{ - Name: params["name"], - Username: params["docker-username"], - Email: params["docker-email"], - Password: params["docker-password"], - Server: params["docker-server"], - } + delegate.Name = params["name"] + delegate.Username = params["docker-username"] + delegate.Email = params["docker-email"] + delegate.Password = params["docker-password"] + delegate.Server = params["docker-server"] return delegate.StructuredGenerate() } @@ -83,6 +94,13 @@ func (s SecretForDockerRegistryGeneratorV1) StructuredGenerate() (runtime.Object secret.Type = api.SecretTypeDockercfg secret.Data = map[string][]byte{} secret.Data[api.DockerConfigKey] = dockercfgContent + if s.AppendHash { + h, err := hash.SecretHash(secret) + if err != nil { + return nil, err + } + secret.Name = fmt.Sprintf("%s-%s", secret.Name, h) + } return secret, nil } @@ -94,6 +112,7 @@ func (s SecretForDockerRegistryGeneratorV1) ParamNames() []GeneratorParam { {"docker-email", false}, {"docker-password", true}, {"docker-server", true}, + {"append-hash", false}, } } diff --git a/pkg/kubectl/secret_for_docker_registry_test.go b/pkg/kubectl/secret_for_docker_registry_test.go index d9b52fe5b8..e1621cbefa 100644 --- a/pkg/kubectl/secret_for_docker_registry_test.go +++ b/pkg/kubectl/secret_for_docker_registry_test.go @@ -59,6 +59,26 @@ func TestSecretForDockerRegistryGenerate(t *testing.T) { }, expectErr: false, }, + "test-valid-use-append-hash": { + params: map[string]interface{}{ + "name": "foo", + "docker-server": server, + "docker-username": username, + "docker-password": password, + "docker-email": email, + "append-hash": true, + }, + expected: &api.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-gb4kftc655", + }, + Data: map[string][]byte{ + api.DockerConfigKey: secretData, + }, + Type: api.SecretTypeDockercfg, + }, + expectErr: false, + }, "test-valid-use-no-email": { params: map[string]interface{}{ "name": "foo", diff --git a/pkg/kubectl/secret_for_tls.go b/pkg/kubectl/secret_for_tls.go index 466bce6ed0..9dd47b9514 100644 --- a/pkg/kubectl/secret_for_tls.go +++ b/pkg/kubectl/secret_for_tls.go @@ -23,6 +23,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/kubectl/util/hash" ) // SecretForTLSGeneratorV1 supports stable generation of a TLS secret. @@ -33,6 +34,8 @@ type SecretForTLSGeneratorV1 struct { Key string // Cert is the path to the user's public key certificate. Cert string + // AppendHash; if true, derive a hash from the Secret and append it to the name + AppendHash bool } // Ensure it supports the generator pattern that uses parameter injection @@ -47,6 +50,16 @@ func (s SecretForTLSGeneratorV1) Generate(genericParams map[string]interface{}) if err != nil { return nil, err } + delegate := &SecretForTLSGeneratorV1{} + hashParam, found := genericParams["append-hash"] + if found { + hashBool, isBool := hashParam.(bool) + if !isBool { + return nil, fmt.Errorf("expected bool, found :%v", hashParam) + } + delegate.AppendHash = hashBool + delete(genericParams, "append-hash") + } params := map[string]string{} for key, value := range genericParams { strVal, isString := value.(string) @@ -55,11 +68,9 @@ func (s SecretForTLSGeneratorV1) Generate(genericParams map[string]interface{}) } params[key] = strVal } - delegate := &SecretForTLSGeneratorV1{ - Name: params["name"], - Key: params["key"], - Cert: params["cert"], - } + delegate.Name = params["name"] + delegate.Key = params["key"] + delegate.Cert = params["cert"] return delegate.StructuredGenerate() } @@ -82,6 +93,13 @@ func (s SecretForTLSGeneratorV1) StructuredGenerate() (runtime.Object, error) { secret.Data = map[string][]byte{} secret.Data[api.TLSCertKey] = []byte(tlsCrt) secret.Data[api.TLSPrivateKeyKey] = []byte(tlsKey) + if s.AppendHash { + h, err := hash.SecretHash(secret) + if err != nil { + return nil, err + } + secret.Name = fmt.Sprintf("%s-%s", secret.Name, h) + } return secret, nil } @@ -100,6 +118,7 @@ func (s SecretForTLSGeneratorV1) ParamNames() []GeneratorParam { {"name", true}, {"key", true}, {"cert", true}, + {"append-hash", false}, } } diff --git a/pkg/kubectl/secret_for_tls_test.go b/pkg/kubectl/secret_for_tls_test.go index be7a82d241..252f84913e 100644 --- a/pkg/kubectl/secret_for_tls_test.go +++ b/pkg/kubectl/secret_for_tls_test.go @@ -145,6 +145,25 @@ func TestSecretForTLSGenerate(t *testing.T) { }, expectErr: false, }, + "test-valid-tls-secret-append-hash": { + params: map[string]interface{}{ + "name": "foo", + "key": validKeyPath, + "cert": validCertPath, + "append-hash": true, + }, + expected: &api.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-272h6tt825", + }, + Data: map[string][]byte{ + api.TLSCertKey: []byte(rsaCertPEM), + api.TLSPrivateKeyKey: []byte(rsaKeyPEM), + }, + Type: api.SecretTypeTLS, + }, + expectErr: false, + }, "test-invalid-key-pair": { params: map[string]interface{}{ "name": "foo", diff --git a/pkg/kubectl/secret_test.go b/pkg/kubectl/secret_test.go index e7c1e7478b..a9777cd1ba 100644 --- a/pkg/kubectl/secret_test.go +++ b/pkg/kubectl/secret_test.go @@ -44,6 +44,19 @@ func TestSecretGenerate(t *testing.T) { }, expectErr: false, }, + { + params: map[string]interface{}{ + "name": "foo", + "append-hash": true, + }, + expected: &api.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-949tdgdkgg", + }, + Data: map[string][]byte{}, + }, + expectErr: false, + }, { params: map[string]interface{}{ "name": "foo", @@ -58,6 +71,21 @@ func TestSecretGenerate(t *testing.T) { }, expectErr: false, }, + { + params: map[string]interface{}{ + "name": "foo", + "type": "my-type", + "append-hash": true, + }, + expected: &api.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-dg474f9t76", + }, + Data: map[string][]byte{}, + Type: "my-type", + }, + expectErr: false, + }, { params: map[string]interface{}{ "name": "foo", @@ -74,6 +102,23 @@ func TestSecretGenerate(t *testing.T) { }, expectErr: false, }, + { + params: map[string]interface{}{ + "name": "foo", + "from-literal": []string{"key1=value1", "key2=value2"}, + "append-hash": true, + }, + expected: &api.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-tf72c228m4", + }, + Data: map[string][]byte{ + "key1": []byte("value1"), + "key2": []byte("value2"), + }, + }, + expectErr: false, + }, { params: map[string]interface{}{ "name": "foo", @@ -110,6 +155,22 @@ func TestSecretGenerate(t *testing.T) { }, expectErr: false, }, + { + params: map[string]interface{}{ + "name": "foo", + "from-literal": []string{"key1==value1"}, + "append-hash": true, + }, + expected: &api.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-fdcc8tkhh5", + }, + Data: map[string][]byte{ + "key1": []byte("=value1"), + }, + }, + expectErr: false, + }, { setup: setupEnvFile("key1=value1", "#", "", "key2=value2"), params: map[string]interface{}{ @@ -127,6 +188,24 @@ 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", + "append-hash": true, + }, + expected: &api.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "valid_env-bkb2m2965h", + }, + 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") @@ -148,6 +227,28 @@ func TestSecretGenerate(t *testing.T) { }, 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", + "append-hash": true, + }, + expected: &api.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "getenv-m7kg2khdb4", + }, + Data: map[string][]byte{ + "g_key1": []byte("1"), + "g_key2": []byte(""), + }, + }, + expectErr: false, + }, { params: map[string]interface{}{ "name": "too_many_args", @@ -180,9 +281,26 @@ func TestSecretGenerate(t *testing.T) { }, expectErr: false, }, + { + setup: setupEnvFile(" key1= value1"), + params: map[string]interface{}{ + "name": "with_spaces", + "from-env-file": "file.env", + "append-hash": true, + }, + expected: &api.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "with_spaces-4488d5b57d", + }, + Data: map[string][]byte{ + "key1": []byte(" value1"), + }, + }, + expectErr: false, + }, } generator := SecretGeneratorV1{} - for _, test := range tests { + for i, test := range tests { if test.setup != nil { if teardown := test.setup(t, test.params); teardown != nil { defer teardown() @@ -190,13 +308,14 @@ func TestSecretGenerate(t *testing.T) { } obj, err := generator.Generate(test.params) if !test.expectErr && err != nil { - t.Errorf("unexpected error: %v", err) + t.Errorf("case %d, unexpected error: %v", i, err) + continue } if test.expectErr && err != nil { continue } if !reflect.DeepEqual(obj.(*api.Secret), test.expected) { - t.Errorf("\nexpected:\n%#v\nsaw:\n%#v", test.expected, obj.(*api.Secret)) + t.Errorf("\ncase %d, expected:\n%#v\nsaw:\n%#v", i, test.expected, obj.(*api.Secret)) } } } diff --git a/pkg/kubectl/util/BUILD b/pkg/kubectl/util/BUILD index 185eb74a82..17fdddedf1 100644 --- a/pkg/kubectl/util/BUILD +++ b/pkg/kubectl/util/BUILD @@ -34,6 +34,7 @@ filegroup( srcs = [ ":package-srcs", "//pkg/kubectl/util/crlf:all-srcs", + "//pkg/kubectl/util/hash:all-srcs", "//pkg/kubectl/util/i18n:all-srcs", "//pkg/kubectl/util/logs:all-srcs", "//pkg/kubectl/util/slice:all-srcs", diff --git a/pkg/kubectl/util/hash/BUILD b/pkg/kubectl/util/hash/BUILD new file mode 100644 index 0000000000..30f5802f70 --- /dev/null +++ b/pkg/kubectl/util/hash/BUILD @@ -0,0 +1,37 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", + "go_test", +) + +go_test( + name = "go_default_test", + srcs = ["hash_test.go"], + library = ":go_default_library", + tags = ["automanaged"], + deps = ["//pkg/api:go_default_library"], +) + +go_library( + name = "go_default_library", + srcs = ["hash.go"], + tags = ["automanaged"], + deps = ["//pkg/api:go_default_library"], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) diff --git a/pkg/kubectl/util/hash/hash.go b/pkg/kubectl/util/hash/hash.go new file mode 100644 index 0000000000..137e180454 --- /dev/null +++ b/pkg/kubectl/util/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/kubernetes/pkg/api" +) + +// ConfigMapHash returns a hash of the ConfigMap. +// The Data, Kind, and Name are taken into account. +func ConfigMapHash(cm *api.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 *api.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 *api.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 *api.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/kubectl/util/hash/hash_test.go b/pkg/kubectl/util/hash/hash_test.go new file mode 100644 index 0000000000..ee344a3542 --- /dev/null +++ b/pkg/kubectl/util/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/kubernetes/pkg/api" +) + +func TestConfigMapHash(t *testing.T) { + cases := []struct { + desc string + cm *api.ConfigMap + hash string + err string + }{ + // empty map + {"empty data", &api.ConfigMap{Data: map[string]string{}}, "42745tchd9", ""}, + // one key + {"one key", &api.ConfigMap{Data: map[string]string{"one": ""}}, "9g67k2htb6", ""}, + // three keys (tests sorting order) + {"three keys", &api.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 *api.Secret + hash string + err string + }{ + // empty map + {"empty data", &api.Secret{Type: "my-type", Data: map[string][]byte{}}, "t75bgf6ctb", ""}, + // one key + {"one key", &api.Secret{Type: "my-type", Data: map[string][]byte{"one": []byte("")}}, "74bd68bm66", ""}, + // three keys (tests sorting order) + {"three keys", &api.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 *api.ConfigMap + expect string + err string + }{ + // empty map + {"empty data", &api.ConfigMap{Data: map[string]string{}}, `{"data":{},"kind":"ConfigMap","name":""}`, ""}, + // one key + {"one key", &api.ConfigMap{Data: map[string]string{"one": ""}}, `{"data":{"one":""},"kind":"ConfigMap","name":""}`, ""}, + // three keys (tests sorting order) + {"three keys", &api.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 *api.Secret + expect string + err string + }{ + // empty map + {"empty data", &api.Secret{Type: "my-type", Data: map[string][]byte{}}, `{"data":{},"kind":"Secret","name":"","type":"my-type"}`, ""}, + // one key + {"one key", &api.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", &api.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", api.ConfigMap{}, 3}, + {"Secret", api.Secret{}, 4}, + } + 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 +}