Add podman system prune --external
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>
This commit is contained in:
		
							parent
							
								
									f1dbfda807
								
							
						
					
					
						commit
						93d2ec148c
					
				|  | @ -47,6 +47,7 @@ func init() { | ||||||
| 	flags := pruneCommand.Flags() | 	flags := pruneCommand.Flags() | ||||||
| 	flags.BoolVarP(&force, "force", "f", false, "Do not prompt for confirmation.  The default is false") | 	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.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") | 	flags.BoolVar(&pruneOptions.Volume, "volumes", false, "Prune volumes") | ||||||
| 	filterFlagName := "filter" | 	filterFlagName := "filter" | ||||||
| 	flags.StringArrayVar(&filters, filterFlagName, []string{}, "Provide filter values (e.g. 'label=<key>=<value>')") | 	flags.StringArrayVar(&filters, filterFlagName, []string{}, "Provide filter values (e.g. 'label=<key>=<value>')") | ||||||
|  | @ -55,8 +56,8 @@ func init() { | ||||||
| 
 | 
 | ||||||
| func prune(cmd *cobra.Command, args []string) error { | func prune(cmd *cobra.Command, args []string) error { | ||||||
| 	var err error | 	var err error | ||||||
| 	// Prompt for confirmation if --force is not set
 | 	// Prompt for confirmation if --force is not set, unless --external
 | ||||||
| 	if !force { | 	if !force && !pruneOptions.External { | ||||||
| 		reader := bufio.NewReader(os.Stdin) | 		reader := bufio.NewReader(os.Stdin) | ||||||
| 		volumeString := "" | 		volumeString := "" | ||||||
| 		if pruneOptions.Volume { | 		if pruneOptions.Volume { | ||||||
|  | @ -113,7 +114,9 @@ func prune(cmd *cobra.Command, args []string) error { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	if !pruneOptions.External { | ||||||
| 		fmt.Printf("Total reclaimed space: %s\n", units.HumanSize((float64)(response.ReclaimedSpace))) | 		fmt.Printf("Total reclaimed space: %s\n", units.HumanSize((float64)(response.ReclaimedSpace))) | ||||||
|  | 	} | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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.) | 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* | #### **--filter**=*filters* | ||||||
| 
 | 
 | ||||||
| Provide filter values. | Provide filter values. | ||||||
|  |  | ||||||
|  | @ -959,6 +959,10 @@ func (r *Runtime) StorageConfig() storage.StoreOptions { | ||||||
| 	return r.storageConfig | 	return r.storageConfig | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (r *Runtime) GarbageCollect() error { | ||||||
|  | 	return r.store.GarbageCollect() | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // RunRoot retrieves the current c/storage temporary directory in use by Libpod.
 | // RunRoot retrieves the current c/storage temporary directory in use by Libpod.
 | ||||||
| func (r *Runtime) RunRoot() string { | func (r *Runtime) RunRoot() string { | ||||||
| 	if r.store == nil { | 	if r.store == nil { | ||||||
|  |  | ||||||
|  | @ -21,6 +21,7 @@ func SystemPrune(w http.ResponseWriter, r *http.Request) { | ||||||
| 	query := struct { | 	query := struct { | ||||||
| 		All      bool `schema:"all"` | 		All      bool `schema:"all"` | ||||||
| 		Volumes  bool `schema:"volumes"` | 		Volumes  bool `schema:"volumes"` | ||||||
|  | 		External bool `schema:"external"` | ||||||
| 	}{} | 	}{} | ||||||
| 
 | 
 | ||||||
| 	if err := decoder.Decode(&query, r.URL.Query()); err != nil { | 	if err := decoder.Decode(&query, r.URL.Query()); err != nil { | ||||||
|  | @ -41,6 +42,7 @@ func SystemPrune(w http.ResponseWriter, r *http.Request) { | ||||||
| 		All:      query.All, | 		All:      query.All, | ||||||
| 		Volume:   query.Volumes, | 		Volume:   query.Volumes, | ||||||
| 		Filters:  *filterMap, | 		Filters:  *filterMap, | ||||||
|  | 		External: query.External, | ||||||
| 	} | 	} | ||||||
| 	report, err := containerEngine.SystemPrune(r.Context(), pruneOptions) | 	report, err := containerEngine.SystemPrune(r.Context(), pruneOptions) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  |  | ||||||
|  | @ -17,6 +17,7 @@ type PruneOptions struct { | ||||||
| 	All      *bool | 	All      *bool | ||||||
| 	Filters  map[string][]string | 	Filters  map[string][]string | ||||||
| 	Volumes  *bool | 	Volumes  *bool | ||||||
|  | 	External *bool | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // VersionOptions are optional options for getting version info
 | // VersionOptions are optional options for getting version info
 | ||||||
|  |  | ||||||
|  | @ -61,3 +61,18 @@ func (o *PruneOptions) GetVolumes() bool { | ||||||
| 	} | 	} | ||||||
| 	return *o.Volumes | 	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 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -21,6 +21,7 @@ type SystemPruneOptions struct { | ||||||
| 	All      bool | 	All      bool | ||||||
| 	Volume   bool | 	Volume   bool | ||||||
| 	Filters  map[string][]string `json:"filters" schema:"filters"` | 	Filters  map[string][]string `json:"filters" schema:"filters"` | ||||||
|  | 	External bool | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // SystemPruneReport provides report after system prune is executed.
 | // SystemPruneReport provides report after system prune is executed.
 | ||||||
|  |  | ||||||
|  | @ -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.
 | // 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) { | func (ic *ContainerEngine) SystemPrune(ctx context.Context, options entities.SystemPruneOptions) (*entities.SystemPruneReport, error) { | ||||||
| 	var systemPruneReport = new(entities.SystemPruneReport) | 	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{} | 	filters := []string{} | ||||||
| 	for k, v := range options.Filters { | 	for k, v := range options.Filters { | ||||||
| 		filters = append(filters, fmt.Sprintf("%s=%s", k, v[0])) | 		filters = append(filters, fmt.Sprintf("%s=%s", k, v[0])) | ||||||
|  |  | ||||||
|  | @ -19,7 +19,7 @@ func (ic *ContainerEngine) SetupRootless(_ context.Context, noMoveProcess bool) | ||||||
| 
 | 
 | ||||||
| // SystemPrune prunes unused data from the system.
 | // SystemPrune prunes unused data from the system.
 | ||||||
| func (ic *ContainerEngine) SystemPrune(ctx context.Context, opts entities.SystemPruneOptions) (*entities.SystemPruneReport, error) { | 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) | 	return system.Prune(ic.ClientCtx, options) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ package integration | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os" | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
| 
 | 
 | ||||||
| 	. "github.com/containers/podman/v4/test/utils" | 	. "github.com/containers/podman/v4/test/utils" | ||||||
| 	. "github.com/onsi/ginkgo" | 	. "github.com/onsi/ginkgo" | ||||||
|  | @ -522,4 +523,87 @@ var _ = Describe("Podman prune", func() { | ||||||
| 
 | 
 | ||||||
| 		podmanTest.Cleanup() | 		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)) | ||||||
|  | 	}) | ||||||
| }) | }) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue