Skip to content

Commit

Permalink
Add podman system prune --external
Browse files Browse the repository at this point in the history
This just calls GC on the local storage, which will remove any leftover
directories from previous containers that are not in the podman db anymore.
This is useful primarily for transient store mode, but can also help in
the case of an unclean shutdown.

Also adds some e2e test to ensure prune --external works.

Signed-off-by: Alexander Larsson <alexl@redhat.com>
  • Loading branch information
alexlarsson committed Dec 5, 2022
1 parent f1dbfda commit 93d2ec1
Show file tree
Hide file tree
Showing 10 changed files with 146 additions and 15 deletions.
9 changes: 6 additions & 3 deletions cmd/podman/system/prune.go
Original file line number Diff line number Diff line change
Expand Up @@ -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=<key>=<value>')")
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}

Expand Down
9 changes: 9 additions & 0 deletions docs/source/markdown/podman-system-prune.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions libpod/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
12 changes: 7 additions & 5 deletions pkg/api/handlers/libpod/system.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
7 changes: 4 additions & 3 deletions pkg/bindings/system/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions pkg/bindings/system/types_prune_options.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions pkg/domain/entities/system.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 12 additions & 0 deletions pkg/domain/infra/abi/system.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]))
Expand Down
2 changes: 1 addition & 1 deletion pkg/domain/infra/tunnel/system.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
84 changes: 84 additions & 0 deletions test/e2e/prune_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package integration
import (
"fmt"
"os"
"path/filepath"

. "github.com/containers/podman/v4/test/utils"
. "github.com/onsi/ginkgo"
Expand Down Expand Up @@ -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))
})
})

0 comments on commit 93d2ec1

Please sign in to comment.