Skip to content

Commit

Permalink
Fix duplicate image entries in k8s.io namespaces
Browse files Browse the repository at this point in the history
The same imageId underk8s.io is showing multiple results: repo:tag, repo:digest, configID.
We expect to display only repo:tag, consistent with other namespaces and CRI.
		e.g.
		nerdctl -n k8s.io images
		REPOSITORY    TAG       IMAGE ID        CREATED        PLATFORM       SIZE         BLOB SIZE
		centos        7         be65f488b776    3 hours ago    linux/amd64    211.5 MiB    72.6 MiB
		centos        <none>    be65f488b776    3 hours ago    linux/amd64    211.5 MiB    72.6 MiB
		<none>        <none>    be65f488b776    3 hours ago    linux/amd64    211.5 MiB    72.6 MiB

		expect:
		nerdctl --kube-hide-dupe -n k8s.io images
		REPOSITORY    TAG       IMAGE ID        CREATED        PLATFORM       SIZE         BLOB SIZE
		centos        7         be65f488b776    3 hours ago    linux/amd64    211.5 MiB    72.6 MiB
Of course, even after deduplicating the images displayed, there are still issues with deleting the images.
It is necessary to distinguish between repo:tag and configId, as well as repoDigest. Considering the situation with tags.
we need to ensure that all repo:tags under the same imageId are cleaned up before proceeding to clean up the configId and repoDigest.

see: #3702

Signed-off-by: fengwei0328 <feng.wei8@zte.com.cn>
  • Loading branch information
fengwei0328 committed Dec 24, 2024
1 parent 1f81225 commit 01269d2
Show file tree
Hide file tree
Showing 9 changed files with 460 additions and 4 deletions.
5 changes: 5 additions & 0 deletions cmd/nerdctl/helpers/flagutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ func ProcessRootCmdFlags(cmd *cobra.Command) (types.GlobalCommandOptions, error)
if err != nil {
return types.GlobalCommandOptions{}, err
}
kubeHideDupe, err := cmd.Flags().GetBool("kube-hide-dupe")
if err != nil {
return types.GlobalCommandOptions{}, err
}
return types.GlobalCommandOptions{
Debug: debug,
DebugFull: debugFull,
Expand All @@ -118,6 +122,7 @@ func ProcessRootCmdFlags(cmd *cobra.Command) (types.GlobalCommandOptions, error)
Experimental: experimental,
HostGatewayIP: hostGatewayIP,
BridgeIP: bridgeIP,
KubeHideDupe: kubeHideDupe,
}, nil
}

Expand Down
67 changes: 67 additions & 0 deletions cmd/nerdctl/image/image_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,3 +317,70 @@ CMD ["echo", "nerdctl-build-notag-string"]

testCase.Run(t)
}

func TestImagesKubeWithKubeHideDupe(t *testing.T) {
nerdtest.Setup()

testCase := &test.Case{
Require: test.Require(
nerdtest.OnlyKubernetes,
),
Setup: func(data test.Data, helpers test.Helpers) {
helpers.Ensure("pull", "--quiet", testutil.BusyboxImage)
},
SubTests: []*test.Case{
{
Description: "The same imageID will not print no-repo:tag in k8s.io with kube-hide-dupe",
Command: test.Command("--kube-hide-dupe", "images"),
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
return &test.Expected{
Output: func(stdout string, info string, t *testing.T) {
var imageID string
var skipLine int
lines := strings.Split(strings.TrimSpace(stdout), "\n")
header := "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tPLATFORM\tSIZE\tBLOB SIZE"
if nerdtest.IsDocker() {
header = "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tSIZE"
}
tab := tabutil.NewReader(header)
err := tab.ParseHeader(lines[0])
assert.NilError(t, err, info)
found := true
for i, line := range lines[1:] {
repo, _ := tab.ReadRow(line, "REPOSITORY")
tag, _ := tab.ReadRow(line, "TAG")
if repo+":"+tag == testutil.BusyboxImage {
skipLine = i
imageID, _ = tab.ReadRow(line, "IMAGE ID")
break
}
}
for i, line := range lines[1:] {
if i == skipLine {
continue
}
id, _ := tab.ReadRow(line, "IMAGE ID")
if id == imageID {
found = false
break
}
}
assert.Assert(t, found, info)
},
}
},
},
{
Description: "the same imageId will print no-repo:tag in k8s.io without kube-hide-dupe",
Command: test.Command("images"),
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
return &test.Expected{
Output: test.Contains("<none>"),
}
},
},
},
}

testCase.Run(t)
}
170 changes: 170 additions & 0 deletions cmd/nerdctl/image/image_remove_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,3 +351,173 @@ func TestIssue3016(t *testing.T) {

testCase.Run(t)
}

func TestRemoveKubeWithKubeHideDupe(t *testing.T) {
var numTags, numNoTags int
testCase := nerdtest.Setup()
testCase.NoParallel = true
testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
helpers.Anyhow("--kube-hide-dupe", "rmi", "-f", testutil.BusyboxImage)
}
testCase.Setup = func(data test.Data, helpers test.Helpers) {
numTags = len(strings.Split(strings.TrimSpace(helpers.Capture("--kube-hide-dupe", "images")), "\n"))
numNoTags = len(strings.Split(strings.TrimSpace(helpers.Capture("images")), "\n"))
}
testCase.Require = test.Require(
nerdtest.OnlyKubernetes,
)
testCase.SubTests = []*test.Case{
{
Description: "After removing the tag without kube-hide-dupe, repodigest is shown as <none>",
NoParallel: true,
Setup: func(data test.Data, helpers test.Helpers) {
helpers.Ensure("pull", testutil.BusyboxImage)
},
Command: test.Command("rmi", "-f", testutil.BusyboxImage),
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
return &test.Expected{
ExitCode: 0,
Errors: []error{},
Output: func(stdout string, info string, t *testing.T) {
helpers.Command("--kube-hide-dupe", "images").Run(&test.Expected{
Output: func(stdout string, info string, t *testing.T) {
lines := strings.Split(strings.TrimSpace(stdout), "\n")
assert.Assert(t, len(lines) == numTags+1, info)
},
})
helpers.Command("images").Run(&test.Expected{
Output: func(stdout string, info string, t *testing.T) {
lines := strings.Split(strings.TrimSpace(stdout), "\n")
assert.Assert(t, len(lines) == numNoTags+1, info)
},
})
},
}
},
},
{
Description: "If there are other tags, the Repodigest will not be deleted",
NoParallel: true,
Cleanup: func(data test.Data, helpers test.Helpers) {
helpers.Anyhow("--kube-hide-dupe", "rmi", data.Identifier())
},
Setup: func(data test.Data, helpers test.Helpers) {
helpers.Ensure("pull", testutil.BusyboxImage)
helpers.Ensure("tag", testutil.BusyboxImage, data.Identifier())
},
Command: test.Command("--kube-hide-dupe", "rmi", testutil.BusyboxImage),
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
return &test.Expected{
ExitCode: 0,
Errors: []error{},
Output: func(stdout string, info string, t *testing.T) {
helpers.Command("--kube-hide-dupe", "images").Run(&test.Expected{
Output: func(stdout string, info string, t *testing.T) {
lines := strings.Split(strings.TrimSpace(stdout), "\n")
assert.Assert(t, len(lines) == numTags+1, info)
},
})
helpers.Command("images").Run(&test.Expected{
Output: func(stdout string, info string, t *testing.T) {
lines := strings.Split(strings.TrimSpace(stdout), "\n")
assert.Assert(t, len(lines) == numNoTags+2, info)
},
})
},
}
},
},
{
Description: "After deleting all repo:tag entries, all repodigests will be cleaned up",
NoParallel: true,
Setup: func(data test.Data, helpers test.Helpers) {
helpers.Ensure("pull", testutil.BusyboxImage)
helpers.Ensure("tag", testutil.BusyboxImage, data.Identifier())
},
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
helpers.Ensure("--kube-hide-dupe", "rmi", "-f", testutil.BusyboxImage)
return helpers.Command("--kube-hide-dupe", "rmi", "-f", data.Identifier())
},
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
return &test.Expected{
Output: func(stdout string, info string, t *testing.T) {
helpers.Command("--kube-hide-dupe", "images").Run(&test.Expected{
Output: func(stdout string, info string, t *testing.T) {
lines := strings.Split(strings.TrimSpace(stdout), "\n")
assert.Assert(t, len(lines) == numTags, info)
},
})
helpers.Command("images").Run(&test.Expected{
Output: func(stdout string, info string, t *testing.T) {
lines := strings.Split(strings.TrimSpace(stdout), "\n")
assert.Assert(t, len(lines) == numNoTags, info)
},
})
},
}
},
},
{
Description: "Test multiple IDs found with provided prefix and force with shortID",
NoParallel: true,
Setup: func(data test.Data, helpers test.Helpers) {
helpers.Ensure("pull", testutil.BusyboxImage)
helpers.Ensure("tag", testutil.BusyboxImage, data.Identifier())
},
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
return helpers.Command("--kube-hide-dupe", "images", testutil.BusyboxImage, "-q")
},
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
return &test.Expected{
Output: func(stdout string, info string, t *testing.T) {
helpers.Command("--kube-hide-dupe", "rmi", stdout[0:12]).Run(&test.Expected{
ExitCode: 1,
Errors: []error{errors.New("multiple IDs found with provided prefix: ")},
})
helpers.Command("--kube-hide-dupe", "rmi", "--force", stdout[0:12]).Run(&test.Expected{
ExitCode: 0,
})
helpers.Command("images").Run(&test.Expected{
Output: func(stdout string, info string, t *testing.T) {
lines := strings.Split(strings.TrimSpace(stdout), "\n")
assert.Assert(t, len(lines) == numNoTags, info)
},
})
},
}
},
},
{
Description: "Test remove image with digestID",
NoParallel: true,
Setup: func(data test.Data, helpers test.Helpers) {
helpers.Ensure("pull", testutil.BusyboxImage)
helpers.Ensure("tag", testutil.BusyboxImage, data.Identifier())
},
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
return helpers.Command("--kube-hide-dupe", "images", testutil.BusyboxImage, "-q", "--no-trunc")
},
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
return &test.Expected{
Output: func(stdout string, info string, t *testing.T) {
imgID := strings.Split(stdout, "\n")
helpers.Command("--kube-hide-dupe", "rmi", imgID[0]).Run(&test.Expected{
ExitCode: 1,
Errors: []error{errors.New("multiple IDs found with provided prefix: ")},
})
helpers.Command("--kube-hide-dupe", "rmi", "--force", imgID[0]).Run(&test.Expected{
ExitCode: 0,
})
helpers.Command("images").Run(&test.Expected{
Output: func(stdout string, info string, t *testing.T) {
lines := strings.Split(strings.TrimSpace(stdout), "\n")
assert.Assert(t, len(lines) == numNoTags, info)
},
})
},
}
},
},
}
testCase.Run(t)
}
1 change: 1 addition & 0 deletions cmd/nerdctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ func initRootCmdFlags(rootCmd *cobra.Command, tomlPath string) (*pflag.FlagSet,
helpers.AddPersistentBoolFlag(rootCmd, "experimental", nil, nil, cfg.Experimental, "NERDCTL_EXPERIMENTAL", "Control experimental: https://github.com/containerd/nerdctl/blob/main/docs/experimental.md")
helpers.AddPersistentStringFlag(rootCmd, "host-gateway-ip", nil, nil, nil, aliasToBeInherited, cfg.HostGatewayIP, "NERDCTL_HOST_GATEWAY_IP", "IP address that the special 'host-gateway' string in --add-host resolves to. Defaults to the IP address of the host. It has no effect without setting --add-host")
helpers.AddPersistentStringFlag(rootCmd, "bridge-ip", nil, nil, nil, aliasToBeInherited, cfg.BridgeIP, "NERDCTL_BRIDGE_IP", "IP address for the default nerdctl bridge network")
rootCmd.PersistentFlags().Bool("kube-hide-dupe", cfg.KubeHideDupe, "Deduplicate images for Kubernetes with namespace k8s.io")
return aliasToBeInherited, nil
}

Expand Down
1 change: 1 addition & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ experimental = true
| `experimental` | `--experimental` | `NERDCTL_EXPERIMENTAL` | Enable [experimental features](experimental.md) | Since 0.22.3 |
| `host_gateway_ip` | `--host-gateway-ip` | `NERDCTL_HOST_GATEWAY_IP` | IP address that the special 'host-gateway' string in --add-host resolves to. Defaults to the IP address of the host. It has no effect without setting --add-host | Since 1.3.0 |
| `bridge_ip` | `--bridge-ip` | `NERDCTL_BRIDGE_IP` | IP address for the default nerdctl bridge network, e.g., 10.1.100.1/24 | Since 2.0.1 |
| `kube-hide-dupe` | `--kube-hide-dupe` | | Deduplicate images for Kubernetes with namespace k8s.io, no more redundant <none> ones are displayed | Since 2.0.3 |

The properties are parsed in the following precedence:
1. CLI flag
Expand Down
44 changes: 43 additions & 1 deletion pkg/cmd/image/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"time"

"github.com/docker/go-units"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/identity"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"

Expand All @@ -44,6 +45,7 @@ import (
"github.com/containerd/nerdctl/v2/pkg/containerdutil"
"github.com/containerd/nerdctl/v2/pkg/formatter"
"github.com/containerd/nerdctl/v2/pkg/imgutil"
"github.com/containerd/nerdctl/v2/pkg/referenceutil"
)

// ListCommandHandler `List` and print images matching filters in `options`.
Expand Down Expand Up @@ -128,6 +130,46 @@ type imagePrintable struct {

func printImages(ctx context.Context, client *containerd.Client, imageList []images.Image, options *types.ImageListOptions) error {
w := options.Stdout
var finalImageList []images.Image
/*
the same imageId under k8s.io is showing multiple results: repo:tag, repo:digest, configID.
We expect to display only repo:tag, consistent with other namespaces and CRI
e.g.
nerdctl -n k8s.io images
REPOSITORY TAG IMAGE ID CREATED PLATFORM SIZE BLOB SIZE
centos 7 be65f488b776 3 hours ago linux/amd64 211.5 MiB 72.6 MiB
centos <none> be65f488b776 3 hours ago linux/amd64 211.5 MiB 72.6 MiB
<none> <none> be65f488b776 3 hours ago linux/amd64 211.5 MiB 72.6 MiB
expect:
nerdctl --kube-hide-dupe -n k8s.io images
REPOSITORY TAG IMAGE ID CREATED PLATFORM SIZE BLOB SIZE
centos 7 be65f488b776 3 hours ago linux/amd64 211.5 MiB 72.6 MiB
*/
if options.GOptions.KubeHideDupe && options.GOptions.Namespace == "k8s.io" {
imageDigest := make(map[digest.Digest]bool)
var imageNoTag []images.Image
for _, img := range imageList {
parsed, err := referenceutil.Parse(img.Name)
if err != nil {
continue
}
if parsed.Tag != "" {
finalImageList = append(finalImageList, img)
imageDigest[img.Target.Digest] = true
continue
}
imageNoTag = append(imageNoTag, img)
}
//Ensure that dangling images without a repo:tag are displayed correctly.
for _, ima := range imageNoTag {
if !imageDigest[ima.Target.Digest] {
finalImageList = append(finalImageList, ima)
imageDigest[ima.Target.Digest] = true
}
}
} else {
finalImageList = imageList
}
digestsFlag := options.Digests
if options.Format == "wide" {
digestsFlag = true
Expand Down Expand Up @@ -174,7 +216,7 @@ func printImages(ctx context.Context, client *containerd.Client, imageList []ima
snapshotter: containerdutil.SnapshotService(client, options.GOptions.Snapshotter),
}

for _, img := range imageList {
for _, img := range finalImageList {
if err := printer.printImage(ctx, img); err != nil {
log.G(ctx).Warn(err)
}
Expand Down
Loading

0 comments on commit 01269d2

Please sign in to comment.