Fix unless-stopped restart policy to match Docker behavior

- Update documentation: Differentiate `unless-stopped` from `always` - containers stopped by the user before a reboot will not restart.
- Add `should-start-on-boot` filter: Identify containers that require a restart after a system reboot.
- Update command documentation: Add `restart-policy` and `label!` filters to the documentation for container commands (rm, ps, start, stop, pause, unpause, restart).
- Add `restart-policy` and `shoud-start-on-boot` to completions.
- Update service: Update `podman-restart.service` to use the `needs-restart=true` filter.
- Preserve state: Preserve the `StoppedByUser` state across reboots.
- Update API: Add a `ShouldStartOnBoot()` method to the Container API.
- Update documentation: Add descriptions for the `should-start-on-boot` filter.

Fixes: https://issues.redhat.com/browse/RHEL-129405
Fixes: https://github.com/containers/podman/issues/20418

Signed-off-by: Jan Rodák <hony.com@seznam.cz>
This commit is contained in:
Jan Rodák
2025-11-26 14:16:51 +01:00
parent 9a811bf5ac
commit 4d3c6311a5
15 changed files with 145 additions and 6 deletions

View File

@@ -1808,7 +1808,18 @@ func AutocompletePsFilters(cmd *cobra.Command, _ []string, toComplete string) ([
"name=": func(s string) ([]string, cobra.ShellCompDirective) { return getContainers(cmd, s, completeNames) },
"network=": func(s string) ([]string, cobra.ShellCompDirective) { return getNetworks(cmd, s, completeDefault) },
"pod=": func(s string) ([]string, cobra.ShellCompDirective) { return getPods(cmd, s, completeDefault) },
"since=": func(s string) ([]string, cobra.ShellCompDirective) { return getContainers(cmd, s, completeDefault) },
"restart-policy=": func(_ string) ([]string, cobra.ShellCompDirective) {
return []string{
define.RestartPolicyAlways,
define.RestartPolicyNo,
define.RestartPolicyOnFailure,
define.RestartPolicyUnlessStopped,
}, cobra.ShellCompDirectiveNoFileComp
},
"should-start-on-boot=": func(_ string) ([]string, cobra.ShellCompDirective) {
return []string{"true", "false"}, cobra.ShellCompDirectiveNoFileComp
},
"since=": func(s string) ([]string, cobra.ShellCompDirective) { return getContainers(cmd, s, completeDefault) },
"status=": func(_ string) ([]string, cobra.ShellCompDirective) {
return containerStatuses, cobra.ShellCompDirectiveNoFileComp
},

View File

@@ -9,8 +9,8 @@ After=network-online.target
Type=oneshot
RemainAfterExit=true
Environment=LOGGING="--log-level=info"
ExecStart=@@PODMAN@@ $LOGGING start --all --filter restart-policy=always
ExecStop=@@PODMAN@@ $LOGGING stop --all --filter restart-policy=always
ExecStart=@@PODMAN@@ $LOGGING start --all --filter should-start-on-boot=true
ExecStop=@@PODMAN@@ $LOGGING stop --all --filter should-start-on-boot=true
[Install]
WantedBy=default.target

View File

@@ -13,7 +13,7 @@ Valid _policy_ values are:
- `never` : Synonym for **no**; do not restart containers on exit
- `on-failure[:max_retries]` : Restart containers when they exit with a non-zero exit code, retrying indefinitely or until the optional *max_retries* count is hit
- `always` : Restart containers when they exit, regardless of status, retrying indefinitely
- `unless-stopped` : Identical to **always**
- `unless-stopped` : Restart containers when they exit, unless the container was explicitly stopped by the user. After a system reboot, containers with this policy will be restarted by podman-restart.service only if they were not explicitly stopped by the user before the reboot. This differs from **always**, which restarts containers after a system reboot regardless of whether they were user-stopped
Podman provides a systemd unit file, podman-restart.service, which restarts containers after a system reboot.

View File

@@ -33,6 +33,7 @@ Valid filters are listed below:
| id | [ID] Container's ID (CID prefix match by default; accepts regex) |
| name | [Name] Container's name (accepts regex) |
| label | [Key] or [Key=Value] Label assigned to a container |
| label! | [Key] or [Key=Value] Label NOT assigned to a container |
| exited | [Int] Container's exit code |
| status | [Status] Container's status: 'created', 'initialized', 'exited', 'paused', 'running', 'unknown' |
| ancestor | [ImageName] Image or descendant used to create container |
@@ -42,8 +43,10 @@ Valid filters are listed below:
| health | [Status] healthy or unhealthy |
| pod | [Pod] name or full or partial ID of pod |
| network | [Network] name or full ID of network |
| restart-policy | [Policy] Container's restart policy (e.g., 'no', 'on-failure', 'always', 'unless-stopped') |
| until | [DateTime] Containers created before the given duration or time. |
| command | [Command] the command the container is executing, only argv[0] is taken |
| should-start-on-boot | [Bool] Containers that need to be restarted after system reboot. True for containers with restart policy 'always', or 'unless-stopped' that were not explicitly stopped by the user |
@@option latest

View File

@@ -62,6 +62,8 @@ Valid filters are listed below:
| network | [Network] name or full ID of network |
| until | [DateTime] container created before the given duration or time. |
| command | [Command] the command the container is executing, only argv[0] is taken |
| restart-policy | [Policy] Container's restart policy (e.g., 'no', 'on-failure', 'always', 'unless-stopped') |
| should-start-on-boot | [Bool] Containers that need to be restarted after system reboot. True for containers with restart policy 'always', or 'unless-stopped' that were not explicitly stopped by the user |
#### **--format**=*format*
@@ -288,6 +290,14 @@ CONTAINER ID IMAGE COMMAND CREATED STATUS
5e3694604817 quay.io/centos/centos:latest sleep 300 3 minutes ago Up 3 minutes centos-test
```
Filter containers that need to be restarted after system reboot.
```
$ podman ps -a --filter should-start-on-boot=true
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ff660efda598 docker.io/library/nginx:latest nginx -g daemon o... 3 minutes ago Up 3 minutes 0.0.0.0:8080->80/tcp webserver
5693e934f4c6 docker.io/library/redis:latest redis-server 3 minutes ago Exited (0) 3 minutes ago 6379/tcp cache
```
Use custom format to show container and pod information.
```
$ podman ps --format "{{.Names}} is in pod {{.PodName}} ({{.Pod}})"

View File

@@ -36,6 +36,7 @@ Valid filters are listed below:
| id | [ID] Container's ID (CID prefix match by default; accepts regex) |
| name | [Name] Container's name (accepts regex) |
| label | [Key] or [Key=Value] Label assigned to a container |
| label! | [Key] or [Key=Value] Label NOT assigned to a container |
| exited | [Int] Container's exit code |
| status | [Status] Container's status: 'created', 'initialized', 'exited', 'paused', 'running', 'unknown' |
| ancestor | [ImageName] Image or descendant used to create container |
@@ -45,8 +46,10 @@ Valid filters are listed below:
| health | [Status] healthy or unhealthy |
| pod | [Pod] name or full or partial ID of pod |
| network | [Network] name or full ID of network |
| restart-policy | [Policy] Container's restart policy (e.g., 'no', 'on-failure', 'always', 'unless-stopped') |
| until | [DateTime] Containers created before the given duration or time. |
| command | [Command] the command the container is executing, only argv[0] is taken |
| should-start-on-boot | [Bool] Containers that need to be restarted after system reboot. True for containers with restart policy 'always', or 'unless-stopped' that were not explicitly stopped by the user |
@@option latest

View File

@@ -40,6 +40,7 @@ Valid filters are listed below:
| id | [ID] Container's ID (CID prefix match by default; accepts regex) |
| name | [Name] Container's name (accepts regex) |
| label | [Key] or [Key=Value] Label assigned to a container |
| label! | [Key] or [Key=Value] Label NOT assigned to a container |
| exited | [Int] Container's exit code |
| status | [Status] Container's status: 'created', 'initialized', 'exited', 'paused', 'running', 'unknown' |
| ancestor | [ImageName] Image or descendant used to create container |
@@ -49,8 +50,10 @@ Valid filters are listed below:
| health | [Status] healthy or unhealthy |
| pod | [Pod] name or full or partial ID of pod |
| network | [Network] name or full ID of network |
| restart-policy | [Policy] Container's restart policy (e.g., 'no', 'on-failure', 'always', 'unless-stopped') |
| until | [DateTime] Containers created before the given duration or time. |
| command | [Command] the command the container is executing, only argv[0] is taken |
| should-start-on-boot | [Bool] Containers that need to be restarted after system reboot. True for containers with restart policy 'always', or 'unless-stopped' that were not explicitly stopped by the user |
#### **--force**, **-f**

View File

@@ -27,7 +27,7 @@ starting multiple containers.
@@option detach-keys
#### **--filter**, **-f**
#### **--filter**, **-f**=*filter*
Filter what containers are going to be started from the given arguments.
Multiple filters can be given with multiple uses of the --filter flag.
@@ -41,6 +41,7 @@ Valid filters are listed below:
| id | [ID] Container's ID (CID prefix match by default; accepts regex) |
| name | [Name] Container's name (accepts regex) |
| label | [Key] or [Key=Value] Label assigned to a container |
| label! | [Key] or [Key=Value] Label NOT assigned to a container |
| exited | [Int] Container's exit code |
| status | [Status] Container's status: 'created', 'initialized', 'exited', 'paused', 'running', 'unknown' |
| ancestor | [ImageName] Image or descendant used to create container |
@@ -50,8 +51,10 @@ Valid filters are listed below:
| health | [Status] healthy or unhealthy |
| pod | [Pod] name or full or partial ID of pod |
| network | [Network] name or full ID of network |
| restart-policy | [Policy] Container's restart policy (e.g., 'no', 'on-failure', 'always', 'unless-stopped') |
| until | [DateTime] Containers created before the given duration or time. |
| command | [Command] the command the container is executing, only argv[0] is taken |
| should-start-on-boot | [Bool] Containers that need to be restarted after system reboot. True for containers with restart policy 'always', or 'unless-stopped' that were not explicitly stopped by the user |
@@option interactive

View File

@@ -39,6 +39,7 @@ Valid filters are listed below:
| id | [ID] Container's ID (CID prefix match by default; accepts regex) |
| name | [Name] Container's name (accepts regex) |
| label | [Key] or [Key=Value] Label assigned to a container |
| label! | [Key] or [Key=Value] Label NOT assigned to a container |
| exited | [Int] Container's exit code |
| status | [Status] Container's status: 'created', 'initialized', 'exited', 'paused', 'running', 'unknown' |
| ancestor | [ImageName] Image or descendant used to create container |
@@ -48,8 +49,10 @@ Valid filters are listed below:
| health | [Status] healthy or unhealthy |
| pod | [Pod] name or full or partial ID of pod |
| network | [Network] name or full ID of network |
| restart-policy | [Policy] Container's restart policy (e.g., 'no', 'on-failure', 'always', 'unless-stopped') |
| until | [DateTime] Containers created before the given duration or time. |
| command | [Command] the command the container is executing, only argv[0] is taken |
| should-start-on-boot | [Bool] Containers that need to be restarted after system reboot. True for containers with restart policy 'always', or 'unless-stopped' that were not explicitly stopped by the user |
@@option ignore

View File

@@ -33,6 +33,7 @@ Valid filters are listed below:
| id | [ID] Container's ID (CID prefix match by default; accepts regex) |
| name | [Name] Container's name (accepts regex) |
| label | [Key] or [Key=Value] Label assigned to a container |
| label! | [Key] or [Key=Value] Label NOT assigned to a container |
| exited | [Int] Container's exit code |
| status | [Status] Container's status: 'created', 'initialized', 'exited', 'paused', 'running', 'unknown' |
| ancestor | [ImageName] Image or descendant used to create container |
@@ -42,8 +43,10 @@ Valid filters are listed below:
| health | [Status] healthy or unhealthy |
| pod | [Pod] name or full or partial ID of pod |
| network | [Network] name or full ID of network |
| restart-policy | [Policy] Container's restart policy (e.g., 'no', 'on-failure', 'always', 'unless-stopped') |
| until | [DateTime] Containers created before the given duration or time. |
| command | [Command] the command the container is executing, only argv[0] is taken |
| should-start-on-boot | [Bool] Containers that need to be restarted after system reboot. True for containers with restart policy 'always', or 'unless-stopped' that were not explicitly stopped by the user |
@@option latest

View File

@@ -189,6 +189,7 @@ type ContainerState struct {
BindMounts map[string]string `json:"bindMounts,omitempty"`
// StoppedByUser indicates whether the container was stopped by an
// explicit call to the Stop() API.
// Warning: This field does persist across system reboots.
StoppedByUser bool `json:"stoppedByUser,omitempty"`
// RestartPolicyMatch indicates whether the conditions for restart
// policy have been met.

View File

@@ -1093,6 +1093,28 @@ func (c *Container) ShouldRestart(_ context.Context) bool {
return c.shouldRestart()
}
// Indicate whether or not the container will should start after a reboot of system
func (c *Container) ShouldStartOnBoot() bool {
if !c.batched {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.syncContainer(); err != nil {
return false
}
}
if c.ensureState(define.ContainerStateConfigured, define.ContainerStateCreated) {
return false
}
configuredRestartPolicy := c.RestartPolicy()
isAlways := configuredRestartPolicy == define.RestartPolicyAlways
isUnlessStopped := configuredRestartPolicy == define.RestartPolicyUnlessStopped && !c.state.StoppedByUser
return isAlways || isUnlessStopped
}
// CopyFromArchive copies the contents from the specified tarStream to path
// *inside* the container.
func (c *Container) CopyFromArchive(_ context.Context, containerPath string, chown, noOverwriteDirNonDir bool, rename map[string]string, tarStream io.Reader) (func() error, error) {

View File

@@ -635,7 +635,6 @@ func resetContainerState(state *ContainerState) {
state.ExecSessions = make(map[string]*ExecSession)
state.LegacyExecSessions = nil
state.BindMounts = make(map[string]string)
state.StoppedByUser = false
state.RestartPolicyMatch = false
state.RestartCount = 0
state.Checkpointed = false

View File

@@ -287,6 +287,18 @@ func GenerateContainerFilterFuncs(filter string, filterValues []string, r *libpo
return func(c *libpod.Container) bool {
return util.StringMatchRegexSlice(c.Command()[0], filterValues)
}, nil
case "should-start-on-boot":
wantRestart := false
var err error
for _, fv := range filterValues {
wantRestart, err = strconv.ParseBool(fv)
if err != nil {
return nil, err
}
}
return func(c *libpod.Container) bool {
return c.ShouldStartOnBoot() == wantRestart
}, nil
}
return nil, fmt.Errorf("%s is an invalid filter", filter)
}

View File

@@ -1051,4 +1051,70 @@ var _ = Describe("Podman ps", func() {
Expect(output).To(HaveLen(1))
Expect(output).Should(ContainElement(ContainSubstring("late")))
})
It("podman ps filter should-start-on-boot", func() {
commands := [][]string{
{"create", "--restart", "unless-stopped", "--name", "test-unless-stopped-user-stop", ALPINE, "top"},
{"create", "--restart", "always", "--name", "test-always-user-stop", ALPINE, "top"},
{"create", "--restart", "no", "--name", "test-no-restart-user-stop", ALPINE, "top"},
{"create", "--restart", "on-failure", "--name", "test-onfailure-user-stop", ALPINE, "top"},
{"create", "--restart", "unless-stopped", "--name", "test-unless-stopped-exit-not-started", ALPINE, "false"},
{"create", "--restart", "always", "--name", "test-always-exit-not-started", ALPINE, "false"},
{"create", "--restart", "on-failure", "--name", "test-onfailure-exit-not-started", ALPINE, "false"},
{"start", "test-unless-stopped-user-stop"},
{"stop", "test-unless-stopped-user-stop"},
{"start", "test-always-user-stop"},
{"stop", "test-always-user-stop"},
{"start", "test-no-restart-user-stop"},
{"stop", "test-no-restart-user-stop"},
{"start", "test-onfailure-user-stop"},
{"stop", "test-onfailure-user-stop"},
}
for _, cmd := range commands {
podmanTest.PodmanExitCleanly(cmd...)
}
commandsExit := [][]string{
{"run", "--name", "test-unless-stopped-exit-bad", "--restart", "unless-stopped", ALPINE, "false"},
{"run", "--name", "test-always-exit-bad", "--restart", "always", ALPINE, "false"},
{"run", "--name", "test-onfailure-exit-bad", "--restart", "on-failure", ALPINE, "false"},
}
for _, cmd := range commandsExit {
session := podmanTest.Podman(cmd)
session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(1))
}
session := podmanTest.PodmanExitCleanly("ps", "-a", "--filter", "should-start-on-boot=true", "--format", "{{.Names}}")
output := session.OutputToString()
Expect(output).To(ContainSubstring("test-unless-stopped-exit-bad"))
Expect(output).To(ContainSubstring("test-always-exit-bad"))
Expect(output).To(ContainSubstring("test-always-user-stop"))
Expect(output).ToNot(ContainSubstring("test-unless-stopped-exit-not-started"))
Expect(output).ToNot(ContainSubstring("test-always-exit-not-started"))
Expect(output).ToNot(ContainSubstring("test-unless-stopped-user-stop"))
Expect(output).ToNot(ContainSubstring("test-no-restart-user-stop"))
Expect(output).ToNot(ContainSubstring("test-onfailure-user-stop"))
Expect(output).ToNot(ContainSubstring("test-onfailure-exit-not-started"))
Expect(output).ToNot(ContainSubstring("test-onfailure-exit-bad"))
session = podmanTest.PodmanExitCleanly("ps", "-a", "--filter", "should-start-on-boot=false", "--format", "{{.Names}}")
output = session.OutputToString()
Expect(output).To(ContainSubstring("test-unless-stopped-user-stop"))
Expect(output).To(ContainSubstring("test-no-restart-user-stop"))
Expect(output).To(ContainSubstring("test-unless-stopped-exit-not-started"))
Expect(output).To(ContainSubstring("test-always-exit-not-started"))
Expect(output).To(ContainSubstring("test-onfailure-user-stop"))
Expect(output).To(ContainSubstring("test-onfailure-exit-not-started"))
Expect(output).To(ContainSubstring("test-onfailure-exit-bad"))
Expect(output).ToNot(ContainSubstring("test-always-user-stop"))
Expect(output).ToNot(ContainSubstring("test-unless-stopped-exit-bad"))
Expect(output).ToNot(ContainSubstring("test-always-exit-bad"))
})
})