diff --git a/api/swagger.yaml b/api/swagger.yaml index 2efe55cc84..78832129ff 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -1921,6 +1921,9 @@ definitions: type: "array" items: $ref: "#/definitions/Mount" + StopSignal: + description: "Signal to stop the container." + type: "string" StopGracePeriod: description: "Amount of time to wait for the container to terminate before forcefully killing it." type: "integer" diff --git a/api/types/swarm/container.go b/api/types/swarm/container.go index f385fa0402..0a7d7321f3 100644 --- a/api/types/swarm/container.go +++ b/api/types/swarm/container.go @@ -32,6 +32,7 @@ type ContainerSpec struct { Dir string `json:",omitempty"` User string `json:",omitempty"` Groups []string `json:",omitempty"` + StopSignal string `json:",omitempty"` TTY bool `json:",omitempty"` OpenStdin bool `json:",omitempty"` ReadOnly bool `json:",omitempty"` diff --git a/cli/command/service/opts.go b/cli/command/service/opts.go index d8618e73ca..adab9b3658 100644 --- a/cli/command/service/opts.go +++ b/cli/command/service/opts.go @@ -269,6 +269,7 @@ type serviceOptions struct { workdir string user string groups opts.ListOpts + stopSignal string tty bool readOnly bool mounts opts.MountOpt @@ -372,17 +373,18 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { }, TaskTemplate: swarm.TaskSpec{ ContainerSpec: swarm.ContainerSpec{ - Image: opts.image, - Args: opts.args, - Env: currentEnv, - Hostname: opts.hostname, - Labels: runconfigopts.ConvertKVStringsToMap(opts.containerLabels.GetAll()), - Dir: opts.workdir, - User: opts.user, - Groups: opts.groups.GetAll(), - TTY: opts.tty, - ReadOnly: opts.readOnly, - Mounts: opts.mounts.Value(), + Image: opts.image, + Args: opts.args, + Env: currentEnv, + Hostname: opts.hostname, + Labels: runconfigopts.ConvertKVStringsToMap(opts.containerLabels.GetAll()), + Dir: opts.workdir, + User: opts.user, + Groups: opts.groups.GetAll(), + StopSignal: opts.stopSignal, + TTY: opts.tty, + ReadOnly: opts.readOnly, + Mounts: opts.mounts.Value(), DNSConfig: &swarm.DNSConfig{ Nameservers: opts.dns.GetAll(), Search: opts.dnsSearch.GetAll(), @@ -470,6 +472,9 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.BoolVar(&opts.readOnly, flagReadOnly, false, "Mount the container's root filesystem as read only") flags.SetAnnotation(flagReadOnly, "version", []string{"1.27"}) + + flags.StringVar(&opts.stopSignal, flagStopSignal, "", "Signal to stop the container") + flags.SetAnnotation(flagStopSignal, "version", []string{"1.27"}) } const ( @@ -523,6 +528,7 @@ const ( flagRestartMaxAttempts = "restart-max-attempts" flagRestartWindow = "restart-window" flagStopGracePeriod = "stop-grace-period" + flagStopSignal = "stop-signal" flagTTY = "tty" flagUpdateDelay = "update-delay" flagUpdateFailureAction = "update-failure-action" diff --git a/cli/command/service/update.go b/cli/command/service/update.go index 7f461c90a9..770a5bd26f 100644 --- a/cli/command/service/update.go +++ b/cli/command/service/update.go @@ -349,6 +349,8 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { cspec.ReadOnly = readOnly } + updateString(flagStopSignal, &cspec.StopSignal) + return nil } diff --git a/cli/command/service/update_test.go b/cli/command/service/update_test.go index f2887e229d..c43e596136 100644 --- a/cli/command/service/update_test.go +++ b/cli/command/service/update_test.go @@ -441,3 +441,25 @@ func TestUpdateReadOnly(t *testing.T) { updateService(flags, spec) assert.Equal(t, cspec.ReadOnly, false) } + +func TestUpdateStopSignal(t *testing.T) { + spec := &swarm.ServiceSpec{} + cspec := &spec.TaskTemplate.ContainerSpec + + // Update with --stop-signal=SIGUSR1 + flags := newUpdateCommand(nil).Flags() + flags.Set("stop-signal", "SIGUSR1") + updateService(flags, spec) + assert.Equal(t, cspec.StopSignal, "SIGUSR1") + + // Update without --stop-signal, no change + flags = newUpdateCommand(nil).Flags() + updateService(flags, spec) + assert.Equal(t, cspec.StopSignal, "SIGUSR1") + + // Update with --stop-signal=SIGWINCH + flags = newUpdateCommand(nil).Flags() + flags.Set("stop-signal", "SIGWINCH") + updateService(flags, spec) + assert.Equal(t, cspec.StopSignal, "SIGWINCH") +} diff --git a/daemon/cluster/convert/container.go b/daemon/cluster/convert/container.go index b4c1e0cee6..d43089b82e 100644 --- a/daemon/cluster/convert/container.go +++ b/daemon/cluster/convert/container.go @@ -14,20 +14,21 @@ import ( func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec { containerSpec := types.ContainerSpec{ - Image: c.Image, - Labels: c.Labels, - Command: c.Command, - Args: c.Args, - Hostname: c.Hostname, - Env: c.Env, - Dir: c.Dir, - User: c.User, - Groups: c.Groups, - TTY: c.TTY, - OpenStdin: c.OpenStdin, - ReadOnly: c.ReadOnly, - Hosts: c.Hosts, - Secrets: secretReferencesFromGRPC(c.Secrets), + Image: c.Image, + Labels: c.Labels, + Command: c.Command, + Args: c.Args, + Hostname: c.Hostname, + Env: c.Env, + Dir: c.Dir, + User: c.User, + Groups: c.Groups, + StopSignal: c.StopSignal, + TTY: c.TTY, + OpenStdin: c.OpenStdin, + ReadOnly: c.ReadOnly, + Hosts: c.Hosts, + Secrets: secretReferencesFromGRPC(c.Secrets), } if c.DNSConfig != nil { @@ -136,20 +137,21 @@ func secretReferencesFromGRPC(sr []*swarmapi.SecretReference) []*types.SecretRef func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) { containerSpec := &swarmapi.ContainerSpec{ - Image: c.Image, - Labels: c.Labels, - Command: c.Command, - Args: c.Args, - Hostname: c.Hostname, - Env: c.Env, - Dir: c.Dir, - User: c.User, - Groups: c.Groups, - TTY: c.TTY, - OpenStdin: c.OpenStdin, - ReadOnly: c.ReadOnly, - Hosts: c.Hosts, - Secrets: secretReferencesToGRPC(c.Secrets), + Image: c.Image, + Labels: c.Labels, + Command: c.Command, + Args: c.Args, + Hostname: c.Hostname, + Env: c.Env, + Dir: c.Dir, + User: c.User, + Groups: c.Groups, + StopSignal: c.StopSignal, + TTY: c.TTY, + OpenStdin: c.OpenStdin, + ReadOnly: c.ReadOnly, + Hosts: c.Hosts, + Secrets: secretReferencesToGRPC(c.Secrets), } if c.DNSConfig != nil { diff --git a/daemon/cluster/executor/container/container.go b/daemon/cluster/executor/container/container.go index 0b733904d5..2535bbb78e 100644 --- a/daemon/cluster/executor/container/container.go +++ b/daemon/cluster/executor/container/container.go @@ -185,6 +185,7 @@ func (c *containerConfig) exposedPorts() map[nat.Port]struct{} { func (c *containerConfig) config() *enginecontainer.Config { config := &enginecontainer.Config{ Labels: c.labels(), + StopSignal: c.spec().StopSignal, Tty: c.spec().TTY, OpenStdin: c.spec().OpenStdin, User: c.spec().User, diff --git a/docs/reference/commandline/service_create.md b/docs/reference/commandline/service_create.md index 6ee3bb535a..ae621577d1 100644 --- a/docs/reference/commandline/service_create.md +++ b/docs/reference/commandline/service_create.md @@ -58,6 +58,7 @@ Options: --restart-window duration Window used to evaluate the restart policy (ns|us|ms|s|m|h) --secret secret Specify secrets to expose to the service --stop-grace-period duration Time to wait before force killing a container (ns|us|ms|s|m|h) + --stop-signal string Signal to stop the container -t, --tty Allocate a pseudo-TTY --update-delay duration Delay between updates (ns|us|ms|s|m|h) (default 0s) --update-failure-action string Action on update failure ("pause"|"continue") (default "pause") diff --git a/docs/reference/commandline/service_update.md b/docs/reference/commandline/service_update.md index 223ba2a51f..80fd5b3c82 100644 --- a/docs/reference/commandline/service_update.md +++ b/docs/reference/commandline/service_update.md @@ -70,6 +70,7 @@ Options: --secret-add secret Add or update a secret on a service --secret-rm list Remove a secret (default []) --stop-grace-period duration Time to wait before force killing a container (ns|us|ms|s|m|h) + --stop-signal string Signal to stop the container -t, --tty Allocate a pseudo-TTY --update-delay duration Delay between updates (ns|us|ms|s|m|h) (default 0s) --update-failure-action string Action on update failure ("pause"|"continue") (default "pause") diff --git a/integration-cli/docker_cli_swarm_test.go b/integration-cli/docker_cli_swarm_test.go index 711ee942d8..909698594d 100644 --- a/integration-cli/docker_cli_swarm_test.go +++ b/integration-cli/docker_cli_swarm_test.go @@ -1764,3 +1764,31 @@ func (s *DockerSwarmSuite) TestNetworkInspectWithDuplicateNames(c *check.C) { c.Assert(err, checker.NotNil, check.Commentf(out)) c.Assert(out, checker.Contains, "network foo is ambiguous (2 matches found based on name)") } + +func (s *DockerSwarmSuite) TestSwarmStopSignal(c *check.C) { + testRequires(c, DaemonIsLinux, UserNamespaceROMount) + + d := s.AddDaemon(c, true, true) + + out, err := d.Cmd("service", "create", "--name", "top", "--stop-signal=SIGHUP", "busybox", "top") + c.Assert(err, checker.IsNil, check.Commentf(out)) + + // make sure task has been deployed. + waitAndAssert(c, defaultReconciliationTimeout, d.CheckActiveContainerCount, checker.Equals, 1) + + out, err = d.Cmd("service", "inspect", "--format", "{{ .Spec.TaskTemplate.ContainerSpec.StopSignal }}", "top") + c.Assert(err, checker.IsNil, check.Commentf(out)) + c.Assert(strings.TrimSpace(out), checker.Equals, "SIGHUP") + + containers := d.ActiveContainers() + out, err = d.Cmd("inspect", "--type", "container", "--format", "{{.Config.StopSignal}}", containers[0]) + c.Assert(err, checker.IsNil, check.Commentf(out)) + c.Assert(strings.TrimSpace(out), checker.Equals, "SIGHUP") + + out, err = d.Cmd("service", "update", "--stop-signal=SIGUSR1", "top") + c.Assert(err, checker.IsNil, check.Commentf(out)) + + out, err = d.Cmd("service", "inspect", "--format", "{{ .Spec.TaskTemplate.ContainerSpec.StopSignal }}", "top") + c.Assert(err, checker.IsNil, check.Commentf(out)) + c.Assert(strings.TrimSpace(out), checker.Equals, "SIGUSR1") +}