Merge pull request #49961 from mtaufen/kubectl-hash

Automatic merge from submit-queue (batch tested with PRs 49961, 50005, 50738, 51045, 49927)

Add --append-hash flag to kubectl create configmap/secret

**What this PR does / why we need it**:
Specifying this new flag will automatically hash the configmap/secret
contents with sha256 and append the first 40 hex-encoded bits of the
hash to the name of the configmap/secret. This is especially useful for
workflows that generate configmaps/secrets from files (e.g.
--from-file).

See this Google doc for more background:
https://docs.google.com/document/d/1x1fJ3pGRx20ujR-Y89HUAw8glUL8-ygaztLkkmQeCdU/edit

**Release note**:
```release-note
Adds --append-hash flag to kubectl create configmap/secret, which will append a short hash of the configmap/secret contents to the name during creation.
```
pull/6/head
Kubernetes Submit Queue 2017-08-29 21:43:29 -07:00 committed by GitHub
commit 2cf5118abb
16 changed files with 727 additions and 35 deletions

View File

@ -133,6 +133,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",

View File

@ -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
}

View File

@ -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)

View File

@ -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)

View File

@ -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
}

View File

@ -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))
}
}
}

View File

@ -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
}

View File

@ -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},
}
}

View File

@ -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",

View File

@ -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},
}
}

View File

@ -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",

View File

@ -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))
}
}
}

View File

@ -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",

View File

@ -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"],
)

View File

@ -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)))
}

View File

@ -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
}