Skip to content

Commit 781eeff

Browse files
authored
Merge pull request #4447 from ChengyuZhu6/manifest-rm
manifest: support nerdctl manifest rm command
2 parents c6e2f70 + 8690f73 commit 781eeff

File tree

8 files changed

+217
-0
lines changed

8 files changed

+217
-0
lines changed

cmd/nerdctl/manifest/manifest.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ func Command() *cobra.Command {
3636
InspectCommand(),
3737
CreateCommand(),
3838
AnnotateCommand(),
39+
RemoveCommand(),
3940
)
4041

4142
return cmd
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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 manifest
18+
19+
import (
20+
"errors"
21+
22+
"github.com/spf13/cobra"
23+
24+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
25+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
26+
"github.com/containerd/nerdctl/v2/pkg/cmd/manifest"
27+
)
28+
29+
func RemoveCommand() *cobra.Command {
30+
var cmd = &cobra.Command{
31+
Use: "rm INDEX/MANIFESTLIST [INDEX/MANIFESTLIST...]",
32+
Short: "Remove one or more index/manifest lists",
33+
Args: cobra.MinimumNArgs(1),
34+
RunE: removeAction,
35+
ValidArgsFunction: removeShellComplete,
36+
SilenceUsage: true,
37+
SilenceErrors: true,
38+
}
39+
return cmd
40+
}
41+
42+
func removeAction(cmd *cobra.Command, refs []string) error {
43+
globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
44+
if err != nil {
45+
return err
46+
}
47+
var errs []error
48+
for _, ref := range refs {
49+
err := manifest.Remove(cmd.Context(), ref, globalOptions)
50+
if err != nil {
51+
errs = append(errs, err)
52+
}
53+
}
54+
return errors.Join(errs...)
55+
}
56+
57+
func removeShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
58+
return completion.ImageNames(cmd)
59+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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 manifest
18+
19+
import (
20+
"errors"
21+
"testing"
22+
23+
"github.com/containerd/nerdctl/mod/tigron/test"
24+
25+
"github.com/containerd/nerdctl/v2/pkg/testutil"
26+
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
27+
)
28+
29+
func TestManifestsRemove(t *testing.T) {
30+
testCase := nerdtest.Setup()
31+
manifestListName1 := "example.com/test-list-remove:v1"
32+
manifestListName2 := "example.com/test-list-remove:v2"
33+
manifestRef1 := testutil.GetTestImageWithoutTag("alpine") + "@" + testutil.GetTestImageManifestDigest("alpine", "linux/amd64")
34+
manifestRef2 := testutil.GetTestImageWithoutTag("alpine") + "@" + testutil.GetTestImageManifestDigest("alpine", "linux/arm64")
35+
36+
testCase.SubTests = []*test.Case{
37+
{
38+
Description: "remove-several-manifestlists",
39+
Setup: func(data test.Data, helpers test.Helpers) {
40+
cmd := helpers.Command("manifest", "create", manifestListName1, manifestRef1)
41+
cmd.Run(&test.Expected{ExitCode: 0})
42+
cmd = helpers.Command("manifest", "create", manifestListName2, manifestRef2)
43+
cmd.Run(&test.Expected{ExitCode: 0})
44+
},
45+
Command: test.Command("manifest", "rm", manifestListName1, manifestListName2),
46+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
47+
return &test.Expected{
48+
ExitCode: 0,
49+
}
50+
},
51+
},
52+
{
53+
Description: "remove-non-existent-manifestlist",
54+
Command: test.Command("manifest", "rm", "example.com/non-existent:latest"),
55+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
56+
return &test.Expected{
57+
ExitCode: 1,
58+
Errors: []error{errors.New(data.Labels().Get("error"))},
59+
}
60+
},
61+
Data: test.WithLabels(map[string]string{
62+
"error": "No such manifest: example.com/non-existent:latest",
63+
}),
64+
},
65+
}
66+
67+
testCase.Run(t)
68+
}

docs/command-reference.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ It does not necessarily mean that the corresponding features are missing in cont
5555
- [:whale: nerdctl manifest annotate](#whale-nerdctl-manifest-annotate)
5656
- [:whale: nerdctl manifest create](#whale-nerdctl-manifest-create)
5757
- [:whale: nerdctl manifest inspect](#whale-nerdctl-manifest-inspect)
58+
- [:whale: nerdctl manifest rm](#whale-nerdctl-manifest-rm)
5859
- [Registry](#registry)
5960
- [:whale: nerdctl login](#whale-nerdctl-login)
6061
- [:whale: nerdctl logout](#whale-nerdctl-logout)
@@ -1102,6 +1103,18 @@ nerdctl manifest inspect alpine:3.22.1
11021103
nerdctl manifest inspect alpine@sha256:eafc1edb577d2e9b458664a15f23ea1c370214193226069eb22921169fc7e43f
11031104
```
11041105

1106+
### :whale: nerdctl manifest rm
1107+
1108+
Remove one or more index/manifest lists.
1109+
1110+
Usage: `nerdctl manifest rm INDEX/MANIFESTLIST [INDEX/MANIFESTLIST...]`
1111+
1112+
Example:
1113+
1114+
```bash
1115+
nerdctl manifest rm alpine:3.22.1 alpine:3.22.2
1116+
```
1117+
11051118
## Registry
11061119

11071120
### :whale: nerdctl login

pkg/cmd/manifest/rm.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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 manifest
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"strings"
23+
24+
"github.com/containerd/nerdctl/v2/pkg/api/types"
25+
"github.com/containerd/nerdctl/v2/pkg/manifeststore"
26+
"github.com/containerd/nerdctl/v2/pkg/manifestutil"
27+
"github.com/containerd/nerdctl/v2/pkg/referenceutil"
28+
)
29+
30+
func Remove(ctx context.Context, ref string, options types.GlobalCommandOptions) error {
31+
parsedRef, err := referenceutil.Parse(ref)
32+
if err != nil {
33+
return fmt.Errorf("failed to parse reference: %w", err)
34+
}
35+
manifestStore, err := manifeststore.NewStore(options.DataRoot)
36+
if err != nil {
37+
return fmt.Errorf("failed to create manifest store: %w", err)
38+
}
39+
_, err = manifestStore.GetList(parsedRef)
40+
if err != nil {
41+
if strings.Contains(err.Error(), "not found") {
42+
return manifestutil.NewNoSuchManifestError(parsedRef.String())
43+
}
44+
return err
45+
}
46+
err = manifestStore.Remove(parsedRef)
47+
if err != nil {
48+
return fmt.Errorf("failed to remove manifest list: %w", err)
49+
}
50+
return nil
51+
}

pkg/manifeststore/manifeststore.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ type Store interface {
3333
GetList(listRef *referenceutil.ImageReference) ([]*manifesttypes.DockerManifestEntry, error)
3434
// Save saves a manifest as part of a index or local manifest list
3535
Save(listRef, manifestRef *referenceutil.ImageReference, manifest *manifesttypes.DockerManifestEntry) error
36+
// Remove removes a index or local manifest list
37+
Remove(listRef *referenceutil.ImageReference) error
3638
}
3739

3840
type manifestStore struct {
@@ -103,6 +105,13 @@ func (s *manifestStore) Save(listRef, manifestRef *referenceutil.ImageReference,
103105
})
104106
}
105107

108+
func (s *manifestStore) Remove(listRef *referenceutil.ImageReference) error {
109+
return s.store.WithLock(func() error {
110+
listPath := makeFilesafeName(listRef.String())
111+
return s.store.Delete(listPath)
112+
})
113+
}
114+
106115
func (s *manifestStore) getManifestFromPath(listPath, manifestPath string) (*manifesttypes.DockerManifestEntry, error) {
107116
data, err := s.store.Get(listPath, manifestPath)
108117
if err != nil {

pkg/manifestutil/manifestutils.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,20 @@ var manifestParsers = map[string]manifestParser{
4545
ocispec.MediaTypeImageIndex: parseOCIIndex,
4646
}
4747

48+
// NoSuchManifestError represents an error when a manifest is not found
49+
type NoSuchManifestError struct {
50+
Ref string
51+
}
52+
53+
func (e *NoSuchManifestError) Error() string {
54+
return fmt.Sprintf("No such manifest: %s", e.Ref)
55+
}
56+
57+
// NewNoSuchManifestError creates a new NoSuchManifestError
58+
func NewNoSuchManifestError(ref string) error {
59+
return &NoSuchManifestError{Ref: ref}
60+
}
61+
4862
// ParseManifest parses manifest data based on media type
4963
func ParseManifest(mediaType string, data []byte) (interface{}, error) {
5064
if parser, exists := manifestParsers[mediaType]; exists {

pkg/testutil/images.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ alpine:
1515
manifest: "sha256:e103c1b4bf019dc290bcc7aca538dc2bf7a9d0fc836e186f5fa34945c5168310"
1616
config: "sha256:49f356fa4513676c5e22e3a8404aad6c7262cc7aaed15341458265320786c58c"
1717
raw: "ewogICAic2NoZW1hVmVyc2lvbiI6IDIsCiAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLmRvY2tlci5kaXN0cmlidXRpb24ubWFuaWZlc3QudjIranNvbiIsCiAgICJjb25maWciOiB7CiAgICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLmRvY2tlci5jb250YWluZXIuaW1hZ2UudjEranNvbiIsCiAgICAgICJzaXplIjogMTQ3MiwKICAgICAgImRpZ2VzdCI6ICJzaGEyNTY6NDlmMzU2ZmE0NTEzNjc2YzVlMjJlM2E4NDA0YWFkNmM3MjYyY2M3YWFlZDE1MzQxNDU4MjY1MzIwNzg2YzU4YyIKICAgfSwKICAgImxheWVycyI6IFsKICAgICAgewogICAgICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLAogICAgICAgICAic2l6ZSI6IDI4MTE5NDcsCiAgICAgICAgICJkaWdlc3QiOiAic2hhMjU2OmNhM2NkNDJhN2M5NTI1ZjZjZTNkNjRjMWE3MDk4MjYxM2E4MjM1ZjBjYzA1N2VjOTI0NDA1MjkyMTg1M2VmMTUiCiAgICAgIH0KICAgXQp9"
18+
linux/arm64:
19+
manifest: "sha256:071fa5de01a240dbef5be09d69f8fef2f89d68445d9175393773ee389b6f5935"
1820

1921
busybox:
2022
ref: "ghcr.io/containerd/busybox"

0 commit comments

Comments
 (0)