diff --git a/pkg/api/testing/fuzzer.go b/pkg/api/testing/fuzzer.go index c9af9a2ac96..d73506f6c60 100644 --- a/pkg/api/testing/fuzzer.go +++ b/pkg/api/testing/fuzzer.go @@ -286,6 +286,10 @@ func FuzzerFor(t *testing.T, version schema.GroupVersion, src rand.Source) *fuzz func(s *api.SecretVolumeSource, c fuzz.Continue) { c.FuzzNoCustom(s) // fuzz self without calling this function again + if c.RandBool() { + opt := c.RandBool() + s.Optional = &opt + } // DefaultMode should always be set, it has a default // value and it is expected to be between 0 and 0777 var mode int32 @@ -296,6 +300,10 @@ func FuzzerFor(t *testing.T, version schema.GroupVersion, src rand.Source) *fuzz func(cm *api.ConfigMapVolumeSource, c fuzz.Continue) { c.FuzzNoCustom(cm) // fuzz self without calling this function again + if c.RandBool() { + opt := c.RandBool() + cm.Optional = &opt + } // DefaultMode should always be set, it has a default // value and it is expected to be between 0 and 0777 var mode int32 @@ -401,6 +409,10 @@ func FuzzerFor(t *testing.T, version schema.GroupVersion, src rand.Source) *fuzz }, func(cm *api.ConfigMapEnvSource, c fuzz.Continue) { c.FuzzNoCustom(cm) // fuzz self without calling this function again + if c.RandBool() { + opt := c.RandBool() + cm.Optional = &opt + } }, func(s *api.SecretEnvSource, c fuzz.Continue) { c.FuzzNoCustom(s) // fuzz self without calling this function again diff --git a/pkg/api/types.go b/pkg/api/types.go index bb7074b9326..f70088262aa 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -729,8 +729,8 @@ type SecretVolumeSource struct { // key and content is the value. If specified, the listed keys will be // projected into the specified paths, and unlisted keys will not be // present. If a key is specified which is not present in the Secret, - // the volume setup will error. Paths must be relative and may not contain - // the '..' path or start with '..'. + // the volume setup will error unless it is marked optional. Paths must be + // relative and may not contain the '..' path or start with '..'. // +optional Items []KeyToPath // Mode bits to use on created files by default. Must be a value between @@ -740,6 +740,9 @@ type SecretVolumeSource struct { // mode, like fsGroup, and the result can be other mode bits set. // +optional DefaultMode *int32 + // Specify whether the Secret or it's key must be defined + // +optional + Optional *bool } // Represents an NFS mount that lasts the lifetime of a pod. @@ -992,8 +995,8 @@ type ConfigMapVolumeSource struct { // key and content is the value. If specified, the listed keys will be // projected into the specified paths, and unlisted keys will not be // present. If a key is specified which is not present in the ConfigMap, - // the volume setup will error. Paths must be relative and may not contain - // the '..' path or start with '..'. + // the volume setup will error unless it is marked optional. Paths must be + // relative and may not contain the '..' path or start with '..'. // +optional Items []KeyToPath // Mode bits to use on created files by default. Must be a value between @@ -1003,6 +1006,9 @@ type ConfigMapVolumeSource struct { // mode, like fsGroup, and the result can be other mode bits set. // +optional DefaultMode *int32 + // Specify whether the ConfigMap or it's keys must be defined + // +optional + Optional *bool } // Maps a string key to a path within a volume. @@ -1124,6 +1130,9 @@ type ConfigMapKeySelector struct { LocalObjectReference // The key to select. Key string + // Specify whether the ConfigMap or it's key must be defined + // +optional + Optional *bool } // SecretKeySelector selects a key of a Secret. @@ -1132,6 +1141,9 @@ type SecretKeySelector struct { LocalObjectReference // The key of the secret to select from. Must be a valid secret key. Key string + // Specify whether the Secret or it's key must be defined + // +optional + Optional *bool } // EnvFromSource represents the source of a set of ConfigMaps @@ -1155,6 +1167,9 @@ type EnvFromSource struct { type ConfigMapEnvSource struct { // The ConfigMap to select from. LocalObjectReference + // Specify whether the ConfigMap must be defined + // +optional + Optional *bool } // SecretEnvSource selects a Secret to populate the environment @@ -1165,6 +1180,9 @@ type ConfigMapEnvSource struct { type SecretEnvSource struct { // The Secret to select from. LocalObjectReference + // Specify whether the Secret must be defined + // +optional + Optional *bool } // HTTPHeader describes a custom header to be used in HTTP probes diff --git a/pkg/api/v1/types.go b/pkg/api/v1/types.go index b38a55c6f0c..0d20931dfc1 100644 --- a/pkg/api/v1/types.go +++ b/pkg/api/v1/types.go @@ -924,8 +924,8 @@ type SecretVolumeSource struct { // key and content is the value. If specified, the listed keys will be // projected into the specified paths, and unlisted keys will not be // present. If a key is specified which is not present in the Secret, - // the volume setup will error. Paths must be relative and may not contain - // the '..' path or start with '..'. + // the volume setup will error unless it is marked optional. Paths must be + // relative and may not contain the '..' path or start with '..'. // +optional Items []KeyToPath `json:"items,omitempty" protobuf:"bytes,2,rep,name=items"` // Optional: mode bits to use on created files by default. Must be a @@ -935,6 +935,9 @@ type SecretVolumeSource struct { // mode, like fsGroup, and the result can be other mode bits set. // +optional DefaultMode *int32 `json:"defaultMode,omitempty" protobuf:"bytes,3,opt,name=defaultMode"` + // Specify whether the Secret or it's keys must be defined + // +optional + Optional *bool `json:"optional,omitempty" protobuf:"varint,4,opt,name=optional"` } const ( @@ -1081,8 +1084,8 @@ type ConfigMapVolumeSource struct { // key and content is the value. If specified, the listed keys will be // projected into the specified paths, and unlisted keys will not be // present. If a key is specified which is not present in the ConfigMap, - // the volume setup will error. Paths must be relative and may not contain - // the '..' path or start with '..'. + // the volume setup will error unless it is marked optional. Paths must be + // relative and may not contain the '..' path or start with '..'. // +optional Items []KeyToPath `json:"items,omitempty" protobuf:"bytes,2,rep,name=items"` // Optional: mode bits to use on created files by default. Must be a @@ -1092,6 +1095,9 @@ type ConfigMapVolumeSource struct { // mode, like fsGroup, and the result can be other mode bits set. // +optional DefaultMode *int32 `json:"defaultMode,omitempty" protobuf:"varint,3,opt,name=defaultMode"` + // Specify whether the ConfigMap or it's keys must be defined + // +optional + Optional *bool `json:"optional,omitempty" protobuf:"varint,4,opt,name=optional"` } const ( @@ -1225,6 +1231,9 @@ type ConfigMapKeySelector struct { LocalObjectReference `json:",inline" protobuf:"bytes,1,opt,name=localObjectReference"` // The key to select. Key string `json:"key" protobuf:"bytes,2,opt,name=key"` + // Specify whether the ConfigMap or it's key must be defined + // +optional + Optional *bool `json:"optional,omitempty" protobuf:"varint,3,opt,name=optional"` } // SecretKeySelector selects a key of a Secret. @@ -1233,6 +1242,9 @@ type SecretKeySelector struct { LocalObjectReference `json:",inline" protobuf:"bytes,1,opt,name=localObjectReference"` // The key of the secret to select from. Must be a valid secret key. Key string `json:"key" protobuf:"bytes,2,opt,name=key"` + // Specify whether the Secret or it's key must be defined + // +optional + Optional *bool `json:"optional,omitempty" protobuf:"varint,3,opt,name=optional"` } // EnvFromSource represents the source of a set of ConfigMaps @@ -1256,6 +1268,9 @@ type EnvFromSource struct { type ConfigMapEnvSource struct { // The ConfigMap to select from. LocalObjectReference `json:",inline" protobuf:"bytes,1,opt,name=localObjectReference"` + // Specify whether the ConfigMap must be defined + // +optional + Optional *bool `json:"optional,omitempty" protobuf:"varint,2,opt,name=optional"` } // SecretEnvSource selects a Secret to populate the environment @@ -1266,6 +1281,9 @@ type ConfigMapEnvSource struct { type SecretEnvSource struct { // The Secret to select from. LocalObjectReference `json:",inline" protobuf:"bytes,1,opt,name=localObjectReference"` + // Specify whether the Secret must be defined + // +optional + Optional *bool `json:"optional,omitempty" protobuf:"varint,2,opt,name=optional"` } // HTTPHeader describes a custom header to be used in HTTP probes diff --git a/pkg/kubectl/describe.go b/pkg/kubectl/describe.go index 88141f09762..c3eaac38d8f 100644 --- a/pkg/kubectl/describe.go +++ b/pkg/kubectl/describe.go @@ -650,13 +650,19 @@ func printGitRepoVolumeSource(git *api.GitRepoVolumeSource, w *PrefixWriter) { } func printSecretVolumeSource(secret *api.SecretVolumeSource, w *PrefixWriter) { + optional := secret.Optional != nil && *secret.Optional w.Write(LEVEL_2, "Type:\tSecret (a volume populated by a Secret)\n"+ - " SecretName:\t%v\n", secret.SecretName) + " SecretName:\t%v\n", + " Optional:\t%v\n", + secret.SecretName, optional) } func printConfigMapVolumeSource(configMap *api.ConfigMapVolumeSource, w *PrefixWriter) { + optional := configMap.Optional != nil && *configMap.Optional w.Write(LEVEL_2, "Type:\tConfigMap (a volume populated by a ConfigMap)\n"+ - " Name:\t%v\n", configMap.Name) + " Name:\t%v\n"+ + " Optional:\t%v\n", + configMap.Name, optional) } func printNFSVolumeSource(nfs *api.NFSVolumeSource, w *PrefixWriter) { @@ -1037,9 +1043,11 @@ func describeContainerEnvVars(container api.Container, resolverFn EnvVarResolver } w.Write(LEVEL_3, "%s:\t%s (%s)\n", e.Name, valueFrom, resource) case e.ValueFrom.SecretKeyRef != nil: - w.Write(LEVEL_3, "%s:\t\n", e.Name, e.ValueFrom.SecretKeyRef.Key, e.ValueFrom.SecretKeyRef.Name) + optional := e.ValueFrom.SecretKeyRef.Optional != nil && *e.ValueFrom.SecretKeyRef.Optional + w.Write(LEVEL_3, "%s:\t\tOptional: %t\n", e.Name, e.ValueFrom.SecretKeyRef.Key, e.ValueFrom.SecretKeyRef.Name, optional) case e.ValueFrom.ConfigMapKeyRef != nil: - w.Write(LEVEL_3, "%s:\t\n", e.Name, e.ValueFrom.ConfigMapKeyRef.Key, e.ValueFrom.ConfigMapKeyRef.Name) + optional := e.ValueFrom.ConfigMapKeyRef.Optional != nil && *e.ValueFrom.ConfigMapKeyRef.Optional + w.Write(LEVEL_3, "%s:\t\tOptional: %t\n", e.Name, e.ValueFrom.ConfigMapKeyRef.Key, e.ValueFrom.ConfigMapKeyRef.Name, optional) } } } @@ -1054,17 +1062,20 @@ func describeContainerEnvFrom(container api.Container, resolverFn EnvVarResolver for _, e := range container.EnvFrom { from := "" name := "" + optional := false if e.ConfigMapRef != nil { from = "ConfigMap" name = e.ConfigMapRef.Name + optional = e.ConfigMapRef.Optional != nil && *e.ConfigMapRef.Optional } else if e.SecretRef != nil { from = "Secret" name = e.SecretRef.Name + optional = e.SecretRef.Optional != nil && *e.SecretRef.Optional } if len(e.Prefix) == 0 { - w.Write(LEVEL_3, "%s\t%s\n", name, from) + w.Write(LEVEL_3, "%s\t%s\tOptional: %t\n", name, from, optional) } else { - w.Write(LEVEL_3, "%s\t%s with prefix '%s'\n", name, from, e.Prefix) + w.Write(LEVEL_3, "%s\t%s with prefix '%s'\tOptional: %t\n", name, from, e.Prefix, optional) } } } diff --git a/pkg/kubectl/describe_test.go b/pkg/kubectl/describe_test.go index a10fb5b77b4..d1573551983 100644 --- a/pkg/kubectl/describe_test.go +++ b/pkg/kubectl/describe_test.go @@ -201,6 +201,7 @@ func VerifyDatesInOrder( } func TestDescribeContainers(t *testing.T) { + trueVal := true testCases := []struct { container api.Container status api.ContainerStatus @@ -295,7 +296,7 @@ func TestDescribeContainers(t *testing.T) { Ready: true, RestartCount: 7, }, - expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image", "envname", "xyz", "a123\tConfigMap"}, + expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image", "envname", "xyz", "a123\tConfigMap\tOptional: false"}, }, { container: api.Container{Name: "test", Image: "image", Env: []api.EnvVar{{Name: "envname", Value: "xyz"}}, EnvFrom: []api.EnvFromSource{{Prefix: "p_", ConfigMapRef: &api.ConfigMapEnvSource{LocalObjectReference: api.LocalObjectReference{Name: "a123"}}}}}, @@ -304,16 +305,25 @@ func TestDescribeContainers(t *testing.T) { Ready: true, RestartCount: 7, }, - expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image", "envname", "xyz", "a123\tConfigMap with prefix 'p_'"}, + expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image", "envname", "xyz", "a123\tConfigMap with prefix 'p_'\tOptional: false"}, }, { - 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"}}}}}, + container: api.Container{Name: "test", Image: "image", Env: []api.EnvVar{{Name: "envname", Value: "xyz"}}, EnvFrom: []api.EnvFromSource{{ConfigMapRef: &api.ConfigMapEnvSource{Optional: &trueVal, 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"}, + expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image", "envname", "xyz", "a123\tConfigMap\tOptional: true"}, + }, + { + 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"}, Optional: &trueVal}}}}, + 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\tOptional: true"}, }, { 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"}}}}}, @@ -322,7 +332,7 @@ func TestDescribeContainers(t *testing.T) { Ready: true, RestartCount: 7, }, - expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image", "envname", "xyz", "a123\tSecret with prefix 'p_'"}, + expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image", "envname", "xyz", "a123\tSecret with prefix 'p_'\tOptional: false"}, }, // Command { diff --git a/pkg/kubelet/kubelet_pods.go b/pkg/kubelet/kubelet_pods.go index cdbb631d87a..6303e5db756 100644 --- a/pkg/kubelet/kubelet_pods.go +++ b/pkg/kubelet/kubelet_pods.go @@ -33,6 +33,7 @@ import ( "time" "github.com/golang/glog" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" @@ -427,14 +428,20 @@ func (kl *Kubelet) makeEnvironmentVariables(pod *v1.Pod, container *v1.Container for _, envFrom := range container.EnvFrom { switch { case envFrom.ConfigMapRef != nil: - name := envFrom.ConfigMapRef.Name + cm := envFrom.ConfigMapRef + name := cm.Name configMap, ok := configMaps[name] if !ok { if kl.kubeClient == nil { return result, fmt.Errorf("Couldn't get configMap %v/%v, no kubeClient defined", pod.Namespace, name) } + optional := cm.Optional != nil && *cm.Optional configMap, err = kl.kubeClient.Core().ConfigMaps(pod.Namespace).Get(name, metav1.GetOptions{}) if err != nil { + if errors.IsNotFound(err) && optional { + // ignore error when marked optional + continue + } return result, err } configMaps[name] = configMap @@ -450,14 +457,20 @@ func (kl *Kubelet) makeEnvironmentVariables(pod *v1.Pod, container *v1.Container tmpEnv[k] = v } case envFrom.SecretRef != nil: - name := envFrom.SecretRef.Name + s := envFrom.SecretRef + name := s.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) } + optional := s.Optional != nil && *s.Optional secret, err = kl.kubeClient.Core().Secrets(pod.Namespace).Get(name, metav1.GetOptions{}) if err != nil { + if errors.IsNotFound(err) && optional { + // ignore error when marked optional + continue + } return result, err } secrets[name] = secret @@ -510,8 +523,10 @@ func (kl *Kubelet) makeEnvironmentVariables(pod *v1.Pod, container *v1.Container return result, err } case envVar.ValueFrom.ConfigMapKeyRef != nil: - name := envVar.ValueFrom.ConfigMapKeyRef.Name - key := envVar.ValueFrom.ConfigMapKeyRef.Key + cm := envVar.ValueFrom.ConfigMapKeyRef + name := cm.Name + key := cm.Key + optional := cm.Optional != nil && *cm.Optional configMap, ok := configMaps[name] if !ok { if kl.kubeClient == nil { @@ -519,17 +534,26 @@ func (kl *Kubelet) makeEnvironmentVariables(pod *v1.Pod, container *v1.Container } configMap, err = kl.kubeClient.Core().ConfigMaps(pod.Namespace).Get(name, metav1.GetOptions{}) if err != nil { + if errors.IsNotFound(err) && optional { + // ignore error when marked optional + continue + } return result, err } configMaps[name] = configMap } runtimeVal, ok = configMap.Data[key] if !ok { + if optional { + continue + } return result, fmt.Errorf("Couldn't find key %v in ConfigMap %v/%v", key, pod.Namespace, name) } case envVar.ValueFrom.SecretKeyRef != nil: - name := envVar.ValueFrom.SecretKeyRef.Name - key := envVar.ValueFrom.SecretKeyRef.Key + s := envVar.ValueFrom.SecretKeyRef + name := s.Name + key := s.Key + optional := s.Optional != nil && *s.Optional secret, ok := secrets[name] if !ok { if kl.kubeClient == nil { @@ -537,17 +561,30 @@ func (kl *Kubelet) makeEnvironmentVariables(pod *v1.Pod, container *v1.Container } secret, err = kl.secretManager.GetSecret(pod.Namespace, name) if err != nil { + if errors.IsNotFound(err) && optional { + // ignore error when marked optional + continue + } return result, err } secrets[name] = secret } runtimeValBytes, ok := secret.Data[key] if !ok { + if optional { + continue + } return result, fmt.Errorf("Couldn't find key %v in Secret %v/%v", key, pod.Namespace, name) } runtimeVal = string(runtimeValBytes) } } + // 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) tmpEnv[envVar.Name] = runtimeVal } diff --git a/pkg/kubelet/kubelet_pods_test.go b/pkg/kubelet/kubelet_pods_test.go index ea73e8125aa..6d6e7bd5cf4 100644 --- a/pkg/kubelet/kubelet_pods_test.go +++ b/pkg/kubelet/kubelet_pods_test.go @@ -26,6 +26,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" @@ -257,6 +258,7 @@ func buildService(name, namespace, clusterIP, protocol string, port int) *v1.Ser } func TestMakeEnvironmentVariables(t *testing.T) { + trueVal := true services := []*v1.Service{ buildService("kubernetes", v1.NamespaceDefault, "1.2.3.1", "TCP", 8081), buildService("test", "test1", "1.2.3.3", "TCP", 8083), @@ -616,6 +618,106 @@ func TestMakeEnvironmentVariables(t *testing.T) { }, }, }, + { + name: "configmapkeyref_missing_optional", + ns: "test", + container: &v1.Container{ + Env: []v1.EnvVar{ + { + Name: "POD_NAME", + ValueFrom: &v1.EnvVarSource{ + ConfigMapKeyRef: &v1.ConfigMapKeySelector{ + LocalObjectReference: v1.LocalObjectReference{Name: "missing-config-map"}, + Key: "key", + Optional: &trueVal, + }, + }, + }, + }, + }, + masterServiceNs: "nothing", + expectedEnvs: nil, + }, + { + name: "configmapkeyref_missing_key_optional", + ns: "test", + container: &v1.Container{ + Env: []v1.EnvVar{ + { + Name: "POD_NAME", + ValueFrom: &v1.EnvVarSource{ + ConfigMapKeyRef: &v1.ConfigMapKeySelector{ + LocalObjectReference: v1.LocalObjectReference{Name: "test-config-map"}, + Key: "key", + Optional: &trueVal, + }, + }, + }, + }, + }, + masterServiceNs: "nothing", + nilLister: true, + configMap: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test1", + Name: "test-configmap", + }, + Data: map[string]string{ + "a": "b", + }, + }, + expectedEnvs: nil, + }, + { + name: "secretkeyref_missing_optional", + ns: "test", + container: &v1.Container{ + Env: []v1.EnvVar{ + { + Name: "POD_NAME", + ValueFrom: &v1.EnvVarSource{ + SecretKeyRef: &v1.SecretKeySelector{ + LocalObjectReference: v1.LocalObjectReference{Name: "missing-secret"}, + Key: "key", + Optional: &trueVal, + }, + }, + }, + }, + }, + masterServiceNs: "nothing", + expectedEnvs: nil, + }, + { + name: "secretkeyref_missing_key_optional", + ns: "test", + container: &v1.Container{ + Env: []v1.EnvVar{ + { + Name: "POD_NAME", + ValueFrom: &v1.EnvVarSource{ + SecretKeyRef: &v1.SecretKeySelector{ + LocalObjectReference: v1.LocalObjectReference{Name: "test-secret"}, + Key: "key", + Optional: &trueVal, + }, + }, + }, + }, + }, + masterServiceNs: "nothing", + nilLister: true, + secret: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test1", + Name: "test-secret", + }, + Data: map[string][]byte{ + "a": []byte("b"), + }, + }, + expectedEnvs: nil, + }, { name: "configmap", ns: "test1", @@ -722,6 +824,19 @@ func TestMakeEnvironmentVariables(t *testing.T) { masterServiceNs: "nothing", expectedError: true, }, + { + name: "configmap_missing_optional", + ns: "test", + container: &v1.Container{ + EnvFrom: []v1.EnvFromSource{ + {ConfigMapRef: &v1.ConfigMapEnvSource{ + Optional: &trueVal, + LocalObjectReference: v1.LocalObjectReference{Name: "missing-config-map"}}}, + }, + }, + masterServiceNs: "nothing", + expectedEnvs: nil, + }, { name: "configmap_invalid_keys", ns: "test1", @@ -876,6 +991,19 @@ func TestMakeEnvironmentVariables(t *testing.T) { masterServiceNs: "nothing", expectedError: true, }, + { + name: "secret_missing_optional", + ns: "test", + container: &v1.Container{ + EnvFrom: []v1.EnvFromSource{ + {SecretRef: &v1.SecretEnvSource{ + LocalObjectReference: v1.LocalObjectReference{Name: "missing-secret"}, + Optional: &trueVal}}, + }, + }, + masterServiceNs: "nothing", + expectedEnvs: nil, + }, { name: "secret_invalid_keys", ns: "test1", @@ -940,10 +1068,17 @@ func TestMakeEnvironmentVariables(t *testing.T) { testKubelet.fakeKubeClient.AddReactor("get", "configmaps", func(action core.Action) (bool, runtime.Object, error) { var err error if tc.configMap == nil { - err = errors.New("no configmap defined") + err = apierrors.NewNotFound(action.GetResource().GroupResource(), "configmap-name") } 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 = apierrors.NewNotFound(action.GetResource().GroupResource(), "secret-name") + } + return true, tc.secret, err + }) testKubelet.fakeKubeClient.AddReactor("get", "secrets", func(action core.Action) (bool, runtime.Object, error) { var err error diff --git a/pkg/kubelet/kubelet_test.go b/pkg/kubelet/kubelet_test.go index 0ffcd416d6d..7ad533fbeb1 100644 --- a/pkg/kubelet/kubelet_test.go +++ b/pkg/kubelet/kubelet_test.go @@ -173,8 +173,12 @@ func newTestKubeletWithImageList( kubelet.cadvisor = mockCadvisor fakeMirrorClient := podtest.NewFakeMirrorClient() - fakeSecretManager := secret.NewFakeManager() - kubelet.podManager = kubepod.NewBasicPodManager(fakeMirrorClient, fakeSecretManager) + secretManager, err := secret.NewSimpleSecretManager(kubelet.kubeClient) + if err != nil { + t.Fatalf("can't create a secret manager: %v", err) + } + kubelet.secretManager = secretManager + kubelet.podManager = kubepod.NewBasicPodManager(fakeMirrorClient, kubelet.secretManager) kubelet.statusManager = status.NewManager(fakeKubeClient, kubelet.podManager) kubelet.containerRefManager = kubecontainer.NewRefManager() diskSpaceManager, err := newDiskSpaceManager(mockCadvisor, DiskSpacePolicy{}) @@ -249,7 +253,7 @@ func newTestKubeletWithImageList( plug := &volumetest.FakeVolumePlugin{PluginName: "fake", Host: nil} kubelet.volumePluginMgr, err = - NewInitializedVolumePluginMgr(kubelet, fakeSecretManager, []volume.VolumePlugin{plug}) + NewInitializedVolumePluginMgr(kubelet, kubelet.secretManager, []volume.VolumePlugin{plug}) require.NoError(t, err, "Failed to initialize VolumePluginMgr") kubelet.mounter = &mount.FakeMounter{} diff --git a/pkg/volume/configmap/configmap.go b/pkg/volume/configmap/configmap.go index 48a389c9f07..b6b231a2e32 100644 --- a/pkg/volume/configmap/configmap.go +++ b/pkg/volume/configmap/configmap.go @@ -20,6 +20,7 @@ import ( "fmt" "github.com/golang/glog" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/kubernetes/pkg/api/v1" @@ -170,10 +171,19 @@ func (b *configMapVolumeMounter) SetUpAt(dir string, fsGroup *int64) error { return fmt.Errorf("Cannot setup configMap volume %v because kube client is not configured", b.volName) } + optional := b.source.Optional != nil && *b.source.Optional configMap, err := kubeClient.Core().ConfigMaps(b.pod.Namespace).Get(b.source.Name, metav1.GetOptions{}) if err != nil { - glog.Errorf("Couldn't get configMap %v/%v: %v", b.pod.Namespace, b.source.Name, err) - return err + if !(errors.IsNotFound(err) && optional) { + glog.Errorf("Couldn't get configMap %v/%v: %v", b.pod.Namespace, b.source.Name, err) + return err + } + configMap = &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: b.pod.Namespace, + Name: b.source.Name, + }, + } } totalBytes := totalBytes(configMap) @@ -183,7 +193,7 @@ func (b *configMapVolumeMounter) SetUpAt(dir string, fsGroup *int64) error { len(configMap.Data), totalBytes) - payload, err := makePayload(b.source.Items, configMap, b.source.DefaultMode) + payload, err := makePayload(b.source.Items, configMap, b.source.DefaultMode, optional) if err != nil { return err } @@ -210,7 +220,7 @@ func (b *configMapVolumeMounter) SetUpAt(dir string, fsGroup *int64) error { return nil } -func makePayload(mappings []v1.KeyToPath, configMap *v1.ConfigMap, defaultMode *int32) (map[string]volumeutil.FileProjection, error) { +func makePayload(mappings []v1.KeyToPath, configMap *v1.ConfigMap, defaultMode *int32, optional bool) (map[string]volumeutil.FileProjection, error) { if defaultMode == nil { return nil, fmt.Errorf("No defaultMode used, not even the default value for it") } @@ -228,6 +238,9 @@ func makePayload(mappings []v1.KeyToPath, configMap *v1.ConfigMap, defaultMode * for _, ktp := range mappings { content, ok := configMap.Data[ktp.Key] if !ok { + if optional { + continue + } err_msg := "references non-existent config key" glog.Errorf(err_msg) return nil, fmt.Errorf(err_msg) diff --git a/pkg/volume/configmap/configmap_test.go b/pkg/volume/configmap/configmap_test.go index b1e122a6080..80fa9b1109e 100644 --- a/pkg/volume/configmap/configmap_test.go +++ b/pkg/volume/configmap/configmap_test.go @@ -43,6 +43,7 @@ func TestMakePayload(t *testing.T) { mappings []v1.KeyToPath configMap *v1.ConfigMap mode int32 + optional bool payload map[string]util.FileProjection success bool }{ @@ -215,10 +216,29 @@ func TestMakePayload(t *testing.T) { }, success: true, }, + { + name: "optional non existent key", + mappings: []v1.KeyToPath{ + { + Key: "zab", + Path: "path/to/foo.txt", + }, + }, + configMap: &v1.ConfigMap{ + Data: map[string]string{ + "foo": "foo", + "bar": "bar", + }, + }, + mode: 0644, + optional: true, + payload: map[string]util.FileProjection{}, + success: true, + }, } for _, tc := range cases { - actualPayload, err := makePayload(tc.mappings, tc.configMap, &tc.mode) + actualPayload, err := makePayload(tc.mappings, tc.configMap, &tc.mode, tc.optional) if err != nil && tc.success { t.Errorf("%v: unexpected failure making payload: %v", tc.name, err) continue @@ -388,6 +408,143 @@ func TestPluginReboot(t *testing.T) { doTestCleanAndTeardown(plugin, testPodUID, testVolumeName, volumePath, t) } +func TestPluginOptional(t *testing.T) { + var ( + testPodUID = types.UID("test_pod_uid") + testVolumeName = "test_volume_name" + testNamespace = "test_configmap_namespace" + testName = "test_configmap_name" + trueVal = true + + volumeSpec = volumeSpec(testVolumeName, testName, 0644) + client = fake.NewSimpleClientset() + pluginMgr = volume.VolumePluginMgr{} + tempDir, host = newTestHost(t, client) + ) + volumeSpec.VolumeSource.ConfigMap.Optional = &trueVal + + defer os.RemoveAll(tempDir) + pluginMgr.InitPlugins(ProbeVolumePlugins(), host) + + plugin, err := pluginMgr.FindPluginByName(configMapPluginName) + if err != nil { + t.Errorf("Can't find the plugin by name") + } + + pod := &v1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, UID: testPodUID}} + mounter, err := plugin.NewMounter(volume.NewSpecFromVolume(volumeSpec), pod, volume.VolumeOptions{}) + if err != nil { + t.Errorf("Failed to make a new Mounter: %v", err) + } + if mounter == nil { + t.Errorf("Got a nil Mounter") + } + + vName, err := plugin.GetVolumeName(volume.NewSpecFromVolume(volumeSpec)) + if err != nil { + t.Errorf("Failed to GetVolumeName: %v", err) + } + if vName != "test_volume_name/test_configmap_name" { + t.Errorf("Got unexpect VolumeName %v", vName) + } + + volumePath := mounter.GetPath() + if !strings.HasSuffix(volumePath, fmt.Sprintf("pods/test_pod_uid/volumes/kubernetes.io~configmap/test_volume_name")) { + t.Errorf("Got unexpected path: %s", volumePath) + } + + fsGroup := int64(1001) + err = mounter.SetUp(&fsGroup) + if err != nil { + t.Errorf("Failed to setup volume: %v", err) + } + if _, err := os.Stat(volumePath); err != nil { + if os.IsNotExist(err) { + t.Errorf("SetUp() failed, volume path not created: %s", volumePath) + } else { + t.Errorf("SetUp() failed: %v", err) + } + } + + infos, err := ioutil.ReadDir(volumePath) + if err != nil { + t.Fatalf("couldn't find volume path, %s", volumePath) + } + if len(infos) != 0 { + t.Errorf("empty directory, %s, not found", volumePath) + } + doTestCleanAndTeardown(plugin, testPodUID, testVolumeName, volumePath, t) +} + +func TestPluginKeysOptional(t *testing.T) { + var ( + testPodUID = types.UID("test_pod_uid") + testVolumeName = "test_volume_name" + testNamespace = "test_configmap_namespace" + testName = "test_configmap_name" + trueVal = true + + volumeSpec = volumeSpec(testVolumeName, testName, 0644) + configMap = configMap(testNamespace, testName) + client = fake.NewSimpleClientset(&configMap) + pluginMgr = volume.VolumePluginMgr{} + tempDir, host = newTestHost(t, client) + ) + volumeSpec.VolumeSource.ConfigMap.Items = []v1.KeyToPath{ + {Key: "data-1", Path: "data-1"}, + {Key: "data-2", Path: "data-2"}, + {Key: "data-3", Path: "data-3"}, + {Key: "missing", Path: "missing"}, + } + volumeSpec.VolumeSource.ConfigMap.Optional = &trueVal + + defer os.RemoveAll(tempDir) + pluginMgr.InitPlugins(ProbeVolumePlugins(), host) + + plugin, err := pluginMgr.FindPluginByName(configMapPluginName) + if err != nil { + t.Errorf("Can't find the plugin by name") + } + + pod := &v1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, UID: testPodUID}} + mounter, err := plugin.NewMounter(volume.NewSpecFromVolume(volumeSpec), pod, volume.VolumeOptions{}) + if err != nil { + t.Errorf("Failed to make a new Mounter: %v", err) + } + if mounter == nil { + t.Errorf("Got a nil Mounter") + } + + vName, err := plugin.GetVolumeName(volume.NewSpecFromVolume(volumeSpec)) + if err != nil { + t.Errorf("Failed to GetVolumeName: %v", err) + } + if vName != "test_volume_name/test_configmap_name" { + t.Errorf("Got unexpect VolumeName %v", vName) + } + + volumePath := mounter.GetPath() + if !strings.HasSuffix(volumePath, fmt.Sprintf("pods/test_pod_uid/volumes/kubernetes.io~configmap/test_volume_name")) { + t.Errorf("Got unexpected path: %s", volumePath) + } + + fsGroup := int64(1001) + err = mounter.SetUp(&fsGroup) + if err != nil { + t.Errorf("Failed to setup volume: %v", err) + } + if _, err := os.Stat(volumePath); err != nil { + if os.IsNotExist(err) { + t.Errorf("SetUp() failed, volume path not created: %s", volumePath) + } else { + t.Errorf("SetUp() failed: %v", err) + } + } + + doTestConfigMapDataInVolume(volumePath, configMap, t) + doTestCleanAndTeardown(plugin, testPodUID, testVolumeName, volumePath, t) +} + func volumeSpec(volumeName, configMapName string, defaultMode int32) *v1.Volume { return &v1.Volume{ Name: volumeName, diff --git a/pkg/volume/secret/secret.go b/pkg/volume/secret/secret.go index 9d35e8918f7..f3af74dba46 100644 --- a/pkg/volume/secret/secret.go +++ b/pkg/volume/secret/secret.go @@ -22,6 +22,8 @@ import ( "runtime" "github.com/golang/glog" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/kubernetes/pkg/api/v1" ioutil "k8s.io/kubernetes/pkg/util/io" @@ -191,10 +193,19 @@ func (b *secretVolumeMounter) SetUpAt(dir string, fsGroup *int64) error { return err } + optional := b.source.Optional != nil && *b.source.Optional secret, err := b.getSecret(b.pod.Namespace, b.source.SecretName) if err != nil { - glog.Errorf("Couldn't get secret %v/%v", b.pod.Namespace, b.source.SecretName) - return err + if !(errors.IsNotFound(err) && optional) { + glog.Errorf("Couldn't get secret %v/%v", b.pod.Namespace, b.source.SecretName) + return err + } + secret = &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: b.pod.Namespace, + Name: b.source.SecretName, + }, + } } totalBytes := totalSecretBytes(secret) @@ -204,7 +215,7 @@ func (b *secretVolumeMounter) SetUpAt(dir string, fsGroup *int64) error { len(secret.Data), totalBytes) - payload, err := makePayload(b.source.Items, secret, b.source.DefaultMode) + payload, err := makePayload(b.source.Items, secret, b.source.DefaultMode, optional) if err != nil { return err } @@ -231,7 +242,7 @@ func (b *secretVolumeMounter) SetUpAt(dir string, fsGroup *int64) error { return nil } -func makePayload(mappings []v1.KeyToPath, secret *v1.Secret, defaultMode *int32) (map[string]volumeutil.FileProjection, error) { +func makePayload(mappings []v1.KeyToPath, secret *v1.Secret, defaultMode *int32, optional bool) (map[string]volumeutil.FileProjection, error) { if defaultMode == nil { return nil, fmt.Errorf("No defaultMode used, not even the default value for it") } @@ -249,6 +260,9 @@ func makePayload(mappings []v1.KeyToPath, secret *v1.Secret, defaultMode *int32) for _, ktp := range mappings { content, ok := secret.Data[ktp.Key] if !ok { + if optional { + continue + } err_msg := "references non-existent secret key" glog.Errorf(err_msg) return nil, fmt.Errorf(err_msg) diff --git a/pkg/volume/secret/secret_test.go b/pkg/volume/secret/secret_test.go index 368df2ff220..ee7e83fd75e 100644 --- a/pkg/volume/secret/secret_test.go +++ b/pkg/volume/secret/secret_test.go @@ -46,6 +46,7 @@ func TestMakePayload(t *testing.T) { mappings []v1.KeyToPath secret *v1.Secret mode int32 + optional bool payload map[string]util.FileProjection success bool }{ @@ -218,10 +219,29 @@ func TestMakePayload(t *testing.T) { }, success: true, }, + { + name: "optional non existent key", + mappings: []v1.KeyToPath{ + { + Key: "zab", + Path: "path/to/foo.txt", + }, + }, + secret: &v1.Secret{ + Data: map[string][]byte{ + "foo": []byte("foo"), + "bar": []byte("bar"), + }, + }, + mode: 0644, + optional: true, + payload: map[string]util.FileProjection{}, + success: true, + }, } for _, tc := range cases { - actualPayload, err := makePayload(tc.mappings, tc.secret, &tc.mode) + actualPayload, err := makePayload(tc.mappings, tc.secret, &tc.mode, tc.optional) if err != nil && tc.success { t.Errorf("%v: unexpected failure making payload: %v", tc.name, err) continue @@ -398,6 +418,154 @@ func TestPluginReboot(t *testing.T) { doTestCleanAndTeardown(plugin, testPodUID, testVolumeName, volumePath, t) } +func TestPluginOptional(t *testing.T) { + var ( + testPodUID = types.UID("test_pod_uid") + testVolumeName = "test_volume_name" + testNamespace = "test_secret_namespace" + testName = "test_secret_name" + trueVal = true + + volumeSpec = volumeSpec(testVolumeName, testName, 0644) + client = fake.NewSimpleClientset() + pluginMgr = volume.VolumePluginMgr{} + rootDir, host = newTestHost(t, client) + ) + volumeSpec.Secret.Optional = &trueVal + defer os.RemoveAll(rootDir) + pluginMgr.InitPlugins(ProbeVolumePlugins(), host) + + plugin, err := pluginMgr.FindPluginByName(secretPluginName) + if err != nil { + t.Errorf("Can't find the plugin by name") + } + + pod := &v1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, UID: testPodUID}} + mounter, err := plugin.NewMounter(volume.NewSpecFromVolume(volumeSpec), pod, volume.VolumeOptions{}) + if err != nil { + t.Errorf("Failed to make a new Mounter: %v", err) + } + if mounter == nil { + t.Errorf("Got a nil Mounter") + } + + volumePath := mounter.GetPath() + if !strings.HasSuffix(volumePath, fmt.Sprintf("pods/test_pod_uid/volumes/kubernetes.io~secret/test_volume_name")) { + t.Errorf("Got unexpected path: %s", volumePath) + } + + err = mounter.SetUp(nil) + if err != nil { + t.Errorf("Failed to setup volume: %v", err) + } + if _, err := os.Stat(volumePath); err != nil { + if os.IsNotExist(err) { + t.Errorf("SetUp() failed, volume path not created: %s", volumePath) + } else { + t.Errorf("SetUp() failed: %v", err) + } + } + + // secret volume should create its own empty wrapper path + podWrapperMetadataDir := fmt.Sprintf("%v/pods/test_pod_uid/plugins/kubernetes.io~empty-dir/wrapped_test_volume_name", rootDir) + + if _, err := os.Stat(podWrapperMetadataDir); err != nil { + if os.IsNotExist(err) { + t.Errorf("SetUp() failed, empty-dir wrapper path is not created: %s", podWrapperMetadataDir) + } else { + t.Errorf("SetUp() failed: %v", err) + } + } + + infos, err := ioutil.ReadDir(volumePath) + if err != nil { + t.Fatalf("couldn't find volume path, %s", volumePath) + } + if len(infos) != 0 { + t.Errorf("empty directory, %s, not found", volumePath) + } + + defer doTestCleanAndTeardown(plugin, testPodUID, testVolumeName, volumePath, t) +} + +func TestPluginOptionalKeys(t *testing.T) { + var ( + testPodUID = types.UID("test_pod_uid") + testVolumeName = "test_volume_name" + testNamespace = "test_secret_namespace" + testName = "test_secret_name" + trueVal = true + + volumeSpec = volumeSpec(testVolumeName, testName, 0644) + secret = secret(testNamespace, testName) + client = fake.NewSimpleClientset(&secret) + pluginMgr = volume.VolumePluginMgr{} + rootDir, host = newTestHost(t, client) + ) + volumeSpec.VolumeSource.Secret.Items = []v1.KeyToPath{ + {Key: "data-1", Path: "data-1"}, + {Key: "data-2", Path: "data-2"}, + {Key: "data-3", Path: "data-3"}, + {Key: "missing", Path: "missing"}, + } + volumeSpec.Secret.Optional = &trueVal + defer os.RemoveAll(rootDir) + pluginMgr.InitPlugins(ProbeVolumePlugins(), host) + + plugin, err := pluginMgr.FindPluginByName(secretPluginName) + if err != nil { + t.Errorf("Can't find the plugin by name") + } + + pod := &v1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, UID: testPodUID}} + mounter, err := plugin.NewMounter(volume.NewSpecFromVolume(volumeSpec), pod, volume.VolumeOptions{}) + if err != nil { + t.Errorf("Failed to make a new Mounter: %v", err) + } + if mounter == nil { + t.Errorf("Got a nil Mounter") + } + + volumePath := mounter.GetPath() + if !strings.HasSuffix(volumePath, fmt.Sprintf("pods/test_pod_uid/volumes/kubernetes.io~secret/test_volume_name")) { + t.Errorf("Got unexpected path: %s", volumePath) + } + + err = mounter.SetUp(nil) + if err != nil { + t.Errorf("Failed to setup volume: %v", err) + } + if _, err := os.Stat(volumePath); err != nil { + if os.IsNotExist(err) { + t.Errorf("SetUp() failed, volume path not created: %s", volumePath) + } else { + t.Errorf("SetUp() failed: %v", err) + } + } + + // secret volume should create its own empty wrapper path + podWrapperMetadataDir := fmt.Sprintf("%v/pods/test_pod_uid/plugins/kubernetes.io~empty-dir/wrapped_test_volume_name", rootDir) + + if _, err := os.Stat(podWrapperMetadataDir); err != nil { + if os.IsNotExist(err) { + t.Errorf("SetUp() failed, empty-dir wrapper path is not created: %s", podWrapperMetadataDir) + } else { + t.Errorf("SetUp() failed: %v", err) + } + } + doTestSecretDataInVolume(volumePath, secret, t) + defer doTestCleanAndTeardown(plugin, testPodUID, testVolumeName, volumePath, t) + + // Metrics only supported on linux + metrics, err := mounter.GetMetrics() + if runtime.GOOS == "linux" { + assert.NotEmpty(t, metrics) + assert.NoError(t, err) + } else { + t.Skipf("Volume metrics not supported on %s", runtime.GOOS) + } +} + func volumeSpec(volumeName, secretName string, defaultMode int32) *v1.Volume { return &v1.Volume{ Name: volumeName, diff --git a/test/e2e/common/configmap.go b/test/e2e/common/configmap.go index 02bb3be510e..8217bdd0038 100644 --- a/test/e2e/common/configmap.go +++ b/test/e2e/common/configmap.go @@ -19,6 +19,7 @@ package common import ( "fmt" "os" + "path" "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -154,6 +155,189 @@ var _ = framework.KubeDescribe("ConfigMap", func() { Eventually(pollLogs, podLogTimeout, framework.Poll).Should(ContainSubstring("value-2")) }) + It("optional updates should be reflected in volume [Conformance] [Volume]", func() { + + // We may have to wait or a full sync period to elapse before the + // Kubelet projects the update into the volume and the container picks + // it up. This timeout is based on the default Kubelet sync period (1 + // minute) plus additional time for fudge factor. + const podLogTimeout = 300 * time.Second + trueVal := true + + volumeMountPath := "/etc/configmap-volumes" + + deleteName := "cm-test-opt-del-" + string(uuid.NewUUID()) + deleteContainerName := "delcm-volume-test" + deleteVolumeName := "deletecm-volume" + deleteConfigMap := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: f.Namespace.Name, + Name: deleteName, + }, + Data: map[string]string{ + "data-1": "value-1", + }, + } + + updateName := "cm-test-opt-upd-" + string(uuid.NewUUID()) + updateContainerName := "updcm-volume-test" + updateVolumeName := "updatecm-volume" + updateConfigMap := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: f.Namespace.Name, + Name: updateName, + }, + Data: map[string]string{ + "data-1": "value-1", + }, + } + + createName := "cm-test-opt-create-" + string(uuid.NewUUID()) + createContainerName := "createcm-volume-test" + createVolumeName := "createcm-volume" + createConfigMap := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: f.Namespace.Name, + Name: createName, + }, + Data: map[string]string{ + "data-1": "value-1", + }, + } + + By(fmt.Sprintf("Creating configMap with name %s", deleteConfigMap.Name)) + var err error + if deleteConfigMap, err = f.ClientSet.Core().ConfigMaps(f.Namespace.Name).Create(deleteConfigMap); err != nil { + framework.Failf("unable to create test configMap %s: %v", deleteConfigMap.Name, err) + } + + By(fmt.Sprintf("Creating configMap with name %s", updateConfigMap.Name)) + if updateConfigMap, err = f.ClientSet.Core().ConfigMaps(f.Namespace.Name).Create(updateConfigMap); err != nil { + framework.Failf("unable to create test configMap %s: %v", updateConfigMap.Name, err) + } + + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-configmaps-" + string(uuid.NewUUID()), + }, + Spec: v1.PodSpec{ + Volumes: []v1.Volume{ + { + Name: deleteVolumeName, + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: deleteName, + }, + Optional: &trueVal, + }, + }, + }, + { + Name: updateVolumeName, + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: updateName, + }, + Optional: &trueVal, + }, + }, + }, + { + Name: createVolumeName, + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: createName, + }, + Optional: &trueVal, + }, + }, + }, + }, + Containers: []v1.Container{ + { + Name: deleteContainerName, + Image: "gcr.io/google_containers/mounttest:0.7", + Command: []string{"/mt", "--break_on_expected_content=false", "--retry_time=120", "--file_content_in_loop=/etc/configmap-volumes/delete/data-1"}, + VolumeMounts: []v1.VolumeMount{ + { + Name: deleteVolumeName, + MountPath: path.Join(volumeMountPath, "delete"), + ReadOnly: true, + }, + }, + }, + { + Name: updateContainerName, + Image: "gcr.io/google_containers/mounttest:0.7", + Command: []string{"/mt", "--break_on_expected_content=false", "--retry_time=120", "--file_content_in_loop=/etc/configmap-volumes/update/data-3"}, + VolumeMounts: []v1.VolumeMount{ + { + Name: updateVolumeName, + MountPath: path.Join(volumeMountPath, "update"), + ReadOnly: true, + }, + }, + }, + { + Name: createContainerName, + Image: "gcr.io/google_containers/mounttest:0.7", + Command: []string{"/mt", "--break_on_expected_content=false", "--retry_time=120", "--file_content_in_loop=/etc/configmap-volumes/create/data-1"}, + VolumeMounts: []v1.VolumeMount{ + { + Name: createVolumeName, + MountPath: path.Join(volumeMountPath, "create"), + ReadOnly: true, + }, + }, + }, + }, + RestartPolicy: v1.RestartPolicyNever, + }, + } + By("Creating the pod") + f.PodClient().CreateSync(pod) + + pollCreateLogs := func() (string, error) { + return framework.GetPodLogs(f.ClientSet, f.Namespace.Name, pod.Name, createContainerName) + } + Eventually(pollCreateLogs, podLogTimeout, framework.Poll).Should(ContainSubstring("Error reading file /etc/configmap-volumes/create/data-1")) + + pollUpdateLogs := func() (string, error) { + return framework.GetPodLogs(f.ClientSet, f.Namespace.Name, pod.Name, updateContainerName) + } + Eventually(pollUpdateLogs, podLogTimeout, framework.Poll).Should(ContainSubstring("Error reading file /etc/configmap-volumes/update/data-3")) + + pollDeleteLogs := func() (string, error) { + return framework.GetPodLogs(f.ClientSet, f.Namespace.Name, pod.Name, deleteContainerName) + } + Eventually(pollDeleteLogs, podLogTimeout, framework.Poll).Should(ContainSubstring("value-1")) + + By(fmt.Sprintf("Deleting configmap %v", deleteConfigMap.Name)) + err = f.ClientSet.Core().ConfigMaps(f.Namespace.Name).Delete(deleteConfigMap.Name, &v1.DeleteOptions{}) + Expect(err).NotTo(HaveOccurred(), "Failed to delete configmap %q in namespace %q", deleteConfigMap.Name, f.Namespace.Name) + + By(fmt.Sprintf("Updating configmap %v", updateConfigMap.Name)) + updateConfigMap.ResourceVersion = "" // to force update + delete(updateConfigMap.Data, "data-1") + updateConfigMap.Data["data-3"] = "value-3" + _, err = f.ClientSet.Core().ConfigMaps(f.Namespace.Name).Update(updateConfigMap) + Expect(err).NotTo(HaveOccurred(), "Failed to update configmap %q in namespace %q", updateConfigMap.Name, f.Namespace.Name) + + By(fmt.Sprintf("Creating configMap with name %s", createConfigMap.Name)) + if createConfigMap, err = f.ClientSet.Core().ConfigMaps(f.Namespace.Name).Create(createConfigMap); err != nil { + framework.Failf("unable to create test configMap %s: %v", createConfigMap.Name, err) + } + + By("waiting to observe update in volume") + + Eventually(pollCreateLogs, podLogTimeout, framework.Poll).Should(ContainSubstring("value-1")) + Eventually(pollUpdateLogs, podLogTimeout, framework.Poll).Should(ContainSubstring("value-3")) + Eventually(pollDeleteLogs, podLogTimeout, framework.Poll).Should(ContainSubstring("Error reading file /etc/configmap-volumes/delete/data-1")) + }) + It("should be consumable via environment variable [Conformance]", func() { name := "configmap-test-" + string(uuid.NewUUID()) configMap := newConfigMap(f, name) diff --git a/test/e2e/common/secrets.go b/test/e2e/common/secrets.go index f947cb5e5e4..676e6e10c91 100644 --- a/test/e2e/common/secrets.go +++ b/test/e2e/common/secrets.go @@ -19,6 +19,8 @@ package common import ( "fmt" "os" + "path" + "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/kubernetes/pkg/api/v1" @@ -26,6 +28,7 @@ import ( "k8s.io/kubernetes/test/e2e/framework" . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" ) var _ = framework.KubeDescribe("Secrets", func() { @@ -150,6 +153,183 @@ var _ = framework.KubeDescribe("Secrets", func() { }) }) + It("optional updates should be reflected in volume [Conformance] [Volume]", func() { + + // We may have to wait or a full sync period to elapse before the + // Kubelet projects the update into the volume and the container picks + // it up. This timeout is based on the default Kubelet sync period (1 + // minute) plus additional time for fudge factor. + const podLogTimeout = 300 * time.Second + trueVal := true + + volumeMountPath := "/etc/secret-volumes" + + deleteName := "s-test-opt-del-" + string(uuid.NewUUID()) + deleteContainerName := "dels-volume-test" + deleteVolumeName := "deletes-volume" + deleteSecret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: f.Namespace.Name, + Name: deleteName, + }, + Data: map[string][]byte{ + "data-1": []byte("value-1"), + }, + } + + updateName := "s-test-opt-upd-" + string(uuid.NewUUID()) + updateContainerName := "upds-volume-test" + updateVolumeName := "updates-volume" + updateSecret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: f.Namespace.Name, + Name: updateName, + }, + Data: map[string][]byte{ + "data-1": []byte("value-1"), + }, + } + + createName := "s-test-opt-create-" + string(uuid.NewUUID()) + createContainerName := "creates-volume-test" + createVolumeName := "creates-volume" + createSecret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: f.Namespace.Name, + Name: createName, + }, + Data: map[string][]byte{ + "data-1": []byte("value-1"), + }, + } + + By(fmt.Sprintf("Creating secret with name %s", deleteSecret.Name)) + var err error + if deleteSecret, err = f.ClientSet.Core().Secrets(f.Namespace.Name).Create(deleteSecret); err != nil { + framework.Failf("unable to create test secret %s: %v", deleteSecret.Name, err) + } + + By(fmt.Sprintf("Creating secret with name %s", updateSecret.Name)) + if updateSecret, err = f.ClientSet.Core().Secrets(f.Namespace.Name).Create(updateSecret); err != nil { + framework.Failf("unable to create test secret %s: %v", updateSecret.Name, err) + } + + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-secrets-" + string(uuid.NewUUID()), + }, + Spec: v1.PodSpec{ + Volumes: []v1.Volume{ + { + Name: deleteVolumeName, + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + SecretName: deleteName, + Optional: &trueVal, + }, + }, + }, + { + Name: updateVolumeName, + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + SecretName: updateName, + Optional: &trueVal, + }, + }, + }, + { + Name: createVolumeName, + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + SecretName: createName, + Optional: &trueVal, + }, + }, + }, + }, + Containers: []v1.Container{ + { + Name: deleteContainerName, + Image: "gcr.io/google_containers/mounttest:0.7", + Command: []string{"/mt", "--break_on_expected_content=false", "--retry_time=120", "--file_content_in_loop=/etc/secret-volumes/delete/data-1"}, + VolumeMounts: []v1.VolumeMount{ + { + Name: deleteVolumeName, + MountPath: path.Join(volumeMountPath, "delete"), + ReadOnly: true, + }, + }, + }, + { + Name: updateContainerName, + Image: "gcr.io/google_containers/mounttest:0.7", + Command: []string{"/mt", "--break_on_expected_content=false", "--retry_time=120", "--file_content_in_loop=/etc/secret-volumes/update/data-3"}, + VolumeMounts: []v1.VolumeMount{ + { + Name: updateVolumeName, + MountPath: path.Join(volumeMountPath, "update"), + ReadOnly: true, + }, + }, + }, + { + Name: createContainerName, + Image: "gcr.io/google_containers/mounttest:0.7", + Command: []string{"/mt", "--break_on_expected_content=false", "--retry_time=120", "--file_content_in_loop=/etc/secret-volumes/create/data-1"}, + VolumeMounts: []v1.VolumeMount{ + { + Name: createVolumeName, + MountPath: path.Join(volumeMountPath, "create"), + ReadOnly: true, + }, + }, + }, + }, + RestartPolicy: v1.RestartPolicyNever, + }, + } + By("Creating the pod") + f.PodClient().CreateSync(pod) + + pollCreateLogs := func() (string, error) { + return framework.GetPodLogs(f.ClientSet, f.Namespace.Name, pod.Name, createContainerName) + } + Eventually(pollCreateLogs, podLogTimeout, framework.Poll).Should(ContainSubstring("Error reading file /etc/secret-volumes/create/data-1")) + + pollUpdateLogs := func() (string, error) { + return framework.GetPodLogs(f.ClientSet, f.Namespace.Name, pod.Name, updateContainerName) + } + Eventually(pollUpdateLogs, podLogTimeout, framework.Poll).Should(ContainSubstring("Error reading file /etc/secret-volumes/update/data-3")) + + pollDeleteLogs := func() (string, error) { + return framework.GetPodLogs(f.ClientSet, f.Namespace.Name, pod.Name, deleteContainerName) + } + Eventually(pollDeleteLogs, podLogTimeout, framework.Poll).Should(ContainSubstring("value-1")) + + By(fmt.Sprintf("Deleting secret %v", deleteSecret.Name)) + err = f.ClientSet.Core().Secrets(f.Namespace.Name).Delete(deleteSecret.Name, &v1.DeleteOptions{}) + Expect(err).NotTo(HaveOccurred(), "Failed to delete secret %q in namespace %q", deleteSecret.Name, f.Namespace.Name) + + By(fmt.Sprintf("Updating secret %v", updateSecret.Name)) + updateSecret.ResourceVersion = "" // to force update + delete(updateSecret.Data, "data-1") + updateSecret.Data["data-3"] = []byte("value-3") + _, err = f.ClientSet.Core().Secrets(f.Namespace.Name).Update(updateSecret) + Expect(err).NotTo(HaveOccurred(), "Failed to update secret %q in namespace %q", updateSecret.Name, f.Namespace.Name) + + By(fmt.Sprintf("Creating secret with name %s", createSecret.Name)) + if createSecret, err = f.ClientSet.Core().Secrets(f.Namespace.Name).Create(createSecret); err != nil { + framework.Failf("unable to create test secret %s: %v", createSecret.Name, err) + } + + By("waiting to observe update in volume") + + Eventually(pollCreateLogs, podLogTimeout, framework.Poll).Should(ContainSubstring("value-1")) + Eventually(pollUpdateLogs, podLogTimeout, framework.Poll).Should(ContainSubstring("value-3")) + Eventually(pollDeleteLogs, podLogTimeout, framework.Poll).Should(ContainSubstring("Error reading file /etc/secret-volumes/delete/data-1")) + }) + It("should be consumable from pods in env vars [Conformance]", func() { name := "secret-test-" + string(uuid.NewUUID()) secret := secretForTest(f.Namespace.Name, name)