* feature!: support for setting HTTP User-Agent header * fix lint * fix e2e * refactor: move http.go to internal/util/useragent package and rename functions to Get and Set * Move packages and use attest version
219 lines
5.0 KiB
Go
219 lines
5.0 KiB
Go
package oci
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/google/go-containerregistry/pkg/name"
|
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
|
"github.com/google/go-containerregistry/pkg/v1/layout"
|
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
|
)
|
|
|
|
const (
|
|
OCIReferenceTarget = "org.opencontainers.image.ref.name"
|
|
LocalPrefix = "oci://"
|
|
RegistryPrefix = "docker://"
|
|
OCI SourceType = "OCI"
|
|
Docker SourceType = "Docker"
|
|
)
|
|
|
|
type (
|
|
SourceType string
|
|
NamedIndex struct {
|
|
Index v1.ImageIndex
|
|
Name string
|
|
}
|
|
)
|
|
|
|
type ImageSpecOption func(*ImageSpec) error
|
|
|
|
type ImageSpec struct {
|
|
// OCI or Docker
|
|
Type SourceType
|
|
// without oci:// or docker:// (name or path)
|
|
Identifier string
|
|
Platform *v1.Platform
|
|
}
|
|
|
|
func IndexFromPath(path string) (*NamedIndex, error) {
|
|
wrapperIdx, err := layout.ImageIndexFromPath(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load image index: %w", err)
|
|
}
|
|
|
|
idxm, err := wrapperIdx.IndexManifest()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get digest: %w", err)
|
|
}
|
|
imageName := idxm.Manifests[0].Annotations[OCIReferenceTarget]
|
|
idxDigest := idxm.Manifests[0].Digest
|
|
|
|
idx, err := wrapperIdx.ImageIndex(idxDigest)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err)
|
|
}
|
|
return &NamedIndex{
|
|
Index: idx,
|
|
Name: imageName,
|
|
}, nil
|
|
}
|
|
|
|
func IndexFromRemote(ctx context.Context, image string) (*NamedIndex, error) {
|
|
ref, err := name.ParseReference(image)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse image reference %s: %w", image, err)
|
|
}
|
|
|
|
// Pull the image from the registry
|
|
idx, err := remote.Index(ref, WithOptions(ctx, nil)...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to pull image %s: %w", image, err)
|
|
}
|
|
return &NamedIndex{
|
|
Index: idx,
|
|
Name: image,
|
|
}, nil
|
|
}
|
|
|
|
func LoadIndex(ctx context.Context, input *ImageSpec) (*NamedIndex, error) {
|
|
if input.Type == OCI {
|
|
return IndexFromPath(input.Identifier)
|
|
}
|
|
return IndexFromRemote(ctx, input.Identifier)
|
|
}
|
|
|
|
func (i *ImageSpec) ForPlatforms(platform string) ([]*ImageSpec, error) {
|
|
platforms := strings.Split(platform, ",")
|
|
var specs []*ImageSpec
|
|
for _, pStr := range platforms {
|
|
p, err := ParsePlatform(pStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
spec := &ImageSpec{
|
|
Type: i.Type,
|
|
Identifier: i.Identifier,
|
|
Platform: p,
|
|
}
|
|
specs = append(specs, spec)
|
|
}
|
|
return specs, nil
|
|
}
|
|
|
|
func ParseImageSpec(img string, options ...ImageSpecOption) (*ImageSpec, error) {
|
|
img = strings.TrimSpace(img)
|
|
if strings.Contains(img, ",") {
|
|
return nil, fmt.Errorf("only one image is supported")
|
|
}
|
|
withoutPrefix := strings.TrimPrefix(strings.TrimPrefix(img, LocalPrefix), RegistryPrefix)
|
|
src := &ImageSpec{
|
|
Identifier: withoutPrefix,
|
|
}
|
|
if strings.HasPrefix(img, LocalPrefix) {
|
|
src.Type = OCI
|
|
} else {
|
|
src.Type = Docker
|
|
}
|
|
for _, option := range options {
|
|
err := option(src)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if src.Platform == nil {
|
|
platform, err := ParsePlatform("")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
src.Platform = platform
|
|
}
|
|
return src, nil
|
|
}
|
|
|
|
func WithPlatform(platform string) ImageSpecOption {
|
|
return func(i *ImageSpec) error {
|
|
if strings.Contains(platform, ",") {
|
|
return fmt.Errorf("only one platform is supported")
|
|
}
|
|
p, err := ParsePlatform(platform)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
i.Platform = p
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func ParseImageSpecs(img string) ([]*ImageSpec, error) {
|
|
outputs := strings.Split(img, ",")
|
|
var sources []*ImageSpec
|
|
for _, output := range outputs {
|
|
src, err := ParseImageSpec(output)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sources = append(sources, src)
|
|
}
|
|
return sources, nil
|
|
}
|
|
|
|
func WithoutTag(image string) (string, error) {
|
|
if strings.HasPrefix(image, LocalPrefix) {
|
|
return image, nil
|
|
}
|
|
prefix := ""
|
|
if strings.HasPrefix(image, RegistryPrefix) {
|
|
image = strings.TrimPrefix(image, RegistryPrefix)
|
|
prefix = RegistryPrefix
|
|
}
|
|
ref, err := name.ParseReference(image)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
repo := ref.Context().Name()
|
|
return prefix + repo, nil
|
|
}
|
|
|
|
type EmptyConfigImage struct {
|
|
v1.Image
|
|
}
|
|
|
|
func (i *EmptyConfigImage) RawConfigFile() ([]byte, error) {
|
|
return []byte("{}"), nil
|
|
}
|
|
|
|
func (i *EmptyConfigImage) Manifest() (*v1.Manifest, error) {
|
|
mf, err := i.Image.Manifest()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get manifest: %w", err)
|
|
}
|
|
mf.Config = v1.Descriptor{
|
|
MediaType: "application/vnd.oci.empty.v1+json",
|
|
Size: 2,
|
|
Digest: v1.Hash{Algorithm: "sha256", Hex: "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"},
|
|
Data: []byte("{}"),
|
|
}
|
|
return mf, nil
|
|
}
|
|
|
|
func (i *EmptyConfigImage) RawManifest() ([]byte, error) {
|
|
mf, err := i.Manifest()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get manifest: %w", err)
|
|
}
|
|
return json.Marshal(mf)
|
|
}
|
|
|
|
func (i *EmptyConfigImage) Digest() (v1.Hash, error) {
|
|
mb, err := i.RawManifest()
|
|
if err != nil {
|
|
return v1.Hash{}, err
|
|
}
|
|
digest, _, err := v1.SHA256(bytes.NewReader(mb))
|
|
return digest, err
|
|
}
|