1 package cache
2
3 import (
4 _ "embed"
5 "fmt"
6 "io"
7 "log"
8 "net/http/httptest"
9 "net/url"
10 "os"
11 "testing"
12
13 "github.com/google/go-containerregistry/pkg/name"
14 v1 "github.com/google/go-containerregistry/pkg/v1"
15 "github.com/stretchr/testify/assert"
16 "github.com/stretchr/testify/require"
17
18 "edge-infra.dev/pkg/f8n/warehouse/cluster"
19 "edge-infra.dev/pkg/f8n/warehouse/lift/unpack"
20 "edge-infra.dev/pkg/f8n/warehouse/oci"
21 "edge-infra.dev/pkg/f8n/warehouse/oci/layer"
22 "edge-infra.dev/pkg/f8n/warehouse/oci/remote"
23 "edge-infra.dev/pkg/f8n/warehouse/oci/walk"
24 "edge-infra.dev/pkg/f8n/warehouse/pallet"
25 "edge-infra.dev/pkg/f8n/warehouse/whtest/registry"
26 "edge-infra.dev/pkg/lib/uuid"
27 "edge-infra.dev/test/fixtures"
28 )
29
30 var (
31 fpath *fixtures.Path
32 r *httptest.Server
33 rURL *url.URL
34 shoot oci.Artifact
35 redpanda oci.Artifact
36 certmgr oci.Artifact
37 store oci.Artifact
38 )
39
40 type simpleTestRecorder struct {
41 gets int
42 hits int
43 }
44
45 func (r *simpleTestRecorder) RecordGet(hit bool, _ string, _ string) {
46 r.gets++
47 if hit {
48 r.hits++
49 }
50 }
51
52 func (r *simpleTestRecorder) misses() int {
53 return r.gets - r.hits
54 }
55
56 func TestMain(m *testing.M) {
57 var err error
58
59 rURL, r, err = registry.New(registry.Logger(log.New(io.Discard, "", 0)))
60 if err != nil {
61 panic(err)
62 }
63 defer func() {
64 r.Close()
65 }()
66
67 fpath, err = fixtures.Layout()
68 if err != nil {
69 panic(err)
70 }
71
72 shoot = loadArtifact(fpath, "shoot:latest")
73 redpanda = loadArtifact(fpath, "redpanda-system:latest")
74 certmgr = loadArtifact(fpath, "cert-manager:latest")
75 store = loadArtifact(fpath, "store:latest")
76
77 os.Exit(m.Run())
78 }
79
80 func TestNew(t *testing.T) {
81 warehouseCache, err := New(
82 WithMemoryCacheSize(250),
83 )
84 assert.NoError(t, err)
85 assert.NotEmpty(t, warehouseCache)
86 }
87
88 func TestCache(t *testing.T) {
89 var (
90 id = "test-cache"
91 cache Cache
92 recorder = &simpleTestRecorder{gets: 0, hits: 0}
93 expectedGets = 0
94 shootTag = testTag(t, id, "shoot", uuid.New().Hash())
95 shootDigest = testDigest(t, id, "shoot", shoot)
96 certmgrTag = testTag(t, id, "cert-manager", uuid.New().Hash())
97 certmgrDigest = testDigest(t, id, "cert-manager", certmgr)
98 )
99
100 cache, err := New(
101 WithMemoryCacheSize(10),
102 WithRecorder(recorder),
103 )
104 assert.NoError(t, err)
105
106 t.Run("Add", func(t *testing.T) {
107 _, err := cache.Add(shoot)
108 assert.NoError(t, err)
109 })
110
111
112
113 t.Run("Get", func(t *testing.T) {
114
115 assert.NoError(t, remote.Write(certmgr, certmgrTag))
116 assert.NoError(t, remote.Write(shoot, shootTag))
117
118 refs := []name.Reference{
119 shootTag, shootDigest, certmgrTag,
120 }
121 artifacts := []oci.Artifact{
122 shoot, shoot, certmgr,
123 }
124 for i, ref := range refs {
125 testGet(t, cache, ref, artifacts[i])
126 expectedGets = expectedGets + 1
127 assert.Equal(t, expectedGets, recorder.gets)
128 }
129 assert.Equal(t, 2, recorder.hits)
130 assert.Equal(t, 1, recorder.misses())
131
132 refs = []name.Reference{
133 certmgrTag, certmgrDigest,
134 }
135 artifacts = []oci.Artifact{
136 certmgr, certmgr,
137 }
138 for i, ref := range refs {
139 testGet(t, cache, ref, artifacts[i])
140 expectedGets = expectedGets + 1
141 assert.Equal(t, expectedGets, recorder.gets)
142 }
143 assert.Equal(t, 4, recorder.hits)
144 assert.Equal(t, 1, recorder.misses())
145
146
147
148 newTag := testTag(t, id, "cert-manager", uuid.New().Hash())
149 assert.NoError(t, remote.Write(certmgr, newTag))
150 t.Log(certmgrDigest)
151 testGet(t, cache, newTag, certmgr)
152 expectedGets = expectedGets + 1
153 assert.Equal(t, expectedGets, recorder.gets)
154
155 assert.Equal(t, 5, recorder.hits)
156 assert.Equal(t, 1, recorder.misses())
157 })
158
159 t.Run("Get_ResolveTag", func(t *testing.T) {
160 _, err := cache.Add(redpanda)
161 assert.NoError(t, err)
162 assert.NoError(t, remote.Write(redpanda, certmgrTag))
163 testGet(t, cache, certmgrTag, redpanda, ResolveTag())
164 expectedGets = expectedGets + 1
165 assert.Equal(t, expectedGets, recorder.gets)
166
167 assert.Equal(t, 6, recorder.hits)
168 assert.Equal(t, 1, recorder.misses())
169 })
170 }
171
172 func TestLazyFetch(t *testing.T) {
173 var (
174 id = "test-lazy-cache"
175 cache Cache
176 recorder = &simpleTestRecorder{0, 0}
177 storeTag = testTag(t, id, "store", uuid.New().Hash())
178 storeDigest = testDigest(t, id, "store", store)
179 )
180
181
182
183 blobs, blobCount, err := analyzeArtifact(store)
184 require.NoError(t, err)
185
186 p, err := pallet.New(store)
187 require.NoError(t, err)
188
189 require.NoError(t, remote.Write(store, storeDigest))
190
191 cache, err = New(
192 WithMemoryCacheSize(500),
193 WithRecorder(recorder),
194 )
195 require.NoError(t, err)
196
197 cachedStore := testColdGet(t, cache, storeDigest, blobs)
198
199
200
201 computeArtifact(t, cachedStore, p.Providers()...)
202
203
204
205 checkCacheForBlobs(t, cache, blobs, blobCount)
206 assert.Equal(t, cache.Len(), blobCount, "cache has more blobs than expected")
207
208
209 finalMissCount := recorder.misses()
210 _, err = cache.Get(storeDigest)
211 assert.NoError(t, err)
212 assert.Equal(t, finalMissCount, recorder.misses())
213
214 t.Run("Tags", func(t *testing.T) {
215
216
217 require.NoError(t, remote.Tag(storeTag.(name.Tag), store))
218 cachedStore, err := cache.Get(storeTag)
219 assert.NoError(t, err)
220 sameDigest(t, store, cachedStore)
221 assert.Equal(t, finalMissCount, recorder.misses())
222 })
223 }
224
225 func TestCommonBlobs(t *testing.T) {
226
227
228
229 var (
230 id = "test-lazy-common-blobs"
231 cache Cache
232 recorder = &simpleTestRecorder{0, 0}
233 shootRef = testDigest(t, id, "store", shoot)
234 redpandaRef = testDigest(t, id, "redpanda-system", redpanda)
235 )
236
237 cache, err := New(
238 WithMemoryCacheSize(500),
239 WithRecorder(recorder),
240 )
241 require.NoError(t, err)
242
243 shootBlobs, shootBlobCount, err := analyzeArtifact(shoot)
244 require.NoError(t, err)
245 shootPallet, err := pallet.New(shoot)
246 require.NoError(t, err)
247 require.NoError(t, remote.Write(shoot, shootRef))
248
249 redpandaBlobs, redpandaBlobCount, err := analyzeArtifact(redpanda)
250 require.NoError(t, err)
251 redpandaPallet, err := pallet.New(redpanda)
252 require.NoError(t, err)
253 require.NoError(t, remote.Write(redpanda, redpandaRef))
254
255
256 cachedRedpanda := testColdGet(t, cache, redpandaRef, redpandaBlobs)
257 computeArtifact(t, cachedRedpanda, redpandaPallet.Providers()...)
258
259 checkCacheForBlobs(t, cache, redpandaBlobs, redpandaBlobCount)
260 assert.Equal(t, cache.Len(), redpandaBlobCount, "cache has more blobs than expected")
261
262
263 missingShootBlobs := map[string][]v1.Hash{}
264 for objType, hashes := range shootBlobs {
265 for _, hash := range hashes {
266 found := false
267 for _, rpHash := range redpandaBlobs[objType] {
268 if hash == rpHash {
269 found = true
270 break
271 }
272 }
273 if !found {
274 missingShootBlobs[objType] = append(missingShootBlobs[objType], hash)
275 }
276 }
277 }
278 cachedShoot := testColdGet(t, cache, shootRef, missingShootBlobs)
279
280
281 misses := recorder.misses()
282 computeArtifact(t, cachedShoot, shootPallet.Providers()...)
283
284 checkCacheForBlobs(t, cache, shootBlobs, shootBlobCount)
285 assert.Equal(t, cache.Len(), shootBlobCount, "cache has more blobs than expected")
286
287
288
289
290
291 assert.Equal(t, (shootBlobCount-redpandaBlobCount-1)+misses, recorder.misses())
292 }
293
294 func sameDigest(t *testing.T, exp, actual oci.Artifact) {
295 ad, err := actual.Digest()
296 assert.NoError(t, err)
297
298 ed, err := exp.Digest()
299 assert.NoError(t, err)
300
301 assert.Equal(t, ed, ad)
302 }
303
304 func testTag(t *testing.T, r, n, tag string) name.Reference {
305 t.Helper()
306 ref, err := name.NewTag(fmt.Sprintf("%s/%s/%s:%s", rURL.Host, r, n, tag))
307 require.NoError(t, err)
308 return ref
309 }
310
311 func testDigest(t *testing.T, r, n string, a oci.Artifact) name.Reference {
312 t.Helper()
313 d, err := a.Digest()
314 require.NoError(t, err)
315 ref, err := name.NewDigest(fmt.Sprintf("%s/%s/%s@%s", rURL.Host, r, n, d))
316 require.NoError(t, err)
317 return ref
318 }
319
320 func testColdGet(t *testing.T, c Cache, ref name.Reference, blobs map[string][]v1.Hash) oci.Artifact {
321 t.Helper()
322
323 cached, err := c.Get(ref)
324 require.NoError(t, err)
325
326 d, err := cached.Digest()
327 require.NoError(t, err)
328
329 assert.True(t, c.Exists(d), "%s expected to be in cache", d)
330
331 for objType, hashes := range blobs {
332 for _, hash := range hashes {
333 if hash != d {
334 assert.False(t, c.Exists(hash),
335 "%s %s unexpectedly in cache", objType, hash)
336 }
337 }
338 }
339
340 return cached
341 }
342
343 func checkCacheForBlobs(t *testing.T, c Cache, blobs map[string][]v1.Hash, expCount int) {
344 t.Helper()
345
346 var missing []v1.Hash
347 for _, hashes := range blobs {
348 for _, hash := range hashes {
349 if !c.Exists(hash) {
350 missing = append(missing, hash)
351 }
352 }
353 }
354 if len(missing) > 0 {
355 t.Errorf("%d/%d, expected digests not in the cache: %v", len(missing), expCount, missing)
356 }
357 }
358
359 func testGet(t *testing.T, c Cache, ref name.Reference, exp oci.Artifact, opts ...GetOption) {
360 t.Helper()
361 t.Log("ref", ref)
362 a, err := c.Get(ref, opts...)
363 assert.NoError(t, err)
364 h, err := a.Digest()
365 assert.NoError(t, err)
366 assert.True(t, c.Exists(h))
367 assert.NoError(t, err)
368 sameDigest(t, exp, a)
369 }
370
371
372
373 func loadArtifact(p *fixtures.Path, refstr string) oci.Artifact {
374 ref, err := name.ParseReference(refstr)
375 if err != nil {
376 panic(err)
377 }
378
379 a, err := p.Get(ref)
380 if err != nil {
381 panic(err)
382 }
383
384 return a
385 }
386
387
388
389
390
391
392
393
394 func analyzeArtifact(a oci.Artifact) (map[string][]v1.Hash, int, error) {
395 result := make(map[string][]v1.Hash)
396 count := 0
397
398 if err := walk.Walk(a, &walk.Fns{
399 Index: func(ii v1.ImageIndex, _ v1.ImageIndex) error {
400 h, err := ii.Digest()
401 if err != nil {
402 return err
403 }
404 for _, idxHash := range result["index"] {
405 if idxHash == h {
406 return nil
407 }
408 }
409 result["index"] = append(result["index"], h)
410 count++
411 return nil
412 },
413 Image: func(img v1.Image, _ v1.ImageIndex) error {
414 h, err := img.Digest()
415 if err != nil {
416 return err
417 }
418 for _, imgHash := range result["image"] {
419 if imgHash == h {
420 return nil
421 }
422 }
423 result["image"] = append(result["image"], h)
424 count++
425 return nil
426 },
427 Layer: func(l layer.Layer, _ v1.Image) error {
428 h, err := l.Digest()
429 if err != nil {
430 return err
431 }
432 for _, layerHash := range result["layer"] {
433 if layerHash == h {
434 return nil
435 }
436 }
437 result["layer"] = append(result["layer"], h)
438 count++
439 return nil
440 },
441 }); err != nil {
442 return nil, 0, err
443 }
444
445 return result, count, nil
446 }
447
448
449
450 func computeArtifact(t *testing.T, a oci.Artifact, providers ...cluster.Provider) {
451 t.Helper()
452
453 fn := func(pallet.Pallet, []layer.Layer) error { return nil }
454 for _, provider := range providers {
455 t.Log("unpacked provider", provider)
456 assert.NoError(t, unpack.Walk(a, fn, unpack.ForProvider(provider)))
457 }
458 }
459
View as plain text