mirror of
https://github.com/hauler-dev/hauler.git
synced 2026-05-09 10:47:20 +00:00
282 lines
8.9 KiB
Go
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)
|
|
}
|
|
}
|