Repository: containerd/containerd
File: core/remotes/docker/referrers.go (Go)

Code before:

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package docker

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"strings"

	"github.com/containerd/errdefs"
	"github.com/containerd/log"
	digest "github.com/opencontainers/go-digest"
	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

func (r dockerFetcher) FetchReferrers(ctx context.Context, dgst digest.Digest, artifactTypes ...string) (io.ReadCloser, ocispec.Descriptor, error) {
	var desc ocispec.Descriptor
	desc.MediaType = ocispec.MediaTypeImageIndex
	ctx = log.WithLogger(ctx, log.G(ctx).WithField("digest", dgst))

	hosts := r.filterHosts(HostCapabilityReferrers)
	fallbackHosts := r.filterHosts(HostCapabilityResolve)
	if len(hosts) == 0 && len(fallbackHosts) == 0 {
		return nil, desc, fmt.Errorf("no pull hosts: %w", errdefs.ErrNotFound)
	}

	ctx, err := ContextWithRepositoryScope(ctx, r.refspec, false)
	if err != nil {
		return nil, desc, err
	}

	for i, host := range hosts {
		req := r.request(host, http.MethodGet, "referrers", dgst.String())
		for _, artifactType := range artifactTypes {
			if err := req.addQuery("artifactType", artifactType); err != nil {
				return nil, desc, err
			}
		}
		if err := req.addNamespace(r.refspec.Hostname()); err != nil {
			return nil, desc, err
		}

Review comment (suggestion):
Mentioned this before, but I still think this should just return `Index`. While your point was that this should be lower-level and wrapped with another function, this function does not return an actual immutable object that can be pulled or stored. The only thing you can do with it is parse it to `Index`. Returning index also allows doing the local `artifactTypes` filter if the internal implementation didn't handle it.

Code after:

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package docker

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"strings"

	"github.com/containerd/errdefs"
	"github.com/containerd/log"
	digest "github.com/opencontainers/go-digest"
	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

func (r dockerFetcher) FetchReferrers(ctx context.Context, dgst digest.Digest, artifactTypes ...string) ([]ocispec.Descriptor, error) {
	rc, size, err := r.openReferrers(ctx, dgst, artifactTypes...)
	if err != nil {
		return nil, err
	}
	defer rc.Close()

	var index ocispec.Index
	dec := json.NewDecoder(io.LimitReader(rc, size))
	if err := dec.Decode(&index); err != nil {
		return nil, fmt.Errorf("failed to decode referrers index: %w", err)
	}
	if _, err := dec.Token(); !errors.Is(err, io.EOF) {
		return nil, fmt.Errorf("unexpected data after JSON object")
	}

	if len(artifactTypes) == 0 {
		return index.Manifests, nil
	}

	var referrers []ocispec.Descriptor
	tFilter := map[string]struct{}{}
	for _, t := range artifactTypes {
		tFilter[t] = struct{}{}