Merge pull request #55902 from yguo0905/annotations

Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

Expose single annotation/label via downward API

**What this PR does / why we need it**:

https://github.com/kubernetes/community/blob/master/contributors/design-proposals/node/annotations-downward-api.md

Support exposing single annotation via both env and volume downward API using the following syntax:

```
metadata.annotations['key']
metadata.labels['key']
```

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
Fixes #

#31218

**Special notes for your reviewer**:

This PR takes over the work in https://github.com/kubernetes/kubernetes/pull/41648.

**Release note**:

```
A single value in metadata.annotations/metadata.labels can be passed into the containers via Downward API
```

/assign @thockin @vishh
This commit is contained in:
Kubernetes Submit Queue 2017-11-22 18:54:29 -08:00 committed by GitHub
commit 83e46f0a9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 531 additions and 27 deletions

View File

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

View File

@ -51,6 +51,7 @@ filegroup(
"//pkg/apis/core/fuzzer:all-srcs", "//pkg/apis/core/fuzzer:all-srcs",
"//pkg/apis/core/helper:all-srcs", "//pkg/apis/core/helper:all-srcs",
"//pkg/apis/core/install:all-srcs", "//pkg/apis/core/install:all-srcs",
"//pkg/apis/core/pods:all-srcs",
"//pkg/apis/core/v1:all-srcs", "//pkg/apis/core/v1:all-srcs",
"//pkg/apis/core/validation:all-srcs", "//pkg/apis/core/validation:all-srcs",
], ],

30
pkg/apis/core/pods/BUILD Normal file
View File

@ -0,0 +1,30 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = ["helpers.go"],
importpath = "k8s.io/kubernetes/pkg/apis/core/pods",
visibility = ["//visibility:public"],
deps = ["//pkg/fieldpath:go_default_library"],
)
go_test(
name = "go_default_test",
srcs = ["helpers_test.go"],
importpath = "k8s.io/kubernetes/pkg/apis/core/pods",
library = ":go_default_library",
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,63 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package pods
import (
"fmt"
"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) {
if version != "v1" {
return "", "", fmt.Errorf("unsupported pod version: %s", version)
}
if path, _, ok := fieldpath.SplitMaybeSubscriptedPath(label); ok {
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",
"metadata.labels",
"metadata.name",
"metadata.namespace",
"metadata.uid",
"spec.nodeName",
"spec.restartPolicy",
"spec.serviceAccountName",
"spec.schedulerName",
"status.phase",
"status.hostIP",
"status.podIP":
return label, value, nil
// This is for backwards compatibility with old v1 clients which send spec.host
case "spec.host":
return "spec.nodeName", value, nil
default:
return "", "", fmt.Errorf("field label not supported: %s", label)
}
}

View File

@ -0,0 +1,87 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package pods
import (
"testing"
)
func TestConvertDownwardAPIFieldLabel(t *testing.T) {
testCases := []struct {
version string
label string
value string
expectedErr bool
expectedLabel string
expectedValue string
}{
{
version: "v2",
label: "metadata.name",
value: "test-pod",
expectedErr: true,
},
{
version: "v1",
label: "invalid-label",
value: "value",
expectedErr: true,
},
{
version: "v1",
label: "metadata.name",
value: "test-pod",
expectedLabel: "metadata.name",
expectedValue: "test-pod",
},
{
version: "v1",
label: "metadata.annotations",
value: "myValue",
expectedLabel: "metadata.annotations",
expectedValue: "myValue",
},
{
version: "v1",
label: "metadata.annotations['myKey']",
value: "myValue",
expectedLabel: "metadata.annotations['myKey']",
expectedValue: "myValue",
},
{
version: "v1",
label: "spec.host",
value: "127.0.0.1",
expectedLabel: "spec.nodeName",
expectedValue: "127.0.0.1",
},
}
for _, tc := range testCases {
label, value, err := ConvertDownwardAPIFieldLabel(tc.version, tc.label, tc.value)
if err != nil {
if tc.expectedErr {
continue
}
t.Errorf("ConvertDownwardAPIFieldLabel(%s, %s, %s) failed: %s",
tc.version, tc.label, tc.value, err)
}
if tc.expectedLabel != label || tc.expectedValue != value {
t.Errorf("ConvertDownwardAPIFieldLabel(%s, %s, %s) = (%s, %s, nil), expected (%s, %s, nil)",
tc.version, tc.label, tc.value, label, value, tc.expectedLabel, tc.expectedValue)
}
}
}

View File

@ -157,20 +157,15 @@ func addConversionFuncs(scheme *runtime.Scheme) error {
err = scheme.AddFieldLabelConversionFunc("v1", "Pod", err = scheme.AddFieldLabelConversionFunc("v1", "Pod",
func(label, value string) (string, string, error) { func(label, value string) (string, string, error) {
switch label { switch label {
case "metadata.annotations", case "metadata.name",
"metadata.labels",
"metadata.name",
"metadata.namespace", "metadata.namespace",
"metadata.uid",
"spec.nodeName", "spec.nodeName",
"spec.restartPolicy", "spec.restartPolicy",
"spec.serviceAccountName",
"spec.schedulerName", "spec.schedulerName",
"status.phase", "status.phase",
"status.hostIP",
"status.podIP": "status.podIP":
return label, value, nil 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": case "spec.host":
return "spec.nodeName", value, nil return "spec.nodeName", value, nil
default: default:

View File

@ -18,10 +18,12 @@ go_library(
"//pkg/api/service:go_default_library", "//pkg/api/service:go_default_library",
"//pkg/apis/core:go_default_library", "//pkg/apis/core:go_default_library",
"//pkg/apis/core/helper:go_default_library", "//pkg/apis/core/helper:go_default_library",
"//pkg/apis/core/pods:go_default_library",
"//pkg/apis/core/v1:go_default_library", "//pkg/apis/core/v1:go_default_library",
"//pkg/apis/core/v1/helper:go_default_library", "//pkg/apis/core/v1/helper:go_default_library",
"//pkg/capabilities:go_default_library", "//pkg/capabilities:go_default_library",
"//pkg/features:go_default_library", "//pkg/features:go_default_library",
"//pkg/fieldpath:go_default_library",
"//pkg/security/apparmor:go_default_library", "//pkg/security/apparmor:go_default_library",
"//vendor/github.com/golang/glog:go_default_library", "//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library",

View File

@ -42,14 +42,15 @@ import (
"k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/validation/field"
utilfeature "k8s.io/apiserver/pkg/util/feature" utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/kubernetes/pkg/api/legacyscheme"
apiservice "k8s.io/kubernetes/pkg/api/service" apiservice "k8s.io/kubernetes/pkg/api/service"
"k8s.io/kubernetes/pkg/apis/core" "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/core/helper" "k8s.io/kubernetes/pkg/apis/core/helper"
podshelper "k8s.io/kubernetes/pkg/apis/core/pods"
corev1 "k8s.io/kubernetes/pkg/apis/core/v1" corev1 "k8s.io/kubernetes/pkg/apis/core/v1"
v1helper "k8s.io/kubernetes/pkg/apis/core/v1/helper" v1helper "k8s.io/kubernetes/pkg/apis/core/v1/helper"
"k8s.io/kubernetes/pkg/capabilities" "k8s.io/kubernetes/pkg/capabilities"
"k8s.io/kubernetes/pkg/features" "k8s.io/kubernetes/pkg/features"
"k8s.io/kubernetes/pkg/fieldpath"
"k8s.io/kubernetes/pkg/security/apparmor" "k8s.io/kubernetes/pkg/security/apparmor"
) )
@ -960,7 +961,7 @@ func validateFlockerVolumeSource(flocker *core.FlockerVolumeSource, fldPath *fie
return allErrs return allErrs
} }
var validDownwardAPIFieldPathExpressions = sets.NewString( var validVolumeDownwardAPIFieldPathExpressions = sets.NewString(
"metadata.name", "metadata.name",
"metadata.namespace", "metadata.namespace",
"metadata.labels", "metadata.labels",
@ -975,7 +976,7 @@ func validateDownwardAPIVolumeFile(file *core.DownwardAPIVolumeFile, fldPath *fi
} }
allErrs = append(allErrs, validateLocalNonReservedPath(file.Path, fldPath.Child("path"))...) allErrs = append(allErrs, validateLocalNonReservedPath(file.Path, fldPath.Child("path"))...)
if file.FieldRef != nil { 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 { if file.ResourceFieldRef != nil {
allErrs = append(allErrs, field.Invalid(fldPath, "resource", "fieldRef and resourceFieldRef can not be specified simultaneously")) allErrs = append(allErrs, field.Invalid(fldPath, "resource", "fieldRef and resourceFieldRef can not be specified simultaneously"))
} }
@ -1898,7 +1899,14 @@ func ValidateEnv(vars []core.EnvVar, fldPath *field.Path) field.ErrorList {
return allErrs 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.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") 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 { func validateEnvVarValueFrom(ev core.EnvVar, fldPath *field.Path) field.ErrorList {
@ -1912,7 +1920,7 @@ func validateEnvVarValueFrom(ev core.EnvVar, fldPath *field.Path) field.ErrorLis
if ev.ValueFrom.FieldRef != nil { if ev.ValueFrom.FieldRef != nil {
numSources++ 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 { if ev.ValueFrom.ResourceFieldRef != nil {
numSources++ numSources++
@ -1945,15 +1953,35 @@ func validateObjectFieldSelector(fs *core.ObjectFieldSelector, expressions *sets
if len(fs.APIVersion) == 0 { if len(fs.APIVersion) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("apiVersion"), "")) 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"), "")) allErrs = append(allErrs, field.Required(fldPath.Child("fieldPath"), ""))
} else { 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))) internalFieldPath, _, err := podshelper.ConvertDownwardAPIFieldLabel(fs.APIVersion, fs.FieldPath, "")
} else if !expressions.Has(internalFieldPath) { if err != nil {
allErrs = append(allErrs, field.NotSupported(fldPath.Child("fieldPath"), internalFieldPath, expressions.List())) allErrs = append(allErrs, field.Invalid(fldPath.Child("fieldPath"), fs.FieldPath, fmt.Sprintf("error converting fieldPath: %v", err)))
return allErrs
}
if path, subscript, ok := fieldpath.SplitMaybeSubscriptedPath(internalFieldPath); ok {
switch path {
case "metadata.annotations":
for _, msg := range validation.IsQualifiedName(strings.ToLower(subscript)) {
allErrs = append(allErrs, field.Invalid(fldPath, subscript, msg))
}
case "metadata.labels":
for _, msg := range validation.IsQualifiedName(subscript) {
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

@ -2628,6 +2628,20 @@ func TestValidateVolumes(t *testing.T) {
FieldPath: "metadata.labels", FieldPath: "metadata.labels",
}, },
}, },
{
Path: "labels with subscript",
FieldRef: &core.ObjectFieldSelector{
APIVersion: "v1",
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{
@ -2635,6 +2649,20 @@ func TestValidateVolumes(t *testing.T) {
FieldPath: "metadata.annotations", FieldPath: "metadata.annotations",
}, },
}, },
{
Path: "annotations with subscript",
FieldRef: &core.ObjectFieldSelector{
APIVersion: "v1",
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{
@ -3824,6 +3852,24 @@ func TestValidateEnv(t *testing.T) {
{Name: "abc", Value: ""}, {Name: "abc", Value: ""},
{Name: "a.b.c", Value: "value"}, {Name: "a.b.c", Value: "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", Name: "abc",
ValueFrom: &core.EnvVarSource{ ValueFrom: &core.EnvVarSource{
@ -4095,7 +4141,20 @@ func TestValidateEnv(t *testing.T) {
expectedError: `[0].valueFrom.fieldRef.fieldPath: Invalid value: "metadata.whoops": error converting fieldPath`, 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: Invalid value: "metadata.name['key']": error converting fieldPath: field label does not support subscript`,
},
{
name: "metadata.labels without subscript",
envs: []core.EnvVar{{ envs: []core.EnvVar{{
Name: "labels", Name: "labels",
ValueFrom: &core.EnvVarSource{ ValueFrom: &core.EnvVarSource{
@ -4108,7 +4167,7 @@ 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.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{{ envs: []core.EnvVar{{
Name: "abc", Name: "abc",
ValueFrom: &core.EnvVarSource{ ValueFrom: &core.EnvVarSource{
@ -4120,6 +4179,32 @@ 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.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",
envs: []core.EnvVar{{ envs: []core.EnvVar{{

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,6 +43,23 @@ func ExtractFieldPathAsString(obj interface{}, fieldPath string) (string, error)
return "", nil return "", nil
} }
if path, subscript, ok := SplitMaybeSubscriptedPath(fieldPath); ok {
switch path {
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
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
default:
return "", fmt.Errorf("fieldPath %q does not support subscript", fieldPath)
}
}
switch fieldPath { switch fieldPath {
case "metadata.annotations": case "metadata.annotations":
return FormatMap(accessor.GetAnnotations()), nil return FormatMap(accessor.GetAnnotations()), nil
@ -57,3 +75,29 @@ func ExtractFieldPathAsString(obj interface{}, fieldPath string) (string, error)
return "", fmt.Errorf("unsupported fieldPath: %v", fieldPath) 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, true).
// - if no, this function returns (fieldPath, "", false).
//
// Example inputs and outputs:
// - "metadata.annotations['myKey']" --> ("metadata.annotations", "myKey", true)
// - "metadata.annotations['a[b]c']" --> ("metadata.annotations", "a[b]c", true)
// - "metadata.labels['']" --> ("metadata.labels", "", true)
// - "metadata.labels" --> ("metadata.labels", "", false)
func SplitMaybeSubscriptedPath(fieldPath string) (string, string, bool) {
if !strings.HasSuffix(fieldPath, "']") {
return fieldPath, "", false
}
s := strings.TrimSuffix(fieldPath, "']")
parts := strings.SplitN(s, "['", 2)
if len(parts) < 2 {
return fieldPath, "", false
}
if len(parts[0]) == 0 {
return fieldPath, "", false
}
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",
@ -88,7 +87,26 @@ func TestExtractFieldPathAsString(t *testing.T) {
}, },
expectedValue: "builder=\"john-doe\"", 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: "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",
@ -99,6 +117,26 @@ func TestExtractFieldPathAsString(t *testing.T) {
}, },
expectedMessageFragment: "unsupported fieldPath", expectedMessageFragment: "unsupported fieldPath",
}, },
{
name: "invalid annotation key",
fieldPath: "metadata.annotations['invalid~key']",
obj: &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{"foo": "bar"},
},
},
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",
},
} }
for _, tc := range cases { for _, tc := range cases {
@ -111,8 +149,93 @@ 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)
} }
} }
} }
func TestSplitMaybeSubscriptedPath(t *testing.T) {
cases := []struct {
fieldPath string
expectedPath string
expectedSubscript string
expectedOK bool
}{
{
fieldPath: "metadata.annotations['key']",
expectedPath: "metadata.annotations",
expectedSubscript: "key",
expectedOK: true,
},
{
fieldPath: "metadata.annotations['a[b']c']",
expectedPath: "metadata.annotations",
expectedSubscript: "a[b']c",
expectedOK: true,
},
{
fieldPath: "metadata.labels['['key']",
expectedPath: "metadata.labels",
expectedSubscript: "['key",
expectedOK: true,
},
{
fieldPath: "metadata.labels['key']']",
expectedPath: "metadata.labels",
expectedSubscript: "key']",
expectedOK: true,
},
{
fieldPath: "metadata.labels['']",
expectedPath: "metadata.labels",
expectedSubscript: "",
expectedOK: true,
},
{
fieldPath: "metadata.labels[' ']",
expectedPath: "metadata.labels",
expectedSubscript: " ",
expectedOK: true,
},
{
fieldPath: "metadata.labels[ 'key' ]",
expectedOK: false,
},
{
fieldPath: "metadata.labels[]",
expectedOK: false,
},
{
fieldPath: "metadata.labels[']",
expectedOK: false,
},
{
fieldPath: "metadata.labels['key']foo",
expectedOK: false,
},
{
fieldPath: "['key']",
expectedOK: false,
},
{
fieldPath: "metadata.labels",
expectedOK: false,
},
}
for _, tc := range cases {
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 {
t.Errorf("SplitMaybeSubscriptedPath(%q) = (%q, %q, true), expect (%q, %q, true)",
tc.fieldPath, path, subscript, tc.expectedPath, tc.expectedSubscript)
}
}
}

View File

@ -29,10 +29,10 @@ go_library(
], ],
importpath = "k8s.io/kubernetes/pkg/kubelet", importpath = "k8s.io/kubernetes/pkg/kubelet",
deps = [ deps = [
"//pkg/api/legacyscheme:go_default_library",
"//pkg/api/v1/pod:go_default_library", "//pkg/api/v1/pod:go_default_library",
"//pkg/api/v1/resource:go_default_library", "//pkg/api/v1/resource:go_default_library",
"//pkg/apis/core:go_default_library", "//pkg/apis/core:go_default_library",
"//pkg/apis/core/pods:go_default_library",
"//pkg/apis/core/v1:go_default_library", "//pkg/apis/core/v1:go_default_library",
"//pkg/apis/core/v1/helper:go_default_library", "//pkg/apis/core/v1/helper:go_default_library",
"//pkg/apis/core/v1/helper/qos:go_default_library", "//pkg/apis/core/v1/helper/qos:go_default_library",

View File

@ -43,9 +43,9 @@ import (
"k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/validation/field"
utilfeature "k8s.io/apiserver/pkg/util/feature" utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/tools/remotecommand" "k8s.io/client-go/tools/remotecommand"
"k8s.io/kubernetes/pkg/api/legacyscheme"
podutil "k8s.io/kubernetes/pkg/api/v1/pod" podutil "k8s.io/kubernetes/pkg/api/v1/pod"
"k8s.io/kubernetes/pkg/api/v1/resource" "k8s.io/kubernetes/pkg/api/v1/resource"
podshelper "k8s.io/kubernetes/pkg/apis/core/pods"
v1helper "k8s.io/kubernetes/pkg/apis/core/v1/helper" v1helper "k8s.io/kubernetes/pkg/apis/core/v1/helper"
v1qos "k8s.io/kubernetes/pkg/apis/core/v1/helper/qos" v1qos "k8s.io/kubernetes/pkg/apis/core/v1/helper/qos"
"k8s.io/kubernetes/pkg/apis/core/v1/validation" "k8s.io/kubernetes/pkg/apis/core/v1/validation"
@ -777,7 +777,7 @@ func (kl *Kubelet) makeEnvironmentVariables(pod *v1.Pod, container *v1.Container
// podFieldSelectorRuntimeValue returns the runtime value of the given // podFieldSelectorRuntimeValue returns the runtime value of the given
// selector for a pod. // selector for a pod.
func (kl *Kubelet) podFieldSelectorRuntimeValue(fs *v1.ObjectFieldSelector, pod *v1.Pod, podIP string) (string, error) { func (kl *Kubelet) podFieldSelectorRuntimeValue(fs *v1.ObjectFieldSelector, pod *v1.Pod, podIP string) (string, error) {
internalFieldPath, _, err := legacyscheme.Scheme.ConvertFieldLabel(fs.APIVersion, "Pod", fs.FieldPath, "") internalFieldPath, _, err := podshelper.ConvertDownwardAPIFieldLabel(fs.APIVersion, fs.FieldPath, "")
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -544,6 +544,48 @@ func TestCollectDataWithDownwardAPI(t *testing.T) {
payload map[string]util.FileProjection payload map[string]util.FileProjection
success bool 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", name: "labels",
volumeFile: []v1.DownwardAPIVolumeFile{ volumeFile: []v1.DownwardAPIVolumeFile{