play kube: add support for env vars defined from secrets

Add support for secretRef and secretKeyRef to allow env vars to be set
from a secret. As K8S secrets are dictionaries the secret value must
be a JSON dictionary compatible with the data field of a K8S secret
object. The keys must consist of alphanumeric characters, '-', '_'
or '.', and the values must be base64 encoded strings.

Signed-off-by: Alban Bedel <albeu@free.fr>
This commit is contained in:
Alban Bedel
2021-03-26 11:13:05 +01:00
parent e5ff694855
commit c59eb6f12b
4 changed files with 400 additions and 13 deletions

View File

@ -9,6 +9,7 @@ import (
"os" "os"
"strings" "strings"
"github.com/containers/common/pkg/secrets"
"github.com/containers/image/v5/types" "github.com/containers/image/v5/types"
"github.com/containers/podman/v3/libpod" "github.com/containers/podman/v3/libpod"
"github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/libpod/define"
@ -135,6 +136,12 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY
report entities.PlayKubeReport report entities.PlayKubeReport
) )
// Create the secret manager before hand
secretsManager, err := secrets.NewManager(ic.Libpod.GetSecretsStorageDir())
if err != nil {
return nil, err
}
// check for name collision between pod and container // check for name collision between pod and container
if podName == "" { if podName == "" {
return nil, errors.Errorf("pod does not have a name") return nil, errors.Errorf("pod does not have a name")
@ -261,16 +268,17 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY
} }
specgenOpts := kube.CtrSpecGenOptions{ specgenOpts := kube.CtrSpecGenOptions{
Container: container, Container: container,
Image: newImage, Image: newImage,
Volumes: volumes, Volumes: volumes,
PodID: pod.ID(), PodID: pod.ID(),
PodName: podName, PodName: podName,
PodInfraID: podInfraID, PodInfraID: podInfraID,
ConfigMaps: configMaps, ConfigMaps: configMaps,
SeccompPaths: seccompPaths, SeccompPaths: seccompPaths,
RestartPolicy: ctrRestartPolicy, RestartPolicy: ctrRestartPolicy,
NetNSIsHost: p.NetNS.IsHost(), NetNSIsHost: p.NetNS.IsHost(),
SecretsManager: secretsManager,
} }
specGen, err := kube.ToSpecGen(ctx, &specgenOpts) specGen, err := kube.ToSpecGen(ctx, &specgenOpts)
if err != nil { if err != nil {

View File

@ -2,11 +2,13 @@ package kube
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"net" "net"
"strings" "strings"
"github.com/containers/common/pkg/parse" "github.com/containers/common/pkg/parse"
"github.com/containers/common/pkg/secrets"
"github.com/containers/podman/v3/libpod/image" "github.com/containers/podman/v3/libpod/image"
ann "github.com/containers/podman/v3/pkg/annotations" ann "github.com/containers/podman/v3/pkg/annotations"
"github.com/containers/podman/v3/pkg/specgen" "github.com/containers/podman/v3/pkg/specgen"
@ -94,6 +96,8 @@ type CtrSpecGenOptions struct {
RestartPolicy string RestartPolicy string
// NetNSIsHost tells the container to use the host netns // NetNSIsHost tells the container to use the host netns
NetNSIsHost bool NetNSIsHost bool
// SecretManager to access the secrets
SecretsManager *secrets.SecretsManager
} }
func ToSpecGen(ctx context.Context, opts *CtrSpecGenOptions) (*specgen.SpecGenerator, error) { func ToSpecGen(ctx context.Context, opts *CtrSpecGenOptions) (*specgen.SpecGenerator, error) {
@ -331,7 +335,21 @@ func quantityToInt64(quantity *resource.Quantity) (int64, error) {
return 0, errors.Errorf("Quantity cannot be represented as int64: %v", quantity) return 0, errors.Errorf("Quantity cannot be represented as int64: %v", quantity)
} }
// envVarsFrom returns all key-value pairs as env vars from a configMap that matches the envFrom setting of a container // read a k8s secret in JSON format from the secret manager
func k8sSecretFromSecretManager(name string, secretsManager *secrets.SecretsManager) (map[string][]byte, error) {
_, jsonSecret, err := secretsManager.LookupSecretData(name)
if err != nil {
return nil, err
}
var secrets map[string][]byte
if err := json.Unmarshal(jsonSecret, &secrets); err != nil {
return nil, errors.Errorf("Secret %v is not valid JSON: %v", name, err)
}
return secrets, nil
}
// envVarsFrom returns all key-value pairs as env vars from a configMap or secret that matches the envFrom setting of a container
func envVarsFrom(envFrom v1.EnvFromSource, opts *CtrSpecGenOptions) (map[string]string, error) { func envVarsFrom(envFrom v1.EnvFromSource, opts *CtrSpecGenOptions) (map[string]string, error) {
envs := map[string]string{} envs := map[string]string{}
@ -352,11 +370,23 @@ func envVarsFrom(envFrom v1.EnvFromSource, opts *CtrSpecGenOptions) (map[string]
} }
} }
if envFrom.SecretRef != nil {
secRef := envFrom.SecretRef
secret, err := k8sSecretFromSecretManager(secRef.Name, opts.SecretsManager)
if err == nil {
for k, v := range secret {
envs[k] = string(v)
}
} else if secRef.Optional == nil || !*secRef.Optional {
return nil, err
}
}
return envs, nil return envs, nil
} }
// envVarValue returns the environment variable value configured within the container's env setting. // envVarValue returns the environment variable value configured within the container's env setting.
// It gets the value from a configMap if specified, otherwise returns env.Value // It gets the value from a configMap or secret if specified, otherwise returns env.Value
func envVarValue(env v1.EnvVar, opts *CtrSpecGenOptions) (string, error) { func envVarValue(env v1.EnvVar, opts *CtrSpecGenOptions) (string, error) {
if env.ValueFrom != nil { if env.ValueFrom != nil {
if env.ValueFrom.ConfigMapKeyRef != nil { if env.ValueFrom.ConfigMapKeyRef != nil {
@ -377,6 +407,21 @@ func envVarValue(env v1.EnvVar, opts *CtrSpecGenOptions) (string, error) {
} }
return "", nil return "", nil
} }
if env.ValueFrom.SecretKeyRef != nil {
secKeyRef := env.ValueFrom.SecretKeyRef
secret, err := k8sSecretFromSecretManager(secKeyRef.Name, opts.SecretsManager)
if err == nil {
if val, ok := secret[secKeyRef.Key]; ok {
return string(val), nil
}
err = errors.Errorf("Secret %v has not %v key", secKeyRef.Name, secKeyRef.Key)
}
if secKeyRef.Optional == nil || !*secKeyRef.Optional {
return "", errors.Errorf("Cannot set env %v: %v", env.Name, err)
}
return "", nil
}
} }
return env.Value, nil return env.Value, nil

View File

@ -1,14 +1,43 @@
package kube package kube
import ( import (
"encoding/json"
"io/ioutil"
"os"
"testing" "testing"
"github.com/containers/common/pkg/secrets"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
v12 "k8s.io/apimachinery/pkg/apis/meta/v1" v12 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
func createSecrets(t *testing.T, d string) *secrets.SecretsManager {
secretsManager, err := secrets.NewManager(d)
assert.NoError(t, err)
driver := "file"
driverOpts := map[string]string{
"path": d,
}
for _, s := range k8sSecrets {
data, err := json.Marshal(s.Data)
assert.NoError(t, err)
_, err = secretsManager.Store(s.ObjectMeta.Name, data, driver, driverOpts)
assert.NoError(t, err)
}
return secretsManager
}
func TestEnvVarsFrom(t *testing.T) { func TestEnvVarsFrom(t *testing.T) {
d, err := ioutil.TempDir("", "secrets")
assert.NoError(t, err)
defer os.RemoveAll(d)
secretsManager := createSecrets(t, d)
tests := []struct { tests := []struct {
name string name string
envFrom v1.EnvFromSource envFrom v1.EnvFromSource
@ -95,6 +124,54 @@ func TestEnvVarsFrom(t *testing.T) {
true, true,
map[string]string{}, map[string]string{},
}, },
{
"SecretExists",
v1.EnvFromSource{
SecretRef: &v1.SecretEnvSource{
LocalObjectReference: v1.LocalObjectReference{
Name: "foo",
},
},
},
CtrSpecGenOptions{
SecretsManager: secretsManager,
},
true,
map[string]string{
"myvar": "foo",
},
},
{
"SecretDoesNotExist",
v1.EnvFromSource{
SecretRef: &v1.SecretEnvSource{
LocalObjectReference: v1.LocalObjectReference{
Name: "doesnotexist",
},
},
},
CtrSpecGenOptions{
SecretsManager: secretsManager,
},
false,
nil,
},
{
"OptionalSecretDoesNotExist",
v1.EnvFromSource{
SecretRef: &v1.SecretEnvSource{
LocalObjectReference: v1.LocalObjectReference{
Name: "doesnotexist",
},
Optional: &optional,
},
},
CtrSpecGenOptions{
SecretsManager: secretsManager,
},
true,
map[string]string{},
},
} }
for _, test := range tests { for _, test := range tests {
@ -108,6 +185,11 @@ func TestEnvVarsFrom(t *testing.T) {
} }
func TestEnvVarValue(t *testing.T) { func TestEnvVarValue(t *testing.T) {
d, err := ioutil.TempDir("", "secrets")
assert.NoError(t, err)
defer os.RemoveAll(d)
secretsManager := createSecrets(t, d)
tests := []struct { tests := []struct {
name string name string
envVar v1.EnvVar envVar v1.EnvVar
@ -251,6 +333,103 @@ func TestEnvVarValue(t *testing.T) {
true, true,
"", "",
}, },
{
"SecretExists",
v1.EnvVar{
Name: "FOO",
ValueFrom: &v1.EnvVarSource{
SecretKeyRef: &v1.SecretKeySelector{
LocalObjectReference: v1.LocalObjectReference{
Name: "foo",
},
Key: "myvar",
},
},
},
CtrSpecGenOptions{
SecretsManager: secretsManager,
},
true,
"foo",
},
{
"ContainerKeyDoesNotExistInSecret",
v1.EnvVar{
Name: "FOO",
ValueFrom: &v1.EnvVarSource{
SecretKeyRef: &v1.SecretKeySelector{
LocalObjectReference: v1.LocalObjectReference{
Name: "foo",
},
Key: "doesnotexist",
},
},
},
CtrSpecGenOptions{
SecretsManager: secretsManager,
},
false,
"",
},
{
"OptionalContainerKeyDoesNotExistInSecret",
v1.EnvVar{
Name: "FOO",
ValueFrom: &v1.EnvVarSource{
SecretKeyRef: &v1.SecretKeySelector{
LocalObjectReference: v1.LocalObjectReference{
Name: "foo",
},
Key: "doesnotexist",
Optional: &optional,
},
},
},
CtrSpecGenOptions{
SecretsManager: secretsManager,
},
true,
"",
},
{
"SecretDoesNotExist",
v1.EnvVar{
Name: "FOO",
ValueFrom: &v1.EnvVarSource{
SecretKeyRef: &v1.SecretKeySelector{
LocalObjectReference: v1.LocalObjectReference{
Name: "doesnotexist",
},
Key: "myvar",
},
},
},
CtrSpecGenOptions{
SecretsManager: secretsManager,
},
false,
"",
},
{
"OptionalSecretDoesNotExist",
v1.EnvVar{
Name: "FOO",
ValueFrom: &v1.EnvVarSource{
SecretKeyRef: &v1.SecretKeySelector{
LocalObjectReference: v1.LocalObjectReference{
Name: "doesnotexist",
},
Key: "myvar",
Optional: &optional,
},
},
},
CtrSpecGenOptions{
SecretsManager: secretsManager,
},
true,
"",
},
} }
for _, test := range tests { for _, test := range tests {
@ -289,3 +468,28 @@ var configMapList = []v1.ConfigMap{
} }
var optional = true var optional = true
var k8sSecrets = []v1.Secret{
{
TypeMeta: v12.TypeMeta{
Kind: "Secret",
},
ObjectMeta: v12.ObjectMeta{
Name: "bar",
},
Data: map[string][]byte{
"myvar": []byte("bar"),
},
},
{
TypeMeta: v12.TypeMeta{
Kind: "Secret",
},
ObjectMeta: v12.ObjectMeta{
Name: "foo",
},
Data: map[string][]byte{
"myvar": []byte("foo"),
},
},
}

View File

@ -142,7 +142,15 @@ spec:
name: {{ .RefName }} name: {{ .RefName }}
key: {{ .RefKey }} key: {{ .RefKey }}
optional: {{ .Optional }} optional: {{ .Optional }}
{{ else }} {{ end }}
{{ if (eq .ValueFrom "secret") }}
valueFrom:
secretKeyRef:
name: {{ .RefName }}
key: {{ .RefKey }}
optional: {{ .Optional }}
{{ end }}
{{ if (eq .ValueFrom "") }}
value: {{ .Value }} value: {{ .Value }}
{{ end }} {{ end }}
{{ end }} {{ end }}
@ -154,6 +162,11 @@ spec:
name: {{ .Name }} name: {{ .Name }}
optional: {{ .Optional }} optional: {{ .Optional }}
{{ end }} {{ end }}
{{ if (eq .From "secret") }}
- secretRef:
name: {{ .Name }}
optional: {{ .Optional }}
{{ end }}
{{ end }} {{ end }}
{{ end }} {{ end }}
image: {{ .Image }} image: {{ .Image }}
@ -342,6 +355,8 @@ var (
seccompPwdEPERM = []byte(`{"defaultAction":"SCMP_ACT_ALLOW","syscalls":[{"name":"getcwd","action":"SCMP_ACT_ERRNO"}]}`) seccompPwdEPERM = []byte(`{"defaultAction":"SCMP_ACT_ALLOW","syscalls":[{"name":"getcwd","action":"SCMP_ACT_ERRNO"}]}`)
// CPU Period in ms // CPU Period in ms
defaultCPUPeriod = 100 defaultCPUPeriod = 100
// Default secret in JSON. Note that the values ("foo" and "bar") are base64 encoded.
defaultSecret = []byte(`{"FOO":"Zm9v","BAR":"YmFy"}`)
) )
func writeYaml(content string, fileName string) error { func writeYaml(content string, fileName string) error {
@ -409,6 +424,16 @@ func generateMultiDocKubeYaml(kubeObjects []string, pathname string) error {
return writeYaml(multiKube, pathname) return writeYaml(multiKube, pathname)
} }
func createSecret(podmanTest *PodmanTestIntegration, name string, value []byte) {
secretFilePath := filepath.Join(podmanTest.TempDir, "secret")
err := ioutil.WriteFile(secretFilePath, value, 0755)
Expect(err).To(BeNil())
secret := podmanTest.Podman([]string{"secret", "create", name, secretFilePath})
secret.WaitWithDefaultTimeout()
Expect(secret.ExitCode()).To(Equal(0))
}
// ConfigMap describes the options a kube yaml can be configured at configmap level // ConfigMap describes the options a kube yaml can be configured at configmap level
type ConfigMap struct { type ConfigMap struct {
Name string Name string
@ -1186,6 +1211,111 @@ var _ = Describe("Podman play kube", func() {
Expect(kube.ExitCode()).To(Equal(0)) Expect(kube.ExitCode()).To(Equal(0))
}) })
It("podman play kube test env value from secret", func() {
createSecret(podmanTest, "foo", defaultSecret)
pod := getPod(withCtr(getCtr(withEnv("FOO", "", "secret", "foo", "FOO", false))))
err = generateKubeYaml("pod", pod, kubeYaml)
Expect(err).To(BeNil())
kube := podmanTest.Podman([]string{"play", "kube", kubeYaml})
kube.WaitWithDefaultTimeout()
Expect(kube.ExitCode()).To(Equal(0))
inspect := podmanTest.Podman([]string{"inspect", getCtrNameInPod(pod), "--format", "'{{ .Config.Env }}'"})
inspect.WaitWithDefaultTimeout()
Expect(inspect.ExitCode()).To(Equal(0))
Expect(inspect.OutputToString()).To(ContainSubstring(`FOO=foo`))
})
It("podman play kube test required env value from missing secret", func() {
pod := getPod(withCtr(getCtr(withEnv("FOO", "", "secret", "foo", "FOO", false))))
err = generateKubeYaml("pod", pod, kubeYaml)
Expect(err).To(BeNil())
kube := podmanTest.Podman([]string{"play", "kube", kubeYaml})
kube.WaitWithDefaultTimeout()
Expect(kube.ExitCode()).To(Not(Equal(0)))
})
It("podman play kube test required env value from secret with missing key", func() {
createSecret(podmanTest, "foo", defaultSecret)
pod := getPod(withCtr(getCtr(withEnv("FOO", "", "secret", "foo", "MISSING", false))))
err = generateKubeYaml("pod", pod, kubeYaml)
Expect(err).To(BeNil())
kube := podmanTest.Podman([]string{"play", "kube", kubeYaml})
kube.WaitWithDefaultTimeout()
Expect(kube.ExitCode()).To(Not(Equal(0)))
})
It("podman play kube test optional env value from missing secret", func() {
pod := getPod(withCtr(getCtr(withEnv("FOO", "", "secret", "foo", "FOO", true))))
err = generateKubeYaml("pod", pod, kubeYaml)
Expect(err).To(BeNil())
kube := podmanTest.Podman([]string{"play", "kube", kubeYaml})
kube.WaitWithDefaultTimeout()
Expect(kube.ExitCode()).To(Equal(0))
inspect := podmanTest.Podman([]string{"inspect", getCtrNameInPod(pod), "--format", "'{{ range .Config.Env }}[{{ . }}]{{end}}'"})
inspect.WaitWithDefaultTimeout()
Expect(inspect.ExitCode()).To(Equal(0))
Expect(inspect.OutputToString()).To(ContainSubstring(`[FOO=]`))
})
It("podman play kube test optional env value from secret with missing key", func() {
createSecret(podmanTest, "foo", defaultSecret)
pod := getPod(withCtr(getCtr(withEnv("FOO", "", "secret", "foo", "MISSING", true))))
err = generateKubeYaml("pod", pod, kubeYaml)
Expect(err).To(BeNil())
kube := podmanTest.Podman([]string{"play", "kube", kubeYaml})
kube.WaitWithDefaultTimeout()
Expect(kube.ExitCode()).To(Equal(0))
inspect := podmanTest.Podman([]string{"inspect", getCtrNameInPod(pod), "--format", "'{{ range .Config.Env }}[{{ . }}]{{end}}'"})
inspect.WaitWithDefaultTimeout()
Expect(inspect.ExitCode()).To(Equal(0))
Expect(inspect.OutputToString()).To(ContainSubstring(`[FOO=]`))
})
It("podman play kube test get all key-value pairs from secret as envs", func() {
createSecret(podmanTest, "foo", defaultSecret)
pod := getPod(withCtr(getCtr(withEnvFrom("foo", "secret", false))))
err = generateKubeYaml("pod", pod, kubeYaml)
Expect(err).To(BeNil())
kube := podmanTest.Podman([]string{"play", "kube", kubeYaml})
kube.WaitWithDefaultTimeout()
Expect(kube.ExitCode()).To(Equal(0))
inspect := podmanTest.Podman([]string{"inspect", getCtrNameInPod(pod), "--format", "'{{ .Config.Env }}'"})
inspect.WaitWithDefaultTimeout()
Expect(inspect.ExitCode()).To(Equal(0))
Expect(inspect.OutputToString()).To(ContainSubstring(`FOO=foo`))
Expect(inspect.OutputToString()).To(ContainSubstring(`BAR=bar`))
})
It("podman play kube test get all key-value pairs from required secret as envs", func() {
pod := getPod(withCtr(getCtr(withEnvFrom("missing_secret", "secret", false))))
err = generateKubeYaml("pod", pod, kubeYaml)
Expect(err).To(BeNil())
kube := podmanTest.Podman([]string{"play", "kube", kubeYaml})
kube.WaitWithDefaultTimeout()
Expect(kube.ExitCode()).To(Not(Equal(0)))
})
It("podman play kube test get all key-value pairs from optional secret as envs", func() {
pod := getPod(withCtr(getCtr(withEnvFrom("missing_secret", "secret", true))))
err = generateKubeYaml("pod", pod, kubeYaml)
Expect(err).To(BeNil())
kube := podmanTest.Podman([]string{"play", "kube", kubeYaml})
kube.WaitWithDefaultTimeout()
Expect(kube.ExitCode()).To(Equal(0))
})
It("podman play kube test hostname", func() { It("podman play kube test hostname", func() {
pod := getPod() pod := getPod()
err := generateKubeYaml("pod", pod, kubeYaml) err := generateKubeYaml("pod", pod, kubeYaml)