v2.16.0 ebd2ab3

manifest_test.go RawHistoryFolder

1package filesystem
2
3import (
4	"os"
5	"path/filepath"
6	"testing"
7	"time"
8
9	"github.com/stretchr/testify/assert"
10	"github.com/stretchr/testify/require"
11)
12
13func TestNewManifestStorage(t *testing.T) {
14	tmpDir := t.TempDir()
15	log := testLogger()
16
17	storage, err := NewManifestStorage(tmpDir, log)
18
19	require.NoError(t, err)
20	assert.NotNil(t, storage)
21
22	// Verify repositories directory was created
23	assert.DirExists(t, filepath.Join(tmpDir, "repositories"))
24}
25
26func TestNewManifestStorage_InvalidPath(t *testing.T) {
27	log := testLogger()
28
29	_, err := NewManifestStorage("/nonexistent/deeply/nested/path/that/should/fail", log)
30
31	assert.Error(t, err)
32}
33
34func TestManifestStorage_PutAndGetManifest(t *testing.T) {
35	tmpDir := t.TempDir()
36	log := testLogger()
37
38	storage, err := NewManifestStorage(tmpDir, log)
39	require.NoError(t, err)
40
41	manifestData := []byte(`{"schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.v2+json"}`)
42	contentType := "application/vnd.docker.distribution.manifest.v2+json"
43
44	// Put manifest
45	err = storage.PutManifest("myapp", "latest", contentType, manifestData)
46	require.NoError(t, err)
47
48	// Get manifest
49	data, ct, err := storage.GetManifest("myapp", "latest")
50	require.NoError(t, err)
51
52	assert.Equal(t, manifestData, data)
53	assert.Equal(t, contentType, ct)
54}
55
56func TestManifestStorage_GetManifest_NotFound(t *testing.T) {
57	tmpDir := t.TempDir()
58	log := testLogger()
59
60	storage, err := NewManifestStorage(tmpDir, log)
61	require.NoError(t, err)
62
63	data, ct, err := storage.GetManifest("notexists", "latest")
64
65	assert.Error(t, err)
66	assert.Nil(t, data)
67	assert.Empty(t, ct)
68	assert.Contains(t, err.Error(), "manifest not found")
69}
70
71func TestManifestStorage_GetManifestModTime(t *testing.T) {
72	tmpDir := t.TempDir()
73	log := testLogger()
74
75	storage, err := NewManifestStorage(tmpDir, log)
76	require.NoError(t, err)
77
78	manifestData := []byte(`{"schemaVersion": 2}`)
79	contentType := "application/vnd.docker.distribution.manifest.v2+json"
80
81	err = storage.PutManifest("myapp", "latest", contentType, manifestData)
82	require.NoError(t, err)
83
84	modTime, err := storage.GetManifestModTime("myapp", "latest")
85	require.NoError(t, err)
86
87	manifestPath := filepath.Join(tmpDir, "repositories", "myapp", "manifests", "latest")
88	info, err := os.Stat(manifestPath)
89	require.NoError(t, err)
90
91	assert.WithinDuration(t, info.ModTime(), modTime, time.Second)
92}
93
94func TestManifestStorage_GetManifestModTime_NotFound(t *testing.T) {
95	tmpDir := t.TempDir()
96	log := testLogger()
97
98	storage, err := NewManifestStorage(tmpDir, log)
99	require.NoError(t, err)
100
101	_, err = storage.GetManifestModTime("notexists", "latest")
102
103	assert.Error(t, err)
104	assert.Contains(t, err.Error(), "manifest not found")
105}
106
107func TestManifestStorage_GetManifest_FallbackContentType(t *testing.T) {
108	tmpDir := t.TempDir()
109	log := testLogger()
110
111	storage, err := NewManifestStorage(tmpDir, log)
112	require.NoError(t, err)
113
114	// Create manifest file directly without content type file
115	manifestDir := filepath.Join(tmpDir, "repositories", "myapp", "manifests")
116	err = os.MkdirAll(manifestDir, 0750)
117	require.NoError(t, err)
118
119	manifestData := []byte(`{"schemaVersion": 2}`)
120	err = os.WriteFile(filepath.Join(manifestDir, "latest"), manifestData, 0600)
121	require.NoError(t, err)
122
123	// Get manifest should use fallback content type
124	data, ct, err := storage.GetManifest("myapp", "latest")
125	require.NoError(t, err)
126
127	assert.Equal(t, manifestData, data)
128	assert.Equal(t, "application/vnd.docker.distribution.manifest.v2+json", ct)
129}
130
131func TestManifestStorage_DeleteManifest(t *testing.T) {
132	tmpDir := t.TempDir()
133	log := testLogger()
134
135	storage, err := NewManifestStorage(tmpDir, log)
136	require.NoError(t, err)
137
138	manifestData := []byte(`{"schemaVersion": 2}`)
139	contentType := "application/vnd.docker.distribution.manifest.v2+json"
140
141	// Put manifest
142	err = storage.PutManifest("myapp", "latest", contentType, manifestData)
143	require.NoError(t, err)
144
145	// Verify it exists
146	_, _, err = storage.GetManifest("myapp", "latest")
147	require.NoError(t, err)
148
149	// Delete manifest
150	err = storage.DeleteManifest("myapp", "latest")
151	require.NoError(t, err)
152
153	// Verify it's gone
154	_, _, err = storage.GetManifest("myapp", "latest")
155	assert.Error(t, err)
156}
157
158func TestManifestStorage_DeleteManifest_NotFound(t *testing.T) {
159	tmpDir := t.TempDir()
160	log := testLogger()
161
162	storage, err := NewManifestStorage(tmpDir, log)
163	require.NoError(t, err)
164
165	err = storage.DeleteManifest("notexists", "latest")
166
167	assert.Error(t, err)
168	assert.Contains(t, err.Error(), "manifest not found")
169}
170
171func TestManifestStorage_ListTags(t *testing.T) {
172	tmpDir := t.TempDir()
173	log := testLogger()
174
175	storage, err := NewManifestStorage(tmpDir, log)
176	require.NoError(t, err)
177
178	manifestData := []byte(`{"schemaVersion": 2}`)
179	contentType := "application/vnd.docker.distribution.manifest.v2+json"
180
181	// Put multiple manifests
182	err = storage.PutManifest("myapp", "latest", contentType, manifestData)
183	require.NoError(t, err)
184	err = storage.PutManifest("myapp", "v1.0", contentType, manifestData)
185	require.NoError(t, err)
186	err = storage.PutManifest("myapp", "v2.0", contentType, manifestData)
187	require.NoError(t, err)
188
189	// List tags
190	tags, err := storage.ListTags("myapp")
191	require.NoError(t, err)
192
193	assert.Len(t, tags, 3)
194	assert.Contains(t, tags, "latest")
195	assert.Contains(t, tags, "v1.0")
196	assert.Contains(t, tags, "v2.0")
197}
198
199func TestManifestStorage_ListTags_Empty(t *testing.T) {
200	tmpDir := t.TempDir()
201	log := testLogger()
202
203	storage, err := NewManifestStorage(tmpDir, log)
204	require.NoError(t, err)
205
206	// List tags for non-existent repo
207	tags, err := storage.ListTags("notexists")
208	require.NoError(t, err)
209
210	assert.Empty(t, tags)
211}
212
213func TestManifestStorage_ListTags_SkipsDigestReference(t *testing.T) {
214	tmpDir := t.TempDir()
215	log := testLogger()
216
217	storage, err := NewManifestStorage(tmpDir, log)
218	require.NoError(t, err)
219
220	manifestData := []byte(`{"schemaVersion": 2}`)
221	contentType := "application/vnd.docker.distribution.manifest.v2+json"
222
223	err = storage.PutManifest("myapp", "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", contentType, manifestData)
224	require.NoError(t, err)
225
226	tags, err := storage.ListTags("myapp")
227	require.NoError(t, err)
228
229	assert.Empty(t, tags)
230}
231
232func TestManifestStorage_ListTags_AfterDelete(t *testing.T) {
233	tmpDir := t.TempDir()
234	log := testLogger()
235
236	storage, err := NewManifestStorage(tmpDir, log)
237	require.NoError(t, err)
238
239	manifestData := []byte(`{"schemaVersion": 2}`)
240	contentType := "application/vnd.docker.distribution.manifest.v2+json"
241
242	// Put manifests
243	err = storage.PutManifest("myapp", "latest", contentType, manifestData)
244	require.NoError(t, err)
245	err = storage.PutManifest("myapp", "v1.0", contentType, manifestData)
246	require.NoError(t, err)
247
248	// Delete one
249	err = storage.DeleteManifest("myapp", "v1.0")
250	require.NoError(t, err)
251
252	// List tags
253	tags, err := storage.ListTags("myapp")
254	require.NoError(t, err)
255
256	assert.Len(t, tags, 1)
257	assert.Contains(t, tags, "latest")
258	assert.NotContains(t, tags, "v1.0")
259}
260
261func TestManifestStorage_ListRepositories(t *testing.T) {
262	tmpDir := t.TempDir()
263	log := testLogger()
264
265	storage, err := NewManifestStorage(tmpDir, log)
266	require.NoError(t, err)
267
268	manifestData := []byte(`{"schemaVersion": 2}`)
269	contentType := "application/vnd.docker.distribution.manifest.v2+json"
270
271	// Put manifests for different repos
272	err = storage.PutManifest("app1", "latest", contentType, manifestData)
273	require.NoError(t, err)
274	err = storage.PutManifest("app2", "latest", contentType, manifestData)
275	require.NoError(t, err)
276	err = storage.PutManifest("org/app3", "latest", contentType, manifestData)
277	require.NoError(t, err)
278
279	// List repositories
280	repos, err := storage.ListRepositories()
281	require.NoError(t, err)
282
283	assert.Len(t, repos, 3)
284	assert.Contains(t, repos, "app1")
285	assert.Contains(t, repos, "app2")
286	assert.Contains(t, repos, "org/app3")
287}
288
289func TestManifestStorage_ListRepositories_Empty(t *testing.T) {
290	tmpDir := t.TempDir()
291	log := testLogger()
292
293	storage, err := NewManifestStorage(tmpDir, log)
294	require.NoError(t, err)
295
296	repos, err := storage.ListRepositories()
297	require.NoError(t, err)
298
299	assert.Empty(t, repos)
300}
301
302func TestManifestStorage_NestedRepositoryNames(t *testing.T) {
303	tmpDir := t.TempDir()
304	log := testLogger()
305
306	storage, err := NewManifestStorage(tmpDir, log)
307	require.NoError(t, err)
308
309	manifestData := []byte(`{"schemaVersion": 2}`)
310	contentType := "application/vnd.docker.distribution.manifest.v2+json"
311
312	tests := []struct {
313		name      string
314		reference string
315	}{
316		{"simple", "latest"},
317		{"org/project", "v1.0"},
318		{"org/team/project", "v2.0"},
319		{"deeply/nested/repo/name", "sha256:abc123"},
320	}
321
322	for _, tt := range tests {
323		t.Run(tt.name, func(t *testing.T) {
324			err := storage.PutManifest(tt.name, tt.reference, contentType, manifestData)
325			require.NoError(t, err)
326
327			data, ct, err := storage.GetManifest(tt.name, tt.reference)
328			require.NoError(t, err)
329			assert.Equal(t, manifestData, data)
330			assert.Equal(t, contentType, ct)
331		})
332	}
333}
334
335func TestManifestStorage_ContentTypes(t *testing.T) {
336	tmpDir := t.TempDir()
337	log := testLogger()
338
339	storage, err := NewManifestStorage(tmpDir, log)
340	require.NoError(t, err)
341
342	tests := []struct {
343		name        string
344		contentType string
345	}{
346		{"v2 manifest", "application/vnd.docker.distribution.manifest.v2+json"},
347		{"v1 manifest", "application/vnd.docker.distribution.manifest.v1+json"},
348		{"OCI manifest", "application/vnd.oci.image.manifest.v1+json"},
349		{"OCI index", "application/vnd.oci.image.index.v1+json"},
350		{"manifest list", "application/vnd.docker.distribution.manifest.list.v2+json"},
351	}
352
353	for i, tt := range tests {
354		t.Run(tt.name, func(t *testing.T) {
355			repoName := "test-repo"
356			reference := "tag-" + string(rune('a'+i))
357			manifestData := []byte(`{"schemaVersion": 2}`)
358
359			err := storage.PutManifest(repoName, reference, tt.contentType, manifestData)
360			require.NoError(t, err)
361
362			_, ct, err := storage.GetManifest(repoName, reference)
363			require.NoError(t, err)
364			assert.Equal(t, tt.contentType, ct)
365		})
366	}
367}
368
369func TestManifestStorage_DuplicateTag(t *testing.T) {
370	tmpDir := t.TempDir()
371	log := testLogger()
372
373	storage, err := NewManifestStorage(tmpDir, log)
374	require.NoError(t, err)
375
376	contentType := "application/vnd.docker.distribution.manifest.v2+json"
377
378	// Put first manifest
379	manifestData1 := []byte(`{"schemaVersion": 2, "version": 1}`)
380	err = storage.PutManifest("myapp", "latest", contentType, manifestData1)
381	require.NoError(t, err)
382
383	// Put second manifest with same tag (overwrite)
384	manifestData2 := []byte(`{"schemaVersion": 2, "version": 2}`)
385	err = storage.PutManifest("myapp", "latest", contentType, manifestData2)
386	require.NoError(t, err)
387
388	// Get manifest should return the latest one
389	data, _, err := storage.GetManifest("myapp", "latest")
390	require.NoError(t, err)
391	assert.Equal(t, manifestData2, data)
392
393	// Tags list should only have one entry
394	tags, err := storage.ListTags("myapp")
395	require.NoError(t, err)
396	assert.Len(t, tags, 1)
397}
398
399func TestManifestStorage_LargeManifest(t *testing.T) {
400	tmpDir := t.TempDir()
401	log := testLogger()
402
403	storage, err := NewManifestStorage(tmpDir, log)
404	require.NoError(t, err)
405
406	// Create a large manifest (simulating one with many layers)
407	largeManifest := make([]byte, 1024*1024) // 1MB
408	for i := range largeManifest {
409		largeManifest[i] = byte('a' + (i % 26))
410	}
411
412	contentType := "application/vnd.docker.distribution.manifest.v2+json"
413
414	err = storage.PutManifest("large-app", "latest", contentType, largeManifest)
415	require.NoError(t, err)
416
417	data, _, err := storage.GetManifest("large-app", "latest")
418	require.NoError(t, err)
419	assert.Equal(t, largeManifest, data)
420}
421
422func TestManifestStorage_SpecialCharactersInReference(t *testing.T) {
423	tmpDir := t.TempDir()
424	log := testLogger()
425
426	storage, err := NewManifestStorage(tmpDir, log)
427	require.NoError(t, err)
428
429	manifestData := []byte(`{"schemaVersion": 2}`)
430	contentType := "application/vnd.docker.distribution.manifest.v2+json"
431
432	// SHA256 digests are valid references
433	reference := "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
434
435	err = storage.PutManifest("myapp", reference, contentType, manifestData)
436	require.NoError(t, err)
437
438	data, _, err := storage.GetManifest("myapp", reference)
439	require.NoError(t, err)
440	assert.Equal(t, manifestData, data)
441}
442
443func TestManifestStorage_CompleteWorkflow(t *testing.T) {
444	tmpDir := t.TempDir()
445	log := testLogger()
446
447	storage, err := NewManifestStorage(tmpDir, log)
448	require.NoError(t, err)
449
450	contentType := "application/vnd.docker.distribution.manifest.v2+json"
451
452	// Simulate a complete registry workflow
453	// 1. Push first version
454	v1Data := []byte(`{"schemaVersion": 2, "version": "1.0"}`)
455	err = storage.PutManifest("myorg/myapp", "v1.0", contentType, v1Data)
456	require.NoError(t, err)
457	err = storage.PutManifest("myorg/myapp", "latest", contentType, v1Data)
458	require.NoError(t, err)
459
460	// 2. Push second version
461	v2Data := []byte(`{"schemaVersion": 2, "version": "2.0"}`)
462	err = storage.PutManifest("myorg/myapp", "v2.0", contentType, v2Data)
463	require.NoError(t, err)
464	err = storage.PutManifest("myorg/myapp", "latest", contentType, v2Data)
465	require.NoError(t, err)
466
467	// 3. Verify tags
468	tags, err := storage.ListTags("myorg/myapp")
469	require.NoError(t, err)
470	assert.Len(t, tags, 3)
471
472	// 4. Verify latest points to v2
473	data, _, err := storage.GetManifest("myorg/myapp", "latest")
474	require.NoError(t, err)
475	assert.Equal(t, v2Data, data)
476
477	// 5. Verify v1 is still accessible
478	data, _, err = storage.GetManifest("myorg/myapp", "v1.0")
479	require.NoError(t, err)
480	assert.Equal(t, v1Data, data)
481
482	// 6. Delete old version
483	err = storage.DeleteManifest("myorg/myapp", "v1.0")
484	require.NoError(t, err)
485
486	// 7. Verify tags updated
487	tags, err = storage.ListTags("myorg/myapp")
488	require.NoError(t, err)
489	assert.Len(t, tags, 2)
490	assert.NotContains(t, tags, "v1.0")
491
492	// 8. List repositories
493	repos, err := storage.ListRepositories()
494	require.NoError(t, err)
495	assert.Contains(t, repos, "myorg/myapp")
496}
497
498func TestManifestStorage_UpdateTagsList_CleansLegacyDigestEntries(t *testing.T) {
499	tmpDir := t.TempDir()
500	log := testLogger()
501
502	storage, err := NewManifestStorage(tmpDir, log)
503	require.NoError(t, err)
504
505	manifestData := []byte(`{"schemaVersion": 2}`)
506	contentType := "application/vnd.docker.distribution.manifest.v2+json"
507
508	// Create initial tags
509	err = storage.PutManifest("myapp", "v1.0", contentType, manifestData)
510	require.NoError(t, err)
511	err = storage.PutManifest("myapp", "v2.0", contentType, manifestData)
512	require.NoError(t, err)
513
514	// Manually inject digest entries into tags.json to simulate legacy data
515	tagsPath := filepath.Join(tmpDir, "repositories", "myapp", "tags.json")
516
517	// Write the contaminated tags list directly
518	err = os.WriteFile(tagsPath, []byte(`["v1.0","v2.0","sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","sha256:a6b1c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"]`), 0600)
519	require.NoError(t, err)
520
521	// Verify tags.json contains digest entries
522	tagsBefore, err := storage.ListTags("myapp")
523	require.NoError(t, err)
524	require.Len(t, tagsBefore, 4, "Should have 4 entries (2 tags + 2 digests)")
525
526	// Add a new tag - this should trigger cleanup of legacy digest entries
527	err = storage.PutManifest("myapp", "v3.0", contentType, manifestData)
528	require.NoError(t, err)
529
530	// Verify that digest entries were cleaned up
531	tagsAfter, err := storage.ListTags("myapp")
532	require.NoError(t, err)
533	assert.Len(t, tagsAfter, 3, "Should have 3 entries (only tags, digests removed)")
534	assert.Contains(t, tagsAfter, "v1.0")
535	assert.Contains(t, tagsAfter, "v2.0")
536	assert.Contains(t, tagsAfter, "v3.0")
537	assert.NotContains(t, tagsAfter, "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
538	assert.NotContains(t, tagsAfter, "sha256:a6b1c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
539}
540
541func TestManifestStorage_RemoveFromTagsList_CleansLegacyDigestEntries(t *testing.T) {
542	tmpDir := t.TempDir()
543	log := testLogger()
544
545	storage, err := NewManifestStorage(tmpDir, log)
546	require.NoError(t, err)
547
548	manifestData := []byte(`{"schemaVersion": 2}`)
549	contentType := "application/vnd.docker.distribution.manifest.v2+json"
550
551	// Create initial tags
552	err = storage.PutManifest("myapp", "v1.0", contentType, manifestData)
553	require.NoError(t, err)
554	err = storage.PutManifest("myapp", "v2.0", contentType, manifestData)
555	require.NoError(t, err)
556	err = storage.PutManifest("myapp", "v3.0", contentType, manifestData)
557	require.NoError(t, err)
558
559	// Manually inject digest entries into tags.json to simulate legacy data
560	tagsPath := filepath.Join(tmpDir, "repositories", "myapp", "tags.json")
561	err = os.WriteFile(tagsPath, []byte(`["v1.0","v2.0","v3.0","sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","sha256:a6b1c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"]`), 0600)
562	require.NoError(t, err)
563
564	// Verify tags.json contains digest entries
565	tagsBefore, err := storage.ListTags("myapp")
566	require.NoError(t, err)
567	require.Len(t, tagsBefore, 5, "Should have 5 entries (3 tags + 2 digests)")
568
569	// Delete a tag - this should trigger cleanup of legacy digest entries
570	err = storage.DeleteManifest("myapp", "v2.0")
571	require.NoError(t, err)
572
573	// Verify that digest entries were cleaned up and v2.0 was removed
574	tagsAfter, err := storage.ListTags("myapp")
575	require.NoError(t, err)
576	assert.Len(t, tagsAfter, 2, "Should have 2 entries (only remaining tags, digests removed)")
577	assert.Contains(t, tagsAfter, "v1.0")
578	assert.Contains(t, tagsAfter, "v3.0")
579	assert.NotContains(t, tagsAfter, "v2.0")
580	assert.NotContains(t, tagsAfter, "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
581	assert.NotContains(t, tagsAfter, "sha256:a6b1c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
582}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close