mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-29 14:37:00 +00:00
Add downward API for environment vars
This commit is contained in:
parent
e0e83fa8fe
commit
7d30f09ebf
@ -188,6 +188,20 @@ func FuzzerFor(t *testing.T, version string, src rand.Source) *fuzz.Fuzzer {
|
||||
c.FuzzNoCustom(ct) // fuzz self without calling this function again
|
||||
ct.TerminationMessagePath = "/" + ct.TerminationMessagePath // Must be non-empty
|
||||
},
|
||||
func(ev *api.EnvVar, c fuzz.Continue) {
|
||||
ev.Name = c.RandString()
|
||||
if c.RandBool() {
|
||||
ev.Value = c.RandString()
|
||||
} else {
|
||||
ev.ValueFrom = &api.EnvVarSource{}
|
||||
ev.ValueFrom.FieldPath = &api.ObjectFieldSelector{}
|
||||
|
||||
versions := []string{"v1beta1", "v1beta2", "v1beta3"}
|
||||
|
||||
ev.ValueFrom.FieldPath.APIVersion = versions[c.Rand.Intn(len(versions))]
|
||||
ev.ValueFrom.FieldPath.FieldPath = c.RandString()
|
||||
}
|
||||
},
|
||||
func(e *api.Event, c fuzz.Continue) {
|
||||
c.FuzzNoCustom(e) // fuzz self without calling this function again
|
||||
// Fix event count to 1, otherwise, if a v1beta1 or v1beta2 event has a count set arbitrarily, it's count is ignored
|
||||
|
@ -508,6 +508,25 @@ type EnvVar struct {
|
||||
Name string `json:"name"`
|
||||
// Optional: defaults to "".
|
||||
Value string `json:"value,omitempty"`
|
||||
// Optional: specify a source the value of this var should come from.
|
||||
ValueFrom *EnvVarSource `json:"valueFrom,omitempty"`
|
||||
}
|
||||
|
||||
// EnvVarSource represents a source for the value of an EnvVar.
|
||||
// Only one of its members may be specified.
|
||||
type EnvVarSource struct {
|
||||
// Selects a field of the pod; only name and namespace are supported.
|
||||
FieldPath *ObjectFieldSelector `json:"fieldPath,omitempty"`
|
||||
}
|
||||
|
||||
// ObjectFieldSelector selects an APIVersioned field of an object.
|
||||
type ObjectFieldSelector struct {
|
||||
// The API version the FieldPath is written in terms of.
|
||||
// If no value is specified, it will be defaulted from the APIVersion
|
||||
// the enclosing object is created with.
|
||||
APIVersion string `json:"apiVersion,omitempty"`
|
||||
// The path of the field to select in the specified API version
|
||||
FieldPath string `json:"fieldPath,omitempty"`
|
||||
}
|
||||
|
||||
// HTTPGetAction describes an action based on HTTP Get requests.
|
||||
|
@ -116,6 +116,11 @@ func init() {
|
||||
out.Value = in.Value
|
||||
out.Key = in.Name
|
||||
out.Name = in.Name
|
||||
|
||||
if err := s.Convert(&in.ValueFrom, &out.ValueFrom, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
func(in *EnvVar, out *newer.EnvVar, s conversion.Scope) error {
|
||||
@ -125,9 +130,13 @@ func init() {
|
||||
} else {
|
||||
out.Name = in.Key
|
||||
}
|
||||
|
||||
if err := s.Convert(&in.ValueFrom, &out.ValueFrom, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
|
||||
// Path & MountType are deprecated.
|
||||
func(in *newer.VolumeMount, out *VolumeMount, s conversion.Scope) error {
|
||||
out.Name = in.Name
|
||||
|
@ -166,6 +166,11 @@ func init() {
|
||||
obj.ExternalID = obj.ID
|
||||
}
|
||||
},
|
||||
func(obj *ObjectFieldSelector) {
|
||||
if obj.APIVersion == "" {
|
||||
obj.APIVersion = "v1beta1"
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -294,3 +294,29 @@ func TestSetDefaultMinionExternalID(t *testing.T) {
|
||||
t.Errorf("Expected default External ID: %s, got: %s", name, m2.ExternalID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetDefaultObjectFieldSelectorAPIVersion(t *testing.T) {
|
||||
s := current.ContainerManifest{
|
||||
Containers: []current.Container{
|
||||
{
|
||||
Env: []current.EnvVar{
|
||||
{
|
||||
ValueFrom: ¤t.EnvVarSource{
|
||||
FieldPath: ¤t.ObjectFieldSelector{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
obj2 := roundTrip(t, runtime.Object(¤t.ContainerManifestList{
|
||||
Items: []current.ContainerManifest{s},
|
||||
}))
|
||||
sList2 := obj2.(*current.ContainerManifestList)
|
||||
s2 := sList2.Items[0]
|
||||
|
||||
apiVersion := s2.Containers[0].Env[0].ValueFrom.FieldPath.APIVersion
|
||||
if apiVersion != "v1beta1" {
|
||||
t.Errorf("Expected default APIVersion v1beta1, got: %v", apiVersion)
|
||||
}
|
||||
}
|
||||
|
@ -397,6 +397,23 @@ type EnvVar struct {
|
||||
Key string `json:"key,omitempty" description:"name of the environment variable; must be a C_IDENTIFIER; deprecated - use name instead"`
|
||||
// Optional: defaults to "".
|
||||
Value string `json:"value,omitempty" description:"value of the environment variable; defaults to empty string"`
|
||||
// Optional: specify a source the value of this var should come from.
|
||||
ValueFrom *EnvVarSource `json:"valueFrom,omitempty" description:"source for the environment variable's value; cannot be used if value is not empty"`
|
||||
}
|
||||
|
||||
// EnvVarSource represents a source for the value of an EnvVar.
|
||||
// Only one of its members may be specified.
|
||||
type EnvVarSource struct {
|
||||
// Selects a field of the pod; only name and namespace are supported.
|
||||
FieldPath *ObjectFieldSelector `json:"fieldPath,omitempty" description:"selects a field of the pod; only name and namespace are supported"`
|
||||
}
|
||||
|
||||
// ObjectFieldSelector selects an APIVersioned field of an object.
|
||||
type ObjectFieldSelector struct {
|
||||
// The API version the FieldPath is written in terms of.
|
||||
APIVersion string `json:"apiVersion,omitempty" description="The API version that FieldPath is written in terms of"`
|
||||
// The path of the field to select in the specified API version
|
||||
FieldPath string `json:"fieldPath,omitempty" description="The path of the field to select in the specified API version"`
|
||||
}
|
||||
|
||||
// HTTPGetAction describes an action based on HTTP Get requests.
|
||||
|
@ -167,6 +167,11 @@ func init() {
|
||||
obj.ExternalID = obj.ID
|
||||
}
|
||||
},
|
||||
func(obj *ObjectFieldSelector) {
|
||||
if obj.APIVersion == "" {
|
||||
obj.APIVersion = "v1beta2"
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -293,3 +293,29 @@ func TestSetDefaultMinionExternalID(t *testing.T) {
|
||||
t.Errorf("Expected default External ID: %s, got: %s", name, m2.ExternalID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetDefaultObjectFieldSelectorAPIVersion(t *testing.T) {
|
||||
s := current.ContainerManifest{
|
||||
Containers: []current.Container{
|
||||
{
|
||||
Env: []current.EnvVar{
|
||||
{
|
||||
ValueFrom: ¤t.EnvVarSource{
|
||||
FieldPath: ¤t.ObjectFieldSelector{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
obj2 := roundTrip(t, runtime.Object(¤t.ContainerManifestList{
|
||||
Items: []current.ContainerManifest{s},
|
||||
}))
|
||||
sList2 := obj2.(*current.ContainerManifestList)
|
||||
s2 := sList2.Items[0]
|
||||
|
||||
apiVersion := s2.Containers[0].Env[0].ValueFrom.FieldPath.APIVersion
|
||||
if apiVersion != "v1beta2" {
|
||||
t.Errorf("Expected default APIVersion v1beta2, got: %v", apiVersion)
|
||||
}
|
||||
}
|
||||
|
@ -381,6 +381,23 @@ type EnvVar struct {
|
||||
Name string `json:"name" description:"name of the environment variable; must be a C_IDENTIFIER"`
|
||||
// Optional: defaults to "".
|
||||
Value string `json:"value,omitempty" description:"value of the environment variable; defaults to empty string"`
|
||||
// Optional: specify a source the value of this var should come from.
|
||||
ValueFrom *EnvVarSource `json:"valueFrom,omitempty" description:"source for the environment variable's value; cannot be used if value is not empty"`
|
||||
}
|
||||
|
||||
// EnvVarSource represents a source for the value of an EnvVar.
|
||||
// Only one of its members may be specified.
|
||||
type EnvVarSource struct {
|
||||
// Selects a field of the pod; only name and namespace are supported.
|
||||
FieldPath *ObjectFieldSelector `json:"fieldPath,omitempty" description:"selects a field of the pod; only name and namespace are supported"`
|
||||
}
|
||||
|
||||
// ObjectFieldSelector selects an APIVersioned field of an object.
|
||||
type ObjectFieldSelector struct {
|
||||
// The API version the FieldPath is written in terms of.
|
||||
APIVersion string `json:"apiVersion,omitempty" description="The API version that FieldPath is written in terms of"`
|
||||
// The path of the field to select in the specified API version
|
||||
FieldPath string `json:"fieldPath,omitempty" description="The path of the field to select in the specified API version"`
|
||||
}
|
||||
|
||||
// HTTPGetAction describes an action based on HTTP Get requests.
|
||||
|
@ -1794,6 +1794,23 @@ func init() {
|
||||
out.Path = in.Path
|
||||
return nil
|
||||
},
|
||||
func(in *EnvVar, out *newer.EnvVar, s conversion.Scope) error {
|
||||
out.Name = in.Name
|
||||
out.Value = in.Value
|
||||
if err := s.Convert(&in.ValueFrom, &out.ValueFrom, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
func(in *newer.EnvVar, out *EnvVar, s conversion.Scope) error {
|
||||
out.Name = in.Name
|
||||
out.Value = in.Value
|
||||
if err := s.Convert(&in.ValueFrom, &out.ValueFrom, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
func(in *PodSpec, out *newer.PodSpec, s conversion.Scope) error {
|
||||
if in.Volumes != nil {
|
||||
out.Volumes = make([]newer.Volume, len(in.Volumes))
|
||||
@ -2715,6 +2732,7 @@ func init() {
|
||||
func(label, value string) (string, string, error) {
|
||||
switch label {
|
||||
case "metadata.name",
|
||||
"metadata.namespace",
|
||||
"status.phase",
|
||||
"spec.host":
|
||||
return label, value, nil
|
||||
|
@ -128,6 +128,11 @@ func init() {
|
||||
obj.Spec.ExternalID = obj.Name
|
||||
}
|
||||
},
|
||||
func(obj *ObjectFieldSelector) {
|
||||
if obj.APIVersion == "" {
|
||||
obj.APIVersion = "v1beta3"
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -302,3 +302,30 @@ func TestSetDefaultNodeExternalID(t *testing.T) {
|
||||
t.Errorf("Expected default External ID: %s, got: %s", name, n2.Spec.ExternalID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetDefaultObjectFieldSelectorAPIVersion(t *testing.T) {
|
||||
s := current.PodSpec{
|
||||
Containers: []current.Container{
|
||||
{
|
||||
Env: []current.EnvVar{
|
||||
{
|
||||
ValueFrom: ¤t.EnvVarSource{
|
||||
FieldPath: ¤t.ObjectFieldSelector{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
pod := ¤t.Pod{
|
||||
Spec: s,
|
||||
}
|
||||
obj2 := roundTrip(t, runtime.Object(pod))
|
||||
pod2 := obj2.(*current.Pod)
|
||||
s2 := pod2.Spec
|
||||
|
||||
apiVersion := s2.Containers[0].Env[0].ValueFrom.FieldPath.APIVersion
|
||||
if apiVersion != "v1beta3" {
|
||||
t.Errorf("Expected default APIVersion v1beta3, got: %v", apiVersion)
|
||||
}
|
||||
}
|
||||
|
@ -519,6 +519,23 @@ type EnvVar struct {
|
||||
Name string `json:"name" description:"name of the environment variable; must be a C_IDENTIFIER"`
|
||||
// Optional: defaults to "".
|
||||
Value string `json:"value,omitempty" description:"value of the environment variable; defaults to empty string"`
|
||||
// Optional: specify a source the value of this var should come from.
|
||||
ValueFrom *EnvVarSource `json:"valueFrom,omitempty" description:"source for the environment variable's value; cannot be used if value is not empty"`
|
||||
}
|
||||
|
||||
// EnvVarSource represents a source for the value of an EnvVar.
|
||||
// Only one of its members may be specified.
|
||||
type EnvVarSource struct {
|
||||
// Selects a field of the pod; only name and namespace are supported.
|
||||
FieldPath *ObjectFieldSelector `json:"fieldPath,omitempty" description:"selects a field of the pod; only name and namespace are supported"`
|
||||
}
|
||||
|
||||
// ObjectFieldSelector selects an APIVersioned field of an object.
|
||||
type ObjectFieldSelector struct {
|
||||
// The API version the FieldPath is written in terms of.
|
||||
APIVersion string `json:"apiVersion,omitempty" description="The API version that FieldPath is written in terms of"`
|
||||
// The path of the field to select in the specified API version
|
||||
FieldPath string `json:"fieldPath,omitempty" description="The path of the field to select in the specified API version"`
|
||||
}
|
||||
|
||||
// HTTPGetAction describes an action based on HTTP Get requests.
|
||||
|
@ -555,15 +555,58 @@ func validateEnv(vars []api.EnvVar) errs.ValidationErrorList {
|
||||
vErrs := errs.ValidationErrorList{}
|
||||
if len(ev.Name) == 0 {
|
||||
vErrs = append(vErrs, errs.NewFieldRequired("name"))
|
||||
}
|
||||
if !util.IsCIdentifier(ev.Name) {
|
||||
} else if !util.IsCIdentifier(ev.Name) {
|
||||
vErrs = append(vErrs, errs.NewFieldInvalid("name", ev.Name, cIdentifierErrorMsg))
|
||||
}
|
||||
vErrs = append(vErrs, validateEnvVarValueFrom(ev).Prefix("valueFrom")...)
|
||||
allErrs = append(allErrs, vErrs.PrefixIndex(i)...)
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
||||
func validateEnvVarValueFrom(ev api.EnvVar) errs.ValidationErrorList {
|
||||
allErrs := errs.ValidationErrorList{}
|
||||
|
||||
if ev.ValueFrom == nil {
|
||||
return allErrs
|
||||
}
|
||||
|
||||
numSources := 0
|
||||
|
||||
switch {
|
||||
case ev.ValueFrom.FieldPath != nil:
|
||||
numSources++
|
||||
allErrs = append(allErrs, validateObjectFieldSelector(ev.ValueFrom.FieldPath).Prefix("fieldPath")...)
|
||||
}
|
||||
|
||||
if ev.Value != "" && numSources != 0 {
|
||||
allErrs = append(allErrs, errs.NewFieldInvalid("", "", "sources cannot be specified when value is not empty"))
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
var validFieldPathExpressions = util.NewStringSet("metadata.name", "metadata.namespace")
|
||||
|
||||
func validateObjectFieldSelector(fs *api.ObjectFieldSelector) errs.ValidationErrorList {
|
||||
allErrs := errs.ValidationErrorList{}
|
||||
|
||||
if fs.APIVersion == "" {
|
||||
allErrs = append(allErrs, errs.NewFieldRequired("apiVersion"))
|
||||
} else if fs.FieldPath == "" {
|
||||
allErrs = append(allErrs, errs.NewFieldRequired("fieldPath"))
|
||||
} else {
|
||||
internalFieldPath, _, err := api.Scheme.ConvertFieldLabel(fs.APIVersion, "Pod", fs.FieldPath, "")
|
||||
if err != nil {
|
||||
allErrs = append(allErrs, errs.NewFieldInvalid("fieldPath", fs.FieldPath, "error converting fieldPath"))
|
||||
} else if !validFieldPathExpressions.Has(internalFieldPath) {
|
||||
allErrs = append(allErrs, errs.NewFieldNotSupported("fieldPath", internalFieldPath))
|
||||
}
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
func validateVolumeMounts(mounts []api.VolumeMount, volumes util.StringSet) errs.ValidationErrorList {
|
||||
allErrs := errs.ValidationErrorList{}
|
||||
|
||||
|
@ -633,23 +633,108 @@ func TestValidateEnv(t *testing.T) {
|
||||
{Name: "ABC", Value: "value"},
|
||||
{Name: "AbC_123", Value: "value"},
|
||||
{Name: "abc", Value: ""},
|
||||
{
|
||||
Name: "abc",
|
||||
ValueFrom: &api.EnvVarSource{
|
||||
FieldPath: &api.ObjectFieldSelector{
|
||||
APIVersion: "v1beta3",
|
||||
FieldPath: "metadata.name",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if errs := validateEnv(successCase); len(errs) != 0 {
|
||||
t.Errorf("expected success: %v", errs)
|
||||
}
|
||||
|
||||
errorCases := map[string][]api.EnvVar{
|
||||
"zero-length name": {{Name: ""}},
|
||||
"name not a C identifier": {{Name: "a.b.c"}},
|
||||
errorCases := []struct {
|
||||
name string
|
||||
envs []api.EnvVar
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "zero-length name",
|
||||
envs: []api.EnvVar{{Name: ""}},
|
||||
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': must match regex [A-Za-z_][A-Za-z0-9_]*",
|
||||
},
|
||||
{
|
||||
name: "value and valueFrom specified",
|
||||
envs: []api.EnvVar{{
|
||||
Name: "abc",
|
||||
Value: "foo",
|
||||
ValueFrom: &api.EnvVarSource{
|
||||
FieldPath: &api.ObjectFieldSelector{
|
||||
APIVersion: "v1beta3",
|
||||
FieldPath: "metadata.name",
|
||||
},
|
||||
},
|
||||
}},
|
||||
expectedError: "[0].valueFrom: invalid value '': sources cannot be specified when value is not empty",
|
||||
},
|
||||
{
|
||||
name: "missing FieldPath on ObjectFieldSelector",
|
||||
envs: []api.EnvVar{{
|
||||
Name: "abc",
|
||||
ValueFrom: &api.EnvVarSource{
|
||||
FieldPath: &api.ObjectFieldSelector{
|
||||
APIVersion: "v1beta3",
|
||||
},
|
||||
},
|
||||
}},
|
||||
expectedError: "[0].valueFrom.fieldPath.fieldPath: required value",
|
||||
},
|
||||
{
|
||||
name: "missing APIVersion on ObjectFieldSelector",
|
||||
envs: []api.EnvVar{{
|
||||
Name: "abc",
|
||||
ValueFrom: &api.EnvVarSource{
|
||||
FieldPath: &api.ObjectFieldSelector{
|
||||
FieldPath: "metadata.name",
|
||||
},
|
||||
},
|
||||
}},
|
||||
expectedError: "[0].valueFrom.fieldPath.apiVersion: required value",
|
||||
},
|
||||
{
|
||||
name: "invalid fieldPath",
|
||||
envs: []api.EnvVar{{
|
||||
Name: "abc",
|
||||
ValueFrom: &api.EnvVarSource{
|
||||
FieldPath: &api.ObjectFieldSelector{
|
||||
FieldPath: "metadata.whoops",
|
||||
APIVersion: "v1beta3",
|
||||
},
|
||||
},
|
||||
}},
|
||||
expectedError: "[0].valueFrom.fieldPath.fieldPath: invalid value 'metadata.whoops': error converting fieldPath",
|
||||
},
|
||||
{
|
||||
name: "unsupported fieldPath",
|
||||
envs: []api.EnvVar{{
|
||||
Name: "abc",
|
||||
ValueFrom: &api.EnvVarSource{
|
||||
FieldPath: &api.ObjectFieldSelector{
|
||||
FieldPath: "status.phase",
|
||||
APIVersion: "v1beta3",
|
||||
},
|
||||
},
|
||||
}},
|
||||
expectedError: "[0].valueFrom.fieldPath.fieldPath: unsupported value 'status.phase'",
|
||||
},
|
||||
}
|
||||
for k, v := range errorCases {
|
||||
if errs := validateEnv(v); len(errs) == 0 {
|
||||
t.Errorf("expected failure for %s", k)
|
||||
for _, tc := range errorCases {
|
||||
if errs := validateEnv(tc.envs); len(errs) == 0 {
|
||||
t.Errorf("expected failure for %s", tc.name)
|
||||
} else {
|
||||
for i := range errs {
|
||||
detail := errs[i].(*errors.ValidationError).Detail
|
||||
if detail != "" && detail != cIdentifierErrorMsg {
|
||||
t.Errorf("%s: expected error detail either empty or %s, got %s", k, cIdentifierErrorMsg, detail)
|
||||
str := errs[i].(*errors.ValidationError).Error()
|
||||
if str != "" && str != tc.expectedError {
|
||||
t.Errorf("%s: expected error detail either empty or %s, got %s", tc.name, tc.expectedError, str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
19
pkg/fieldpath/doc.go
Normal file
19
pkg/fieldpath/doc.go
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
Copyright 2015 Google Inc. All rights reserved.
|
||||
|
||||
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 fieldpath supplies methods for extracting fields from objects
|
||||
// given a path to a field.
|
||||
package fieldpath
|
47
pkg/fieldpath/fieldpath.go
Normal file
47
pkg/fieldpath/fieldpath.go
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
Copyright 2015 Google Inc. All rights reserved.
|
||||
|
||||
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 fieldpath
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
|
||||
)
|
||||
|
||||
// ExtractFieldPathAsString extracts the field from the given object
|
||||
// and returns it as a string. The object must be a pointer to an
|
||||
// API type.
|
||||
//
|
||||
// Currently, this API is limited to supporting the fieldpaths:
|
||||
//
|
||||
// 1. metadata.name - The name of an API object
|
||||
// 2. metadata.namespace - The namespace of an API object
|
||||
func ExtractFieldPathAsString(obj interface{}, fieldPath string) (string, error) {
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
switch fieldPath {
|
||||
case "metadata.name":
|
||||
return accessor.Name(), nil
|
||||
case "metadata.namespace":
|
||||
return accessor.Namespace(), nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("Unsupported fieldPath: %v", fieldPath)
|
||||
}
|
86
pkg/fieldpath/fieldpath_test.go
Normal file
86
pkg/fieldpath/fieldpath_test.go
Normal file
@ -0,0 +1,86 @@
|
||||
/*
|
||||
Copyright 2015 Google Inc. All rights reserved.
|
||||
|
||||
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 fieldpath
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
)
|
||||
|
||||
func TestExtractFieldPathAsString(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
fieldPath string
|
||||
obj interface{}
|
||||
expectedValue string
|
||||
expectedMessageFragment string
|
||||
}{
|
||||
{
|
||||
name: "not an API object",
|
||||
fieldPath: "metadata.name",
|
||||
obj: "",
|
||||
expectedMessageFragment: "expected struct",
|
||||
},
|
||||
{
|
||||
name: "ok - namespace",
|
||||
fieldPath: "metadata.namespace",
|
||||
obj: &api.Pod{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Namespace: "object-namespace",
|
||||
},
|
||||
},
|
||||
expectedValue: "object-namespace",
|
||||
},
|
||||
{
|
||||
name: "ok - name",
|
||||
fieldPath: "metadata.name",
|
||||
obj: &api.Pod{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "object-name",
|
||||
},
|
||||
},
|
||||
expectedValue: "object-name",
|
||||
},
|
||||
{
|
||||
name: "invalid expression",
|
||||
fieldPath: "metadata.whoops",
|
||||
obj: &api.Pod{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Namespace: "object-namespace",
|
||||
},
|
||||
},
|
||||
expectedMessageFragment: "Unsupported fieldPath",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
actual, err := ExtractFieldPathAsString(tc.obj, tc.fieldPath)
|
||||
if err != nil {
|
||||
if tc.expectedMessageFragment != "" {
|
||||
if !strings.Contains(err.Error(), tc.expectedMessageFragment) {
|
||||
t.Errorf("%v: Unexpected error message: %q, expected to contain %q", tc.name, err, tc.expectedMessageFragment)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("%v: unexpected error: %v", tc.name, err)
|
||||
}
|
||||
} else if e := tc.expectedValue; e != "" && e != actual {
|
||||
t.Errorf("%v: Unexpected result; got %q, expected %q", tc.name, actual, e)
|
||||
}
|
||||
}
|
||||
}
|
@ -36,6 +36,7 @@ import (
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/record"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/fieldpath"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/cadvisor"
|
||||
kubecontainer "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/container"
|
||||
@ -706,7 +707,7 @@ func (kl *Kubelet) GenerateRunContainerOptions(pod *api.Pod, container *api.Cont
|
||||
return nil, fmt.Errorf("impossible: cannot find the mounted volumes for pod %q", kubecontainer.GetPodFullName(pod))
|
||||
}
|
||||
opts.Binds = makeBinds(container, vol)
|
||||
opts.Envs, err = kl.makeEnvironmentVariables(pod.Namespace, container)
|
||||
opts.Envs, err = kl.makeEnvironmentVariables(pod, container)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -784,7 +785,7 @@ func (kl *Kubelet) getServiceEnvVarMap(ns string) (map[string]string, error) {
|
||||
}
|
||||
|
||||
// Make the service environment variables for a pod in the given namespace.
|
||||
func (kl *Kubelet) makeEnvironmentVariables(ns string, container *api.Container) ([]string, error) {
|
||||
func (kl *Kubelet) makeEnvironmentVariables(pod *api.Pod, container *api.Container) ([]string, error) {
|
||||
var result []string
|
||||
// Note: These are added to the docker.Config, but are not included in the checksum computed
|
||||
// by dockertools.BuildDockerName(...). That way, we can still determine whether an
|
||||
@ -795,7 +796,7 @@ func (kl *Kubelet) makeEnvironmentVariables(ns string, container *api.Container)
|
||||
// To avoid this users can: (1) wait between starting a service and starting; or (2) detect
|
||||
// missing service env var and exit and be restarted; or (3) use DNS instead of env vars
|
||||
// and keep trying to resolve the DNS name of the service (recommended).
|
||||
serviceEnv, err := kl.getServiceEnvVarMap(ns)
|
||||
serviceEnv, err := kl.getServiceEnvVarMap(pod.Namespace)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
@ -807,7 +808,13 @@ func (kl *Kubelet) makeEnvironmentVariables(ns string, container *api.Container)
|
||||
// env vars.
|
||||
// TODO: remove this net line once all platforms use apiserver+Pods.
|
||||
delete(serviceEnv, value.Name)
|
||||
result = append(result, fmt.Sprintf("%s=%s", value.Name, value.Value))
|
||||
|
||||
runtimeValue, err := kl.runtimeEnvVarValue(value, pod)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
result = append(result, fmt.Sprintf("%s=%s", value.Name, runtimeValue))
|
||||
}
|
||||
|
||||
// Append remaining service env vars.
|
||||
@ -817,6 +824,33 @@ func (kl *Kubelet) makeEnvironmentVariables(ns string, container *api.Container)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// runtimeEnvVarValue determines the value that an env var should take when a container
|
||||
// is started. If the value of the env var is the empty string, the source of the env var
|
||||
// is resolved, if one is specified.
|
||||
//
|
||||
// TODO: preliminary factoring; make better
|
||||
func (kl *Kubelet) runtimeEnvVarValue(envVar api.EnvVar, pod *api.Pod) (string, error) {
|
||||
runtimeVal := envVar.Value
|
||||
if runtimeVal != "" {
|
||||
return runtimeVal, nil
|
||||
}
|
||||
|
||||
if envVar.ValueFrom != nil && envVar.ValueFrom.FieldPath != nil {
|
||||
return kl.podFieldSelectorRuntimeValue(envVar.ValueFrom.FieldPath, pod)
|
||||
}
|
||||
|
||||
return runtimeVal, nil
|
||||
}
|
||||
|
||||
func (kl *Kubelet) podFieldSelectorRuntimeValue(fs *api.ObjectFieldSelector, pod *api.Pod) (string, error) {
|
||||
internalFieldPath, _, err := api.Scheme.ConvertFieldLabel(fs.APIVersion, "Pod", fs.FieldPath, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fieldpath.ExtractFieldPathAsString(pod, internalFieldPath)
|
||||
}
|
||||
|
||||
// getClusterDNS returns a list of the DNS servers and a list of the DNS search
|
||||
// domains of the cluster.
|
||||
func (kl *Kubelet) getClusterDNS(pod *api.Pod) ([]string, []string, error) {
|
||||
|
@ -68,6 +68,8 @@ type TestKubelet struct {
|
||||
fakeMirrorClient *fakeMirrorClient
|
||||
}
|
||||
|
||||
const testKubeletHostname = "testnode"
|
||||
|
||||
func newTestKubelet(t *testing.T) *TestKubelet {
|
||||
fakeDocker := &dockertools.FakeDockerClient{Errors: make(map[string]error), RemovedImages: util.StringSet{}}
|
||||
fakeRecorder := &record.FakeRecorder{}
|
||||
@ -2378,6 +2380,39 @@ func TestMakeEnvironmentVariables(t *testing.T) {
|
||||
"KUBERNETES_RO_PORT_8087_TCP_ADDR=1.2.3.7"),
|
||||
21,
|
||||
},
|
||||
{
|
||||
"downward api pod",
|
||||
"downward-api",
|
||||
&api.Container{
|
||||
Env: []api.EnvVar{
|
||||
{
|
||||
Name: "POD_NAME",
|
||||
ValueFrom: &api.EnvVarSource{
|
||||
FieldPath: &api.ObjectFieldSelector{
|
||||
APIVersion: "v1beta3",
|
||||
FieldPath: "metadata.name",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "POD_NAMESPACE",
|
||||
ValueFrom: &api.EnvVarSource{
|
||||
FieldPath: &api.ObjectFieldSelector{
|
||||
APIVersion: "v1beta3",
|
||||
FieldPath: "metadata.namespace",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"nothing",
|
||||
true,
|
||||
util.NewStringSet(
|
||||
"POD_NAME=dapi-test-pod-name",
|
||||
"POD_NAMESPACE=downward-api",
|
||||
),
|
||||
2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@ -2390,7 +2425,14 @@ func TestMakeEnvironmentVariables(t *testing.T) {
|
||||
kl.serviceLister = testServiceLister{services}
|
||||
}
|
||||
|
||||
result, err := kl.makeEnvironmentVariables(tc.ns, tc.container)
|
||||
testPod := &api.Pod{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Namespace: tc.ns,
|
||||
Name: "dapi-test-pod-name",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := kl.makeEnvironmentVariables(testPod, tc.container)
|
||||
if err != nil {
|
||||
t.Errorf("[%v] Unexpected error: %v", tc.name, err)
|
||||
}
|
||||
|
96
test/e2e/downward_api.go
Normal file
96
test/e2e/downward_api.go
Normal file
@ -0,0 +1,96 @@
|
||||
/*
|
||||
Copyright 2015 Google Inc. All rights reserved.
|
||||
|
||||
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 e2e
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Downward API", func() {
|
||||
var c *client.Client
|
||||
var ns string
|
||||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
c, err = loadClient()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
ns_, err := createTestingNS("downward-api", c)
|
||||
ns = ns_.Name
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
// Clean up the namespace if a non-default one was used
|
||||
if ns != api.NamespaceDefault {
|
||||
By("Cleaning up the namespace")
|
||||
err := c.Namespaces().Delete(ns)
|
||||
expectNoError(err)
|
||||
}
|
||||
})
|
||||
|
||||
It("should provide pod name and namespace as env vars", func() {
|
||||
podName := "downward-api-" + string(util.NewUUID())
|
||||
pod := &api.Pod{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: podName,
|
||||
Labels: map[string]string{"name": podName},
|
||||
},
|
||||
Spec: api.PodSpec{
|
||||
Containers: []api.Container{
|
||||
{
|
||||
Name: "dapi-container",
|
||||
Image: "gcr.io/google_containers/busybox",
|
||||
Command: []string{"sh", "-c", "env"},
|
||||
Env: []api.EnvVar{
|
||||
{
|
||||
Name: "POD_NAME",
|
||||
ValueFrom: &api.EnvVarSource{
|
||||
FieldPath: &api.ObjectFieldSelector{
|
||||
APIVersion: "v1beta3",
|
||||
FieldPath: "metadata.name",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "POD_NAMESPACE",
|
||||
ValueFrom: &api.EnvVarSource{
|
||||
FieldPath: &api.ObjectFieldSelector{
|
||||
APIVersion: "v1beta3",
|
||||
FieldPath: "metadata.namespace",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
RestartPolicy: api.RestartPolicyNever,
|
||||
},
|
||||
}
|
||||
|
||||
testContainerOutputInNamespace("downward api env vars", c, pod, []string{
|
||||
fmt.Sprintf("POD_NAME=%v", podName),
|
||||
fmt.Sprintf("POD_NAMESPACE=%v", ns),
|
||||
}, ns)
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue
Block a user