Skip to content

Commit 4d459d2

Browse files
committed
cmd: support nerdctl manifeset inspect
Signed-off-by: ChengyuZhu6 <hudson@cyzhu.com>
1 parent 38596c8 commit 4d459d2

File tree

11 files changed

+383
-5
lines changed

11 files changed

+383
-5
lines changed

cmd/nerdctl/image/image_inspect.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import (
3434
func inspectCommand() *cobra.Command {
3535
cmd := &cobra.Command{
3636
Use: "inspect [flags] IMAGE [IMAGE...]",
37-
Args: cobra.MinimumNArgs(1),
37+
Args: helpers.IsExactArgs(1),
3838
Short: "Display detailed information on one or more images.",
3939
Long: "Hint: set `--mode=native` for showing the full output",
4040
RunE: imageInspectAction,

cmd/nerdctl/main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import (
4040
"github.com/containerd/nerdctl/v2/cmd/nerdctl/internal"
4141
"github.com/containerd/nerdctl/v2/cmd/nerdctl/ipfs"
4242
"github.com/containerd/nerdctl/v2/cmd/nerdctl/login"
43+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/manifest"
4344
"github.com/containerd/nerdctl/v2/cmd/nerdctl/namespace"
4445
"github.com/containerd/nerdctl/v2/cmd/nerdctl/network"
4546
"github.com/containerd/nerdctl/v2/cmd/nerdctl/system"
@@ -344,6 +345,10 @@ Config file ($NERDCTL_TOML): %s
344345

345346
// IPFS
346347
ipfs.NewIPFSCommand(),
348+
349+
// Manifest
350+
manifest.Command(),
351+
manifest.InspectCommand(),
347352
)
348353
addApparmorCommand(rootCmd)
349354
container.AddCpCommand(rootCmd)

cmd/nerdctl/manifest/manifest.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
21+
"github.com/spf13/cobra"
22+
)
23+
24+
func Command() *cobra.Command {
25+
cmd := &cobra.Command{
26+
Annotations: map[string]string{helpers.Category: helpers.Management},
27+
Use: "manifest",
28+
Short: "Manage image manifests and manifest lists.",
29+
RunE: helpers.UnknownSubcommandAction,
30+
SilenceUsage: true,
31+
SilenceErrors: true,
32+
}
33+
34+
cmd.AddCommand(
35+
InspectCommand(),
36+
)
37+
38+
return cmd
39+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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+
"github.com/containerd/log"
21+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
22+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
23+
"github.com/containerd/nerdctl/v2/pkg/api/types"
24+
"github.com/containerd/nerdctl/v2/pkg/clientutil"
25+
"github.com/containerd/nerdctl/v2/pkg/cmd/manifest"
26+
"github.com/containerd/nerdctl/v2/pkg/formatter"
27+
"github.com/spf13/cobra"
28+
)
29+
30+
func InspectCommand() *cobra.Command {
31+
var cmd = &cobra.Command{
32+
Use: "inspect [OPTIONS] [MANIFEST_LIST] MANIFEST",
33+
Short: "Display the contents of a manifest list or manifest",
34+
Args: cobra.MinimumNArgs(1),
35+
RunE: inspectAction,
36+
ValidArgsFunction: inspectShellComplete,
37+
SilenceUsage: true,
38+
SilenceErrors: true,
39+
}
40+
cmd.Flags().BoolP("insecure", "i", false, "Allow insecure TLS connections to the registry")
41+
cmd.Flags().BoolP("verbose", "v", false, "Verbose output additional info including layers and platform")
42+
return cmd
43+
}
44+
45+
func processInspectFlags(cmd *cobra.Command) (types.ManifestInspectOptions, error) {
46+
globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
47+
if err != nil {
48+
return types.ManifestInspectOptions{}, err
49+
}
50+
insecure, err := cmd.Flags().GetBool("insecure")
51+
if err != nil {
52+
return types.ManifestInspectOptions{}, err
53+
}
54+
verbose, err := cmd.Flags().GetBool("verbose")
55+
if err != nil {
56+
return types.ManifestInspectOptions{}, err
57+
}
58+
return types.ManifestInspectOptions{
59+
Stdout: cmd.OutOrStdout(),
60+
GOptions: globalOptions,
61+
Insecure: insecure,
62+
Verbose: verbose,
63+
}, nil
64+
}
65+
66+
func inspectAction(cmd *cobra.Command, args []string) error {
67+
inspectOptions, err := processInspectFlags(cmd)
68+
if err != nil {
69+
return err
70+
}
71+
rawRef := args[0]
72+
73+
client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), inspectOptions.GOptions.Namespace, inspectOptions.GOptions.Address)
74+
if err != nil {
75+
return err
76+
}
77+
defer cancel()
78+
79+
res, err := manifest.Inspect(ctx, client, rawRef, inspectOptions)
80+
if err != nil {
81+
return err
82+
}
83+
if formatErr := formatter.FormatSlice("", inspectOptions.Stdout, res); formatErr != nil {
84+
log.G(ctx).Error(formatErr)
85+
}
86+
return nil
87+
}
88+
89+
func inspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
90+
return completion.ImageNames(cmd)
91+
}

pkg/api/types/manifest_types.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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 types
18+
19+
import "io"
20+
21+
// ManifestInspectOptions specifies options for `nerdctl manifest inspect`.
22+
type ManifestInspectOptions struct {
23+
Stdout io.Writer
24+
GOptions GlobalCommandOptions
25+
// Insecure allows insecure TLS connections to the registry.
26+
Insecure bool
27+
}

pkg/cmd/image/inspect.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,12 @@ import (
3131
"github.com/containerd/nerdctl/v2/pkg/api/types"
3232
"github.com/containerd/nerdctl/v2/pkg/containerdutil"
3333
"github.com/containerd/nerdctl/v2/pkg/imageinspector"
34+
"github.com/containerd/nerdctl/v2/pkg/imgutil"
3435
"github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat"
3536
"github.com/containerd/nerdctl/v2/pkg/referenceutil"
3637
)
3738

38-
func inspectIdentifier(ctx context.Context, client *containerd.Client, identifier string) ([]images.Image, string, string, error) {
39+
func InspectIdentifier(ctx context.Context, client *containerd.Client, identifier string) ([]images.Image, string, string, error) {
3940
// Figure out what we have here - digest, tag, name
4041
parsedReference, err := referenceutil.Parse(identifier)
4142
if err != nil {
@@ -71,14 +72,20 @@ func inspectIdentifier(ctx context.Context, client *containerd.Client, identifie
7172
digest = imageList[0].Target.Digest.String()
7273
}
7374
}
74-
7575
// At this point, we DO have a digest (or short id), so, that is what we are retrieving
7676
filters = []string{fmt.Sprintf("target.digest~=^%s$", digest)}
7777
imageList, err = client.ImageService().List(ctx, filters...)
7878
if err != nil {
7979
return nil, "", "", fmt.Errorf("containerd image service failed: %w", err)
8080
}
8181

82+
// We do have a manifest digest, so we need to find the image that contains it
83+
if len(imageList) == 0 && digest != "" {
84+
img, err := imgutil.FindImageByManifestDigest(ctx, client, digest)
85+
if err == nil && img != nil {
86+
imageList = append(imageList, *img)
87+
}
88+
}
8289
// TODO: docker does allow retrieving images by Id, so implement as a last ditch effort (probably look-up the store)
8390

8491
// Return the list we found, along with normalized name and tag
@@ -98,7 +105,7 @@ func Inspect(ctx context.Context, client *containerd.Client, identifiers []strin
98105
snapshotter := containerdutil.SnapshotService(client, options.GOptions.Snapshotter)
99106
// We have to query per provided identifier, as we need to post-process results for the case name + digest
100107
for _, identifier := range identifiers {
101-
candidateImageList, requestedName, requestedTag, err := inspectIdentifier(ctx, client, identifier)
108+
candidateImageList, requestedName, requestedTag, err := InspectIdentifier(ctx, client, identifier)
102109
if err != nil {
103110
errs = append(errs, fmt.Errorf("%w: %s", err, identifier))
104111
continue

pkg/cmd/manifest/inspect.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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+
"errors"
22+
"fmt"
23+
24+
containerd "github.com/containerd/containerd/v2/client"
25+
"github.com/containerd/nerdctl/v2/pkg/api/types"
26+
imageinspect "github.com/containerd/nerdctl/v2/pkg/cmd/image"
27+
"github.com/containerd/nerdctl/v2/pkg/containerdutil"
28+
"github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
29+
"github.com/containerd/nerdctl/v2/pkg/manifestinspector"
30+
"github.com/containerd/nerdctl/v2/pkg/referenceutil"
31+
)
32+
33+
func Inspect(ctx context.Context, client *containerd.Client, rawRef string, options types.ManifestInspectOptions) ([]any, error) {
34+
manifest, err := getManifest(ctx, client, rawRef, options)
35+
if err != nil {
36+
return nil, err
37+
}
38+
return manifest, nil
39+
}
40+
41+
func getManifest(ctx context.Context, client *containerd.Client, rawRef string, options types.ManifestInspectOptions) ([]any, error) {
42+
var errs []error
43+
var entries []interface{}
44+
snapshotter := containerdutil.SnapshotService(client, options.GOptions.Snapshotter)
45+
46+
candidateImageList, _, _, err := imageinspect.InspectIdentifier(ctx, client, rawRef)
47+
if err != nil {
48+
errs = append(errs, fmt.Errorf("%w: %s", err, rawRef))
49+
return nil, err
50+
}
51+
for _, candidateImage := range candidateImageList {
52+
entry, err := manifestinspector.Inspect(ctx, client, rawRef, candidateImage, snapshotter)
53+
if err != nil {
54+
errs = append(errs, fmt.Errorf("%w: %s", err, candidateImage.Name))
55+
continue
56+
}
57+
entry = filterEntries(entry, rawRef)
58+
if entry.Index != nil {
59+
entries = append(entries, entry.Index)
60+
} else {
61+
entries = append(entries, entry.Manifest)
62+
}
63+
}
64+
if len(errs) > 0 {
65+
return []any{}, fmt.Errorf("%d errors:\n%w", len(errs), errors.Join(errs...))
66+
}
67+
68+
if len(entries) == 0 {
69+
return []any{}, fmt.Errorf("no manifest found for %s", rawRef)
70+
}
71+
72+
return entries, nil
73+
}
74+
75+
func filterEntries(entries *native.Manifest, rawRef string) *native.Manifest {
76+
parsedReference, err := referenceutil.Parse(rawRef)
77+
if err != nil {
78+
return nil
79+
}
80+
digest := ""
81+
if parsedReference.Digest != "" {
82+
digest = parsedReference.Digest.String()
83+
}
84+
if digest == "" {
85+
return entries
86+
}
87+
if entries.Index != nil && digest != entries.IndexDesc.Digest.String() {
88+
entries.Index = nil
89+
entries.IndexDesc = nil
90+
}
91+
return entries
92+
}

pkg/imageinspector/imageinspector.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ func Inspect(ctx context.Context, client *containerd.Client, image images.Image,
4949
n.ManifestDesc = maniDesc
5050
n.Manifest = mani
5151
}
52-
5352
imageConfig, imageConfigDesc, err := imgutil.ReadImageConfig(ctx, img)
5453
if err != nil {
5554
log.G(ctx).WithError(err).WithField("id", image.Name).Warnf("failed to inspect image config")

pkg/imgutil/imgutil.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import (
3939
"github.com/containerd/platforms"
4040

4141
"github.com/containerd/nerdctl/v2/pkg/api/types"
42+
"github.com/containerd/nerdctl/v2/pkg/containerdutil"
4243
"github.com/containerd/nerdctl/v2/pkg/errutil"
4344
"github.com/containerd/nerdctl/v2/pkg/healthcheck"
4445
"github.com/containerd/nerdctl/v2/pkg/idutil/imagewalker"
@@ -498,3 +499,37 @@ func addHealthCheckToImageConfig(rawConfigContent []byte, config *ocispec.ImageC
498499
}
499500
return nil
500501
}
502+
503+
func FindImageByManifestDigest(
504+
ctx context.Context,
505+
client *containerd.Client,
506+
targetDigest string,
507+
) (*images.Image, error) {
508+
imageList, err := client.ImageService().List(ctx)
509+
if err != nil {
510+
return nil, err
511+
}
512+
provider := containerdutil.NewProvider(client)
513+
for _, img := range imageList {
514+
desc := img.Target
515+
if images.IsIndexType(desc.MediaType) {
516+
indexData, err := containerdutil.ReadBlob(ctx, provider, desc)
517+
if err != nil {
518+
continue
519+
}
520+
var index ocispec.Index
521+
if err := json.Unmarshal(indexData, &index); err != nil {
522+
continue
523+
}
524+
for _, mani := range index.Manifests {
525+
if mani.Digest.String() == targetDigest {
526+
return &img, nil
527+
}
528+
}
529+
}
530+
if images.IsManifestType(desc.MediaType) && desc.Digest.String() == targetDigest {
531+
return &img, nil
532+
}
533+
}
534+
return nil, fmt.Errorf("no image found for manifest digest %s", targetDigest)
535+
}

0 commit comments

Comments
 (0)