Skip to content

Commit

Permalink
feature: cli supports --env-file
Browse files Browse the repository at this point in the history
Signed-off-by: Lang Chi <21860405@zju.edu.cn>
  • Loading branch information
lang710 committed May 28, 2019
1 parent 7235f82 commit e6b4612
Show file tree
Hide file tree
Showing 9 changed files with 285 additions and 0 deletions.
1 change: 1 addition & 0 deletions cli/common_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func addCommonFlags(flagSet *pflag.FlagSet) *container {
flagSet.BoolVar(&c.enableLxcfs, "enableLxcfs", false, "Enable lxcfs for the container, only effective when enable-lxcfs switched on in Pouchd")
flagSet.StringVar(&c.entrypoint, "entrypoint", "", "Overwrite the default ENTRYPOINT of the image")
flagSet.StringArrayVarP(&c.env, "env", "e", nil, "Set environment variables for container('--env A=' means setting env A to empty, '--env B' means removing env B from container env inherited from image)")
flagSet.StringArrayVar(&c.envfile, "env-file", nil, "Read in a file of environment variables")
flagSet.StringVar(&c.hostname, "hostname", "", "Set container's hostname")
flagSet.BoolVar(&c.disableNetworkFiles, "disable-network-files", false, "Disable the generation of network files(/etc/hostname, /etc/hosts and /etc/resolv.conf) for container. If true, no network files will be generated. Default false")

Expand Down
1 change: 1 addition & 0 deletions cli/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type container struct {
volumesFrom []string
runtime string
env []string
envfile []string
entrypoint string
workdir string
user string
Expand Down
6 changes: 6 additions & 0 deletions cli/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ func (cc *CreateCommand) runCreate(args []string) error {
if err != nil {
return fmt.Errorf("failed to create container: %v", err)
}

// collect all the environment variables for the container
config.Env, err = readKVStrings(cc.envfile, cc.env)
if err != nil {
return nil
}
config.ContainerConfig.OpenStdin = cc.openstdin

config.Image = args[0]
Expand Down
73 changes: 73 additions & 0 deletions cli/envfile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package main

import (
"bufio"
"fmt"
"os"
"strings"
)

// 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
func readKVStrings(files []string, override []string) ([]string, error) {
envVariables := []string{}
for _, ef := range files {
parsedVars, err := parseEnvFile(ef)
if err != nil {
return nil, err
}
envVariables = append(envVariables, parsedVars...)
}
// parse the '-e' and '--env' after, to allow override
envVariables = append(envVariables, override...)

return envVariables, nil
}

// parseEnvFile reads a file with environment variables enumerated by lines
func parseEnvFile(filename string) ([]string, error) {
fh, err := os.Open(filename)
if err != nil {
return []string{}, err
}
defer fh.Close()

lines := []string{}
scanner := bufio.NewScanner(fh)
for scanner.Scan() {
// trim the line from all leading whitespace first
line := strings.TrimLeft(scanner.Text(), whiteSpaces)
// line is not empty, and not starting with '#'
if len(line) > 0 && !strings.HasPrefix(line, "#") {
data := strings.SplitN(line, "=", 2)

// trim the front of a variable, but nothing else
variable := strings.TrimLeft(data[0], whiteSpaces)
if strings.ContainsAny(variable, whiteSpaces) {
return []string{}, ErrBadEnvVariable{fmt.Sprintf("variable '%s' has white spaces", 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()
}

const whiteSpaces = " \t"

// ErrBadEnvVariable typed error for bad environment variable
type ErrBadEnvVariable struct {
msg string
}

// Error implements error interface.
func (e ErrBadEnvVariable) Error() string {
return fmt.Sprintf("poorly formatted environment: %s", e.msg)
}
142 changes: 142 additions & 0 deletions cli/envfile_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package main

import (
"bufio"
"fmt"
"io/ioutil"
"os"
"reflect"
"strings"
"testing"
)

func tmpFileWithContent(content string, t *testing.T) string {
tmpFile, err := ioutil.TempFile("", "envfile-test")
if err != nil {
t.Fatal(err)
}
defer tmpFile.Close()

tmpFile.WriteString(content)
return tmpFile.Name()
}

// Test ParseEnvFile for a file with a few well formatted lines
func TestParseEnvFileGoodFile(t *testing.T) {
content := `foo=bar
baz=quux
# comment
_foobar=foobaz
with.dots=working
and_underscore=working too
`
// Adding a newline + a line with pure whitespace.
// This is being done like this instead of the block above
// because it's common for editors to trim trailing whitespace
// from lines, which becomes annoying since that's the
// exact thing we need to test.
content += "\n \t "
tmpFile := tmpFileWithContent(content, t)
defer os.Remove(tmpFile)

lines, err := parseEnvFile(tmpFile)
if err != nil {
t.Fatal(err)
}

expectedLines := []string{
"foo=bar",
"baz=quux",
"_foobar=foobaz",
"with.dots=working",
"and_underscore=working too",
}

if !reflect.DeepEqual(lines, expectedLines) {
t.Fatal("lines not equal to expected_lines")
}
}

// Test ParseEnvFile for an empty file
func TestParseEnvFileEmptyFile(t *testing.T) {
tmpFile := tmpFileWithContent("", t)
defer os.Remove(tmpFile)

lines, err := parseEnvFile(tmpFile)
if err != nil {
t.Fatal(err)
}

if len(lines) != 0 {
t.Fatal("lines not empty; expected empty")
}
}

// Test ParseEnvFile for a non existent file
func TestParseEnvFileNonExistentFile(t *testing.T) {
_, err := parseEnvFile("foo_bar_baz")
if err == nil {
t.Fatal("ParseEnvFile succeeded; expected failure")
}
if _, ok := err.(*os.PathError); !ok {
t.Fatalf("Expected a PathError, got [%v]", err)
}
}

// Test ParseEnvFile for a badly formatted file
func TestParseEnvFileBadlyFormattedFile(t *testing.T) {
content := `foo=bar
f =quux
`

tmpFile := tmpFileWithContent(content, t)
defer os.Remove(tmpFile)

_, err := parseEnvFile(tmpFile)
if err == nil {
t.Fatalf("Expected an ErrBadEnvVariable, got nothing")
}
if _, ok := err.(ErrBadEnvVariable); !ok {
t.Fatalf("Expected an ErrBadEnvVariable, got [%v]", err)
}
expectedMessage := "poorly formatted environment: variable 'f ' has white spaces"
if err.Error() != expectedMessage {
t.Fatalf("Expected [%v], got [%v]", expectedMessage, err.Error())
}
}

// Test ParseEnvFile for a file with a line exceeding bufio.MaxScanTokenSize
func TestParseEnvFileLineTooLongFile(t *testing.T) {
content := strings.Repeat("a", bufio.MaxScanTokenSize+42)
content = fmt.Sprint("foo=", content)

tmpFile := tmpFileWithContent(content, t)
defer os.Remove(tmpFile)

_, err := parseEnvFile(tmpFile)
if err == nil {
t.Fatal("ParseEnvFile succeeded; expected failure")
}
}

// ParseEnvFile with a random file, pass through
func TestParseEnvFileRandomFile(t *testing.T) {
content := `first line
another invalid line`
tmpFile := tmpFileWithContent(content, t)
defer os.Remove(tmpFile)

_, err := parseEnvFile(tmpFile)

if err == nil {
t.Fatalf("Expected an ErrBadEnvVariable, got nothing")
}
if _, ok := err.(ErrBadEnvVariable); !ok {
t.Fatalf("Expected an ErrBadEnvvariable, got [%v]", err)
}
expectedMessage := "poorly formatted environment: variable 'first line' has white spaces"
if err.Error() != expectedMessage {
t.Fatalf("Expected [%v], got [%v]", expectedMessage, err.Error())
}
}
6 changes: 6 additions & 0 deletions cli/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ func (rc *RunCommand) runRun(args []string) error {
return fmt.Errorf("failed to run container: %v", err)
}

//collect all the environment variables for the container
config.Env, err = readKVStrings(rc.envfile, rc.env)
if err != nil {
return nil
}

config.Image = args[0]
if len(args) > 1 {
config.Cmd = args[1:]
Expand Down
41 changes: 41 additions & 0 deletions test/cli_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,47 @@ func (suite *PouchCreateSuite) TestCreateWithEnv(c *check.C) {
}
}

// TestCreateWithEnvfile tests creating container with envfile
func (suite *PouchCreateSuite) TestCreateWithEnvfile(c *check.C) {
name := "TestCreateWithEnvfile"

envfile := "fixtures/valid.env"
env1 := "TEST1=value1"
env2 := "TEST2=value2"
res := command.PouchRun("run", "-d",
"--name", name,
"--env-file", envfile,
"-e", env2,
busyboxImage,
"top")
defer DelContainerForceMultyTime(c, name)

res.Assert(c, icmd.Success)

envs, err := inspectFilter(name, ".Config.Env")
c.Assert(err, check.IsNil)

// check if these envs are in inspect result of container.
if !strings.Contains(envs, env1) {
c.Fatalf("container env in inspect result should have %s in %s while no\n", env1, envs)
}
if !strings.Contains(envs, env2) {
c.Fatalf("container env in inspect result should have %s in %s while no\n", env2, envs)
}

// check if these envs are in the real container envs
ret := command.PouchRun("exec", name, "env")
envs = ret.Stdout()

if !strings.Contains(envs, env1) {
c.Fatalf("container's runtime env should have %s in %s while no\n", env1, envs)
}
if !strings.Contains(envs, env2) {
c.Fatalf("container's runtime env should have %s in %s while no\n", env2, envs)
}
}

// TestCreateWithWorkDir tests creating container with a workdir works.
// TestCreateWithWorkDir tests creating container with a workdir works.
func (suite *PouchCreateSuite) TestCreateWithWorkDir(c *check.C) {
name := "TestCreateWithWorkDir"
Expand Down
13 changes: 13 additions & 0 deletions test/cli_run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,19 @@ func (suite *PouchRunSuite) TestRunWithEnv(c *check.C) {
c.Assert(strings.TrimSpace(res.Stdout()), check.Equals, "a,b,c-b1")
}

func (suite *PouchRunSuite) TestRunWithEnvfile(c *check.C) {
envfile := "fixtures/valid.env"

res := command.PouchRun("run", "--rm",
"--env-file", envfile,
"--env", "TEST2=value2",
busyboxImage,
"sh", "-c", "echo ${TEST1}-${TEST2}",
)
res.Assert(c, icmd.Success)
c.Assert(strings.TrimSpace(res.Stdout()), check.Equals, "value1-value2")
}

// TestRunWithTty tests running container with -tty flag and attach stdin in a non-tty client.
func (suite *PouchRunSuite) TestRunWithTty(c *check.C) {
name := "TestRunWithTty"
Expand Down
2 changes: 2 additions & 0 deletions test/fixtures/valid.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
TEST1=value1

0 comments on commit e6b4612

Please sign in to comment.