Skip to content

Commit a70a438

Browse files
committed
Add functionality to search registries
podman search searches a registry for a matching image this adds the functionality to support that some registries respond to the v2 endpoint while others only respond to the v1 endpoint. This checks both endpoints for a result, and if none is given the user is informed. Signed-off-by: umohnani8 <[email protected]>
1 parent 495da41 commit a70a438

File tree

1 file changed

+99
-2
lines changed

1 file changed

+99
-2
lines changed

docker/docker_client.go

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import (
88
"io"
99
"io/ioutil"
1010
"net/http"
11+
"net/url"
1112
"path/filepath"
13+
"strconv"
1214
"strings"
1315
"time"
1416

@@ -24,8 +26,9 @@ import (
2426
)
2527

2628
const (
27-
dockerHostname = "docker.io"
28-
dockerRegistry = "registry-1.docker.io"
29+
dockerHostname = "docker.io"
30+
dockerV1Hostname = "index.docker.io"
31+
dockerRegistry = "registry-1.docker.io"
2932

3033
systemPerHostCertDirPath = "/etc/docker/certs.d"
3134

@@ -221,6 +224,100 @@ func CheckAuth(ctx context.Context, sCtx *types.SystemContext, username, passwor
221224
}
222225
}
223226

227+
// SearchResult holds the information of each matching image
228+
// It matches the output returned by the v1 endpoint
229+
type SearchResult struct {
230+
Name string `json:"name"`
231+
Description string `json:"description"`
232+
// StarCount states the number of stars the image has
233+
StarCount int `json:"star_count"`
234+
IsTrusted bool `json:"is_trusted"`
235+
// IsAutomated states whether the image is an automated build
236+
IsAutomated bool `json:"is_automated"`
237+
// IsOfficial states whether the image is an official build
238+
IsOfficial bool `json:"is_official"`
239+
}
240+
241+
// SearchRegistry queries a registry for images that contain "image" in their name
242+
// The limit is the max number of results desired
243+
// Note: The limit value doesn't work with all registries
244+
// for example registry.access.redhat.com returns all the results without limiting it to the limit value
245+
func SearchRegistry(ctx context.Context, sCtx *types.SystemContext, registry, image string, limit int) ([]SearchResult, error) {
246+
type V2Results struct {
247+
// Repositories holds the results returned by the /v2/_catalog endpoint
248+
Repositories []string `json:"repositories"`
249+
}
250+
type V1Results struct {
251+
// Results holds the results returned by the /v1/search endpoint
252+
Results []SearchResult `json:"results"`
253+
}
254+
v2Res := &V2Results{}
255+
v1Res := &V1Results{}
256+
257+
// The /v2/_catalog endpoint has been disabled for docker.io therefore the call made to that endpoint will fail
258+
// So using the v1 hostname for docker.io for simplicity of implementation and the fact that it returns search results
259+
if registry == dockerHostname {
260+
registry = dockerV1Hostname
261+
}
262+
263+
client, err := newDockerClientWithDetails(sCtx, registry, "", "", "", nil, "")
264+
if err != nil {
265+
return nil, errors.Wrapf(err, "error creating new docker client")
266+
}
267+
268+
logrus.Debugf("trying to talk to v2 search endpoint\n")
269+
resp, err := client.makeRequest(ctx, "GET", "/v2/_catalog", nil, nil)
270+
if err != nil {
271+
logrus.Debugf("error getting search results from v2 endpoint %q: %v", registry, err)
272+
} else {
273+
defer resp.Body.Close()
274+
if resp.StatusCode != http.StatusOK {
275+
logrus.Debugf("error getting search results from v2 endpoint %q, status code %q", registry, resp.StatusCode)
276+
} else {
277+
if err := json.NewDecoder(resp.Body).Decode(v2Res); err != nil {
278+
return nil, err
279+
}
280+
searchRes := []SearchResult{}
281+
for _, repo := range v2Res.Repositories {
282+
if strings.Contains(repo, image) {
283+
res := SearchResult{
284+
Name: repo,
285+
}
286+
searchRes = append(searchRes, res)
287+
}
288+
}
289+
return searchRes, nil
290+
}
291+
}
292+
293+
// set up the query values for the v1 endpoint
294+
u := url.URL{
295+
Path: "/v1/search",
296+
}
297+
q := u.Query()
298+
q.Set("q", image)
299+
q.Set("n", strconv.Itoa(limit))
300+
u.RawQuery = q.Encode()
301+
302+
logrus.Debugf("trying to talk to v1 search endpoint\n")
303+
resp, err = client.makeRequest(ctx, "GET", u.String(), nil, nil)
304+
if err != nil {
305+
logrus.Debugf("error getting search results from v1 endpoint %q: %v", registry, err)
306+
} else {
307+
defer resp.Body.Close()
308+
if resp.StatusCode != http.StatusOK {
309+
logrus.Debugf("error getting search results from v1 endpoint %q, status code %q", registry, resp.StatusCode)
310+
} else {
311+
if err := json.NewDecoder(resp.Body).Decode(v1Res); err != nil {
312+
return nil, err
313+
}
314+
return v1Res.Results, nil
315+
}
316+
}
317+
318+
return nil, errors.Wrapf(err, "couldn't search registry %q", registry)
319+
}
320+
224321
// makeRequest creates and executes a http.Request with the specified parameters, adding authentication and TLS options for the Docker client.
225322
// The host name and schema is taken from the client or autodetected, and the path is relative to it, i.e. the path usually starts with /v2/.
226323
func (c *dockerClient) makeRequest(ctx context.Context, method, path string, headers map[string][]string, stream io.Reader) (*http.Response, error) {

0 commit comments

Comments
 (0)