diff --git a/pkg/api/types.go b/pkg/api/types.go index 19e0f8a31cd..c6d6a39f3b1 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -1827,7 +1827,8 @@ type Secret struct { TypeMeta `json:",inline"` ObjectMeta `json:"metadata,omitempty"` - // Data contains the secret data. Each key must be a valid DNS_SUBDOMAIN. + // Data contains the secret data. Each key must be a valid DNS_SUBDOMAIN + // or leading dot followed by valid DNS_SUBDOMAIN. // The serialized form of the secret data is a base64 encoded string, // representing the arbitrary (possibly non-string) data value here. Data map[string][]byte `json:"data,omitempty"` @@ -1860,6 +1861,15 @@ const ( ServiceAccountTokenKey = "token" // ServiceAccountKubeconfigKey is the key of the optional kubeconfig data for SecretTypeServiceAccountToken secrets ServiceAccountKubeconfigKey = "kubernetes.kubeconfig" + + // SecretTypeDockercfg contains a dockercfg file that follows the same format rules as ~/.dockercfg + // + // Required fields: + // - Secret.Data[".dockercfg"] - a serialized ~/.dockercfg file + SecretTypeDockercfg SecretType = "kubernetes.io/dockercfg" + + // DockerConfigKey is the key of the required data for SecretTypeDockercfg secrets + DockerConfigKey = ".dockercfg" ) type SecretList struct { diff --git a/pkg/api/v1/types.go b/pkg/api/v1/types.go index 21980ab935d..162c34291eb 100644 --- a/pkg/api/v1/types.go +++ b/pkg/api/v1/types.go @@ -1721,10 +1721,11 @@ type Secret struct { TypeMeta `json:",inline"` ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see http://docs.k8s.io/api-conventions.md#metadata"` - // Data contains the secret data. Each key must be a valid DNS_SUBDOMAIN. + // Data contains the secret data. Each key must be a valid DNS_SUBDOMAIN + // or leading dot followed by valid DNS_SUBDOMAIN. // The serialized form of the secret data is a base64 encoded string, // representing the arbitrary (possibly non-string) data value here. - Data map[string][]byte `json:"data,omitempty" description:"data contains the secret data. Each key must be a valid DNS_SUBDOMAIN. Each value must be a base64 encoded string"` + Data map[string][]byte `json:"data,omitempty" description:"data contains the secret data. Each key must be a valid DNS_SUBDOMAIN or leading dot followed by valid DNS_SUBDOMAIN. Each value must be a base64 encoded string as described in https://tools.ietf.org/html/rfc4648#section-4"` // Used to facilitate programmatic handling of secret data. Type SecretType `json:"type,omitempty" description:"type facilitates programmatic handling of secret data"` @@ -1752,6 +1753,17 @@ const ( ServiceAccountUIDKey = "kubernetes.io/service-account.uid" // ServiceAccountTokenKey is the key of the required data for SecretTypeServiceAccountToken secrets ServiceAccountTokenKey = "token" + // ServiceAccountKubeconfigKey is the key of the optional kubeconfig data for SecretTypeServiceAccountToken secrets + ServiceAccountKubeconfigKey = "kubernetes.kubeconfig" + + // SecretTypeDockercfg contains a dockercfg file that follows the same format rules as ~/.dockercfg + // + // Required fields: + // - Secret.Data[".dockercfg"] - a serialized ~/.dockercfg file + SecretTypeDockercfg SecretType = "kubernetes.io/dockercfg" + + // DockerConfigKey is the key of the required data for SecretTypeDockercfg secrets + DockerConfigKey = ".dockercfg" ) type SecretList struct { diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index e833f7c933f..b7807e84d1a 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -1625,10 +1625,11 @@ type NFSVolumeSource struct { type Secret struct { TypeMeta `json:",inline"` - // Data contains the secret data. Each key must be a valid DNS_SUBDOMAIN. + // Data contains the secret data. Each key must be a valid DNS_SUBDOMAIN + // or leading dot followed by valid DNS_SUBDOMAIN. // The serialized form of the secret data is a base64 encoded string, // representing the arbitrary (possibly non-string) data value here. - Data map[string][]byte `json:"data,omitempty" description:"data contains the secret data. Each key must be a valid DNS_SUBDOMAIN. Each value must be a base64 encoded string"` + Data map[string][]byte `json:"data,omitempty" description:"data contains the secret data. Each key must be a valid DNS_SUBDOMAIN or leading dot followed by valid DNS_SUBDOMAIN. Each value must be a base64 encoded string as described in https://tools.ietf.org/html/rfc4648#section-4"` // Used to facilitate programmatic handling of secret data. Type SecretType `json:"type,omitempty" description:"type facilitates programmatic handling of secret data"` @@ -1656,6 +1657,17 @@ const ( ServiceAccountUIDKey = "kubernetes.io/service-account.uid" // ServiceAccountTokenKey is the key of the required data for SecretTypeServiceAccountToken secrets ServiceAccountTokenKey = "token" + // ServiceAccountKubeconfigKey is the key of the optional kubeconfig data for SecretTypeServiceAccountToken secrets + ServiceAccountKubeconfigKey = "kubernetes.kubeconfig" + + // SecretTypeDockercfg contains a dockercfg file that follows the same format rules as ~/.dockercfg + // + // Required fields: + // - Secret.Data[".dockercfg"] - a serialized ~/.dockercfg file + SecretTypeDockercfg SecretType = "kubernetes.io/dockercfg" + + // DockerConfigKey is the key of the required data for SecretTypeDockercfg secrets + DockerConfigKey = ".dockercfg" ) type SecretList struct { diff --git a/pkg/api/v1beta2/types.go b/pkg/api/v1beta2/types.go index 9fb2bf319dc..1e6caa11338 100644 --- a/pkg/api/v1beta2/types.go +++ b/pkg/api/v1beta2/types.go @@ -1702,10 +1702,11 @@ type NFSVolumeSource struct { type Secret struct { TypeMeta `json:",inline"` - // Data contains the secret data. Each key must be a valid DNS_SUBDOMAIN. + // Data contains the secret data. Each key must be a valid DNS_SUBDOMAIN + // or leading dot followed by valid DNS_SUBDOMAIN. // The serialized form of the secret data is a base64 encoded string, // representing the arbitrary (possibly non-string) data value here. - Data map[string][]byte `json:"data,omitempty" description:"data contains the secret data. Each key must be a valid DNS_SUBDOMAIN. Each value must be a base64 encoded string"` + Data map[string][]byte `json:"data,omitempty" description:"data contains the secret data. Each key must be a valid DNS_SUBDOMAIN or leading dot followed by valid DNS_SUBDOMAIN. Each value must be a base64 encoded string as described in https://tools.ietf.org/html/rfc4648#section-4"` // Used to facilitate programmatic handling of secret data. Type SecretType `json:"type,omitempty" description:"type facilitates programmatic handling of secret data"` @@ -1733,6 +1734,17 @@ const ( ServiceAccountUIDKey = "kubernetes.io/service-account.uid" // ServiceAccountTokenKey is the key of the required data for SecretTypeServiceAccountToken secrets ServiceAccountTokenKey = "token" + // ServiceAccountKubeconfigKey is the key of the optional kubeconfig data for SecretTypeServiceAccountToken secrets + ServiceAccountKubeconfigKey = "kubernetes.kubeconfig" + + // SecretTypeDockercfg contains a dockercfg file that follows the same format rules as ~/.dockercfg + // + // Required fields: + // - Secret.Data[".dockercfg"] - a serialized ~/.dockercfg file + SecretTypeDockercfg SecretType = "kubernetes.io/dockercfg" + + // DockerConfigKey is the key of the required data for SecretTypeDockercfg secrets + DockerConfigKey = ".dockercfg" ) type SecretList struct { diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index 1f388f85c2a..53cd3ef9b8c 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -1721,10 +1721,11 @@ type Secret struct { TypeMeta `json:",inline"` ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see http://docs.k8s.io/api-conventions.md#metadata"` - // Data contains the secret data. Each key must be a valid DNS_SUBDOMAIN. + // Data contains the secret data. Each key must be a valid DNS_SUBDOMAIN + // or leading dot followed by valid DNS_SUBDOMAIN. // The serialized form of the secret data is a base64 encoded string, // representing the arbitrary (possibly non-string) data value here. - Data map[string][]byte `json:"data,omitempty" description:"data contains the secret data. Each key must be a valid DNS_SUBDOMAIN. Each value must be a base64 encoded string"` + Data map[string][]byte `json:"data,omitempty" description:"data contains the secret data. Each key must be a valid DNS_SUBDOMAIN or leading dot followed by valid DNS_SUBDOMAIN. Each value must be a base64 encoded string as described in https://tools.ietf.org/html/rfc4648#section-4"` // Used to facilitate programmatic handling of secret data. Type SecretType `json:"type,omitempty" description:"type facilitates programmatic handling of secret data"` @@ -1752,6 +1753,17 @@ const ( ServiceAccountUIDKey = "kubernetes.io/service-account.uid" // ServiceAccountTokenKey is the key of the required data for SecretTypeServiceAccountToken secrets ServiceAccountTokenKey = "token" + // ServiceAccountKubeconfigKey is the key of the optional kubeconfig data for SecretTypeServiceAccountToken secrets + ServiceAccountKubeconfigKey = "kubernetes.kubeconfig" + + // SecretTypeDockercfg contains a dockercfg file that follows the same format rules as ~/.dockercfg + // + // Required fields: + // - Secret.Data[".dockercfg"] - a serialized ~/.dockercfg file + SecretTypeDockercfg SecretType = "kubernetes.io/dockercfg" + + // DockerConfigKey is the key of the required data for SecretTypeDockercfg secrets + DockerConfigKey = ".dockercfg" ) type SecretList struct { diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index bab284dcfd3..2b519b309ea 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -17,9 +17,11 @@ limitations under the License. package validation import ( + "encoding/json" "fmt" "net" "path" + "regexp" "strings" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" @@ -1257,6 +1259,16 @@ func ValidateServiceAccountUpdate(oldServiceAccount, newServiceAccount *api.Serv return allErrs } +const SecretKeyFmt string = "\\.?" + util.DNS1123LabelFmt + "(\\." + util.DNS1123LabelFmt + ")*" + +var secretKeyRegexp = regexp.MustCompile("^" + SecretKeyFmt + "$") + +// IsSecretKey tests for a string that conforms to the definition of a +// subdomain in DNS (RFC 1123), except that a leading dot is allowed +func IsSecretKey(value string) bool { + return len(value) <= util.DNS1123SubdomainMaxLength && secretKeyRegexp.MatchString(value) +} + // ValidateSecret tests if required fields in the Secret are set. func ValidateSecret(secret *api.Secret) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} @@ -1264,8 +1276,8 @@ func ValidateSecret(secret *api.Secret) errs.ValidationErrorList { totalSize := 0 for key, value := range secret.Data { - if !util.IsDNS1123Subdomain(key) { - allErrs = append(allErrs, errs.NewFieldInvalid(fmt.Sprintf("data[%s]", key), key, dnsSubdomainErrorMsg)) + if !IsSecretKey(key) { + allErrs = append(allErrs, errs.NewFieldInvalid(fmt.Sprintf("data[%s]", key), key, fmt.Sprintf("must have at most %d characters and match regex %s", util.DNS1123SubdomainMaxLength, SecretKeyFmt))) } totalSize += len(value) @@ -1284,6 +1296,18 @@ func ValidateSecret(secret *api.Secret) errs.ValidationErrorList { } case api.SecretTypeOpaque, "": // no-op + case api.SecretTypeDockercfg: + dockercfgBytes, exists := secret.Data[api.DockerConfigKey] + if !exists { + allErrs = append(allErrs, errs.NewFieldRequired(fmt.Sprintf("data[%s]", api.DockerConfigKey))) + break + } + + // make sure that the content is well-formed json. + if err := json.Unmarshal(dockercfgBytes, &map[string]interface{}{}); err != nil { + allErrs = append(allErrs, errs.NewFieldInvalid(fmt.Sprintf("data[%s]", api.DockerConfigKey), "", err.Error())) + } + default: // no-op } diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index ef1ea40ae3c..bb780eee1a2 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -2869,12 +2869,15 @@ func TestValidateSecret(t *testing.T) { } var ( - emptyName = validSecret() - invalidName = validSecret() - emptyNs = validSecret() - invalidNs = validSecret() - overMaxSize = validSecret() - invalidKey = validSecret() + emptyName = validSecret() + invalidName = validSecret() + emptyNs = validSecret() + invalidNs = validSecret() + overMaxSize = validSecret() + invalidKey = validSecret() + leadingDotKey = validSecret() + dotKey = validSecret() + doubleDotKey = validSecret() ) emptyName.Name = "" @@ -2885,6 +2888,9 @@ func TestValidateSecret(t *testing.T) { "over": make([]byte, api.MaxSecretSize+1), } invalidKey.Data["a..b"] = []byte("whoops") + leadingDotKey.Data[".key"] = []byte("bar") + dotKey.Data["."] = []byte("bar") + doubleDotKey.Data[".."] = []byte("bar") // kubernetes.io/service-account-token secret validation validServiceAccountTokenSecret := func() api.Secret { @@ -2916,18 +2922,62 @@ func TestValidateSecret(t *testing.T) { secret api.Secret valid bool }{ - "valid": {validSecret(), true}, - "empty name": {emptyName, false}, - "invalid name": {invalidName, false}, - "empty namespace": {emptyNs, false}, - "invalid namespace": {invalidNs, false}, - "over max size": {overMaxSize, false}, - "invalid key": {invalidKey, false}, - + "valid": {validSecret(), true}, + "empty name": {emptyName, false}, + "invalid name": {invalidName, false}, + "empty namespace": {emptyNs, false}, + "invalid namespace": {invalidNs, false}, + "over max size": {overMaxSize, false}, + "invalid key": {invalidKey, false}, "valid service-account-token secret": {validServiceAccountTokenSecret(), true}, "empty service-account-token annotation": {emptyTokenAnnotation, false}, "missing service-account-token annotation": {missingTokenAnnotation, false}, "missing service-account-token annotations": {missingTokenAnnotations, false}, + "leading dot key": {leadingDotKey, true}, + "dot key": {dotKey, false}, + "double dot key": {doubleDotKey, false}, + } + + for name, tc := range tests { + errs := ValidateSecret(&tc.secret) + if tc.valid && len(errs) > 0 { + t.Errorf("%v: Unexpected error: %v", name, errs) + } + if !tc.valid && len(errs) == 0 { + t.Errorf("%v: Unexpected non-error", name) + } + } +} + +func TestValidateDockerConfigSecret(t *testing.T) { + validDockerSecret := func() api.Secret { + return api.Secret{ + ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "bar"}, + Type: api.SecretTypeDockercfg, + Data: map[string][]byte{ + api.DockerConfigKey: []byte(`{"https://index.docker.io/v1/": {"auth": "Y2x1ZWRyb29sZXIwMDAxOnBhc3N3b3Jk","email": "fake@example.com"}}`), + }, + } + } + + var ( + missingDockerConfigKey = validDockerSecret() + emptyDockerConfigKey = validDockerSecret() + invalidDockerConfigKey = validDockerSecret() + ) + + delete(missingDockerConfigKey.Data, api.DockerConfigKey) + emptyDockerConfigKey.Data[api.DockerConfigKey] = []byte("") + invalidDockerConfigKey.Data[api.DockerConfigKey] = []byte("bad") + + tests := map[string]struct { + secret api.Secret + valid bool + }{ + "valid": {validDockerSecret(), true}, + "missing dockercfg": {missingDockerConfigKey, false}, + "empty dockercfg": {emptyDockerConfigKey, false}, + "invalid dockercfg": {invalidDockerConfigKey, false}, } for name, tc := range tests { diff --git a/pkg/credentialprovider/config.go b/pkg/credentialprovider/config.go index aa74e28fc94..d9ffe4803f3 100644 --- a/pkg/credentialprovider/config.go +++ b/pkg/credentialprovider/config.go @@ -147,17 +147,17 @@ func readDockerConfigFileFromBytes(contents []byte) (cfg DockerConfig, err error return } -// dockerConfigEntryWithAuth is used solely for deserializing the Auth field +// DockerConfigEntryWithAuth is used solely for deserializing the Auth field // into a dockerConfigEntry during JSON deserialization. -type dockerConfigEntryWithAuth struct { - Username string - Password string - Email string - Auth string +type DockerConfigEntryWithAuth struct { + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Email string `json:"email,omitempty"` + Auth string `json:"auth,omitempty"` } func (ident *DockerConfigEntry) UnmarshalJSON(data []byte) error { - var tmp dockerConfigEntryWithAuth + var tmp DockerConfigEntryWithAuth err := json.Unmarshal(data, &tmp) if err != nil { return err @@ -194,3 +194,16 @@ func decodeDockerConfigFieldAuth(field string) (username, password string, err e return } + +func (ident DockerConfigEntry) ConvertToDockerConfigCompatible() DockerConfigEntryWithAuth { + ret := DockerConfigEntryWithAuth{ident.Username, ident.Password, ident.Email, ""} + ret.Auth = encodeDockerConfigFieldAuth(ident.Username, ident.Password) + + return ret +} + +func encodeDockerConfigFieldAuth(username, password string) string { + fieldValue := username + ":" + password + + return base64.StdEncoding.EncodeToString([]byte(fieldValue)) +} diff --git a/pkg/credentialprovider/config_test.go b/pkg/credentialprovider/config_test.go index 86532d271c8..4f52c7e4454 100644 --- a/pkg/credentialprovider/config_test.go +++ b/pkg/credentialprovider/config_test.go @@ -166,3 +166,33 @@ func TestDecodeDockerConfigFieldAuth(t *testing.T) { } } } + +func TestDockerConfigEntryJSONCompatibleEncode(t *testing.T) { + tests := []struct { + input DockerConfigEntry + expect []byte + }{ + // simple case, just decode the fields + { + expect: []byte(`{"username":"foo","password":"bar","email":"foo@example.com","auth":"Zm9vOmJhcg=="}`), + input: DockerConfigEntry{ + Username: "foo", + Password: "bar", + Email: "foo@example.com", + }, + }, + } + + for i, tt := range tests { + toEncode := tt.input.ConvertToDockerConfigCompatible() + + actual, err := json.Marshal(toEncode) + if err != nil { + t.Errorf("case %d: unexpected error: %v", i, err) + } + + if string(tt.expect) != string(actual) { + t.Errorf("case %d: expected %v, got %v", i, string(tt.expect), string(actual)) + } + } +} diff --git a/pkg/registry/secret/etcd/etcd_test.go b/pkg/registry/secret/etcd/etcd_test.go index 04574f43886..dc8933c268f 100644 --- a/pkg/registry/secret/etcd/etcd_test.go +++ b/pkg/registry/secret/etcd/etcd_test.go @@ -63,7 +63,7 @@ func TestCreate(t *testing.T) { }, &api.Secret{ ObjectMeta: api.ObjectMeta{Name: "name"}, - Data: map[string][]byte{".dotfile": []byte("")}, + Data: map[string][]byte{"~.dotfile": []byte("")}, }, ) }