From ed2dee1e4379d2221db5d1485608a05ea9de9c80 Mon Sep 17 00:00:00 2001 From: Mike Sul Date: Wed, 10 Dec 2025 12:50:22 +0100 Subject: [PATCH 1/2] update_test: Add test to verify app pruning Ensure uninstall with pruning does not remove needed images The test verifies that uninstalling apps with pruning does not remove too many images, which could affect the detection of the currently running apps. Specifically, pruning all images without containers can remove tags for images that are still used by running containers. This can lead to a client incorrectly determining that an app is not running due to a missing image. Also, it seems like the issue in the docker since it removes wrong tags. Signed-off-by: Mike Sul --- test/integration/update_test.go | 88 +++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/test/integration/update_test.go b/test/integration/update_test.go index 208a655..a25cbc8 100644 --- a/test/integration/update_test.go +++ b/test/integration/update_test.go @@ -551,3 +551,91 @@ services: t.Fatalf("there is/are %d failed updates, expected %d\n", failureCount, 0) } } + +func TestAppPruning(t *testing.T) { + appComposeDef01 := ` +services: + srvs-01: + image: registry:5000/factory/runner-image:v0.1 + command: sh -c "while true; do sleep 60; done" + ports: + - 8081:81 + busybox-1: + image: ghcr.io/foundriesio/busybox:1.36 + command: sh -c "while true; do sleep 60; done" + busybox-2: + image: ghcr.io/foundriesio/busybox:1.36-multiarch + command: sh -c "while true; do sleep 120; done" +` + appComposeDef02 := ` +services: + srvs-01: + image: registry:5000/factory/runner-image:v0.1 + command: sh -c "while true; do sleep 60; done" + ports: + - 8082:82 + busybox-1: + image: ghcr.io/foundriesio/busybox:1.36 + command: sh -c "while true; do sleep 70; done" + busybox-2: + image: ghcr.io/foundriesio/busybox:1.36-multiarch + command: sh -c "while true; do sleep 110; done" +` + var appURIs []string + for _, appDef := range []string{appComposeDef01, appComposeDef02} { + app := f.NewApp(t, appDef) + app.Publish(t) + appURIs = append(appURIs, app.PublishedUri) + } + + cfg := f.NewTestConfig(t) + ctx := context.Background() + + updateRunner, err := update.NewUpdate(cfg, "target-10") + f.Check(t, err) + + // do update + f.Check(t, updateRunner.Init(ctx, appURIs)) + f.Check(t, updateRunner.Fetch(ctx)) + f.Check(t, updateRunner.Install(ctx)) + f.Check(t, updateRunner.Start(ctx)) + f.Check(t, updateRunner.Complete(ctx, update.CompleteWithPruning())) + + // check that both apps are running + appsStatus, err := compose.CheckAppsStatus(ctx, cfg, nil) + f.Check(t, err) + if !appsStatus.AreRunning() { + t.Fatal("apps are expected to be running") + } + + // do sync update, remove the second app + updateRunner, err = update.NewUpdate(cfg, "target-10") + f.Check(t, err) + oneAppURI := []string{appURIs[0]} + f.Check(t, updateRunner.Init(ctx, oneAppURI, update.WithInitCheckStatus(false))) + f.Check(t, updateRunner.Fetch(ctx)) + // Stop apps before installing the update, which effectively removes the second app + f.Check(t, compose.StopApps(ctx, cfg, appURIs)) + f.Check(t, updateRunner.Install(ctx)) + // Start only the first app + f.Check(t, updateRunner.Start(ctx)) + // Complete with pruning to remove the second app + f.Check(t, updateRunner.Complete(ctx, update.CompleteWithPruning())) + + appsStatus, err = compose.CheckAppsStatus(ctx, cfg, nil) + f.Check(t, err) + if !appsStatus.AreRunning() { + t.Fatal("app is expected to be running") + } + if len(appsStatus.Apps) > 1 || len(appsStatus.Apps) == 0 { + t.Fatalf("only one app is expected to be running, found %d", len(appsStatus.Apps)) + } + if appsStatus.Apps[0].Ref().String() != appURIs[0] { + t.Fatalf("expected app URI %s, found %s", appURIs[0], appsStatus.Apps[0].Ref().String()) + } + + // stop, uninstall, and remove all apps + f.Check(t, compose.StopApps(ctx, cfg, oneAppURI)) + f.Check(t, compose.UninstallApps(ctx, cfg, oneAppURI, compose.WithImagePruning())) + f.Check(t, compose.RemoveApps(ctx, cfg, oneAppURI)) +} From 111dbeb8d41d1949726f130a9e3910ac91365a17 Mon Sep 17 00:00:00 2001 From: Mike Sul Date: Wed, 10 Dec 2025 13:00:50 +0100 Subject: [PATCH 2/2] uninstall: Prune only dangling images Prune only dangling images - the images that are not tagged and not referenced by any container. Signed-off-by: Mike Sul --- pkg/compose/uninstall.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/compose/uninstall.go b/pkg/compose/uninstall.go index d8b100e..ee5d2f9 100644 --- a/pkg/compose/uninstall.go +++ b/pkg/compose/uninstall.go @@ -64,16 +64,16 @@ func UninstallApps(ctx context.Context, cfg *Config, appRefs []string, options . } if opts.Prune { - // Prune unused images, it should remove app images of stopped apps - // from the docker store, unless they are used by some 3rd party containers/apps - // TODO: don't remove unused images that does not belong to any of the specified apps - // If the same image is used by one of the specified apps and some 3rd party app - how - // to figure out it so we can skip this image removal cli, errClient := GetDockerClient(cfg.DockerHost) if errClient != nil { return errClient } - _, err = cli.ImagesPrune(ctx, filters.NewArgs(filters.Arg("dangling", "false"))) + // Prune only dangling images. + // The dangling images are the ones that are not tagged and not referenced by any container. + // TODO: consider pruning volumes and networks if needed. + // TODO: consider pruning only those images that are related to the uninstalled apps, + // otherwise it prunes all dangling images including those that are not managed by composectl + _, err = cli.ImagesPrune(ctx, filters.NewArgs(filters.Arg("dangling", "true"))) } return err }