Add support for configmap volumes to play kube

If the k8s yaml has volumes from a configmap, play kube
will now create a volume based on the data from the
configmap and volume source and set it to the right path
in the container accordingly.
Add tests for this and update some test for ENV from configmap.

Signed-off-by: Urvashi Mohnani <umohnani@redhat.com>
This commit is contained in:
Urvashi Mohnani 2021-11-28 19:32:10 -05:00
parent 7324d94648
commit 7d331d35dd
4 changed files with 232 additions and 39 deletions

View File

@ -239,27 +239,6 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY
return nil, err
}
podSpec := entities.PodSpec{PodSpecGen: *p}
volumes, err := kube.InitializeVolumes(podYAML.Spec.Volumes)
if err != nil {
return nil, err
}
seccompPaths, err := kube.InitializeSeccompPaths(podYAML.ObjectMeta.Annotations, options.SeccompProfileRoot)
if err != nil {
return nil, err
}
var ctrRestartPolicy string
switch podYAML.Spec.RestartPolicy {
case v1.RestartPolicyAlways:
ctrRestartPolicy = define.RestartPolicyAlways
case v1.RestartPolicyOnFailure:
ctrRestartPolicy = define.RestartPolicyOnFailure
case v1.RestartPolicyNever:
ctrRestartPolicy = define.RestartPolicyNo
default: // Default to Always
ctrRestartPolicy = define.RestartPolicyAlways
}
configMapIndex := make(map[string]struct{})
for _, configMap := range configMaps {
@ -284,6 +263,56 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY
configMaps = append(configMaps, cm)
}
volumes, err := kube.InitializeVolumes(podYAML.Spec.Volumes, configMaps)
if err != nil {
return nil, err
}
// Go through the volumes and create a podman volume for all volumes that have been
// defined by a configmap
for _, v := range volumes {
if v.Type == kube.KubeVolumeTypeConfigMap && !v.Optional {
vol, err := ic.Libpod.NewVolume(ctx, libpod.WithVolumeName(v.Source))
if err != nil {
return nil, errors.Wrapf(err, "cannot create a local volume for volume from configmap %q", v.Source)
}
mountPoint, err := vol.MountPoint()
if err != nil || mountPoint == "" {
return nil, errors.Wrapf(err, "unable to get mountpoint of volume %q", vol.Name())
}
// Create files and add data to the volume mountpoint based on the Items in the volume
for k, v := range v.Items {
dataPath := filepath.Join(mountPoint, k)
f, err := os.Create(dataPath)
if err != nil {
return nil, errors.Wrapf(err, "cannot create file %q at volume mountpoint %q", k, mountPoint)
}
defer f.Close()
_, err = f.WriteString(v)
if err != nil {
return nil, err
}
}
}
}
seccompPaths, err := kube.InitializeSeccompPaths(podYAML.ObjectMeta.Annotations, options.SeccompProfileRoot)
if err != nil {
return nil, err
}
var ctrRestartPolicy string
switch podYAML.Spec.RestartPolicy {
case v1.RestartPolicyAlways:
ctrRestartPolicy = define.RestartPolicyAlways
case v1.RestartPolicyOnFailure:
ctrRestartPolicy = define.RestartPolicyOnFailure
case v1.RestartPolicyNever:
ctrRestartPolicy = define.RestartPolicyNo
default: // Default to Always
ctrRestartPolicy = define.RestartPolicyAlways
}
if podOpt.Infra {
infraImage := util.DefaultContainerConfig().Engine.InfraImage
infraOptions := entities.NewInfraContainerCreateOptions()

View File

@ -310,6 +310,11 @@ func ToSpecGen(ctx context.Context, opts *CtrSpecGenOptions) (*specgen.SpecGener
if !exists {
return nil, errors.Errorf("Volume mount %s specified for container but not configured in volumes", volume.Name)
}
// Skip if the volume is optional. This means that a configmap for a configmap volume was not found but it was
// optional so we can move on without throwing an error
if exists && volumeSource.Optional {
continue
}
dest, options, err := parseMountPath(volume.MountPath, volume.ReadOnly)
if err != nil {
@ -341,6 +346,13 @@ func ToSpecGen(ctx context.Context, opts *CtrSpecGenOptions) (*specgen.SpecGener
Options: options,
}
s.Volumes = append(s.Volumes, &namedVolume)
case KubeVolumeTypeConfigMap:
cmVolume := specgen.NamedVolume{
Dest: volume.MountPath,
Name: volumeSource.Source,
Options: options,
}
s.Volumes = append(s.Volumes, &cmVolume)
default:
return nil, errors.Errorf("Unsupported volume source type")
}

View File

@ -23,6 +23,7 @@ type KubeVolumeType int
const (
KubeVolumeTypeBindMount KubeVolumeType = iota
KubeVolumeTypeNamed KubeVolumeType = iota
KubeVolumeTypeConfigMap KubeVolumeType = iota
)
// nolint:golint
@ -31,6 +32,14 @@ type KubeVolume struct {
Type KubeVolumeType
// Path for bind mount or volume name for named volume
Source string
// Items to add to a named volume created where the key is the file name and the value is the data
// This is only used when there are volumes in the yaml that refer to a configmap
// Example: if configmap has data "SPECIAL_LEVEL: very" then the file name is "SPECIAL_LEVEL" and the
// data in that file is "very".
Items map[string]string
// If the volume is optional, we can move on if it is not found
// Only used when there are volumes in a yaml that refer to a configmap
Optional bool
}
// Create a KubeVolume from an HostPathVolumeSource
@ -98,23 +107,64 @@ func VolumeFromPersistentVolumeClaim(claim *v1.PersistentVolumeClaimVolumeSource
}, nil
}
// Create a KubeVolume from one of the supported VolumeSource
func VolumeFromSource(volumeSource v1.VolumeSource) (*KubeVolume, error) {
if volumeSource.HostPath != nil {
return VolumeFromHostPath(volumeSource.HostPath)
} else if volumeSource.PersistentVolumeClaim != nil {
return VolumeFromPersistentVolumeClaim(volumeSource.PersistentVolumeClaim)
func VolumeFromConfigMap(configMapVolumeSource *v1.ConfigMapVolumeSource, configMaps []v1.ConfigMap) (*KubeVolume, error) {
var configMap *v1.ConfigMap
kv := &KubeVolume{Type: KubeVolumeTypeConfigMap, Items: map[string]string{}}
for _, cm := range configMaps {
if cm.Name == configMapVolumeSource.Name {
matchedCM := cm
// Set the source to the config map name
kv.Source = cm.Name
configMap = &matchedCM
break
}
}
if configMap == nil {
// If the volumeSource was optional, move on even if a matching configmap wasn't found
if *configMapVolumeSource.Optional {
kv.Source = configMapVolumeSource.Name
kv.Optional = *configMapVolumeSource.Optional
return kv, nil
}
return nil, errors.Errorf("no such ConfigMap %q", configMapVolumeSource.Name)
}
// If there are Items specified in the volumeSource, that overwrites the Data from the configmap
if len(configMapVolumeSource.Items) > 0 {
for _, item := range configMapVolumeSource.Items {
if val, ok := configMap.Data[item.Key]; ok {
kv.Items[item.Path] = val
}
}
} else {
return nil, errors.Errorf("HostPath and PersistentVolumeClaim are currently the only supported VolumeSource")
for k, v := range configMap.Data {
kv.Items[k] = v
}
}
return kv, nil
}
// Create a KubeVolume from one of the supported VolumeSource
func VolumeFromSource(volumeSource v1.VolumeSource, configMaps []v1.ConfigMap) (*KubeVolume, error) {
switch {
case volumeSource.HostPath != nil:
return VolumeFromHostPath(volumeSource.HostPath)
case volumeSource.PersistentVolumeClaim != nil:
return VolumeFromPersistentVolumeClaim(volumeSource.PersistentVolumeClaim)
case volumeSource.ConfigMap != nil:
return VolumeFromConfigMap(volumeSource.ConfigMap, configMaps)
default:
return nil, errors.Errorf("HostPath, ConfigMap, and PersistentVolumeClaim are currently the only supported VolumeSource")
}
}
// Create a map of volume name to KubeVolume
func InitializeVolumes(specVolumes []v1.Volume) (map[string]*KubeVolume, error) {
func InitializeVolumes(specVolumes []v1.Volume, configMaps []v1.ConfigMap) (map[string]*KubeVolume, error) {
volumes := make(map[string]*KubeVolume)
for _, specVolume := range specVolumes {
volume, err := VolumeFromSource(specVolume.VolumeSource)
volume, err := VolumeFromSource(specVolume.VolumeSource, configMaps)
if err != nil {
return nil, errors.Wrapf(err, "failed to create volume %q", specVolume.Name)
}

View File

@ -4,8 +4,6 @@ import (
"bytes"
"context"
"fmt"
"github.com/containers/podman/v3/pkg/bindings"
"github.com/containers/podman/v3/pkg/bindings/play"
"io/ioutil"
"net"
"net/url"
@ -17,6 +15,8 @@ import (
"time"
"github.com/containers/podman/v3/libpod/define"
"github.com/containers/podman/v3/pkg/bindings"
"github.com/containers/podman/v3/pkg/bindings/play"
"github.com/containers/podman/v3/pkg/util"
. "github.com/containers/podman/v3/test/utils"
"github.com/containers/storage/pkg/stringid"
@ -379,6 +379,18 @@ spec:
{{- if (eq .VolumeType "PersistentVolumeClaim") }}
persistentVolumeClaim:
claimName: {{ .PersistentVolumeClaim.ClaimName }}
{{- end }}
{{- if (eq .VolumeType "ConfigMap") }}
configMap:
name: {{ .ConfigMap.Name }}
optional: {{ .ConfigMap.Optional }}
{{- with .ConfigMap.Items }}
items:
{{- range . }}
- key: {{ .key }}
path: {{ .path }}
{{- end }}
{{- end }}
{{- end }}
{{ end }}
{{ end }}
@ -619,14 +631,14 @@ func createSecret(podmanTest *PodmanTestIntegration, name string, value []byte)
Expect(secret).Should(Exit(0))
}
// ConfigMap describes the options a kube yaml can be configured at configmap level
type ConfigMap struct {
// CM describes the options a kube yaml can be configured at configmap level
type CM struct {
Name string
Data map[string]string
}
func getConfigMap(options ...configMapOption) *ConfigMap {
cm := ConfigMap{
func getConfigMap(options ...configMapOption) *CM {
cm := CM{
Name: defaultConfigMapName,
Data: map[string]string{},
}
@ -638,16 +650,16 @@ func getConfigMap(options ...configMapOption) *ConfigMap {
return &cm
}
type configMapOption func(*ConfigMap)
type configMapOption func(*CM)
func withConfigMapName(name string) configMapOption {
return func(configmap *ConfigMap) {
return func(configmap *CM) {
configmap.Name = name
}
}
func withConfigMapData(k, v string) configMapOption {
return func(configmap *ConfigMap) {
return func(configmap *CM) {
configmap.Data[k] = v
}
}
@ -1053,11 +1065,18 @@ type PersistentVolumeClaim struct {
ClaimName string
}
type ConfigMap struct {
Name string
Items []map[string]string
Optional bool
}
type Volume struct {
VolumeType string
Name string
HostPath
PersistentVolumeClaim
ConfigMap
}
// getHostPathVolume takes a type and a location for a HostPath
@ -1085,6 +1104,20 @@ func getPersistentVolumeClaimVolume(vName string) *Volume {
}
}
// getConfigMap returns a new ConfigMap Volume given the name and items
// of the ConfigMap.
func getConfigMapVolume(vName string, items []map[string]string, optional bool) *Volume {
return &Volume{
VolumeType: "ConfigMap",
Name: defaultVolName,
ConfigMap: ConfigMap{
Name: vName,
Items: items,
Optional: optional,
},
}
}
type Env struct {
Name string
Value string
@ -2317,6 +2350,75 @@ VOLUME %s`, ALPINE, hostPathDir+"/")
Expect(inspect.OutputToString()).To(Equal(correct))
})
It("podman play kube ConfigMap volume with no items", func() {
volumeName := "cmVol"
cm := getConfigMap(withConfigMapName(volumeName), withConfigMapData("FOO", "foobar"))
cmYaml, err := getKubeYaml("configmap", cm)
Expect(err).To(BeNil())
ctr := getCtr(withVolumeMount("/test", false), withImage(BB))
pod := getPod(withVolume(getConfigMapVolume(volumeName, []map[string]string{}, false)), withCtr(ctr))
podYaml, err := getKubeYaml("pod", pod)
Expect(err).To(BeNil())
yamls := []string{cmYaml, podYaml}
err = generateMultiDocKubeYaml(yamls, kubeYaml)
Expect(err).To(BeNil())
kube := podmanTest.Podman([]string{"play", "kube", kubeYaml})
kube.WaitWithDefaultTimeout()
Expect(kube).Should(Exit(0))
cmData := podmanTest.Podman([]string{"exec", getCtrNameInPod(pod), "cat", "/test/FOO"})
cmData.WaitWithDefaultTimeout()
Expect(cmData).Should(Exit(0))
Expect(cmData.OutputToString()).To(Equal("foobar"))
})
It("podman play kube ConfigMap volume with items", func() {
volumeName := "cmVol"
cm := getConfigMap(withConfigMapName(volumeName), withConfigMapData("FOO", "foobar"))
cmYaml, err := getKubeYaml("configmap", cm)
Expect(err).To(BeNil())
volumeContents := []map[string]string{{
"key": "FOO",
"path": "BAR",
}}
ctr := getCtr(withVolumeMount("/test", false), withImage(BB))
pod := getPod(withVolume(getConfigMapVolume(volumeName, volumeContents, false)), withCtr(ctr))
podYaml, err := getKubeYaml("pod", pod)
Expect(err).To(BeNil())
yamls := []string{cmYaml, podYaml}
err = generateMultiDocKubeYaml(yamls, kubeYaml)
Expect(err).To(BeNil())
kube := podmanTest.Podman([]string{"play", "kube", kubeYaml})
kube.WaitWithDefaultTimeout()
Expect(kube).Should(Exit(0))
cmData := podmanTest.Podman([]string{"exec", getCtrNameInPod(pod), "cat", "/test/BAR"})
cmData.WaitWithDefaultTimeout()
Expect(cmData).Should(Exit(0))
Expect(cmData.OutputToString()).To(Equal("foobar"))
cmData = podmanTest.Podman([]string{"exec", getCtrNameInPod(pod), "cat", "/test/FOO"})
cmData.WaitWithDefaultTimeout()
Expect(cmData).Should(Not(Exit(0)))
})
It("podman play kube with a missing optional ConfigMap volume", func() {
volumeName := "cmVol"
ctr := getCtr(withVolumeMount("/test", false), withImage(BB))
pod := getPod(withVolume(getConfigMapVolume(volumeName, []map[string]string{}, true)), withCtr(ctr))
err = generateKubeYaml("pod", pod, kubeYaml)
Expect(err).To(BeNil())
kube := podmanTest.Podman([]string{"play", "kube", kubeYaml})
kube.WaitWithDefaultTimeout()
Expect(kube).Should(Exit(0))
})
It("podman play kube applies labels to pods", func() {
var numReplicas int32 = 5
expectedLabelKey := "key1"