Expose single annotation/label via downward API

This commit is contained in:
Yang Guo 2017-02-17 17:37:15 +01:00
parent 754017bef4
commit 82c9eec164
9 changed files with 276 additions and 19 deletions

View File

@ -4,6 +4,7 @@
- k8s.io/apiserver/pkg/util/feature
- k8s.io/kubernetes/pkg/apis/core
- k8s.io/kubernetes/pkg/features
- k8s.io/kubernetes/pkg/fieldpath
- k8s.io/kubernetes/pkg/util
- k8s.io/api/core/v1

View File

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

View File

@ -28,6 +28,7 @@ import (
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/kubernetes/pkg/apis/core"
"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
@ -156,7 +157,8 @@ func addConversionFuncs(scheme *runtime.Scheme) error {
// Add field conversion funcs.
err = scheme.AddFieldLabelConversionFunc("v1", "Pod",
func(label, value string) (string, string, error) {
switch label {
path, _ := fieldpath.SplitMaybeSubscriptedPath(label)
switch path {
case "metadata.annotations",
"metadata.labels",
"metadata.name",
@ -170,7 +172,7 @@ func addConversionFuncs(scheme *runtime.Scheme) error {
"status.hostIP",
"status.podIP":
return label, value, nil
// This is for backwards compatibility with old v1 clients which send spec.host
// This is for backwards compatibility with old v1 clients which send spec.host
case "spec.host":
return "spec.nodeName", value, nil
default:

View File

@ -22,6 +22,7 @@ go_library(
"//pkg/apis/core/v1/helper:go_default_library",
"//pkg/capabilities:go_default_library",
"//pkg/features:go_default_library",
"//pkg/fieldpath:go_default_library",
"//pkg/security/apparmor:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",

View File

@ -50,6 +50,7 @@ import (
v1helper "k8s.io/kubernetes/pkg/apis/core/v1/helper"
"k8s.io/kubernetes/pkg/capabilities"
"k8s.io/kubernetes/pkg/features"
"k8s.io/kubernetes/pkg/fieldpath"
"k8s.io/kubernetes/pkg/security/apparmor"
)
@ -960,11 +961,13 @@ func validateFlockerVolumeSource(flocker *core.FlockerVolumeSource, fldPath *fie
return allErrs
}
var validDownwardAPIFieldPathExpressions = sets.NewString(
var validVolumeDownwardAPIFieldPathExpressions = sets.NewString(
"metadata.name",
"metadata.namespace",
"metadata.labels",
"metadata.labels[]", // represents "metadata.labels" with an arbitary subscript
"metadata.annotations",
"metadata.annotations[]", // represents "metadata.annotations" with an arbitary subscript
"metadata.uid")
func validateDownwardAPIVolumeFile(file *core.DownwardAPIVolumeFile, fldPath *field.Path) field.ErrorList {
@ -975,7 +978,7 @@ func validateDownwardAPIVolumeFile(file *core.DownwardAPIVolumeFile, fldPath *fi
}
allErrs = append(allErrs, validateLocalNonReservedPath(file.Path, fldPath.Child("path"))...)
if file.FieldRef != nil {
allErrs = append(allErrs, validateObjectFieldSelector(file.FieldRef, &validDownwardAPIFieldPathExpressions, fldPath.Child("fieldRef"))...)
allErrs = append(allErrs, validateObjectFieldSelector(file.FieldRef, &validVolumeDownwardAPIFieldPathExpressions, fldPath.Child("fieldRef"))...)
if file.ResourceFieldRef != nil {
allErrs = append(allErrs, field.Invalid(fldPath, "resource", "fieldRef and resourceFieldRef can not be specified simultaneously"))
}
@ -1898,7 +1901,16 @@ func ValidateEnv(vars []core.EnvVar, fldPath *field.Path) field.ErrorList {
return allErrs
}
var validFieldPathExpressionsEnv = sets.NewString("metadata.name", "metadata.namespace", "metadata.uid", "spec.nodeName", "spec.serviceAccountName", "status.hostIP", "status.podIP")
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.namespace",
"metadata.uid",
"spec.nodeName",
"spec.serviceAccountName",
"status.hostIP",
"status.podIP")
var validContainerResourceFieldPathExpressions = sets.NewString("limits.cpu", "limits.memory", "limits.ephemeral-storage", "requests.cpu", "requests.memory", "requests.ephemeral-storage")
func validateEnvVarValueFrom(ev core.EnvVar, fldPath *field.Path) field.ErrorList {
@ -1912,7 +1924,7 @@ func validateEnvVarValueFrom(ev core.EnvVar, fldPath *field.Path) field.ErrorLis
if ev.ValueFrom.FieldRef != nil {
numSources++
allErrs = append(allErrs, validateObjectFieldSelector(ev.ValueFrom.FieldRef, &validFieldPathExpressionsEnv, fldPath.Child("fieldRef"))...)
allErrs = append(allErrs, validateObjectFieldSelector(ev.ValueFrom.FieldRef, &validEnvDownwardAPIFieldPathExpressions, fldPath.Child("fieldRef"))...)
}
if ev.ValueFrom.ResourceFieldRef != nil {
numSources++
@ -1945,14 +1957,32 @@ func validateObjectFieldSelector(fs *core.ObjectFieldSelector, expressions *sets
if len(fs.APIVersion) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("apiVersion"), ""))
} else if len(fs.FieldPath) == 0 {
return allErrs
}
if len(fs.FieldPath) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("fieldPath"), ""))
} else {
internalFieldPath, _, err := legacyscheme.Scheme.ConvertFieldLabel(fs.APIVersion, "Pod", fs.FieldPath, "")
if err != nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("fieldPath"), fs.FieldPath, fmt.Sprintf("error converting fieldPath: %v", err)))
} else if !expressions.Has(internalFieldPath) {
allErrs = append(allErrs, field.NotSupported(fldPath.Child("fieldPath"), internalFieldPath, expressions.List()))
return allErrs
}
internalFieldPath, _, err := legacyscheme.Scheme.ConvertFieldLabel(fs.APIVersion, "Pod", fs.FieldPath, "")
if err != nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("fieldPath"), fs.FieldPath, fmt.Sprintf("error converting fieldPath: %v", err)))
return allErrs
}
path, subscript := fieldpath.SplitMaybeSubscriptedPath(internalFieldPath)
if len(subscript) > 0 {
// This is to indicate that the internalFieldPath has a subscript, so
// that we can compare the path against the allowed path set easily.
path += "[]"
}
if !expressions.Has(path) {
allErrs = append(allErrs, field.NotSupported(fldPath.Child("fieldPath"), path, expressions.List()))
return allErrs
}
if len(subscript) > 0 {
for _, msg := range validation.IsQualifiedName(subscript) {
allErrs = append(allErrs, field.Invalid(fldPath, subscript, msg))
}
}

View File

@ -2628,6 +2628,13 @@ func TestValidateVolumes(t *testing.T) {
FieldPath: "metadata.labels",
},
},
{
Path: "labels with subscript",
FieldRef: &core.ObjectFieldSelector{
APIVersion: "v1",
FieldPath: "metadata.labels['key']",
},
},
{
Path: "annotations",
FieldRef: &core.ObjectFieldSelector{
@ -2635,6 +2642,13 @@ func TestValidateVolumes(t *testing.T) {
FieldPath: "metadata.annotations",
},
},
{
Path: "annotations with subscript",
FieldRef: &core.ObjectFieldSelector{
APIVersion: "v1",
FieldPath: "metadata.annotations['key']",
},
},
{
Path: "namespace",
FieldRef: &core.ObjectFieldSelector{
@ -3824,6 +3838,24 @@ func TestValidateEnv(t *testing.T) {
{Name: "abc", Value: ""},
{Name: "a.b.c", Value: "value"},
{Name: "a-b-c", Value: "value"},
{
Name: "abc",
ValueFrom: &core.EnvVarSource{
FieldRef: &core.ObjectFieldSelector{
APIVersion: legacyscheme.Registry.GroupOrDie(core.GroupName).GroupVersion.String(),
FieldPath: "metadata.annotations['key']",
},
},
},
{
Name: "abc",
ValueFrom: &core.EnvVarSource{
FieldRef: &core.ObjectFieldSelector{
APIVersion: legacyscheme.Registry.GroupOrDie(core.GroupName).GroupVersion.String(),
FieldPath: "metadata.labels['key']",
},
},
},
{
Name: "abc",
ValueFrom: &core.EnvVarSource{
@ -4095,7 +4127,20 @@ func TestValidateEnv(t *testing.T) {
expectedError: `[0].valueFrom.fieldRef.fieldPath: Invalid value: "metadata.whoops": error converting fieldPath`,
},
{
name: "invalid fieldPath labels",
name: "metadata.name with subscript",
envs: []core.EnvVar{{
Name: "labels",
ValueFrom: &core.EnvVarSource{
FieldRef: &core.ObjectFieldSelector{
FieldPath: "metadata.name['key']",
APIVersion: "v1",
},
},
}},
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"`,
},
{
name: "metadata.labels without subscript",
envs: []core.EnvVar{{
Name: "labels",
ValueFrom: &core.EnvVarSource{
@ -4105,10 +4150,10 @@ func TestValidateEnv(t *testing.T) {
},
},
}},
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"`,
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"`,
},
{
name: "invalid fieldPath annotations",
name: "metadata.annotations without subscript",
envs: []core.EnvVar{{
Name: "abc",
ValueFrom: &core.EnvVarSource{
@ -4118,7 +4163,7 @@ func TestValidateEnv(t *testing.T) {
},
},
}},
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"`,
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"`,
},
{
name: "unsupported fieldPath",
@ -4131,7 +4176,7 @@ func TestValidateEnv(t *testing.T) {
},
},
}},
expectedError: `valueFrom.fieldRef.fieldPath: Unsupported value: "status.phase": supported values: "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.annotations[]", "metadata.labels[]", "metadata.name", "metadata.namespace", "metadata.uid", "spec.nodeName", "spec.serviceAccountName", "status.hostIP", "status.podIP"`,
},
}
for _, tc := range errorCases {

View File

@ -42,7 +42,20 @@ func ExtractFieldPathAsString(obj interface{}, fieldPath string) (string, error)
return "", nil
}
switch fieldPath {
path, subscript := SplitMaybeSubscriptedPath(fieldPath)
if len(subscript) > 0 {
switch path {
case "metadata.annotations":
return accessor.GetAnnotations()[subscript], nil
case "metadata.labels":
return accessor.GetLabels()[subscript], nil
default:
return "", fmt.Errorf("fieldPath %q does not support subscript", fieldPath)
}
}
switch path {
case "metadata.annotations":
return FormatMap(accessor.GetAnnotations()), nil
case "metadata.labels":
@ -57,3 +70,29 @@ func ExtractFieldPathAsString(obj interface{}, fieldPath string) (string, error)
return "", fmt.Errorf("unsupported fieldPath: %v", fieldPath)
}
// SplitMaybeSubscriptedPath checks whether the specified fieldPath is
// subscripted, and
// - if yes, this function splits the fieldPath into path and subscript, and
// returns (path, subscript).
// - if no, this function returns (fieldPath, "").
//
// Example inputs and outputs:
// - "metadata.annotations['myKey']" --> ("metadata.annotations", "myKey")
// - "metadata.annotations['a[b]c']" --> ("metadata.annotations", "a[b]c")
// - "metadata.labels" --> ("metadata.labels", "")
// - "metadata.labels['']" --> ("metadata.labels", "")
func SplitMaybeSubscriptedPath(fieldPath string) (string, string) {
if !strings.HasSuffix(fieldPath, "']") {
return fieldPath, ""
}
s := strings.TrimSuffix(fieldPath, "']")
parts := strings.SplitN(s, "['", 2)
if len(parts) < 2 {
return fieldPath, ""
}
if len(parts[0]) == 0 || len(parts[1]) == 0 {
return fieldPath, ""
}
return parts[0], parts[1]
}

View File

@ -88,6 +88,16 @@ func TestExtractFieldPathAsString(t *testing.T) {
},
expectedValue: "builder=\"john-doe\"",
},
{
name: "ok - annotation",
fieldPath: "metadata.annotations['spec.pod.beta.kubernetes.io/statefulset-index']",
obj: &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{"spec.pod.beta.kubernetes.io/statefulset-index": "1"},
},
},
expectedValue: "1",
},
{
name: "invalid expression",
@ -99,6 +109,16 @@ func TestExtractFieldPathAsString(t *testing.T) {
},
expectedMessageFragment: "unsupported fieldPath",
},
{
name: "invalid annotation",
fieldPath: "metadata.annotations['unknown.key']",
obj: &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{"foo": "bar"},
},
},
expectedMessageFragment: "unsupported fieldPath",
},
}
for _, tc := range cases {
@ -116,3 +136,79 @@ func TestExtractFieldPathAsString(t *testing.T) {
}
}
}
func TestSplitMaybeSubscriptedPath(t *testing.T) {
cases := []struct {
fieldPath string
expectedPath string
expectedSubscript string
}{
{
fieldPath: "metadata.annotations['key']",
expectedPath: "metadata.annotations",
expectedSubscript: "key",
},
{
fieldPath: "metadata.annotations['a[b']c']",
expectedPath: "metadata.annotations",
expectedSubscript: "a[b']c",
},
{
fieldPath: "metadata.labels['['key']",
expectedPath: "metadata.labels",
expectedSubscript: "['key",
},
{
fieldPath: "metadata.labels['key']']",
expectedPath: "metadata.labels",
expectedSubscript: "key']",
},
{
fieldPath: "metadata.labels[ 'key' ]",
expectedPath: "metadata.labels[ 'key' ]",
expectedSubscript: "",
},
{
fieldPath: "metadata.labels['']",
expectedPath: "metadata.labels['']",
expectedSubscript: "",
},
{
fieldPath: "metadata.labels[' ']",
expectedPath: "metadata.labels",
expectedSubscript: " ",
},
{
fieldPath: "metadata.labels[]",
expectedPath: "metadata.labels[]",
expectedSubscript: "",
},
{
fieldPath: "metadata.labels[']",
expectedPath: "metadata.labels[']",
expectedSubscript: "",
},
{
fieldPath: "metadata.labels['key']foo",
expectedPath: "metadata.labels['key']foo",
expectedSubscript: "",
},
{
fieldPath: "['key']",
expectedPath: "['key']",
expectedSubscript: "",
},
{
fieldPath: "metadata.labels",
expectedPath: "metadata.labels",
expectedSubscript: "",
},
}
for _, tc := range cases {
path, subscript := SplitMaybeSubscriptedPath(tc.fieldPath)
if path != tc.expectedPath || subscript != tc.expectedSubscript {
t.Errorf("SplitMaybeSubscriptedPath(%q) = (%q, %q), expect (%q, %q)",
tc.fieldPath, path, subscript, tc.expectedPath, tc.expectedSubscript)
}
}
}

View File

@ -544,6 +544,48 @@ func TestCollectDataWithDownwardAPI(t *testing.T) {
payload map[string]util.FileProjection
success bool
}{
{
name: "annotation",
volumeFile: []v1.DownwardAPIVolumeFile{
{Path: "annotation", FieldRef: &v1.ObjectFieldSelector{
FieldPath: "metadata.annotations['a1']"}}},
pod: &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: testPodName,
Namespace: testNamespace,
Annotations: map[string]string{
"a1": "value1",
"a2": "value2",
},
UID: testPodUID},
},
mode: 0644,
payload: map[string]util.FileProjection{
"annotation": {Data: []byte("value1"), Mode: 0644},
},
success: true,
},
{
name: "annotation-error",
volumeFile: []v1.DownwardAPIVolumeFile{
{Path: "annotation", FieldRef: &v1.ObjectFieldSelector{
FieldPath: "metadata.annotations['']"}}},
pod: &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: testPodName,
Namespace: testNamespace,
Annotations: map[string]string{
"a1": "value1",
"a2": "value2",
},
UID: testPodUID},
},
mode: 0644,
payload: map[string]util.FileProjection{
"annotation": {Data: []byte("does-not-matter-because-this-test-case-will-fail-anyway"), Mode: 0644},
},
success: false,
},
{
name: "labels",
volumeFile: []v1.DownwardAPIVolumeFile{