Browse code

Add `--env-file` flag to `docker create service`

This fix tries to address the issue in 24712 and add
`--env-file` file to `docker create service`.

Related documentation has been updated.

An additional integration has been added.

This fix fixes 24712.

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>

Yong Tang authored on 2016/07/20 15:58:32
Showing 5 changed files
... ...
@@ -32,6 +32,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
32 32
 	flags.VarP(&opts.labels, flagLabel, "l", "Service labels")
33 33
 	flags.Var(&opts.containerLabels, flagContainerLabel, "Container labels")
34 34
 	flags.VarP(&opts.env, flagEnv, "e", "Set environment variables")
35
+	flags.Var(&opts.envFile, flagEnvFile, "Read in a file of environment variables")
35 36
 	flags.Var(&opts.mounts, flagMount, "Attach a mount to the service")
36 37
 	flags.StringSliceVar(&opts.constraints, flagConstraint, []string{}, "Placement constraints")
37 38
 	flags.StringSliceVar(&opts.networks, flagNetwork, []string{}, "Network attachments")
... ...
@@ -395,6 +395,7 @@ type serviceOptions struct {
395 395
 	image           string
396 396
 	args            []string
397 397
 	env             opts.ListOpts
398
+	envFile         opts.ListOpts
398 399
 	workdir         string
399 400
 	user            string
400 401
 	groups          []string
... ...
@@ -422,6 +423,7 @@ func newServiceOptions() *serviceOptions {
422 422
 		labels:          opts.NewListOpts(runconfigopts.ValidateEnv),
423 423
 		containerLabels: opts.NewListOpts(runconfigopts.ValidateEnv),
424 424
 		env:             opts.NewListOpts(runconfigopts.ValidateEnv),
425
+		envFile:         opts.NewListOpts(nil),
425 426
 		endpoint: endpointOptions{
426 427
 			ports: opts.NewListOpts(ValidatePort),
427 428
 		},
... ...
@@ -432,6 +434,25 @@ func newServiceOptions() *serviceOptions {
432 432
 func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
433 433
 	var service swarm.ServiceSpec
434 434
 
435
+	envVariables, err := runconfigopts.ReadKVStrings(opts.envFile.GetAll(), opts.env.GetAll())
436
+	if err != nil {
437
+		return service, err
438
+	}
439
+
440
+	currentEnv := make([]string, 0, len(envVariables))
441
+	for _, env := range envVariables { // need to process each var, in order
442
+		k := strings.SplitN(env, "=", 2)[0]
443
+		for i, current := range currentEnv { // remove duplicates
444
+			if current == env {
445
+				continue // no update required, may hide this behind flag to preserve order of envVariables
446
+			}
447
+			if strings.HasPrefix(current, k+"=") {
448
+				currentEnv = append(currentEnv[:i], currentEnv[i+1:]...)
449
+			}
450
+		}
451
+		currentEnv = append(currentEnv, env)
452
+	}
453
+
435 454
 	service = swarm.ServiceSpec{
436 455
 		Annotations: swarm.Annotations{
437 456
 			Name:   opts.name,
... ...
@@ -441,7 +462,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
441 441
 			ContainerSpec: swarm.ContainerSpec{
442 442
 				Image:           opts.image,
443 443
 				Args:            opts.args,
444
-				Env:             opts.env.GetAll(),
444
+				Env:             currentEnv,
445 445
 				Labels:          runconfigopts.ConvertKVStringsToMap(opts.containerLabels.GetAll()),
446 446
 				Dir:             opts.workdir,
447 447
 				User:            opts.user,
... ...
@@ -532,6 +553,7 @@ const (
532 532
 	flagContainerLabelAdd     = "container-label-add"
533 533
 	flagEndpointMode          = "endpoint-mode"
534 534
 	flagEnv                   = "env"
535
+	flagEnvFile               = "env-file"
535 536
 	flagEnvRemove             = "env-rm"
536 537
 	flagEnvAdd                = "env-add"
537 538
 	flagGroupAdd              = "group-add"
... ...
@@ -25,6 +25,7 @@ Options:
25 25
       --container-label value            Service container labels (default [])
26 26
       --endpoint-mode string             Endpoint mode (vip or dnsrr)
27 27
   -e, --env value                        Set environment variables (default [])
28
+      --env-file value                   Read in a file of environment variables (default [])
28 29
       --group-add value                  Add additional user groups to the container (default [])
29 30
       --help                             Print usage
30 31
   -l, --label value                      Service labels (default [])
... ...
@@ -9,6 +9,7 @@ import (
9 9
 	"net/http"
10 10
 	"net/http/httptest"
11 11
 	"os"
12
+	"path/filepath"
12 13
 	"strings"
13 14
 	"time"
14 15
 
... ...
@@ -568,3 +569,22 @@ func (s *DockerSwarmSuite) TestSwarmNetworkPlugin(c *check.C) {
568 568
 	c.Assert(err, checker.IsNil)
569 569
 	c.Assert(strings.TrimSpace(out), checker.Equals, "foo")
570 570
 }
571
+
572
+// Test case for #24712
573
+func (s *DockerSwarmSuite) TestSwarmServiceEnvFile(c *check.C) {
574
+	d := s.AddDaemon(c, true, true)
575
+
576
+	path := filepath.Join(d.folder, "env.txt")
577
+	err := ioutil.WriteFile(path, []byte("VAR1=A\nVAR2=A\n"), 0644)
578
+	c.Assert(err, checker.IsNil)
579
+
580
+	name := "worker"
581
+	out, err := d.Cmd("service", "create", "--env-file", path, "--env", "VAR1=B", "--env", "VAR1=C", "--env", "VAR2=", "--env", "VAR2", "--name", name, "busybox", "top")
582
+	c.Assert(err, checker.IsNil)
583
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
584
+
585
+	// The complete env is [VAR1=A VAR2=A VAR1=B VAR1=C VAR2= VAR2] and duplicates will be removed => [VAR1=C VAR2]
586
+	out, err = d.Cmd("inspect", "--format", "{{ .Spec.TaskTemplate.ContainerSpec.Env }}", name)
587
+	c.Assert(err, checker.IsNil)
588
+	c.Assert(out, checker.Contains, "[VAR1=C VAR2]")
589
+}
... ...
@@ -427,13 +427,13 @@ func Parse(flags *pflag.FlagSet, copts *ContainerOptions) (*container.Config, *c
427 427
 	}
428 428
 
429 429
 	// collect all the environment variables for the container
430
-	envVariables, err := readKVStrings(copts.envFile.GetAll(), copts.env.GetAll())
430
+	envVariables, err := ReadKVStrings(copts.envFile.GetAll(), copts.env.GetAll())
431 431
 	if err != nil {
432 432
 		return nil, nil, nil, err
433 433
 	}
434 434
 
435 435
 	// collect all the labels for the container
436
-	labels, err := readKVStrings(copts.labelsFile.GetAll(), copts.labels.GetAll())
436
+	labels, err := ReadKVStrings(copts.labelsFile.GetAll(), copts.labels.GetAll())
437 437
 	if err != nil {
438 438
 		return nil, nil, nil, err
439 439
 	}
... ...
@@ -663,9 +663,9 @@ func Parse(flags *pflag.FlagSet, copts *ContainerOptions) (*container.Config, *c
663 663
 	return config, hostConfig, networkingConfig, nil
664 664
 }
665 665
 
666
-// reads a file of line terminated key=value pairs, and overrides any keys
666
+// ReadKVStrings reads a file of line terminated key=value pairs, and overrides any keys
667 667
 // present in the file with additional pairs specified in the override parameter
668
-func readKVStrings(files []string, override []string) ([]string, error) {
668
+func ReadKVStrings(files []string, override []string) ([]string, error) {
669 669
 	envVariables := []string{}
670 670
 	for _, ef := range files {
671 671
 		parsedVars, err := ParseEnvFile(ef)