From d16afe5bfedd91cfef0037e88c4feccca6b4ee8d Mon Sep 17 00:00:00 2001 From: Kavish Gour Date: Fri, 5 Dec 2025 10:19:45 +0400 Subject: [PATCH 1/2] Fix(quadlet): Deduplicate removal files for .app expansion (#27653) Refactor removelist into sets Add quadlet rm --all test with safename add an integration test for quadlet rm --all(skip for remote clients) Signed-off-by: Kavish Gour --- pkg/domain/infra/abi/quadlet.go | 97 +++++++++---- test/apiv2/36-quadlets.at | 247 +++++++++++++++++++++++--------- test/e2e/quadlet_test.go | 47 ++++++ 3 files changed, 294 insertions(+), 97 deletions(-) diff --git a/pkg/domain/infra/abi/quadlet.go b/pkg/domain/infra/abi/quadlet.go index 4446ebaa090..cf12f1e643f 100644 --- a/pkg/domain/infra/abi/quadlet.go +++ b/pkg/domain/infra/abi/quadlet.go @@ -835,12 +835,18 @@ func (ic *ContainerEngine) QuadletRemove(ctx context.Context, quadlets []string, Errors: make(map[string]error), Removed: []string{}, } - removeList := []string{} - reverseMap, appMap, err := buildAppMap(systemdquadlet.GetInstallUnitDirPath(rootless.IsRootless())) + + installDir := systemdquadlet.GetInstallUnitDirPath(rootless.IsRootless()) + + reverseMap, appMap, err := buildAppMap(installDir) if err != nil { return nil, fmt.Errorf("unable to build app map: %w", err) } - expandQuadletList := []string{} + + // Use sets for deduplication + removeSet := make(map[string]struct{}) + expandQuadletSet := make(map[string]struct{}) + // Process all `.app` files in arguments, if `.app` file // is found then expand it to its respective quadlet files // and remove it from the processing list. @@ -852,32 +858,35 @@ func (ic *ContainerEngine) QuadletRemove(ctx context.Context, quadlets []string, if ok { for _, file := range files { if !systemdquadlet.IsExtSupported(file) { - removeList = append(removeList, file) + removeSet[file] = struct{}{} } else { - expandQuadletList = append(expandQuadletList, file) + expandQuadletSet[file] = struct{}{} } } } // also add .app file itself to the remove list so it can // be cleaned after removal of all components in the list - if !slices.Contains(removeList, quadlet) { - removeList = append(removeList, quadlet) - } + removeSet[quadlet] = struct{}{} } else { - expandQuadletList = append(expandQuadletList, quadlet) + expandQuadletSet[quadlet] = struct{}{} } } - quadlets = expandQuadletList + + if len(quadlets) == 0 && !options.All { + return nil, errors.New("must provide at least 1 quadlet to remove") + } + // Convert expandQuadletSet to slice + quadlets = make([]string, 0, len(expandQuadletSet)) + for quadlet := range expandQuadletSet { + quadlets = append(quadlets, quadlet) + } + allQuadletPaths := make([]string, 0, len(quadlets)) allServiceNames := make([]string, 0, len(quadlets)) runningQuadlets := make([]string, 0, len(quadlets)) serviceNameToQuadletName := make(map[string]string) needReload := options.ReloadSystemd - if len(quadlets) == 0 && !options.All { - return nil, errors.New("must provide at least 1 quadlet to remove") - } - // Is systemd available to the current user? // We cannot proceed if not. conn, err := systemd.ConnectToDBUS() @@ -888,7 +897,12 @@ func (ic *ContainerEngine) QuadletRemove(ctx context.Context, quadlets []string, if options.All { allQuadlets := getAllQuadletPaths() - quadlets = allQuadlets + for _, quadlet := range allQuadlets { + if _, exists := expandQuadletSet[quadlet]; !exists { + quadlets = append(quadlets, quadlet) + expandQuadletSet[quadlet] = struct{}{} + } + } } // We are using index wise iteration here instead of `range` @@ -913,30 +927,43 @@ func (ic *ContainerEngine) QuadletRemove(ctx context.Context, quadlets []string, } continue } - value, ok := reverseMap[quadlet] + // Use base filename for reverseMap lookup since map keys are filenames, not full paths + quadletBaseName := filepath.Base(quadlet) + value, ok := reverseMap[quadletBaseName] if ok { // If this is part of app and we are cleaning entire .app // make sure to add .app file itself to the removal list // if it does not already exists. - if !slices.Contains(removeList, value) { - removeList = append(removeList, value) - } - appFilePath := filepath.Join(systemdquadlet.GetInstallUnitDirPath(rootless.IsRootless()), value) + removeSet[value] = struct{}{} + appFilePath := filepath.Join(installDir, value) filesToRemove, err := getAssetListFromFile(appFilePath) if err != nil { return nil, fmt.Errorf("unable to get list of files to remove: %w", err) } for _, entry := range filesToRemove { if !systemdquadlet.IsExtSupported(entry) { - removeList = append(removeList, entry) - if !slices.Contains(removeList, value) { - // In the last also clean ..app file - removeList = append(removeList, value) - } + removeSet[entry] = struct{}{} continue } - if !slices.Contains(quadlets, entry) { - quadlets = append(quadlets, entry) + var entryToAdd string + // Note: We treat --all and specific arguments (e.g. foo.container) + // as mutually exclusive here. The loop that runs to expand + // .app, adds filenames to expandQuadletSet. While the + // options.All uses getAllQuadletPaths() to add full paths. + // The dedup check won't detect these refer to the same file, + // so a quadlet could be processed twice. + // Given this is low-risk, --all is highly unlikely to used + // with explicit .app arguments such as: + // 'podman quadlet rm --all --force foo.app' + // Documenting the behavior is preferred over normalizing all entries to full paths. + if options.All { + entryToAdd = filepath.Join(installDir, entry) + } else { + entryToAdd = entry + } + if _, exists := expandQuadletSet[entryToAdd]; !exists { + quadlets = append(quadlets, entryToAdd) + expandQuadletSet[entryToAdd] = struct{}{} } } } @@ -1012,13 +1039,23 @@ func (ic *ContainerEngine) QuadletRemove(ctx context.Context, quadlets []string, continue } } - for _, entry := range removeList { - os.Remove(filepath.Join(systemdquadlet.GetInstallUnitDirPath(rootless.IsRootless()), entry)) - } report.Removed = append(report.Removed, quadletName) } } + // Remove .app and .asset files after the main quadlet removal loop + // This ensures they are cleaned up properly since they are not included in allQuadletPaths + for entry := range removeSet { + entryPath := filepath.Join(installDir, entry) + if err := os.Remove(entryPath); err != nil { + if !errors.Is(err, fs.ErrNotExist) { + logrus.Warnf("Failed to remove metadata file %s: %v", entry, err) + } + } else { + logrus.Debugf("Removed metadata file %s", entry) + } + } + // Reload systemd, if necessary/requested. if needReload { if err := conn.ReloadContext(ctx); err != nil { diff --git a/test/apiv2/36-quadlets.at b/test/apiv2/36-quadlets.at index 90646deda5d..b008207164d 100644 --- a/test/apiv2/36-quadlets.at +++ b/test/apiv2/36-quadlets.at @@ -6,20 +6,19 @@ # NOTE: Once podman-remote quadlet support is added we can enable the podman quadlet tests in # test/system/253-podman-quadlet.bats which should cover it in more detail then. - function is_rootless() { - [ "$(id -u)" -ne 0 ] + [ "$(id -u)" -ne 0 ] } function get_quadlet_install_dir() { - if is_rootless; then - # For rootless: $XDG_CONFIG_HOME/containers/systemd or ~/.config/containers/systemd - local config_home=${XDG_CONFIG_HOME:-$HOME/.config} - echo "$config_home/containers/systemd" - else - # For root: /etc/containers/systemd - echo "/etc/containers/systemd" - fi + if is_rootless; then + # For rootless: $XDG_CONFIG_HOME/containers/systemd or ~/.config/containers/systemd + local config_home=${XDG_CONFIG_HOME:-$HOME/.config} + echo "$config_home/containers/systemd" + else + # For root: /etc/containers/systemd + echo "/etc/containers/systemd" + fi } quadlet_install_dir=$(get_quadlet_install_dir) @@ -47,21 +46,23 @@ quadlet_build_name="$quadlet_name.build" TMPD=$(mktemp -d podman-apiv2-test.quadlet.XXXXXXXX) -quadlet_container_file_content=$(cat << EOF +quadlet_container_file_content=$( + cat < $TMPD/$quadlet_container_name -echo "$quadlet_build_file_content" > $TMPD/$quadlet_build_name +echo "$quadlet_container_file_content" >$TMPD/$quadlet_container_name +echo "$quadlet_build_file_content" >$TMPD/$quadlet_build_name # this should ensure the .config/containers/systemd directory is created podman quadlet install $TMPD/$quadlet_container_name @@ -69,9 +70,9 @@ podman quadlet install $TMPD/$quadlet_build_name filter_param=$(printf '{"name":["%s"]}' "$quadlet_name") t GET "libpod/quadlets/json?filters=$filter_param" 200 \ - length=2 \ - .[0].Name="$quadlet_build_name" \ - .[1].Name="$quadlet_container_name" + length=2 \ + .[0].Name="$quadlet_build_name" \ + .[1].Name="$quadlet_container_name" filter_param=$(printf '{"name":["%s"]}' "$quadlet_container_name") t GET "libpod/quadlets/json?filters=$filter_param" 200 \ @@ -116,44 +117,44 @@ t GET "libpod/quadlets/$quadlet_name/exists" 500 podman quadlet rm $quadlet_podfilter_ctr $quadlet_podfilter_pod $quadlet_container_name $quadlet_build_name rm -rf $TMPD - TMPD=$(mktemp -d podman-apiv2-test.quadlets.XXXXXXXX) # Scenario: try to send nothing t POST "libpod/quadlets" 400 \ - .cause~.*'Content-Type: application/json is not supported. Should be "application/x-tar"' + .cause~.*'Content-Type: application/json is not supported. Should be "application/x-tar"' # Scenario: send an empty tar archive will fail with no files found in request -tar -C "$TMPD" -cvf "$TMPD/empty.tar" -T /dev/null &> /dev/null +tar -C "$TMPD" -cvf "$TMPD/empty.tar" -T /dev/null &>/dev/null t POST "libpod/quadlets" "$TMPD/empty.tar" 400 \ - .cause="no files found in request" + .cause="no files found in request" # Scenario: send a plaintext file will fail with no quadlet files found in request -echo "test" > "$TMPD/test.txt" +echo "test" >"$TMPD/test.txt" t POST "libpod/quadlets" --form="test.txt=@$TMPD/test.txt" 400 \ - .cause="no quadlet files found in request" + .cause="no quadlet files found in request" # Scenario: send an invalid quadlet type in a tar archive will fail with no quadlet files found in request -echo "test" > "$TMPD/test.txt" -tar -C "$TMPD" -cvf "$TMPD/test.tar" "test.txt" &> /dev/null +echo "test" >"$TMPD/test.txt" +tar -C "$TMPD" -cvf "$TMPD/test.tar" "test.txt" &>/dev/null t POST "libpod/quadlets" "$TMPD/test.tar" 400 \ - .cause="no quadlet files found in request" + .cause="no quadlet files found in request" # Scenario 1: install a single quadlet quadlet_1=quadlet-test-1-$(cat /proc/sys/kernel/random/uuid).container -quadlet_1_content=$(cat << EOF +quadlet_1_content=$( + cat < "$TMPD/$quadlet_1" -tar --format=posix -C "$TMPD" -cvf "$TMPD/$quadlet_1.tar" "$quadlet_1" &> /dev/null +echo "$quadlet_1_content" >"$TMPD/$quadlet_1" +tar --format=posix -C "$TMPD" -cvf "$TMPD/$quadlet_1.tar" "$quadlet_1" &>/dev/null t POST "libpod/quadlets" "$TMPD/$quadlet_1.tar" 200 \ - '.InstalledQuadlets|length=1' \ - '.QuadletErrors|length=0' + '.InstalledQuadlets|length=1' \ + '.QuadletErrors|length=0' t GET "libpod/quadlets/$quadlet_1/file" 200 is "$output" "$quadlet_1_content" "quadlet-1 should be installed" @@ -161,26 +162,27 @@ is "$output" "$quadlet_1_content" "quadlet-1 should be installed" # Scenario: install a quadlet that already exists, verify it won't be overwritten # then use replace=true to overwrite it and verify quadlet_2=$quadlet_1 -quadlet_2_content=$(cat << EOF +quadlet_2_content=$( + cat < "$TMPD/$quadlet_2" -tar --format=posix -C "$TMPD" -cvf "$TMPD/$quadlet_2.tar" "$quadlet_2" &> /dev/null +echo "$quadlet_2_content" >"$TMPD/$quadlet_2" +tar --format=posix -C "$TMPD" -cvf "$TMPD/$quadlet_2.tar" "$quadlet_2" &>/dev/null t POST "libpod/quadlets" "$TMPD/$quadlet_2.tar" 400 \ - .cause~.*"a Quadlet with name $quadlet_2 already exists, refusing to overwrite" + .cause~.*"a Quadlet with name $quadlet_2 already exists, refusing to overwrite" t GET "libpod/quadlets/$quadlet_1/file" 200 is "$output" "$quadlet_1_content" "quadlet-1 should not be overwritten" #replace t POST "libpod/quadlets?replace=true" "$TMPD/$quadlet_2.tar" 200 \ - '.InstalledQuadlets|length=1' \ - '.QuadletErrors|length=0' + '.InstalledQuadlets|length=1' \ + '.QuadletErrors|length=0' t GET "libpod/quadlets/$quadlet_2/file" 200 is "$output" "$quadlet_2_content" "quadlet-1 should be overwritten by quadlet-2" @@ -189,73 +191,79 @@ is "$output" "$quadlet_2_content" "quadlet-1 should be overwritten by quadlet-2" quadlet_3=quadlet-test-3-$(cat /proc/sys/kernel/random/uuid).container quadlet_4=quadlet-test-4-$(cat /proc/sys/kernel/random/uuid).container -quadlet_3_content=$(cat << EOF +quadlet_3_content=$( + cat < "$TMPD/$quadlet_3" -echo "$quadlet_4_content" > "$TMPD/$quadlet_4" -tar --format=posix -C "$TMPD" -cvf "$TMPD/$quadlet_3_4.tar" "$quadlet_3" "$quadlet_4" &> /dev/null +echo "$quadlet_3_content" >"$TMPD/$quadlet_3" +echo "$quadlet_4_content" >"$TMPD/$quadlet_4" +tar --format=posix -C "$TMPD" -cvf "$TMPD/$quadlet_3_4.tar" "$quadlet_3" "$quadlet_4" &>/dev/null t POST "libpod/quadlets" "$TMPD/$quadlet_3_4.tar" 400 \ - .cause="only a single quadlet file is allowed per request" + .cause="only a single quadlet file is allowed per request" # Scenario: install tar that contains one quadlet file and a non-quadlet file will succeed # then update the quadlet file, and the non-quadlet file, and verify the update is successful quadlet_5=quadlet-test-5-$(cat /proc/sys/kernel/random/uuid).container containerfile_1=quadlet-test-containerfile-1-$(cat /proc/sys/kernel/random/uuid).Containerfile -containerfile_1_content=$(cat << EOF +containerfile_1_content=$( + cat < "$TMPD/$quadlet_5" -echo "$containerfile_1_content" > "$TMPD/$containerfile_1" -tar --format=posix -C "$TMPD" -cvf "$TMPD/$quadlet_5$containerfile_1.tar" "$quadlet_5" "$containerfile_1" &> /dev/null +echo "$quadlet_5_content" >"$TMPD/$quadlet_5" +echo "$containerfile_1_content" >"$TMPD/$containerfile_1" +tar --format=posix -C "$TMPD" -cvf "$TMPD/$quadlet_5$containerfile_1.tar" "$quadlet_5" "$containerfile_1" &>/dev/null t POST "libpod/quadlets" "$TMPD/$quadlet_5$containerfile_1.tar" 200 \ - '.InstalledQuadlets|length=2' \ - '.QuadletErrors|length=0' + '.InstalledQuadlets|length=2' \ + '.QuadletErrors|length=0' t GET "libpod/quadlets/$quadlet_5/file" 200 is "$output" "$quadlet_5_content" "quadlet-5 should be installed" is "$(cat "$quadlet_install_dir/$containerfile_1")" "$containerfile_1_content" "containerfile_1 should be installed" -echo "$quadlet_5_updated_content" > "$TMPD/$quadlet_5" -echo "$containerfile_1_updated_content" > "$TMPD/$containerfile_1" -tar --format=posix -C "$TMPD" -cvf "$TMPD/$quadlet_5$containerfile_1.tar" "$quadlet_5" "$containerfile_1" &> /dev/null +echo "$quadlet_5_updated_content" >"$TMPD/$quadlet_5" +echo "$containerfile_1_updated_content" >"$TMPD/$containerfile_1" +tar --format=posix -C "$TMPD" -cvf "$TMPD/$quadlet_5$containerfile_1.tar" "$quadlet_5" "$containerfile_1" &>/dev/null # update with no replace and check nothing changed t POST "libpod/quadlets" "$TMPD/$quadlet_5$containerfile_1.tar" 400 @@ -266,8 +274,8 @@ is "$(cat "$quadlet_install_dir/$containerfile_1")" "$containerfile_1_content" " # replace t POST "libpod/quadlets?replace=true" "$TMPD/$quadlet_5$containerfile_1.tar" 200 \ - '.InstalledQuadlets|length=2' \ - '.QuadletErrors|length=0' + '.InstalledQuadlets|length=2' \ + '.QuadletErrors|length=0' t GET "libpod/quadlets/$quadlet_5/file" 200 is "$output" "$quadlet_5_updated_content" "quadlet-5 should be updated" @@ -277,34 +285,38 @@ is "$(cat "$quadlet_install_dir/$containerfile_1")" "$containerfile_1_updated_co quadlet_6=quadlet-test-6-$(cat /proc/sys/kernel/random/uuid).container containerfile_2=quadlet-test-containerfile-2-$(cat /proc/sys/kernel/random/uuid).Containerfile -quadlet_6_content=$(cat << EOF +quadlet_6_content=$( + cat < "$TMPD/$quadlet_6" -echo "$containerfile_2_content" > "$TMPD/$containerfile_2" +echo "$quadlet_6_content" >"$TMPD/$quadlet_6" +echo "$containerfile_2_content" >"$TMPD/$containerfile_2" t POST "libpod/quadlets" --form="quadlet_6=@$TMPD/$quadlet_6" --form="containerfile_2=@$TMPD/$containerfile_2" 200 @@ -313,8 +325,8 @@ is "$output" "$quadlet_6_content" "quadlet-6 should be installed" is "$(cat "$quadlet_install_dir/$containerfile_2")" "$containerfile_2_content" "containerfile_2 should be installed" # update with no replace and check nothing changed -echo "$quadlet_6_updated_content" > "$TMPD/$quadlet_6" -echo "$containerfile_2_updated_content" > "$TMPD/$containerfile_2" +echo "$quadlet_6_updated_content" >"$TMPD/$quadlet_6" +echo "$containerfile_2_updated_content" >"$TMPD/$containerfile_2" t POST "libpod/quadlets" --form="quadlet_6=@$TMPD/$quadlet_6" --form="containerfile_2=@$TMPD/$containerfile_2" 400 t GET "libpod/quadlets/$quadlet_6/file" 200 @@ -331,8 +343,8 @@ is "$(cat "$quadlet_install_dir/$containerfile_2")" "$containerfile_2_updated_co # clean up podman quadlet rm "$quadlet_1" \ - "$quadlet_5" \ - "$quadlet_6" + "$quadlet_5" \ + "$quadlet_6" rm -f "$quadlet_install_dir/$containerfile_1" rm -f "$quadlet_install_dir/$containerfile_2" @@ -405,4 +417,105 @@ rm -rf $TMPDIR # bunch of asset files might be left behind so we might need to clean them up find $quadlet_install_dir -type f -regextype posix-extended -regex '.*quadlet-test.*-[0-9a-f]{8}\b-[0-9a-f]{4}\b-[0-9a-f]{4}\b-[0-9a-f]{4}\b-[0-9a-f]{12}.*asset$' -delete; +# podman quadlet rm -all --force : Testing removal of all quadlets at once +# +# Install quadlets(.container, .network, .volume) + +quadlet_name=quadlet-remove-all-test-$(cat /proc/sys/kernel/random/uuid) + +quadlet_container="$quadlet_name.container" + +quadlet_network="$quadlet_name.network" + +quadlet_volume="$quadlet_name.volume" + +TMPDIR=$(mktemp -d podman-apiv2-test.quadlet.remove.all.XXXXXXXX) + +quadlet_container_file_content=$( + cat <$TMPDIR/$quadlet_container +echo "$quadlet_network_file_content" >$TMPDIR/$quadlet_network +echo "$quadlet_volume_file_content" >$TMPDIR/$quadlet_volume + +# Install all quadlets +podman quadlet install $TMPDIR + +# Verify all quadlets exist via API (file endpoint) + +t GET libpod/quadlets/$quadlet_container/file 200 +is "$output" "$quadlet_container_file_content" + +t GET libpod/quadlets/$quadlet_network/file 200 +is "$output" "$quadlet_network_file_content" + +t GET libpod/quadlets/$quadlet_volume/file 200 +is "$output" "$quadlet_volume_file_content" + +# Verify all quadlets appear in the list using filters + +filter_param=$(printf '{"name":["%s"]}' "$quadlet_container") +t GET "libpod/quadlets/json?filters=$filter_param" 200 \ + length=1 \ + .[0].Name="$quadlet_container" + +filter_param=$(printf '{"name":["%s"]}' "$quadlet_network") +t GET "libpod/quadlets/json?filters=$filter_param" 200 \ + length=1 \ + .[0].Name="$quadlet_network" + +filter_param=$(printf '{"name":["%s"]}' "$quadlet_volume") +t GET "libpod/quadlets/json?filters=$filter_param" 200 \ + length=1 \ + .[0].Name="$quadlet_volume" + +# Remove ALL quadlets using CLI +podman quadlet rm -af + +# Verify all quadlets are gone via API (should return 404) + +t GET libpod/quadlets/$quadlet_container/file 404 +t GET libpod/quadlets/$quadlet_network/file 404 +t GET libpod/quadlets/$quadlet_volume/file 404 + +# Verify all quadlets no longer appear in filtered lists + +filter_param=$(printf '{"name":["%s"]}' "$quadlet_container") +t GET "libpod/quadlets/json?filters=$filter_param" 200 \ + length=0 + +filter_param=$(printf '{"name":["%s"]}' "$quadlet_network") +t GET "libpod/quadlets/json?filters=$filter_param" 200 \ + length=0 + +filter_param=$(printf '{"name":["%s"]}' "$quadlet_volume") +t GET "libpod/quadlets/json?filters=$filter_param" 200 \ + length=0 + +# Verify the complete list is now empty + +t GET libpod/quadlets/json 200 \ + length=0 + +# Cleanup +rm -rf $TMPDIR + # vim: filetype=sh diff --git a/test/e2e/quadlet_test.go b/test/e2e/quadlet_test.go index 877d0f7e332..d6924dc73a1 100644 --- a/test/e2e/quadlet_test.go +++ b/test/e2e/quadlet_test.go @@ -13,7 +13,9 @@ import ( "strings" "github.com/mattn/go-shellwords" + "go.podman.io/podman/v6/pkg/rootless" "go.podman.io/podman/v6/pkg/systemd/parser" + systemdquadlet "go.podman.io/podman/v6/pkg/systemd/quadlet" . "go.podman.io/podman/v6/test/utils" "go.podman.io/podman/v6/version" @@ -1371,4 +1373,49 @@ BOGUS=foo }, ), ) + Describe("Running quadlet force remove all", func() { + It("Should remove all quadlets at once", func() { + SkipIfRemote("quadlet is not supported for remote clients") + SkipIfSystemdNotRunning("quadlet install requires a running systemd") + quadletName := fmt.Sprintf("quadlet-remove-all-test-%d", GinkgoRandomSeed()) + containerFile := quadletName + ".container" + networkFile := quadletName + ".network" + volumeFile := quadletName + ".volume" + + tmpDir := filepath.Join(podmanTest.TempDir, quadletName) + err := os.Mkdir(tmpDir, 0o755) + Expect(err).ToNot(HaveOccurred()) + + containerContent := []byte(fmt.Sprintf("[Container]\nImage=%s\n", ALPINE)) + networkContent := []byte("[Network]\nLabel=app=nginx-network\n") + volumeContent := []byte("[Volume]\nVolumeName=does_not_exist\n") + + Expect(os.WriteFile(filepath.Join(tmpDir, containerFile), containerContent, 0o644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(tmpDir, networkFile), networkContent, 0o644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(tmpDir, volumeFile), volumeContent, 0o644)).To(Succeed()) + + // Install quadlets + podmanTest.PodmanExitCleanly("quadlet", "install", tmpDir) + + // Verify quadlets appear in list + listSession := podmanTest.PodmanExitCleanly("quadlet", "list", "--format", "json") + output := listSession.OutputToString() + Expect(output).To(ContainSubstring(containerFile)) + Expect(output).To(ContainSubstring(networkFile)) + Expect(output).To(ContainSubstring(volumeFile)) + + // Remove all quadlets + podmanTest.PodmanExitCleanly("quadlet", "rm", "-af") + + // Verify if list is empty + listSession = podmanTest.PodmanExitCleanly("quadlet", "list", "--format", "json") + output = listSession.OutputToString() + Expect(output).To(Equal("[]")) + + // Verify if AppFile is removed + installDir := systemdquadlet.GetInstallUnitDirPath(rootless.IsRootless()) + appFile := "." + quadletName + ".app" + Expect(filepath.Join(installDir, appFile)).ToNot(BeAnExistingFile()) + }) + }) }) From 1ab78c4553f4cd1659f81cccdae26f2d5bedcd80 Mon Sep 17 00:00:00 2001 From: Kavish Gour Date: Wed, 1 Apr 2026 18:34:15 +0400 Subject: [PATCH 2/2] quadlet-rm: disallow combining --all flag with individual quadlets Ensure that the --all (-a) flag cannot be used simultaneously with specific quadlet arguments on the CLI. This prevents logical conflicts when a user provides both the --all flag and specific quadlets. - Add validation check in cmd/podman/quadlet/remove.go - Update the docs to document this behaviour with an example - Improve user feedback by explicitly stating why the command failed Signed-off-by: Kavish Gour --- cmd/podman/quadlet/remove.go | 3 +++ docs/source/markdown/podman-quadlet-rm.1.md | 2 ++ test/e2e/quadlet_test.go | 1 + 3 files changed, 6 insertions(+) diff --git a/cmd/podman/quadlet/remove.go b/cmd/podman/quadlet/remove.go index 27378817397..40d53dce36c 100644 --- a/cmd/podman/quadlet/remove.go +++ b/cmd/podman/quadlet/remove.go @@ -49,6 +49,9 @@ func rm(_ *cobra.Command, args []string) error { if len(args) < 1 && !removeOptions.All { return errors.New("at least one quadlet file must be selected") } + if len(args) > 0 && removeOptions.All { + return errors.New("-a or --all cannot be used when combined with individual quadlets") + } var errs utils.OutputErrors removeReport, err := registry.ContainerEngine().QuadletRemove(registry.Context(), args, removeOptions) if err != nil { diff --git a/docs/source/markdown/podman-quadlet-rm.1.md b/docs/source/markdown/podman-quadlet-rm.1.md index a9195f4da7d..495fcdc07f7 100644 --- a/docs/source/markdown/podman-quadlet-rm.1.md +++ b/docs/source/markdown/podman-quadlet-rm.1.md @@ -21,6 +21,8 @@ of a single application. Remove all Quadlets for the current user. +Note: The `--all` or `-a` flag cannot be used when combined with individual Quadlets in the same command (e.g. `podman quadlet rm --all foo.container`); doing so will result in an error. + #### **--force**, **-f** Remove running quadlets. diff --git a/test/e2e/quadlet_test.go b/test/e2e/quadlet_test.go index d6924dc73a1..cd04fcdd52b 100644 --- a/test/e2e/quadlet_test.go +++ b/test/e2e/quadlet_test.go @@ -1376,6 +1376,7 @@ BOGUS=foo Describe("Running quadlet force remove all", func() { It("Should remove all quadlets at once", func() { SkipIfRemote("quadlet is not supported for remote clients") + SkipIfInContainer("quadlet install requires a running systemd") SkipIfSystemdNotRunning("quadlet install requires a running systemd") quadletName := fmt.Sprintf("quadlet-remove-all-test-%d", GinkgoRandomSeed()) containerFile := quadletName + ".container"