From d1dc259ef2796f6d8562203e564f47e383a5d62d Mon Sep 17 00:00:00 2001 From: Paul Morie Date: Mon, 25 Jan 2016 14:41:16 -0500 Subject: [PATCH] ConfigMap volume source --- cmd/kubelet/app/plugins.go | 2 + hack/verify-flags/exceptions.txt | 4 +- pkg/api/types.go | 32 +++ pkg/api/v1/types.go | 32 +++ pkg/api/validation/validation.go | 24 +- pkg/volume/configmap/configmap.go | 230 +++++++++++++++ pkg/volume/configmap/configmap_test.go | 380 +++++++++++++++++++++++++ pkg/volume/configmap/doc.go | 18 ++ test/e2e/configmap.go | 242 ++++++++++++++++ test/e2e/downwardapi_volume.go | 6 +- 10 files changed, 962 insertions(+), 8 deletions(-) create mode 100644 pkg/volume/configmap/configmap.go create mode 100644 pkg/volume/configmap/configmap_test.go create mode 100644 pkg/volume/configmap/doc.go diff --git a/cmd/kubelet/app/plugins.go b/cmd/kubelet/app/plugins.go index 40eb75c09c..c9388d1278 100644 --- a/cmd/kubelet/app/plugins.go +++ b/cmd/kubelet/app/plugins.go @@ -32,6 +32,7 @@ import ( "k8s.io/kubernetes/pkg/volume/azure_file" "k8s.io/kubernetes/pkg/volume/cephfs" "k8s.io/kubernetes/pkg/volume/cinder" + "k8s.io/kubernetes/pkg/volume/configmap" "k8s.io/kubernetes/pkg/volume/downwardapi" "k8s.io/kubernetes/pkg/volume/empty_dir" "k8s.io/kubernetes/pkg/volume/fc" @@ -80,6 +81,7 @@ func ProbeVolumePlugins(pluginDir string) []volume.VolumePlugin { allPlugins = append(allPlugins, flocker.ProbeVolumePlugins()...) allPlugins = append(allPlugins, flexvolume.ProbeVolumePlugins(pluginDir)...) allPlugins = append(allPlugins, azure_file.ProbeVolumePlugins()...) + allPlugins = append(allPlugins, configmap.ProbeVolumePlugins()...) return allPlugins } diff --git a/hack/verify-flags/exceptions.txt b/hack/verify-flags/exceptions.txt index 8ff808b3ed..93fcac7a84 100644 --- a/hack/verify-flags/exceptions.txt +++ b/hack/verify-flags/exceptions.txt @@ -5,7 +5,6 @@ cluster/aws/templates/configure-vm-aws.sh: # We set the hostname_override to th cluster/aws/templates/configure-vm-aws.sh: api_servers: '${API_SERVERS}' cluster/aws/templates/configure-vm-aws.sh: env-to-grains "hostname_override" cluster/aws/templates/configure-vm-aws.sh: env-to-grains "runtime_config" -cluster/aws/templates/salt-minion.sh:# We set the hostname_override to the full EC2 private dns name cluster/centos/util.sh: local node_ip=${node#*@} cluster/gce/configure-vm.sh: advertise_address: '${EXTERNAL_IP}' cluster/gce/configure-vm.sh: api_servers: '${KUBERNETES_MASTER_NAME}' @@ -95,12 +94,11 @@ hack/local-up-cluster.sh: runtime_config="" pkg/kubelet/qos/memory_policy_test.go: t.Errorf("oom_score_adj should be between %d and %d, but was %d", test.lowOOMScoreAdj, test.highOOMScoreAdj, oomScoreAdj) pkg/kubelet/qos/memory_policy_test.go: highOOMScoreAdj int // The min oom_score_adj score the container should be assigned. pkg/kubelet/qos/memory_policy_test.go: lowOOMScoreAdj int // The max oom_score_adj score the container should be assigned. -pkg/util/oom/oom_linux.go: err = fmt.Errorf("failed to read oom_score_adj: %v", readErr) -pkg/util/oom/oom_linux.go: err = fmt.Errorf("failed to set oom_score_adj to %d: %v", oomScoreAdj, writeErr) pkg/util/oom/oom_linux.go: return fmt.Errorf("invalid PID %d specified for oom_score_adj", pid) pkg/util/oom/oom_linux.go: oomScoreAdjPath := path.Join("/proc", pidStr, "oom_score_adj") pkg/util/oom/oom_linux.go:// Writes 'value' to /proc//oom_score_adj for all processes in cgroup cgroupName. pkg/util/oom/oom_linux.go:// Writes 'value' to /proc//oom_score_adj. PID = 0 means self +test/e2e/configmap.go: Command: []string{"/mt", "--break_on_expected_content=false", "--retry_time=120", "--file_content_in_loop=/etc/configmap-volume/data-1"}, test/e2e/downwardapi_volume.go: Command: []string{"/mt", "--break_on_expected_content=false", "--retry_time=120", "--file_content_in_loop=" + filePath}, test/e2e/es_cluster_logging.go: Failf("No cluster_name field in Elasticsearch response: %v", esResponse) test/e2e/es_cluster_logging.go: // Check to see if have a cluster_name field. diff --git a/pkg/api/types.go b/pkg/api/types.go index f2ac32f43f..99e9bb4ae0 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -217,6 +217,8 @@ type VolumeSource struct { FC *FCVolumeSource `json:"fc,omitempty"` // AzureFile represents an Azure File Service mount on the host and bind mount to the pod. AzureFile *AzureFileVolumeSource `json:"azureFile,omitempty"` + // ConfigMap represents a configMap that should populate this volume + ConfigMap *ConfigMapVolumeSource `json:"configMap,omitempty"` } // Similar to VolumeSource but meant for the administrator who creates PVs. @@ -688,6 +690,36 @@ type AzureFileVolumeSource struct { ReadOnly bool `json:"readOnly,omitempty"` } +// Adapts a ConfigMap into a volume. +// +// The contents of the target ConfigMap's Data field will be presented in a +// volume as files using the keys in the Data field as the file names, unless +// the items element is populated with specific mappings of keys to paths. +// ConfigMap volumes support ownership management and SELinux relabeling. +type ConfigMapVolumeSource struct { + LocalObjectReference `json:",inline"` + // If unspecified, each key-value pair in the Data field of the referenced + // ConfigMap will be projected into the volume as a file whose name is the + // 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 '..'. + Items []KeyToPath `json:"items,omitempty"` +} + +// Maps a string key to a path within a volume. +type KeyToPath struct { + // The key to project. + Key string `json:"key"` + + // The relative path of the file to map the key to. + // May not be an absolute path. + // May not contain the path element '..'. + // May not start with the string '..'. + Path string `json:"path"` +} + // ContainerPort represents a network port in a single container type ContainerPort struct { // Optional: If specified, this must be an IANA_SVC_NAME Each named port diff --git a/pkg/api/v1/types.go b/pkg/api/v1/types.go index 532505827b..a8d1771e33 100644 --- a/pkg/api/v1/types.go +++ b/pkg/api/v1/types.go @@ -263,6 +263,8 @@ type VolumeSource struct { FC *FCVolumeSource `json:"fc,omitempty"` // AzureFile represents an Azure File Service mount on the host and bind mount to the pod. AzureFile *AzureFileVolumeSource `json:"azureFile,omitempty"` + // ConfigMap represents a configMap that should populate this volume + ConfigMap *ConfigMapVolumeSource `json:"configMap,omitempty"` } // PersistentVolumeClaimVolumeSource references the user's PVC in the same namespace. @@ -808,6 +810,36 @@ type AzureFileVolumeSource struct { ReadOnly bool `json:"readOnly,omitempty"` } +// Adapts a ConfigMap into a volume. +// +// The contents of the target ConfigMap's Data field will be presented in a +// volume as files using the keys in the Data field as the file names, unless +// the items element is populated with specific mappings of keys to paths. +// ConfigMap volumes support ownership management and SELinux relabeling. +type ConfigMapVolumeSource struct { + LocalObjectReference `json:",inline"` + // If unspecified, each key-value pair in the Data field of the referenced + // ConfigMap will be projected into the volume as a file whose name is the + // 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 '..'. + Items []KeyToPath `json:"items,omitempty"` +} + +// Maps a string key to a path within a volume. +type KeyToPath struct { + // The key to project. + Key string `json:"key"` + + // The relative path of the file to map the key to. + // May not be an absolute path. + // May not contain the path element '..'. + // May not start with the string '..'. + Path string `json:"path"` +} + // ContainerPort represents a network port in a single container. type ContainerPort struct { // If specified, this must be an IANA_SVC_NAME and unique within the pod. Each diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index ff38e7fa09..20af1c5a78 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -491,8 +491,20 @@ func validateVolumeSource(source *api.VolumeSource, fldPath *field.Path) field.E } } if source.FlexVolume != nil { - numVolumes++ - allErrs = append(allErrs, validateFlexVolumeSource(source.FlexVolume, fldPath.Child("flexVolume"))...) + if numVolumes > 0 { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("flexVolume"), "may not specifiy more than 1 volume type")) + } else { + numVolumes++ + allErrs = append(allErrs, validateFlexVolumeSource(source.FlexVolume, fldPath.Child("flexVolume"))...) + } + } + if source.ConfigMap != nil { + if numVolumes > 0 { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("configMap"), "may not specifiy more than 1 volume type")) + } else { + numVolumes++ + allErrs = append(allErrs, validateConfigMapVolumeSource(source.ConfigMap, fldPath.Child("configMap"))...) + } } if source.AzureFile != nil { numVolumes++ @@ -584,6 +596,14 @@ func validateSecretVolumeSource(secretSource *api.SecretVolumeSource, fldPath *f return allErrs } +func validateConfigMapVolumeSource(configMapSource *api.ConfigMapVolumeSource, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if len(configMapSource.Name) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("name"), "")) + } + return allErrs +} + func validatePersistentClaimVolumeSource(claim *api.PersistentVolumeClaimVolumeSource, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} if len(claim.ClaimName) == 0 { diff --git a/pkg/volume/configmap/configmap.go b/pkg/volume/configmap/configmap.go new file mode 100644 index 0000000000..0cbee39a69 --- /dev/null +++ b/pkg/volume/configmap/configmap.go @@ -0,0 +1,230 @@ +/* +Copyright 2015 The Kubernetes Authors 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 configmap + +import ( + "fmt" + "path" + + "github.com/golang/glog" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/types" + ioutil "k8s.io/kubernetes/pkg/util/io" + "k8s.io/kubernetes/pkg/util/mount" + "k8s.io/kubernetes/pkg/util/strings" + "k8s.io/kubernetes/pkg/volume" + volumeutil "k8s.io/kubernetes/pkg/volume/util" +) + +// ProbeVolumePlugin is the entry point for plugin detection in a package. +func ProbeVolumePlugins() []volume.VolumePlugin { + return []volume.VolumePlugin{&configMapPlugin{}} +} + +const ( + configMapPluginName = "kubernetes.io/configmap" +) + +// configMapPlugin implements the VolumePlugin interface. +type configMapPlugin struct { + host volume.VolumeHost +} + +var _ volume.VolumePlugin = &configMapPlugin{} + +func (plugin *configMapPlugin) Init(host volume.VolumeHost) error { + plugin.host = host + return nil +} + +func (plugin *configMapPlugin) Name() string { + return configMapPluginName +} + +func (plugin *configMapPlugin) CanSupport(spec *volume.Spec) bool { + return spec.Volume != nil && spec.Volume.ConfigMap != nil +} + +func (plugin *configMapPlugin) NewBuilder(spec *volume.Spec, pod *api.Pod, opts volume.VolumeOptions) (volume.Builder, error) { + return &configMapVolumeBuilder{ + configMapVolume: &configMapVolume{spec.Name(), pod.UID, plugin, plugin.host.GetMounter(), plugin.host.GetWriter(), volume.MetricsNil{}}, + source: *spec.Volume.ConfigMap, + pod: *pod, + opts: &opts}, nil +} + +func (plugin *configMapPlugin) NewCleaner(volName string, podUID types.UID) (volume.Cleaner, error) { + return &configMapVolumeCleaner{&configMapVolume{volName, podUID, plugin, plugin.host.GetMounter(), plugin.host.GetWriter(), volume.MetricsNil{}}}, nil +} + +type configMapVolume struct { + volName string + podUID types.UID + plugin *configMapPlugin + mounter mount.Interface + writer ioutil.Writer + volume.MetricsNil +} + +var _ volume.Volume = &configMapVolume{} + +func (sv *configMapVolume) GetPath() string { + return sv.plugin.host.GetPodVolumeDir(sv.podUID, strings.EscapeQualifiedNameForDisk(configMapPluginName), sv.volName) +} + +// configMapVolumeBuilder handles retrieving secrets from the API server +// and placing them into the volume on the host. +type configMapVolumeBuilder struct { + *configMapVolume + + source api.ConfigMapVolumeSource + pod api.Pod + opts *volume.VolumeOptions +} + +var _ volume.Builder = &configMapVolumeBuilder{} + +func (sv *configMapVolume) GetAttributes() volume.Attributes { + return volume.Attributes{ + ReadOnly: true, + Managed: true, + SupportsSELinux: true, + } +} + +func (b *configMapVolumeBuilder) getMetaDir() string { + return path.Join(b.plugin.host.GetPodPluginDir(b.podUID, strings.EscapeQualifiedNameForDisk(configMapPluginName)), b.volName) +} + +// This is the spec for the volume that this plugin wraps. +var wrappedVolumeSpec = volume.Spec{ + Volume: &api.Volume{VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{Medium: api.StorageMediumMemory}}}, +} + +func (b *configMapVolumeBuilder) SetUp(fsGroup *int64) error { + return b.SetUpAt(b.GetPath(), fsGroup) +} + +func (b *configMapVolumeBuilder) SetUpAt(dir string, fsGroup *int64) error { + glog.V(3).Infof("Setting up volume %v for pod %v at %v", b.volName, b.pod.UID, dir) + + // Wrap EmptyDir, let it do the setup. + wrapped, err := b.plugin.host.NewWrapperBuilder(b.volName, wrappedVolumeSpec, &b.pod, *b.opts) + if err != nil { + return err + } + if err := wrapped.SetUpAt(dir, fsGroup); err != nil { + return err + } + + kubeClient := b.plugin.host.GetKubeClient() + if kubeClient == nil { + return fmt.Errorf("Cannot setup configMap volume %v because kube client is not configured", b.volName) + } + + configMap, err := kubeClient.Core().ConfigMaps(b.pod.Namespace).Get(b.source.Name) + if err != nil { + glog.Errorf("Couldn't get configMap %v/%v: %v", b.pod.Namespace, b.source.Name, err) + return err + } + + totalBytes := totalBytes(configMap) + glog.V(3).Infof("Received configMap %v/%v containing (%v) pieces of data, %v total bytes", + b.pod.Namespace, + b.source.Name, + len(configMap.Data), + totalBytes) + + payload, err := makePayload(b.source.Items, configMap) + if err != nil { + return err + } + + writerContext := fmt.Sprintf("pod %v/%v volume %v", b.pod.Namespace, b.pod.Name, b.volName) + writer, err := volumeutil.NewAtomicWriter(dir, writerContext) + if err != nil { + glog.Errorf("Error creating atomic writer: %v", err) + return err + } + + err = writer.Write(payload) + if err != nil { + glog.Errorf("Error writing payload to dir: %v", err) + return err + } + + err = volume.SetVolumeOwnership(b, fsGroup) + if err != nil { + glog.Errorf("Error applying volume ownership settings for group: %v", fsGroup) + return err + } + + return nil +} + +func makePayload(mappings []api.KeyToPath, configMap *api.ConfigMap) (map[string][]byte, error) { + payload := make(map[string][]byte, len(configMap.Data)) + + if len(mappings) == 0 { + for name, data := range configMap.Data { + payload[name] = []byte(data) + } + } else { + for _, ktp := range mappings { + content, ok := configMap.Data[ktp.Key] + if !ok { + glog.Errorf("references non-existent config key") + return nil, fmt.Errorf("references non-existent config key") + } + + payload[ktp.Path] = []byte(content) + } + } + + return payload, nil +} + +func totalBytes(configMap *api.ConfigMap) int { + totalSize := 0 + for _, value := range configMap.Data { + totalSize += len(value) + } + + return totalSize +} + +// configMapVolumeCleaner handles cleaning up configMap volumes. +type configMapVolumeCleaner struct { + *configMapVolume +} + +var _ volume.Cleaner = &configMapVolumeCleaner{} + +func (c *configMapVolumeCleaner) TearDown() error { + return c.TearDownAt(c.GetPath()) +} + +func (c *configMapVolumeCleaner) TearDownAt(dir string) error { + glog.V(3).Infof("Tearing down volume %v for pod %v at %v", c.volName, c.podUID, dir) + + // Wrap EmptyDir, let it do the teardown. + wrapped, err := c.plugin.host.NewWrapperCleaner(c.volName, wrappedVolumeSpec, c.podUID) + if err != nil { + return err + } + return wrapped.TearDownAt(dir) +} diff --git a/pkg/volume/configmap/configmap_test.go b/pkg/volume/configmap/configmap_test.go new file mode 100644 index 0000000000..57213a0023 --- /dev/null +++ b/pkg/volume/configmap/configmap_test.go @@ -0,0 +1,380 @@ +/* +Copyright 2015 The Kubernetes Authors 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 configmap + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "reflect" + "strings" + "testing" + + "k8s.io/kubernetes/pkg/api" + clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" + "k8s.io/kubernetes/pkg/client/testing/fake" + "k8s.io/kubernetes/pkg/types" + "k8s.io/kubernetes/pkg/volume" + "k8s.io/kubernetes/pkg/volume/empty_dir" + "k8s.io/kubernetes/pkg/volume/util" +) + +func TestMakePayload(t *testing.T) { + cases := []struct { + name string + mappings []api.KeyToPath + configMap *api.ConfigMap + payload map[string][]byte + success bool + }{ + { + name: "no overrides", + configMap: &api.ConfigMap{ + Data: map[string]string{ + "foo": "foo", + "bar": "bar", + }, + }, + payload: map[string][]byte{ + "foo": []byte("foo"), + "bar": []byte("bar"), + }, + success: true, + }, + { + name: "basic 1", + mappings: []api.KeyToPath{ + { + Key: "foo", + Path: "path/to/foo.txt", + }, + }, + configMap: &api.ConfigMap{ + Data: map[string]string{ + "foo": "foo", + "bar": "bar", + }, + }, + payload: map[string][]byte{ + "path/to/foo.txt": []byte("foo"), + }, + success: true, + }, + { + name: "subdirs", + mappings: []api.KeyToPath{ + { + Key: "foo", + Path: "path/to/1/2/3/foo.txt", + }, + }, + configMap: &api.ConfigMap{ + Data: map[string]string{ + "foo": "foo", + "bar": "bar", + }, + }, + payload: map[string][]byte{ + "path/to/1/2/3/foo.txt": []byte("foo"), + }, + success: true, + }, + { + name: "subdirs 2", + mappings: []api.KeyToPath{ + { + Key: "foo", + Path: "path/to/1/2/3/foo.txt", + }, + }, + configMap: &api.ConfigMap{ + Data: map[string]string{ + "foo": "foo", + "bar": "bar", + }, + }, + payload: map[string][]byte{ + "path/to/1/2/3/foo.txt": []byte("foo"), + }, + success: true, + }, + { + name: "subdirs 3", + mappings: []api.KeyToPath{ + { + Key: "foo", + Path: "path/to/1/2/3/foo.txt", + }, + { + Key: "bar", + Path: "another/path/to/the/esteemed/bar.bin", + }, + }, + configMap: &api.ConfigMap{ + Data: map[string]string{ + "foo": "foo", + "bar": "bar", + }, + }, + payload: map[string][]byte{ + "path/to/1/2/3/foo.txt": []byte("foo"), + "another/path/to/the/esteemed/bar.bin": []byte("bar"), + }, + success: true, + }, + { + name: "non existent key", + mappings: []api.KeyToPath{ + { + Key: "zab", + Path: "path/to/foo.txt", + }, + }, + configMap: &api.ConfigMap{ + Data: map[string]string{ + "foo": "foo", + "bar": "bar", + }, + }, + success: false, + }, + } + + for _, tc := range cases { + actualPayload, err := makePayload(tc.mappings, tc.configMap) + if err != nil && tc.success { + t.Errorf("%v: unexpected failure making payload: %v", tc.name, err) + continue + } + + if err == nil && !tc.success { + t.Errorf("%v: unexpected success making payload", tc.name) + continue + } + + if !tc.success { + continue + } + + if e, a := tc.payload, actualPayload; !reflect.DeepEqual(e, a) { + t.Errorf("%v: expected and actual payload do not match", tc.name) + } + } +} + +func newTestHost(t *testing.T, clientset clientset.Interface) (string, volume.VolumeHost) { + tempDir, err := ioutil.TempDir("/tmp", "configmap_volume_test.") + if err != nil { + t.Fatalf("can't make a temp rootdir: %v", err) + } + + return tempDir, volume.NewFakeVolumeHost(tempDir, clientset, empty_dir.ProbeVolumePlugins()) +} + +func TestCanSupport(t *testing.T) { + pluginMgr := volume.VolumePluginMgr{} + _, host := newTestHost(t, nil) + pluginMgr.InitPlugins(ProbeVolumePlugins(), host) + + plugin, err := pluginMgr.FindPluginByName(configMapPluginName) + if err != nil { + t.Errorf("Can't find the plugin by name") + } + if plugin.Name() != configMapPluginName { + t.Errorf("Wrong name: %s", plugin.Name()) + } + if !plugin.CanSupport(&volume.Spec{Volume: &api.Volume{VolumeSource: api.VolumeSource{ConfigMap: &api.ConfigMapVolumeSource{LocalObjectReference: api.LocalObjectReference{""}}}}}) { + t.Errorf("Expected true") + } + if plugin.CanSupport(&volume.Spec{}) { + t.Errorf("Expected false") + } +} + +func TestPlugin(t *testing.T) { + var ( + testPodUID = types.UID("test_pod_uid") + testVolumeName = "test_volume_name" + testNamespace = "test_configmap_namespace" + testName = "test_configmap_name" + + volumeSpec = volumeSpec(testVolumeName, testName) + configMap = configMap(testNamespace, testName) + client = fake.NewSimpleClientset(&configMap) + pluginMgr = volume.VolumePluginMgr{} + _, host = newTestHost(t, client) + ) + + pluginMgr.InitPlugins(ProbeVolumePlugins(), host) + + plugin, err := pluginMgr.FindPluginByName(configMapPluginName) + if err != nil { + t.Errorf("Can't find the plugin by name") + } + + pod := &api.Pod{ObjectMeta: api.ObjectMeta{UID: testPodUID}} + builder, err := plugin.NewBuilder(volume.NewSpecFromVolume(volumeSpec), pod, volume.VolumeOptions{}) + if err != nil { + t.Errorf("Failed to make a new Builder: %v", err) + } + if builder == nil { + t.Errorf("Got a nil Builder") + } + + volumePath := builder.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 = builder.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) +} + +// Test the case where the plugin's ready file exists, but the volume dir is not a +// mountpoint, which is the state the system will be in after reboot. The dir +// should be mounter and the configMap data written to it. +func TestPluginReboot(t *testing.T) { + var ( + testPodUID = types.UID("test_pod_uid3") + testVolumeName = "test_volume_name" + testNamespace = "test_configmap_namespace" + testName = "test_configmap_name" + + volumeSpec = volumeSpec(testVolumeName, testName) + configMap = configMap(testNamespace, testName) + client = fake.NewSimpleClientset(&configMap) + pluginMgr = volume.VolumePluginMgr{} + rootDir, host = newTestHost(t, client) + ) + + pluginMgr.InitPlugins(ProbeVolumePlugins(), host) + + plugin, err := pluginMgr.FindPluginByName(configMapPluginName) + if err != nil { + t.Errorf("Can't find the plugin by name") + } + + pod := &api.Pod{ObjectMeta: api.ObjectMeta{UID: testPodUID}} + builder, err := plugin.NewBuilder(volume.NewSpecFromVolume(volumeSpec), pod, volume.VolumeOptions{}) + if err != nil { + t.Errorf("Failed to make a new Builder: %v", err) + } + if builder == nil { + t.Errorf("Got a nil Builder") + } + + podMetadataDir := fmt.Sprintf("%v/pods/test_pod_uid3/plugins/kubernetes.io~configmap/test_volume_name", rootDir) + util.SetReady(podMetadataDir) + volumePath := builder.GetPath() + if !strings.HasSuffix(volumePath, fmt.Sprintf("pods/test_pod_uid3/volumes/kubernetes.io~configmap/test_volume_name")) { + t.Errorf("Got unexpected path: %s", volumePath) + } + + fsGroup := int64(1001) + err = builder.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) *api.Volume { + return &api.Volume{ + Name: volumeName, + VolumeSource: api.VolumeSource{ + ConfigMap: &api.ConfigMapVolumeSource{ + LocalObjectReference: api.LocalObjectReference{ + Name: configMapName, + }, + }, + }, + } +} + +func configMap(namespace, name string) api.ConfigMap { + return api.ConfigMap{ + ObjectMeta: api.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + Data: map[string]string{ + "data-1": "value-1", + "data-2": "value-2", + "data-3": "value-3", + }, + } +} + +func doTestConfigMapDataInVolume(volumePath string, configMap api.ConfigMap, t *testing.T) { + for key, value := range configMap.Data { + configMapDataHostPath := path.Join(volumePath, key) + if _, err := os.Stat(configMapDataHostPath); err != nil { + t.Fatalf("SetUp() failed, couldn't find configMap data on disk: %v", configMapDataHostPath) + } else { + actualValue, err := ioutil.ReadFile(configMapDataHostPath) + if err != nil { + t.Fatalf("Couldn't read configMap data from: %v", configMapDataHostPath) + } + + if value != string(actualValue) { + t.Errorf("Unexpected value; expected %q, got %q", value, actualValue) + } + } + } +} + +func doTestCleanAndTeardown(plugin volume.VolumePlugin, podUID types.UID, testVolumeName, volumePath string, t *testing.T) { + cleaner, err := plugin.NewCleaner(testVolumeName, podUID) + if err != nil { + t.Errorf("Failed to make a new Cleaner: %v", err) + } + if cleaner == nil { + t.Errorf("Got a nil Cleaner") + } + + if err := cleaner.TearDown(); err != nil { + t.Errorf("Expected success, got: %v", err) + } + if _, err := os.Stat(volumePath); err == nil { + t.Errorf("TearDown() failed, volume path still exists: %s", volumePath) + } else if !os.IsNotExist(err) { + t.Errorf("SetUp() failed: %v", err) + } +} diff --git a/pkg/volume/configmap/doc.go b/pkg/volume/configmap/doc.go new file mode 100644 index 0000000000..7d5c6c8730 --- /dev/null +++ b/pkg/volume/configmap/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2015 The Kubernetes Authors 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 configmap contains the internal representation of configMap volumes. +package configmap diff --git a/test/e2e/configmap.go b/test/e2e/configmap.go index 56b269c33c..0a45197075 100644 --- a/test/e2e/configmap.go +++ b/test/e2e/configmap.go @@ -18,16 +18,258 @@ package e2e import ( "fmt" + "time" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/util" . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" ) var _ = Describe("ConfigMap", func() { + f := NewFramework("configmap") + It("should be consumable from pods in volume [Conformance]", func() { + name := "configmap-test-volume-" + string(util.NewUUID()) + volumeName := "configmap-volume" + volumeMountPath := "/etc/configmap-volume" + + configMap := &api.ConfigMap{ + ObjectMeta: api.ObjectMeta{ + Namespace: f.Namespace.Name, + Name: name, + }, + Data: map[string]string{ + "data-1": "value-1", + "data-2": "value-2", + "data-3": "value-3", + }, + } + + By(fmt.Sprintf("Creating configMap with name %s", configMap.Name)) + defer func() { + By("Cleaning up the configMap") + if err := f.Client.ConfigMaps(f.Namespace.Name).Delete(configMap.Name); err != nil { + Failf("unable to delete configMap %v: %v", configMap.Name, err) + } + }() + var err error + if configMap, err = f.Client.ConfigMaps(f.Namespace.Name).Create(configMap); err != nil { + Failf("unable to create test configMap %s: %v", configMap.Name, err) + } + + pod := &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Name: "pod-configmaps-" + string(util.NewUUID()), + }, + Spec: api.PodSpec{ + Volumes: []api.Volume{ + { + Name: volumeName, + VolumeSource: api.VolumeSource{ + ConfigMap: &api.ConfigMapVolumeSource{ + LocalObjectReference: api.LocalObjectReference{ + Name: name, + }, + }, + }, + }, + }, + Containers: []api.Container{ + { + Name: "configmap-volume-test", + Image: "gcr.io/google_containers/mounttest:0.6", + Args: []string{"--file_content=/etc/configmap-volume/data-1"}, + VolumeMounts: []api.VolumeMount{ + { + Name: volumeName, + MountPath: volumeMountPath, + ReadOnly: true, + }, + }, + }, + }, + RestartPolicy: api.RestartPolicyNever, + }, + } + + testContainerOutput("consume configMaps", f.Client, pod, 0, []string{ + "content of file \"/etc/configmap-volume/data-1\": value-1", + }, f.Namespace.Name) + }) + + It("should be consumable from pods in volume with mappings [Conformance]", func() { + name := "configmap-test-volume-map-" + string(util.NewUUID()) + volumeName := "configmap-volume" + volumeMountPath := "/etc/configmap-volume" + + configMap := &api.ConfigMap{ + ObjectMeta: api.ObjectMeta{ + Namespace: f.Namespace.Name, + Name: name, + }, + Data: map[string]string{ + "data-1": "value-1", + "data-2": "value-2", + "data-3": "value-3", + }, + } + + By(fmt.Sprintf("Creating configMap with name %s", configMap.Name)) + defer func() { + By("Cleaning up the configMap") + if err := f.Client.ConfigMaps(f.Namespace.Name).Delete(configMap.Name); err != nil { + Failf("unable to delete configMap %v: %v", configMap.Name, err) + } + }() + var err error + if configMap, err = f.Client.ConfigMaps(f.Namespace.Name).Create(configMap); err != nil { + Failf("unable to create test configMap %s: %v", configMap.Name, err) + } + + pod := &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Name: "pod-configmaps-" + string(util.NewUUID()), + }, + Spec: api.PodSpec{ + Volumes: []api.Volume{ + { + Name: volumeName, + VolumeSource: api.VolumeSource{ + ConfigMap: &api.ConfigMapVolumeSource{ + LocalObjectReference: api.LocalObjectReference{ + Name: name, + }, + Items: []api.KeyToPath{ + { + Key: "data-2", + Path: "path/to/data-2", + }, + }, + }, + }, + }, + }, + Containers: []api.Container{ + { + Name: "configmap-volume-test", + Image: "gcr.io/google_containers/mounttest:0.6", + Args: []string{"--file_content=/etc/configmap-volume/path/to/data-2"}, + VolumeMounts: []api.VolumeMount{ + { + Name: volumeName, + MountPath: volumeMountPath, + ReadOnly: true, + }, + }, + }, + }, + RestartPolicy: api.RestartPolicyNever, + }, + } + + testContainerOutput("consume configMaps", f.Client, pod, 0, []string{ + "content of file \"/etc/configmap-volume/path/to/data-2\": value-2", + }, f.Namespace.Name) + }) + + It("updates should be reflected in volume [Conformance]", 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 = 90 * time.Second + + name := "configmap-test-upd-" + string(util.NewUUID()) + volumeName := "configmap-volume" + volumeMountPath := "/etc/configmap-volume" + containerName := "configmap-volume-test" + + configMap := &api.ConfigMap{ + ObjectMeta: api.ObjectMeta{ + Namespace: f.Namespace.Name, + Name: name, + }, + Data: map[string]string{ + "data-1": "value-1", + }, + } + + By(fmt.Sprintf("Creating configMap with name %s", configMap.Name)) + defer func() { + By("Cleaning up the configMap") + if err := f.Client.ConfigMaps(f.Namespace.Name).Delete(configMap.Name); err != nil { + Failf("unable to delete configMap %v: %v", configMap.Name, err) + } + }() + var err error + if configMap, err = f.Client.ConfigMaps(f.Namespace.Name).Create(configMap); err != nil { + Failf("unable to create test configMap %s: %v", configMap.Name, err) + } + + pod := &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Name: "pod-configmaps-" + string(util.NewUUID()), + }, + Spec: api.PodSpec{ + Volumes: []api.Volume{ + { + Name: volumeName, + VolumeSource: api.VolumeSource{ + ConfigMap: &api.ConfigMapVolumeSource{ + LocalObjectReference: api.LocalObjectReference{ + Name: name, + }, + }, + }, + }, + }, + Containers: []api.Container{ + { + Name: containerName, + Image: "gcr.io/google_containers/mounttest:0.6", + Command: []string{"/mt", "--break_on_expected_content=false", "--retry_time=120", "--file_content_in_loop=/etc/configmap-volume/data-1"}, + VolumeMounts: []api.VolumeMount{ + { + Name: volumeName, + MountPath: volumeMountPath, + ReadOnly: true, + }, + }, + }, + }, + RestartPolicy: api.RestartPolicyNever, + }, + } + + defer func() { + By("Deleting the pod") + f.Client.Pods(f.Namespace.Name).Delete(pod.Name, api.NewDeleteOptions(0)) + }() + By("Creating the pod") + _, err = f.Client.Pods(f.Namespace.Name).Create(pod) + Expect(err).NotTo(HaveOccurred()) + + expectNoError(waitForPodRunningInNamespace(f.Client, pod.Name, f.Namespace.Name)) + + pollLogs := func() (string, error) { + return getPodLogs(f.Client, f.Namespace.Name, pod.Name, containerName) + } + + Eventually(pollLogs, podLogTimeout, poll).Should(ContainSubstring("value-1")) + + By(fmt.Sprintf("Updating configmap %v", configMap.Name)) + configMap.ResourceVersion = "" // to force update + configMap.Data["data-1"] = "value-2" + _, err = f.Client.ConfigMaps(f.Namespace.Name).Update(configMap) + Expect(err).NotTo(HaveOccurred()) + + Eventually(pollLogs, podLogTimeout, poll).Should(ContainSubstring("value-2")) + }) + It("should be consumable via environment variable [Conformance]", func() { name := "configmap-test-" + string(util.NewUUID()) configMap := &api.ConfigMap{ diff --git a/test/e2e/downwardapi_volume.go b/test/e2e/downwardapi_volume.go index d5e43496ea..99380649d8 100644 --- a/test/e2e/downwardapi_volume.go +++ b/test/e2e/downwardapi_volume.go @@ -27,10 +27,10 @@ import ( . "github.com/onsi/gomega" ) -// How long to wait for a log pod to be displayed -const podLogTimeout = 45 * time.Second - var _ = Describe("Downward API volume", func() { + // How long to wait for a log pod to be displayed + const podLogTimeout = 45 * time.Second + f := NewFramework("downward-api") It("should provide podname only [Conformance]", func() { podName := "downwardapi-volume-" + string(util.NewUUID())