diff --git a/cmd/podman/containers/stop.go b/cmd/podman/containers/stop.go index bd592d01071..5f6c32a0ce3 100644 --- a/cmd/podman/containers/stop.go +++ b/cmd/podman/containers/stop.go @@ -70,6 +70,10 @@ func stopFlags(cmd *cobra.Command) { flags.IntVarP(&stopTimeout, timeFlagName, "t", int(containerConfig.Engine.StopTimeout), "Seconds to wait for stop before killing the container") _ = cmd.RegisterFlagCompletionFunc(timeFlagName, completion.AutocompleteNone) + signalFlagName := "signal" + flags.StringVarP(&stopOptions.Signal, signalFlagName, "s", "", "Signal to send for the graceful stop, before the timeout (default: the container's stop signal)") + _ = cmd.RegisterFlagCompletionFunc(signalFlagName, common.AutocompleteStopSignal) + filterFlagName := "filter" flags.StringArrayVarP(&filters, filterFlagName, "f", []string{}, "Filter output based on conditions given") _ = cmd.RegisterFlagCompletionFunc(filterFlagName, common.AutocompletePsFilters) diff --git a/docs/source/markdown/podman-stop.1.md.in b/docs/source/markdown/podman-stop.1.md.in index e8a2a3aaa8f..f99363ca99e 100644 --- a/docs/source/markdown/podman-stop.1.md.in +++ b/docs/source/markdown/podman-stop.1.md.in @@ -31,6 +31,12 @@ Command does not fail when *file* is missing and user specified --ignore. @@option latest +#### **--signal**, **-s**=*signal* + +Signal to send to the container for the graceful stop, before the timeout. For +more information on Linux signals, refer to *signal(7)*. The default is the +container's stop signal (see **--stop-signal** in **podman-create(1)**). + @@option time ## EXAMPLES diff --git a/libpod/container_api.go b/libpod/container_api.go index e96cb2e0a25..b688e6f982c 100644 --- a/libpod/container_api.go +++ b/libpod/container_api.go @@ -283,24 +283,33 @@ func (c *Container) Stop() error { // manually. If timeout is 0, SIGKILL will be used immediately to kill the // container. func (c *Container) StopWithTimeout(timeout uint) (finalErr error) { - return c.StopWithArgs(timeout, true) + return c.StopWithArgs(timeout, 0, true) +} + +// StopWithTimeoutAndSignal is like StopWithTimeout but delivers the given signal +// for the graceful stop instead of the container's configured StopSignal. A +// signal of 0 means "use the configured StopSignal" (the Docker stop --signal +// override). +func (c *Container) StopWithTimeoutAndSignal(timeout uint, signal uint) (finalErr error) { + return c.StopWithArgs(timeout, signal, true) } // StopService stops the container without marking it as stopped by user (e.g. for // systemd ExecStop). Containers with restart policy unless-stopped will be // eligible to start again on next boot. func (c *Container) StopService(timeout uint) (finalErr error) { - return c.StopWithArgs(timeout, false) + return c.StopWithArgs(timeout, 0, false) } -// StopWithArgs is a version of Stop that allows a timeout to be specified manually -// and controls whether to set the StoppedByUser state field. If timeout is 0, -// SIGKILL will be used immediately to kill the container. +// StopWithArgs is a version of Stop that allows a timeout and a graceful stop +// signal to be specified manually, and controls whether to set the StoppedByUser +// state field. A signal of 0 uses the container's configured StopSignal. If +// timeout is 0, SIGKILL will be used immediately to kill the container. // // An explicit stop is treated as a user-driven lifecycle action. Because of // that, this path may not trigger automatic restart-policy handling in cleanup, // even when stoppedByUser is false. -func (c *Container) StopWithArgs(timeout uint, stoppedByUser bool) (finalErr error) { +func (c *Container) StopWithArgs(timeout uint, signal uint, stoppedByUser bool) (finalErr error) { // Have to lock the pod the container is a part of. // This prevents running `podman stop` at the same time a // `podman pod start` is running, which could lead to weird races. @@ -338,7 +347,7 @@ func (c *Container) StopWithArgs(timeout uint, stoppedByUser bool) (finalErr err return err } } - return c.stopInternal(timeout, stoppedByUser) + return c.stopInternal(timeout, signal, stoppedByUser) } // Kill sends a signal to a container diff --git a/libpod/container_internal.go b/libpod/container_internal.go index 45984bfee80..e78831b052d 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -1375,12 +1375,12 @@ func (c *Container) stopWithAll() bool { // Internal, non-locking function to stop container func (c *Container) stop(timeout uint) error { - return c.stopInternal(timeout, true) + return c.stopInternal(timeout, 0, true) } // Internal, non-locking function to stop container // stoppedByUser controls whether to set the StoppedByUser state field. -func (c *Container) stopInternal(timeout uint, stoppedByUser bool) error { +func (c *Container) stopInternal(timeout uint, signal uint, stoppedByUser bool) error { // This is explicit container stop that flows pass through Running -> Stopping -> Stopped/Exited states. // As a result, this does not satisfy the Running/Paused -> Stopped/Exited // transition that is required to trigger restart policy during cleanup. @@ -1433,7 +1433,7 @@ func (c *Container) stopInternal(timeout uint, stoppedByUser bool) error { c.lock.Unlock() } - stopErr := c.ociRuntime.StopContainer(c, timeout, all) + stopErr := c.ociRuntime.StopContainer(c, timeout, signal, all) if !c.batched { c.lock.Lock() @@ -1502,7 +1502,7 @@ func (c *Container) waitForConmonToExitAndSave() error { // this to get the real exit code... But I'm not // that dedicated. all := c.stopWithAll() - if err := c.ociRuntime.StopContainer(c, 0, all); err != nil { + if err := c.ociRuntime.StopContainer(c, 0, 0, all); err != nil { logrus.Errorf("Error stopping container %s after Conmon exited prematurely: %v", c.ID(), err) } } diff --git a/libpod/oci.go b/libpod/oci.go index fa7b10dc35a..1074f069e68 100644 --- a/libpod/oci.go +++ b/libpod/oci.go @@ -44,7 +44,7 @@ type OCIRuntime interface { //nolint:interfacebloat // If all is set, we will attempt to use the --all flag will `kill` in // the OCI runtime to kill all processes in the container, including // exec sessions. This is only supported if the container has cgroups. - StopContainer(ctr *Container, timeout uint, all bool) error + StopContainer(ctr *Container, timeout uint, signal uint, all bool) error // DeleteContainer deletes the given container from the OCI runtime. DeleteContainer(ctr *Container) error // PauseContainer pauses the given container. diff --git a/libpod/oci_conmon_common.go b/libpod/oci_conmon_common.go index ef707238f4e..81031d12e15 100644 --- a/libpod/oci_conmon_common.go +++ b/libpod/oci_conmon_common.go @@ -316,7 +316,7 @@ func (r *ConmonOCIRuntime) killContainer(ctr *Container, signal uint, all, captu // after to pull the exit code. // IMPORTANT: Thus function is called from an unlocked container state in // the stop() code path so do not modify the state here. -func (r *ConmonOCIRuntime) StopContainer(ctr *Container, timeout uint, all bool) error { +func (r *ConmonOCIRuntime) StopContainer(ctr *Container, timeout uint, signal uint, all bool) error { logrus.Debugf("Stopping container %s (PID %d)", ctr.ID(), ctr.state.PID) // Ping the container to see if it's alive @@ -366,7 +366,10 @@ func (r *ConmonOCIRuntime) StopContainer(ctr *Container, timeout uint, all bool) } if timeout > 0 { - stopSignal := ctr.config.StopSignal + stopSignal := signal + if stopSignal == 0 { + stopSignal = ctr.config.StopSignal + } if stopSignal == 0 { stopSignal = uint(syscall.SIGTERM) } diff --git a/libpod/oci_missing.go b/libpod/oci_missing.go index 37898e6e9b5..7b39580f008 100644 --- a/libpod/oci_missing.go +++ b/libpod/oci_missing.go @@ -95,7 +95,7 @@ func (r *MissingRuntime) KillContainer(_ *Container, _ uint, _ bool) error { } // StopContainer is not available as the runtime is missing -func (r *MissingRuntime) StopContainer(_ *Container, _ uint, _ bool) error { +func (r *MissingRuntime) StopContainer(_ *Container, _ uint, _ uint, _ bool) error { return r.printError() } diff --git a/pkg/api/handlers/compat/containers_stop.go b/pkg/api/handlers/compat/containers_stop.go index 0e14596f4ec..c9e14a142d4 100644 --- a/pkg/api/handlers/compat/containers_stop.go +++ b/pkg/api/handlers/compat/containers_stop.go @@ -25,9 +25,10 @@ func StopContainer(w http.ResponseWriter, r *http.Request) { // /{version}/containers/(name)/stop query := struct { - Ignore bool `schema:"ignore"` - DockerTimeout int `schema:"t"` - LibpodTimeout uint `schema:"timeout"` + Ignore bool `schema:"ignore"` + DockerTimeout int `schema:"t"` + LibpodTimeout uint `schema:"timeout"` + Signal string `schema:"signal"` }{ // override any golang type defaults } @@ -38,6 +39,7 @@ func StopContainer(w http.ResponseWriter, r *http.Request) { name := utils.GetName(r) options := entities.StopOptions{ Ignore: query.Ignore, + Signal: query.Signal, } if utils.IsLibpodRequest(r) { if _, found := r.URL.Query()["timeout"]; found { diff --git a/pkg/bindings/containers/types.go b/pkg/bindings/containers/types.go index 59c322bae33..a435c76b933 100644 --- a/pkg/bindings/containers/types.go +++ b/pkg/bindings/containers/types.go @@ -248,6 +248,7 @@ type WaitOptions struct { //go:generate go run ../generator/generator.go StopOptions type StopOptions struct { Ignore *bool + Signal *string Timeout *uint } diff --git a/pkg/bindings/containers/types_stop_options.go b/pkg/bindings/containers/types_stop_options.go index 1023a284f15..ffe93339579 100644 --- a/pkg/bindings/containers/types_stop_options.go +++ b/pkg/bindings/containers/types_stop_options.go @@ -32,6 +32,21 @@ func (o *StopOptions) GetIgnore() bool { return *o.Ignore } +// WithSignal set field Signal to given value +func (o *StopOptions) WithSignal(value string) *StopOptions { + o.Signal = &value + return o +} + +// GetSignal returns value of field Signal +func (o *StopOptions) GetSignal() string { + if o.Signal == nil { + var z string + return z + } + return *o.Signal +} + // WithTimeout set field Timeout to given value func (o *StopOptions) WithTimeout(value uint) *StopOptions { o.Timeout = &value diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go index 6b13013fbc5..862549c867c 100644 --- a/pkg/domain/entities/containers.go +++ b/pkg/domain/entities/containers.go @@ -102,6 +102,9 @@ type StopOptions struct { Ignore bool Latest bool Timeout *uint + // Signal optionally overrides the container's configured StopSignal for the + // graceful stop (the Docker `stop --signal` override). Empty means StopSignal. + Signal string } type StopReport struct { diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index 6f8025ccfbb..5859f2e8ac2 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -387,7 +387,17 @@ func (ic *ContainerEngine) containerStopImpl(ctx context.Context, namesOrIds []s } func (ic *ContainerEngine) ContainerStop(ctx context.Context, namesOrIds []string, options entities.StopOptions) ([]*entities.StopReport, error) { - return ic.containerStopImpl(ctx, namesOrIds, options, func(c *libpod.Container, t uint) error { return c.StopWithTimeout(t) }) + var sig uint // 0 = use the container's configured StopSignal + if options.Signal != "" { + s, err := signal.ParseSignalNameOrNumber(options.Signal) + if err != nil { + return nil, err + } + sig = uint(s) + } + return ic.containerStopImpl(ctx, namesOrIds, options, func(c *libpod.Container, t uint) error { + return c.StopWithTimeoutAndSignal(t, sig) + }) } func (ic *ContainerEngine) ContainerStopService(ctx context.Context, namesOrIds []string, options entities.StopOptions) ([]*entities.StopReport, error) { diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index 6e7be5b1f21..2b69515bdb9 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -150,6 +150,9 @@ func (ic *ContainerEngine) ContainerStop(_ context.Context, namesOrIds []string, idToRawInput[ctrs[i].ID] = rawInputs[i] } options := new(containers.StopOptions).WithIgnore(opts.Ignore) + if opts.Signal != "" { + options.WithSignal(opts.Signal) + } if to := opts.Timeout; to != nil { options.WithTimeout(*to) } diff --git a/test/e2e/stop_test.go b/test/e2e/stop_test.go index aee8913c17d..a75d30991c4 100644 --- a/test/e2e/stop_test.go +++ b/test/e2e/stop_test.go @@ -161,6 +161,24 @@ var _ = Describe("Podman stop", func() { Expect(strings.TrimSpace(finalCtrs.OutputToString())).To(Equal("")) }) + It("podman stop container --signal", func() { + // The container traps SIGUSR1 and exits 42; --signal must deliver that + // instead of the container's default stop signal. + ctr := "test-stop-signal" + session := podmanTest.Podman([]string{"run", "-d", "--name", ctr, ALPINE, "sh", "-c", "trap 'exit 42' USR1; while :; do sleep 0.1; done"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(ExitCleanly()) + + stop := podmanTest.Podman([]string{"stop", "--signal", "SIGUSR1", "-t", "20", ctr}) + stop.WaitWithDefaultTimeout() + Expect(stop).Should(ExitCleanly()) + + inspect := podmanTest.Podman([]string{"inspect", ctr, "--format", "{{.State.ExitCode}}"}) + inspect.WaitWithDefaultTimeout() + Expect(inspect).Should(ExitCleanly()) + Expect(inspect.OutputToString()).To(Equal("42")) + }) + It("podman stop container --timeout", func() { session := podmanTest.Podman([]string{"run", "-d", "--name", "test5", ALPINE, "sleep", "100"}) session.WaitWithDefaultTimeout()