Skip to content

Commit 9266aa5

Browse files
committed
Handle uninstantiated template quadlets
Fixes: #26960 Signed-off-by: Šimon Brauner <sbrauner@redhat.com>
1 parent ef7fdba commit 9266aa5

5 files changed

Lines changed: 107 additions & 18 deletions

File tree

docs/source/markdown/podman-quadlet-list.1.md.in

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Supported filters:
2323
| Filter | Description |
2424
|------------|--------------------------------------------------------------------------------------------------|
2525
| name | Filter by quadlet name |
26-
| status | Filter by quadlet status. Valid values: `Not loaded`, `active/running`, `inactive/dead`, `failed/failed`, `activating/start`, `deactivating/stop` |
26+
| status | Filter by quadlet status. Valid values: `Not loaded`, `loaded template` `active/running`, `inactive/dead`, `failed/failed`, `activating/start`, `deactivating/stop` |
2727

2828
#### **--format**=*format*
2929

@@ -36,7 +36,7 @@ Print results with a Go template.
3636
| .App | Name of application if Quadlet is part of an app |
3737
| .Name | Name of the Quadlet file |
3838
| .Path | Quadlet file path on disk |
39-
| .Status | Quadlet status corresponding to systemd unit |
39+
| .Status | Quadlet status corresponding to systemd unit (`Not loaded` and `loaded template` are from podman, other values are from systemd) |
4040
| .UnitName | Systemd unit name corresponding to quadlet |
4141

4242
@@option noheading

docs/source/markdown/podman-quadlet-rm.1.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ podman\-quadlet\-rm - Removes an installed quadlet
1111
Remove one or more installed Quadlets from the current user. Following command also takes application name
1212
as input and removes all the Quadlets which belongs to that specific application.
1313

14+
When the argument is uninstantiated template quadlet, this command removes the template quadlet file (e.g. `templateName@.container`) and the generated systemd template unit (e.g. `templateName@.service`, unless **--reload-systemd** is set to `false`). Instances of the systemd template unit (e.g. `templateName@instanceName.service`) may persist, and can be removed with **systemctl(1)**.
15+
1416
Note: If a quadlet is part of an application, removing that specific quadlet will remove the entire application.
1517
When a quadlet is installed from a directory, all files installed from that directory—including both quadlet and non-quadlet files—are considered part
1618
of a single application.
@@ -31,7 +33,8 @@ Do not error for Quadlets that do not exist.
3133

3234
#### **--reload-systemd**
3335

34-
Reload systemd after removing Quadlets (default true).
36+
Reload systemd after removing Quadlets if at least
37+
one of them had a corresponding systemd unit (default true).
3538
In order to disable it users need to manually set the value
3639
of this flag to `false`.
3740

pkg/domain/infra/abi/quadlet.go

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,8 @@ func (ic *ContainerEngine) QuadletList(ctx context.Context, options entities.Qua
692692
}
693693

694694
reports := make([]*entities.ListQuadlet, 0, len(quadletPaths))
695-
allServiceNames := make([]string, 0, len(quadletPaths))
695+
concreteServiceNames := make([]string, 0, len(quadletPaths))
696+
templateServiceNames := make([]string, 0, len(quadletPaths))
696697
partialReports := make(map[string]entities.ListQuadlet)
697698

698699
for _, path := range quadletPaths {
@@ -714,16 +715,21 @@ func (ic *ContainerEngine) QuadletList(ctx context.Context, options entities.Qua
714715
continue
715716
}
716717
partialReports[serviceName] = report
717-
allServiceNames = append(allServiceNames, serviceName)
718+
719+
if systemdquadlet.IsTemplateUnitFileName(serviceName) {
720+
templateServiceNames = append(templateServiceNames, serviceName)
721+
} else {
722+
concreteServiceNames = append(concreteServiceNames, serviceName)
723+
}
718724
}
719725

720-
// Get status of all systemd units with given names.
721-
statuses, err := conn.ListUnitsByNamesContext(ctx, allServiceNames)
726+
// Get status of concrete systemd units with given names.
727+
statuses, err := conn.ListUnitsByNamesContext(ctx, concreteServiceNames)
722728
if err != nil {
723729
return nil, fmt.Errorf("querying systemd for unit status: %w", err)
724730
}
725-
if len(statuses) != len(allServiceNames) {
726-
logrus.Warnf("Queried for %d services but received %d responses", len(allServiceNames), len(statuses))
731+
if len(statuses) != len(concreteServiceNames) {
732+
logrus.Warnf("Queried for %d services but received %d responses", len(concreteServiceNames), len(statuses))
727733
}
728734
for _, unitStatus := range statuses {
729735
report, ok := partialReports[unitStatus.Name]
@@ -744,6 +750,30 @@ func (ic *ContainerEngine) QuadletList(ctx context.Context, options entities.Qua
744750
delete(partialReports, unitStatus.Name)
745751
}
746752

753+
// Uninstantiated template units need to be handled separately.
754+
unitFiles, err := conn.ListUnitFilesByPatternsContext(ctx, []string{}, templateServiceNames)
755+
if err != nil {
756+
return nil, fmt.Errorf("querying systemd for unit file: %w", err)
757+
}
758+
unitFilesFound := make(map[string]struct{})
759+
for _, unitFile := range unitFiles {
760+
unitFilesFound[filepath.Base(unitFile.Path)] = struct{}{}
761+
}
762+
for _, templateServiceName := range templateServiceNames {
763+
report := partialReports[templateServiceName]
764+
765+
report.UnitName = templateServiceName
766+
767+
if _, ok := unitFilesFound[templateServiceName]; ok {
768+
report.Status = "loaded template"
769+
} else {
770+
report.Status = "Not loaded"
771+
}
772+
773+
reports = append(reports, &report)
774+
delete(partialReports, templateServiceName)
775+
}
776+
747777
// This should not happen.
748778
// Systemd will give us output for everything we sent to them, even if it's not a valid unit.
749779
// We can find them with LoadState, as we do above.
@@ -852,10 +882,11 @@ func (ic *ContainerEngine) QuadletRemove(ctx context.Context, quadlets []string,
852882
}
853883
quadlets = expandQuadletList
854884
allQuadletPaths := make([]string, 0, len(quadlets))
855-
allServiceNames := make([]string, 0, len(quadlets))
885+
concreteServiceNames := make([]string, 0, len(quadlets))
886+
templateServiceNames := make([]string, 0, len(quadlets))
856887
runningQuadlets := make([]string, 0, len(quadlets))
857888
serviceNameToQuadletName := make(map[string]string)
858-
needReload := options.ReloadSystemd
889+
needReload := false
859890

860891
if len(quadlets) == 0 && !options.All {
861892
return nil, errors.New("must provide at least 1 quadlet to remove")
@@ -932,15 +963,19 @@ func (ic *ContainerEngine) QuadletRemove(ctx context.Context, quadlets []string,
932963
continue
933964
}
934965

935-
allServiceNames = append(allServiceNames, serviceName)
966+
if systemdquadlet.IsTemplateUnitFileName(serviceName) {
967+
templateServiceNames = append(templateServiceNames, serviceName)
968+
} else {
969+
concreteServiceNames = append(concreteServiceNames, serviceName)
970+
}
936971
serviceNameToQuadletName[serviceName] = quadlet
937972
}
938973

939-
if len(allServiceNames) != 0 {
974+
if len(concreteServiceNames) != 0 {
940975
// Check if units are loaded into systemd, and further if they are running.
941976
// If running and force is not set, error.
942977
// If force is set, try and stop the unit.
943-
statuses, err := conn.ListUnitsByNamesContext(ctx, allServiceNames)
978+
statuses, err := conn.ListUnitsByNamesContext(ctx, concreteServiceNames)
944979
if err != nil {
945980
return nil, fmt.Errorf("querying systemd for unit status: %w", err)
946981
}
@@ -951,7 +986,7 @@ func (ic *ContainerEngine) QuadletRemove(ctx context.Context, quadlets []string,
951986
// Nothing to do here if it doesn't exist in systemd
952987
continue
953988
}
954-
needReload = options.ReloadSystemd
989+
needReload = needReload || options.ReloadSystemd
955990
if unitStatus.ActiveState == "active" {
956991
if !options.Force {
957992
report.Errors[quadletName] = fmt.Errorf("quadlet %s is running and force is not set, refusing to remove: %w", quadletName, define.ErrQuadletRunning)
@@ -976,6 +1011,17 @@ func (ic *ContainerEngine) QuadletRemove(ctx context.Context, quadlets []string,
9761011
}
9771012
}
9781013

1014+
if len(templateServiceNames) != 0 {
1015+
// Uninstantiated template units need to be handled separately.
1016+
unitFiles, err := conn.ListUnitFilesByPatternsContext(ctx, []string{}, templateServiceNames)
1017+
if err != nil {
1018+
return nil, fmt.Errorf("querying systemd for unit file: %w", err)
1019+
}
1020+
if len(unitFiles) != 0 {
1021+
needReload = needReload || options.ReloadSystemd
1022+
}
1023+
}
1024+
9791025
// Remove the actual files behind the quadlets
9801026
if len(allQuadletPaths) != 0 {
9811027
for _, path := range allQuadletPaths {

pkg/systemd/quadlet/quadlet.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -939,8 +939,9 @@ func ConvertContainer(container *parser.UnitFile, unitsInfoMap map[string]*UnitI
939939
return service, warnings, nil
940940
}
941941

942-
func isTemplateUnit(unit *parser.UnitFile) bool {
943-
base := strings.TrimSuffix(unit.Filename, filepath.Ext(unit.Filename))
942+
// Determine if the given file name belongs to an uninstentiated template unit.
943+
func IsTemplateUnitFileName(fileName string) bool {
944+
base := strings.TrimSuffix(fileName, filepath.Ext(fileName))
944945
return strings.HasSuffix(base, "@")
945946
}
946947

@@ -2223,7 +2224,7 @@ func handlePod(quadletUnitFile, serviceUnitFile *parser.UnitFile, groupName stri
22232224
// If we want to start the container with the pod, we add it to this list.
22242225
// This creates corresponding Wants=/Before= statements in the pod service.
22252226
// Do not add this for template units as dependency cannot be created for them.
2226-
if !isTemplateUnit(quadletUnitFile) && quadletUnitFile.LookupBooleanWithDefault(groupName, KeyStartWithPod, true) {
2227+
if !IsTemplateUnitFileName(quadletUnitFile.Filename) && quadletUnitFile.LookupBooleanWithDefault(groupName, KeyStartWithPod, true) {
22272228
podInfo.ContainersToStart = append(podInfo.ContainersToStart, serviceUnitFile.Filename)
22282229
}
22292230
}

test/system/253-podman-quadlet.bats

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,45 @@ EOF
104104
assert "$output" !~ "alpine-quadlet.container" "list should not contain removed container"
105105
}
106106

107+
@test "quadlet verb - install, list, print, rm - template" {
108+
# Determine the install directory path based on rootless/root
109+
local install_dir
110+
install_dir=$(get_quadlet_install_dir)
111+
# Create a test quadlet file
112+
local quadlet_name=templated-quadlet@.container
113+
local quadlet_unit_name=${quadlet_name%.container}.service
114+
local quadlet_file=$PODMAN_TMPDIR/$quadlet_name
115+
cat > "$quadlet_file" <<EOF
116+
[Container]
117+
Image=$IMAGE
118+
Exec=sh -c "echo STARTED CONTAINER; trap 'exit' SIGTERM; while :; do sleep 0.1; done"
119+
EOF
120+
# Test quadlet install
121+
run_podman quadlet install "$quadlet_file"
122+
# Verify install output contains the quadlet name
123+
assert "$output" =~ "$quadlet_name" "install output should contain quadlet name"
124+
125+
# Test quadlet list
126+
run_podman quadlet list
127+
assert "$output" =~ "$quadlet_name" "list should contain $quadlet_name"
128+
assert "$output" =~ "$quadlet_unit_name" "UNIT NAME should be $quadlet_unit_name"
129+
# Loaded status should be loaded template
130+
assert "$output" =~ "loaded template" "STATUS should be 'loaded template'"
131+
assert "$output" =~ "$install_dir/$quadlet_name" "PATH ON DISK should show the quadlet file path"
132+
133+
# Test quadlet print
134+
run_podman quadlet print "$quadlet_name"
135+
assert "$output" == "$(<"$quadlet_file")" "print output matches quadlet file"
136+
137+
# Test quadlet rm
138+
run_podman quadlet rm "$quadlet_name"
139+
# Verify remove output contains the quadlet name
140+
assert "$output" =~ "$quadlet_name" "remove output should contain quadlet name"
141+
# Verify removal
142+
run_podman quadlet list
143+
assert "$output" !~ "$quadlet_name" "list should not contain removed container"
144+
}
145+
107146
@test "quadlet verb - install multiple files from directory and remove by app name" {
108147
# Create a directory for multiple quadlet files
109148
local app_name="test-app-$(safe_name)"

0 commit comments

Comments
 (0)