Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validation cleanup part 7 #18694

Merged
merged 3 commits into from
Dec 19, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 30 additions & 0 deletions docs/devel/api-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ using resources with kubectl can be found in [Working with resources](../user-gu
- [Naming conventions](#naming-conventions)
- [Label, selector, and annotation conventions](#label-selector-and-annotation-conventions)
- [WebSockets and SPDY](#websockets-and-spdy)
- [Validation](#validation)

<!-- END MUNGE: GENERATED_TOC -->

Expand Down Expand Up @@ -787,6 +788,35 @@ There are two primary protocols in use today:
Clients should use the SPDY protocols if their clients have native support, or WebSockets as a fallback. Note that WebSockets is susceptible to Head-of-Line blocking and so clients must read and process each message sequentionally. In the future, an HTTP/2 implementation will be exposed that deprecates SPDY.


## Validation

API objects are validated upon receipt by the apiserver. Validation errors are
flagged and returned to the caller in a `Failure` status with `reason` set to
`Invalid`. In order to facilitate consistent error messages, we ask that
validation logic adheres to the following guidelines whenever possible (though
exceptional cases will exist).

* Be as precise as possible.
* Telling users what they CAN do is more useful than telling them what they
CANNOT do.
* When asserting a requirement in the positive, use "must". Examples: "must be
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A formatting requirement? It should be consistent with the next point.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I specifically split the negative case into formatting and behavioral, but in the positive sense there is no difference in words.

greater than 0", "must match regex '[a-z]+'". Words like "should" imply that
the assertion is optional, and must be avoided.
* When asserting a formatting requirement in the negative, use "must not".
Example: "must not contain '..'". Words like "should not" imply that the
assertion is optional, and must be avoided.
* When asserting a behavioral requirement in the negative, use "may not".
Examples: "may not be specified when otherField is empty", "only `name` may be
specified".
* When referencing a literal string value, indicate the literal in
single-quotes. Example: "must not contain '..'".
* When referencing another field name, indicate the name in back-quotes.
Example: "must be greater than `request`".
* When specifying inequalities, use words rather than symbols. Examples: "must
be less than 256", "must be greater than or equal to 0". Do not use words
like "larger than", "bigger than", "more than", "higher than", etc.
* When specifying numeric ranges, use inclusive ranges when possible.

<!-- BEGIN MUNGE: GENERATED_ANALYTICS -->
[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/devel/api-conventions.md?pixel)]()
<!-- END MUNGE: GENERATED_ANALYTICS -->
2 changes: 1 addition & 1 deletion hack/test-cmd.sh
Original file line number Diff line number Diff line change
Expand Up @@ -980,7 +980,7 @@ __EOF__
# Pre-condition: use --name flag
output_message=$(! kubectl expose -f hack/testdata/pod-with-large-name.yaml --name=invalid-large-service-name --port=8081 2>&1 "${kube_flags[@]}")
# Post-condition: should fail due to invalid name
kube::test::if_has_string "${output_message}" 'metadata.name: invalid value'
kube::test::if_has_string "${output_message}" 'metadata.name: Invalid value'
# Pre-condition: default run without --name flag; should succeed by truncating the inherited name
output_message=$(kubectl expose -f hack/testdata/pod-with-large-name.yaml --port=8081 2>&1 "${kube_flags[@]}")
# Post-condition: inherited name from pod has been truncated
Expand Down
2 changes: 1 addition & 1 deletion pkg/api/errors/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ func TestNewInvalid(t *testing.T) {
},
},
{
field.Required(field.NewPath("field[0].name")),
field.Required(field.NewPath("field[0].name"), ""),
&unversioned.StatusDetails{
Kind: "Kind",
Name: "name",
Expand Down
476 changes: 296 additions & 180 deletions pkg/api/validation/validation.go

Large diffs are not rendered by default.

60 changes: 30 additions & 30 deletions pkg/api/validation/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,15 @@ func TestValidateObjectMetaCustomName(t *testing.T) {
func TestValidateObjectMetaNamespaces(t *testing.T) {
errs := ValidateObjectMeta(
&api.ObjectMeta{Name: "test", Namespace: "foo.bar"},
false,
true,
func(s string, prefix bool) (bool, string) {
return true, ""
},
field.NewPath("field"))
if len(errs) != 1 {
t.Fatalf("unexpected errors: %v", errs)
}
if !strings.Contains(errs[0].Error(), "invalid value 'foo.bar'") {
if !strings.Contains(errs[0].Error(), `Invalid value: "foo.bar"`) {
t.Errorf("unexpected error message: %v", errs)
}
maxLength := 63
Expand All @@ -84,15 +84,15 @@ func TestValidateObjectMetaNamespaces(t *testing.T) {
}
errs = ValidateObjectMeta(
&api.ObjectMeta{Name: "test", Namespace: string(b)},
false,
true,
func(s string, prefix bool) (bool, string) {
return true, ""
},
field.NewPath("field"))
if len(errs) != 1 {
t.Fatalf("unexpected errors: %v", errs)
}
if !strings.Contains(errs[0].Error(), "invalid value") {
if !strings.Contains(errs[0].Error(), "Invalid value") {
t.Errorf("unexpected error message: %v", errs)
}
}
Expand Down Expand Up @@ -629,23 +629,23 @@ func TestValidateVolumes(t *testing.T) {
},
"absolute path": {
[]api.Volume{{Name: "absolutepath", VolumeSource: absolutePathName}},
field.ErrorTypeForbidden,
field.ErrorTypeInvalid,
"downwardAPI.path", "",
},
"dot dot path": {
[]api.Volume{{Name: "dotdotpath", VolumeSource: dotDotInPath}},
field.ErrorTypeInvalid,
"downwardAPI.path", `must not contain ".."`,
"downwardAPI.path", `must not contain '..'`,
},
"dot dot file name": {
[]api.Volume{{Name: "dotdotfilename", VolumeSource: dotDotPathName}},
field.ErrorTypeInvalid,
"downwardAPI.path", `must not start with ".."`,
"downwardAPI.path", `must not start with '..'`,
},
"dot dot first level dirent": {
[]api.Volume{{Name: "dotdotdirfilename", VolumeSource: dotDotFirstLevelDirent}},
field.ErrorTypeInvalid,
"downwardAPI.path", `must not start with ".."`,
"downwardAPI.path", `must not start with '..'`,
},
"empty wwn": {
[]api.Volume{{Name: "badimage", VolumeSource: zeroWWN}},
Expand All @@ -665,16 +665,16 @@ func TestValidateVolumes(t *testing.T) {
"starts with '..'": {
[]api.Volume{{Name: "badprefix", VolumeSource: startsWithDots}},
field.ErrorTypeInvalid,
"gitRepo.directory", `must not start with ".."`,
"gitRepo.directory", `must not start with '..'`,
},
"contains '..'": {
[]api.Volume{{Name: "containsdots", VolumeSource: containsDots}},
field.ErrorTypeInvalid,
"gitRepo.directory", `must not contain ".."`,
"gitRepo.directory", `must not contain '..'`,
},
"absolute target": {
[]api.Volume{{Name: "absolutetarget", VolumeSource: absPath}},
field.ErrorTypeForbidden,
field.ErrorTypeInvalid,
"gitRepo.directory", "",
},
}
Expand Down Expand Up @@ -824,12 +824,12 @@ func TestValidateEnv(t *testing.T) {
{
name: "zero-length name",
envs: []api.EnvVar{{Name: ""}},
expectedError: "[0].name: required value",
expectedError: "[0].name: Required value",
},
{
name: "name not a C identifier",
envs: []api.EnvVar{{Name: "a.b.c"}},
expectedError: `[0].name: invalid value 'a.b.c', Details: must be a C identifier (matching regex [A-Za-z_][A-Za-z0-9_]*): e.g. "my_name" or "MyName"`,
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",
Expand All @@ -843,7 +843,7 @@ func TestValidateEnv(t *testing.T) {
},
},
}},
expectedError: "[0].valueFrom: invalid value '', Details: sources cannot be specified when value is not empty",
expectedError: "[0].valueFrom: Invalid value: \"\": may not be specified when `value` is not empty",
},
{
name: "missing FieldPath on ObjectFieldSelector",
Expand All @@ -855,7 +855,7 @@ func TestValidateEnv(t *testing.T) {
},
},
}},
expectedError: "[0].valueFrom.fieldRef.fieldPath: required value",
expectedError: `[0].valueFrom.fieldRef.fieldPath: Required value`,
},
{
name: "missing APIVersion on ObjectFieldSelector",
Expand All @@ -867,7 +867,7 @@ func TestValidateEnv(t *testing.T) {
},
},
}},
expectedError: "[0].valueFrom.fieldRef.apiVersion: required value",
expectedError: `[0].valueFrom.fieldRef.apiVersion: Required value`,
},
{
name: "invalid fieldPath",
Expand All @@ -880,7 +880,7 @@ func TestValidateEnv(t *testing.T) {
},
},
}},
expectedError: "[0].valueFrom.fieldRef.fieldPath: invalid value 'metadata.whoops', Details: error converting fieldPath",
expectedError: `[0].valueFrom.fieldRef.fieldPath: Invalid value: "metadata.whoops": error converting fieldPath`,
},
{
name: "invalid fieldPath labels",
Expand All @@ -893,7 +893,7 @@ func TestValidateEnv(t *testing.T) {
},
},
}},
expectedError: "[0].valueFrom.fieldRef.fieldPath: unsupported value 'metadata.labels', Details: supported values: metadata.name, metadata.namespace, status.podIP",
expectedError: `[0].valueFrom.fieldRef.fieldPath: Unsupported value: "metadata.labels": supported values: metadata.name, metadata.namespace, status.podIP`,
},
{
name: "invalid fieldPath annotations",
Expand All @@ -906,7 +906,7 @@ func TestValidateEnv(t *testing.T) {
},
},
}},
expectedError: "[0].valueFrom.fieldRef.fieldPath: unsupported value 'metadata.annotations', Details: supported values: metadata.name, metadata.namespace, status.podIP",
expectedError: `[0].valueFrom.fieldRef.fieldPath: Unsupported value: "metadata.annotations": supported values: metadata.name, metadata.namespace, status.podIP`,
},
{
name: "unsupported fieldPath",
Expand All @@ -919,7 +919,7 @@ func TestValidateEnv(t *testing.T) {
},
},
}},
expectedError: "valueFrom.fieldRef.fieldPath: unsupported value 'status.phase', Details: supported values: metadata.name, metadata.namespace, status.podIP",
expectedError: `valueFrom.fieldRef.fieldPath: Unsupported value: "status.phase": supported values: metadata.name, metadata.namespace, status.podIP`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The multiple colons in a single sentence still makes me frown. Is this following some tradition?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's pretty go-style - add details: at the front.

On Tue, Dec 15, 2015 at 11:42 PM, Chao Xu notifications@github.com wrote:

In pkg/api/validation/validation_test.go
#18694 (comment)
:

@@ -919,7 +919,7 @@ func TestValidateEnv(t *testing.T) {
},
},
}},

  •       expectedError: "valueFrom.fieldRef.fieldPath: unsupported value 'status.phase', Details: supported values: metadata.name, metadata.namespace, status.podIP",
    
  •       expectedError: `valueFrom.fieldRef.fieldPath: Unsupported value: "status.phase": supported values: metadata.name, metadata.namespace, status.podIP`,
    

The multiple colons in a single sentence still makes me frown. Is this
following some tradition?


Reply to this email directly or view it on GitHub
https://github.com/kubernetes/kubernetes/pull/18694/files#r47744682.

},
}
for _, tc := range errorCases {
Expand Down Expand Up @@ -3314,7 +3314,7 @@ func TestValidateLimitRange(t *testing.T) {
},
},
}},
"not supported when limit type is Pod",
"may not be specified when `type` is 'Pod'",
},
"default-request-limit-type-pod": {
api.LimitRange{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: api.LimitRangeSpec{
Expand All @@ -3327,7 +3327,7 @@ func TestValidateLimitRange(t *testing.T) {
},
},
}},
"not supported when limit type is Pod",
"may not be specified when `type` is 'Pod'",
},
"min value 100m is greater than max value 10m": {
api.LimitRange{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: api.LimitRangeSpec{
Expand Down Expand Up @@ -4002,7 +4002,7 @@ func TestValidateEndpoints(t *testing.T) {
},
},
errorType: "FieldValueInvalid",
errorDetail: "invalid IPv4 address",
errorDetail: "must be a valid IPv4 address",
},
"Multiple ports, one without name": {
endpoints: api.Endpoints{
Expand Down Expand Up @@ -4052,7 +4052,7 @@ func TestValidateEndpoints(t *testing.T) {
},
},
errorType: "FieldValueInvalid",
errorDetail: "invalid IPv4 address",
errorDetail: "must be a valid IPv4 address",
},
"Port missing number": {
endpoints: api.Endpoints{
Expand Down Expand Up @@ -4122,7 +4122,7 @@ func TestValidateEndpoints(t *testing.T) {

for k, v := range errorCases {
if errs := ValidateEndpoints(&v.endpoints); len(errs) == 0 || errs[0].Type != v.errorType || !strings.Contains(errs[0].Detail, v.errorDetail) {
t.Errorf("Expected error type %s with detail %s for %s, got %v", v.errorType, v.errorDetail, k, errs)
t.Errorf("[%s] Expected error type %s with detail %q, got %v", k, v.errorType, v.errorDetail, errs)
}
}
}
Expand Down Expand Up @@ -4172,7 +4172,7 @@ func TestValidateSecurityContext(t *testing.T) {
}
for k, v := range successCases {
if errs := ValidateSecurityContext(v.sc, field.NewPath("field")); len(errs) != 0 {
t.Errorf("Expected success for %s, got %v", k, errs)
t.Errorf("[%s] Expected success, got %v", k, errs)
}
}

Expand All @@ -4192,17 +4192,17 @@ func TestValidateSecurityContext(t *testing.T) {
"request privileged when capabilities forbids": {
sc: privRequestWithGlobalDeny,
errorType: "FieldValueForbidden",
errorDetail: "",
errorDetail: "disallowed by policy",
},
"negative RunAsUser": {
sc: negativeRunAsUser,
errorType: "FieldValueInvalid",
errorDetail: "runAsUser cannot be negative",
errorDetail: isNegativeErrorMsg,
},
}
for k, v := range errorCases {
if errs := ValidateSecurityContext(v.sc, field.NewPath("field")); len(errs) == 0 || errs[0].Type != v.errorType || errs[0].Detail != v.errorDetail {
t.Errorf("Expected error type %s with detail %s for %s, got %v", v.errorType, v.errorDetail, k, errs)
if errs := ValidateSecurityContext(v.sc, field.NewPath("field")); len(errs) == 0 || errs[0].Type != v.errorType || !strings.Contains(errs[0].Detail, v.errorDetail) {
t.Errorf("[%s] Expected error type %q with detail %q, got %v", k, v.errorType, v.errorDetail, errs)
}
}
}
Expand Down