github.com/quay/claircore@v1.5.28/libindex/fetcher_test.go (about)

     1  package libindex
     2  
     3  import (
     4  	"archive/tar"
     5  	"context"
     6  	"crypto/sha256"
     7  	"fmt"
     8  	"io"
     9  	"math/rand"
    10  	"net/http"
    11  	"net/http/httptest"
    12  	"os"
    13  	"path/filepath"
    14  	"runtime"
    15  	"strconv"
    16  	"sync/atomic"
    17  	"testing"
    18  
    19  	"github.com/quay/zlog"
    20  
    21  	"github.com/quay/claircore"
    22  	"github.com/quay/claircore/internal/wart"
    23  	"github.com/quay/claircore/test"
    24  )
    25  
    26  type fetchTestcase struct {
    27  	N int
    28  }
    29  
    30  func (tc fetchTestcase) Run(ctx context.Context) func(*testing.T) {
    31  	return func(t *testing.T) {
    32  		ctx := zlog.Test(ctx, t)
    33  		c, descs := test.ServeLayers(t, tc.N)
    34  		for _, l := range descs {
    35  			t.Logf("%+v", l)
    36  		}
    37  		a := NewRemoteFetchArena(c, t.TempDir())
    38  
    39  		fetcher := a.Realizer(ctx)
    40  		layers := wart.DescriptionsToLayers(descs)
    41  		if err := fetcher.Realize(ctx, layers); err != nil {
    42  			t.Error(err)
    43  		}
    44  		for _, l := range layers {
    45  			t.Logf("%+v", l)
    46  		}
    47  		if err := fetcher.Close(); err != nil {
    48  			t.Error(err)
    49  		}
    50  	}
    51  }
    52  
    53  func TestFetchSimple(t *testing.T) {
    54  	ctx := context.Background()
    55  	tt := []fetchTestcase{
    56  		{N: 1},
    57  		{N: 4},
    58  		{N: 32},
    59  	}
    60  
    61  	for _, tc := range tt {
    62  		t.Run(strconv.Itoa(tc.N), tc.Run(ctx))
    63  	}
    64  }
    65  
    66  func TestFetchInvalid(t *testing.T) {
    67  	// TODO(hank) Rewrite this into unified testcases.
    68  	ctx := context.Background()
    69  	tt := []struct {
    70  		name  string
    71  		layer []*claircore.Layer
    72  	}{
    73  		{
    74  			name: "no remote path or local path provided",
    75  			layer: []*claircore.Layer{
    76  				{
    77  					URI: "",
    78  				},
    79  			},
    80  		},
    81  		{
    82  			name: "path with no scheme",
    83  			layer: []*claircore.Layer{
    84  				{
    85  					URI: "www.example.com/path/to/tar?query=one",
    86  				},
    87  			},
    88  		},
    89  	}
    90  
    91  	tmp := t.TempDir()
    92  	for _, table := range tt {
    93  		t.Run(table.name, func(t *testing.T) {
    94  			ctx := zlog.Test(ctx, t)
    95  			a := NewRemoteFetchArena(http.DefaultClient, tmp)
    96  			fetcher := a.Realizer(ctx)
    97  			if err := fetcher.Realize(ctx, table.layer); err == nil {
    98  				t.Fatal("expected error, got nil")
    99  			}
   100  		})
   101  	}
   102  }
   103  
   104  func TestFetchConcurrent(t *testing.T) {
   105  	t.Parallel()
   106  	ctx := zlog.Test(context.Background(), t)
   107  	descs, h := commonLayerServer(t, 25)
   108  	srv := httptest.NewUnstartedServer(h)
   109  	srv.Start()
   110  	for i := range descs {
   111  		descs[i].URI = srv.URL + descs[i].URI
   112  	}
   113  	t.Cleanup(srv.Close)
   114  	a := NewRemoteFetchArena(srv.Client(), t.TempDir())
   115  	t.Cleanup(func() {
   116  		if err := a.Close(ctx); err != nil {
   117  			t.Error(err)
   118  		}
   119  	})
   120  
   121  	t.Run("OldInterface", func(t *testing.T) {
   122  		ctx := zlog.Test(ctx, t)
   123  
   124  		t.Run("Thread", func(t *testing.T) {
   125  			run := func(a *RemoteFetchArena, ls []claircore.LayerDescription) func(*testing.T) {
   126  				ps := wart.DescriptionsToLayers(ls)
   127  				// Leave the bottom half the same, shuffle the top half.
   128  				off := len(ps) / 2
   129  				rand.Shuffle(off, func(i, j int) {
   130  					i, j = i+off, j+off
   131  					ps[i], ps[j] = ps[j], ps[i]
   132  				})
   133  				return func(t *testing.T) {
   134  					t.Parallel()
   135  					ctx := zlog.Test(ctx, t)
   136  					f := a.Realizer(ctx)
   137  					t.Cleanup(func() {
   138  						if err := f.Close(); err != nil {
   139  							t.Error(err)
   140  						}
   141  					})
   142  					if err := f.Realize(ctx, ps); err != nil {
   143  						t.Error(err)
   144  					}
   145  				}
   146  			}
   147  			for i := 0; i < runtime.GOMAXPROCS(0); i++ {
   148  				t.Run(strconv.Itoa(i), run(a, descs))
   149  			}
   150  		})
   151  	})
   152  
   153  	t.Run("NewInterface", func(t *testing.T) {
   154  		ctx := zlog.Test(ctx, t)
   155  		t.Run("Thread", func(t *testing.T) {
   156  			run := func(a *RemoteFetchArena, descs []claircore.LayerDescription) func(*testing.T) {
   157  				ds := make([]claircore.LayerDescription, len(descs))
   158  				copy(ds, descs)
   159  				// Leave the bottom half the same, shuffle the top half.
   160  				off := len(ds) / 2
   161  				rand.Shuffle(off, func(i, j int) {
   162  					i, j = i+off, j+off
   163  					ds[i], ds[j] = ds[j], ds[i]
   164  				})
   165  				return func(t *testing.T) {
   166  					t.Parallel()
   167  					ctx := zlog.Test(ctx, t)
   168  					f := a.Realizer(ctx).(*FetchProxy)
   169  					defer func() {
   170  						if err := f.Close(); err != nil {
   171  							t.Error(err)
   172  						}
   173  					}()
   174  					ls, err := f.RealizeDescriptions(ctx, ds)
   175  					if err != nil {
   176  						t.Errorf("RealizeDescriptions error: %v", err)
   177  					}
   178  					t.Logf("layers: %v", ls)
   179  				}
   180  			}
   181  			for i := 0; i < runtime.GOMAXPROCS(0); i++ {
   182  				t.Run(strconv.Itoa(i), run(a, descs))
   183  			}
   184  		})
   185  	})
   186  }
   187  
   188  func commonLayerServer(t testing.TB, ct int) ([]claircore.LayerDescription, http.Handler) {
   189  	// TODO(hank) Cache all this? The contents are basically static.
   190  	t.Helper()
   191  	dir := t.TempDir()
   192  	descs := make([]claircore.LayerDescription, ct)
   193  	fetch := make(map[string]*uint64, ct)
   194  	for i := 0; i < ct; i++ {
   195  		n := strconv.Itoa(i)
   196  		f, err := os.Create(filepath.Join(dir, strconv.Itoa(i)))
   197  		if err != nil {
   198  			t.Fatal(err)
   199  		}
   200  		h := sha256.New()
   201  		w := tar.NewWriter(io.MultiWriter(f, h))
   202  		if err := w.WriteHeader(&tar.Header{
   203  			Name: n,
   204  			Size: 33,
   205  		}); err != nil {
   206  			t.Fatal(err)
   207  		}
   208  		fmt.Fprintf(w, "%032d\n", i)
   209  
   210  		if err := w.Close(); err != nil {
   211  			t.Fatal(err)
   212  		}
   213  		if err := f.Close(); err != nil {
   214  			t.Fatal(err)
   215  		}
   216  		l := &descs[i]
   217  		l.URI = "/" + strconv.Itoa(i)
   218  		fetch[l.URI] = new(uint64)
   219  		l.Digest = fmt.Sprintf("sha256:%x", h.Sum(nil))
   220  		l.Headers = make(http.Header)
   221  		l.MediaType = `application/vnd.oci.image.layer.nondistributable.v1.tar`
   222  		if err != nil {
   223  			t.Fatal(err)
   224  		}
   225  	}
   226  
   227  	t.Cleanup(func() {
   228  		// We know we're doing 2 sets of fetches.
   229  		max := ct * 2 * runtime.GOMAXPROCS(0)
   230  		var total int
   231  		for _, v := range fetch {
   232  			total += int(*v)
   233  		}
   234  		switch {
   235  		case total > max:
   236  			t.Errorf("more fetches than should be possible: %d > %d", total, max)
   237  		case total == max:
   238  			t.Errorf("prevented no fetches: %d == %d", total, max)
   239  		case total < max:
   240  			t.Logf("prevented %[3]d fetches: %[1]d < %d", total, max, max-total)
   241  		}
   242  
   243  	})
   244  	inner := http.FileServer(http.Dir(dir))
   245  	return descs, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   246  		ct := fetch[r.URL.Path]
   247  		atomic.AddUint64(ct, 1)
   248  		inner.ServeHTTP(w, r)
   249  	})
   250  }