diff --git a/cmd/podman/images/prune.go b/cmd/podman/images/prune.go index 0474aefd14..1c204448d3 100644 --- a/cmd/podman/images/prune.go +++ b/cmd/podman/images/prune.go @@ -46,6 +46,7 @@ func init() { flags := pruneCmd.Flags() flags.BoolVarP(&pruneOpts.All, "all", "a", false, "Remove all images not in use by containers, not just dangling ones") + flags.BoolVarP(&pruneOpts.BuildCache, "build-cache", "", false, "Remove persistent build cache created by --mount=type=cache") flags.BoolVarP(&pruneOpts.External, "external", "", false, "Remove images even when they are used by external containers (e.g., by build containers)") flags.BoolVarP(&force, "force", "f", false, "Do not prompt for confirmation") diff --git a/docs/source/markdown/podman-image-prune.1.md b/docs/source/markdown/podman-image-prune.1.md index b0f895d771..284de5e80d 100644 --- a/docs/source/markdown/podman-image-prune.1.md +++ b/docs/source/markdown/podman-image-prune.1.md @@ -17,6 +17,10 @@ The image prune command does not prune cache images that only use layers that ar Remove dangling images and images that have no associated containers. +#### **--build-cache** + +Remove persistent build cache create for `--mount=type=cache`. + #### **--external** Remove images even when they are used by external containers (e.g., build containers). diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go index 91d2b7377a..26b899f002 100644 --- a/pkg/api/handlers/libpod/images.go +++ b/pkg/api/handlers/libpod/images.go @@ -118,8 +118,9 @@ func PruneImages(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) query := struct { - All bool `schema:"all"` - External bool `schema:"external"` + All bool `schema:"all"` + External bool `schema:"external"` + BuildCache bool `schema:"buildcache"` }{ // override any golang type defaults } @@ -157,9 +158,10 @@ func PruneImages(w http.ResponseWriter, r *http.Request) { imageEngine := abi.ImageEngine{Libpod: runtime} pruneOptions := entities.ImagePruneOptions{ - All: query.All, - External: query.External, - Filter: libpodFilters, + All: query.All, + External: query.External, + Filter: libpodFilters, + BuildCache: query.BuildCache, } imagePruneReports, err := imageEngine.Prune(r.Context(), pruneOptions) if err != nil { diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go index fa01634b5c..1486da78bd 100644 --- a/pkg/api/server/register_images.go +++ b/pkg/api/server/register_images.go @@ -1129,6 +1129,12 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // description: | // Remove images even when they are used by external containers (e.g, by build containers) // - in: query + // name: buildcache + // default: false + // type: boolean + // description: | + // Remove persistent build cache created by build instructions such as `--mount=type=cache`. + // - in: query // name: filters // type: string // description: | diff --git a/pkg/bindings/images/types.go b/pkg/bindings/images/types.go index 7621f8cd7d..0db6d6f027 100644 --- a/pkg/bindings/images/types.go +++ b/pkg/bindings/images/types.go @@ -93,6 +93,8 @@ type PruneOptions struct { All *bool // Prune images even when they're used by external containers External *bool + // Prune persistent build cache + BuildCache *bool // Filters to apply when pruning images Filters map[string][]string } diff --git a/pkg/bindings/images/types_prune_options.go b/pkg/bindings/images/types_prune_options.go index ead74e7ea2..bbd12a1693 100644 --- a/pkg/bindings/images/types_prune_options.go +++ b/pkg/bindings/images/types_prune_options.go @@ -47,6 +47,21 @@ func (o *PruneOptions) GetExternal() bool { return *o.External } +// WithBuildCache set field BuildCache to given value +func (o *PruneOptions) WithBuildCache(value bool) *PruneOptions { + o.BuildCache = &value + return o +} + +// GetBuildCache returns value of field BuildCache +func (o *PruneOptions) GetBuildCache() bool { + if o.BuildCache == nil { + var z bool + return z + } + return *o.BuildCache +} + // WithFilters set field Filters to given value func (o *PruneOptions) WithFilters(value map[string][]string) *PruneOptions { o.Filters = value diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go index 9b3a40eb71..4426ead84d 100644 --- a/pkg/domain/entities/images.go +++ b/pkg/domain/entities/images.go @@ -249,9 +249,10 @@ type ImageListOptions struct { } type ImagePruneOptions struct { - All bool `json:"all" schema:"all"` - External bool `json:"external" schema:"external"` - Filter []string `json:"filter" schema:"filter"` + All bool `json:"all" schema:"all"` + External bool `json:"external" schema:"external"` + BuildCache bool `json:"buildcache" schema:"buildcache"` + Filter []string `json:"filter" schema:"filter"` } type ImageTagOptions struct{} diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index 8f1e4b607f..454c48b353 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -19,6 +19,7 @@ import ( "time" bdefine "github.com/containers/buildah/define" + "github.com/containers/buildah/pkg/volumes" "github.com/containers/common/libimage" "github.com/containers/common/libimage/filter" "github.com/containers/common/pkg/config" @@ -107,6 +108,13 @@ func (ir *ImageEngine) Prune(ctx context.Context, opts entities.ImagePruneOption numPreviouslyRemovedImages = numRemovedImages } + if opts.BuildCache || opts.All { + // Clean build cache if any + if err := volumes.CleanCacheMount(); err != nil { + return nil, err + } + } + return pruneReports, nil } diff --git a/pkg/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go index 0fb142b549..5544d0f4ae 100644 --- a/pkg/domain/infra/tunnel/images.go +++ b/pkg/domain/infra/tunnel/images.go @@ -102,7 +102,7 @@ func (ir *ImageEngine) Prune(ctx context.Context, opts entities.ImagePruneOption f := strings.Split(filter, "=") filters[f[0]] = f[1:] } - options := new(images.PruneOptions).WithAll(opts.All).WithFilters(filters).WithExternal(opts.External) + options := new(images.PruneOptions).WithAll(opts.All).WithFilters(filters).WithExternal(opts.External).WithBuildCache(opts.BuildCache) reports, err := images.Prune(ir.ClientCtx, options) if err != nil { return nil, err diff --git a/test/e2e/build/buildkit-mount/Containerfilecacheread b/test/e2e/build/buildkit-mount/Containerfilecacheread new file mode 100644 index 0000000000..be123158c9 --- /dev/null +++ b/test/e2e/build/buildkit-mount/Containerfilecacheread @@ -0,0 +1,4 @@ +FROM alpine +RUN mkdir /test +# use option z if selinux is enabled +RUN --mount=type=cache,target=/test,z cat /test/world diff --git a/test/e2e/build/buildkit-mount/Containerfilecachewrite b/test/e2e/build/buildkit-mount/Containerfilecachewrite new file mode 100644 index 0000000000..67a65939ea --- /dev/null +++ b/test/e2e/build/buildkit-mount/Containerfilecachewrite @@ -0,0 +1,4 @@ +FROM alpine +RUN mkdir /test +# use option z if selinux is enabled +RUN --mount=type=cache,target=/test,z echo hello > /test/world diff --git a/test/e2e/build_test.go b/test/e2e/build_test.go index 20462d457a..38e29f7a74 100644 --- a/test/e2e/build_test.go +++ b/test/e2e/build_test.go @@ -41,6 +41,37 @@ var _ = Describe("Podman build", func() { Expect(session).Should(ExitCleanly()) }) + It("podman image prune should clean build cache", Serial, func() { + // try writing something to persistent cache + session := podmanTest.Podman([]string{"build", "-f", "build/buildkit-mount/Containerfilecachewrite"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(ExitCleanly()) + + // try reading something from persistent cache + session = podmanTest.Podman([]string{"build", "-f", "build/buildkit-mount/Containerfilecacheread"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(ExitCleanly()) + Expect(session.OutputToString()).To(ContainSubstring("hello")) + + // Prune build cache + session = podmanTest.Podman([]string{"image", "prune", "-f", "--build-cache"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(ExitCleanly()) + + expectedErr := "open '/test/world': No such file or directory" + // try reading something from persistent cache should fail + session = podmanTest.Podman([]string{"build", "-f", "build/buildkit-mount/Containerfilecacheread"}) + session.WaitWithDefaultTimeout() + if IsRemote() { + // In the case of podman remote the error from build is not being propogated to `stderr` instead it appears + // on the `stdout` this could be a potential bug in `remote build` which needs to be fixed and visited. + Expect(session.OutputToString()).To(ContainSubstring(expectedErr)) + Expect(session).Should(ExitWithError(1, "exit status 1")) + } else { + Expect(session).Should(ExitWithError(1, expectedErr)) + } + }) + It("podman build and remove basic alpine with TMPDIR as relative", func() { // preserve TMPDIR if it was originally set if cacheDir, found := os.LookupEnv("TMPDIR"); found {