mirror of
https://github.com/hauler-dev/hauler.git
synced 2026-08-23 22:36:35 +00:00
192 lines
4.5 KiB
Go
192 lines
4.5 KiB
Go
package layout
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/containerd/containerd/content"
|
|
"github.com/containerd/containerd/content/local"
|
|
"github.com/containerd/containerd/remotes/docker"
|
|
"github.com/google/go-containerregistry/pkg/name"
|
|
"github.com/opencontainers/go-digest"
|
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
orascontent "oras.land/oras-go/pkg/content"
|
|
"oras.land/oras-go/pkg/oras"
|
|
|
|
"github.com/rancherfederal/hauler/pkg/artifact/types"
|
|
)
|
|
|
|
// interface guards
|
|
var (
|
|
_ content.Provider = (*OCIStore)(nil)
|
|
_ content.Ingester = (*OCIStore)(nil)
|
|
)
|
|
|
|
// OCIStore represents a content compatible store adhering by the oci-layout spec
|
|
type OCIStore struct {
|
|
content.Store
|
|
|
|
root string
|
|
index *ocispec.Index
|
|
digestMap map[string]ocispec.Descriptor
|
|
}
|
|
|
|
// Copy placeholder until we migrate to oras 0.5
|
|
// Will loop through each appropriately named index and copy the contents to the desired registry
|
|
func Copy(ctx context.Context, s *OCIStore, registry string) error {
|
|
for _, desc := range s.index.Manifests {
|
|
manifestBlobPath, err := s.blobPath(desc.Digest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
manifestData, err := os.ReadFile(manifestBlobPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
m, mdesc, err := loadManifest(manifestData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
refName, ok := desc.Annotations[ocispec.AnnotationRefName]
|
|
if !ok {
|
|
return fmt.Errorf("no name found to push image")
|
|
}
|
|
|
|
rref, err := RelocateReference(refName, registry)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resolver := docker.NewResolver(docker.ResolverOptions{})
|
|
_, err = oras.Push(ctx, resolver, rref.Name(), s, m.Layers,
|
|
oras.WithConfig(m.Config), oras.WithNameValidation(nil), oras.WithManifest(mdesc))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// NewOCIStore will return a new OCIStore given a path to an oci-layout compatible directory
|
|
func NewOCIStore(path string) (*OCIStore, error) {
|
|
fs, err := local.NewStore(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
store := &OCIStore{
|
|
Store: fs,
|
|
|
|
root: path,
|
|
}
|
|
|
|
if err := store.validateOCILayout(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := store.LoadIndex(); err != nil {
|
|
return nil, nil
|
|
}
|
|
|
|
return store, nil
|
|
}
|
|
|
|
// LoadIndex will load an oci-layout compatible directory
|
|
func (s *OCIStore) LoadIndex() error {
|
|
path := filepath.Join(s.root, types.OCIImageIndexFile)
|
|
indexFile, err := os.Open(path)
|
|
if err != nil {
|
|
// TODO: Don't just bomb out?
|
|
return err
|
|
}
|
|
defer indexFile.Close()
|
|
|
|
if err := json.NewDecoder(indexFile).Decode(&s.index); err != nil {
|
|
return err
|
|
}
|
|
|
|
s.digestMap = make(map[string]ocispec.Descriptor)
|
|
for _, desc := range s.index.Manifests {
|
|
if name := desc.Annotations[ocispec.AnnotationRefName]; name != "" {
|
|
s.digestMap[name] = desc
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *OCIStore) validateOCILayout() error {
|
|
layoutFilePath := filepath.Join(s.root, ocispec.ImageLayoutFile)
|
|
layoutFile, err := os.Open(layoutFilePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer layoutFile.Close()
|
|
|
|
var layout *ocispec.ImageLayout
|
|
if err := json.NewDecoder(layoutFile).Decode(&layout); err != nil {
|
|
return err
|
|
}
|
|
|
|
if layout.Version != ocispec.ImageLayoutVersion {
|
|
return orascontent.ErrUnsupportedVersion
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *OCIStore) blobPath(d digest.Digest) (string, error) {
|
|
if err := d.Validate(); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return filepath.Join(s.root, "blobs", d.Algorithm().String(), d.Hex()), nil
|
|
}
|
|
|
|
// manifest is a field wrapper around ocispec.Manifest that contains the mediaType field
|
|
type manifest struct {
|
|
ocispec.Manifest `json:",inline"`
|
|
|
|
MediaType string `json:"mediaType"`
|
|
}
|
|
|
|
// loadManifest
|
|
func loadManifest(data []byte) (ocispec.Manifest, ocispec.Descriptor, error) {
|
|
var m manifest
|
|
if err := json.Unmarshal(data, &m); err != nil {
|
|
return ocispec.Manifest{}, ocispec.Descriptor{}, err
|
|
}
|
|
|
|
desc := ocispec.Descriptor{
|
|
MediaType: m.MediaType,
|
|
Digest: digest.FromBytes(data),
|
|
Size: int64(len(data)),
|
|
}
|
|
|
|
return m.Manifest, desc, nil
|
|
}
|
|
|
|
// RelocateReference returns a name.Reference given a reference and registry
|
|
func RelocateReference(reference string, registry string) (name.Reference, error) {
|
|
ref, err := name.ParseReference(reference)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
relocated, err := name.ParseReference(ref.Context().RepositoryStr(), name.WithDefaultRegistry(registry))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, err := name.NewDigest(ref.Name()); err == nil {
|
|
return relocated.Context().Digest(ref.Identifier()), nil
|
|
}
|
|
return relocated.Context().Tag(ref.Identifier()), nil
|
|
}
|