Skip to content

Commit

Permalink
Add support for configmap volumes to play kube
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
umohnani8 committed Dec 2, 2021
1 parent 7324d94 commit 7d331d3
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 37 deletions.
71 changes: 50 additions & 21 deletions pkg/domain/infra/abi/play.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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()
Expand Down
12 changes: 12 additions & 0 deletions pkg/specgen/generate/kube/kube.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
}
Expand Down
64 changes: 57 additions & 7 deletions pkg/specgen/generate/kube/volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type KubeVolumeType int
const (
KubeVolumeTypeBindMount KubeVolumeType = iota
KubeVolumeTypeNamed KubeVolumeType = iota
KubeVolumeTypeConfigMap KubeVolumeType = iota
)

// nolint:golint
Expand All @@ -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
Expand Down Expand Up @@ -98,23 +107,64 @@ func VolumeFromPersistentVolumeClaim(claim *v1.PersistentVolumeClaimVolumeSource
}, nil
}

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 {
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) (*KubeVolume, error) {
if volumeSource.HostPath != nil {
func VolumeFromSource(volumeSource v1.VolumeSource, configMaps []v1.ConfigMap) (*KubeVolume, error) {
switch {
case volumeSource.HostPath != nil:
return VolumeFromHostPath(volumeSource.HostPath)
} else if volumeSource.PersistentVolumeClaim != nil {
case volumeSource.PersistentVolumeClaim != nil:
return VolumeFromPersistentVolumeClaim(volumeSource.PersistentVolumeClaim)
} else {
return nil, errors.Errorf("HostPath and PersistentVolumeClaim are currently the only supported VolumeSource")
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)
}
Expand Down
120 changes: 111 additions & 9 deletions test/e2e/play_kube_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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 }}
Expand Down Expand Up @@ -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{},
}
Expand All @@ -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
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 7d331d3

Please sign in to comment.