@@ -19,58 +19,182 @@ package image
19
19
import (
20
20
"context"
21
21
"fmt"
22
+ "regexp"
23
+ "strings"
22
24
"time"
23
25
24
26
"github.com/containerd/containerd"
27
+ "github.com/containerd/containerd/images"
25
28
"github.com/containerd/log"
26
29
"github.com/containerd/nerdctl/v2/pkg/api/types"
27
30
"github.com/containerd/nerdctl/v2/pkg/formatter"
28
- "github.com/containerd/nerdctl/v2/pkg/idutil/imagewalker"
29
31
"github.com/containerd/nerdctl/v2/pkg/imageinspector"
30
32
"github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat"
33
+ "github.com/containerd/nerdctl/v2/pkg/referenceutil"
34
+ "github.com/distribution/reference"
31
35
)
32
36
37
+ func inspectIdentifier (ctx context.Context , client * containerd.Client , identifier string ) ([]images.Image , string , string , error ) {
38
+ // Figure out what we have here - digest, tag, name
39
+ parsedIdentifier , err := referenceutil .ParseAnyReference (identifier )
40
+ if err != nil {
41
+ return nil , "" , "" , fmt .Errorf ("invalid identifier %s: %w" , identifier , err )
42
+ }
43
+ digest := ""
44
+ if identifierDigest , hasDigest := parsedIdentifier .(reference.Digested ); hasDigest {
45
+ digest = identifierDigest .Digest ().String ()
46
+ }
47
+ name := ""
48
+ if identifierName , hasName := parsedIdentifier .(reference.Named ); hasName {
49
+ name = identifierName .Name ()
50
+ }
51
+ tag := "latest"
52
+ if identifierTag , hasTag := parsedIdentifier .(reference.Tagged ); hasTag && identifierTag .Tag () != "" {
53
+ tag = identifierTag .Tag ()
54
+ }
55
+
56
+ // Initialize filters
57
+ var filters []string
58
+ // This will hold the final image list, if any
59
+ var imageList []images.Image
60
+
61
+ // No digest in the request? Then assume it is a name
62
+ if digest == "" {
63
+ filters = []string {fmt .Sprintf ("name==%s:%s" , name , tag )}
64
+ // Query it
65
+ imageList , err = client .ImageService ().List (ctx , filters ... )
66
+ if err != nil {
67
+ return nil , "" , "" , fmt .Errorf ("containerd image service failed: %w" , err )
68
+ }
69
+ // Nothing? Then it could be a short id (aka truncated digest) - we are going to use this
70
+ if len (imageList ) == 0 {
71
+ digest = fmt .Sprintf ("sha256:%s.*" , regexp .QuoteMeta (strings .TrimPrefix (identifier , "sha256:" )))
72
+ name = ""
73
+ tag = ""
74
+ } else {
75
+ // Otherwise, we found one by name. Get the digest from it.
76
+ digest = imageList [0 ].Target .Digest .String ()
77
+ }
78
+ }
79
+
80
+ // At this point, we DO have a digest (or short id), so, that is what we are retrieving
81
+ filters = []string {fmt .Sprintf ("target.digest~=^%s$" , digest )}
82
+ imageList , err = client .ImageService ().List (ctx , filters ... )
83
+ if err != nil {
84
+ return nil , "" , "" , fmt .Errorf ("containerd image service failed: %w" , err )
85
+ }
86
+
87
+ // TODO: docker does allow retrieving images by Id, so implement as a last ditch effort (probably look-up the store)
88
+
89
+ // Return the list we found, along with normalized name and tag
90
+ return imageList , name , tag , nil
91
+ }
92
+
33
93
// Inspect prints detailed information of each image in `images`.
34
- func Inspect (ctx context.Context , client * containerd.Client , images []string , options types.ImageInspectOptions ) error {
35
- f := & imageInspector {
36
- mode : options .Mode ,
94
+ func Inspect (ctx context.Context , client * containerd.Client , identifiers []string , options types.ImageInspectOptions ) error {
95
+ // Verify we have a valid mode
96
+ // TODO: move this out of here, to Cobra command line arg validation
97
+ if options .Mode != "native" && options .Mode != "dockercompat" {
98
+ return fmt .Errorf ("unknown mode %q" , options .Mode )
37
99
}
38
- walker := & imagewalker.ImageWalker {
39
- Client : client ,
40
- OnFound : func (ctx context.Context , found imagewalker.Found ) error {
41
- ctx , cancel := context .WithTimeout (ctx , 5 * time .Second )
42
- defer cancel ()
43
100
44
- n , err := imageinspector .Inspect (ctx , client , found .Image , options .GOptions .Snapshotter )
101
+ // Set a timeout
102
+ ctx , cancel := context .WithTimeout (ctx , 5 * time .Second )
103
+ defer cancel ()
104
+
105
+ // Will hold the final answers
106
+ var entries []interface {}
107
+
108
+ // We have to query per provided identifier, as we need to post-process results for the case name + digest
109
+ for _ , identifier := range identifiers {
110
+ candidateImageList , requestedName , requestedTag , err := inspectIdentifier (ctx , client , identifier )
111
+ if err != nil {
112
+ log .G (ctx ).WithError (err ).WithField ("identifier" , identifier ).Error ("failure calling inspect" )
113
+ continue
114
+ }
115
+
116
+ var validatedImage * dockercompat.Image
117
+ var repoTags []string
118
+ var repoDigests []string
119
+
120
+ // Go through the candidates
121
+ for _ , candidateImage := range candidateImageList {
122
+ // Inspect the image
123
+ candidateNativeImage , err := imageinspector .Inspect (ctx , client , candidateImage , options .GOptions .Snapshotter )
45
124
if err != nil {
46
- return err
125
+ log .G (ctx ).WithError (err ).WithField ("name" , candidateImage .Name ).Error ("failure inspecting image" )
126
+ continue
47
127
}
48
- switch f .mode {
49
- case "native" :
50
- f .entries = append (f .entries , n )
51
- case "dockercompat" :
52
- d , err := dockercompat .ImageFromNative (n )
128
+
129
+ // If native, we just add everything in there and that's it
130
+ if options .Mode == "native" {
131
+ entries = append (entries , candidateNativeImage )
132
+ continue
133
+ }
134
+
135
+ // If dockercompat: does the candidate have a name? Get it if so
136
+ candidateRef , err := referenceutil .ParseAnyReference (candidateNativeImage .Image .Name )
137
+ if err != nil {
138
+ log .G (ctx ).WithError (err ).WithField ("name" , candidateNativeImage .Image .Name ).Error ("the found image has an unparsable name" )
139
+ continue
140
+ }
141
+ parsedCandidateNameTag , candidateHasAName := candidateRef .(reference.NamedTagged )
142
+
143
+ // If we were ALSO asked for a specific name on top of the digest, we need to make sure we keep only the image with that name
144
+ if requestedName != "" {
145
+ // If the candidate did not have a name, then we should ignore this one and continue
146
+ if ! candidateHasAName {
147
+ continue
148
+ }
149
+
150
+ // Otherwise, the candidate has a name. If it is the one we want, store it and continue, otherwise, fall through
151
+ candidateTag := parsedCandidateNameTag .Tag ()
152
+ if candidateTag == "" {
153
+ candidateTag = "latest"
154
+ }
155
+ if parsedCandidateNameTag .Name () == requestedName && candidateTag == requestedTag {
156
+ validatedImage , err = dockercompat .ImageFromNative (candidateNativeImage )
157
+ if err != nil {
158
+ log .G (ctx ).WithError (err ).WithField ("name" , candidateNativeImage .Image .Name ).Error ("could not get a docker compat version of the native image" )
159
+ }
160
+ continue
161
+ }
162
+ } else if validatedImage == nil {
163
+ // Alternatively, we got a request by digest only, so, if we do not know about it already, store it and continue
164
+ validatedImage , err = dockercompat .ImageFromNative (candidateNativeImage )
53
165
if err != nil {
54
- return err
166
+ log . G ( ctx ). WithError ( err ). WithField ( "name" , candidateNativeImage . Image . Name ). Error ( "could not get a docker compat version of the native image" )
55
167
}
56
- f .entries = append (f .entries , d )
57
- default :
58
- return fmt .Errorf ("unknown mode %q" , f .mode )
168
+ continue
169
+ }
170
+
171
+ // Fallthrough cases:
172
+ // - we got a request by digest, but we already had the image stored
173
+ // - we got a request by name, and the name of the candidate did not match the requested name
174
+ // Now, check if the candidate has a name - if it does, populate repoTags and repoDigests
175
+ if candidateHasAName {
176
+ repoTags = append (repoTags , fmt .Sprintf ("%s:%s" , reference .FamiliarName (parsedCandidateNameTag ), parsedCandidateNameTag .Tag ()))
177
+ repoDigests = append (repoDigests , fmt .Sprintf ("%s@%s" , reference .FamiliarName (parsedCandidateNameTag ), candidateImage .Target .Digest .String ()))
59
178
}
60
- return nil
61
- },
179
+ }
180
+
181
+ // Done iterating through candidates. Did we find anything that matches?
182
+ if validatedImage != nil {
183
+ // Then slap in the repoTags and repoDigests we found from the other candidates
184
+ validatedImage .RepoTags = append (validatedImage .RepoTags , repoTags ... )
185
+ validatedImage .RepoDigests = append (validatedImage .RepoDigests , repoDigests ... )
186
+ // Store our image
187
+ // foundImages[validatedDigest] = validatedImage
188
+ entries = append (entries , validatedImage )
189
+ }
62
190
}
63
191
64
- err := walker . WalkAll ( ctx , images , true )
65
- if len (f . entries ) > 0 {
66
- if formatErr := formatter .FormatSlice (options .Format , options .Stdout , f . entries ); formatErr != nil {
192
+ // Display
193
+ if len (entries ) > 0 {
194
+ if formatErr := formatter .FormatSlice (options .Format , options .Stdout , entries ); formatErr != nil {
67
195
log .G (ctx ).Error (formatErr )
68
196
}
69
197
}
70
- return err
71
- }
72
198
73
- type imageInspector struct {
74
- mode string
75
- entries []interface {}
199
+ return nil
76
200
}
0 commit comments