package oci
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"github.com/containerd/containerd/v2/core/images"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
var (
ErrNotFound = errors.New("content not found")
)
type EventType string
const (
CreateEvent EventType = "CREATE"
UpdateEvent EventType = "UPDATE"
DeleteEvent EventType = "DELETE"
)
type ImageEvent struct {
Image Image
Type EventType
}
type Client interface {
Name() string
Verify(ctx context.Context) error
Subscribe(ctx context.Context) (<-chan ImageEvent, <-chan error, error)
ListImages(ctx context.Context) ([]Image, error)
Resolve(ctx context.Context, ref string) (digest.Digest, error)
Size(ctx context.Context, dgst digest.Digest) (int64, error)
GetManifest(ctx context.Context, dgst digest.Digest) ([]byte, string, error)
GetBlob(ctx context.Context, dgst digest.Digest) (io.ReadSeekCloser, error)
}
type UnknownDocument struct {
MediaType string `json:"mediaType"`
specs.Versioned
}
func DetermineMediaType(b []byte) (string, error) {
var ud UnknownDocument
if err := json.Unmarshal(b, &ud); err != nil {
return "", err
}
if ud.SchemaVersion == 2 && ud.MediaType != "" {
return ud.MediaType, nil
}
data := map[string]json.RawMessage{}
if err := json.Unmarshal(b, &data); err != nil {
return "", err
}
_, architectureOk := data["architecture"]
_, osOk := data["os"]
_, rootfsOk := data["rootfs"]
if architectureOk && osOk && rootfsOk {
return ocispec.MediaTypeImageConfig, nil
}
_, manifestsOk := data["manifests"]
if ud.SchemaVersion == 2 && manifestsOk {
return ocispec.MediaTypeImageIndex, nil
}
_, configOk := data["config"]
if ud.SchemaVersion == 2 && configOk {
return ocispec.MediaTypeImageManifest, nil
}
return "", errors.New("not able to determine media type")
}
func WalkImage(ctx context.Context, client Client, img Image) ([]string, error) {
keys := []string{}
err := walk(ctx, []digest.Digest{img.Digest}, func(dgst digest.Digest) ([]digest.Digest, error) {
b, mt, err := client.GetManifest(ctx, dgst)
if err != nil {
return nil, err
}
keys = append(keys, dgst.String())
switch mt {
case images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex:
var idx ocispec.Index
if err := json.Unmarshal(b, &idx); err != nil {
return nil, err
}
manifestDgsts := []digest.Digest{}
for _, m := range idx.Manifests {
_, err := client.Size(ctx, m.Digest)
if errors.Is(err, ErrNotFound) {
continue
}
if err != nil {
return nil, err
}
manifestDgsts = append(manifestDgsts, m.Digest)
}
if len(manifestDgsts) == 0 {
return nil, fmt.Errorf("could not find any platforms with local content in manifest %s", dgst)
}
return manifestDgsts, nil
case images.MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
var manifest ocispec.Manifest
err := json.Unmarshal(b, &manifest)
if err != nil {
return nil, err
}
keys = append(keys, manifest.Config.Digest.String())
for _, layer := range manifest.Layers {
keys = append(keys, layer.Digest.String())
}
return nil, nil
default:
return nil, fmt.Errorf("unexpected media type %s for digest %s", mt, dgst)
}
})
if err != nil {
return nil, fmt.Errorf("failed to walk image manifests: %w", err)
}
if len(keys) == 0 {
return nil, errors.New("no image digests found")
}
return keys, nil
}
func walk(ctx context.Context, dgsts []digest.Digest, handler func(dgst digest.Digest) ([]digest.Digest, error)) error {
for _, dgst := range dgsts {
children, err := handler(dgst)
if err != nil {
return err
}
if len(children) == 0 {
continue
}
err = walk(ctx, children, handler)
if err != nil {
return err
}
}
return nil
}