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 }