diff --git a/cmd/podman/system/prune.go b/cmd/podman/system/prune.go index 1d6ba8155b..f7cf7b551c 100644 --- a/cmd/podman/system/prune.go +++ b/cmd/podman/system/prune.go @@ -47,6 +47,7 @@ func init() { flags := pruneCommand.Flags() flags.BoolVarP(&force, "force", "f", false, "Do not prompt for confirmation. The default is false") flags.BoolVarP(&pruneOptions.All, "all", "a", false, "Remove all unused data") + flags.BoolVar(&pruneOptions.External, "external", false, "Remove container data in storage not controlled by podman") flags.BoolVar(&pruneOptions.Volume, "volumes", false, "Prune volumes") filterFlagName := "filter" flags.StringArrayVar(&filters, filterFlagName, []string{}, "Provide filter values (e.g. 'label==')") @@ -55,8 +56,8 @@ func init() { func prune(cmd *cobra.Command, args []string) error { var err error - // Prompt for confirmation if --force is not set - if !force { + // Prompt for confirmation if --force is not set, unless --external + if !force && !pruneOptions.External { reader := bufio.NewReader(os.Stdin) volumeString := "" if pruneOptions.Volume { @@ -113,7 +114,9 @@ func prune(cmd *cobra.Command, args []string) error { return err } - fmt.Printf("Total reclaimed space: %s\n", units.HumanSize((float64)(response.ReclaimedSpace))) + if !pruneOptions.External { + fmt.Printf("Total reclaimed space: %s\n", units.HumanSize((float64)(response.ReclaimedSpace))) + } return nil } diff --git a/docs/source/markdown/podman-system-prune.1.md b/docs/source/markdown/podman-system-prune.1.md index cb6603791e..775b397fe2 100644 --- a/docs/source/markdown/podman-system-prune.1.md +++ b/docs/source/markdown/podman-system-prune.1.md @@ -18,6 +18,15 @@ By default, volumes are not removed to prevent important data from being deleted Recursively remove all unused pods, containers, images, networks, and volume data. (Maximum 50 iterations.) +#### **--external** + +Removes all leftover container storage files from local storage that are not managed by podman. In normal circumstances no such data should exist, but in case of an unclean shutdown the podman database may be corrupted and cause his. + +However, when using transient storage mode, the podman database does not persist. This means containers can will leave the writable layers on disk after a reboot. If you use transient store +it it recommended that you run **podman system prune --external** once some time after each boot. + +This option is incompatible with **--all** and **--filter** and drops the default behaviour of removing unused resources. + #### **--filter**=*filters* Provide filter values. diff --git a/libpod/runtime.go b/libpod/runtime.go index 46c755ebc2..8e2f6b3639 100644 --- a/libpod/runtime.go +++ b/libpod/runtime.go @@ -959,6 +959,10 @@ func (r *Runtime) StorageConfig() storage.StoreOptions { return r.storageConfig } +func (r *Runtime) GarbageCollect() error { + return r.store.GarbageCollect() +} + // RunRoot retrieves the current c/storage temporary directory in use by Libpod. func (r *Runtime) RunRoot() string { if r.store == nil { diff --git a/pkg/api/handlers/libpod/system.go b/pkg/api/handlers/libpod/system.go index 7418dc4dfb..70d4493f81 100644 --- a/pkg/api/handlers/libpod/system.go +++ b/pkg/api/handlers/libpod/system.go @@ -19,8 +19,9 @@ func SystemPrune(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) query := struct { - All bool `schema:"all"` - Volumes bool `schema:"volumes"` + All bool `schema:"all"` + Volumes bool `schema:"volumes"` + External bool `schema:"external"` }{} if err := decoder.Decode(&query, r.URL.Query()); err != nil { @@ -38,9 +39,10 @@ func SystemPrune(w http.ResponseWriter, r *http.Request) { containerEngine := abi.ContainerEngine{Libpod: runtime} pruneOptions := entities.SystemPruneOptions{ - All: query.All, - Volume: query.Volumes, - Filters: *filterMap, + All: query.All, + Volume: query.Volumes, + Filters: *filterMap, + External: query.External, } report, err := containerEngine.SystemPrune(r.Context(), pruneOptions) if err != nil { diff --git a/pkg/bindings/system/types.go b/pkg/bindings/system/types.go index 9efd280044..89e093f688 100644 --- a/pkg/bindings/system/types.go +++ b/pkg/bindings/system/types.go @@ -14,9 +14,10 @@ type EventsOptions struct { // //go:generate go run ../generator/generator.go PruneOptions type PruneOptions struct { - All *bool - Filters map[string][]string - Volumes *bool + All *bool + Filters map[string][]string + Volumes *bool + External *bool } // VersionOptions are optional options for getting version info diff --git a/pkg/bindings/system/types_prune_options.go b/pkg/bindings/system/types_prune_options.go index 47805febf8..d004985205 100644 --- a/pkg/bindings/system/types_prune_options.go +++ b/pkg/bindings/system/types_prune_options.go @@ -61,3 +61,18 @@ func (o *PruneOptions) GetVolumes() bool { } return *o.Volumes } + +// WithExternal set field External to given value +func (o *PruneOptions) WithExternal(value bool) *PruneOptions { + o.External = &value + return o +} + +// GetExternal returns value of field External +func (o *PruneOptions) GetExternal() bool { + if o.External == nil { + var z bool + return z + } + return *o.External +} diff --git a/pkg/domain/entities/system.go b/pkg/domain/entities/system.go index 51fbb90cb3..5d7ef92d74 100644 --- a/pkg/domain/entities/system.go +++ b/pkg/domain/entities/system.go @@ -18,9 +18,10 @@ type ServiceOptions struct { // SystemPruneOptions provides options to prune system. type SystemPruneOptions struct { - All bool - Volume bool - Filters map[string][]string `json:"filters" schema:"filters"` + All bool + Volume bool + Filters map[string][]string `json:"filters" schema:"filters"` + External bool } // SystemPruneReport provides report after system prune is executed. diff --git a/pkg/domain/infra/abi/system.go b/pkg/domain/infra/abi/system.go index 75693ae8f6..45731f4e07 100644 --- a/pkg/domain/infra/abi/system.go +++ b/pkg/domain/infra/abi/system.go @@ -160,6 +160,18 @@ func (ic *ContainerEngine) SetupRootless(_ context.Context, noMoveProcess bool) // SystemPrune removes unused data from the system. Pruning pods, containers, networks, volumes and images. func (ic *ContainerEngine) SystemPrune(ctx context.Context, options entities.SystemPruneOptions) (*entities.SystemPruneReport, error) { var systemPruneReport = new(entities.SystemPruneReport) + + if options.External { + if options.All || options.Volume || len(options.Filters) > 0 { + return nil, fmt.Errorf("system prune --external cannot be combined with other options") + } + err := ic.Libpod.GarbageCollect() + if err != nil { + return nil, err + } + return systemPruneReport, nil + } + filters := []string{} for k, v := range options.Filters { filters = append(filters, fmt.Sprintf("%s=%s", k, v[0])) diff --git a/pkg/domain/infra/tunnel/system.go b/pkg/domain/infra/tunnel/system.go index 4185134dcb..29737775d5 100644 --- a/pkg/domain/infra/tunnel/system.go +++ b/pkg/domain/infra/tunnel/system.go @@ -19,7 +19,7 @@ func (ic *ContainerEngine) SetupRootless(_ context.Context, noMoveProcess bool) // SystemPrune prunes unused data from the system. func (ic *ContainerEngine) SystemPrune(ctx context.Context, opts entities.SystemPruneOptions) (*entities.SystemPruneReport, error) { - options := new(system.PruneOptions).WithAll(opts.All).WithVolumes(opts.Volume).WithFilters(opts.Filters) + options := new(system.PruneOptions).WithAll(opts.All).WithVolumes(opts.Volume).WithFilters(opts.Filters).WithExternal(opts.External) return system.Prune(ic.ClientCtx, options) } diff --git a/test/e2e/prune_test.go b/test/e2e/prune_test.go index 2ae05307cc..fb7265f07d 100644 --- a/test/e2e/prune_test.go +++ b/test/e2e/prune_test.go @@ -3,6 +3,7 @@ package integration import ( "fmt" "os" + "path/filepath" . "github.com/containers/podman/v4/test/utils" . "github.com/onsi/ginkgo" @@ -522,4 +523,87 @@ var _ = Describe("Podman prune", func() { podmanTest.Cleanup() }) + + It("podman system prune --all --external fails", func() { + prune := podmanTest.Podman([]string{"system", "prune", "--all", "--enternal"}) + prune.WaitWithDefaultTimeout() + Expect(prune).Should(Exit(125)) + }) + + It("podman system prune --external leaves referenced containers", func() { + containerStorageDir := filepath.Join(podmanTest.Root, podmanTest.ImageCacheFS+"-containers") + + create := podmanTest.Podman([]string{"create", "--name", "test", BB}) + create.WaitWithDefaultTimeout() + Expect(create).Should(Exit(0)) + + // Container should exist + Expect(podmanTest.NumberOfContainers()).To(Equal(1)) + + // have: containers.json, containers.lock and container dir + dirents, err := os.ReadDir(containerStorageDir) + Expect(err).To(BeNil()) + Expect(dirents).To(HaveLen(3)) + + prune := podmanTest.Podman([]string{"system", "prune", "--external", "-f"}) + prune.WaitWithDefaultTimeout() + Expect(prune).Should(Exit(0)) + + // Container should still exist + Expect(podmanTest.NumberOfContainers()).To(Equal(1)) + + // still have: containers.json, containers.lock and container dir + dirents, err = os.ReadDir(containerStorageDir) + Expect(err).To(BeNil()) + Expect(dirents).To(HaveLen(3)) + + }) + + It("podman system prune --external removes unreferenced containers", func() { + SkipIfRemote("Can't drop database while daemon running") + + containerStorageDir := filepath.Join(podmanTest.Root, podmanTest.ImageCacheFS+"-containers") + dbDir := filepath.Join(podmanTest.Root, "libpod") + + // Create container 1 + create := podmanTest.Podman([]string{"create", "--name", "test", BB}) + create.WaitWithDefaultTimeout() + Expect(create).Should(Exit(0)) + + Expect(podmanTest.NumberOfContainers()).To(Equal(1)) + + // containers.json, containers.lock and container 1 dir + dirents, err := os.ReadDir(containerStorageDir) + Expect(err).To(BeNil()) + Expect(dirents).To(HaveLen(3)) + + // Drop podman database and storage, losing track of container 1 (but directory remains) + err = os.Remove(filepath.Join(containerStorageDir, "containers.json")) + Expect(err).To(BeNil()) + err = os.RemoveAll(dbDir) + Expect(err).To(BeNil()) + + Expect(podmanTest.NumberOfContainers()).To(Equal(0)) + + // Create container 2 + create = podmanTest.Podman([]string{"create", "--name", "test", BB}) + create.WaitWithDefaultTimeout() + Expect(create).Should(Exit(0)) + + Expect(podmanTest.NumberOfContainers()).To(Equal(1)) + + // containers.json, containers.lock and container 1&2 dir + dirents, err = os.ReadDir(containerStorageDir) + Expect(err).To(BeNil()) + Expect(dirents).To(HaveLen(4)) + + prune := podmanTest.Podman([]string{"system", "prune", "--external", "-f"}) + prune.WaitWithDefaultTimeout() + Expect(prune).Should(Exit(0)) + + // container 1 dir should be gone now + dirents, err = os.ReadDir(containerStorageDir) + Expect(err).To(BeNil()) + Expect(dirents).To(HaveLen(3)) + }) })