diff --git a/docker/docker_client.go b/docker/docker_client.go index 91006e8d24..b1256b9cbd 100644 --- a/docker/docker_client.go +++ b/docker/docker_client.go @@ -8,7 +8,9 @@ import ( "io" "io/ioutil" "net/http" + "net/url" "path/filepath" + "strconv" "strings" "time" @@ -24,8 +26,9 @@ import ( ) const ( - dockerHostname = "docker.io" - dockerRegistry = "registry-1.docker.io" + dockerHostname = "docker.io" + dockerV1Hostname = "index.docker.io" + dockerRegistry = "registry-1.docker.io" systemPerHostCertDirPath = "/etc/docker/certs.d" @@ -221,6 +224,100 @@ func CheckAuth(ctx context.Context, sCtx *types.SystemContext, username, passwor } } +// SearchResult holds the information of each matching image +// It matches the output returned by the v1 endpoint +type SearchResult struct { + Name string `json:"name"` + Description string `json:"description"` + // StarCount states the number of stars the image has + StarCount int `json:"star_count"` + IsTrusted bool `json:"is_trusted"` + // IsAutomated states whether the image is an automated build + IsAutomated bool `json:"is_automated"` + // IsOfficial states whether the image is an official build + IsOfficial bool `json:"is_official"` +} + +// SearchRegistry queries a registry for images that contain "image" in their name +// The limit is the max number of results desired +// Note: The limit value doesn't work with all registries +// for example registry.access.redhat.com returns all the results without limiting it to the limit value +func SearchRegistry(ctx context.Context, sCtx *types.SystemContext, registry, image string, limit int) ([]SearchResult, error) { + type V2Results struct { + // Repositories holds the results returned by the /v2/_catalog endpoint + Repositories []string `json:"repositories"` + } + type V1Results struct { + // Results holds the results returned by the /v1/search endpoint + Results []SearchResult `json:"results"` + } + v2Res := &V2Results{} + v1Res := &V1Results{} + + // The /v2/_catalog endpoint has been disabled for docker.io therefore the call made to that endpoint will fail + // So using the v1 hostname for docker.io for simplicity of implementation and the fact that it returns search results + if registry == dockerHostname { + registry = dockerV1Hostname + } + + client, err := newDockerClientWithDetails(sCtx, registry, "", "", "", nil, "") + if err != nil { + return nil, errors.Wrapf(err, "error creating new docker client") + } + + logrus.Debugf("trying to talk to v2 search endpoint\n") + resp, err := client.makeRequest(ctx, "GET", "/v2/_catalog", nil, nil) + if err != nil { + logrus.Debugf("error getting search results from v2 endpoint %q: %v", registry, err) + } else { + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + logrus.Debugf("error getting search results from v2 endpoint %q, status code %q", registry, resp.StatusCode) + } else { + if err := json.NewDecoder(resp.Body).Decode(v2Res); err != nil { + return nil, err + } + searchRes := []SearchResult{} + for _, repo := range v2Res.Repositories { + if strings.Contains(repo, image) { + res := SearchResult{ + Name: repo, + } + searchRes = append(searchRes, res) + } + } + return searchRes, nil + } + } + + // set up the query values for the v1 endpoint + u := url.URL{ + Path: "/v1/search", + } + q := u.Query() + q.Set("q", image) + q.Set("n", strconv.Itoa(limit)) + u.RawQuery = q.Encode() + + logrus.Debugf("trying to talk to v1 search endpoint\n") + resp, err = client.makeRequest(ctx, "GET", u.String(), nil, nil) + if err != nil { + logrus.Debugf("error getting search results from v1 endpoint %q: %v", registry, err) + } else { + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + logrus.Debugf("error getting search results from v1 endpoint %q, status code %q", registry, resp.StatusCode) + } else { + if err := json.NewDecoder(resp.Body).Decode(v1Res); err != nil { + return nil, err + } + return v1Res.Results, nil + } + } + + return nil, errors.Wrapf(err, "couldn't search registry %q", registry) +} + // makeRequest creates and executes a http.Request with the specified parameters, adding authentication and TLS options for the Docker client. // 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/. func (c *dockerClient) makeRequest(ctx context.Context, method, path string, headers map[string][]string, stream io.Reader) (*http.Response, error) {