istio

Форк
0
/
cache_test.go 
960 строк · 34.2 Кб
1
// Copyright Istio Authors
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//     http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14

15
package wasm
16

17
import (
18
	"crypto/sha256"
19
	"crypto/tls"
20
	"encoding/hex"
21
	"errors"
22
	"fmt"
23
	"net/http"
24
	"net/http/httptest"
25
	"net/url"
26
	"os"
27
	"path/filepath"
28
	"strings"
29
	"sync/atomic"
30
	"testing"
31
	"time"
32

33
	"github.com/google/go-cmp/cmp"
34
	"github.com/google/go-cmp/cmp/cmpopts"
35
	"github.com/google/go-containerregistry/pkg/crane"
36
	"github.com/google/go-containerregistry/pkg/registry"
37
	"github.com/google/go-containerregistry/pkg/v1/empty"
38
	"github.com/google/go-containerregistry/pkg/v1/mutate"
39
	"github.com/google/go-containerregistry/pkg/v1/remote"
40
	"github.com/google/go-containerregistry/pkg/v1/types"
41

42
	extensions "istio.io/api/extensions/v1alpha1"
43
	"istio.io/istio/pkg/util/sets"
44
)
45

46
// Wasm header = magic number (4 bytes) + Wasm spec version (4 bytes).
47
var wasmHeader = append(wasmMagicNumber, []byte{0x1, 0x00, 0x00, 0x00}...)
48

49
func TestWasmCache(t *testing.T) {
50
	// Setup http server.
51
	tsNumRequest := int32(0)
52

53
	httpData := append(wasmHeader, []byte("data")...)
54
	invalidHTTPData := []byte("invalid binary")
55
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
56
		atomic.AddInt32(&tsNumRequest, 1)
57

58
		if r.URL.Path == "/different-url" {
59
			w.Write(append(httpData, []byte("different data")...))
60
		} else if r.URL.Path == "/invalid-wasm-header" {
61
			w.Write(invalidHTTPData)
62
		} else {
63
			w.Write(httpData)
64
		}
65
	}))
66
	defer ts.Close()
67
	httpDataSha := sha256.Sum256(httpData)
68
	httpDataCheckSum := hex.EncodeToString(httpDataSha[:])
69
	invalidHTTPDataSha := sha256.Sum256(invalidHTTPData)
70
	invalidHTTPDataCheckSum := hex.EncodeToString(invalidHTTPDataSha[:])
71

72
	reg := registry.New()
73
	// Set up a fake registry for OCI images.
74
	tos := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
75
		atomic.AddInt32(&tsNumRequest, 1)
76
		reg.ServeHTTP(w, r)
77
	}))
78
	defer tos.Close()
79
	ou, err := url.Parse(tos.URL)
80
	if err != nil {
81
		t.Fatal(err)
82
	}
83

84
	dockerImageDigest, invalidOCIImageDigest := setupOCIRegistry(t, ou.Host)
85

86
	ociWasmFile := fmt.Sprintf("%s.wasm", dockerImageDigest)
87
	ociURLWithTag := fmt.Sprintf("oci://%s/test/valid/docker:v0.1.0", ou.Host)
88
	ociURLWithLatestTag := fmt.Sprintf("oci://%s/test/valid/docker:latest", ou.Host)
89
	ociURLWithDigest := fmt.Sprintf("oci://%s/test/valid/docker@sha256:%s", ou.Host, dockerImageDigest)
90

91
	// Calculate cachehit sum.
92
	cacheHitSha := sha256.Sum256([]byte("cachehit"))
93
	cacheHitSum := hex.EncodeToString(cacheHitSha[:])
94

95
	cases := []struct {
96
		name                   string
97
		initialCachedModules   map[moduleKey]cacheEntry
98
		initialCachedChecksums map[string]*checksumEntry
99
		fetchURL               string
100
		purgeInterval          time.Duration
101
		wasmModuleExpiry       time.Duration
102
		checkPurgeTimeout      time.Duration
103
		getOptions             GetOptions
104
		wantCachedModules      map[moduleKey]*cacheEntry
105
		wantCachedChecksums    map[string]*checksumEntry
106
		wantFileName           string
107
		wantErrorMsgPrefix     string
108
		wantVisitServer        bool
109
	}{
110
		{
111
			name:                   "cache miss",
112
			initialCachedModules:   map[moduleKey]cacheEntry{},
113
			initialCachedChecksums: map[string]*checksumEntry{},
114
			fetchURL:               ts.URL,
115
			getOptions: GetOptions{
116
				Checksum:        httpDataCheckSum,
117
				ResourceName:    "namespace.resource",
118
				ResourceVersion: "0",
119
				RequestTimeout:  time.Second * 10,
120
			},
121
			wantCachedModules: map[moduleKey]*cacheEntry{
122
				{name: ts.URL, checksum: httpDataCheckSum}: {modulePath: httpDataCheckSum + ".wasm"},
123
			},
124
			wantCachedChecksums: map[string]*checksumEntry{
125
				ts.URL: {checksum: httpDataCheckSum, resourceVersionByResource: map[string]string{"namespace.resource": "0"}},
126
			},
127
			wantFileName:    fmt.Sprintf("%s.wasm", httpDataCheckSum),
128
			wantVisitServer: true,
129
		},
130
		{
131
			name: "cache hit",
132
			initialCachedModules: map[moduleKey]cacheEntry{
133
				{name: moduleNameFromURL(ts.URL), checksum: cacheHitSum}: {modulePath: "test.wasm"},
134
			},
135
			initialCachedChecksums: map[string]*checksumEntry{},
136
			fetchURL:               ts.URL,
137
			getOptions: GetOptions{
138
				Checksum:        cacheHitSum,
139
				ResourceName:    "namespace.resource",
140
				ResourceVersion: "0",
141
				RequestTimeout:  time.Second * 10,
142
			},
143
			wantCachedModules: map[moduleKey]*cacheEntry{
144
				{name: ts.URL, checksum: cacheHitSum}: {modulePath: "test.wasm"},
145
			},
146
			wantCachedChecksums: map[string]*checksumEntry{
147
				ts.URL: {checksum: cacheHitSum, resourceVersionByResource: map[string]string{"namespace.resource": "0"}},
148
			},
149
			wantFileName:    "test.wasm",
150
			wantVisitServer: false,
151
		},
152
		{
153
			name:                   "invalid scheme",
154
			initialCachedModules:   map[moduleKey]cacheEntry{},
155
			initialCachedChecksums: map[string]*checksumEntry{},
156
			fetchURL:               "foo://abc",
157
			getOptions: GetOptions{
158
				Checksum:       httpDataCheckSum,
159
				RequestTimeout: time.Second * 10,
160
			},
161
			wantCachedModules:   map[moduleKey]*cacheEntry{},
162
			wantCachedChecksums: map[string]*checksumEntry{},
163
			wantFileName:        fmt.Sprintf("%s.wasm", httpDataCheckSum),
164
			wantErrorMsgPrefix:  "unsupported Wasm module downloading URL scheme: foo",
165
			wantVisitServer:     false,
166
		},
167
		{
168
			name:                   "download failure",
169
			initialCachedModules:   map[moduleKey]cacheEntry{},
170
			initialCachedChecksums: map[string]*checksumEntry{},
171
			fetchURL:               "https://-invalid-url",
172
			getOptions: GetOptions{
173
				RequestTimeout: time.Second * 10,
174
			},
175
			wantCachedModules:   map[moduleKey]*cacheEntry{},
176
			wantCachedChecksums: map[string]*checksumEntry{},
177
			wantErrorMsgPrefix:  "wasm module download failed after 5 attempts, last error: Get \"https://-invalid-url\"",
178
			wantVisitServer:     false,
179
		},
180
		{
181
			name:                   "wrong checksum",
182
			initialCachedModules:   map[moduleKey]cacheEntry{},
183
			initialCachedChecksums: map[string]*checksumEntry{},
184
			fetchURL:               ts.URL,
185
			getOptions: GetOptions{
186
				Checksum:       "wrongchecksum\n",
187
				RequestTimeout: time.Second * 10,
188
			},
189
			wantCachedModules:   map[moduleKey]*cacheEntry{},
190
			wantCachedChecksums: map[string]*checksumEntry{},
191
			wantErrorMsgPrefix:  fmt.Sprintf("module downloaded from %v has checksum %s, which does not match", ts.URL, httpDataCheckSum),
192
			wantVisitServer:     true,
193
		},
194
		{
195
			// this might be common error in user configuration, that url was updated, but not checksum.
196
			// Test that downloading still proceeds and error returns.
197
			name: "different url same checksum",
198
			initialCachedModules: map[moduleKey]cacheEntry{
199
				{name: moduleNameFromURL(ts.URL), checksum: httpDataCheckSum}: {modulePath: fmt.Sprintf("%s.wasm", httpDataCheckSum)},
200
			},
201
			initialCachedChecksums: map[string]*checksumEntry{},
202
			fetchURL:               ts.URL + "/different-url",
203
			getOptions: GetOptions{
204
				Checksum:       httpDataCheckSum,
205
				RequestTimeout: time.Second * 10,
206
			},
207
			wantCachedModules: map[moduleKey]*cacheEntry{
208
				{name: ts.URL, checksum: httpDataCheckSum}: {modulePath: httpDataCheckSum + ".wasm"},
209
			},
210
			wantCachedChecksums: map[string]*checksumEntry{},
211
			wantErrorMsgPrefix:  fmt.Sprintf("module downloaded from %v/different-url has checksum", ts.URL),
212
			wantVisitServer:     true,
213
		},
214
		{
215
			name: "invalid wasm header",
216
			initialCachedModules: map[moduleKey]cacheEntry{
217
				{name: moduleNameFromURL(ts.URL), checksum: httpDataCheckSum}: {modulePath: fmt.Sprintf("%s.wasm", httpDataCheckSum)},
218
			},
219
			initialCachedChecksums: map[string]*checksumEntry{},
220
			fetchURL:               ts.URL + "/invalid-wasm-header",
221
			getOptions: GetOptions{
222
				Checksum:       invalidHTTPDataCheckSum,
223
				RequestTimeout: time.Second * 10,
224
			},
225
			wantCachedModules: map[moduleKey]*cacheEntry{
226
				{name: ts.URL, checksum: httpDataCheckSum}: {modulePath: httpDataCheckSum + ".wasm"},
227
			},
228
			wantCachedChecksums: map[string]*checksumEntry{},
229
			wantErrorMsgPrefix:  fmt.Sprintf("fetched Wasm binary from %s is invalid", ts.URL+"/invalid-wasm-header"),
230
			wantVisitServer:     true,
231
		},
232
		{
233
			name:                   "purge on expiry",
234
			initialCachedModules:   map[moduleKey]cacheEntry{},
235
			initialCachedChecksums: map[string]*checksumEntry{},
236
			fetchURL:               ts.URL,
237
			purgeInterval:          1 * time.Millisecond,
238
			wasmModuleExpiry:       1 * time.Millisecond,
239
			checkPurgeTimeout:      5 * time.Second,
240
			getOptions: GetOptions{
241
				Checksum:       httpDataCheckSum,
242
				RequestTimeout: time.Second * 10,
243
			},
244
			wantCachedModules:   map[moduleKey]*cacheEntry{},
245
			wantCachedChecksums: map[string]*checksumEntry{},
246
			wantFileName:        fmt.Sprintf("%s.wasm", httpDataCheckSum),
247
			wantVisitServer:     true,
248
		},
249
		{
250
			name:                   "fetch oci without digest",
251
			initialCachedModules:   map[moduleKey]cacheEntry{},
252
			initialCachedChecksums: map[string]*checksumEntry{},
253
			fetchURL:               ociURLWithTag,
254
			getOptions: GetOptions{
255
				ResourceName:    "namespace.resource",
256
				ResourceVersion: "0",
257
				RequestTimeout:  time.Second * 10,
258
			},
259
			wantCachedModules: map[moduleKey]*cacheEntry{
260
				{name: moduleNameFromURL(ociURLWithTag), checksum: dockerImageDigest}: {modulePath: ociWasmFile},
261
			},
262
			wantCachedChecksums: map[string]*checksumEntry{
263
				ociURLWithTag: {checksum: dockerImageDigest, resourceVersionByResource: map[string]string{"namespace.resource": "0"}},
264
			},
265
			wantFileName:    ociWasmFile,
266
			wantVisitServer: true,
267
		},
268
		{
269
			name:                   "fetch oci with digest",
270
			initialCachedModules:   map[moduleKey]cacheEntry{},
271
			initialCachedChecksums: map[string]*checksumEntry{},
272
			fetchURL:               ociURLWithTag,
273
			getOptions: GetOptions{
274
				Checksum:        dockerImageDigest,
275
				ResourceName:    "namespace.resource",
276
				ResourceVersion: "0",
277
				RequestTimeout:  time.Second * 10,
278
			},
279
			wantCachedModules: map[moduleKey]*cacheEntry{
280
				{name: moduleNameFromURL(ociURLWithTag), checksum: dockerImageDigest}: {modulePath: ociWasmFile},
281
			},
282
			wantCachedChecksums: map[string]*checksumEntry{
283
				ociURLWithTag: {checksum: dockerImageDigest, resourceVersionByResource: map[string]string{"namespace.resource": "0"}},
284
			},
285
			wantFileName:    ociWasmFile,
286
			wantVisitServer: true,
287
		},
288
		{
289
			name: "cache hit for tagged oci url with digest",
290
			initialCachedModules: map[moduleKey]cacheEntry{
291
				{name: moduleNameFromURL(ociURLWithTag), checksum: dockerImageDigest}: {modulePath: ociWasmFile},
292
			},
293
			initialCachedChecksums: map[string]*checksumEntry{},
294
			fetchURL:               ociURLWithTag,
295
			getOptions: GetOptions{
296
				Checksum:        dockerImageDigest,
297
				ResourceName:    "namespace.resource",
298
				ResourceVersion: "0",
299
				RequestTimeout:  time.Second * 10,
300
			},
301
			wantCachedModules: map[moduleKey]*cacheEntry{
302
				{name: moduleNameFromURL(ociURLWithTag), checksum: dockerImageDigest}: {modulePath: ociWasmFile},
303
			},
304
			wantCachedChecksums: map[string]*checksumEntry{
305
				ociURLWithTag: {checksum: dockerImageDigest, resourceVersionByResource: map[string]string{"namespace.resource": "0"}},
306
			},
307
			wantFileName:    ociWasmFile,
308
			wantVisitServer: false,
309
		},
310
		{
311
			name: "cache hit for tagged oci url without digest",
312
			initialCachedModules: map[moduleKey]cacheEntry{
313
				{name: moduleNameFromURL(ociURLWithTag), checksum: dockerImageDigest}: {modulePath: ociWasmFile},
314
			},
315
			initialCachedChecksums: map[string]*checksumEntry{
316
				ociURLWithTag: {
317
					checksum: dockerImageDigest,
318
					resourceVersionByResource: map[string]string{
319
						"namespace.resource": "123456",
320
					},
321
				},
322
			},
323
			fetchURL: ociURLWithTag,
324
			getOptions: GetOptions{
325
				ResourceName:    "namespace.resource",
326
				ResourceVersion: "0",
327
				RequestTimeout:  time.Second * 10,
328
			},
329
			wantCachedModules: map[moduleKey]*cacheEntry{
330
				{name: moduleNameFromURL(ociURLWithTag), checksum: dockerImageDigest}: {modulePath: ociWasmFile},
331
			},
332
			wantCachedChecksums: map[string]*checksumEntry{
333
				ociURLWithTag: {checksum: dockerImageDigest, resourceVersionByResource: map[string]string{"namespace.resource": "0"}},
334
			},
335
			wantFileName:    ociWasmFile,
336
			wantVisitServer: false,
337
		},
338
		{
339
			name: "cache miss for tagged oci url without digest",
340
			initialCachedModules: map[moduleKey]cacheEntry{
341
				{name: moduleNameFromURL(ociURLWithTag), checksum: dockerImageDigest}: {modulePath: ociWasmFile},
342
			},
343
			initialCachedChecksums: map[string]*checksumEntry{},
344
			getOptions: GetOptions{
345
				ResourceName:    "namespace.resource",
346
				ResourceVersion: "0",
347
				RequestTimeout:  time.Second * 10,
348
			},
349
			fetchURL: ociURLWithTag,
350
			wantCachedModules: map[moduleKey]*cacheEntry{
351
				{name: moduleNameFromURL(ociURLWithTag), checksum: dockerImageDigest}: {modulePath: ociWasmFile},
352
			},
353
			wantCachedChecksums: map[string]*checksumEntry{
354
				ociURLWithTag: {checksum: dockerImageDigest, resourceVersionByResource: map[string]string{"namespace.resource": "0"}},
355
			},
356
			wantFileName:    ociWasmFile,
357
			wantVisitServer: true,
358
		},
359
		{
360
			name: "cache hit for oci url suffixed by digest",
361
			initialCachedModules: map[moduleKey]cacheEntry{
362
				{name: moduleNameFromURL(ociURLWithTag), checksum: dockerImageDigest}: {modulePath: ociWasmFile},
363
			},
364
			initialCachedChecksums: map[string]*checksumEntry{},
365
			fetchURL:               ociURLWithDigest,
366
			getOptions: GetOptions{
367
				ResourceName:    "namespace.resource",
368
				ResourceVersion: "0",
369
				RequestTimeout:  time.Second * 10,
370
			},
371
			wantCachedModules: map[moduleKey]*cacheEntry{
372
				{name: moduleNameFromURL(ociURLWithTag), checksum: dockerImageDigest}: {modulePath: ociWasmFile},
373
			},
374
			wantCachedChecksums: map[string]*checksumEntry{},
375
			wantFileName:        ociWasmFile,
376
			wantVisitServer:     false,
377
		},
378
		{
379
			name: "pull due to pull-always policy when cache hit",
380
			initialCachedModules: map[moduleKey]cacheEntry{
381
				{name: moduleNameFromURL(ociURLWithTag), checksum: dockerImageDigest}: {modulePath: ociWasmFile},
382
			},
383
			initialCachedChecksums: map[string]*checksumEntry{
384
				ociURLWithTag: {
385
					checksum: dockerImageDigest,
386
					resourceVersionByResource: map[string]string{
387
						"namespace.resource": "123456",
388
					},
389
				},
390
			},
391
			fetchURL: ociURLWithTag,
392
			getOptions: GetOptions{
393
				ResourceName:    "namespace.resource",
394
				ResourceVersion: "0",
395
				RequestTimeout:  time.Second * 10,
396
				PullPolicy:      extensions.PullPolicy_Always,
397
			},
398
			wantCachedModules: map[moduleKey]*cacheEntry{
399
				{name: moduleNameFromURL(ociURLWithTag), checksum: dockerImageDigest}: {modulePath: ociWasmFile},
400
			},
401
			wantCachedChecksums: map[string]*checksumEntry{
402
				ociURLWithTag: {checksum: dockerImageDigest, resourceVersionByResource: map[string]string{"namespace.resource": "0"}},
403
			},
404
			wantFileName:    ociWasmFile,
405
			wantVisitServer: true,
406
		},
407
		{
408
			name: "do not pull due to resourceVersion is the same",
409
			initialCachedModules: map[moduleKey]cacheEntry{
410
				{name: moduleNameFromURL(ociURLWithTag), checksum: dockerImageDigest}: {modulePath: ociWasmFile},
411
			},
412
			initialCachedChecksums: map[string]*checksumEntry{
413
				ociURLWithTag: {
414
					checksum: dockerImageDigest,
415
					resourceVersionByResource: map[string]string{
416
						"namespace.resource": "123456",
417
					},
418
				},
419
			},
420
			fetchURL: ociURLWithTag,
421
			getOptions: GetOptions{
422
				ResourceName:    "namespace.resource",
423
				ResourceVersion: "123456",
424
				RequestTimeout:  time.Second * 10,
425
				PullPolicy:      extensions.PullPolicy_Always,
426
			},
427
			wantCachedModules: map[moduleKey]*cacheEntry{
428
				{name: moduleNameFromURL(ociURLWithTag), checksum: dockerImageDigest}: {modulePath: ociWasmFile},
429
			},
430
			wantCachedChecksums: map[string]*checksumEntry{
431
				ociURLWithTag: {checksum: dockerImageDigest, resourceVersionByResource: map[string]string{"namespace.resource": "123456"}},
432
			},
433
			wantFileName:    ociWasmFile,
434
			wantVisitServer: false,
435
		},
436
		{
437
			name: "pull due to if-not-present policy when cache hit",
438
			initialCachedModules: map[moduleKey]cacheEntry{
439
				{name: moduleNameFromURL(ociURLWithTag), checksum: dockerImageDigest}: {modulePath: ociWasmFile},
440
			},
441
			initialCachedChecksums: map[string]*checksumEntry{
442
				ociURLWithTag: {
443
					checksum: dockerImageDigest,
444
					resourceVersionByResource: map[string]string{
445
						"namespace.resource": "123456",
446
					},
447
				},
448
			},
449
			fetchURL: ociURLWithTag,
450
			getOptions: GetOptions{
451
				ResourceName:    "namespace.resource",
452
				ResourceVersion: "0",
453
				RequestTimeout:  time.Second * 10,
454
				PullPolicy:      extensions.PullPolicy_IfNotPresent,
455
			},
456
			wantCachedModules: map[moduleKey]*cacheEntry{
457
				{name: moduleNameFromURL(ociURLWithTag), checksum: dockerImageDigest}: {modulePath: ociWasmFile},
458
			},
459
			wantCachedChecksums: map[string]*checksumEntry{
460
				ociURLWithTag: {checksum: dockerImageDigest, resourceVersionByResource: map[string]string{"namespace.resource": "0"}},
461
			},
462
			wantFileName:    ociWasmFile,
463
			wantVisitServer: false,
464
		},
465
		{
466
			name: "do not pull in spite of pull-always policy due to checksum",
467
			initialCachedModules: map[moduleKey]cacheEntry{
468
				{name: moduleNameFromURL(ociURLWithTag), checksum: dockerImageDigest}: {modulePath: ociWasmFile},
469
			},
470
			fetchURL: ociURLWithTag,
471
			getOptions: GetOptions{
472
				Checksum:        dockerImageDigest,
473
				ResourceName:    "namespace.resource",
474
				ResourceVersion: "0",
475
				RequestTimeout:  time.Second * 10,
476
				PullPolicy:      extensions.PullPolicy_Always,
477
			},
478
			wantCachedModules: map[moduleKey]*cacheEntry{
479
				{name: moduleNameFromURL(ociURLWithTag), checksum: dockerImageDigest}: {modulePath: ociWasmFile},
480
			},
481
			wantCachedChecksums: map[string]*checksumEntry{
482
				ociURLWithTag: {checksum: dockerImageDigest, resourceVersionByResource: map[string]string{"namespace.resource": "0"}},
483
			},
484
			wantFileName:    ociWasmFile,
485
			wantVisitServer: false,
486
		},
487
		{
488
			name: "pull due to latest tag",
489
			initialCachedModules: map[moduleKey]cacheEntry{
490
				{name: moduleNameFromURL(ociURLWithLatestTag), checksum: dockerImageDigest}: {modulePath: ociWasmFile},
491
			},
492
			initialCachedChecksums: map[string]*checksumEntry{
493
				ociURLWithLatestTag: {
494
					checksum: dockerImageDigest,
495
					resourceVersionByResource: map[string]string{
496
						"namespace.resource": "123456",
497
					},
498
				},
499
			},
500
			fetchURL: ociURLWithLatestTag,
501
			getOptions: GetOptions{
502
				ResourceName:    "namespace.resource",
503
				ResourceVersion: "0",
504
				RequestTimeout:  time.Second * 10,
505
				PullPolicy:      extensions.PullPolicy_UNSPECIFIED_POLICY, // Default policy
506
			},
507
			wantCachedModules: map[moduleKey]*cacheEntry{
508
				{name: moduleNameFromURL(ociURLWithLatestTag), checksum: dockerImageDigest}: {modulePath: ociWasmFile},
509
			},
510
			wantCachedChecksums: map[string]*checksumEntry{
511
				ociURLWithLatestTag: {checksum: dockerImageDigest, resourceVersionByResource: map[string]string{"namespace.resource": "0"}},
512
			},
513
			wantFileName:    ociWasmFile,
514
			wantVisitServer: true,
515
		},
516
		{
517
			name: "do not pull in spite of latest tag due to checksum",
518
			initialCachedModules: map[moduleKey]cacheEntry{
519
				{name: moduleNameFromURL(ociURLWithLatestTag), checksum: dockerImageDigest}: {modulePath: ociWasmFile},
520
			},
521
			initialCachedChecksums: map[string]*checksumEntry{
522
				ociURLWithLatestTag: {
523
					checksum: dockerImageDigest,
524
					resourceVersionByResource: map[string]string{
525
						"namespace.resource": "123456",
526
					},
527
				},
528
			},
529
			fetchURL: ociURLWithLatestTag,
530
			getOptions: GetOptions{
531
				ResourceName:    "namespace.resource",
532
				ResourceVersion: "0",
533
				RequestTimeout:  time.Second * 10,
534
				Checksum:        dockerImageDigest,
535
				PullPolicy:      extensions.PullPolicy_UNSPECIFIED_POLICY, // Default policy
536
			},
537
			wantCachedModules: map[moduleKey]*cacheEntry{
538
				{name: moduleNameFromURL(ociURLWithLatestTag), checksum: dockerImageDigest}: {modulePath: ociWasmFile},
539
			},
540
			wantCachedChecksums: map[string]*checksumEntry{
541
				ociURLWithLatestTag: {checksum: dockerImageDigest, resourceVersionByResource: map[string]string{"namespace.resource": "0"}},
542
			},
543
			wantFileName:    ociWasmFile,
544
			wantVisitServer: false,
545
		},
546
		{
547
			name: "do not pull in spite of latest tag due to IfNotPresent policy",
548
			initialCachedModules: map[moduleKey]cacheEntry{
549
				{name: moduleNameFromURL(ociURLWithLatestTag), checksum: dockerImageDigest}: {modulePath: ociWasmFile},
550
			},
551
			initialCachedChecksums: map[string]*checksumEntry{
552
				ociURLWithLatestTag: {
553
					checksum: dockerImageDigest,
554
					resourceVersionByResource: map[string]string{
555
						"namespace.resource": "123456",
556
					},
557
				},
558
			},
559
			fetchURL: ociURLWithLatestTag,
560
			getOptions: GetOptions{
561
				ResourceName:    "namespace.resource",
562
				ResourceVersion: "0",
563
				RequestTimeout:  time.Second * 10,
564
				PullPolicy:      extensions.PullPolicy_IfNotPresent,
565
			},
566
			wantCachedModules: map[moduleKey]*cacheEntry{
567
				{name: moduleNameFromURL(ociURLWithLatestTag), checksum: dockerImageDigest}: {modulePath: ociWasmFile},
568
			},
569
			wantCachedChecksums: map[string]*checksumEntry{
570
				ociURLWithLatestTag: {checksum: dockerImageDigest, resourceVersionByResource: map[string]string{"namespace.resource": "0"}},
571
			},
572
			wantFileName:    ociWasmFile,
573
			wantVisitServer: false,
574
		},
575
		{
576
			name: "purge OCI image on expiry",
577
			initialCachedModules: map[moduleKey]cacheEntry{
578
				{name: moduleNameFromURL(ociURLWithTag) + "-purged", checksum: dockerImageDigest}: {
579
					modulePath:      ociWasmFile,
580
					referencingURLs: sets.New(ociURLWithTag),
581
				},
582
			},
583
			initialCachedChecksums: map[string]*checksumEntry{
584
				ociURLWithTag: {
585
					checksum: dockerImageDigest,
586
					resourceVersionByResource: map[string]string{
587
						"namespace.resource": "123456",
588
					},
589
				},
590
				"test-url": {
591
					checksum: "test-checksum",
592
					resourceVersionByResource: map[string]string{
593
						"namespace.resource2": "123456",
594
					},
595
				},
596
			},
597
			fetchURL:         ociURLWithDigest,
598
			purgeInterval:    1 * time.Millisecond,
599
			wasmModuleExpiry: 1 * time.Millisecond,
600
			getOptions: GetOptions{
601
				ResourceName:    "namespace.resource",
602
				ResourceVersion: "0",
603
				RequestTimeout:  time.Second * 10,
604
			},
605
			checkPurgeTimeout: 5 * time.Second,
606
			wantCachedModules: map[moduleKey]*cacheEntry{},
607
			wantCachedChecksums: map[string]*checksumEntry{
608
				"test-url": {checksum: "test-checksum", resourceVersionByResource: map[string]string{"namespace.resource2": "123456"}},
609
			},
610
			wantFileName:    ociWasmFile,
611
			wantVisitServer: true,
612
		},
613
		{
614
			name:                 "fetch oci timed out",
615
			initialCachedModules: map[moduleKey]cacheEntry{},
616
			fetchURL:             ociURLWithTag,
617
			getOptions: GetOptions{
618
				ResourceName:    "namespace.resource",
619
				ResourceVersion: "0",
620
				RequestTimeout:  0, // Cause timeout immediately.
621
			},
622
			wantCachedModules:   map[moduleKey]*cacheEntry{},
623
			wantCachedChecksums: map[string]*checksumEntry{},
624
			wantErrorMsgPrefix:  fmt.Sprintf("could not fetch Wasm OCI image: could not fetch manifest: Get \"https://%s/v2/\"", ou.Host),
625
			wantVisitServer:     false,
626
		},
627
		{
628
			name:                 "fetch oci with wrong digest",
629
			initialCachedModules: map[moduleKey]cacheEntry{},
630
			fetchURL:             ociURLWithTag,
631
			getOptions: GetOptions{
632
				ResourceName:    "namespace.resource",
633
				ResourceVersion: "0",
634
				RequestTimeout:  time.Second * 10,
635
				Checksum:        "wrongdigest",
636
			},
637
			wantCachedModules:   map[moduleKey]*cacheEntry{},
638
			wantCachedChecksums: map[string]*checksumEntry{},
639
			wantErrorMsgPrefix: fmt.Sprintf(
640
				"module downloaded from %v has checksum %v, which does not match:", fmt.Sprintf("oci://%s/test/valid/docker:v0.1.0", ou.Host), dockerImageDigest,
641
			),
642
			wantVisitServer: true,
643
		},
644
		{
645
			name:                 "fetch invalid oci",
646
			initialCachedModules: map[moduleKey]cacheEntry{},
647
			fetchURL:             fmt.Sprintf("oci://%s/test/invalid", ou.Host),
648
			getOptions: GetOptions{
649
				ResourceName:    "namespace.resource",
650
				ResourceVersion: "0",
651
				Checksum:        invalidOCIImageDigest,
652
				RequestTimeout:  time.Second * 10,
653
			},
654
			wantCachedModules:   map[moduleKey]*cacheEntry{},
655
			wantCachedChecksums: map[string]*checksumEntry{},
656
			wantErrorMsgPrefix: `could not fetch Wasm binary: the given image is in invalid format as an OCI image: 2 errors occurred:
657
	* could not parse as compat variant: invalid media type application/vnd.oci.image.layer.v1.tar (expect application/vnd.oci.image.layer.v1.tar+gzip)
658
	* could not parse as oci variant: number of layers must be 2 but got 1`,
659
			wantVisitServer: true,
660
		},
661
	}
662

663
	for _, c := range cases {
664
		t.Run(c.name, func(t *testing.T) {
665
			tmpDir := t.TempDir()
666
			options := defaultOptions()
667
			if c.purgeInterval != 0 {
668
				options.PurgeInterval = c.purgeInterval
669
			}
670
			if c.wasmModuleExpiry != 0 {
671
				options.ModuleExpiry = c.wasmModuleExpiry
672
			}
673
			cache := NewLocalFileCache(tmpDir, options)
674
			cache.httpFetcher.initialBackoff = time.Microsecond
675
			defer close(cache.stopChan)
676

677
			var cacheHitKey *moduleKey
678
			initTime := time.Now()
679
			cache.mux.Lock()
680
			for k, m := range c.initialCachedModules {
681
				filePath := generateModulePath(t, tmpDir, k.name, m.modulePath)
682
				err := os.WriteFile(filePath, []byte("data/\n"), 0o644)
683
				if err != nil {
684
					t.Fatalf("failed to write initial wasm module file %v", err)
685
				}
686
				mkey := moduleKey{name: k.name, checksum: k.checksum}
687

688
				cache.modules[mkey] = &cacheEntry{modulePath: filePath, last: initTime}
689
				if m.referencingURLs != nil {
690
					cache.modules[mkey].referencingURLs = m.referencingURLs.Copy()
691
				} else {
692
					cache.modules[mkey].referencingURLs = sets.New[string]()
693
				}
694

695
				if moduleNameFromURL(c.fetchURL) == k.name && c.getOptions.Checksum == k.checksum {
696
					cacheHitKey = &mkey
697
				}
698
			}
699

700
			for k, m := range c.initialCachedChecksums {
701
				cache.checksums[k] = m
702
			}
703

704
			// put the tmp dir into the module path.
705
			for k, m := range c.wantCachedModules {
706
				c.wantCachedModules[k].modulePath = generateModulePath(t, tmpDir, k.name, m.modulePath)
707
			}
708
			cache.mux.Unlock()
709

710
			atomic.StoreInt32(&tsNumRequest, 0)
711
			if c.getOptions.PullSecret == nil {
712
				c.getOptions.PullSecret = []byte{}
713
			}
714
			gotFilePath, gotErr := cache.Get(c.fetchURL, c.getOptions)
715
			serverVisited := atomic.LoadInt32(&tsNumRequest) > 0
716

717
			if c.checkPurgeTimeout > 0 {
718
				moduleDeleted := false
719
				for start := time.Now(); time.Since(start) < c.checkPurgeTimeout; {
720
					fileCount := 0
721
					err = filepath.Walk(tmpDir,
722
						func(path string, info os.FileInfo, err error) error {
723
							if err != nil {
724
								return err
725
							}
726
							if !info.IsDir() {
727
								fileCount++
728
							}
729
							return nil
730
						})
731
					// Check existence of module files. files should be deleted before timing out.
732
					if err == nil && fileCount == 0 {
733
						moduleDeleted = true
734
						break
735
					}
736
				}
737

738
				if !moduleDeleted {
739
					t.Fatalf("Wasm modules are not purged before purge timeout")
740
				}
741
			}
742

743
			cache.mux.Lock()
744
			if cacheHitKey != nil {
745
				if entry, ok := cache.modules[*cacheHitKey]; ok && entry.last == initTime {
746
					t.Errorf("Wasm module cache entry's last access time not updated after get operation, key: %v", *cacheHitKey)
747
				}
748
			}
749

750
			if diff := cmp.Diff(c.wantCachedModules, cache.modules,
751
				cmpopts.IgnoreFields(cacheEntry{}, "last", "referencingURLs"),
752
				cmp.AllowUnexported(cacheEntry{}),
753
			); diff != "" {
754
				t.Errorf("unexpected module cache: (-want, +got)\n%v", diff)
755
			}
756

757
			if diff := cmp.Diff(c.wantCachedChecksums, cache.checksums,
758
				cmp.AllowUnexported(checksumEntry{}),
759
			); diff != "" {
760
				t.Errorf("unexpected checksums: (-want, +got)\n%v", diff)
761
			}
762

763
			cache.mux.Unlock()
764

765
			wantFilePath := generateModulePath(t, tmpDir, moduleNameFromURL(c.fetchURL), c.wantFileName)
766
			if c.wantErrorMsgPrefix != "" {
767
				if gotErr == nil {
768
					t.Errorf("Wasm module cache lookup got no error, want error prefix `%v`", c.wantErrorMsgPrefix)
769
				} else if !strings.Contains(gotErr.Error(), c.wantErrorMsgPrefix) {
770
					t.Errorf("Wasm module cache lookup got error `%v`, want error prefix `%v`", gotErr, c.wantErrorMsgPrefix)
771
				}
772
			} else if gotFilePath != wantFilePath {
773
				t.Errorf("Wasm module local file path got %v, want %v", gotFilePath, wantFilePath)
774
				if gotErr != nil {
775
					t.Errorf("got unexpected error %v", gotErr)
776
				}
777
			}
778
			if c.wantVisitServer != serverVisited {
779
				t.Errorf("test wasm binary server encountered the unexpected visiting status got %v, want %v", serverVisited, c.wantVisitServer)
780
			}
781
		})
782
	}
783
}
784

785
func setupOCIRegistry(t *testing.T, host string) (dockerImageDigest, invalidOCIImageDigest string) {
786
	// Push *compat* variant docker image (others are well tested in imagefetcher's test and the behavior is consistent).
787
	ref := fmt.Sprintf("%s/test/valid/docker:v0.1.0", host)
788
	binary := append(wasmHeader, []byte("this is wasm plugin")...)
789
	transport := remote.DefaultTransport.(*http.Transport).Clone()
790
	transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} // nolint: gosec // test only code
791
	fetchOpt := crane.WithTransport(transport)
792

793
	// Create docker layer.
794
	l, err := newMockLayer(types.DockerLayer,
795
		map[string][]byte{"plugin.wasm": binary})
796
	if err != nil {
797
		t.Fatal(err)
798
	}
799
	img, err := mutate.Append(empty.Image, mutate.Addendum{Layer: l})
800
	if err != nil {
801
		t.Fatal(err)
802
	}
803

804
	// Set manifest type.
805
	manifest, err := img.Manifest()
806
	if err != nil {
807
		t.Fatal(err)
808
	}
809
	manifest.MediaType = types.DockerManifestSchema2
810

811
	// Push image to the registry.
812
	err = crane.Push(img, ref, fetchOpt)
813
	if err != nil {
814
		t.Fatal(err)
815
	}
816

817
	// Push image to the registry with latest tag as well
818
	ref = fmt.Sprintf("%s/test/valid/docker:latest", host)
819
	err = crane.Push(img, ref, fetchOpt)
820
	if err != nil {
821
		t.Fatal(err)
822
	}
823

824
	// Calculate sum
825
	d, _ := img.Digest()
826
	dockerImageDigest = d.Hex
827

828
	// Finally push the invalid image.
829
	ref = fmt.Sprintf("%s/test/invalid", host)
830
	l, err = newMockLayer(types.OCIUncompressedLayer, map[string][]byte{"not-wasm.txt": []byte("a")})
831
	if err != nil {
832
		t.Fatal(err)
833
	}
834
	img2, err := mutate.Append(empty.Image, mutate.Addendum{Layer: l})
835
	if err != nil {
836
		t.Fatal(err)
837
	}
838

839
	// Set manifest type so it will pass the docker parsing branch.
840
	img2 = mutate.MediaType(img2, types.OCIManifestSchema1)
841

842
	d, _ = img2.Digest()
843
	invalidOCIImageDigest = d.Hex
844

845
	// Push image to the registry.
846
	err = crane.Push(img2, ref, fetchOpt)
847
	if err != nil {
848
		t.Fatal(err)
849
	}
850
	return
851
}
852

853
func TestWasmCachePolicyChangesUsingHTTP(t *testing.T) {
854
	tmpDir := t.TempDir()
855
	cache := NewLocalFileCache(tmpDir, defaultOptions())
856
	defer close(cache.stopChan)
857

858
	gotNumRequest := 0
859
	binary1 := append(wasmHeader, 1)
860
	binary2 := append(wasmHeader, 2)
861

862
	// Create a test server which returns 0 for the first two calls, and returns 1 for the following calls.
863
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
864
		if gotNumRequest <= 1 {
865
			w.Write(binary1)
866
		} else {
867
			w.Write(binary2)
868
		}
869
		gotNumRequest++
870
	}))
871
	defer ts.Close()
872
	url1 := ts.URL
873
	url2 := ts.URL + "/next"
874
	wantFilePath1 := generateModulePath(t, tmpDir, url1, fmt.Sprintf("%x.wasm", sha256.Sum256(binary1)))
875
	wantFilePath2 := generateModulePath(t, tmpDir, url2, fmt.Sprintf("%x.wasm", sha256.Sum256(binary2)))
876
	var defaultPullPolicy extensions.PullPolicy
877

878
	testWasmGet := func(downloadURL string, policy extensions.PullPolicy, resourceVersion string, wantFilePath string, wantNumRequest int) {
879
		t.Helper()
880
		gotFilePath, err := cache.Get(downloadURL, GetOptions{
881
			ResourceName:    "namespace.resource",
882
			ResourceVersion: resourceVersion,
883
			RequestTimeout:  time.Second * 10,
884
			PullSecret:      []byte{},
885
			PullPolicy:      policy,
886
		})
887
		if err != nil {
888
			t.Fatalf("failed to download Wasm module: %v", err)
889
		}
890
		if gotFilePath != wantFilePath {
891
			t.Fatalf("wasm download path got %v want %v", gotFilePath, wantFilePath)
892
		}
893
		if gotNumRequest != wantNumRequest {
894
			t.Fatalf("wasm download call got %v want %v", gotNumRequest, wantNumRequest)
895
		}
896
	}
897

898
	// 1st time: Initially load the binary1.
899
	testWasmGet(url1, defaultPullPolicy, "1", wantFilePath1, 1)
900
	// 2nd time: Should not pull the binary and use the cache because defaultPullPolicy is IfNotPresent
901
	testWasmGet(url1, defaultPullPolicy, "2", wantFilePath1, 1)
902
	// 3rd time: Should not pull the binary because the policy is IfNotPresent
903
	testWasmGet(url1, extensions.PullPolicy_IfNotPresent, "3", wantFilePath1, 1)
904
	// 4th time: Should not pull the binary because the resource version is not changed
905
	testWasmGet(url1, extensions.PullPolicy_Always, "3", wantFilePath1, 1)
906
	// 5th time: Should pull the binary because the resource version is changed.
907
	testWasmGet(url1, extensions.PullPolicy_Always, "4", wantFilePath1, 2)
908
	// 6th time: Should pull the binary because URL is changed.
909
	testWasmGet(url2, extensions.PullPolicy_Always, "4", wantFilePath2, 3)
910
}
911

912
func TestAllInsecureServer(t *testing.T) {
913
	tmpDir := t.TempDir()
914
	options := defaultOptions()
915
	options.InsecureRegistries = sets.New("*")
916
	cache := NewLocalFileCache(tmpDir, options)
917
	defer close(cache.stopChan)
918

919
	// Set up a fake registry for OCI images with TLS Server
920
	// Without "insecure" option, this should cause an error.
921
	tos := httptest.NewTLSServer(registry.New())
922
	defer tos.Close()
923
	ou, err := url.Parse(tos.URL)
924
	if err != nil {
925
		t.Fatal(err)
926
	}
927

928
	dockerImageDigest, _ := setupOCIRegistry(t, ou.Host)
929
	ociURLWithTag := fmt.Sprintf("oci://%s/test/valid/docker:v0.1.0", ou.Host)
930
	var defaultPullPolicy extensions.PullPolicy
931

932
	gotFilePath, err := cache.Get(ociURLWithTag, GetOptions{
933
		ResourceName:    "namespace.resource",
934
		ResourceVersion: "123456",
935
		RequestTimeout:  time.Second * 10,
936
		PullSecret:      []byte{},
937
		PullPolicy:      defaultPullPolicy,
938
	})
939
	if err != nil {
940
		t.Fatalf("failed to download Wasm module: %v", err)
941
	}
942

943
	wantFilePath := generateModulePath(t, tmpDir, moduleNameFromURL(ociURLWithTag), fmt.Sprintf("%s.wasm", dockerImageDigest))
944
	if gotFilePath != wantFilePath {
945
		t.Errorf("Wasm module local file path got %v, want %v", gotFilePath, wantFilePath)
946
	}
947
}
948

949
func generateModulePath(t *testing.T, baseDir, resourceName, filename string) string {
950
	t.Helper()
951
	sha := sha256.Sum256([]byte(resourceName))
952
	moduleDir := filepath.Join(baseDir, hex.EncodeToString(sha[:]))
953
	if _, err := os.Stat(moduleDir); errors.Is(err, os.ErrNotExist) {
954
		err := os.Mkdir(moduleDir, 0o755)
955
		if err != nil {
956
			t.Fatalf("failed to create module dir %s: %v", moduleDir, err)
957
		}
958
	}
959
	return filepath.Join(moduleDir, filename)
960
}
961

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.