Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1c184d2
feat: support index based reading of OCI artifacts
jakobmoellerdev Oct 14, 2025
7ce6b75
fix: handle nested artifact manifests in local blob access method
jakobmoellerdev Oct 14, 2025
c7eb44e
Merge remote-tracking branch 'upstream/main' into index-based-oci-art…
jakobmoellerdev Dec 18, 2025
66b018d
fix: make sure format is respected for artifactset
jakobmoellerdev Dec 18, 2025
8914447
feat: embed media type into ctf
jakobmoellerdev Dec 18, 2025
43ea712
feat: dynamically synthesize local blob based on mime type
jakobmoellerdev Dec 18, 2025
7b24f46
chore: test containers
jakobmoellerdev Dec 18, 2025
2ff06d0
test: backwards compatibility test run for blobs
jakobmoellerdev Dec 18, 2025
9f95f98
feat: allow multiple ref tags for OCI artifact synthesis
jakobmoellerdev Dec 22, 2025
9b13a06
chore: review
jakobmoellerdev Dec 29, 2025
e14ac45
Merge remote-tracking branch 'upstream/main' into index-based-oci-art…
jakobmoellerdev Dec 29, 2025
caaff95
chore: refactor add tags
jakobmoellerdev Dec 29, 2025
8b1e934
chore: refactor add tags
jakobmoellerdev Dec 29, 2025
6595729
Merge branch 'main' into index-based-oci-artifacts
jakobmoellerdev Jan 7, 2026
0bb1612
Merge branch 'main' into index-based-oci-artifacts
jakobmoellerdev Jan 8, 2026
0e5616d
Merge branch 'main' into index-based-oci-artifacts
jakobmoellerdev Jan 8, 2026
fd04434
Merge remote-tracking branch 'upstream/main' into index-based-oci-art…
jakobmoellerdev Jan 19, 2026
cabd751
fix: dont tag on localBlob synthesis
jakobmoellerdev Jan 19, 2026
6cb6ce1
test: update context handling in index transfer tests
jakobmoellerdev Jan 20, 2026
22e8025
fix: simplify tag handling in artifact synthesis
jakobmoellerdev Jan 20, 2026
ea27517
fix: add missing index access initialization in component version
jakobmoellerdev Jan 20, 2026
e857e44
Merge branch 'main' into index-based-oci-artifacts
jakobmoellerdev Jan 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,10 @@ var _ = Describe("CTF to CTF-with-resource to OCI roundtrip", Ordered, func() {
Expect(config.Config.Entrypoint).ToNot(BeEmpty())
})

It("copies OCI archive to Docker with skopeo", func() {
It("copies OCI archive to Docker with skopeo", func(ctx SpecContext) {
// Use skopeo to copy from OCI archive (tgz) to docker daemon
imageTag = "ocm-test-hello:" + resourceVersion
cmd := exec.Command("skopeo", "copy",
cmd := exec.CommandContext(ctx, "skopeo", "copy",
"oci-archive:"+resourcesOciTgz+":"+resourceVersion,
"docker-daemon:"+imageTag,
"--override-os=linux")
Expand All @@ -225,8 +225,7 @@ var _ = Describe("CTF to CTF-with-resource to OCI roundtrip", Ordered, func() {
Expect(string(out)).To(ContainSubstring("Hello OCM!"))
})

It("downloads resource from target CTF without --oci-layout and verifies it", func() {
ctx := context.Background()
It("downloads resource from target CTF without --oci-layout and verifies it", func(ctx SpecContext) {
resourcesOcmTgz = filepath.Join(tempDir, "resource-ocm-layout")

buf := bytes.NewBuffer(nil)
Expand Down
11 changes: 6 additions & 5 deletions api/oci/extensions/repositories/artifactset/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,12 +244,13 @@ func OpenFromDataAccess(acc accessobj.AccessMode, mediatype string, data blobacc
}
defer reader.Close()
o.SetReader(reader)
fmt := accessio.FormatTar

if mime.IsGZip(mediatype) {
fmt = accessio.FormatTGZ
if o.GetFileFormat() == nil {
fmt := accessio.FormatTar
if mime.IsGZip(mediatype) {
fmt = accessio.FormatTGZ
}
o.SetFileFormat(fmt)
}
o.SetFileFormat(fmt)
return Open(acc&accessobj.ACC_READONLY, "", 0, o)
}

Expand Down
27 changes: 12 additions & 15 deletions api/oci/extensions/repositories/artifactset/utils_synthesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,35 +84,32 @@ func SynthesizeArtifactBlobWithFilter(ns cpi.NamespaceAccess, ref string, filter
return nil, err
}
}
return SynthesizeArtifactBlobForArtifact(art, ref, filter)
return SynthesizeArtifactBlobForArtifact(art, []string{ref}, filter)
}

func SynthesizeArtifactBlobForArtifact(art cpi.ArtifactAccess, ref string, filter ...filters.Filter) (ArtifactBlob, error) {
func SynthesizeArtifactBlobForArtifact(art cpi.ArtifactAccess, refs []string, filter ...filters.Filter) (ArtifactBlob, error) {
blob, err := art.Blob()
if err != nil {
return nil, err
}

vers, err := ociutils.ParseVersion(ref)
if err != nil {
return nil, err
tags := make([]string, 0, len(refs))
for _, ref := range refs {
vers, err := ociutils.ParseVersion(ref)
if err != nil {
return nil, err
}
if vers.IsTagged() {
tags = append(tags, vers.GetTag())
}
}

return SythesizeArtifactSet(func(set *ArtifactSet) (string, error) {
dig, err := transfer.TransferArtifactWithFilter(art, set, filters.And(filter...))
dig, err := transfer.TransferArtifactWithFilter(art, set, filters.And(filter...), tags...)
if err != nil {
return "", fmt.Errorf("failed to transfer artifact: %w", err)
}

if ok := vers.IsTagged(); ok {
err = set.AddTags(*dig, vers.GetTag())
if err != nil {
return "", fmt.Errorf("failed to add tag: %w", err)
}
}

set.Annotate(MAINARTIFACT_ANNOTATION, dig.String())

return blob.MimeType(), nil
})
}
Expand Down
1 change: 1 addition & 0 deletions api/oci/extensions/repositories/ctf/index/ctfindex.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type ArtifactMeta struct {
Repository string `json:"repository"`
Tag string `json:"tag,omitempty"`
Digest digest.Digest `json:"digest,omitempty"`
MediaType string `json:"mediaType,omitempty"`
}

func Decode(data []byte) (*ArtifactIndex, error) {
Expand Down
1 change: 1 addition & 0 deletions api/oci/extensions/repositories/ctf/index/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ func (r *RepositoryIndex) GetDescriptor() *ArtifactIndex {
Repository: vers.Repository,
Tag: vers.Tag,
Digest: vers.Digest,
MediaType: vers.MediaType,
}
index.Index = append(index.Index, *d)
}
Expand Down
5 changes: 1 addition & 4 deletions api/oci/extensions/repositories/ctf/namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,6 @@ func (n *namespaceContainer) Close() error {
return nil
}

func (n *namespaceContainer) GetBlobDescriptor(digest digest.Digest) *cpi.Descriptor {
return nil
}

func (n *namespaceContainer) ListTags() ([]string, error) {
return n.repo.getIndex().GetTags(n.impl.GetNamespace()), nil // return digests as tags, also
}
Expand Down Expand Up @@ -84,6 +80,7 @@ func (n *namespaceContainer) AddArtifact(artifact cpi.Artifact, tags ...string)
Repository: n.impl.GetNamespace(),
Tag: "",
Digest: blob.Digest(),
MediaType: blob.MimeType(),
})
return blob, n.AddTags(blob.Digest(), tags...)
}
Expand Down
8 changes: 7 additions & 1 deletion api/ocm/extensions/accessmethods/ociartifact/method.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ const (
const (
LegacyType = "ociRegistry"
LegacyTypeV1 = LegacyType + runtime.VersionSeparator + "v1"

LegacyType2 = "OCIImage"
LegacyType2V1 = LegacyType2 + runtime.VersionSeparator + "v1"
)

func init() {
Expand All @@ -43,6 +46,9 @@ func init() {

accspeccpi.RegisterAccessType(accspeccpi.NewAccessSpecType[*AccessSpec](LegacyType))
accspeccpi.RegisterAccessType(accspeccpi.NewAccessSpecType[*AccessSpec](LegacyTypeV1))

accspeccpi.RegisterAccessType(accspeccpi.NewAccessSpecType[*AccessSpec](LegacyType2))
accspeccpi.RegisterAccessType(accspeccpi.NewAccessSpecType[*AccessSpec](LegacyType2V1))
}

func Is(spec accspeccpi.AccessSpec) bool {
Expand Down Expand Up @@ -355,7 +361,7 @@ func (m *accessMethod) getBlob() (artifactset.ArtifactBlob, error) {
}
logger := Logger(WrapContextProvider(m.ctx))
logger.Info("synthesize artifact blob", "ref", m.reference)
m.blob, err = artifactset.SynthesizeArtifactBlobForArtifact(m.art, m.ref.VersionSpec())
m.blob, err = artifactset.SynthesizeArtifactBlobForArtifact(m.art, []string{m.ref.VersionSpec()})
logger.Info("synthesize artifact blob done", "ref", m.reference, "error", logging.ErrorMessage(err))
if err != nil {
m.err = err
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package genericocireg

import (
"bytes"
"fmt"
"io"
"os"
"strings"
Expand All @@ -13,6 +14,7 @@ import (

"ocm.software/ocm/api/oci"
"ocm.software/ocm/api/oci/artdesc"
"ocm.software/ocm/api/oci/extensions/repositories/artifactset"
"ocm.software/ocm/api/ocm/cpi/accspeccpi"
"ocm.software/ocm/api/ocm/extensions/accessmethods/localblob"
"ocm.software/ocm/api/utils/blobaccess/blobaccess"
Expand All @@ -26,6 +28,7 @@ type localBlobAccessMethod struct {
spec *localblob.AccessSpec
namespace oci.NamespaceAccess
artifact oci.ArtifactAccess
mimeType string
}

var _ accspeccpi.AccessMethodImpl = (*localBlobAccessMethod)(nil)
Expand All @@ -40,6 +43,11 @@ func newLocalBlobAccessMethodImpl(a *localblob.AccessSpec, ns oci.NamespaceAcces
namespace: ns,
artifact: art,
}
if m.spec.MediaType == artdesc.MediaTypeImageIndex || m.spec.MediaType == artdesc.MediaTypeImageManifest {
// if we discover a localblob with an index or manifest media type, we can
// assume that we are dealing with a new style of artifact created by the new reference library.
m.mimeType = artifactset.MediaType(m.spec.MediaType)
}
ref.BeforeCleanup(refmgmt.CleanupHandlerFunc(m.cache))
return m, nil
}
Expand Down Expand Up @@ -99,8 +107,32 @@ func (m *localBlobAccessMethod) getBlob() (blobaccess.DataAccess, error) {
err error
)
if len(refs) < 2 {
_, data, err = m.namespace.GetBlobData(digest.Digest(m.spec.LocalReference))
if err != nil {
if m.spec.MediaType == artdesc.MediaTypeImageIndex || m.spec.MediaType == artdesc.MediaTypeImageManifest {
// if we have a nested manifest or index, we can use the blob synthesis utility here to download
// the entire artifact set.
art, err := m.namespace.GetArtifact(m.spec.LocalReference)
if err != nil {
return nil, fmt.Errorf("failed to get artifact for local reference %q: %w", m.spec.LocalReference, err)
}
defer art.Close()
var artifactRefs []string
if m.spec.ReferenceName != "" {
// if we have a reference name, it consists of repository and tag
// so we can extract the tag to use it
refSpec, err := oci.ParseRef(m.spec.ReferenceName)
if err != nil {
return nil, fmt.Errorf("failed to parse reference name %q: %w", m.spec.ReferenceName, err)
}
if refSpec.GetTag() != "" {
artifactRefs = append(artifactRefs, refSpec.GetTag())
}
}
artblob, err := artifactset.SynthesizeArtifactBlobForArtifact(art, artifactRefs)
if err != nil {
return nil, fmt.Errorf("failed to synthesize artifact blob: %w", err)
}
data = artblob
} else if _, data, err = m.namespace.GetBlobData(digest.Digest(m.spec.LocalReference)); err != nil {
return nil, err
}
} else {
Expand All @@ -123,10 +155,13 @@ func (m *localBlobAccessMethod) Get() ([]byte, error) {
}

func (m *localBlobAccessMethod) MimeType() string {
if m.mimeType != "" {
return m.mimeType
}
return m.spec.MediaType
}

////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////

type composedBlock struct {
m *localBlobAccessMethod
Expand Down
97 changes: 75 additions & 22 deletions api/ocm/extensions/repositories/genericocireg/componentversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,38 +46,82 @@ func newComponentVersionAccess(mode accessobj.AccessMode, comp *componentAccessI
return &repocpi.ComponentVersionAccessInfo{c, true, persistent}, nil
}

////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////

type ComponentVersionContainer struct {
bridge repocpi.ComponentVersionAccessBridge

comp *componentAccessImpl
version string
access oci.ArtifactAccess
manifest oci.ManifestAccess
state accessobj.State
comp *componentAccessImpl
version string
indexArtifact oci.ArtifactAccess
manifestArtifact oci.ArtifactAccess
manifest oci.ManifestAccess
index oci.IndexAccess
state accessobj.State
}

var _ repocpi.ComponentVersionAccessImpl = (*ComponentVersionContainer)(nil)

func newComponentVersionContainer(mode accessobj.AccessMode, comp *componentAccessImpl, version string, access oci.ArtifactAccess) (*ComponentVersionContainer, error) {
m := access.ManifestAccess()
if m == nil {
return nil, errors.ErrInvalid("artifact type")
var m oci.ManifestAccess
var i oci.IndexAccess

var manifestArtifact, indexArtifact oci.ArtifactAccess

if access.IsIndex() {
idx, err := access.Index()
if err != nil {
return nil, err
}
if len(idx.Manifests) < 1 {
return nil, fmt.Errorf("index has no manifests")
}
first := idx.Manifests[0]
firstArtifact, err := access.GetArtifact(first.Digest)
if err != nil {
return nil, err
}
if !firstArtifact.IsManifest() {
return nil, fmt.Errorf("first manifest in index is not a manifest")
}

m = firstArtifact.ManifestAccess()
manifestArtifact = firstArtifact
i = access.IndexAccess()
indexArtifact = access
} else {
m = access.ManifestAccess()
if m == nil {
return nil, fmt.Errorf("artifact is neither manifest nor index")
}

manifestArtifact = access
}
state, err := NewState(mode, comp.name, version, m, compatattr.Get(comp.GetContext()))

state, err := NewState(mode, comp.name, version, i, m, compatattr.Get(comp.GetContext()))
if err != nil {
access.Close()
err = errors.Join(err, manifestArtifact.Close())
if indexArtifact != nil {
err = errors.Join(err, indexArtifact.Close())
}
return nil, err
}

return &ComponentVersionContainer{
comp: comp,
version: version,
access: access,
manifest: m,
state: state,
}, nil
container := &ComponentVersionContainer{
comp: comp,
version: version,
manifestArtifact: manifestArtifact,
indexArtifact: indexArtifact,
manifest: m,
state: state,
}

if indexArtifact != nil {
// index based manifests are optional and only read based for next gen support
container.SetReadOnly()
}

return container, nil
}

func (c *ComponentVersionContainer) SetBridge(impl repocpi.ComponentVersionAccessBridge) {
Expand All @@ -89,11 +133,20 @@ func (c *ComponentVersionContainer) GetParentBridge() repocpi.ComponentAccessBri
}

func (c *ComponentVersionContainer) Close() error {
if c.manifest == nil {
if c.manifest == nil && c.manifestArtifact == nil {
return accessio.ErrClosed
}
c.manifest = nil
return c.access.Close()
c.index = nil

var err error
if c.indexArtifact != nil {
err = errors.Join(err, c.indexArtifact.Close())
}
if c.manifestArtifact != nil {
err = errors.Join(err, c.manifestArtifact.Close())
}
return err
}

func (c *ComponentVersionContainer) SetReadOnly() {
Expand Down Expand Up @@ -140,9 +193,9 @@ func (c *ComponentVersionContainer) AccessMethod(a cpi.AccessSpec, cv refmgmt.Ex

switch a.GetKind() {
case localblob.Type:
return newLocalBlobAccessMethod(accessSpec.(*localblob.AccessSpec), c.comp.namespace, c.access, cv)
return newLocalBlobAccessMethod(accessSpec.(*localblob.AccessSpec), c.comp.namespace, c.manifestArtifact, cv)
case localociblob.Type:
return newLocalOCIBlobAccessMethod(accessSpec.(*localblob.AccessSpec), c.comp.namespace, c.access, cv)
return newLocalOCIBlobAccessMethod(accessSpec.(*localblob.AccessSpec), c.comp.namespace, c.manifestArtifact, cv)
case relativeociref.Type:
m, err := ociartifact.NewMethod(c.GetContext(), a, accessSpec.(*relativeociref.AccessSpec).Reference, c.comp.repo.ocirepo)
if err == nil {
Expand Down
4 changes: 2 additions & 2 deletions api/ocm/extensions/repositories/genericocireg/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func (h handler) Info(m cpi.ManifestAccess, config []byte) interface{} {
info := &ComponentVersionInfo{
Description: "component version",
}
acc := NewStateAccess(m)
acc := NewStateAccess(nil, m)
data, err := blobaccess.BlobData(acc.Get())
if err != nil {
info.Error = "cannot read component descriptor: " + err.Error()
Expand All @@ -46,7 +46,7 @@ func (h handler) Info(m cpi.ManifestAccess, config []byte) interface{} {

func (h handler) Description(pr common.Printer, m cpi.ManifestAccess, config []byte) {
pr.Printf("component version:\n")
acc := NewStateAccess(m)
acc := NewStateAccess(nil, m)
data, err := blobaccess.BlobData(acc.Get())
if err != nil {
pr.Printf(" cannot read component descriptor: %s\n", err.Error())
Expand Down
Loading
Loading