diff --git a/README.md b/README.md index 8221e4ad637..8c6de05eed5 100644 --- a/README.md +++ b/README.md @@ -317,6 +317,7 @@ It does not necessarily mean that the corresponding features are missing in cont - [:whale: nerdctl events](#whale-nerdctl-events) - [:whale: nerdctl info](#whale-nerdctl-info) - [:whale: nerdctl version](#whale-nerdctl-version) + - [:whale: nerdctl system prune](#whale-nerdctl-system-prune) - [Stats](#stats) - [:whale: nerdctl stats](#whale-nerdctl-stats) - [:whale: nerdctl top](#whale-nerdctl-top) @@ -1218,6 +1219,20 @@ Usage: `nerdctl version [OPTIONS]` Flags: - :whale: `-f, --format`: Format the output using the given Go template, e.g, `{{json .}}` +### :whale: nerdctl system prune +Remove unused data + +:warning: Currently, `nerdctl system prune` requires --all to be specified. + +Usage: `nerdctl system prune [OPTIONS]` + +Flags: +- :whale: `-a, --all`: Remove all unused images, not just dangling ones +- :whale: `-f, --force`: Do not prompt for confirmation +- :whale: `--volumes`: Prune volumes + +Unimplemented `docker system prune` flags: `--filter` + ## Stats ### :whale: nerdctl stats Display a live stream of container(s) resource usage statistics. diff --git a/cmd/nerdctl/builder.go b/cmd/nerdctl/builder.go index 32e2a150f41..f22c1ffab9d 100644 --- a/cmd/nerdctl/builder.go +++ b/cmd/nerdctl/builder.go @@ -58,7 +58,7 @@ func newBuilderPruneCommand() *cobra.Command { return buildPruneCommand } -func builderPruneAction(cmd *cobra.Command, args []string) error { +func builderPruneAction(cmd *cobra.Command, _ []string) error { buildkitHost, err := getBuildkitHost(cmd) if err != nil { return err diff --git a/cmd/nerdctl/container_prune.go b/cmd/nerdctl/container_prune.go index 95b186ba406..9925ff6b313 100644 --- a/cmd/nerdctl/container_prune.go +++ b/cmd/nerdctl/container_prune.go @@ -17,10 +17,12 @@ package main import ( + "context" "errors" "fmt" "strings" + "github.com/containerd/containerd" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -46,7 +48,9 @@ func containerPruneAction(cmd *cobra.Command, _ []string) error { if !force { var confirm string - fmt.Fprintf(cmd.OutOrStdout(), "%s", "WARNING! This will remove all stopped containers.\nAre you sure you want to continue? [y/N] ") + msg := "This will remove all stopped containers." + msg += "\nAre you sure you want to continue? [y/N] " + fmt.Fprintf(cmd.OutOrStdout(), "WARNING! %s", msg) fmt.Fscanf(cmd.InOrStdin(), "%s", &confirm) if strings.ToLower(confirm) != "y" { @@ -60,6 +64,10 @@ func containerPruneAction(cmd *cobra.Command, _ []string) error { } defer cancel() + return containerPrune(cmd, client, ctx) +} + +func containerPrune(cmd *cobra.Command, client *containerd.Client, ctx context.Context) error { containers, err := client.Containers(ctx) if err != nil { return err @@ -83,6 +91,7 @@ func containerPruneAction(cmd *cobra.Command, _ []string) error { for _, id := range deleted { fmt.Fprintln(cmd.OutOrStdout(), id) } + fmt.Fprintln(cmd.OutOrStdout(), "") } return nil diff --git a/cmd/nerdctl/image_prune.go b/cmd/nerdctl/image_prune.go index b8e88292a09..8f83ad0f95f 100644 --- a/cmd/nerdctl/image_prune.go +++ b/cmd/nerdctl/image_prune.go @@ -17,11 +17,14 @@ package main import ( + "context" "fmt" "strings" + "github.com/containerd/containerd" "github.com/containerd/containerd/images" "github.com/containerd/containerd/platforms" + "github.com/opencontainers/go-digest" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -77,6 +80,10 @@ func imagePruneAction(cmd *cobra.Command, _ []string) error { } defer cancel() + return imagePrune(cmd, client, ctx) +} + +func imagePrune(cmd *cobra.Command, client *containerd.Client, ctx context.Context) error { var ( imageStore = client.ImageService() contentStore = client.ContentStore() @@ -96,6 +103,7 @@ func imagePruneAction(cmd *cobra.Command, _ []string) error { } delOpts := []images.DeleteOpt{images.SynchronousDelete()} + removedImages := make(map[string][]digest.Digest) for _, image := range imageList { if _, ok := usedImages[image.Name]; ok { continue @@ -107,11 +115,20 @@ func imagePruneAction(cmd *cobra.Command, _ []string) error { } if err := imageStore.Delete(ctx, image.Name, delOpts...); err != nil { logrus.WithError(err).Warnf("failed to delete image %s", image.Name) + continue } - fmt.Fprintf(cmd.OutOrStdout(), "Untagged: %s\n", image.Name) - for _, digest := range digests { - fmt.Fprintf(cmd.OutOrStdout(), "deleted: %s\n", digest) + removedImages[image.Name] = digests + } + + if len(removedImages) > 0 { + fmt.Fprintln(cmd.OutOrStdout(), "Deleted Images:") + for image, digests := range removedImages { + fmt.Fprintf(cmd.OutOrStdout(), "Untagged: %s\n", image) + for _, digest := range digests { + fmt.Fprintf(cmd.OutOrStdout(), "deleted: %s\n", digest) + } } + fmt.Fprintln(cmd.OutOrStdout(), "") } return nil } diff --git a/cmd/nerdctl/network_prune.go b/cmd/nerdctl/network_prune.go index 3654d617e6a..28337179d16 100644 --- a/cmd/nerdctl/network_prune.go +++ b/cmd/nerdctl/network_prune.go @@ -43,15 +43,7 @@ func newNetworkPruneCommand() *cobra.Command { return networkPruneCommand } -func networkPruneAction(cmd *cobra.Command, args []string) error { - cniPath, err := cmd.Flags().GetString("cni-path") - if err != nil { - return err - } - cniNetconfpath, err := cmd.Flags().GetString("cni-netconfpath") - if err != nil { - return err - } +func networkPruneAction(cmd *cobra.Command, _ []string) error { force, err := cmd.Flags().GetBool("force") if err != nil { return err @@ -70,16 +62,28 @@ func networkPruneAction(cmd *cobra.Command, args []string) error { } } - e, err := netutil.NewCNIEnv(cniPath, cniNetconfpath) + client, ctx, cancel, err := newClient(cmd) if err != nil { return err } + defer cancel() - client, ctx, cancel, err := newClient(cmd) + return networkPrune(cmd, client, ctx) +} + +func networkPrune(cmd *cobra.Command, client *containerd.Client, ctx context.Context) error { + cniPath, err := cmd.Flags().GetString("cni-path") + if err != nil { + return err + } + cniNetconfpath, err := cmd.Flags().GetString("cni-netconfpath") + if err != nil { + return err + } + e, err := netutil.NewCNIEnv(cniPath, cniNetconfpath) if err != nil { return err } - defer cancel() containers, err := client.Containers(ctx) if err != nil { @@ -114,6 +118,7 @@ func networkPruneAction(cmd *cobra.Command, args []string) error { for _, name := range removedNetworks { fmt.Fprintln(cmd.OutOrStdout(), name) } + fmt.Fprintln(cmd.OutOrStdout(), "") } return nil } diff --git a/cmd/nerdctl/system.go b/cmd/nerdctl/system.go index 9d767e67352..bb9635be295 100644 --- a/cmd/nerdctl/system.go +++ b/cmd/nerdctl/system.go @@ -31,6 +31,7 @@ func newSystemCommand() *cobra.Command { systemCommand.AddCommand( newEventsCommand(), newInfoCommand(), + newSystemPruneCommand(), ) return systemCommand } diff --git a/cmd/nerdctl/system_prune.go b/cmd/nerdctl/system_prune.go new file mode 100644 index 00000000000..0eab374a6ae --- /dev/null +++ b/cmd/nerdctl/system_prune.go @@ -0,0 +1,103 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package main + +import ( + "fmt" + "strings" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func newSystemPruneCommand() *cobra.Command { + systemPruneCommand := &cobra.Command{ + Use: "prune [flags]", + Short: "Remove unused data", + Args: cobra.NoArgs, + RunE: systemPruneAction, + SilenceUsage: true, + SilenceErrors: true, + } + systemPruneCommand.Flags().BoolP("all", "a", false, "Remove all unused images, not just dangling ones") + systemPruneCommand.Flags().BoolP("force", "f", false, "Do not prompt for confirmation") + systemPruneCommand.Flags().Bool("volumes", false, "Prune volumes") + return systemPruneCommand +} + +func systemPruneAction(cmd *cobra.Command, args []string) error { + all, err := cmd.Flags().GetBool("all") + if err != nil { + return err + } + + if !all { + logrus.Warn("Currently, `nerdctl system prune` requires --all to be specified. Skip pruning.") + // NOP + return nil + } + + vFlag, err := cmd.Flags().GetBool("volumes") + if err != nil { + return err + } + + force, err := cmd.Flags().GetBool("force") + if err != nil { + return err + } + + if !force { + var confirm string + msg := `This will remove: + - all stopped containers + - all networks not used by at least one container` + if vFlag { + msg += ` + - all volumes not used by at least one container` + } + msg += ` + - all images without at least one container associated to them +` + msg += "\nAre you sure you want to continue? [y/N] " + fmt.Fprintf(cmd.OutOrStdout(), "WARNING! %s", msg) + fmt.Fscanf(cmd.InOrStdin(), "%s", &confirm) + + if strings.ToLower(confirm) != "y" { + return nil + } + } + + client, ctx, cancel, err := newClient(cmd) + if err != nil { + return err + } + defer cancel() + + if err := containerPrune(cmd, client, ctx); err != nil { + return err + } + if err := networkPrune(cmd, client, ctx); err != nil { + return err + } + if vFlag { + if err := volumePrune(cmd, client, ctx); err != nil { + return err + } + } + return imagePrune(cmd, client, ctx) +} diff --git a/cmd/nerdctl/system_prune_linux_test.go b/cmd/nerdctl/system_prune_linux_test.go new file mode 100644 index 00000000000..114faeaf288 --- /dev/null +++ b/cmd/nerdctl/system_prune_linux_test.go @@ -0,0 +1,54 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package main + +import ( + "fmt" + "testing" + + "github.com/containerd/nerdctl/pkg/testutil" +) + +func TestSystemPrune(t *testing.T) { + base := testutil.NewBase(t) + base.Cmd("container", "prune", "-f").AssertOK() + base.Cmd("network", "prune", "-f").AssertOK() + base.Cmd("volume", "prune", "-f").AssertOK() + base.Cmd("image", "prune", "-f", "--all").AssertOK() + + nID := testutil.Identifier(t) + base.Cmd("network", "create", nID).AssertOK() + defer base.Cmd("network", "rm", nID).Run() + + vID := testutil.Identifier(t) + base.Cmd("volume", "create", vID).AssertOK() + defer base.Cmd("volume", "rm", vID).Run() + + tID := testutil.Identifier(t) + base.Cmd("run", "-v", fmt.Sprintf("%s:/volume", vID), "--net", nID, + "--name", tID, testutil.CommonImage).AssertOK() + defer base.Cmd("rm", "-f", tID).Run() + + base.Cmd("ps", "-a").AssertOutContains(tID) + base.Cmd("images").AssertOutContains("alpine") + + base.Cmd("system", "prune", "-f", "--volumes", "--all").AssertOK() + base.Cmd("volume", "ls").AssertNoOut(vID) + base.Cmd("ps", "-a").AssertNoOut(tID) + base.Cmd("network", "ls").AssertNoOut(nID) + base.Cmd("images").AssertNoOut("alpine") +} diff --git a/cmd/nerdctl/volume_prune.go b/cmd/nerdctl/volume_prune.go index 231997b636d..630ec2d9a12 100644 --- a/cmd/nerdctl/volume_prune.go +++ b/cmd/nerdctl/volume_prune.go @@ -17,9 +17,11 @@ package main import ( + "context" "fmt" "strings" + "github.com/containerd/containerd" "github.com/spf13/cobra" ) @@ -37,12 +39,6 @@ func newVolumePruneCommand() *cobra.Command { } func volumePruneAction(cmd *cobra.Command, _ []string) error { - client, ctx, cancel, err := newClient(cmd) - if err != nil { - return err - } - defer cancel() - force, err := cmd.Flags().GetBool("force") if err != nil { return err @@ -60,6 +56,16 @@ func volumePruneAction(cmd *cobra.Command, _ []string) error { } } + client, ctx, cancel, err := newClient(cmd) + if err != nil { + return err + } + defer cancel() + + return volumePrune(cmd, client, ctx) +} + +func volumePrune(cmd *cobra.Command, client *containerd.Client, ctx context.Context) error { volStore, err := getVolumeStore(cmd) if err != nil { return err @@ -92,6 +98,7 @@ func volumePruneAction(cmd *cobra.Command, _ []string) error { for _, name := range removedNames { fmt.Fprintln(cmd.OutOrStdout(), name) } + fmt.Fprintln(cmd.OutOrStdout(), "") } return nil }