From f839db4692c9a20b3dd2175426a717d152c1ec33 Mon Sep 17 00:00:00 2001 From: Paulo Pires Date: Sat, 15 Dec 2018 11:01:40 +0000 Subject: [PATCH] tests: envvars processing Signed-off-by: Paulo Pires --- vkubelet/env_internal_test.go | 554 ++++++++++++++++++++++++++++++++++ 1 file changed, 554 insertions(+) create mode 100644 vkubelet/env_internal_test.go diff --git a/vkubelet/env_internal_test.go b/vkubelet/env_internal_test.go new file mode 100644 index 000000000..dc44bf18d --- /dev/null +++ b/vkubelet/env_internal_test.go @@ -0,0 +1,554 @@ +package vkubelet + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + + "github.com/virtual-kubelet/virtual-kubelet/manager" +) + +const ( + // invalidKey1 is a key that cannot be used as the name of an environment variable (since it starts with a digit). + invalidKey1 = "1INVALID" + // invalidKey2 is a key that cannot be used as the name of an environment variable (since it starts with a digit). + invalidKey2 = "2INVALID" + // invalidKey3 is a key that cannot be used as the name of an environment variable (since it starts with a digit). + invalidKey3 = "3INVALID" + // keyFoo is a key that can be used as the name of an environment variable. + keyFoo = "FOO" + // keyBar is a key that can be used as the name of an environment variable. + keyBar = "BAR" + // keyBaz is a key that can be used as the name of an environment variable. + keyBaz = "BAZ" + // namespace is the namespace to which mock resources used in the tests belong. + namespace = "foo" + // prefixConfigMap1 is the prefix used in ".envFrom" fields that reference "config-map-1". + prefixConfigMap1 = "FROM_CONFIGMAP_1_" + // prefixConfigMap2 is the prefix used in ".envFrom" fields that reference "config-map-2". + prefixConfigMap2 = "FROM_CONFIGMAP_2_" + // prefixSecret1 is the prefix used in ".envFrom" fields that reference "secret-1". + prefixSecret1 = "FROM_SECRET_1_" + // prefixSecret1 is the prefix used in ".envFrom" fields that reference "secret-1". + prefixSecret2 = "FROM_SECRET_2_" +) + +var ( + // bFalse represents the "false" value. + // Used so we can take its address when a pointer to a bool is required. + bFalse = false + // bFalse represents the "true" value. + // Used so we can take its address when a pointer to a bool is required. + bTrue = true + // configMap1 is a configmap containing a single key, valid as the name of an environment variable. + configMap1 = mockConfigMap(namespace, "configmap-1", map[string]string{ + keyFoo: "__foo__", + }) + // configMap2 is a configmap containing a single key, valid as the name of an environment variable. + configMap2 = mockConfigMap(namespace, "configmap-2", map[string]string{ + keyBar: "__bar__", + }) + // configMap3 is a configmap containing a single key, valid as the name of an environment variable. + configMap3 = mockConfigMap(namespace, "configmap-2", map[string]string{ + keyFoo: "__foo__", + keyBar: "__bar__", + }) + // invalidConfigMap1 is a configmap containing two keys, one of which is invalid as the name of an environment variable. + invalidConfigMap1 = mockConfigMap(namespace, "invalid-configmap-1", map[string]string{ + keyFoo: "__foo__", + invalidKey1: "will-be-skipped", + invalidKey2: "will-be-skipped", + }) + // secret1 is a secret containing a single key, valid as the name of an environment variable. + secret1 = mockSecret(namespace, "secret-1", map[string]string{ + keyBaz: "__baz__", + }) + // secret2 is a secret containing a single key, valid as the name of an environment variable. + secret2 = mockSecret(namespace, "secret-2", map[string]string{ + keyFoo: "__foo__", + }) + // invalidSecret1 is a secret containing two keys, one of which is invalid as the name for an environment variable. + invalidSecret1 = mockSecret(namespace, "invalid-secret-1", map[string]string{ + invalidKey3: "will-be-skipped", + keyBaz: "__baz__", + }) +) + +// TestPopulatePodWithInitContainers populates the environment of a pod with four containers (two init containers, two containers). +// Then, it checks that the resulting environment for each container contains the expected environment variables. +func TestPopulatePodWithInitContainers(t *testing.T) { + rm := fakeResourceManager(configMap1, configMap2, secret1, secret2) + er := fakeEventRecorder() + + // Create a pod object having two init containers and two containers. + // The containers' environment is to be populated from two configmaps and two secrets. + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "pod-0", + }, + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{ + { + EnvFrom: []corev1.EnvFromSource{ + { + Prefix: prefixConfigMap1, + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: configMap1.Name, + }, + }, + }, + }, + }, + { + EnvFrom: []corev1.EnvFromSource{ + { + Prefix: prefixConfigMap2, + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: configMap2.Name, + }, + }, + }, + }, + }, + }, + Containers: []corev1.Container{ + { + EnvFrom: []corev1.EnvFromSource{ + { + Prefix: prefixSecret1, + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: secret1.Name, + }, + }, + }, + }, + }, + { + EnvFrom: []corev1.EnvFromSource{ + { + Prefix: prefixSecret2, + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: secret2.Name, + }, + }, + }, + }, + }, + }, + }, + } + + // Populate the pod's environment. + err := populateEnvironmentVariables(context.Background(), pod, rm, er) + assert.NoError(t, err) + + // Make sure that all the containers' environments contain all the expected keys and values. + assert.ElementsMatch(t, pod.Spec.InitContainers[0].Env, []corev1.EnvVar{ + { + Name: prefixConfigMap1 + keyFoo, + Value: configMap1.Data[keyFoo], + }, + }) + assert.ElementsMatch(t, pod.Spec.InitContainers[1].Env, []corev1.EnvVar{ + { + Name: prefixConfigMap2 + keyBar, + Value: configMap2.Data[keyBar], + }, + }) + assert.ElementsMatch(t, pod.Spec.Containers[0].Env, []corev1.EnvVar{ + { + Name: prefixSecret1 + keyBaz, + Value: string(secret1.Data[keyBaz]), + }, + }) + assert.ElementsMatch(t, pod.Spec.Containers[1].Env, []corev1.EnvVar{ + { + Name: prefixSecret2 + keyFoo, + Value: string(secret2.Data[keyFoo]), + }, + }) +} + +// TestEnvFromTwoConfigMapsAndOneSecret populates the environment of a container from two configmaps and one secret. +// Then, it checks that the resulting environment contains all the expected environment variables and values. +func TestEnvFromTwoConfigMapsAndOneSecret(t *testing.T) { + rm := fakeResourceManager(configMap1, configMap2, secret1) + er := fakeEventRecorder() + + // Create a pod object having a single container. + // The container's environment is to be populated from two configmaps and one secret. + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "pod-0", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + EnvFrom: []corev1.EnvFromSource{ + { + Prefix: prefixConfigMap1, + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: configMap1.Name, + }, + }, + }, + { + Prefix: prefixConfigMap2, + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: configMap2.Name, + }, + }, + }, + { + Prefix: prefixSecret1, + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: secret1.Name, + }, + }, + }, + }, + }, + }, + }, + } + + // Populate the container's environment. + err := populateContainerEnvironment(context.Background(), pod, &pod.Spec.Containers[0], rm, er) + assert.NoError(t, err) + + // Make sure that the container's environment contains all the expected keys and values. + assert.ElementsMatch(t, pod.Spec.Containers[0].Env, []corev1.EnvVar{ + { + Name: prefixConfigMap1 + keyFoo, + Value: configMap1.Data[keyFoo], + }, + { + Name: prefixConfigMap2 + keyBar, + Value: configMap2.Data[keyBar], + }, + { + Name: prefixSecret1 + keyBaz, + Value: string(secret1.Data[keyBaz]), + }, + }) + + // Make sure that no events have been recorded, as the configmaps and secrets are valid. + assert.Len(t, er.Events, 0) +} + +// TestEnvFromConfigMapAndSecretWithInvalidKeys populates the environment of a container from a configmap and a secret containing invalid keys. +// Then, it checks that the resulting environment contains all the expected environment variables and values, and that the invalid keys have been skipped. +func TestEnvFromConfigMapAndSecretWithInvalidKeys(t *testing.T) { + rm := fakeResourceManager(invalidConfigMap1, invalidSecret1) + er := fakeEventRecorder() + + // Create a pod object having a single container. + // The container's environment is to be populated from a configmap and a secret, both of which have some invalid keys. + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "pod-0", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + EnvFrom: []corev1.EnvFromSource{ + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: invalidConfigMap1.Name, + }, + }, + }, + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: invalidSecret1.Name, + }, + }, + }, + }, + }, + }, + }, + } + + // Populate the pods's environment. + err := populateEnvironmentVariables(context.Background(), pod, rm, er) + assert.NoError(t, err) + + // Make sure that the container's environment has two variables (corresponding to the single valid key in both the configmap and the secret). + assert.ElementsMatch(t, pod.Spec.Containers[0].Env, []corev1.EnvVar{ + { + Name: keyFoo, + Value: invalidConfigMap1.Data[keyFoo], + }, + { + Name: keyBaz, + Value: string(invalidSecret1.Data[keyBaz]), + }, + }) + + // Make sure that two events have been received (one for the configmap and one for the secret). + assert.Len(t, er.Events, 2) + + // Grab the first event (which should correspond to the configmap) and make sure it has the correct reason and message. + event1 := <-er.Events + assert.Contains(t, event1, ReasonInvalidEnvironmentVariableNames) + assert.Contains(t, event1, invalidKey1) + assert.Contains(t, event1, invalidKey2) + + // Grab the second event (which should correspond to the secret) and make sure it has the correct reason and message. + event2 := <-er.Events + assert.Contains(t, event2, ReasonInvalidEnvironmentVariableNames) + assert.Contains(t, event2, invalidKey3) +} + +// TestEnvOverridesEnvFrom populates the environment of a container from a configmap, and from another configmap's key with a "conflicting" key. +// Then, it checks that the value of the "conflicting" key has been correctly overriden. +func TestEnvOverridesEnvFrom(t *testing.T) { + rm := fakeResourceManager(configMap3) + er := fakeEventRecorder() + + // override will override the value of "keyFoo" from "configMap3". + override := "__override__" + + // Create a pod object having a single container. + // The container's environment is to be populated from a configmap, and later overriden with a value provided directly. + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "pod-0", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + EnvFrom: []corev1.EnvFromSource{ + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: configMap3.Name, + }, + }, + }, + }, + Env: []corev1.EnvVar{ + { + Name: keyFoo, // One of the keys in configMap3. + Value: override, + }, + }, + }, + }, + }, + } + + // Populate the pods's environment. + err := populateEnvironmentVariables(context.Background(), pod, rm, er) + assert.NoError(t, err) + + // Make sure that the container's environment contains all the expected keys and values. + assert.ElementsMatch(t, pod.Spec.Containers[0].Env, []corev1.EnvVar{ + { + Name: keyFoo, + Value: override, + }, + { + Name: keyBar, + Value: configMap3.Data[keyBar], + }, + }) + + // Make sure that no events have been recorded, as the configmaps and secrets are valid. + assert.Len(t, er.Events, 0) +} + +// TestEnvFromInexistentConfigMaps populates the environment of a container from two configmaps (one of them optional) that do not exist. +// Then, it checks that the expected events have been recorded. +func TestEnvFromInexistentConfigMaps(t *testing.T) { + rm := fakeResourceManager() + er := fakeEventRecorder() + + missingConfigMap1Name := "missing-config-map-1" + missingConfigMap2Name := "missing-config-map-2" + + // Create a pod object having a single container. + // The container's environment is to be populated from two configmaps that do not exist. + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "pod-0", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + EnvFrom: []corev1.EnvFromSource{ + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: missingConfigMap1Name, + }, + // The configmap reference is optional. + Optional: &bTrue, + }, + }, + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: missingConfigMap2Name, + }, + // The configmap reference is mandatory. + Optional: &bFalse, + }, + }, + }, + }, + }, + }, + } + + // Populate the pods's environment. + err := populateEnvironmentVariables(context.Background(), pod, rm, er) + assert.Error(t, err) + + // Make sure that two events have been recorded with the correct reason and message. + assert.Len(t, er.Events, 2) + event1 := <-er.Events + assert.Contains(t, event1, ReasonOptionalConfigMapNotFound) + assert.Contains(t, event1, missingConfigMap1Name) + event2 := <-er.Events + assert.Contains(t, event2, ReasonMandatoryConfigMapNotFound) + assert.Contains(t, event2, missingConfigMap2Name) +} + +func TestEnvFromInexistentSecrets(t *testing.T) { + rm := fakeResourceManager() + er := fakeEventRecorder() + + missingSecret1Name := "missing-secret-1" + missingSecret2Name := "missing-secret-2" + + // Create a pod object having a single container. + // The container's environment is to be populated from two secrets that do not exist. + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "pod-0", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + EnvFrom: []corev1.EnvFromSource{ + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: missingSecret1Name, + }, + // The secret reference is optional. + Optional: &bTrue, + }, + }, + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: missingSecret2Name, + }, + // The secret reference is mandatory. + Optional: &bFalse, + }, + }, + }, + }, + }, + }, + } + + // Populate the pods's environment. + err := populateEnvironmentVariables(context.Background(), pod, rm, er) + assert.Error(t, err) + + // Make sure that two events have been recorded with the correct reason and message. + assert.Len(t, er.Events, 2) + event1 := <-er.Events + assert.Contains(t, event1, ReasonOptionalSecretNotFound) + assert.Contains(t, event1, missingSecret1Name) + event2 := <-er.Events + assert.Contains(t, event2, ReasonMandatorySecretNotFound) + assert.Contains(t, event2, missingSecret2Name) +} + +// mockConfigMap returns a configmap with the specified namespace, name and data. +func mockConfigMap(namespace, name string, data map[string]string) *corev1.ConfigMap { + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + Data: data, + } +} + +// fakeEventRecorder returns an event recorder that can be used to capture events. +func fakeEventRecorder() *record.FakeRecorder { + return record.NewFakeRecorder(10) +} + +// fakeResourceManager returns an instance of the resource manager that will return the specified objects when its "GetX" functions are called. +// Objects can be any valid Kubernetes object (corev1.Pod, corev1.ConfigMap, corev1.Secret, ...). +func fakeResourceManager(objects ...runtime.Object) *manager.ResourceManager { + // Create a fake Kubernetes client that will list the specified objects. + kubeClient := fake.NewSimpleClientset(objects...) + // Create a shared informer factory from where we can grab informers and listers for pods, configmaps and secrets. + kubeInformerFactory := informers.NewSharedInformerFactory(kubeClient, 30*time.Second) + // Grab and start informers for pods, configmaps and secrets. + pInformer := kubeInformerFactory.Core().V1().Pods() + mInformer := kubeInformerFactory.Core().V1().ConfigMaps() + sInformer := kubeInformerFactory.Core().V1().Secrets() + go pInformer.Informer().Run(wait.NeverStop) + go mInformer.Informer().Run(wait.NeverStop) + go sInformer.Informer().Run(wait.NeverStop) + // Wait for the caches to be synced. + if !cache.WaitForCacheSync(wait.NeverStop, pInformer.Informer().HasSynced, mInformer.Informer().HasSynced, sInformer.Informer().HasSynced) { + panic("failed to wait for caches to be synced") + } + // Create a new instance of the resource manager using the listers for pods, configmaps and secrets. + r, err := manager.NewResourceManager(pInformer.Lister(), sInformer.Lister(), mInformer.Lister()) + if err != nil { + panic(err) + } + return r +} + +// mockSecret returns a secret with the specified namespace, name and data. +func mockSecret(namespace, name string, data map[string]string) *corev1.Secret { + res := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + Data: make(map[string][]byte), + } + for key, val := range data { + res.Data[key] = []byte(val) + } + return res +}