v2.10.2 705579a

manifest_test.go RawHistoryFolder

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

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