diff --git a/pkg/api/testing/fuzzer.go b/pkg/api/testing/fuzzer.go index 774bda50e8e..8f02fb94521 100644 --- a/pkg/api/testing/fuzzer.go +++ b/pkg/api/testing/fuzzer.go @@ -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 diff --git a/pkg/api/types.go b/pkg/api/types.go index ccbd8d2957d..ee21168d474 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -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. diff --git a/pkg/api/v1beta1/conversion.go b/pkg/api/v1beta1/conversion.go index 257f3e352fc..b4edd19a70d 100644 --- a/pkg/api/v1beta1/conversion.go +++ b/pkg/api/v1beta1/conversion.go @@ -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 diff --git a/pkg/api/v1beta1/defaults.go b/pkg/api/v1beta1/defaults.go index 4d8a9b9cdfe..5b01435a71b 100644 --- a/pkg/api/v1beta1/defaults.go +++ b/pkg/api/v1beta1/defaults.go @@ -166,6 +166,11 @@ func init() { obj.ExternalID = obj.ID } }, + func(obj *ObjectFieldSelector) { + if obj.APIVersion == "" { + obj.APIVersion = "v1beta1" + } + }, ) } diff --git a/pkg/api/v1beta1/defaults_test.go b/pkg/api/v1beta1/defaults_test.go index 3f304d1a512..e932bb4a57b 100644 --- a/pkg/api/v1beta1/defaults_test.go +++ b/pkg/api/v1beta1/defaults_test.go @@ -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) + } +} diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index bf977cbac70..dd85b845ae3 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -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. diff --git a/pkg/api/v1beta2/defaults.go b/pkg/api/v1beta2/defaults.go index 27516e4b61a..305a01915b0 100644 --- a/pkg/api/v1beta2/defaults.go +++ b/pkg/api/v1beta2/defaults.go @@ -167,6 +167,11 @@ func init() { obj.ExternalID = obj.ID } }, + func(obj *ObjectFieldSelector) { + if obj.APIVersion == "" { + obj.APIVersion = "v1beta2" + } + }, ) } diff --git a/pkg/api/v1beta2/defaults_test.go b/pkg/api/v1beta2/defaults_test.go index 112a362c9e2..bb2d8f80012 100644 --- a/pkg/api/v1beta2/defaults_test.go +++ b/pkg/api/v1beta2/defaults_test.go @@ -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) + } +} diff --git a/pkg/api/v1beta2/types.go b/pkg/api/v1beta2/types.go index 8b86646ec3c..a3672bfa253 100644 --- a/pkg/api/v1beta2/types.go +++ b/pkg/api/v1beta2/types.go @@ -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. diff --git a/pkg/api/v1beta3/conversion.go b/pkg/api/v1beta3/conversion.go index e07f7c3672f..13bd54b051d 100644 --- a/pkg/api/v1beta3/conversion.go +++ b/pkg/api/v1beta3/conversion.go @@ -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 diff --git a/pkg/api/v1beta3/defaults.go b/pkg/api/v1beta3/defaults.go index a5a21b61a21..f2c5713409e 100644 --- a/pkg/api/v1beta3/defaults.go +++ b/pkg/api/v1beta3/defaults.go @@ -128,6 +128,11 @@ func init() { obj.Spec.ExternalID = obj.Name } }, + func(obj *ObjectFieldSelector) { + if obj.APIVersion == "" { + obj.APIVersion = "v1beta3" + } + }, ) } diff --git a/pkg/api/v1beta3/defaults_test.go b/pkg/api/v1beta3/defaults_test.go index 1d5e07b89fc..7b2c94f13a9 100644 --- a/pkg/api/v1beta3/defaults_test.go +++ b/pkg/api/v1beta3/defaults_test.go @@ -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) + } +} diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index 4c2a62a61c9..97ff351e50a 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -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. diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 9c0f97e0d68..078f384c723 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -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{} diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index 46e11fa048d..30b29e530bf 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -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) } } } diff --git a/pkg/fieldpath/doc.go b/pkg/fieldpath/doc.go new file mode 100644 index 00000000000..e7b6bba0d0d --- /dev/null +++ b/pkg/fieldpath/doc.go @@ -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 diff --git a/pkg/fieldpath/fieldpath.go b/pkg/fieldpath/fieldpath.go new file mode 100644 index 00000000000..cea520681a5 --- /dev/null +++ b/pkg/fieldpath/fieldpath.go @@ -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) +} diff --git a/pkg/fieldpath/fieldpath_test.go b/pkg/fieldpath/fieldpath_test.go new file mode 100644 index 00000000000..877acb81a26 --- /dev/null +++ b/pkg/fieldpath/fieldpath_test.go @@ -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) + } + } +} diff --git a/pkg/kubelet/kubelet.go b/pkg/kubelet/kubelet.go index e0ff522b2f4..0d8ce7a6d2c 100644 --- a/pkg/kubelet/kubelet.go +++ b/pkg/kubelet/kubelet.go @@ -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) { diff --git a/pkg/kubelet/kubelet_test.go b/pkg/kubelet/kubelet_test.go index 45e4358487b..1ea4250eb1b 100644 --- a/pkg/kubelet/kubelet_test.go +++ b/pkg/kubelet/kubelet_test.go @@ -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) } diff --git a/test/e2e/downward_api.go b/test/e2e/downward_api.go new file mode 100644 index 00000000000..282ddd701d2 --- /dev/null +++ b/test/e2e/downward_api.go @@ -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) + }) +})