Path: blob/main/components/registry-facade/pkg/registry/manifest_test.go
2499 views
// Copyright (c) 2020 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package registry56import (7"context"8"encoding/json"9"errors"10"fmt"11"io"12"strings"13"syscall"14"testing"15"time"1617"github.com/containerd/containerd/content"18"github.com/containerd/containerd/errdefs"19"github.com/containerd/containerd/remotes"20"github.com/opencontainers/go-digest"21"github.com/opencontainers/image-spec/specs-go"22ociv1 "github.com/opencontainers/image-spec/specs-go/v1"23"github.com/stretchr/testify/assert"24"github.com/stretchr/testify/require"25"k8s.io/apimachinery/pkg/util/wait"26)2728func TestDownloadManifest(t *testing.T) {29tests := []struct {30Name string31Store BlobStore32FetchMF bool33}{34{35Name: "no store",36},37{38Name: "accepts nothing store",39Store: &alwaysNotFoundStore{},40},41{42Name: "misbehaving store",43Store: &misbehavingStore{},44},45{46Name: "fetch mf no store",47FetchMF: true,48},49{50Name: "fetch mf with store",51Store: &alwaysNotFoundStore{},52FetchMF: true,53},54}5556for _, test := range tests {57t.Run(test.Name, func(t *testing.T) {58cfg, err := json.Marshal(ociv1.ImageConfig{})59if err != nil {60t.Fatal(err)61}62cfgDgst := digest.FromBytes(cfg)6364mf, err := json.Marshal(ociv1.Manifest{65Versioned: specs.Versioned{SchemaVersion: 1},66MediaType: ociv1.MediaTypeImageManifest,67Config: ociv1.Descriptor{68MediaType: ociv1.MediaTypeImageConfig,69Digest: cfgDgst,70Size: int64(len(cfg)),71},72})73if err != nil {74t.Fatal(err)75}76mfDgst := digest.FromBytes(mf)77mfDesc := ociv1.Descriptor{78MediaType: ociv1.MediaTypeImageManifest,79Digest: mfDgst,80Size: int64(len(mf)),81}8283mfl, err := json.Marshal(ociv1.Index{84MediaType: ociv1.MediaTypeImageIndex,85Manifests: []ociv1.Descriptor{mfDesc},86})87if err != nil {88t.Fatal(err)89}90mflDgst := digest.FromBytes(mfl)91mflDesc := ociv1.Descriptor{92MediaType: ociv1.MediaTypeImageIndex,93Digest: mflDgst,94Size: int64(len(mfl)),95}9697fetcher := AsFetcherFunc(&fakeFetcher{98Content: map[string][]byte{99mflDgst.Encoded(): mfl,100mfDgst.Encoded(): mf,101cfgDgst.Encoded(): cfg,102},103})104desc := mflDesc105if test.FetchMF {106desc = mfDesc107}108_, _, err = DownloadManifest(context.Background(), fetcher, desc, WithStore(test.Store))109if err != nil {110t.Fatal(err)111}112})113}114}115116type alwaysNotFoundStore struct{}117118func (fbs *alwaysNotFoundStore) ReaderAt(ctx context.Context, desc ociv1.Descriptor) (content.ReaderAt, error) {119return nil, errdefs.ErrNotFound120}121122// Some implementations require WithRef to be included in opts.123func (fbs *alwaysNotFoundStore) Writer(ctx context.Context, opts ...content.WriterOpt) (content.Writer, error) {124return nil, errdefs.ErrAlreadyExists125}126127// Info will return metadata about content available in the content store.128//129// If the content is not present, ErrNotFound will be returned.130func (fbs *alwaysNotFoundStore) Info(ctx context.Context, dgst digest.Digest) (content.Info, error) {131return content.Info{}, errdefs.ErrNotFound132}133134type misbehavingStore struct{}135136func (fbs *misbehavingStore) ReaderAt(ctx context.Context, desc ociv1.Descriptor) (content.ReaderAt, error) {137return nil, fmt.Errorf("foobar")138}139140// Some implementations require WithRef to be included in opts.141func (fbs *misbehavingStore) Writer(ctx context.Context, opts ...content.WriterOpt) (content.Writer, error) {142return nil, fmt.Errorf("some error")143}144145// Info will return metadata about content available in the content store.146//147// If the content is not present, ErrNotFound will be returned.148func (fbs *misbehavingStore) Info(ctx context.Context, dgst digest.Digest) (content.Info, error) {149return content.Info{}, fmt.Errorf("you wish")150}151152// manifestFailingReader is a reader that fails after a certain point.153type manifestFailingReader struct {154reader io.Reader155failAfterBytes int156failError error157bytesRead int158}159160func (fr *manifestFailingReader) Read(p []byte) (n int, err error) {161if fr.bytesRead >= fr.failAfterBytes {162return 0, fr.failError163}164n, err = fr.reader.Read(p)165if err != nil {166return n, err167}168fr.bytesRead += n169if fr.bytesRead >= fr.failAfterBytes {170// Return the error, but also the bytes read in this call.171return n, fr.failError172}173return n, nil174}175176func (fr *manifestFailingReader) Close() error {177return nil178}179180type mockFetcher struct {181// How many times Fetch should fail before succeeding.182failCount int183// The error to return on failure.184failError error185186// Internal counter for calls.187callCount int188// The data to return on success.189successData string190191// Whether to use a reader that fails mid-stream on the first call.192failReaderOnFirstCall bool193// The number of bytes to read successfully before the reader fails.194failAfterBytes int195}196197func (m *mockFetcher) Fetch(ctx context.Context, desc ociv1.Descriptor) (io.ReadCloser, error) {198m.callCount++199if m.callCount <= m.failCount {200return nil, m.failError201}202203if m.failReaderOnFirstCall && m.callCount == 1 {204return &manifestFailingReader{205reader: strings.NewReader(m.successData),206failAfterBytes: m.failAfterBytes,207failError: m.failError,208}, nil209}210211return io.NopCloser(strings.NewReader(m.successData)), nil212}213214func TestDownloadManifest_RetryOnReadAll(t *testing.T) {215// Arrange216mockFetcher := &mockFetcher{217failCount: 0, // Fetch succeeds immediately218failReaderOnFirstCall: true,219failAfterBytes: 5,220failError: syscall.EPIPE,221successData: `{"schemaVersion": 2, "mediaType": "application/vnd.oci.image.manifest.v1+json"}`,222}223224fetcherFunc := func() (remotes.Fetcher, error) {225return mockFetcher, nil226}227228// Use short backoff for testing229originalBackoff := fetcherBackoffParams230fetcherBackoffParams = wait.Backoff{231Duration: 1 * time.Millisecond,232Steps: 3,233}234defer func() { fetcherBackoffParams = originalBackoff }()235236// Act237_, _, err := DownloadManifest(context.Background(), fetcherFunc, ociv1.Descriptor{MediaType: ociv1.MediaTypeImageManifest})238239// Assert240require.NoError(t, err)241assert.Equal(t, 2, mockFetcher.callCount, "Expected Fetch to be called twice (1st succeeds, read fails, 2nd succeeds)")242}243244func TestDownloadConfig_RetryOnReadAll(t *testing.T) {245// Arrange246mockFetcher := &mockFetcher{247failCount: 0, // Fetch succeeds immediately248failReaderOnFirstCall: true,249failAfterBytes: 5,250failError: syscall.EPIPE,251successData: `{"architecture": "amd64", "os": "linux"}`,252}253254fetcherFunc := func() (remotes.Fetcher, error) {255return mockFetcher, nil256}257258// Use short backoff for testing259originalBackoff := fetcherBackoffParams260fetcherBackoffParams = wait.Backoff{261Duration: 1 * time.Millisecond,262Steps: 3,263}264defer func() { fetcherBackoffParams = originalBackoff }()265266// Act267_, err := DownloadConfig(context.Background(), fetcherFunc, "ref", ociv1.Descriptor{MediaType: ociv1.MediaTypeImageConfig})268269// Assert270require.NoError(t, err)271assert.Equal(t, 2, mockFetcher.callCount, "Expected Fetch to be called twice (1st succeeds, read fails, 2nd succeeds)")272}273274func TestDownloadManifest_RetryOnFetch(t *testing.T) {275// Arrange276mockFetcher := &mockFetcher{277failCount: 2,278failError: errors.New("transient network error"),279successData: `{"schemaVersion": 2, "mediaType": "application/vnd.oci.image.manifest.v1+json"}`,280}281282fetcherFunc := func() (remotes.Fetcher, error) {283return mockFetcher, nil284}285286// Use short backoff for testing287originalBackoff := fetcherBackoffParams288fetcherBackoffParams = wait.Backoff{289Duration: 1 * time.Millisecond,290Steps: 3,291}292defer func() { fetcherBackoffParams = originalBackoff }()293294// Act295_, _, err := DownloadManifest(context.Background(), fetcherFunc, ociv1.Descriptor{MediaType: ociv1.MediaTypeImageManifest})296297// Assert298require.NoError(t, err)299assert.Equal(t, 3, mockFetcher.callCount, "Expected Fetch to be called 3 times (2 failures + 1 success)")300}301302303