Skip to content

Commit 32cd3f7

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

5 files changed

Lines changed: 106 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,8 +23,8 @@ 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` |
2726
| pod | Filter by the `Pod=` value (displays only for .container units) |
27+
| status | Filter by quadlet status. Valid values: `Not loaded`, `loaded template`, `active/running`, `inactive/dead`, `failed/failed`, `activating/start`, `deactivating/stop` |
2828

2929
#### **--format**=*format*
3030

@@ -38,7 +38,7 @@ Print results with a Go template.
3838
| .Name | Name of the Quadlet file |
3939
| .Path | Quadlet file path on disk |
4040
| .Pod | Pod quadlet file from `Pod=` in `[Container]` (empty if not set) |
41-
| .Status | Quadlet status corresponding to systemd unit |
41+
| .Status | Quadlet status corresponding to systemd unit (`Not loaded` and `loaded template` are from podman, other values are from systemd) |
4242
| .UnitName | Systemd unit name corresponding to quadlet |
4343

4444
@@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
@@ -703,7 +703,8 @@ func (ic *ContainerEngine) QuadletList(ctx context.Context, options entities.Qua
703703
}
704704

705705
reports := make([]*entities.ListQuadlet, 0, len(quadletPaths))
706-
allServiceNames := make([]string, 0, len(quadletPaths))
706+
concreteServiceNames := make([]string, 0, len(quadletPaths))
707+
templateServiceNames := make([]string, 0, len(quadletPaths))
707708
partialReports := make(map[string]entities.ListQuadlet)
708709

709710
for _, path := range quadletPaths {
@@ -728,16 +729,21 @@ func (ic *ContainerEngine) QuadletList(ctx context.Context, options entities.Qua
728729
report.Pod = pod
729730
}
730731
partialReports[serviceName] = report
731-
allServiceNames = append(allServiceNames, serviceName)
732+
733+
if systemdquadlet.IsTemplateUnitFileName(serviceName) {
734+
templateServiceNames = append(templateServiceNames, serviceName)
735+
} else {
736+
concreteServiceNames = append(concreteServiceNames, serviceName)
737+
}
732738
}
733739

734-
// Get status of all systemd units with given names.
735-
statuses, err := conn.ListUnitsByNamesContext(ctx, allServiceNames)
740+
// Get status of concrete systemd units with given names.
741+
statuses, err := conn.ListUnitsByNamesContext(ctx, concreteServiceNames)
736742
if err != nil {
737743
return nil, fmt.Errorf("querying systemd for unit status: %w", err)
738744
}
739-
if len(statuses) != len(allServiceNames) {
740-
logrus.Warnf("Queried for %d services but received %d responses", len(allServiceNames), len(statuses))
745+
if len(statuses) != len(concreteServiceNames) {
746+
logrus.Warnf("Queried for %d services but received %d responses", len(concreteServiceNames), len(statuses))
741747
}
742748
for _, unitStatus := range statuses {
743749
report, ok := partialReports[unitStatus.Name]
@@ -758,6 +764,30 @@ func (ic *ContainerEngine) QuadletList(ctx context.Context, options entities.Qua
758764
delete(partialReports, unitStatus.Name)
759765
}
760766

767+
// Uninstantiated template units need to be handled separately.
768+
unitFiles, err := conn.ListUnitFilesByPatternsContext(ctx, []string{}, templateServiceNames)
769+
if err != nil {
770+
return nil, fmt.Errorf("querying systemd for unit file: %w", err)
771+
}
772+
unitFilesFound := make(map[string]struct{})
773+
for _, unitFile := range unitFiles {
774+
unitFilesFound[filepath.Base(unitFile.Path)] = struct{}{}
775+
}
776+
for _, templateServiceName := range templateServiceNames {
777+
report := partialReports[templateServiceName]
778+
779+
report.UnitName = templateServiceName
780+
781+
if _, ok := unitFilesFound[templateServiceName]; ok {
782+
report.Status = "loaded template"
783+
} else {
784+
report.Status = "Not loaded"
785+
}
786+
787+
reports = append(reports, &report)
788+
delete(partialReports, templateServiceName)
789+
}
790+
761791
// This should not happen.
762792
// Systemd will give us output for everything we sent to them, even if it's not a valid unit.
763793
// We can find them with LoadState, as we do above.
@@ -869,10 +899,11 @@ func (ic *ContainerEngine) QuadletRemove(ctx context.Context, quadlets []string,
869899
}
870900
quadlets = expandQuadletList
871901
allQuadletPaths := make([]string, 0, len(quadlets))
872-
allServiceNames := make([]string, 0, len(quadlets))
902+
concreteServiceNames := make([]string, 0, len(quadlets))
903+
templateServiceNames := make([]string, 0, len(quadlets))
873904
runningQuadlets := make([]string, 0, len(quadlets))
874905
serviceNameToQuadletName := make(map[string]string)
875-
needReload := options.ReloadSystemd
906+
needReload := false
876907

877908
if len(quadlets) == 0 && !options.All {
878909
return nil, errors.New("must provide at least 1 quadlet to remove")
@@ -949,15 +980,19 @@ func (ic *ContainerEngine) QuadletRemove(ctx context.Context, quadlets []string,
949980
continue
950981
}
951982

952-
allServiceNames = append(allServiceNames, serviceName)
983+
if systemdquadlet.IsTemplateUnitFileName(serviceName) {
984+
templateServiceNames = append(templateServiceNames, serviceName)
985+
} else {
986+
concreteServiceNames = append(concreteServiceNames, serviceName)
987+
}
953988
serviceNameToQuadletName[serviceName] = quadlet
954989
}
955990

956-
if len(allServiceNames) != 0 {
991+
if len(concreteServiceNames) != 0 {
957992
// Check if units are loaded into systemd, and further if they are running.
958993
// If running and force is not set, error.
959994
// If force is set, try and stop the unit.
960-
statuses, err := conn.ListUnitsByNamesContext(ctx, allServiceNames)
995+
statuses, err := conn.ListUnitsByNamesContext(ctx, concreteServiceNames)
961996
if err != nil {
962997
return nil, fmt.Errorf("querying systemd for unit status: %w", err)
963998
}
@@ -968,7 +1003,7 @@ func (ic *ContainerEngine) QuadletRemove(ctx context.Context, quadlets []string,
9681003
// Nothing to do here if it doesn't exist in systemd
9691004
continue
9701005
}
971-
needReload = options.ReloadSystemd
1006+
needReload = needReload || options.ReloadSystemd
9721007
if unitStatus.ActiveState == "active" {
9731008
if !options.Force {
9741009
report.Errors[quadletName] = fmt.Errorf("quadlet %s is running and force is not set, refusing to remove: %w", quadletName, define.ErrQuadletRunning)
@@ -993,6 +1028,17 @@ func (ic *ContainerEngine) QuadletRemove(ctx context.Context, quadlets []string,
9931028
}
9941029
}
9951030

1031+
if len(templateServiceNames) != 0 {
1032+
// Uninstantiated template units need to be handled separately.
1033+
unitFiles, err := conn.ListUnitFilesByPatternsContext(ctx, []string{}, templateServiceNames)
1034+
if err != nil {
1035+
return nil, fmt.Errorf("querying systemd for unit file: %w", err)
1036+
}
1037+
if len(unitFiles) != 0 {
1038+
needReload = needReload || options.ReloadSystemd
1039+
}
1040+
}
1041+
9961042
// Remove the actual files behind the quadlets
9971043
if len(allQuadletPaths) != 0 {
9981044
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 uninstantiated template unit.
943+
func IsTemplateUnitFileName(fileName string) bool {
944+
base := strings.TrimSuffix(fileName, filepath.Ext(fileName))
944945
return strings.HasSuffix(base, "@")
945946
}
946947

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

test/system/253-podman-quadlet.bats

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,44 @@ EOF
154154
run_podman quadlet rm $ctr_unit
155155
}
156156

157+
@test "quadlet verb - install, list, print, rm - template" {
158+
# Determine the install directory path based on rootless/root
159+
local install_dir
160+
install_dir=$(get_quadlet_install_dir)
161+
# Create a test quadlet file
162+
local quadlet_name=templated-quadlet@.container
163+
local quadlet_unit_name=${quadlet_name%.container}.service
164+
local quadlet_file=$PODMAN_TMPDIR/$quadlet_name
165+
cat > "$quadlet_file" <<EOF
166+
[Container]
167+
Image=$IMAGE
168+
Exec=sh -c "echo STARTED CONTAINER; trap 'exit' SIGTERM; while :; do sleep 0.1; done"
169+
EOF
170+
# Test quadlet install
171+
run_podman quadlet install "$quadlet_file"
172+
# Verify install output contains the quadlet name
173+
assert "$output" =~ "$quadlet_name" "install output should contain quadlet name"
174+
175+
# Test quadlet list
176+
run_podman quadlet list
177+
assert "$output" =~ "$quadlet_name" "list should contain $quadlet_name"
178+
assert "$output" =~ "$quadlet_unit_name" "UNIT NAME should be $quadlet_unit_name"
179+
# Loaded status should be loaded template
180+
assert "$output" =~ "loaded template" "STATUS should be 'loaded template'"
181+
assert "$output" =~ "$install_dir/$quadlet_name" "PATH ON DISK should show the quadlet file path"
182+
183+
# Test quadlet print
184+
run_podman quadlet print "$quadlet_name"
185+
assert "$output" == "$(<"$quadlet_file")" "print output matches quadlet file"
186+
187+
# Test quadlet rm
188+
run_podman quadlet rm "$quadlet_name"
189+
# Verify remove output contains the quadlet name
190+
assert "$output" =~ "$quadlet_name" "remove output should contain quadlet name"
191+
# Verify removal
192+
run_podman quadlet list
193+
assert "$output" !~ "$quadlet_name" "list should not contain removed container"
194+
}
157195

158196
@test "quadlet verb - install multiple files from directory and remove by app name" {
159197
# Create a directory for multiple quadlet files

0 commit comments

Comments
 (0)