Files
hauler/pkg/content/oci_test.go
2026-03-18 23:24:47 -04:00

282 lines
8.9 KiB
Go

package content
// oci_test.go covers the annotation-normalization correctness of LoadIndex()
// and ociPusher.Push(). Specifically, it verifies that descriptors returned
// by Walk() carry the normalized dev.hauler/... kind annotation value, not the
// legacy dev.cosignproject.cosign/... value that may be present on disk.
import (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"hauler.dev/go/hauler/pkg/consts"
)
// buildMinimalOCILayout writes the smallest valid OCI layout (oci-layout marker
// + index.json with the supplied descriptors) into dir. No blobs are written;
// this is sufficient for testing LoadIndex/Walk without a full store.
func buildMinimalOCILayout(t *testing.T, dir string, manifests []ocispec.Descriptor) {
t.Helper()
// oci-layout marker
layoutMarker := map[string]string{"imageLayoutVersion": "1.0.0"}
markerData, err := json.Marshal(layoutMarker)
if err != nil {
t.Fatalf("marshal oci-layout: %v", err)
}
if err := os.WriteFile(filepath.Join(dir, ocispec.ImageLayoutFile), markerData, 0644); err != nil {
t.Fatalf("write oci-layout: %v", err)
}
// index.json
idx := ocispec.Index{
Versioned: specs.Versioned{SchemaVersion: 2},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: manifests,
}
data, err := json.MarshalIndent(idx, "", " ")
if err != nil {
t.Fatalf("marshal index.json: %v", err)
}
if err := os.WriteFile(filepath.Join(dir, ocispec.ImageIndexFile), data, 0644); err != nil {
t.Fatalf("write index.json: %v", err)
}
}
// fakeDigest returns a syntactically valid digest string that can be used in
// test descriptors without any real blob.
func fakeDigest(hex string) digest.Digest {
// pad hex to 64 chars
for len(hex) < 64 {
hex += "0"
}
return digest.Digest("sha256:" + hex)
}
// --------------------------------------------------------------------------
// TestLoadIndex_NormalizesLegacyKindInDescriptorAnnotations
// --------------------------------------------------------------------------
// TestLoadIndex_NormalizesLegacyKindInDescriptorAnnotations verifies that
// after LoadIndex() (called implicitly by Walk()), every descriptor returned
// by Walk carries a normalized dev.hauler/... kind annotation, not the legacy
// dev.cosignproject.cosign/... value stored on disk.
func TestLoadIndex_NormalizesLegacyKindInDescriptorAnnotations(t *testing.T) {
dir := t.TempDir()
legacyKinds := []string{
"dev.cosignproject.cosign/image",
"dev.cosignproject.cosign/imageIndex",
"dev.cosignproject.cosign/sigs",
"dev.cosignproject.cosign/atts",
"dev.cosignproject.cosign/sboms",
}
var manifests []ocispec.Descriptor
for i, legacyKind := range legacyKinds {
d := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: fakeDigest(strings.Repeat(string(rune('a'+i)), 1)),
Size: 100,
Annotations: map[string]string{
ocispec.AnnotationRefName: "example.com/repo:tag" + strings.Repeat(string(rune('a'+i)), 1),
consts.KindAnnotationName: legacyKind,
},
}
manifests = append(manifests, d)
}
buildMinimalOCILayout(t, dir, manifests)
o, err := NewOCI(dir)
if err != nil {
t.Fatalf("NewOCI: %v", err)
}
var walked []ocispec.Descriptor
if err := o.Walk(func(_ string, desc ocispec.Descriptor) error {
walked = append(walked, desc)
return nil
}); err != nil {
t.Fatalf("Walk: %v", err)
}
if len(walked) == 0 {
t.Fatal("Walk returned no descriptors")
}
const legacyPrefix = "dev.cosignproject.cosign"
const newPrefix = "dev.hauler"
for _, desc := range walked {
kind := desc.Annotations[consts.KindAnnotationName]
if strings.HasPrefix(kind, legacyPrefix) {
t.Errorf("descriptor %s: Walk returned legacy kind %q; want normalized dev.hauler/... value",
desc.Digest, kind)
}
if !strings.HasPrefix(kind, newPrefix) {
t.Errorf("descriptor %s: Walk returned unexpected kind %q; want dev.hauler/... prefix",
desc.Digest, kind)
}
}
}
// --------------------------------------------------------------------------
// TestLoadIndex_DoesNotMutateOnDiskAnnotations
// --------------------------------------------------------------------------
// TestLoadIndex_DoesNotMutateOnDiskAnnotations verifies that the normalization
// performed by LoadIndex() is in-memory only: the index.json on disk must
// still carry the original (legacy) annotation values after a Walk() call.
func TestLoadIndex_DoesNotMutateOnDiskAnnotations(t *testing.T) {
dir := t.TempDir()
legacyKind := "dev.cosignproject.cosign/image"
manifests := []ocispec.Descriptor{
{
MediaType: ocispec.MediaTypeImageManifest,
Digest: fakeDigest("b"),
Size: 100,
Annotations: map[string]string{
ocispec.AnnotationRefName: "example.com/repo:tagb",
consts.KindAnnotationName: legacyKind,
},
},
}
buildMinimalOCILayout(t, dir, manifests)
o, err := NewOCI(dir)
if err != nil {
t.Fatalf("NewOCI: %v", err)
}
// Trigger LoadIndex via Walk.
if err := o.Walk(func(_ string, _ ocispec.Descriptor) error { return nil }); err != nil {
t.Fatalf("Walk: %v", err)
}
// Re-read index.json from disk and verify the annotation is unchanged.
data, err := os.ReadFile(filepath.Join(dir, ocispec.ImageIndexFile))
if err != nil {
t.Fatalf("read index.json: %v", err)
}
var idx ocispec.Index
if err := json.Unmarshal(data, &idx); err != nil {
t.Fatalf("unmarshal index.json: %v", err)
}
for _, desc := range idx.Manifests {
got := desc.Annotations[consts.KindAnnotationName]
if got != legacyKind {
t.Errorf("on-disk kind was mutated: got %q, want %q", got, legacyKind)
}
}
}
// --------------------------------------------------------------------------
// TestPush_NormalizesLegacyKindInStoredDescriptor
// --------------------------------------------------------------------------
// TestPush_NormalizesLegacyKindInStoredDescriptor verifies that after a Push()
// that matches the root digest, the descriptor stored in nameMap (and therefore
// returned by subsequent Walk() calls) carries the normalized dev.hauler/...
// kind annotation rather than the legacy value.
func TestPush_NormalizesLegacyKindInStoredDescriptor(t *testing.T) {
dir := t.TempDir()
buildMinimalOCILayout(t, dir, nil) // start with empty index
o, err := NewOCI(dir)
if err != nil {
t.Fatalf("NewOCI: %v", err)
}
// Build a minimal manifest blob so Push() can write it to disk.
manifest := ocispec.Manifest{
Versioned: specs.Versioned{SchemaVersion: 2},
MediaType: ocispec.MediaTypeImageManifest,
Config: ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageConfig,
Digest: fakeDigest("config0"),
Size: 2,
},
}
manifestData, err := json.Marshal(manifest)
if err != nil {
t.Fatalf("marshal manifest: %v", err)
}
manifestDigest := digest.FromBytes(manifestData)
// Ensure the blobs directory exists so Push can write.
blobsDir := filepath.Join(dir, ocispec.ImageBlobsDir, "sha256")
if err := os.MkdirAll(blobsDir, 0755); err != nil {
t.Fatalf("mkdir blobs: %v", err)
}
legacyKind := "dev.cosignproject.cosign/sigs"
baseRef := "example.com/repo:tagsig"
pusher, err := o.Pusher(context.Background(), baseRef+"@"+manifestDigest.String())
if err != nil {
t.Fatalf("Pusher: %v", err)
}
desc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: manifestDigest,
Size: int64(len(manifestData)),
Annotations: map[string]string{
ocispec.AnnotationRefName: baseRef,
consts.KindAnnotationName: legacyKind,
},
}
w, err := pusher.Push(context.Background(), desc)
if err != nil {
t.Fatalf("Push: %v", err)
}
if _, err := w.Write(manifestData); err != nil {
t.Fatalf("Write manifest: %v", err)
}
if err := w.Close(); err != nil {
t.Fatalf("Close writer: %v", err)
}
// Now Walk and verify the descriptor in nameMap has the normalized kind.
// We need a fresh OCI instance so Walk calls LoadIndex (which reads SaveIndex output).
o2, err := NewOCI(dir)
if err != nil {
t.Fatalf("NewOCI second: %v", err)
}
const legacyPrefix = "dev.cosignproject.cosign"
const newPrefix = "dev.hauler"
var found bool
if err := o2.Walk(func(_ string, d ocispec.Descriptor) error {
found = true
kind := d.Annotations[consts.KindAnnotationName]
if strings.HasPrefix(kind, legacyPrefix) {
t.Errorf("Push stored descriptor with legacy kind %q; want normalized dev.hauler/... value", kind)
}
if !strings.HasPrefix(kind, newPrefix) {
t.Errorf("Push stored descriptor with unexpected kind %q; want dev.hauler/... prefix", kind)
}
return nil
}); err != nil {
t.Fatalf("Walk: %v", err)
}
if !found {
t.Fatal("Walk returned no descriptors after Push")
}
// Also verify the caller's original descriptor map was NOT mutated.
if desc.Annotations[consts.KindAnnotationName] != legacyKind {
t.Errorf("Push mutated caller's descriptor annotations: got %q, want %q",
desc.Annotations[consts.KindAnnotationName], legacyKind)
}
}