diff --git a/docs/labels.md b/docs/labels.md index d7bd40a3ce7..5d6a987423c 100644 --- a/docs/labels.md +++ b/docs/labels.md @@ -32,10 +32,10 @@ These are just examples; you are free to develop your own conventions. ## Syntax and character set -As already mentioned, well formed _labels_ are key value pairs. Valid label keys have two segments - prefix and name - separated by a slash (`/`). The name segment is required and must be a DNS label: 63 characters or less, all lowercase, beginning and ending with an alphanumeric character (`[a-z0-9A-Z]`), with dashes (`-`) and alphanumerics between. The prefix and slash are optional. If specified, the prefix must be a DNS subdomain (a series of DNS labels separated by dots (`.`), not longer than 253 characters in total. -If the prefix is omitted, the label key is presumed to be private to the user. System components which use labels must specify a prefix. The `kubernetes.io` prefix is reserved for kubernetes core components. +_Labels_ are key value pairs. Valid label keys have two segments: an optional prefix and name, separated by a slash (`/`). The name segment is required and must be 63 characters or less, beginning and ending with an alphanumeric character (`[a-z0-9A-Z]`) with dashes (`-`), underscores (`_`), dots (`.`), and alphanumerics between. The prefix is optional. If specified, the prefix must be a DNS subdomain: a series of DNS labels separated by dots (`.`), not longer than 253 characters in total, followed by a slash (`/`). +If the prefix is omitted, the label key is presumed to be private to the user. System components which use labels must specify a prefix. The `kubernetes.io/` prefix is reserved for kubernetes core components. -Valid label values must be shorter than 64 characters, accepted characters are (`[-A-Za-z0-9_.]`) but the first character must be (`[A-Za-z0-9]`). +Valid label values must be 63 characters or less and must be empty or begin and end with an alphanumeric character (`[a-z0-9A-Z]`) with dashes (`-`), underscores (`_`), dots (`.`), and alphanumerics between. ## Label selectors diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 8cd15a2bffd..29d052abd3c 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -32,18 +32,18 @@ import ( "github.com/golang/glog" ) -const cIdentifierErrorMsg string = "must match regex " + util.CIdentifierFmt -const isNegativeErrorMsg string = "value must not be negative" +const cIdentifierErrorMsg string = `must be a C identifier (matching regex ` + util.CIdentifierFmt + `): e.g. "my_name" or "MyName"` +const isNegativeErrorMsg string = `must be non-negative` func intervalErrorMsg(lo, hi int) string { - return fmt.Sprintf("must be greater than %d and less than %d", lo, hi) + return fmt.Sprintf(`must be greater than %d and less than %d`, lo, hi) } -var labelValueErrorMsg string = fmt.Sprintf("must have at most %d characters and match regex %s", util.LabelValueMaxLength, util.LabelValueFmt) -var qualifiedNameErrorMsg string = fmt.Sprintf("must have at most %d characters and match regex %s", util.QualifiedNameMaxLength, util.QualifiedNameFmt) -var dnsSubdomainErrorMsg string = fmt.Sprintf("must have at most %d characters and match regex %s", util.DNS1123SubdomainMaxLength, util.DNS1123SubdomainFmt) -var dns1123LabelErrorMsg string = fmt.Sprintf("must have at most %d characters and match regex %s", util.DNS1123LabelMaxLength, util.DNS1123LabelFmt) -var dns952LabelErrorMsg string = fmt.Sprintf("must have at most %d characters and match regex %s", util.DNS952LabelMaxLength, util.DNS952LabelFmt) +var labelValueErrorMsg string = fmt.Sprintf(`must have at most %d characters, matching regex %s: e.g. "MyValue" or ""`, util.LabelValueMaxLength, util.LabelValueFmt) +var qualifiedNameErrorMsg string = fmt.Sprintf(`must be a qualified name (at most %d characters, matching regex %s), with an optional DNS subdomain prefix (at most %d characters, matching regex %s) and slash (/): e.g. "MyName" or "example.com/MyName"`, util.QualifiedNameMaxLength, util.QualifiedNameFmt, util.DNS1123SubdomainMaxLength, util.DNS1123SubdomainFmt) +var dnsSubdomainErrorMsg string = fmt.Sprintf(`must be a DNS subdomain (at most %d characters, matching regex %s): e.g. "example.com"`, util.DNS1123SubdomainMaxLength, util.DNS1123SubdomainFmt) +var dns1123LabelErrorMsg string = fmt.Sprintf(`must be a DNS label (at most %d characters, matching regex %s): e.g. "my-name"`, util.DNS1123LabelMaxLength, util.DNS1123LabelFmt) +var dns952LabelErrorMsg string = fmt.Sprintf(`must be a DNS 952 label (at most %d characters, matching regex %s): e.g. "my-name"`, util.DNS952LabelMaxLength, util.DNS952LabelFmt) var pdPartitionErrorMsg string = intervalErrorMsg(0, 255) var portRangeErrorMsg string = intervalErrorMsg(0, 65536) diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index 2d61d86ea0e..528659a501b 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -544,7 +544,7 @@ func TestValidateEnv(t *testing.T) { { name: "name not a C identifier", envs: []api.EnvVar{{Name: "a.b.c"}}, - expectedError: "[0].name: invalid value 'a.b.c': must match regex [A-Za-z_][A-Za-z0-9_]*", + expectedError: `[0].name: invalid value 'a.b.c': must be a C identifier (matching regex [A-Za-z_][A-Za-z0-9_]*): e.g. "my_name" or "MyName"`, }, { name: "value and valueFrom specified", @@ -2413,10 +2413,6 @@ func TestValidateServiceUpdate(t *testing.T) { } func TestValidateResourceNames(t *testing.T) { - longString := "a" - for i := 0; i < 6; i++ { - longString += longString - } table := []struct { input string success bool @@ -2432,7 +2428,8 @@ func TestValidateResourceNames(t *testing.T) { {"my.favorite.app.co/_12345", false}, {"my.favorite.app.co/12345_", false}, {"kubernetes.io/..", false}, - {"kubernetes.io/" + longString, true}, + {"kubernetes.io/" + strings.Repeat("a", 63), true}, + {"kubernetes.io/" + strings.Repeat("a", 64), false}, {"kubernetes.io//", false}, {"kubernetes.io", false}, {"kubernetes.io/will/not/work/", false}, diff --git a/pkg/util/validation.go b/pkg/util/validation.go index 8104ed41f0b..7cd66227663 100644 --- a/pkg/util/validation.go +++ b/pkg/util/validation.go @@ -19,13 +19,35 @@ package util import ( "net" "regexp" + "strings" ) const qnameCharFmt string = "[A-Za-z0-9]" const qnameExtCharFmt string = "[-A-Za-z0-9_.]" -const qnameTokenFmt string = "(" + qnameCharFmt + qnameExtCharFmt + "*)?" + qnameCharFmt +const QualifiedNameFmt string = "(" + qnameCharFmt + qnameExtCharFmt + "*)?" + qnameCharFmt +const QualifiedNameMaxLength int = 63 -const LabelValueFmt string = "(" + qnameTokenFmt + ")?" +var qualifiedNameRegexp = regexp.MustCompile("^" + QualifiedNameFmt + "$") + +func IsQualifiedName(value string) bool { + parts := strings.Split(value, "/") + var left, right string + switch len(parts) { + case 1: + left, right = "", parts[0] + case 2: + left, right = parts[0], parts[1] + default: + return false + } + + if left != "" && !IsDNS1123Subdomain(left) { + return false + } + return right != "" && len(right) <= QualifiedNameMaxLength && qualifiedNameRegexp.MatchString(right) +} + +const LabelValueFmt string = "(" + QualifiedNameFmt + ")?" const LabelValueMaxLength int = 63 var labelValueRegexp = regexp.MustCompile("^" + LabelValueFmt + "$") @@ -34,15 +56,6 @@ func IsValidLabelValue(value string) bool { return (len(value) <= LabelValueMaxLength && labelValueRegexp.MatchString(value)) } -const QualifiedNameFmt string = "(" + qnameTokenFmt + "/)?" + qnameTokenFmt -const QualifiedNameMaxLength int = 253 - -var qualifiedNameRegexp = regexp.MustCompile("^" + QualifiedNameFmt + "$") - -func IsQualifiedName(value string) bool { - return (len(value) <= QualifiedNameMaxLength && qualifiedNameRegexp.MatchString(value)) -} - const DNS1123LabelFmt string = "[a-z0-9]([-a-z0-9]*[a-z0-9])?" const DNS1123LabelMaxLength int = 63 diff --git a/pkg/util/validation_test.go b/pkg/util/validation_test.go index 6651f398711..c5ad31cedf3 100644 --- a/pkg/util/validation_test.go +++ b/pkg/util/validation_test.go @@ -168,24 +168,30 @@ func TestIsQualifiedName(t *testing.T) { "1-num.2-num/3-num", "1234/5678", "1.2.3.4/5678", - "UppercaseIsOK123", + "Uppercase_Is_OK_123", + "example.com/Uppercase_Is_OK_123", + strings.Repeat("a", 63), + strings.Repeat("a", 253) + "/" + strings.Repeat("b", 63), } for i := range successCases { if !IsQualifiedName(successCases[i]) { - t.Errorf("case[%d] expected success", i) + t.Errorf("case[%d]: %q: expected success", i, successCases[i]) } } errorCases := []string{ "nospecialchars%^=@", "cantendwithadash-", + "-cantstartwithadash-", "only/one/slash", - strings.Repeat("a", 254), - "-cantstartwithadash", + "Example.com/abc", + "example_com/abc", + strings.Repeat("a", 64), + strings.Repeat("a", 254) + "/abc", } for i := range errorCases { if IsQualifiedName(errorCases[i]) { - t.Errorf("case[%d] expected failure", i) + t.Errorf("case[%d]: %q: expected failure", i, errorCases[i]) } } }