Fix up handling of environment variables

The way docker works is if a user specifies a non `-e Name=Value`, IE
just a `-e Name`, then the environment variable Name from the clients
OS.ENV is used.

Also by default Docker containers run with the HOSTNAME environment set
to the HOSTNAME specified for the container.

Signed-off-by: Daniel J Walsh <dwalsh@redhat.com>

Closes: #21
Approved by: baude
This commit is contained in:
Daniel J Walsh
2017-11-07 10:03:46 -05:00
committed by Atomic Bot
parent 3b72af6147
commit 57599f0075
8 changed files with 94 additions and 107 deletions

View File

@ -19,8 +19,8 @@ tests:
inherit: true inherit: true
host: host:
distro: centos/7/atomic/alpha distro: centos/7/atomic/smoketested
specs: specs:
ram: 8192 ram: 8192
context: centos/7/atomic/alpha context: centos/7/atomic/smoketested

View File

@ -27,7 +27,10 @@ const (
) )
var ( var (
defaultEnvVariables = []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "TERM=xterm"} defaultEnvVariables = map[string]string{
"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TERM": "xterm",
}
) )
type createResourceConfig struct { type createResourceConfig struct {
@ -64,16 +67,16 @@ type createConfig struct {
cidFile string cidFile string
cgroupParent string // cgroup-parent cgroupParent string // cgroup-parent
command []string command []string
detach bool // detach detach bool // detach
devices []*pb.Device // device devices []*pb.Device // device
dnsOpt []string //dns-opt dnsOpt []string //dns-opt
dnsSearch []string //dns-search dnsSearch []string //dns-search
dnsServers []string //dns dnsServers []string //dns
entrypoint string //entrypoint entrypoint string //entrypoint
env []string //env env map[string]string //env
expose []string //expose expose []string //expose
groupAdd []uint32 // group-add groupAdd []uint32 // group-add
hostname string //hostname hostname string //hostname
image string image string
interactive bool //interactive interactive bool //interactive
ip6Address string //ipv6 ip6Address string //ipv6
@ -264,8 +267,8 @@ func parseCreateOpts(c *cli.Context, runtime *libpod.Runtime) (*createConfig, er
return &createConfig{}, errors.Wrapf(err, "unable to process labels") return &createConfig{}, errors.Wrapf(err, "unable to process labels")
} }
// ENVIRONMENT VARIABLES // ENVIRONMENT VARIABLES
env, err := getAllEnvironmentVariables(c.StringSlice("env-file"), c.StringSlice("env")) env := defaultEnvVariables
if err != nil { if err := readKVStrings(env, c.StringSlice("env-file"), c.StringSlice("env")); err != nil {
return &createConfig{}, errors.Wrapf(err, "unable to process environment variables") return &createConfig{}, errors.Wrapf(err, "unable to process environment variables")
} }
@ -338,7 +341,7 @@ func parseCreateOpts(c *cli.Context, runtime *libpod.Runtime) (*createConfig, er
dnsServers: c.StringSlice("dns"), dnsServers: c.StringSlice("dns"),
entrypoint: c.String("entrypoint"), entrypoint: c.String("entrypoint"),
env: env, env: env,
expose: c.StringSlice("env"), expose: c.StringSlice("expose"),
groupAdd: groupAdd, groupAdd: groupAdd,
hostname: c.String("hostname"), hostname: c.String("hostname"),
image: image, image: image,

View File

@ -7,44 +7,14 @@ import (
) )
func getAllLabels(labelFile, inputLabels []string) (map[string]string, error) { func getAllLabels(labelFile, inputLabels []string) (map[string]string, error) {
var labelValues []string
labels := make(map[string]string) labels := make(map[string]string)
labelValues, labelErr := readKVStrings(labelFile, inputLabels) labelErr := readKVStrings(labels, labelFile, inputLabels)
if labelErr != nil { if labelErr != nil {
return labels, errors.Wrapf(labelErr, "unable to process labels from --label and label-file") return labels, errors.Wrapf(labelErr, "unable to process labels from --label and label-file")
} }
// Process KEY=VALUE stringslice in string map for WithLabels func
if len(labelValues) > 0 {
for _, i := range labelValues {
spliti := strings.Split(i, "=")
if len(spliti) < 2 {
return labels, errors.Errorf("labels must be in KEY=VALUE format: %s is invalid", i)
}
labels[spliti[0]] = spliti[1]
}
}
return labels, nil return labels, nil
} }
func getAllEnvironmentVariables(envFiles, envInput []string) ([]string, error) {
env, err := readKVStrings(envFiles, envInput)
if err != nil {
return []string{}, errors.Wrapf(err, "unable to process variables from --env and --env-file")
}
// Add default environment variables if nothing defined
if len(env) == 0 {
env = append(env, defaultEnvVariables...)
}
// Each environment variable must be in the K=V format
for _, i := range env {
spliti := strings.Split(i, "=")
if len(spliti) != 2 {
return env, errors.Errorf("environment variables must be in the format KEY=VALUE: %s is invalid", i)
}
}
return env, nil
}
func convertStringSliceToMap(strSlice []string, delimiter string) (map[string]string, error) { func convertStringSliceToMap(strSlice []string, delimiter string) (map[string]string, error) {
sysctl := make(map[string]string) sysctl := make(map[string]string)
for _, inputSysctl := range strSlice { for _, inputSysctl := range strSlice {

View File

@ -68,32 +68,3 @@ func TestGetAllLabelsFile(t *testing.T) {
result, _ := getAllLabels(fileLabels, Var1) result, _ := getAllLabels(fileLabels, Var1)
assert.Equal(t, len(result), 3) assert.Equal(t, len(result), 3)
} }
func TestGetAllEnvironmentVariables(t *testing.T) {
fileEnvs := []string{}
result, _ := getAllEnvironmentVariables(fileEnvs, Var1)
assert.Equal(t, len(result), 2)
}
func TestGetAllEnvironmentVariablesBadKeyValue(t *testing.T) {
inEnvs := []string{"ONE1", "TWO=2"}
fileEnvs := []string{}
_, err := getAllEnvironmentVariables(fileEnvs, inEnvs)
assert.Error(t, err, assert.AnError)
}
func TestGetAllEnvironmentVariablesBadEnvFile(t *testing.T) {
fileEnvs := []string{"/foobar5001/be"}
_, err := getAllEnvironmentVariables(fileEnvs, Var1)
assert.Error(t, err, assert.AnError)
}
func TestGetAllEnvironmentVariablesFile(t *testing.T) {
content := []byte("THREE=3")
tFile, err := createTmpFile(content)
defer os.Remove(tFile)
assert.NoError(t, err)
fileEnvs := []string{tFile}
result, _ := getAllEnvironmentVariables(fileEnvs, Var1)
assert.Equal(t, len(result), 3)
}

View File

@ -327,55 +327,62 @@ func doesEnvExist(name string) bool {
// reads a file of line terminated key=value pairs, and overrides any keys // reads a file of line terminated key=value pairs, and overrides any keys
// present in the file with additional pairs specified in the override parameter // present in the file with additional pairs specified in the override parameter
// for env-file and labels-file flags // for env-file and labels-file flags
func readKVStrings(files []string, override []string) ([]string, error) { func readKVStrings(env map[string]string, files []string, override []string) error {
envVariables := []string{}
for _, ef := range files { for _, ef := range files {
parsedVars, err := parseEnvFile(ef) if err := parseEnvFile(env, ef); err != nil {
if err != nil { return err
return nil, err
} }
envVariables = append(envVariables, parsedVars...)
} }
// parse the '-e' and '--env' after, to allow override for _, line := range override {
envVariables = append(envVariables, override...) if err := parseEnv(env, line); err != nil {
return err
}
}
return nil
}
return envVariables, nil func parseEnv(env map[string]string, line string) error {
data := strings.SplitN(line, "=", 2)
// trim the front of a variable, but nothing else
name := strings.TrimLeft(data[0], whiteSpaces)
if strings.ContainsAny(name, whiteSpaces) {
return errors.Errorf("name %q has white spaces, poorly formatted name", name)
}
if len(data) > 1 {
env[name] = data[1]
} else {
// if only a pass-through variable is given, clean it up.
val, exists := os.LookupEnv(name)
if !exists {
return errors.Errorf("environment variable %q does not exist", name)
}
env[name] = val
}
return nil
} }
// parseEnvFile reads a file with environment variables enumerated by lines // parseEnvFile reads a file with environment variables enumerated by lines
func parseEnvFile(filename string) ([]string, error) { func parseEnvFile(env map[string]string, filename string) error {
fh, err := os.Open(filename) fh, err := os.Open(filename)
if err != nil { if err != nil {
return []string{}, err return err
} }
defer fh.Close() defer fh.Close()
lines := []string{}
scanner := bufio.NewScanner(fh) scanner := bufio.NewScanner(fh)
for scanner.Scan() { for scanner.Scan() {
// trim the line from all leading whitespace first // trim the line from all leading whitespace first
line := strings.TrimLeft(scanner.Text(), whiteSpaces) line := strings.TrimLeft(scanner.Text(), whiteSpaces)
// line is not empty, and not starting with '#' // line is not empty, and not starting with '#'
if len(line) > 0 && !strings.HasPrefix(line, "#") { if len(line) > 0 && !strings.HasPrefix(line, "#") {
data := strings.SplitN(line, "=", 2) if err := parseEnv(env, line); err != nil {
return err
// trim the front of a variable, but nothing else
variable := strings.TrimLeft(data[0], whiteSpaces)
if strings.ContainsAny(variable, whiteSpaces) {
return []string{}, errors.Errorf("variable %q has white spaces, poorly formatted environment", variable)
}
if len(data) > 1 {
// pass the value through, no trimming
lines = append(lines, fmt.Sprintf("%s=%s", variable, data[1]))
} else {
// if only a pass-through variable is given, clean it up.
lines = append(lines, fmt.Sprintf("%s=%s", strings.TrimSpace(line), os.Getenv(line)))
} }
} }
} }
return lines, scanner.Err() return scanner.Err()
} }
// NsIpc represents the container ipc stack. // NsIpc represents the container ipc stack.

View File

@ -52,6 +52,9 @@ func createConfigToOCISpec(config *createConfig) (*spec.Spec, error) {
} }
g.SetRootReadonly(config.readOnlyRootfs) g.SetRootReadonly(config.readOnlyRootfs)
g.SetHostname(config.hostname) g.SetHostname(config.hostname)
if config.hostname != "" {
g.AddProcessEnv("HOSTNAME", config.hostname)
}
for _, sysctl := range config.sysctl { for _, sysctl := range config.sysctl {
s := strings.SplitN(sysctl, "=", 2) s := strings.SplitN(sysctl, "=", 2)
@ -124,6 +127,10 @@ func createConfigToOCISpec(config *createConfig) (*spec.Spec, error) {
g.AddTmpfsMount(spliti[0], options) g.AddTmpfsMount(spliti[0], options)
} }
for name, val := range config.env {
g.AddProcessEnv(name, val)
}
configSpec := g.Spec() configSpec := g.Spec()
if config.seccompProfilePath != "" && config.seccompProfilePath != "unconfined" { if config.seccompProfilePath != "" && config.seccompProfilePath != "unconfined" {
@ -138,8 +145,6 @@ func createConfigToOCISpec(config *createConfig) (*spec.Spec, error) {
configSpec.Linux.Seccomp = &seccompConfig configSpec.Linux.Seccomp = &seccompConfig
} }
configSpec.Process.Env = config.env
// BIND MOUNTS // BIND MOUNTS
configSpec.Mounts = append(configSpec.Mounts, config.GetVolumeMounts()...) configSpec.Mounts = append(configSpec.Mounts, config.GetVolumeMounts()...)

View File

@ -14,7 +14,7 @@ elif [[ ! -z "$TRAVIS" ]]; then
elif [[ ! -z "$PAPR" ]]; then elif [[ ! -z "$PAPR" ]]; then
CRIO_ROOT="/var/tmp/checkout" CRIO_ROOT="/var/tmp/checkout"
else else
CRIO_ROOT=$(cd "$INTEGRATION_ROOT/../.."; pwd -P)} CRIO_ROOT=$(cd "$INTEGRATION_ROOT/.."; pwd -P)
fi fi
KPOD_BINARY=${KPOD_BINARY:-${CRIO_ROOT}/bin/kpod} KPOD_BINARY=${KPOD_BINARY:-${CRIO_ROOT}/bin/kpod}

View File

@ -60,3 +60,34 @@ ALPINE="docker.io/library/alpine:latest"
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
} }
@test "run environment test" {
${KPOD_BINARY} ${KPOD_OPTIONS} pull ${ALPINE}
run bash -c "${KPOD_BINARY} ${KPOD_OPTIONS} run -env FOO=BAR ${ALPINE} printenv FOO | tr -d '\r'"
echo "$output"
[ "$status" -eq 0 ]
[ $output = "BAR" ]
run bash -c "${KPOD_BINARY} ${KPOD_OPTIONS} run -env PATH="/bin" ${ALPINE} printenv PATH | tr -d '\r'"
echo "$output"
[ "$status" -eq 0 ]
[ $output = "/bin" ]
run bash -c "export FOO=BAR; ${KPOD_BINARY} ${KPOD_OPTIONS} run -env FOO ${ALPINE} printenv FOO | tr -d '\r'"
echo "$output"
[ "$status" -eq 0 ]
[ "$output" = "BAR" ]
run ${KPOD_BINARY} ${KPOD_OPTIONS} run -env FOO ${ALPINE} printenv
echo "$output"
[ "$status" -ne 0 ]
# We don't currently set the hostname in containers, since we are not setting up
# networking. As soon as kpod run gets network support we need to uncomment this
# test.
# run bash -c "${KPOD_BINARY} ${KPOD_OPTIONS} run ${ALPINE} sh -c printenv | grep HOSTNAME"
# echo "$output"
# [ "$status" -eq 0 ]
}