Skip to content

Commit 8d65d1e

Browse files
Merge pull request #25102 from Honny1/prune
Clean up after unexpectedly terminated build
2 parents 9403c3d + 81eb84f commit 8d65d1e

File tree

10 files changed

+158
-11
lines changed

10 files changed

+158
-11
lines changed

cmd/podman/system/prune.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ func init() {
4848
flags.BoolVarP(&force, "force", "f", false, "Do not prompt for confirmation. The default is false")
4949
flags.BoolVarP(&pruneOptions.All, "all", "a", false, "Remove all unused data")
5050
flags.BoolVar(&pruneOptions.External, "external", false, "Remove container data in storage not controlled by podman")
51+
flags.BoolVar(&pruneOptions.Build, "build", false, "Remove build containers")
5152
flags.BoolVar(&pruneOptions.Volume, "volumes", false, "Prune volumes")
5253
filterFlagName := "filter"
5354
flags.StringArrayVar(&filters, filterFlagName, []string{}, "Provide filter values (e.g. 'label=<key>=<value>')")
@@ -64,8 +65,12 @@ func prune(cmd *cobra.Command, args []string) error {
6465
volumeString = `
6566
- all volumes not used by at least one container`
6667
}
67-
68-
fmt.Printf(createPruneWarningMessage(pruneOptions), volumeString, "Are you sure you want to continue? [y/N] ")
68+
buildString := ""
69+
if pruneOptions.Build {
70+
buildString = `
71+
- all build containers`
72+
}
73+
fmt.Printf(createPruneWarningMessage(pruneOptions), volumeString, buildString, "Are you sure you want to continue? [y/N] ")
6974

7075
answer, err := reader.ReadString('\n')
7176
if err != nil {
@@ -124,15 +129,15 @@ func createPruneWarningMessage(pruneOpts entities.SystemPruneOptions) string {
124129
if pruneOpts.All {
125130
return `WARNING! This command removes:
126131
- all stopped containers
127-
- all networks not used by at least one container%s
132+
- all networks not used by at least one container%s%s
128133
- all images without at least one container associated with them
129134
- all build cache
130135
131136
%s`
132137
}
133138
return `WARNING! This command removes:
134139
- all stopped containers
135-
- all networks not used by at least one container%s
140+
- all networks not used by at least one container%s%s
136141
- all dangling images
137142
- all dangling build cache
138143

docs/source/markdown/podman-system-prune.1.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,28 @@ podman\-system\-prune - Remove all unused pods, containers, images, networks, an
77
**podman system prune** [*options*]
88

99
## DESCRIPTION
10-
**podman system prune** removes all unused containers (both dangling and unreferenced), pods, networks, and optionally, volumes from local storage.
10+
**podman system prune** removes all unused containers (both dangling and unreferenced), build containers, pods, networks, and optionally, volumes from local storage.
1111

1212
Use the **--all** option to delete all unused images. Unused images are dangling images as well as any image that does not have any containers based on it.
1313

1414
By default, volumes are not removed to prevent important data from being deleted if there is currently no container using the volume. Use the **--volumes** flag when running the command to prune volumes as well.
1515

16+
By default, build containers are not removed to prevent interference with builds in progress. Use the **--build** flag when running the command to remove build containers as well.
17+
1618
## OPTIONS
1719
#### **--all**, **-a**
1820

1921
Recursively remove all unused pods, containers, images, networks, and volume data. (Maximum 50 iterations.)
2022

23+
#### **--build**
24+
25+
Removes any build containers that were created during the build, but were not removed because the build was unexpectedly terminated.
26+
27+
Note: **This is not safe operation and should be executed only when no builds are in progress. It can interfere with builds in progress.**
28+
2129
#### **--external**
2230

23-
Removes all leftover container storage files from local storage not managed by Podman. In normal circumstances, no such data exists, but in case of an unclean shutdown, the Podman database may be corrupted and cause this.
31+
Tries to clean up remainders of previous containers or layers that are not references in the storage json files. These can happen in the case of unclean shutdowns or regular restarts in transient storage mode.
2432

2533
However, when using transient storage mode, the Podman database does not persist. This means containers leave the writable layers on disk after a reboot. When using a transient store, it is recommended that the **podman system prune --external** command is run during boot.
2634

libpod/runtime.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"github.com/containers/podman/v5/libpod/plugin"
3434
"github.com/containers/podman/v5/libpod/shutdown"
3535
"github.com/containers/podman/v5/pkg/domain/entities"
36+
"github.com/containers/podman/v5/pkg/domain/entities/reports"
3637
"github.com/containers/podman/v5/pkg/rootless"
3738
"github.com/containers/podman/v5/pkg/systemd"
3839
"github.com/containers/podman/v5/pkg/util"
@@ -1264,6 +1265,43 @@ func (r *Runtime) LockConflicts() (map[uint32][]string, []uint32, error) {
12641265
return toReturn, locksHeld, nil
12651266
}
12661267

1268+
// PruneBuildContainers removes any build containers that were created during the build,
1269+
// but were not removed because the build was unexpectedly terminated.
1270+
//
1271+
// Note: This is not safe operation and should be executed only when no builds are in progress. It can interfere with builds in progress.
1272+
func (r *Runtime) PruneBuildContainers() ([]*reports.PruneReport, error) {
1273+
stageContainersPruneReports := []*reports.PruneReport{}
1274+
1275+
containers, err := r.store.Containers()
1276+
if err != nil {
1277+
return stageContainersPruneReports, err
1278+
}
1279+
for _, container := range containers {
1280+
path, err := r.store.ContainerDirectory(container.ID)
1281+
if err != nil {
1282+
return stageContainersPruneReports, err
1283+
}
1284+
if err := fileutils.Exists(filepath.Join(path, "buildah.json")); err != nil {
1285+
continue
1286+
}
1287+
1288+
report := &reports.PruneReport{
1289+
Id: container.ID,
1290+
}
1291+
size, err := r.store.ContainerSize(container.ID)
1292+
if err != nil {
1293+
report.Err = err
1294+
}
1295+
report.Size = uint64(size)
1296+
1297+
if err := r.store.DeleteContainer(container.ID); err != nil {
1298+
report.Err = errors.Join(report.Err, err)
1299+
}
1300+
stageContainersPruneReports = append(stageContainersPruneReports, report)
1301+
}
1302+
return stageContainersPruneReports, nil
1303+
}
1304+
12671305
// SystemCheck checks our storage for consistency, and depending on the options
12681306
// specified, will attempt to remove anything which fails consistency checks.
12691307
func (r *Runtime) SystemCheck(ctx context.Context, options entities.SystemCheckOptions) (entities.SystemCheckReport, error) {

pkg/api/handlers/libpod/system.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func SystemPrune(w http.ResponseWriter, r *http.Request) {
2525
All bool `schema:"all"`
2626
Volumes bool `schema:"volumes"`
2727
External bool `schema:"external"`
28+
Build bool `schema:"build"`
2829
}{}
2930

3031
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
@@ -46,6 +47,7 @@ func SystemPrune(w http.ResponseWriter, r *http.Request) {
4647
Volume: query.Volumes,
4748
Filters: *filterMap,
4849
External: query.External,
50+
Build: query.Build,
4951
}
5052
report, err := containerEngine.SystemPrune(r.Context(), pruneOptions)
5153
if err != nil {

pkg/bindings/system/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type PruneOptions struct {
1818
Filters map[string][]string
1919
Volumes *bool
2020
External *bool
21+
Build *bool
2122
}
2223

2324
// VersionOptions are optional options for getting version info

pkg/bindings/system/types_prune_options.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/domain/entities/types/system.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ type SystemPruneOptions struct {
4343
Volume bool
4444
Filters map[string][]string `json:"filters" schema:"filters"`
4545
External bool
46+
Build bool
4647
}
4748

4849
// SystemPruneReport provides report after system prune is executed.

pkg/domain/infra/abi/system.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,16 @@ func (ic *ContainerEngine) Info(ctx context.Context) (*define.Info, error) {
6161
return info, nil
6262
}
6363

64-
// SystemPrune removes unused data from the system. Pruning pods, containers, networks, volumes and images.
64+
// SystemPrune removes unused data from the system. Pruning pods, containers, build container, networks, volumes and images.
6565
func (ic *ContainerEngine) SystemPrune(ctx context.Context, options entities.SystemPruneOptions) (*entities.SystemPruneReport, error) {
6666
var systemPruneReport = new(entities.SystemPruneReport)
6767

6868
if options.External {
69-
if options.All || options.Volume || len(options.Filters) > 0 {
69+
if options.All || options.Volume || len(options.Filters) > 0 || options.Build {
7070
return nil, fmt.Errorf("system prune --external cannot be combined with other options")
7171
}
72-
err := ic.Libpod.GarbageCollect()
73-
if err != nil {
72+
73+
if err := ic.Libpod.GarbageCollect(); err != nil {
7474
return nil, err
7575
}
7676
return systemPruneReport, nil
@@ -81,6 +81,17 @@ func (ic *ContainerEngine) SystemPrune(ctx context.Context, options entities.Sys
8181
filters = append(filters, fmt.Sprintf("%s=%s", k, v[0]))
8282
}
8383
reclaimedSpace := (uint64)(0)
84+
85+
// Prune Build Containers
86+
if options.Build {
87+
stageContainersPruneReports, err := ic.Libpod.PruneBuildContainers()
88+
if err != nil {
89+
return nil, err
90+
}
91+
reclaimedSpace += reports.PruneReportsSize(stageContainersPruneReports)
92+
systemPruneReport.ContainerPruneReports = append(systemPruneReport.ContainerPruneReports, stageContainersPruneReports...)
93+
}
94+
8495
found := true
8596
for found {
8697
found = false

pkg/domain/infra/tunnel/system.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func (ic *ContainerEngine) SetupRootless(_ context.Context, noMoveProcess bool,
1919

2020
// SystemPrune prunes unused data from the system.
2121
func (ic *ContainerEngine) SystemPrune(ctx context.Context, opts entities.SystemPruneOptions) (*entities.SystemPruneReport, error) {
22-
options := new(system.PruneOptions).WithAll(opts.All).WithVolumes(opts.Volume).WithFilters(opts.Filters).WithExternal(opts.External)
22+
options := new(system.PruneOptions).WithAll(opts.All).WithVolumes(opts.Volume).WithFilters(opts.Filters).WithExternal(opts.External).WithBuild(opts.Build)
2323
return system.Prune(ic.ClientCtx, options)
2424
}
2525

test/e2e/prune_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"fmt"
77
"os"
88
"path/filepath"
9+
"syscall"
10+
"time"
911

1012
. "github.com/containers/podman/v5/test/utils"
1113
. "github.com/onsi/ginkgo/v2"
@@ -23,6 +25,11 @@ FROM scratch
2325
ENV test1=test1
2426
ENV test2=test2`
2527

28+
var longBuildImage = fmt.Sprintf(`
29+
FROM %s
30+
RUN echo "Hello, World!"
31+
RUN RUN echo "Please use signal 9 this will never ends" && sleep 10000s`, ALPINE)
32+
2633
var _ = Describe("Podman prune", func() {
2734

2835
It("podman container prune containers", func() {
@@ -580,4 +587,63 @@ var _ = Describe("Podman prune", func() {
580587
Expect(err).ToNot(HaveOccurred())
581588
Expect(dirents).To(HaveLen(3))
582589
})
590+
591+
It("podman system prune --build clean up after terminated build", func() {
592+
useCustomNetworkDir(podmanTest, tempdir)
593+
594+
podmanTest.BuildImage(pruneImage, "alpine_notleaker:latest", "false")
595+
596+
create := podmanTest.Podman([]string{"create", "--name", "test", BB, "sleep", "10000"})
597+
create.WaitWithDefaultTimeout()
598+
Expect(create).Should(ExitCleanly())
599+
600+
containerFilePath := filepath.Join(podmanTest.TempDir, "ContainerFile-podman-leaker")
601+
err := os.WriteFile(containerFilePath, []byte(longBuildImage), 0755)
602+
Expect(err).ToNot(HaveOccurred())
603+
604+
build := podmanTest.Podman([]string{"build", "-f", containerFilePath, "-t", "podmanleaker"})
605+
// Build will never finish so let's wait for build to ask for SIGKILL to simulate a failed build that leaves stage containers.
606+
matchedOutput := false
607+
for range 900 {
608+
if build.LineInOutputContains("Please use signal 9") {
609+
matchedOutput = true
610+
build.Signal(syscall.SIGKILL)
611+
break
612+
}
613+
time.Sleep(100 * time.Millisecond)
614+
}
615+
if !matchedOutput {
616+
Fail("Did not match special string in podman build")
617+
}
618+
619+
// Check Intermediate image of stage container
620+
none := podmanTest.Podman([]string{"images", "-a"})
621+
none.WaitWithDefaultTimeout()
622+
Expect(none).Should(ExitCleanly())
623+
Expect(none.OutputToString()).Should(ContainSubstring("none"))
624+
625+
// Check if Container and Stage Container exist
626+
count := podmanTest.Podman([]string{"ps", "-aq", "--external"})
627+
count.WaitWithDefaultTimeout()
628+
Expect(count).Should(ExitCleanly())
629+
Expect(count.OutputToStringArray()).To(HaveLen(3))
630+
631+
prune := podmanTest.Podman([]string{"system", "prune", "--build", "-f"})
632+
prune.WaitWithDefaultTimeout()
633+
Expect(prune).Should(ExitCleanly())
634+
635+
// Container should still exist, but no stage containers
636+
count = podmanTest.Podman([]string{"ps", "-aq", "--external"})
637+
count.WaitWithDefaultTimeout()
638+
Expect(count).Should(ExitCleanly())
639+
Expect(count.OutputToString()).To(BeEmpty())
640+
641+
Expect(podmanTest.NumberOfContainers()).To(Equal(0))
642+
643+
after := podmanTest.Podman([]string{"images", "-a"})
644+
after.WaitWithDefaultTimeout()
645+
Expect(after).Should(ExitCleanly())
646+
Expect(after.OutputToString()).ShouldNot(ContainSubstring("none"))
647+
Expect(after.OutputToString()).Should(ContainSubstring("notleaker"))
648+
})
583649
})

0 commit comments

Comments
 (0)