Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for configmap volumes to play kube #12440

Merged
merged 1 commit into from
Dec 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
umohnani8 marked this conversation as resolved.
Show resolved Hide resolved
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 {
umohnani8 marked this conversation as resolved.
Show resolved Hide resolved
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