mirror of
https://github.com/hauler-dev/hauler.git
synced 2026-08-20 04:46:42 +00:00
digest only regression fix (#643)
Signed-off-by: Adam Martin <adam.martin@ranchergovernment.com>
This commit is contained in:
@@ -241,6 +241,63 @@ func TestLifecycle_Chart_AddSaveLoadExtract(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestLifecycle_DigestOnlyImage_AddSaveLoad exercises the full save/load round-trip
|
||||
// for an image added by digest only (no tag). Before fix #642, the image disappeared
|
||||
// from index.json after LoadCmd because CopyAll appended a double-@ digest.
|
||||
func TestLifecycle_DigestOnlyImage_AddSaveLoad(t *testing.T) {
|
||||
ctx := newTestContext(t)
|
||||
|
||||
// Step 1: seed a tagged image so we can get its digest
|
||||
srcHost, srcOpts := newLocalhostRegistry(t)
|
||||
srcImg := seedImage(t, srcHost, "lifecycle/digestonly", "v1", srcOpts...)
|
||||
hash, err := srcImg.Digest()
|
||||
if err != nil {
|
||||
t.Fatalf("srcImg.Digest: %v", err)
|
||||
}
|
||||
|
||||
// Step 2: add BY DIGEST (not tag) into store A
|
||||
storeA := newTestStore(t)
|
||||
rso := defaultRootOpts(storeA.Root)
|
||||
ro := defaultCliOpts()
|
||||
digestRef := srcHost + "/lifecycle/digestonly@" + hash.String()
|
||||
if err := storeImage(ctx, storeA, v1.Image{Name: digestRef}, "", false, rso, ro, ""); err != nil {
|
||||
t.Fatalf("storeImage by digest: %v", err)
|
||||
}
|
||||
// The image should be findable by its digest hex
|
||||
assertArtifactInStore(t, storeA, hash.Hex)
|
||||
|
||||
// Flush index.json for SaveCmd
|
||||
if err := storeA.SaveIndex(); err != nil {
|
||||
t.Fatalf("SaveIndex: %v", err)
|
||||
}
|
||||
|
||||
// Step 3: SaveCmd -> archive
|
||||
archivePath := filepath.Join(t.TempDir(), "lifecycle-digestonly.tar.zst")
|
||||
saveOpts := newSaveOpts(storeA.Root, archivePath)
|
||||
if err := SaveCmd(ctx, saveOpts, defaultRootOpts(storeA.Root), defaultCliOpts()); err != nil {
|
||||
t.Fatalf("SaveCmd: %v", err)
|
||||
}
|
||||
|
||||
// Step 4: LoadCmd -> fresh store B
|
||||
storeBDir := t.TempDir()
|
||||
loadOpts := &flags.LoadOpts{
|
||||
StoreRootOpts: defaultRootOpts(storeBDir),
|
||||
FileName: []string{archivePath},
|
||||
}
|
||||
if err := LoadCmd(ctx, loadOpts, defaultRootOpts(storeBDir), defaultCliOpts()); err != nil {
|
||||
t.Fatalf("LoadCmd: %v", err)
|
||||
}
|
||||
|
||||
storeB, err := store.NewLayout(storeBDir)
|
||||
if err != nil {
|
||||
t.Fatalf("store.NewLayout(storeB): %v", err)
|
||||
}
|
||||
|
||||
// Regression assertion: the digest-only image must survive the save/load round-trip.
|
||||
// Before fix 1, the image disappears from the loaded store's index.json.
|
||||
assertArtifactInStore(t, storeB, hash.Hex)
|
||||
}
|
||||
|
||||
// TestLifecycle_Remove_ThenSave verifies that removing one artifact from a store
|
||||
// with two file artifacts, then saving/loading, results in only the retained
|
||||
// artifact being present.
|
||||
|
||||
@@ -322,6 +322,20 @@ func (x *exports) record(ctx context.Context, index libv1.ImageIndex, desc libv1
|
||||
slices.Sort(xd.RepoTags)
|
||||
xd.RepoTags = slices.Compact(xd.RepoTags)
|
||||
ref = tag.Digest(digest)
|
||||
case name.Digest:
|
||||
// For digest-only refs, derive a deterministic, docker-valid tag from
|
||||
// the manifest digest so ctr/docker can import the image (#642).
|
||||
// Convention mirrors copy.go:229: "sha256-<hex>".
|
||||
named, err := referencev3.ParseNormalizedNamed(tag.Repository.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
familiarRepo := referencev3.FamiliarName(named)
|
||||
digestTag := strings.ReplaceAll(digest, ":", "-") // e.g. "sha256-498a..."
|
||||
repotag := familiarRepo + ":" + digestTag
|
||||
xd.RepoTags = append(xd.RepoTags[:], repotag)
|
||||
slices.Sort(xd.RepoTags)
|
||||
xd.RepoTags = slices.Compact(xd.RepoTags)
|
||||
}
|
||||
|
||||
l.Debugf("image [%s]: type=%s, size=%d", ref.Name(), desc.MediaType, desc.Size)
|
||||
|
||||
@@ -113,6 +113,37 @@ func TestWriteExportsManifest(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestWriteExportsManifest_DigestOnlyImageHasRepoTag(t *testing.T) {
|
||||
ctx := newTestContext(t)
|
||||
|
||||
// Seed a tagged image so we can reference it by digest
|
||||
host, srcOpts := newLocalhostRegistry(t)
|
||||
img := seedImage(t, host, "test/digestonly", "v1", srcOpts...)
|
||||
hash, err := img.Digest()
|
||||
if err != nil {
|
||||
t.Fatalf("img.Digest: %v", err)
|
||||
}
|
||||
|
||||
// Add the image BY DIGEST
|
||||
s := newTestStore(t)
|
||||
if err := s.AddImage(ctx, host+"/test/digestonly@"+hash.String(), "", false); err != nil {
|
||||
t.Fatalf("AddImage by digest: %v", err)
|
||||
}
|
||||
|
||||
if err := writeExportsManifest(ctx, s.Root, ""); err != nil {
|
||||
t.Fatalf("writeExportsManifest: %v", err)
|
||||
}
|
||||
|
||||
entries := readManifestJSON(t, s.Root)
|
||||
if len(entries) != 1 {
|
||||
t.Fatalf("expected 1 manifest entry, got %d", len(entries))
|
||||
}
|
||||
// Before fix 2, digest-only refs fall through the switch without setting RepoTags.
|
||||
if len(entries[0].RepoTags) == 0 {
|
||||
t.Errorf("expected at least one RepoTag for digest-only image, got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteExportsManifest_SkipsNonImages(t *testing.T) {
|
||||
ctx := newTestContext(t)
|
||||
|
||||
|
||||
+8
-2
@@ -753,9 +753,15 @@ func (l *Layout) CopyAll(ctx context.Context, to content.Target, toMapper func(s
|
||||
toRef = tr
|
||||
}
|
||||
|
||||
// Append the digest to help the target pusher identify the root descriptor
|
||||
// Format: "reference@digest" allows the pusher to update its index.json
|
||||
// Append the digest to help the target pusher identify the root descriptor.
|
||||
// AnnotationRefName for digest-only images already ends in "@sha256:...".
|
||||
// Strip any existing digest before appending the authoritative descriptor
|
||||
// digest so the destination pusher can match the root manifest. A double "@"
|
||||
// yields a digest the pusher never matches, leaving the image unindexed (#642).
|
||||
if desc.Digest.Validate() == nil {
|
||||
if at := strings.Index(toRef, "@"); at != -1 {
|
||||
toRef = toRef[:at]
|
||||
}
|
||||
toRef = fmt.Sprintf("%s@%s", toRef, desc.Digest)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user