Validate key subscript for metadata.annotations and metadata.labels separately

This commit is contained in:
Yang Guo 2017-11-22 12:38:09 -08:00
parent 34a7b3dea8
commit c74b97b29d
8 changed files with 153 additions and 71 deletions

View File

@ -22,13 +22,25 @@ import (
"k8s.io/kubernetes/pkg/fieldpath" "k8s.io/kubernetes/pkg/fieldpath"
) )
// ConvertDownwardAPIFieldLabel converts the specified downward API field label
// and its value in the pod of the specified version to the internal version,
// and returns the converted label and value. This function returns an error if
// the conversion fails.
func ConvertDownwardAPIFieldLabel(version, label, value string) (string, string, error) { func ConvertDownwardAPIFieldLabel(version, label, value string) (string, string, error) {
if version != "v1" { if version != "v1" {
return "", "", fmt.Errorf("unsupported pod version: %s", version) return "", "", fmt.Errorf("unsupported pod version: %s", version)
} }
path, _ := fieldpath.SplitMaybeSubscriptedPath(label) if path, _, ok := fieldpath.SplitMaybeSubscriptedPath(label); ok {
switch path { switch path {
case "metadata.annotations", "metadata.labels":
return label, value, nil
default:
return "", "", fmt.Errorf("field label does not support subscript: %s", label)
}
}
switch label {
case "metadata.annotations", case "metadata.annotations",
"metadata.labels", "metadata.labels",
"metadata.name", "metadata.name",

View File

@ -16,7 +16,6 @@ go_library(
"//pkg/apis/core:go_default_library", "//pkg/apis/core:go_default_library",
"//pkg/apis/extensions:go_default_library", "//pkg/apis/extensions:go_default_library",
"//pkg/features:go_default_library", "//pkg/features:go_default_library",
"//pkg/fieldpath:go_default_library",
"//pkg/util/parsers:go_default_library", "//pkg/util/parsers:go_default_library",
"//pkg/util/pointer:go_default_library", "//pkg/util/pointer:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library",

View File

@ -28,7 +28,6 @@ import (
"k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/kubernetes/pkg/apis/core" "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/fieldpath"
) )
// This is a "fast-path" that avoids reflection for common types. It focuses on the objects that are // This is a "fast-path" that avoids reflection for common types. It focuses on the objects that are
@ -157,8 +156,7 @@ func addConversionFuncs(scheme *runtime.Scheme) error {
// Add field conversion funcs. // Add field conversion funcs.
err = scheme.AddFieldLabelConversionFunc("v1", "Pod", err = scheme.AddFieldLabelConversionFunc("v1", "Pod",
func(label, value string) (string, string, error) { func(label, value string) (string, string, error) {
path, _ := fieldpath.SplitMaybeSubscriptedPath(label) switch label {
switch path {
case "metadata.name", case "metadata.name",
"metadata.namespace", "metadata.namespace",
"spec.nodeName", "spec.nodeName",

View File

@ -965,9 +965,7 @@ var validVolumeDownwardAPIFieldPathExpressions = sets.NewString(
"metadata.name", "metadata.name",
"metadata.namespace", "metadata.namespace",
"metadata.labels", "metadata.labels",
"metadata.labels[]", // represents "metadata.labels" with an arbitary subscript
"metadata.annotations", "metadata.annotations",
"metadata.annotations[]", // represents "metadata.annotations" with an arbitary subscript
"metadata.uid") "metadata.uid")
func validateDownwardAPIVolumeFile(file *core.DownwardAPIVolumeFile, fldPath *field.Path) field.ErrorList { func validateDownwardAPIVolumeFile(file *core.DownwardAPIVolumeFile, fldPath *field.Path) field.ErrorList {
@ -1902,8 +1900,6 @@ func ValidateEnv(vars []core.EnvVar, fldPath *field.Path) field.ErrorList {
} }
var validEnvDownwardAPIFieldPathExpressions = sets.NewString( var validEnvDownwardAPIFieldPathExpressions = sets.NewString(
"metadata.annotations[]", // represents "metadata.annotations" with an arbitary subscript
"metadata.labels[]", // represents "metadata.labels" with an arbitary subscript
"metadata.name", "metadata.name",
"metadata.namespace", "metadata.namespace",
"metadata.uid", "metadata.uid",
@ -1970,20 +1966,22 @@ func validateObjectFieldSelector(fs *core.ObjectFieldSelector, expressions *sets
return allErrs return allErrs
} }
path, subscript := fieldpath.SplitMaybeSubscriptedPath(internalFieldPath) if path, subscript, ok := fieldpath.SplitMaybeSubscriptedPath(internalFieldPath); ok {
if len(subscript) > 0 { switch path {
// This is to indicate that the internalFieldPath has a subscript, so case "metadata.annotations":
// that we can compare the path against the allowed path set easily. for _, msg := range validation.IsQualifiedName(strings.ToLower(subscript)) {
path += "[]" allErrs = append(allErrs, field.Invalid(fldPath, subscript, msg))
} }
if !expressions.Has(path) { case "metadata.labels":
allErrs = append(allErrs, field.NotSupported(fldPath.Child("fieldPath"), path, expressions.List()))
return allErrs
}
if len(subscript) > 0 {
for _, msg := range validation.IsQualifiedName(subscript) { for _, msg := range validation.IsQualifiedName(subscript) {
allErrs = append(allErrs, field.Invalid(fldPath, subscript, msg)) allErrs = append(allErrs, field.Invalid(fldPath, subscript, msg))
} }
default:
allErrs = append(allErrs, field.Invalid(fldPath, path, "does not support subscript"))
}
} else if !expressions.Has(path) {
allErrs = append(allErrs, field.NotSupported(fldPath.Child("fieldPath"), path, expressions.List()))
return allErrs
} }
return allErrs return allErrs

View File

@ -2635,6 +2635,13 @@ func TestValidateVolumes(t *testing.T) {
FieldPath: "metadata.labels['key']", FieldPath: "metadata.labels['key']",
}, },
}, },
{
Path: "labels with complex subscript",
FieldRef: &core.ObjectFieldSelector{
APIVersion: "v1",
FieldPath: "metadata.labels['test.example.com/key']",
},
},
{ {
Path: "annotations", Path: "annotations",
FieldRef: &core.ObjectFieldSelector{ FieldRef: &core.ObjectFieldSelector{
@ -2649,6 +2656,13 @@ func TestValidateVolumes(t *testing.T) {
FieldPath: "metadata.annotations['key']", FieldPath: "metadata.annotations['key']",
}, },
}, },
{
Path: "annotations with complex subscript",
FieldRef: &core.ObjectFieldSelector{
APIVersion: "v1",
FieldPath: "metadata.annotations['TEST.EXAMPLE.COM/key']",
},
},
{ {
Path: "namespace", Path: "namespace",
FieldRef: &core.ObjectFieldSelector{ FieldRef: &core.ObjectFieldSelector{
@ -4137,7 +4151,7 @@ func TestValidateEnv(t *testing.T) {
}, },
}, },
}}, }},
expectedError: `[0].valueFrom.fieldRef.fieldPath: Unsupported value: "metadata.name[]": supported values: "metadata.annotations[]", "metadata.labels[]", "metadata.name", "metadata.namespace", "metadata.uid", "spec.nodeName", "spec.serviceAccountName", "status.hostIP", "status.podIP"`, expectedError: `[0].valueFrom.fieldRef.fieldPath: Invalid value: "metadata.name['key']": error converting fieldPath: field label does not support subscript`,
}, },
{ {
name: "metadata.labels without subscript", name: "metadata.labels without subscript",
@ -4150,7 +4164,7 @@ func TestValidateEnv(t *testing.T) {
}, },
}, },
}}, }},
expectedError: `[0].valueFrom.fieldRef.fieldPath: Unsupported value: "metadata.labels": supported values: "metadata.annotations[]", "metadata.labels[]", "metadata.name", "metadata.namespace", "metadata.uid", "spec.nodeName", "spec.serviceAccountName", "status.hostIP", "status.podIP"`, expectedError: `[0].valueFrom.fieldRef.fieldPath: Unsupported value: "metadata.labels": supported values: "metadata.name", "metadata.namespace", "metadata.uid", "spec.nodeName", "spec.serviceAccountName", "status.hostIP", "status.podIP"`,
}, },
{ {
name: "metadata.annotations without subscript", name: "metadata.annotations without subscript",
@ -4163,7 +4177,33 @@ func TestValidateEnv(t *testing.T) {
}, },
}, },
}}, }},
expectedError: `[0].valueFrom.fieldRef.fieldPath: Unsupported value: "metadata.annotations": supported values: "metadata.annotations[]", "metadata.labels[]", "metadata.name", "metadata.namespace", "metadata.uid", "spec.nodeName", "spec.serviceAccountName", "status.hostIP", "status.podIP"`, expectedError: `[0].valueFrom.fieldRef.fieldPath: Unsupported value: "metadata.annotations": supported values: "metadata.name", "metadata.namespace", "metadata.uid", "spec.nodeName", "spec.serviceAccountName", "status.hostIP", "status.podIP"`,
},
{
name: "metadata.annotations with invalid key",
envs: []core.EnvVar{{
Name: "abc",
ValueFrom: &core.EnvVarSource{
FieldRef: &core.ObjectFieldSelector{
FieldPath: "metadata.annotations['invalid~key']",
APIVersion: "v1",
},
},
}},
expectedError: `field[0].valueFrom.fieldRef: Invalid value: "invalid~key"`,
},
{
name: "metadata.labels with invalid key",
envs: []core.EnvVar{{
Name: "abc",
ValueFrom: &core.EnvVarSource{
FieldRef: &core.ObjectFieldSelector{
FieldPath: "metadata.labels['Www.k8s.io/test']",
APIVersion: "v1",
},
},
}},
expectedError: `field[0].valueFrom.fieldRef: Invalid value: "Www.k8s.io/test"`,
}, },
{ {
name: "unsupported fieldPath", name: "unsupported fieldPath",
@ -4176,7 +4216,7 @@ func TestValidateEnv(t *testing.T) {
}, },
}, },
}}, }},
expectedError: `valueFrom.fieldRef.fieldPath: Unsupported value: "status.phase": supported values: "metadata.annotations[]", "metadata.labels[]", "metadata.name", "metadata.namespace", "metadata.uid", "spec.nodeName", "spec.serviceAccountName", "status.hostIP", "status.podIP"`, expectedError: `valueFrom.fieldRef.fieldPath: Unsupported value: "status.phase": supported values: "metadata.name", "metadata.namespace", "metadata.uid", "spec.nodeName", "spec.serviceAccountName", "status.hostIP", "status.podIP"`,
}, },
} }
for _, tc := range errorCases { for _, tc := range errorCases {

View File

@ -13,7 +13,10 @@ go_library(
"fieldpath.go", "fieldpath.go",
], ],
importpath = "k8s.io/kubernetes/pkg/fieldpath", importpath = "k8s.io/kubernetes/pkg/fieldpath",
deps = ["//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library"], deps = [
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/validation:go_default_library",
],
) )
go_test( go_test(

View File

@ -21,6 +21,7 @@ import (
"strings" "strings"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/util/validation"
) )
// FormatMap formats map[string]string to a string. // FormatMap formats map[string]string to a string.
@ -42,20 +43,24 @@ func ExtractFieldPathAsString(obj interface{}, fieldPath string) (string, error)
return "", nil return "", nil
} }
path, subscript := SplitMaybeSubscriptedPath(fieldPath) if path, subscript, ok := SplitMaybeSubscriptedPath(fieldPath); ok {
if len(subscript) > 0 {
switch path { switch path {
case "metadata.annotations": case "metadata.annotations":
if errs := validation.IsQualifiedName(strings.ToLower(subscript)); len(errs) != 0 {
return "", fmt.Errorf("invalid key subscript in %s: %s", fieldPath, strings.Join(errs, ";"))
}
return accessor.GetAnnotations()[subscript], nil return accessor.GetAnnotations()[subscript], nil
case "metadata.labels": case "metadata.labels":
if errs := validation.IsQualifiedName(subscript); len(errs) != 0 {
return "", fmt.Errorf("invalid key subscript in %s: %s", fieldPath, strings.Join(errs, ";"))
}
return accessor.GetLabels()[subscript], nil return accessor.GetLabels()[subscript], nil
default: default:
return "", fmt.Errorf("fieldPath %q does not support subscript", fieldPath) return "", fmt.Errorf("fieldPath %q does not support subscript", fieldPath)
} }
} }
switch path { switch fieldPath {
case "metadata.annotations": case "metadata.annotations":
return FormatMap(accessor.GetAnnotations()), nil return FormatMap(accessor.GetAnnotations()), nil
case "metadata.labels": case "metadata.labels":
@ -74,25 +79,25 @@ func ExtractFieldPathAsString(obj interface{}, fieldPath string) (string, error)
// SplitMaybeSubscriptedPath checks whether the specified fieldPath is // SplitMaybeSubscriptedPath checks whether the specified fieldPath is
// subscripted, and // subscripted, and
// - if yes, this function splits the fieldPath into path and subscript, and // - if yes, this function splits the fieldPath into path and subscript, and
// returns (path, subscript). // returns (path, subscript, true).
// - if no, this function returns (fieldPath, ""). // - if no, this function returns (fieldPath, "", false).
// //
// Example inputs and outputs: // Example inputs and outputs:
// - "metadata.annotations['myKey']" --> ("metadata.annotations", "myKey") // - "metadata.annotations['myKey']" --> ("metadata.annotations", "myKey", true)
// - "metadata.annotations['a[b]c']" --> ("metadata.annotations", "a[b]c") // - "metadata.annotations['a[b]c']" --> ("metadata.annotations", "a[b]c", true)
// - "metadata.labels" --> ("metadata.labels", "") // - "metadata.labels['']" --> ("metadata.labels", "", true)
// - "metadata.labels['']" --> ("metadata.labels", "") // - "metadata.labels" --> ("metadata.labels", "", false)
func SplitMaybeSubscriptedPath(fieldPath string) (string, string) { func SplitMaybeSubscriptedPath(fieldPath string) (string, string, bool) {
if !strings.HasSuffix(fieldPath, "']") { if !strings.HasSuffix(fieldPath, "']") {
return fieldPath, "" return fieldPath, "", false
} }
s := strings.TrimSuffix(fieldPath, "']") s := strings.TrimSuffix(fieldPath, "']")
parts := strings.SplitN(s, "['", 2) parts := strings.SplitN(s, "['", 2)
if len(parts) < 2 { if len(parts) < 2 {
return fieldPath, "" return fieldPath, "", false
} }
if len(parts[0]) == 0 || len(parts[1]) == 0 { if len(parts[0]) == 0 {
return fieldPath, "" return fieldPath, "", false
} }
return parts[0], parts[1] return parts[0], parts[1], true
} }

View File

@ -36,7 +36,6 @@ func TestExtractFieldPathAsString(t *testing.T) {
name: "not an API object", name: "not an API object",
fieldPath: "metadata.name", fieldPath: "metadata.name",
obj: "", obj: "",
expectedMessageFragment: "expected struct",
}, },
{ {
name: "ok - namespace", name: "ok - namespace",
@ -98,7 +97,16 @@ func TestExtractFieldPathAsString(t *testing.T) {
}, },
expectedValue: "1", expectedValue: "1",
}, },
{
name: "ok - annotation",
fieldPath: "metadata.annotations['Www.k8s.io/test']",
obj: &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{"Www.k8s.io/test": "1"},
},
},
expectedValue: "1",
},
{ {
name: "invalid expression", name: "invalid expression",
fieldPath: "metadata.whoops", fieldPath: "metadata.whoops",
@ -110,14 +118,24 @@ func TestExtractFieldPathAsString(t *testing.T) {
expectedMessageFragment: "unsupported fieldPath", expectedMessageFragment: "unsupported fieldPath",
}, },
{ {
name: "invalid annotation", name: "invalid annotation key",
fieldPath: "metadata.annotations['unknown.key']", fieldPath: "metadata.annotations['invalid~key']",
obj: &v1.Pod{ obj: &v1.Pod{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{"foo": "bar"}, Annotations: map[string]string{"foo": "bar"},
}, },
}, },
expectedMessageFragment: "unsupported fieldPath", expectedMessageFragment: "invalid key subscript in metadata.annotations",
},
{
name: "invalid label key",
fieldPath: "metadata.labels['Www.k8s.io/test']",
obj: &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{"foo": "bar"},
},
},
expectedMessageFragment: "invalid key subscript in metadata.labels",
}, },
} }
@ -131,6 +149,8 @@ func TestExtractFieldPathAsString(t *testing.T) {
} else { } else {
t.Errorf("%v: unexpected error: %v", tc.name, err) t.Errorf("%v: unexpected error: %v", tc.name, err)
} }
} else if tc.expectedMessageFragment != "" {
t.Errorf("%v: expected error: %v", tc.name, tc.expectedMessageFragment)
} else if e := tc.expectedValue; e != "" && e != actual { } else if e := tc.expectedValue; e != "" && e != actual {
t.Errorf("%v: unexpected result; got %q, expected %q", tc.name, actual, e) t.Errorf("%v: unexpected result; got %q, expected %q", tc.name, actual, e)
} }
@ -142,72 +162,79 @@ func TestSplitMaybeSubscriptedPath(t *testing.T) {
fieldPath string fieldPath string
expectedPath string expectedPath string
expectedSubscript string expectedSubscript string
expectedOK bool
}{ }{
{ {
fieldPath: "metadata.annotations['key']", fieldPath: "metadata.annotations['key']",
expectedPath: "metadata.annotations", expectedPath: "metadata.annotations",
expectedSubscript: "key", expectedSubscript: "key",
expectedOK: true,
}, },
{ {
fieldPath: "metadata.annotations['a[b']c']", fieldPath: "metadata.annotations['a[b']c']",
expectedPath: "metadata.annotations", expectedPath: "metadata.annotations",
expectedSubscript: "a[b']c", expectedSubscript: "a[b']c",
expectedOK: true,
}, },
{ {
fieldPath: "metadata.labels['['key']", fieldPath: "metadata.labels['['key']",
expectedPath: "metadata.labels", expectedPath: "metadata.labels",
expectedSubscript: "['key", expectedSubscript: "['key",
expectedOK: true,
}, },
{ {
fieldPath: "metadata.labels['key']']", fieldPath: "metadata.labels['key']']",
expectedPath: "metadata.labels", expectedPath: "metadata.labels",
expectedSubscript: "key']", expectedSubscript: "key']",
}, expectedOK: true,
{
fieldPath: "metadata.labels[ 'key' ]",
expectedPath: "metadata.labels[ 'key' ]",
expectedSubscript: "",
}, },
{ {
fieldPath: "metadata.labels['']", fieldPath: "metadata.labels['']",
expectedPath: "metadata.labels['']", expectedPath: "metadata.labels",
expectedSubscript: "", expectedSubscript: "",
expectedOK: true,
}, },
{ {
fieldPath: "metadata.labels[' ']", fieldPath: "metadata.labels[' ']",
expectedPath: "metadata.labels", expectedPath: "metadata.labels",
expectedSubscript: " ", expectedSubscript: " ",
expectedOK: true,
},
{
fieldPath: "metadata.labels[ 'key' ]",
expectedOK: false,
}, },
{ {
fieldPath: "metadata.labels[]", fieldPath: "metadata.labels[]",
expectedPath: "metadata.labels[]", expectedOK: false,
expectedSubscript: "",
}, },
{ {
fieldPath: "metadata.labels[']", fieldPath: "metadata.labels[']",
expectedPath: "metadata.labels[']", expectedOK: false,
expectedSubscript: "",
}, },
{ {
fieldPath: "metadata.labels['key']foo", fieldPath: "metadata.labels['key']foo",
expectedPath: "metadata.labels['key']foo", expectedOK: false,
expectedSubscript: "",
}, },
{ {
fieldPath: "['key']", fieldPath: "['key']",
expectedPath: "['key']", expectedOK: false,
expectedSubscript: "",
}, },
{ {
fieldPath: "metadata.labels", fieldPath: "metadata.labels",
expectedPath: "metadata.labels", expectedOK: false,
expectedSubscript: "",
}, },
} }
for _, tc := range cases { for _, tc := range cases {
path, subscript := SplitMaybeSubscriptedPath(tc.fieldPath) path, subscript, ok := SplitMaybeSubscriptedPath(tc.fieldPath)
if !ok {
if tc.expectedOK {
t.Errorf("SplitMaybeSubscriptedPath(%q) expected to return (_, _, true)", tc.fieldPath)
}
continue
}
if path != tc.expectedPath || subscript != tc.expectedSubscript { if path != tc.expectedPath || subscript != tc.expectedSubscript {
t.Errorf("SplitMaybeSubscriptedPath(%q) = (%q, %q), expect (%q, %q)", t.Errorf("SplitMaybeSubscriptedPath(%q) = (%q, %q, true), expect (%q, %q, true)",
tc.fieldPath, path, subscript, tc.expectedPath, tc.expectedSubscript) tc.fieldPath, path, subscript, tc.expectedPath, tc.expectedSubscript)
} }
} }