Skip to content

Commit b75f9a2

Browse files
authored
fix behavior of rewrite when registry is docker.io (#719)
1 parent 6d8a7cc commit b75f9a2

4 files changed

Lines changed: 217 additions & 13 deletions

File tree

cmd/hauler/cli/store/add.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,13 @@ func rewriteReference(ctx context.Context, s *store.Layout, oldRef name.Referenc
619619
// index.docker.io. Preserve the original registry when the source is non-docker.
620620
if newRegistry == "index.docker.io" && !strings.HasPrefix(rawRewrite, "docker.io") && !strings.HasPrefix(rawRewrite, "index.docker.io") {
621621
newRegistry = oldRegistry
622-
newRepo = strings.TrimPrefix(newRepo, "library/") //if rewrite has library/ prefix in path it is stripped off unless registry specified in rewrite
622+
rewriteRepo := strings.TrimPrefix(rawRewrite, "/")
623+
if i := strings.LastIndex(rewriteRepo, ":"); i != -1 {
624+
rewriteRepo = rewriteRepo[:i]
625+
}
626+
if !strings.HasPrefix(rewriteRepo, "library/") {
627+
newRepo = strings.TrimPrefix(newRepo, "library/")
628+
}
623629
}
624630
oldTotal := oldRepo + ":" + oldTag
625631
newTotal := newRepo + ":" + newTag
@@ -1452,6 +1458,7 @@ func fetchChart(ctx context.Context, s *store.Layout, j chartJob, tempRoot strin
14521458
// rewrite. A rewrite that omits a tag inherits ref's.
14531459
func rewriteChartReference(ctx context.Context, s *store.Layout, ref name.Reference, rewrite string) error {
14541460
rewrite = strings.TrimPrefix(rewrite, "/")
1461+
rawRewrite := rewrite
14551462
newRef, err := name.ParseReference(rewrite)
14561463
if err != nil {
14571464
// error... don't continue with a bad reference
@@ -1474,6 +1481,13 @@ func rewriteChartReference(ctx context.Context, s *store.Layout, ref name.Refere
14741481
// rename chart name in store
14751482
oldRepo := ref.Context().RepositoryStr()
14761483
newRepo := newRef.Context().RepositoryStr()
1484+
rewriteRepo := rawRewrite
1485+
if i := strings.LastIndex(rewriteRepo, ":"); i != -1 {
1486+
rewriteRepo = rewriteRepo[:i]
1487+
}
1488+
if !strings.HasPrefix(rewriteRepo, "library/") {
1489+
newRepo = strings.TrimPrefix(newRepo, "library/")
1490+
}
14771491
newTag := newRef.Identifier()
14781492
if tag, ok := newRef.(name.Tag); ok {
14791493
newTag = tag.TagStr()

cmd/hauler/cli/store/add_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,108 @@ func TestRewriteReference(t *testing.T) {
375375
// condition fires → registry reverts to host, no library/ to strip
376376
assertAnnotationsInStore(t, s, "newrepo/img:v2", host+"/newrepo/img:v2")
377377
})
378+
379+
// The library/-detection must look at rawRewrite's path (not the
380+
// go-containerregistry-normalized newRepo, which always carries "library/" for
381+
// single-segment repos), so that a rewrite which explicitly asks for
382+
// "library/..." is honored instead of being unconditionally stripped.
383+
384+
t.Run("path-only rewrite with explicit library/ prefix is preserved", func(t *testing.T) {
385+
s := newTestStore(t)
386+
seedStoreDescriptor(t, s, map[string]string{
387+
ocispec.AnnotationRefName: "library/nginx:latest",
388+
consts.ContainerdImageNameKey: "index.docker.io/library/nginx:latest",
389+
})
390+
391+
oldRef, _ := name.NewTag("nginx:latest")
392+
newRef, _ := name.NewTag("library/nginx:v2")
393+
rawRewrite := "library/nginx:v2"
394+
395+
if err := rewriteReference(ctx, s, oldRef, newRef, rawRewrite); err != nil {
396+
t.Fatalf("rewriteReference: %v", err)
397+
}
398+
// rewriteRepo (derived from rawRewrite) starts with "library/" → must be kept
399+
assertAnnotationsInStore(t, s, "library/nginx:v2", "index.docker.io/library/nginx:v2")
400+
})
401+
402+
t.Run("leading slash rewrite with explicit library/ prefix is preserved", func(t *testing.T) {
403+
s := newTestStore(t)
404+
seedStoreDescriptor(t, s, map[string]string{
405+
ocispec.AnnotationRefName: "library/nginx:latest",
406+
consts.ContainerdImageNameKey: "index.docker.io/library/nginx:latest",
407+
})
408+
409+
oldRef, _ := name.NewTag("nginx:latest")
410+
newRef, _ := name.NewTag("library/nginx:v2")
411+
// AddImageCmd passes the pre-trim rewrite string through as rawRewrite, so a
412+
// leading "/" must still be handled correctly here.
413+
rawRewrite := "/library/nginx:v2"
414+
415+
if err := rewriteReference(ctx, s, oldRef, newRef, rawRewrite); err != nil {
416+
t.Fatalf("rewriteReference: %v", err)
417+
}
418+
assertAnnotationsInStore(t, s, "library/nginx:v2", "index.docker.io/library/nginx:v2")
419+
})
420+
}
421+
422+
func TestRewriteChartReference(t *testing.T) {
423+
ctx := newTestContext(t)
424+
425+
// A chart rewritten to a bare single-segment name must not keep an erroneous
426+
// "library/" prefix picked up from go-containerregistry's docker hub
427+
// normalization, unless the rewrite explicitly asked for one.
428+
429+
t.Run("path-only rewrite strips library/ prefix from docker hub normalization", func(t *testing.T) {
430+
s := newTestStore(t)
431+
seedStoreDescriptor(t, s, map[string]string{
432+
ocispec.AnnotationRefName: "library/mychart:1.0.0",
433+
})
434+
435+
ref, _ := name.NewTag("mychart:1.0.0")
436+
if err := rewriteChartReference(ctx, s, ref, "mychart:2.0.0"); err != nil {
437+
t.Fatalf("rewriteChartReference: %v", err)
438+
}
439+
assertArtifactInStore(t, s, "mychart:2.0.0")
440+
})
441+
442+
t.Run("explicit library/ prefix in rewrite is preserved", func(t *testing.T) {
443+
s := newTestStore(t)
444+
seedStoreDescriptor(t, s, map[string]string{
445+
ocispec.AnnotationRefName: "library/mychart:1.0.0",
446+
})
447+
448+
ref, _ := name.NewTag("mychart:1.0.0")
449+
if err := rewriteChartReference(ctx, s, ref, "library/mychart:2.0.0"); err != nil {
450+
t.Fatalf("rewriteChartReference: %v", err)
451+
}
452+
assertArtifactInStore(t, s, "library/mychart:2.0.0")
453+
})
454+
455+
t.Run("leading slash rewrite with explicit library/ prefix is preserved", func(t *testing.T) {
456+
s := newTestStore(t)
457+
seedStoreDescriptor(t, s, map[string]string{
458+
ocispec.AnnotationRefName: "library/mychart:1.0.0",
459+
})
460+
461+
ref, _ := name.NewTag("mychart:1.0.0")
462+
if err := rewriteChartReference(ctx, s, ref, "/library/mychart:2.0.0"); err != nil {
463+
t.Fatalf("rewriteChartReference: %v", err)
464+
}
465+
assertArtifactInStore(t, s, "library/mychart:2.0.0")
466+
})
467+
468+
t.Run("rewrite omitting tag inherits the source tag", func(t *testing.T) {
469+
s := newTestStore(t)
470+
seedStoreDescriptor(t, s, map[string]string{
471+
ocispec.AnnotationRefName: "library/mychart:1.0.0",
472+
})
473+
474+
ref, _ := name.NewTag("mychart:1.0.0")
475+
if err := rewriteChartReference(ctx, s, ref, "myneworg/mychart"); err != nil {
476+
t.Fatalf("rewriteChartReference: %v", err)
477+
}
478+
assertArtifactInStore(t, s, "myneworg/mychart:1.0.0")
479+
})
378480
}
379481

380482
// --------------------------------------------------------------------------

cmd/hauler/cli/store/info.go

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,26 @@ func newItemWithDigest(s *store.Layout, digestStr string, desc ocispec.Descripto
562562
return item
563563
}
564564

565+
// resolveDisplayReference returns the fully-qualified reference string to display
566+
// for desc. ContainerdImageNameKey already holds the canonical "registry/repo:tag"
567+
// string exactly as computed when the artifact was stored (see rewriteReference in
568+
// cmd/hauler/cli/store/add.go), so it's used verbatim. Re-parsing it through
569+
// name.ParseReference and calling .Name() would re-trigger go-containerregistry's
570+
// Docker Hub "library/" namespace normalization for any single-segment repo under
571+
// index.docker.io, undoing a rewrite like "hello-world-custom" back to
572+
// "library/hello-world-custom". AnnotationRefName, used as a fallback, has no
573+
// registry component, so it still needs reference.Parse to fill one in.
574+
func resolveDisplayReference(desc ocispec.Descriptor) (string, error) {
575+
if refName := desc.Annotations[consts.ContainerdImageNameKey]; refName != "" {
576+
return refName, nil
577+
}
578+
ref, err := reference.Parse(desc.Annotations[ocispec.AnnotationRefName])
579+
if err != nil {
580+
return "", err
581+
}
582+
return ref.Name(), nil
583+
}
584+
565585
func newItem(s *store.Layout, desc ocispec.Descriptor, m ocispec.Manifest, plat string, o *flags.InfoOpts) item {
566586
var size int64 = 0
567587
for _, l := range m.Layers {
@@ -570,11 +590,7 @@ func newItem(s *store.Layout, desc ocispec.Descriptor, m ocispec.Manifest, plat
570590

571591
ctype := resolveCtype(desc, m.Config.MediaType)
572592

573-
refName := desc.Annotations[consts.ContainerdImageNameKey]
574-
if refName == "" {
575-
refName = desc.Annotations[ocispec.AnnotationRefName]
576-
}
577-
ref, err := reference.Parse(refName)
593+
refName, err := resolveDisplayReference(desc)
578594
if err != nil {
579595
return item{}
580596
}
@@ -584,7 +600,7 @@ func newItem(s *store.Layout, desc ocispec.Descriptor, m ocispec.Manifest, plat
584600
}
585601

586602
return item{
587-
Reference: ref.Name(),
603+
Reference: refName,
588604
Type: ctype,
589605
Platform: plat,
590606
Digest: desc.Digest.String(),
@@ -637,17 +653,13 @@ func fallbackItem(desc ocispec.Descriptor, plat string, problem store.BlobResult
637653
plat = "-"
638654
}
639655

640-
refName := desc.Annotations[consts.ContainerdImageNameKey]
641-
if refName == "" {
642-
refName = desc.Annotations[ocispec.AnnotationRefName]
643-
}
644-
ref, err := reference.Parse(refName)
656+
refName, err := resolveDisplayReference(desc)
645657
if err != nil {
646658
return item{}
647659
}
648660

649661
return item{
650-
Reference: ref.Name(),
662+
Reference: refName,
651663
Type: resolveCtype(desc, ""),
652664
Platform: plat,
653665
Layers: 0,

cmd/hauler/cli/store/info_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,82 @@ func TestNewItem(t *testing.T) {
215215
}
216216
}
217217

218+
func TestResolveDisplayReference(t *testing.T) {
219+
// ContainerdImageNameKey already holds the fully-qualified reference exactly as
220+
// computed by rewriteReference (see add.go), so it must be returned verbatim.
221+
// Re-parsing it through the reference package would re-trigger
222+
// go-containerregistry's docker hub "library/" normalization for a
223+
// single-segment repo, undoing a rewrite like "hello-world-custom" back to
224+
// "library/hello-world-custom".
225+
t.Run("ContainerdImageNameKey is used verbatim, without re-injecting library/", func(t *testing.T) {
226+
desc := ocispec.Descriptor{
227+
Annotations: map[string]string{
228+
consts.ContainerdImageNameKey: "index.docker.io/hello-world-custom:v2",
229+
ocispec.AnnotationRefName: "hello-world-custom:v2",
230+
},
231+
}
232+
got, err := resolveDisplayReference(desc)
233+
if err != nil {
234+
t.Fatalf("resolveDisplayReference: %v", err)
235+
}
236+
if want := "index.docker.io/hello-world-custom:v2"; got != want {
237+
t.Errorf("got %q, want %q", got, want)
238+
}
239+
})
240+
241+
t.Run("falls back to AnnotationRefName parsed when ContainerdImageNameKey absent", func(t *testing.T) {
242+
desc := ocispec.Descriptor{
243+
Annotations: map[string]string{
244+
ocispec.AnnotationRefName: "hello-world-custom:v2",
245+
},
246+
}
247+
got, err := resolveDisplayReference(desc)
248+
if err != nil {
249+
t.Fatalf("resolveDisplayReference: %v", err)
250+
}
251+
if want := "hauler/hello-world-custom:v2"; got != want {
252+
t.Errorf("got %q, want %q", got, want)
253+
}
254+
})
255+
256+
t.Run("returns error when fallback ref cannot be parsed", func(t *testing.T) {
257+
desc := ocispec.Descriptor{Annotations: map[string]string{}}
258+
if _, err := resolveDisplayReference(desc); err == nil {
259+
t.Fatal("expected error, got nil")
260+
}
261+
})
262+
}
263+
264+
func TestNewItem_ReferenceUsesContainerdImageNameVerbatim(t *testing.T) {
265+
desc := ocispec.Descriptor{
266+
Annotations: map[string]string{
267+
consts.ContainerdImageNameKey: "index.docker.io/hello-world-custom:v2",
268+
ocispec.AnnotationRefName: "hello-world-custom:v2",
269+
},
270+
}
271+
m := ocispec.Manifest{Config: ocispec.Descriptor{MediaType: consts.DockerConfigJSON}}
272+
o := &flags.InfoOpts{TypeFilter: "all"}
273+
274+
got := newItem(nil, desc, m, "linux/amd64", o)
275+
if want := "index.docker.io/hello-world-custom:v2"; got.Reference != want {
276+
t.Errorf("got Reference %q, want %q", got.Reference, want)
277+
}
278+
}
279+
280+
func TestFallbackItem_ReferenceUsesContainerdImageNameVerbatim(t *testing.T) {
281+
desc := ocispec.Descriptor{
282+
Annotations: map[string]string{
283+
consts.ContainerdImageNameKey: "index.docker.io/hello-world-custom:v2",
284+
ocispec.AnnotationRefName: "hello-world-custom:v2",
285+
},
286+
}
287+
288+
got := fallbackItem(desc, "linux/amd64", store.BlobResult{})
289+
if want := "index.docker.io/hello-world-custom:v2"; got.Reference != want {
290+
t.Errorf("got Reference %q, want %q", got.Reference, want)
291+
}
292+
}
293+
218294
func TestInfoCmd(t *testing.T) {
219295
ctx := newTestContext(t)
220296
s := newTestStore(t)

0 commit comments

Comments
 (0)