Add downward API for environment vars

This commit is contained in:
Paul Morie 2015-04-23 16:57:30 -04:00
parent e0e83fa8fe
commit 7d30f09ebf
21 changed files with 674 additions and 17 deletions

View File

@ -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 c.FuzzNoCustom(ct) // fuzz self without calling this function again
ct.TerminationMessagePath = "/" + ct.TerminationMessagePath // Must be non-empty 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) { func(e *api.Event, c fuzz.Continue) {
c.FuzzNoCustom(e) // fuzz self without calling this function again 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 // Fix event count to 1, otherwise, if a v1beta1 or v1beta2 event has a count set arbitrarily, it's count is ignored

View File

@ -508,6 +508,25 @@ type EnvVar struct {
Name string `json:"name"` Name string `json:"name"`
// Optional: defaults to "". // Optional: defaults to "".
Value string `json:"value,omitempty"` 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. // HTTPGetAction describes an action based on HTTP Get requests.

View File

@ -116,6 +116,11 @@ func init() {
out.Value = in.Value out.Value = in.Value
out.Key = in.Name out.Key = in.Name
out.Name = in.Name out.Name = in.Name
if err := s.Convert(&in.ValueFrom, &out.ValueFrom, 0); err != nil {
return err
}
return nil return nil
}, },
func(in *EnvVar, out *newer.EnvVar, s conversion.Scope) error { func(in *EnvVar, out *newer.EnvVar, s conversion.Scope) error {
@ -125,9 +130,13 @@ func init() {
} else { } else {
out.Name = in.Key out.Name = in.Key
} }
if err := s.Convert(&in.ValueFrom, &out.ValueFrom, 0); err != nil {
return err
}
return nil return nil
}, },
// Path & MountType are deprecated. // Path & MountType are deprecated.
func(in *newer.VolumeMount, out *VolumeMount, s conversion.Scope) error { func(in *newer.VolumeMount, out *VolumeMount, s conversion.Scope) error {
out.Name = in.Name out.Name = in.Name

View File

@ -166,6 +166,11 @@ func init() {
obj.ExternalID = obj.ID obj.ExternalID = obj.ID
} }
}, },
func(obj *ObjectFieldSelector) {
if obj.APIVersion == "" {
obj.APIVersion = "v1beta1"
}
},
) )
} }

View File

@ -294,3 +294,29 @@ func TestSetDefaultMinionExternalID(t *testing.T) {
t.Errorf("Expected default External ID: %s, got: %s", name, m2.ExternalID) 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: &current.EnvVarSource{
FieldPath: &current.ObjectFieldSelector{},
},
},
},
},
},
}
obj2 := roundTrip(t, runtime.Object(&current.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)
}
}

View File

@ -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"` Key string `json:"key,omitempty" description:"name of the environment variable; must be a C_IDENTIFIER; deprecated - use name instead"`
// Optional: defaults to "". // Optional: defaults to "".
Value string `json:"value,omitempty" description:"value of the environment variable; defaults to empty string"` 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. // HTTPGetAction describes an action based on HTTP Get requests.

View File

@ -167,6 +167,11 @@ func init() {
obj.ExternalID = obj.ID obj.ExternalID = obj.ID
} }
}, },
func(obj *ObjectFieldSelector) {
if obj.APIVersion == "" {
obj.APIVersion = "v1beta2"
}
},
) )
} }

View File

@ -293,3 +293,29 @@ func TestSetDefaultMinionExternalID(t *testing.T) {
t.Errorf("Expected default External ID: %s, got: %s", name, m2.ExternalID) 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: &current.EnvVarSource{
FieldPath: &current.ObjectFieldSelector{},
},
},
},
},
},
}
obj2 := roundTrip(t, runtime.Object(&current.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)
}
}

View File

@ -381,6 +381,23 @@ type EnvVar struct {
Name string `json:"name" description:"name of the environment variable; must be a C_IDENTIFIER"` Name string `json:"name" description:"name of the environment variable; must be a C_IDENTIFIER"`
// Optional: defaults to "". // Optional: defaults to "".
Value string `json:"value,omitempty" description:"value of the environment variable; defaults to empty string"` 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. // HTTPGetAction describes an action based on HTTP Get requests.

View File

@ -1794,6 +1794,23 @@ func init() {
out.Path = in.Path out.Path = in.Path
return nil 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 { func(in *PodSpec, out *newer.PodSpec, s conversion.Scope) error {
if in.Volumes != nil { if in.Volumes != nil {
out.Volumes = make([]newer.Volume, len(in.Volumes)) out.Volumes = make([]newer.Volume, len(in.Volumes))
@ -2715,6 +2732,7 @@ func init() {
func(label, value string) (string, string, error) { func(label, value string) (string, string, error) {
switch label { switch label {
case "metadata.name", case "metadata.name",
"metadata.namespace",
"status.phase", "status.phase",
"spec.host": "spec.host":
return label, value, nil return label, value, nil

View File

@ -128,6 +128,11 @@ func init() {
obj.Spec.ExternalID = obj.Name obj.Spec.ExternalID = obj.Name
} }
}, },
func(obj *ObjectFieldSelector) {
if obj.APIVersion == "" {
obj.APIVersion = "v1beta3"
}
},
) )
} }

View File

@ -302,3 +302,30 @@ func TestSetDefaultNodeExternalID(t *testing.T) {
t.Errorf("Expected default External ID: %s, got: %s", name, n2.Spec.ExternalID) 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: &current.EnvVarSource{
FieldPath: &current.ObjectFieldSelector{},
},
},
},
},
},
}
pod := &current.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)
}
}

View File

@ -519,6 +519,23 @@ type EnvVar struct {
Name string `json:"name" description:"name of the environment variable; must be a C_IDENTIFIER"` Name string `json:"name" description:"name of the environment variable; must be a C_IDENTIFIER"`
// Optional: defaults to "". // Optional: defaults to "".
Value string `json:"value,omitempty" description:"value of the environment variable; defaults to empty string"` 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. // HTTPGetAction describes an action based on HTTP Get requests.

View File

@ -555,15 +555,58 @@ func validateEnv(vars []api.EnvVar) errs.ValidationErrorList {
vErrs := errs.ValidationErrorList{} vErrs := errs.ValidationErrorList{}
if len(ev.Name) == 0 { if len(ev.Name) == 0 {
vErrs = append(vErrs, errs.NewFieldRequired("name")) vErrs = append(vErrs, errs.NewFieldRequired("name"))
} } else if !util.IsCIdentifier(ev.Name) {
if !util.IsCIdentifier(ev.Name) {
vErrs = append(vErrs, errs.NewFieldInvalid("name", ev.Name, cIdentifierErrorMsg)) vErrs = append(vErrs, errs.NewFieldInvalid("name", ev.Name, cIdentifierErrorMsg))
} }
vErrs = append(vErrs, validateEnvVarValueFrom(ev).Prefix("valueFrom")...)
allErrs = append(allErrs, vErrs.PrefixIndex(i)...) allErrs = append(allErrs, vErrs.PrefixIndex(i)...)
} }
return allErrs 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 { func validateVolumeMounts(mounts []api.VolumeMount, volumes util.StringSet) errs.ValidationErrorList {
allErrs := errs.ValidationErrorList{} allErrs := errs.ValidationErrorList{}

View File

@ -633,23 +633,108 @@ func TestValidateEnv(t *testing.T) {
{Name: "ABC", Value: "value"}, {Name: "ABC", Value: "value"},
{Name: "AbC_123", Value: "value"}, {Name: "AbC_123", Value: "value"},
{Name: "abc", Value: ""}, {Name: "abc", Value: ""},
{
Name: "abc",
ValueFrom: &api.EnvVarSource{
FieldPath: &api.ObjectFieldSelector{
APIVersion: "v1beta3",
FieldPath: "metadata.name",
},
},
},
} }
if errs := validateEnv(successCase); len(errs) != 0 { if errs := validateEnv(successCase); len(errs) != 0 {
t.Errorf("expected success: %v", errs) t.Errorf("expected success: %v", errs)
} }
errorCases := map[string][]api.EnvVar{ errorCases := []struct {
"zero-length name": {{Name: ""}}, name string
"name not a C identifier": {{Name: "a.b.c"}}, 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 { for _, tc := range errorCases {
if errs := validateEnv(v); len(errs) == 0 { if errs := validateEnv(tc.envs); len(errs) == 0 {
t.Errorf("expected failure for %s", k) t.Errorf("expected failure for %s", tc.name)
} else { } else {
for i := range errs { for i := range errs {
detail := errs[i].(*errors.ValidationError).Detail str := errs[i].(*errors.ValidationError).Error()
if detail != "" && detail != cIdentifierErrorMsg { if str != "" && str != tc.expectedError {
t.Errorf("%s: expected error detail either empty or %s, got %s", k, cIdentifierErrorMsg, detail) t.Errorf("%s: expected error detail either empty or %s, got %s", tc.name, tc.expectedError, str)
} }
} }
} }

19
pkg/fieldpath/doc.go Normal file
View 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

View 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)
}

View 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)
}
}
}

View File

@ -36,6 +36,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache" "github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/record" "github.com/GoogleCloudPlatform/kubernetes/pkg/client/record"
"github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider" "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider"
"github.com/GoogleCloudPlatform/kubernetes/pkg/fieldpath"
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/cadvisor" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/cadvisor"
kubecontainer "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/container" 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)) return nil, fmt.Errorf("impossible: cannot find the mounted volumes for pod %q", kubecontainer.GetPodFullName(pod))
} }
opts.Binds = makeBinds(container, vol) opts.Binds = makeBinds(container, vol)
opts.Envs, err = kl.makeEnvironmentVariables(pod.Namespace, container) opts.Envs, err = kl.makeEnvironmentVariables(pod, container)
if err != nil { if err != nil {
return nil, err 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. // 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 var result []string
// Note: These are added to the docker.Config, but are not included in the checksum computed // 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 // 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 // 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 // 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). // 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 { if err != nil {
return result, err return result, err
} }
@ -807,7 +808,13 @@ func (kl *Kubelet) makeEnvironmentVariables(ns string, container *api.Container)
// env vars. // env vars.
// TODO: remove this net line once all platforms use apiserver+Pods. // TODO: remove this net line once all platforms use apiserver+Pods.
delete(serviceEnv, value.Name) 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. // Append remaining service env vars.
@ -817,6 +824,33 @@ func (kl *Kubelet) makeEnvironmentVariables(ns string, container *api.Container)
return result, nil 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 // getClusterDNS returns a list of the DNS servers and a list of the DNS search
// domains of the cluster. // domains of the cluster.
func (kl *Kubelet) getClusterDNS(pod *api.Pod) ([]string, []string, error) { func (kl *Kubelet) getClusterDNS(pod *api.Pod) ([]string, []string, error) {

View File

@ -68,6 +68,8 @@ type TestKubelet struct {
fakeMirrorClient *fakeMirrorClient fakeMirrorClient *fakeMirrorClient
} }
const testKubeletHostname = "testnode"
func newTestKubelet(t *testing.T) *TestKubelet { func newTestKubelet(t *testing.T) *TestKubelet {
fakeDocker := &dockertools.FakeDockerClient{Errors: make(map[string]error), RemovedImages: util.StringSet{}} fakeDocker := &dockertools.FakeDockerClient{Errors: make(map[string]error), RemovedImages: util.StringSet{}}
fakeRecorder := &record.FakeRecorder{} fakeRecorder := &record.FakeRecorder{}
@ -2378,6 +2380,39 @@ func TestMakeEnvironmentVariables(t *testing.T) {
"KUBERNETES_RO_PORT_8087_TCP_ADDR=1.2.3.7"), "KUBERNETES_RO_PORT_8087_TCP_ADDR=1.2.3.7"),
21, 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 { for _, tc := range testCases {
@ -2390,7 +2425,14 @@ func TestMakeEnvironmentVariables(t *testing.T) {
kl.serviceLister = testServiceLister{services} 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 { if err != nil {
t.Errorf("[%v] Unexpected error: %v", tc.name, err) t.Errorf("[%v] Unexpected error: %v", tc.name, err)
} }

96
test/e2e/downward_api.go Normal file
View 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)
})
})