manifest_test.go RawHistoryFolder

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

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