Skip to content

Commit 472c30e

Browse files
committed
feat(system): add nerdctl system df
Add `nerdctl system df`, the equivalent of `docker system df`, reporting how much disk space the images, containers and local volumes of the current namespace use, plus the BuildKit build cache. The sizes follow the Docker v29 definitions for the containerd image store: - the size of an image is the content present in the content store plus its unpacked snapshots, which is the same value `nerdctl images` shows as DISK USAGE, - the SIZE of the Images row counts every snapshot and every blob once, so it is the space really taken on disk rather than the sum of the image sizes, - an image is active when a container references it, and only the part of an unused image that no other image shares is reclaimable, - containers contribute their read-write layer only, and everything not running is reclaimable, - a volume is active when a container mounts it, counted once however many paths it is mounted at, as the LINKS column of Docker is a count of containers, - a build cache record is reclaimable when it is neither in use nor shared. An image reports when it was built, read from the config of the manifest the platform matcher of this host selects: the platforms of an index are not necessarily built together, and the creation time of the local record only says when the image was pulled or tagged. The record time stays as the fallback for the images that state nothing. Both `--format` and `-v/--verbose` are supported, including the Docker `table TEMPLATE` format, e.g. `table {{.Type}}\t{{.Size}}`: the literal \t and \n are expanded, the chosen columns get a header that names them whatever the template does to the values below, and the rows stay aligned. That helper lives in pkg/formatter so that the other commands, which all share this gap, can adopt it. Identifiers are shortened for the table output only, so that a custom format stays usable to look a resource up. The work is split the way `system prune` already is: `pkg/cmd/system` orchestrates, and each kind of resource is measured by its own package. Closes #3942 Signed-off-by: Eugene Kalinin <e.v.kalinin@gmail.com>
1 parent c235f00 commit 472c30e

22 files changed

Lines changed: 2875 additions & 24 deletions

File tree

cmd/nerdctl/system/system.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ func Command() *cobra.Command {
3333
}
3434
// versionCommand is not here
3535
cmd.AddCommand(
36+
dfCommand(),
3637
EventsCommand(),
3738
InfoCommand(),
3839
pruneCommand(),

cmd/nerdctl/system/system_df.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package system
18+
19+
import (
20+
"github.com/spf13/cobra"
21+
22+
"github.com/containerd/log"
23+
24+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/builder"
25+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
26+
"github.com/containerd/nerdctl/v2/pkg/api/types"
27+
"github.com/containerd/nerdctl/v2/pkg/clientutil"
28+
"github.com/containerd/nerdctl/v2/pkg/cmd/system"
29+
)
30+
31+
func dfCommand() *cobra.Command {
32+
cmd := &cobra.Command{
33+
Use: "df [flags]",
34+
Short: "Show nerdctl disk usage",
35+
Args: cobra.NoArgs,
36+
RunE: dfAction,
37+
SilenceUsage: true,
38+
SilenceErrors: true,
39+
}
40+
cmd.Flags().BoolP("verbose", "v", false, "Show detailed information on space usage")
41+
cmd.Flags().String("format", "", "Format the output using the given Go template, e.g, '{{json .}}'")
42+
return cmd
43+
}
44+
45+
func dfOptions(cmd *cobra.Command) (types.SystemDfOptions, error) {
46+
globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
47+
if err != nil {
48+
return types.SystemDfOptions{}, err
49+
}
50+
51+
verbose, err := cmd.Flags().GetBool("verbose")
52+
if err != nil {
53+
return types.SystemDfOptions{}, err
54+
}
55+
56+
format, err := cmd.Flags().GetString("format")
57+
if err != nil {
58+
return types.SystemDfOptions{}, err
59+
}
60+
61+
buildkitHost, err := builder.GetBuildkitHost(cmd, globalOptions.Namespace)
62+
if err != nil {
63+
log.L.WithError(err).Warn("BuildKit is not running. The build cache usage will be reported as empty.")
64+
buildkitHost = ""
65+
}
66+
67+
return types.SystemDfOptions{
68+
Stdout: cmd.OutOrStdout(),
69+
Stderr: cmd.ErrOrStderr(),
70+
GOptions: globalOptions,
71+
Format: format,
72+
Verbose: verbose,
73+
BuildKitHost: buildkitHost,
74+
}, nil
75+
}
76+
77+
func dfAction(cmd *cobra.Command, _ []string) error {
78+
options, err := dfOptions(cmd)
79+
if err != nil {
80+
return err
81+
}
82+
83+
client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address)
84+
if err != nil {
85+
return err
86+
}
87+
defer cancel()
88+
89+
return system.Df(ctx, client, options)
90+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package system
18+
19+
import (
20+
"fmt"
21+
"testing"
22+
23+
"github.com/containerd/nerdctl/mod/tigron/test"
24+
"github.com/containerd/nerdctl/mod/tigron/tig"
25+
26+
"github.com/containerd/nerdctl/v2/pkg/testutil"
27+
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
28+
)
29+
30+
// TestSystemDfVolumes covers the Local Volumes row, which the rest of TestSystemDf cannot: a volume
31+
// is only counted once a container mounts it, and the target of a mount is written differently on
32+
// each platform.
33+
func TestSystemDfVolumes(t *testing.T) {
34+
testCase := nerdtest.Setup()
35+
36+
// The counts are only meaningful when nothing else is running against the same namespace.
37+
testCase.NoParallel = true
38+
39+
testCase.SubTests = []*test.Case{
40+
{
41+
Description: "mounted volume is active",
42+
Require: nerdtest.Private,
43+
Setup: func(data test.Data, helpers test.Helpers) {
44+
data.Labels().Set(baselineLabel, helpers.Capture("system", "df"))
45+
helpers.Ensure("volume", "create", data.Identifier())
46+
helpers.Ensure("run", "-d", "--name", data.Identifier(),
47+
"-v", fmt.Sprintf("%s:/volume", data.Identifier()),
48+
testutil.CommonImage, "sleep", nerdtest.Infinity)
49+
},
50+
Cleanup: func(data test.Data, helpers test.Helpers) {
51+
helpers.Anyhow("rm", "-f", data.Identifier())
52+
helpers.Anyhow("volume", "rm", "-f", data.Identifier())
53+
},
54+
Command: test.Command("system", "df"),
55+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
56+
return &test.Expected{
57+
ExitCode: 0,
58+
Output: func(stdout string, t tig.T) {
59+
// The volume is created by this test and the container it runs mounts it,
60+
// so both counts went up by it.
61+
dfGrewBy(t, data, stdout, "Local Volumes", totalColumn, 1)
62+
dfGrewBy(t, data, stdout, "Local Volumes", activeColumn, 1)
63+
},
64+
}
65+
},
66+
},
67+
}
68+
69+
testCase.Run(t)
70+
}

0 commit comments

Comments
 (0)