Repository: containers/podman
Pull request #25842: Quadlet - Add support for adding ExecReload command
Author: login: ygalblum
Labels: name: lgtm
description: Indicates that a PR is ready to be merged.
name: approved
description: Indicates a PR has been approved by an approver from all required OWNERS files.
name: release-note
name: locked - please file new issue/PR
description: Assist humans wanting to comment on an old issue or PR with locked comments.
Created: 2025-04-09T16:43:17Z | Merged: 2025-04-10T14:21:27Z

#### Does this PR introduce a user-facing change?
Yes

```release-note
Quadlet - add support for adding ExecReload command by either sending a signal or running exec
```

Resolves: #22036 



Diff:
diff --git a/docs/source/markdown/podman-systemd.unit.5.md b/docs/source/markdown/podman-systemd.unit.5.md
index eec0a977993..1c35c997eae 100644
--- a/docs/source/markdown/podman-systemd.unit.5.md
+++ b/docs/source/markdown/podman-systemd.unit.5.md
@@ -333,6 +333,8 @@ Valid options for `[Container]` are listed below:
 | Pull=never                           | --pull never                                         |
 | ReadOnly=true                        | --read-only                                          |
 | ReadOnlyTmpfs=true                   | --read-only-tmpfs                                    |
+| ReloadCmd=/usr/bin/command           | Add ExecReload and run exec with the value           |
+| ReloadSignal=SIGHUP                  | Add ExecReload and run kill with the signal          |
 | Retry=5                              | --retry=5                                            |
 | RetryDelay=5s                        | --retry-delay=5s                                     |
 | Rootfs=/var/lib/rootfs               | --rootfs /var/lib/rootfs                             |
@@ -784,6 +786,22 @@ If enabled, makes the image read-only.
 
 If ReadOnly is set to `true`, mount a read-write tmpfs on /dev, /dev/shm, /run, /tmp, and /var/tmp.
 
+### `ReloadCmd=`
+
+Add `ExecReload` line to the `Service` that runs ` podman exec` with this command in this container.
+
+In order to execute the reload run `systemctl reload <Service>`
+
+Mutually exclusive with `ReloadSignal`
+
+### `ReloadSignal=`
+
+Add `ExecReload` line to the `Service` that runs `podman kill` with this signal which sends the signal to the main container process.
+
+In order to execute the reload run `systemctl reload <Service>`
+
+Mutually exclusive with `ReloadCmd`
+
 ### `Retry=`
 
 Number of times to retry the image pull when a HTTP error occurs. Equivalent to the Podman `--retry` option.
diff --git a/pkg/systemd/quadlet/quadlet.go b/pkg/systemd/quadlet/quadlet.go
index 0ed87134b39..2c12a2212af 100644
--- a/pkg/systemd/quadlet/quadlet.go
+++ b/pkg/systemd/quadlet/quadlet.go
@@ -139,6 +139,8 @@ const (
 	KeyPull                  = "Pull"
 	KeyReadOnly              = "ReadOnly"
 	KeyReadOnlyTmpfs         = "ReadOnlyTmpfs"
+	KeyReloadCmd             = "ReloadCmd"
+	KeyReloadSignal          = "ReloadSignal"
 	KeyRemapGid              = "RemapGid"     // deprecated
 	KeyRemapUid              = "RemapUid"     // deprecated
 	KeyRemapUidSize          = "RemapUidSize" // deprecated
@@ -256,6 +258,8 @@ var (
 		KeyPull:                  true,
 		KeyReadOnly:              true,
 		KeyReadOnlyTmpfs:         true,
+		KeyReloadCmd:             true,
+		KeyReloadSignal:          true,
 		KeyRemapGid:              true,
 		KeyRemapUid:              true,
 		KeyRemapUidSize:          true,
@@ -578,6 +582,10 @@ func ConvertContainer(container *parser.UnitFile, isUser bool, unitsInfoMap map[
 	serviceStopCmd.Args[0] = fmt.Sprintf("-%s", serviceStopCmd.Args[0])
 	service.AddCmdline(ServiceGroup, "ExecStopPost", serviceStopCmd.Args)
 
+	if err := handleExecReload(container, service, ContainerGroup); err != nil {
+		return nil, warnings, err
+	}
+
 	podman := createBasePodmanCommand(container, ContainerGroup)
 
 	podman.add("run")
@@ -2226,3 +2234,29 @@ func addDefaultDependencies(service *parser.UnitFile, isUser bool) {
 		service.PrependUnitLine(UnitGroup, "Wants", networkUnit)
 	}
 }
+
+func handleExecReload(quadletUnitFile, serviceUnitFile *parser.UnitFile, groupName string) error {
+	reloadSignal, signalOk := quadletUnitFile.Lookup(groupName, KeyReloadSignal)
+	signalOk = signalOk && len(reloadSignal) > 0
+	reloadcmd, cmdOk := quadletUnitFile.LookupLastArgs(groupName, KeyReloadCmd)
+	cmdOk = cmdOk && len(reloadcmd) > 0
+
+	if !cmdOk && !signalOk {
+		return nil
+	}
+
+	if cmdOk && signalOk {
+		return fmt.Errorf("%s and %s are mutually exclusive but both are set", KeyReloadCmd, KeyReloadSignal)
+	}
+
+	serviceReloadCmd := createBasePodmanCommand(quadletUnitFile, groupName)
+	if cmdOk {
+		serviceReloadCmd.add("exec", "--cidfile=%t/%N.cid")
+		serviceReloadCmd.add(reloadcmd...)
+	} else {
+		serviceReloadCmd.add("kill", "--cidfile=%t/%N.cid", "--signal", reloadSignal)
+	}
+	serviceUnitFile.AddCmdline(ServiceGroup, "ExecReload", serviceReloadCmd.Args)
+
+	return nil
+}
diff --git a/test/e2e/quadlet/reloadboth.container b/test/e2e/quadlet/reloadboth.container
new file mode 100644
index 00000000000..f3a1dde72ce
--- /dev/null
+++ b/test/e2e/quadlet/reloadboth.container
@@ -0,0 +1,7 @@
+## assert-failed
+## assert-stderr-contains "ReloadCmd and ReloadSignal are mutually exclusive but both are set"
+
+[Container]
+Image=localhost/imagename
+ReloadCmd=/usr/bin/command
+ReloadSignal=SIGHUP
diff --git a/test/e2e/quadlet/reloadcmd.container b/test/e2e/quadlet/reloadcmd.container
new file mode 100644
index 00000000000..cb3643663a2
--- /dev/null
+++ b/test/e2e/quadlet/reloadcmd.container
@@ -0,0 +1,8 @@
+## assert-podman-reload-args "exec"
+## assert-podman-reload-args "--cidfile=%t/%N.cid"
+## assert-podman-reload-final-args "/some/binary file" "--arg1" "arg 2"
+
+[Container]
+Image=localhost/imagename
+ReloadCmd="/some/binary file" --arg1 \
+  "arg 2"
diff --git a/test/e2e/quadlet/reloadsignal.container b/test/e2e/quadlet/reloadsignal.container
new file mode 100644
index 00000000000..60b8220e996
--- /dev/null
+++ b/test/e2e/quadlet/reloadsignal.container
@@ -0,0 +1,7 @@
+## assert-podman-reload-args "kill"
+## assert-podman-reload-args "--cidfile=%t/%N.cid"
+## assert-podman-reload-args "--signal" "SIGHUP"
+
+[Container]
+Image=localhost/imagename
+ReloadSignal=SIGHUP
diff --git a/test/e2e/quadlet_test.go b/test/e2e/quadlet_test.go
index b716441c6c2..a621b6e3615 100644
--- a/test/e2e/quadlet_test.go
+++ b/test/e2e/quadlet_test.go
@@ -477,6 +477,30 @@ func (t *quadletTestcase) assertStopPostPodmanArgsKeyValRegex(args []string, uni
 	return t.assertPodmanArgsKeyVal(args, unit, "ExecStopPost", true, false)
 }
 
+func (t *quadletTestcase) assertReloadPodmanArgs(args []string, unit *parser.UnitFile) bool {
+	return t.assertPodmanArgs(args, unit, "ExecReload", false, false)
+}
+
+func (t *quadletTestcase) assertReloadPodmanGlobalArgs(args []string, unit *parser.UnitFile) bool {
+	return t.assertPodmanArgs(args, unit, "ExecReload", false, true)
+}
+
+func (t *quadletTestcase) assertReloadPodmanFinalArgs(args []string, unit *parser.UnitFile) bool {
+	return t.assertPodmanFinalArgs(args, unit, "ExecReload")
+}
+
+func (t *quadletTestcase) assertReloadPodmanFinalArgsRegex(args []string, unit *parser.UnitFile) bool {
+	return t.assertPodmanFinalArgsRegex(args, unit, "ExecReload")
+}
+
+func (t *quadletTestcase) assertReloadPodmanArgsKeyVal(args []string, unit *parser.UnitFile) bool {
+	return t.assertPodmanArgsKeyVal(args, unit, "ExecReload", false, false)
+}
+
+func (t *quadletTestcase) assertReloadPodmanArgsKeyValRegex(args []string, unit *parser.UnitFile) bool {
+	return t.assertPodmanArgsKeyVal(args, unit, "ExecReload", true, false)
+}
+
 func (t *quadletTestcase) assertSymlink(args []string, unit *parser.UnitFile) bool {
 	Expect(args).To(HaveLen(2))
 	symlink := args[0]
@@ -590,6 +614,18 @@ func (t *quadletTestcase) doAssert(check []string, unit *parser.UnitFile, sessio
 		ok = t.assertStopPostPodmanArgsKeyVal(args, unit)
 	case "assert-podman-stop-post-args-key-val-regex":
 		ok = t.assertStopPostPodmanArgsKeyValRegex(args, unit)
+	case "assert-podman-reload-args":
+		ok = t.assertReloadPodmanArgs(args, unit)
+	case "assert-podman-reload-global-args":
+		ok = t.assertReloadPodmanGlobalArgs(args, unit)
+	case "assert-podman-reload-final-args":
+		ok = t.assertReloadPodmanFinalArgs(args, unit)
+	case "assert-podman-reload-final-args-regex":
+		ok = t.assertReloadPodmanFinalArgsRegex(args, unit)
+	case "assert-podman-reload-args-key-val":
+		ok = t.assertReloadPodmanArgsKeyVal(args, unit)
+	case "assert-podman-reload-args-key-val-regex":
+		ok = t.assertReloadPodmanArgsKeyValRegex(args, unit)
 
 	default:
 		return fmt.Errorf("Unsupported assertion %s", op)
@@ -926,6 +962,8 @@ BOGUS=foo
 		Entry("CgroupMode", "cgroups-mode.container"),
 		Entry("Container - No Default Dependencies", "no_deps.container"),
 		Entry("retry.container", "retry.container"),
+		Entry("reloadcmd.container", "reloadcmd.container"),
+		Entry("reloadsignal.container", "reloadsignal.container"),
 
 		Entry("basic.volume", "basic.volume"),
 		Entry("device-copy.volume", "device-copy.volume"),
@@ -1063,6 +1101,7 @@ BOGUS=foo
 		Entry("pod.not-found.container", "pod.not-found.container", "converting \"pod.not-found.container\": quadlet pod unit not-found.pod does not exist"),
 		Entry("subidmapping-with-remap.container", "subidmapping-with-remap.container", "converting \"subidmapping-with-remap.container\": deprecated Remap keys are set along with explicit mapping keys"),
 		Entry("userns-with-remap.container", "userns-with-remap.container", "converting \"userns-with-remap.container\": deprecated Remap keys are set along with explicit mapping keys"),
+		Entry("reloadboth.container", "reloadboth.container", "converting \"reloadboth.container\": ReloadCmd and ReloadSignal are mutually exclusive but both are set"),
 
 		Entry("image-no-image.volume", "image-no-image.volume", "converting \"image-no-image.volume\": the key Image is mandatory when using the image driver"),
 		Entry("Volume - Quadlet image (.build) not found", "build-not-found.quadlet.volume", "converting \"build-not-found.quadlet.volume\": requested Quadlet image not-found.build was not found"),


Reviews:
user: login: Luap99
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T17:53:51Z
user: login: ygalblum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T18:40:51Z
user: login: ygalblum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T18:40:58Z
user: login: Luap99
body: /lgtm
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-10T10:08:25Z

Review comments:
diff_hunk: @@ -784,6 +786,18 @@ If enabled, makes the image read-only.
 
 If ReadOnly is set to `true`, mount a read-write tmpfs on /dev, /dev/shm, /run, /tmp, and /var/tmp.
 
+### `ReloadCmd=`
+
+Add `ExecReload` line to the `Service` that runs `exec` with this value.
path: docs/source/markdown/podman-systemd.unit.5.md
user: login: Luap99
body: ```suggestion
Add `ExecReload` line to the `Service` that runs ` podman exec` with this command in this container.
```

Also maybe we should define what ExecReload does, i.e. it will not be run by default but can be triggered by running `systemctl reload <service>`
created_at: 2025-04-09T17:49:34Z
author_association: MEMBER
diff_hunk: @@ -784,6 +786,18 @@ If enabled, makes the image read-only.
 
 If ReadOnly is set to `true`, mount a read-write tmpfs on /dev, /dev/shm, /run, /tmp, and /var/tmp.
 
+### `ReloadCmd=`
+
+Add `ExecReload` line to the `Service` that runs `exec` with this value.
+
+Mutually exclusive with `ReloadSignal`
+
+### `ReloadSignal=`
+
+Add `ExecReload` line to the `Service` that runs `kill` with this signal.
path: docs/source/markdown/podman-systemd.unit.5.md
user: login: Luap99
body: ```suggestion
Add `ExecReload` line to the `Service` that runs `podman kill` with this signal which sends the signal to the main container process.
```
created_at: 2025-04-09T17:50:07Z
author_association: MEMBER
diff_hunk: @@ -784,6 +786,18 @@ If enabled, makes the image read-only.
 
 If ReadOnly is set to `true`, mount a read-write tmpfs on /dev, /dev/shm, /run, /tmp, and /var/tmp.
 
+### `ReloadCmd=`
+
+Add `ExecReload` line to the `Service` that runs `exec` with this value.
path: docs/source/markdown/podman-systemd.unit.5.md
user: login: ygalblum
body: Done
created_at: 2025-04-09T18:40:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -784,6 +786,18 @@ If enabled, makes the image read-only.
 
 If ReadOnly is set to `true`, mount a read-write tmpfs on /dev, /dev/shm, /run, /tmp, and /var/tmp.
 
+### `ReloadCmd=`
+
+Add `ExecReload` line to the `Service` that runs `exec` with this value.
+
+Mutually exclusive with `ReloadSignal`
+
+### `ReloadSignal=`
+
+Add `ExecReload` line to the `Service` that runs `kill` with this signal.
path: docs/source/markdown/podman-systemd.unit.5.md
user: login: ygalblum
body: Done
created_at: 2025-04-09T18:40:58Z
author_association: CONTRIBUTOR

PR comments:
user: login: packit-as-a-service[bot]
created_at: 2025-04-09T16:46:59Z
body: [NON-BLOCKING] Packit jobs failed. @containers/packit-build please check. Everyone else, feel free to ignore.
author_association: NONE
user: login: openshift-ci[bot]
created_at: 2025-04-10T10:08:30Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/containers/podman/pull/25842#pullrequestreview-2756127757" title="LGTM">Luap99</a>*, *<a href="https://github.com/containers/podman/pull/25842#" title="Author self-approved">ygalblum</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=containers%2Fpodman).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/containers/podman/blob/main/OWNERS)~~ [Luap99,ygalblum]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: CONTRIBUTOR
user: login: Luap99
created_at: 2025-04-10T10:09:18Z
body: Oh, removing lgtm label again, I thought it already had a second LGTM.
author_association: MEMBER
user: login: mheon
created_at: 2025-04-10T14:18:26Z
body: /lgtm
author_association: MEMBER