diff --git a/pkg/api/testing/fuzzer.go b/pkg/api/testing/fuzzer.go index 8908c3a32be..5fe1e082665 100644 --- a/pkg/api/testing/fuzzer.go +++ b/pkg/api/testing/fuzzer.go @@ -386,11 +386,16 @@ func FuzzerFor(t *testing.T, version schema.GroupVersion, src rand.Source) *fuzz } if c.RandBool() { c.Fuzz(&ev.ConfigMapRef) + } else { + c.Fuzz(&ev.SecretRef) } }, func(cm *api.ConfigMapEnvSource, c fuzz.Continue) { c.FuzzNoCustom(cm) // fuzz self without calling this function again }, + func(s *api.SecretEnvSource, c fuzz.Continue) { + c.FuzzNoCustom(s) // fuzz self without calling this function again + }, func(sc *api.SecurityContext, c fuzz.Continue) { c.FuzzNoCustom(sc) // fuzz self without calling this function again if c.RandBool() { diff --git a/pkg/api/types.go b/pkg/api/types.go index 7f0ccf97d0d..710168b306c 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -1142,6 +1142,9 @@ type EnvFromSource struct { // The ConfigMap to select from. //+optional ConfigMapRef *ConfigMapEnvSource + // The Secret to select from. + //+optional + SecretRef *SecretEnvSource } // ConfigMapEnvSource selects a ConfigMap to populate the environment @@ -1154,6 +1157,16 @@ type ConfigMapEnvSource struct { LocalObjectReference } +// SecretEnvSource selects a Secret to populate the environment +// variables with. +// +// The contents of the target Secret's Data field will represent the +// key-value pairs as environment variables. +type SecretEnvSource struct { + // The Secret to select from. + LocalObjectReference +} + // HTTPHeader describes a custom header to be used in HTTP probes type HTTPHeader struct { // The header field name diff --git a/pkg/api/v1/types.go b/pkg/api/v1/types.go index e3388f0f3eb..55af32adb40 100644 --- a/pkg/api/v1/types.go +++ b/pkg/api/v1/types.go @@ -1243,6 +1243,9 @@ type EnvFromSource struct { // The ConfigMap to select from // +optional ConfigMapRef *ConfigMapEnvSource `json:"configMapRef,omitempty" protobuf:"bytes,2,opt,name=configMapRef"` + // The Secret to select from + // +optional + SecretRef *SecretEnvSource `json:"secretRef,omitempty" protobuf:"bytes,3,opt,name=secretRef"` } // ConfigMapEnvSource selects a ConfigMap to populate the environment @@ -1255,6 +1258,16 @@ type ConfigMapEnvSource struct { LocalObjectReference `json:",inline" protobuf:"bytes,1,opt,name=localObjectReference"` } +// SecretEnvSource selects a Secret to populate the environment +// variables with. +// +// The contents of the target Secret's Data field will represent the +// key-value pairs as environment variables. +type SecretEnvSource struct { + // The Secret to select from. + LocalObjectReference `json:",inline" protobuf:"bytes,1,opt,name=localObjectReference"` +} + // HTTPHeader describes a custom header to be used in HTTP probes type HTTPHeader struct { // The header field name diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index b488cc28a4c..a17ee9639e6 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -1260,9 +1260,22 @@ func validateEnvFrom(vars []api.EnvFromSource, fldPath *field.Path) field.ErrorL allErrs = append(allErrs, field.Invalid(idxPath.Child("prefix"), ev.Prefix, msg)) } } + + numSources := 0 if ev.ConfigMapRef != nil { + numSources++ allErrs = append(allErrs, validateConfigMapEnvSource(ev.ConfigMapRef, idxPath.Child("configMapRef"))...) } + if ev.SecretRef != nil { + numSources++ + allErrs = append(allErrs, validateSecretEnvSource(ev.SecretRef, idxPath.Child("secretRef"))...) + } + + if numSources == 0 { + allErrs = append(allErrs, field.Invalid(fldPath, "", "must specify one of: `configMapRef` or `secretRef`")) + } else if numSources > 1 { + allErrs = append(allErrs, field.Invalid(fldPath, "", "may not have more than one field specified at a time")) + } } return allErrs } @@ -1275,6 +1288,14 @@ func validateConfigMapEnvSource(configMapSource *api.ConfigMapEnvSource, fldPath return allErrs } +func validateSecretEnvSource(secretSource *api.SecretEnvSource, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if len(secretSource.Name) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("name"), "")) + } + return allErrs +} + var validContainerResourceDivisorForCPU = sets.NewString("1m", "1") var validContainerResourceDivisorForMemory = sets.NewString("1", "1k", "1M", "1G", "1T", "1P", "1E", "1Ki", "1Mi", "1Gi", "1Ti", "1Pi", "1Ei") diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index f9265eb2d00..c9dbad75838 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -2285,6 +2285,17 @@ func TestValidateEnvFrom(t *testing.T) { LocalObjectReference: api.LocalObjectReference{Name: "abc"}, }, }, + { + SecretRef: &api.SecretEnvSource{ + LocalObjectReference: api.LocalObjectReference{Name: "abc"}, + }, + }, + { + Prefix: "pre_", + SecretRef: &api.SecretEnvSource{ + LocalObjectReference: api.LocalObjectReference{Name: "abc"}, + }, + }, } if errs := validateEnvFrom(successCase, field.NewPath("field")); len(errs) != 0 { t.Errorf("expected success: %v", errs) @@ -2316,6 +2327,46 @@ func TestValidateEnvFrom(t *testing.T) { }, expectedError: `field[0].prefix: Invalid value: "a.b": ` + idErrMsg, }, + { + name: "zero-length name", + envs: []api.EnvFromSource{ + { + SecretRef: &api.SecretEnvSource{ + LocalObjectReference: api.LocalObjectReference{Name: ""}}, + }, + }, + expectedError: "field[0].secretRef.name: Required value", + }, + { + name: "invalid prefix", + envs: []api.EnvFromSource{ + { + Prefix: "a.b", + SecretRef: &api.SecretEnvSource{ + LocalObjectReference: api.LocalObjectReference{Name: "abc"}}, + }, + }, + expectedError: `field[0].prefix: Invalid value: "a.b": ` + idErrMsg, + }, + { + name: "no refs", + envs: []api.EnvFromSource{ + {}, + }, + expectedError: "field: Invalid value: \"\": must specify one of: `configMapRef` or `secretRef`", + }, + { + name: "multiple refs", + envs: []api.EnvFromSource{ + { + SecretRef: &api.SecretEnvSource{ + LocalObjectReference: api.LocalObjectReference{Name: "abc"}}, + ConfigMapRef: &api.ConfigMapEnvSource{ + LocalObjectReference: api.LocalObjectReference{Name: "abc"}}, + }, + }, + expectedError: "field: Invalid value: \"\": may not have more than one field specified at a time", + }, } for _, tc := range errorCases { if errs := validateEnvFrom(tc.envs, field.NewPath("field")); len(errs) == 0 { diff --git a/pkg/kubectl/describe.go b/pkg/kubectl/describe.go index 01d949371a0..88141f09762 100644 --- a/pkg/kubectl/describe.go +++ b/pkg/kubectl/describe.go @@ -1052,10 +1052,19 @@ func describeContainerEnvFrom(container api.Container, resolverFn EnvVarResolver w.Write(LEVEL_2, "Environment Variables from:%s\n", none) for _, e := range container.EnvFrom { + from := "" + name := "" + if e.ConfigMapRef != nil { + from = "ConfigMap" + name = e.ConfigMapRef.Name + } else if e.SecretRef != nil { + from = "Secret" + name = e.SecretRef.Name + } if len(e.Prefix) == 0 { - w.Write(LEVEL_3, "%s\tConfigMap\n", e.ConfigMapRef.Name) + w.Write(LEVEL_3, "%s\t%s\n", name, from) } else { - w.Write(LEVEL_3, "%s\tConfigMap with prefix '%s'\n", e.ConfigMapRef.Name, e.Prefix) + w.Write(LEVEL_3, "%s\t%s with prefix '%s'\n", name, from, e.Prefix) } } } diff --git a/pkg/kubectl/describe_test.go b/pkg/kubectl/describe_test.go index e3cbeb8e02b..a10fb5b77b4 100644 --- a/pkg/kubectl/describe_test.go +++ b/pkg/kubectl/describe_test.go @@ -306,6 +306,24 @@ func TestDescribeContainers(t *testing.T) { }, expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image", "envname", "xyz", "a123\tConfigMap with prefix 'p_'"}, }, + { + container: api.Container{Name: "test", Image: "image", Env: []api.EnvVar{{Name: "envname", Value: "xyz"}}, EnvFrom: []api.EnvFromSource{{SecretRef: &api.SecretEnvSource{LocalObjectReference: api.LocalObjectReference{Name: "a123"}}}}}, + status: api.ContainerStatus{ + Name: "test", + Ready: true, + RestartCount: 7, + }, + expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image", "envname", "xyz", "a123\tSecret"}, + }, + { + container: api.Container{Name: "test", Image: "image", Env: []api.EnvVar{{Name: "envname", Value: "xyz"}}, EnvFrom: []api.EnvFromSource{{Prefix: "p_", SecretRef: &api.SecretEnvSource{LocalObjectReference: api.LocalObjectReference{Name: "a123"}}}}}, + status: api.ContainerStatus{ + Name: "test", + Ready: true, + RestartCount: 7, + }, + expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image", "envname", "xyz", "a123\tSecret with prefix 'p_'"}, + }, // Command { container: api.Container{Name: "test", Image: "image", Command: []string{"sleep", "1000"}}, diff --git a/pkg/kubelet/kubelet_pods.go b/pkg/kubelet/kubelet_pods.go index da53ca6afb8..8600fa1f793 100644 --- a/pkg/kubelet/kubelet_pods.go +++ b/pkg/kubelet/kubelet_pods.go @@ -418,13 +418,15 @@ func (kl *Kubelet) makeEnvironmentVariables(pod *v1.Pod, container *v1.Container var ( configMaps = make(map[string]*v1.ConfigMap) + secrets = make(map[string]*v1.Secret) tmpEnv = make(map[string]string) ) // Env will override EnvFrom variables. // Process EnvFrom first then allow Env to replace existing values. for _, envFrom := range container.EnvFrom { - if envFrom.ConfigMapRef != nil { + switch { + case envFrom.ConfigMapRef != nil: name := envFrom.ConfigMapRef.Name configMap, ok := configMaps[name] if !ok { @@ -432,12 +434,12 @@ func (kl *Kubelet) makeEnvironmentVariables(pod *v1.Pod, container *v1.Container return result, fmt.Errorf("Couldn't get configMap %v/%v, no kubeClient defined", pod.Namespace, name) } configMap, err = kl.kubeClient.Core().ConfigMaps(pod.Namespace).Get(name, metav1.GetOptions{}) - if err != nil { return result, err } configMaps[name] = configMap } + for k, v := range configMap.Data { if len(envFrom.Prefix) > 0 { k = envFrom.Prefix + k @@ -445,14 +447,31 @@ func (kl *Kubelet) makeEnvironmentVariables(pod *v1.Pod, container *v1.Container if errMsgs := utilvalidation.IsCIdentifier(k); len(errMsgs) != 0 { return result, fmt.Errorf("Invalid environment variable name, %v, from configmap %v/%v: %s", k, pod.Namespace, name, errMsgs[0]) } - // Accesses apiserver+Pods. - // So, the master may set service env vars, or kubelet may. In case both are doing - // it, we delete the key from the kubelet-generated ones so we don't have duplicate - // env vars. - // TODO: remove this next line once all platforms use apiserver+Pods. - delete(serviceEnv, k) tmpEnv[k] = v } + case envFrom.SecretRef != nil: + name := envFrom.SecretRef.Name + secret, ok := secrets[name] + if !ok { + if kl.kubeClient == nil { + return result, fmt.Errorf("Couldn't get secret %v/%v, no kubeClient defined", pod.Namespace, name) + } + secret, err = kl.kubeClient.Core().Secrets(pod.Namespace).Get(name, metav1.GetOptions{}) + if err != nil { + return result, err + } + secrets[name] = secret + } + + for k, v := range secret.Data { + if len(envFrom.Prefix) > 0 { + k = envFrom.Prefix + k + } + if errMsgs := utilvalidation.IsCIdentifier(k); len(errMsgs) != 0 { + return result, fmt.Errorf("Invalid environment variable name, %v, from secret %v/%v: %s", k, pod.Namespace, name, errMsgs[0]) + } + tmpEnv[k] = string(v) + } } } @@ -466,17 +485,9 @@ func (kl *Kubelet) makeEnvironmentVariables(pod *v1.Pod, container *v1.Container // 2. Create the container's environment in the order variables are declared // 3. Add remaining service environment vars var ( - secrets = make(map[string]*v1.Secret) mappingFunc = expansion.MappingFuncFor(tmpEnv, serviceEnv) ) for _, envVar := range container.Env { - // Accesses apiserver+Pods. - // So, the master may set service env vars, or kubelet may. In case both are doing - // it, we delete the key from the kubelet-generated ones so we don't have duplicate - // env vars. - // TODO: remove this next line once all platforms use apiserver+Pods. - delete(serviceEnv, envVar.Name) - runtimeVal := envVar.Value if runtimeVal != "" { // Step 1a: expand variable references @@ -548,7 +559,14 @@ func (kl *Kubelet) makeEnvironmentVariables(pod *v1.Pod, container *v1.Container // Append remaining service env vars. for k, v := range serviceEnv { - result = append(result, kubecontainer.EnvVar{Name: k, Value: v}) + // Accesses apiserver+Pods. + // So, the master may set service env vars, or kubelet may. In case both are doing + // it, we skip the key from the kubelet-generated ones so we don't have duplicate + // env vars. + // TODO: remove this next line once all platforms use apiserver+Pods. + if _, present := tmpEnv[k]; !present { + result = append(result, kubecontainer.EnvVar{Name: k, Value: v}) + } } return result, nil } diff --git a/pkg/kubelet/kubelet_pods_test.go b/pkg/kubelet/kubelet_pods_test.go index 1629110b574..1247e382c74 100644 --- a/pkg/kubelet/kubelet_pods_test.go +++ b/pkg/kubelet/kubelet_pods_test.go @@ -274,6 +274,7 @@ func TestMakeEnvironmentVariables(t *testing.T) { masterServiceNs string // the namespace to read master service info from nilLister bool // whether the lister should be nil configMap *v1.ConfigMap // an optional ConfigMap to pull from + secret *v1.Secret // an optional Secret to pull from expectedEnvs []kubecontainer.EnvVar // a set of expected environment vars expectedError bool // does the test fail }{ @@ -766,6 +767,160 @@ func TestMakeEnvironmentVariables(t *testing.T) { }, }, }, + { + name: "secret", + ns: "test1", + container: &v1.Container{ + EnvFrom: []v1.EnvFromSource{ + { + SecretRef: &v1.SecretEnvSource{LocalObjectReference: v1.LocalObjectReference{Name: "test-secret"}}, + }, + { + Prefix: "p_", + SecretRef: &v1.SecretEnvSource{LocalObjectReference: v1.LocalObjectReference{Name: "test-secret"}}, + }, + }, + Env: []v1.EnvVar{ + { + Name: "TEST_LITERAL", + Value: "test-test-test", + }, + { + Name: "EXPANSION_TEST", + Value: "$(REPLACE_ME)", + }, + { + Name: "DUPE_TEST", + Value: "ENV_VAR", + }, + }, + }, + masterServiceNs: "nothing", + nilLister: false, + secret: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test1", + Name: "test-secret", + }, + Data: map[string][]byte{ + "REPLACE_ME": []byte("FROM_SECRET"), + "DUPE_TEST": []byte("SECRET"), + }, + }, + expectedEnvs: []kubecontainer.EnvVar{ + { + Name: "TEST_LITERAL", + Value: "test-test-test", + }, + { + Name: "TEST_SERVICE_HOST", + Value: "1.2.3.3", + }, + { + Name: "TEST_SERVICE_PORT", + Value: "8083", + }, + { + Name: "TEST_PORT", + Value: "tcp://1.2.3.3:8083", + }, + { + Name: "TEST_PORT_8083_TCP", + Value: "tcp://1.2.3.3:8083", + }, + { + Name: "TEST_PORT_8083_TCP_PROTO", + Value: "tcp", + }, + { + Name: "TEST_PORT_8083_TCP_PORT", + Value: "8083", + }, + { + Name: "TEST_PORT_8083_TCP_ADDR", + Value: "1.2.3.3", + }, + { + Name: "REPLACE_ME", + Value: "FROM_SECRET", + }, + { + Name: "EXPANSION_TEST", + Value: "FROM_SECRET", + }, + { + Name: "DUPE_TEST", + Value: "ENV_VAR", + }, + { + Name: "p_REPLACE_ME", + Value: "FROM_SECRET", + }, + { + Name: "p_DUPE_TEST", + Value: "SECRET", + }, + }, + }, + { + name: "secret_missing", + ns: "test1", + container: &v1.Container{ + EnvFrom: []v1.EnvFromSource{ + {SecretRef: &v1.SecretEnvSource{LocalObjectReference: v1.LocalObjectReference{Name: "test-secret"}}}, + }, + }, + masterServiceNs: "nothing", + expectedError: true, + }, + { + name: "secret_invalid_keys", + ns: "test1", + container: &v1.Container{ + EnvFrom: []v1.EnvFromSource{ + {SecretRef: &v1.SecretEnvSource{LocalObjectReference: v1.LocalObjectReference{Name: "test-secret"}}}, + }, + }, + masterServiceNs: "nothing", + secret: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test1", + Name: "test-secret", + }, + Data: map[string][]byte{ + "1234": []byte("abc"), + }, + }, + expectedError: true, + }, + { + name: "secret_invalid_keys_valid", + ns: "test", + container: &v1.Container{ + EnvFrom: []v1.EnvFromSource{ + { + Prefix: "p_", + SecretRef: &v1.SecretEnvSource{LocalObjectReference: v1.LocalObjectReference{Name: "test-secret"}}, + }, + }, + }, + masterServiceNs: "", + secret: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test1", + Name: "test-secret", + }, + Data: map[string][]byte{ + "1234": []byte("abc"), + }, + }, + expectedEnvs: []kubecontainer.EnvVar{ + { + Name: "p_1234", + Value: "abc", + }, + }, + }, } for _, tc := range testCases { @@ -786,6 +941,14 @@ func TestMakeEnvironmentVariables(t *testing.T) { return true, tc.configMap, err }) + testKubelet.fakeKubeClient.AddReactor("get", "secrets", func(action core.Action) (bool, runtime.Object, error) { + var err error + if tc.secret == nil { + err = errors.New("no secret defined") + } + return true, tc.secret, err + }) + testPod := &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Namespace: tc.ns, diff --git a/test/e2e/common/secrets.go b/test/e2e/common/secrets.go index 7711175a500..f947cb5e5e4 100644 --- a/test/e2e/common/secrets.go +++ b/test/e2e/common/secrets.go @@ -193,8 +193,62 @@ var _ = framework.KubeDescribe("Secrets", func() { "SECRET_DATA=value-1", }) }) + + It("should be consumable via the environment [Conformance]", func() { + name := "secret-test-" + string(uuid.NewUUID()) + secret := newEnvFromSecret(f.Namespace.Name, name) + By(fmt.Sprintf("creating secret %v/%v", f.Namespace.Name, secret.Name)) + var err error + if secret, err = f.ClientSet.Core().Secrets(f.Namespace.Name).Create(secret); err != nil { + framework.Failf("unable to create test secret %s: %v", secret.Name, err) + } + + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-configmaps-" + string(uuid.NewUUID()), + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "env-test", + Image: "gcr.io/google_containers/busybox:1.24", + Command: []string{"sh", "-c", "env"}, + EnvFrom: []v1.EnvFromSource{ + { + SecretRef: &v1.SecretEnvSource{LocalObjectReference: v1.LocalObjectReference{Name: name}}, + }, + { + Prefix: "p_", + SecretRef: &v1.SecretEnvSource{LocalObjectReference: v1.LocalObjectReference{Name: name}}, + }, + }, + }, + }, + RestartPolicy: v1.RestartPolicyNever, + }, + } + + f.TestContainerOutput("consume secrets", pod, 0, []string{ + "data_1=value-1", "data_2=value-2", "data_3=value-3", + "p_data_1=value-1", "p_data_2=value-2", "p_data_3=value-3", + }) + }) }) +func newEnvFromSecret(namespace, name string) *v1.Secret { + return &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + Data: map[string][]byte{ + "data_1": []byte("value-1\n"), + "data_2": []byte("value-2\n"), + "data_3": []byte("value-3\n"), + }, + } +} + func secretForTest(namespace, name string) *v1.Secret { return &v1.Secret{ ObjectMeta: metav1.ObjectMeta{