diff --git a/api/client/service/opts.go b/api/client/service/opts.go index 9660f8a3d9..9ea334fe97 100644 --- a/api/client/service/opts.go +++ b/api/client/service/opts.go @@ -399,6 +399,7 @@ type serviceOptions struct { env opts.ListOpts workdir string user string + groups []string mounts MountOpt resources resourceOptions @@ -446,6 +447,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { Labels: runconfigopts.ConvertKVStringsToMap(opts.containerLabels.GetAll()), Dir: opts.workdir, User: opts.user, + Groups: opts.groups, Mounts: opts.mounts.Value(), StopGracePeriod: opts.stopGrace.Value(), }, @@ -491,6 +493,7 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.StringVarP(&opts.workdir, flagWorkdir, "w", "", "Working directory inside the container") flags.StringVarP(&opts.user, flagUser, "u", "", "Username or UID (format: [:])") + flags.StringSliceVar(&opts.groups, flagGroupAdd, []string{}, "Add additional user groups to the container") flags.Var(&opts.resources.limitCPU, flagLimitCPU, "Limit CPUs") flags.Var(&opts.resources.limitMemBytes, flagLimitMemory, "Limit Memory") @@ -528,6 +531,8 @@ const ( flagEnv = "env" flagEnvRemove = "env-rm" flagEnvAdd = "env-add" + flagGroupAdd = "group-add" + flagGroupRemove = "group-rm" flagLabel = "label" flagLabelRemove = "label-rm" flagLabelAdd = "label-add" diff --git a/api/client/service/update.go b/api/client/service/update.go index 9a00c11b83..e843b69427 100644 --- a/api/client/service/update.go +++ b/api/client/service/update.go @@ -39,6 +39,7 @@ func newUpdateCommand(dockerCli *client.DockerCli) *cobra.Command { addServiceFlags(cmd, opts) flags.Var(newListOptsVar(), flagEnvRemove, "Remove an environment variable") + flags.Var(newListOptsVar(), flagGroupRemove, "Remove previously added user groups from the container") flags.Var(newListOptsVar(), flagLabelRemove, "Remove a label by its key") flags.Var(newListOptsVar(), flagContainerLabelRemove, "Remove a container label by its key") flags.Var(newListOptsVar(), flagMountRemove, "Remove a mount by its target path") @@ -211,6 +212,12 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { spec.EndpointSpec.Mode = swarm.ResolutionMode(value) } + if anyChanged(flags, flagGroupAdd, flagGroupRemove) { + if err := updateGroups(flags, &cspec.Groups); err != nil { + return err + } + } + if anyChanged(flags, flagPublishAdd, flagPublishRemove) { if spec.EndpointSpec == nil { spec.EndpointSpec = &swarm.EndpointSpec{} @@ -370,6 +377,29 @@ func updateMounts(flags *pflag.FlagSet, mounts *[]mounttypes.Mount) { *mounts = newMounts } +func updateGroups(flags *pflag.FlagSet, groups *[]string) error { + if flags.Changed(flagGroupAdd) { + values, err := flags.GetStringSlice(flagGroupAdd) + if err != nil { + return err + } + *groups = append(*groups, values...) + } + toRemove := buildToRemoveSet(flags, flagGroupRemove) + + newGroups := []string{} + for _, group := range *groups { + if _, exists := toRemove[group]; !exists { + newGroups = append(newGroups, group) + } + } + // Sort so that result is predictable. + sort.Strings(newGroups) + + *groups = newGroups + return nil +} + type byPortConfig []swarm.PortConfig func (r byPortConfig) Len() int { return len(r) } diff --git a/api/client/service/update_test.go b/api/client/service/update_test.go index 0a532cd346..ff3a211111 100644 --- a/api/client/service/update_test.go +++ b/api/client/service/update_test.go @@ -100,6 +100,23 @@ func TestUpdateEnvironmentWithDuplicateKeys(t *testing.T) { assert.Equal(t, envs[0], "A=b") } +func TestUpdateGroups(t *testing.T) { + flags := newUpdateCommand(nil).Flags() + flags.Set("group-add", "wheel") + flags.Set("group-add", "docker") + flags.Set("group-rm", "root") + flags.Set("group-add", "foo") + flags.Set("group-rm", "docker") + + groups := []string{"bar", "root"} + + updateGroups(flags, &groups) + assert.Equal(t, len(groups), 3) + assert.Equal(t, groups[0], "bar") + assert.Equal(t, groups[1], "foo") + assert.Equal(t, groups[2], "wheel") +} + func TestUpdateMounts(t *testing.T) { flags := newUpdateCommand(nil).Flags() flags.Set("mount-add", "type=volume,target=/toadd") diff --git a/daemon/cluster/convert/container.go b/daemon/cluster/convert/container.go index 2fa220a70d..1c44f99ae1 100644 --- a/daemon/cluster/convert/container.go +++ b/daemon/cluster/convert/container.go @@ -19,6 +19,7 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec { Env: c.Env, Dir: c.Dir, User: c.User, + Groups: c.Groups, } // Mounts @@ -67,6 +68,7 @@ func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) { Env: c.Env, Dir: c.Dir, User: c.User, + Groups: c.Groups, } if c.StopGracePeriod != nil { diff --git a/docs/reference/commandline/service_create.md b/docs/reference/commandline/service_create.md index 164d3d2119..09e1c39615 100644 --- a/docs/reference/commandline/service_create.md +++ b/docs/reference/commandline/service_create.md @@ -20,6 +20,7 @@ Options: --container-label value Service container labels (default []) --endpoint-mode string Endpoint mode (vip or dnsrr) -e, --env value Set environment variables (default []) + --group-add value Add additional user groups to the container (default []) --help Print usage -l, --label value Service labels (default []) --limit-cpu value Limit CPUs (default 0.000) diff --git a/docs/reference/commandline/service_update.md b/docs/reference/commandline/service_update.md index b84afcd7f5..3879a7dcc4 100644 --- a/docs/reference/commandline/service_update.md +++ b/docs/reference/commandline/service_update.md @@ -24,6 +24,8 @@ Options: --endpoint-mode string Endpoint mode (vip or dnsrr) --env-add value Add or update environment variables (default []) --env-rm value Remove an environment variable (default []) + --group-add value Add additional user groups to the container (default []) + --group-rm value Remove previously added user groups from the container (default []) --help Print usage --image string Service image tag --label-add value Add or update service labels (default []) diff --git a/integration-cli/docker_cli_swarm_test.go b/integration-cli/docker_cli_swarm_test.go index c1401ac419..3c079c75e7 100644 --- a/integration-cli/docker_cli_swarm_test.go +++ b/integration-cli/docker_cli_swarm_test.go @@ -220,3 +220,25 @@ func (s *DockerSwarmSuite) TestSwarmPublishAdd(c *check.C) { c.Assert(err, checker.IsNil) c.Assert(strings.TrimSpace(out), checker.Equals, "[{ tcp 20 80}]") } + +func (s *DockerSwarmSuite) TestSwarmServiceWithGroup(c *check.C) { + d := s.AddDaemon(c, true, true) + + name := "top" + out, err := d.Cmd("service", "create", "--name", name, "--user", "root:root", "--group-add", "wheel", "--group-add", "audio", "--group-add", "staff", "--group-add", "777", "busybox", "sh", "-c", "id > /id && top") + c.Assert(err, checker.IsNil) + c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "") + + // make sure task has been deployed. + waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, 1) + + out, err = d.Cmd("ps", "-q") + c.Assert(err, checker.IsNil) + c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "") + + container := strings.TrimSpace(out) + + out, err = d.Cmd("exec", container, "cat", "/id") + c.Assert(err, checker.IsNil) + c.Assert(strings.TrimSpace(out), checker.Equals, "uid=0(root) gid=0(root) groups=10(wheel),29(audio),50(staff),777") +}