mirror of
https://github.com/regclient/regclient.git
synced 2025-04-18 22:44:00 +03:00
I feel like I need to explain, this is all to move the descriptor package. The platform package could not use the predefined errors in types because of a circular dependency from descriptor. The most appropriate way to reorg this is to move descriptor out of the type package since it was more complex than a self contained type. When doing that, type aliases were needed to avoid breaking changes to existing users. Those aliases themselves caused circular dependency loops because of the media types and errors, so those were also pulled out to separate packages. All of the old values were aliased and deprecated, and to fix the linter, those deprecations were fixed by updating the imports... everywhere. Signed-off-by: Brandon Mitchell <git@bmitch.net>
1802 lines
51 KiB
Go
1802 lines
51 KiB
Go
package regclient
|
|
|
|
import (
|
|
"archive/tar"
|
|
"compress/gzip"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
// crypto libraries included for go-digest
|
|
_ "crypto/sha256"
|
|
_ "crypto/sha512"
|
|
|
|
digest "github.com/opencontainers/go-digest"
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"github.com/regclient/regclient/pkg/archive"
|
|
"github.com/regclient/regclient/scheme"
|
|
"github.com/regclient/regclient/types"
|
|
"github.com/regclient/regclient/types/descriptor"
|
|
"github.com/regclient/regclient/types/docker/schema2"
|
|
"github.com/regclient/regclient/types/errs"
|
|
"github.com/regclient/regclient/types/manifest"
|
|
"github.com/regclient/regclient/types/mediatype"
|
|
v1 "github.com/regclient/regclient/types/oci/v1"
|
|
"github.com/regclient/regclient/types/platform"
|
|
"github.com/regclient/regclient/types/ref"
|
|
"github.com/regclient/regclient/types/warning"
|
|
)
|
|
|
|
const (
|
|
dockerManifestFilename = "manifest.json"
|
|
ociLayoutVersion = "1.0.0"
|
|
ociIndexFilename = "index.json"
|
|
ociLayoutFilename = "oci-layout"
|
|
annotationRefName = "org.opencontainers.image.ref.name"
|
|
annotationImageName = "io.containerd.image.name"
|
|
)
|
|
|
|
// used by import/export to match docker tar expected format
|
|
type dockerTarManifest struct {
|
|
Config string
|
|
RepoTags []string
|
|
Layers []string
|
|
Parent digest.Digest `json:",omitempty"`
|
|
LayerSources map[digest.Digest]descriptor.Descriptor `json:",omitempty"`
|
|
}
|
|
|
|
type tarFileHandler func(header *tar.Header, trd *tarReadData) error
|
|
type tarReadData struct {
|
|
tr *tar.Reader
|
|
name string
|
|
handleAdded bool
|
|
handlers map[string]tarFileHandler
|
|
links map[string][]string
|
|
processed map[string]bool
|
|
finish []func() error
|
|
// data processed from various handlers
|
|
manifests map[digest.Digest]manifest.Manifest
|
|
ociIndex v1.Index
|
|
ociManifest manifest.Manifest
|
|
dockerManifestFound bool
|
|
dockerManifestList []dockerTarManifest
|
|
dockerManifest schema2.Manifest
|
|
}
|
|
type tarWriteData struct {
|
|
tw *tar.Writer
|
|
dirs map[string]bool
|
|
files map[string]bool
|
|
// uid, gid int
|
|
mode int64
|
|
timestamp time.Time
|
|
}
|
|
|
|
type imageOpt struct {
|
|
callback func(kind types.CallbackKind, instance string, state types.CallbackState, cur, total int64)
|
|
checkBaseDigest string
|
|
checkBaseRef string
|
|
checkSkipConfig bool
|
|
child bool
|
|
exportCompress bool
|
|
exportRef ref.Ref
|
|
fastCheck bool
|
|
forceRecursive bool
|
|
importName string
|
|
includeExternal bool
|
|
digestTags bool
|
|
platform string
|
|
platforms []string
|
|
referrerConfs []scheme.ReferrerConfig
|
|
tagList []string
|
|
mu sync.Mutex
|
|
seen map[string]*imageSeen
|
|
finalFn []func(context.Context) error
|
|
}
|
|
|
|
type imageSeen struct {
|
|
done chan struct{}
|
|
err error
|
|
}
|
|
|
|
// ImageOpts define options for the Image* commands.
|
|
type ImageOpts func(*imageOpt)
|
|
|
|
// ImageWithCallback provides progress data to a callback function.
|
|
func ImageWithCallback(callback func(kind types.CallbackKind, instance string, state types.CallbackState, cur, total int64)) ImageOpts {
|
|
return func(opts *imageOpt) {
|
|
opts.callback = callback
|
|
}
|
|
}
|
|
|
|
// ImageWithCheckBaseDigest provides a base digest to compare in ImageCheckBase.
|
|
func ImageWithCheckBaseDigest(d string) ImageOpts {
|
|
return func(opts *imageOpt) {
|
|
opts.checkBaseDigest = d
|
|
}
|
|
}
|
|
|
|
// ImageWithCheckBaseRef provides a base reference to compare in ImageCheckBase.
|
|
func ImageWithCheckBaseRef(r string) ImageOpts {
|
|
return func(opts *imageOpt) {
|
|
opts.checkBaseRef = r
|
|
}
|
|
}
|
|
|
|
// ImageWithCheckSkipConfig skips the configuration check in ImageCheckBase.
|
|
func ImageWithCheckSkipConfig() ImageOpts {
|
|
return func(opts *imageOpt) {
|
|
opts.checkSkipConfig = true
|
|
}
|
|
}
|
|
|
|
// ImageWithChild attempts to copy every manifest and blob even if parent manifests already exist in ImageCopy.
|
|
func ImageWithChild() ImageOpts {
|
|
return func(opts *imageOpt) {
|
|
opts.child = true
|
|
}
|
|
}
|
|
|
|
// ImageWithExportCompress adds gzip compression to tar export output in ImageExport.
|
|
func ImageWithExportCompress() ImageOpts {
|
|
return func(opts *imageOpt) {
|
|
opts.exportCompress = true
|
|
}
|
|
}
|
|
|
|
// ImageWithExportRef overrides the image name embedded in the export file in ImageExport.
|
|
func ImageWithExportRef(r ref.Ref) ImageOpts {
|
|
return func(opts *imageOpt) {
|
|
opts.exportRef = r
|
|
}
|
|
}
|
|
|
|
// ImageWithFastCheck skips check for referrers when manifest has already been copied in ImageCopy.
|
|
func ImageWithFastCheck() ImageOpts {
|
|
return func(opts *imageOpt) {
|
|
opts.fastCheck = true
|
|
}
|
|
}
|
|
|
|
// ImageWithForceRecursive attempts to copy every manifest and blob even if parent manifests already exist in ImageCopy.
|
|
func ImageWithForceRecursive() ImageOpts {
|
|
return func(opts *imageOpt) {
|
|
opts.forceRecursive = true
|
|
}
|
|
}
|
|
|
|
// ImageWithImportName selects the name of the image to import when multiple images are included in ImageImport.
|
|
func ImageWithImportName(name string) ImageOpts {
|
|
return func(opts *imageOpt) {
|
|
opts.importName = name
|
|
}
|
|
}
|
|
|
|
// ImageWithIncludeExternal attempts to copy every manifest and blob even if parent manifests already exist in ImageCopy.
|
|
func ImageWithIncludeExternal() ImageOpts {
|
|
return func(opts *imageOpt) {
|
|
opts.includeExternal = true
|
|
}
|
|
}
|
|
|
|
// ImageWithDigestTags looks for "sha-<digest>.*" tags in the repo to copy with any manifest in ImageCopy.
|
|
// These are used by some artifact systems like sigstore/cosign.
|
|
func ImageWithDigestTags() ImageOpts {
|
|
return func(opts *imageOpt) {
|
|
opts.digestTags = true
|
|
}
|
|
}
|
|
|
|
// ImageWithPlatform requests specific platforms from a manifest list in ImageCheckBase.
|
|
func ImageWithPlatform(p string) ImageOpts {
|
|
return func(opts *imageOpt) {
|
|
opts.platform = p
|
|
}
|
|
}
|
|
|
|
// ImageWithPlatforms only copies specific platforms from a manifest list in ImageCopy.
|
|
// This will result in a failure on many registries that validate manifests.
|
|
// Use the empty string to indicate images without a platform definition should be copied.
|
|
func ImageWithPlatforms(p []string) ImageOpts {
|
|
return func(opts *imageOpt) {
|
|
opts.platforms = p
|
|
}
|
|
}
|
|
|
|
// ImageWithReferrers recursively recursively includes referrer images in ImageCopy.
|
|
func ImageWithReferrers(rOpts ...scheme.ReferrerOpts) ImageOpts {
|
|
return func(opts *imageOpt) {
|
|
if opts.referrerConfs == nil {
|
|
opts.referrerConfs = []scheme.ReferrerConfig{}
|
|
}
|
|
rConf := scheme.ReferrerConfig{}
|
|
for _, rOpt := range rOpts {
|
|
rOpt(&rConf)
|
|
}
|
|
opts.referrerConfs = append(opts.referrerConfs, rConf)
|
|
}
|
|
}
|
|
|
|
// ImageCheckBase returns nil if the base image is unchanged.
|
|
// A base image mismatch returns an error that wraps errs.ErrMismatch.
|
|
func (rc *RegClient) ImageCheckBase(ctx context.Context, r ref.Ref, opts ...ImageOpts) error {
|
|
var opt imageOpt
|
|
for _, optFn := range opts {
|
|
optFn(&opt)
|
|
}
|
|
var m manifest.Manifest
|
|
var err error
|
|
|
|
// if the base name is not provided, check image for base annotations
|
|
if opt.checkBaseRef == "" {
|
|
m, err = rc.ManifestGet(ctx, r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ma, ok := m.(manifest.Annotator)
|
|
if !ok {
|
|
return fmt.Errorf("image does not support annotations, base image must be provided%.0w", errs.ErrMissingAnnotation)
|
|
}
|
|
annot, err := ma.GetAnnotations()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if baseName, ok := annot[types.AnnotationBaseImageName]; ok {
|
|
opt.checkBaseRef = baseName
|
|
} else {
|
|
return fmt.Errorf("image does not have a base annotation, base image must be provided%.0w", errs.ErrMissingAnnotation)
|
|
}
|
|
if baseDig, ok := annot[types.AnnotationBaseImageDigest]; ok {
|
|
opt.checkBaseDigest = baseDig
|
|
}
|
|
}
|
|
baseR, err := ref.New(opt.checkBaseRef)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rc.Close(ctx, baseR)
|
|
|
|
// if the digest is available, check if that matches the base name
|
|
if opt.checkBaseDigest != "" {
|
|
baseMH, err := rc.ManifestHead(ctx, baseR, WithManifestRequireDigest())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
expectDig, err := digest.Parse(opt.checkBaseDigest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if baseMH.GetDescriptor().Digest == expectDig {
|
|
rc.log.WithFields(logrus.Fields{
|
|
"name": baseR.CommonName(),
|
|
"digest": baseMH.GetDescriptor().Digest.String(),
|
|
}).Debug("base image digest matches")
|
|
return nil
|
|
} else {
|
|
rc.log.WithFields(logrus.Fields{
|
|
"name": baseR.CommonName(),
|
|
"digest": baseMH.GetDescriptor().Digest.String(),
|
|
"expected": expectDig.String(),
|
|
}).Debug("base image digest changed")
|
|
return fmt.Errorf("base digest changed, %s, expected %s, received %s%.0w",
|
|
baseR.CommonName(), expectDig.String(), baseMH.GetDescriptor().Digest.String(), errs.ErrMismatch)
|
|
}
|
|
}
|
|
|
|
// if the digest is not available, compare layers of each manifest
|
|
if m == nil {
|
|
m, err = rc.ManifestGet(ctx, r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if m.IsList() && opt.platform != "" {
|
|
p, err := platform.Parse(opt.platform)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
d, err := manifest.GetPlatformDesc(m, &p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rp := r
|
|
rp.Digest = d.Digest.String()
|
|
m, err = rc.ManifestGet(ctx, rp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if m.IsList() {
|
|
// loop through each platform
|
|
ml, ok := m.(manifest.Indexer)
|
|
if !ok {
|
|
return fmt.Errorf("manifest list is not an Indexer")
|
|
}
|
|
dl, err := ml.GetManifestList()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rp := r
|
|
for _, d := range dl {
|
|
rp.Digest = d.Digest.String()
|
|
optP := append(opts, ImageWithPlatform(d.Platform.String()))
|
|
err = rc.ImageCheckBase(ctx, rp, optP...)
|
|
if err != nil {
|
|
return fmt.Errorf("platform %s mismatch: %w", d.Platform.String(), err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
img, ok := m.(manifest.Imager)
|
|
if !ok {
|
|
return fmt.Errorf("manifest must be an image")
|
|
}
|
|
layers, err := img.GetLayers()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
baseM, err := rc.ManifestGet(ctx, baseR)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if baseM.IsList() && opt.platform != "" {
|
|
p, err := platform.Parse(opt.platform)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
d, err := manifest.GetPlatformDesc(baseM, &p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rp := baseR
|
|
rp.Digest = d.Digest.String()
|
|
baseM, err = rc.ManifestGet(ctx, rp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
baseImg, ok := baseM.(manifest.Imager)
|
|
if !ok {
|
|
return fmt.Errorf("base image manifest must be an image")
|
|
}
|
|
baseLayers, err := baseImg.GetLayers()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(baseLayers) <= 0 {
|
|
return fmt.Errorf("base image has no layers")
|
|
}
|
|
for i := range baseLayers {
|
|
if i >= len(layers) {
|
|
return fmt.Errorf("image has fewer layers than base image")
|
|
}
|
|
if !layers[i].Same(baseLayers[i]) {
|
|
rc.log.WithFields(logrus.Fields{
|
|
"layer": i,
|
|
"expected": layers[i].Digest.String(),
|
|
"digest": baseLayers[i].Digest.String(),
|
|
}).Debug("image layer changed")
|
|
return fmt.Errorf("base layer changed, %s[%d], expected %s, received %s%.0w",
|
|
baseR.CommonName(), i, layers[i].Digest.String(), baseLayers[i].Digest.String(), errs.ErrMismatch)
|
|
}
|
|
}
|
|
|
|
if opt.checkSkipConfig {
|
|
return nil
|
|
}
|
|
|
|
// if the layers match, compare the config history
|
|
confDesc, err := img.GetConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
conf, err := rc.BlobGetOCIConfig(ctx, r, confDesc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
confOCI := conf.GetConfig()
|
|
baseConfDesc, err := baseImg.GetConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
baseConf, err := rc.BlobGetOCIConfig(ctx, baseR, baseConfDesc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
baseConfOCI := baseConf.GetConfig()
|
|
for i := range baseConfOCI.History {
|
|
if i >= len(confOCI.History) {
|
|
return fmt.Errorf("image has fewer history entries than base image")
|
|
}
|
|
if baseConfOCI.History[i].Author != confOCI.History[i].Author ||
|
|
baseConfOCI.History[i].Comment != confOCI.History[i].Comment ||
|
|
!baseConfOCI.History[i].Created.Equal(*confOCI.History[i].Created) ||
|
|
baseConfOCI.History[i].CreatedBy != confOCI.History[i].CreatedBy ||
|
|
baseConfOCI.History[i].EmptyLayer != confOCI.History[i].EmptyLayer {
|
|
rc.log.WithFields(logrus.Fields{
|
|
"index": i,
|
|
"expected": confOCI.History[i],
|
|
"history": baseConfOCI.History[i],
|
|
}).Debug("image history changed")
|
|
return fmt.Errorf("base history changed, %s[%d], expected %v, received %v%.0w",
|
|
baseR.CommonName(), i, confOCI.History[i], baseConfOCI.History[i], errs.ErrMismatch)
|
|
}
|
|
}
|
|
|
|
rc.log.WithFields(logrus.Fields{
|
|
"base": baseR.CommonName(),
|
|
}).Debug("base image layers and history matches")
|
|
return nil
|
|
}
|
|
|
|
// ImageCopy copies an image.
|
|
// This will retag an image in the same repository, only pushing and pulling the top level manifest.
|
|
// On the same registry, it will attempt to use cross-repository blob mounts to avoid pulling blobs.
|
|
// Blobs are only pulled when they don't exist on the target and a blob mount fails.
|
|
// Referrers are optionally copied recursively.
|
|
func (rc *RegClient) ImageCopy(ctx context.Context, refSrc ref.Ref, refTgt ref.Ref, opts ...ImageOpts) error {
|
|
opt := imageOpt{
|
|
seen: map[string]*imageSeen{},
|
|
finalFn: []func(context.Context) error{},
|
|
}
|
|
for _, optFn := range opts {
|
|
optFn(&opt)
|
|
}
|
|
// dedup warnings
|
|
if w := warning.FromContext(ctx); w == nil {
|
|
ctx = warning.NewContext(ctx, &warning.Warning{Hook: warning.DefaultHook()})
|
|
}
|
|
// block GC from running (in OCIDir) during the copy
|
|
schemeTgtAPI, err := rc.schemeGet(refTgt.Scheme)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if tgtGCLocker, isGCLocker := schemeTgtAPI.(scheme.GCLocker); isGCLocker {
|
|
tgtGCLocker.GCLock(refTgt)
|
|
defer tgtGCLocker.GCUnlock(refTgt)
|
|
}
|
|
// run the copy of manifests and blobs recursively
|
|
err = rc.imageCopyOpt(ctx, refSrc, refTgt, descriptor.Descriptor{}, opt.child, []digest.Digest{}, &opt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// run any final functions, digest-tags and referrers that detected loops are retried here
|
|
for _, fn := range opt.finalFn {
|
|
err := fn(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// imageCopyOpt is a thread safe copy of a manifest and nested content.
|
|
func (rc *RegClient) imageCopyOpt(ctx context.Context, refSrc ref.Ref, refTgt ref.Ref, d descriptor.Descriptor, child bool, parents []digest.Digest, opt *imageOpt) (err error) {
|
|
var mSrc, mTgt manifest.Manifest
|
|
var sDig digest.Digest
|
|
seenCB := func(error) {}
|
|
defer func() {
|
|
if seenCB != nil {
|
|
seenCB(err)
|
|
}
|
|
}()
|
|
// if digest is provided and we are already copying it, wait
|
|
if d.Digest != "" {
|
|
sDig = d.Digest
|
|
} else if refSrc.Digest != "" {
|
|
sDig = digest.Digest(refSrc.Digest)
|
|
}
|
|
if sDig != "" {
|
|
if seenCB, err = imageSeenOrWait(ctx, opt, refTgt.Tag, sDig, parents); seenCB == nil {
|
|
return err
|
|
}
|
|
}
|
|
// check target with head request
|
|
mTgt, err = rc.ManifestHead(ctx, refTgt, WithManifestRequireDigest())
|
|
var urlError *url.Error
|
|
if err != nil && errors.As(err, &urlError) {
|
|
return fmt.Errorf("failed to access target registry: %w", err)
|
|
}
|
|
// for non-recursive copies, compare to source digest
|
|
if err == nil && (opt.fastCheck || (!opt.forceRecursive && opt.referrerConfs == nil && !opt.digestTags)) {
|
|
if sDig == "" {
|
|
mSrc, err = rc.ManifestHead(ctx, refSrc, WithManifestRequireDigest())
|
|
if err != nil {
|
|
return fmt.Errorf("copy failed, error getting source: %w", err)
|
|
}
|
|
sDig = mSrc.GetDescriptor().Digest
|
|
if seenCB, err = imageSeenOrWait(ctx, opt, refTgt.Tag, sDig, parents); seenCB == nil {
|
|
return err
|
|
}
|
|
}
|
|
if sDig == mTgt.GetDescriptor().Digest {
|
|
if opt.callback != nil {
|
|
opt.callback(types.CallbackManifest, d.Digest.String(), types.CallbackSkipped, mTgt.GetDescriptor().Size, mTgt.GetDescriptor().Size)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
// when copying/updating digest tags or referrers, only the source digest is needed for an image
|
|
if mTgt != nil && mSrc == nil && !opt.forceRecursive && sDig == "" {
|
|
mSrc, err = rc.ManifestHead(ctx, refSrc, WithManifestRequireDigest())
|
|
if err != nil {
|
|
return fmt.Errorf("copy failed, error getting source: %w", err)
|
|
}
|
|
sDig = mSrc.GetDescriptor().Digest
|
|
if seenCB, err = imageSeenOrWait(ctx, opt, refTgt.Tag, sDig, parents); seenCB == nil {
|
|
return err
|
|
}
|
|
}
|
|
// get the source manifest when a copy is needed or recursion into the content is needed
|
|
if sDig == "" || mTgt == nil || sDig != mTgt.GetDescriptor().Digest || opt.forceRecursive || mTgt.IsList() {
|
|
mSrc, err = rc.ManifestGet(ctx, refSrc, WithManifestDesc(d))
|
|
if err != nil {
|
|
return fmt.Errorf("copy failed, error getting source: %w", err)
|
|
}
|
|
if sDig == "" {
|
|
sDig = mSrc.GetDescriptor().Digest
|
|
if seenCB, err = imageSeenOrWait(ctx, opt, refTgt.Tag, sDig, parents); seenCB == nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
// setup vars for a copy
|
|
mOpts := []ManifestOpts{}
|
|
if child {
|
|
mOpts = append(mOpts, WithManifestChild())
|
|
}
|
|
bOpt := []BlobOpts{}
|
|
if opt.callback != nil {
|
|
bOpt = append(bOpt, BlobWithCallback(opt.callback))
|
|
}
|
|
waitCh := make(chan error)
|
|
waitCount := 0
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
parentsNew := make([]digest.Digest, len(parents)+1)
|
|
copy(parentsNew, parents)
|
|
parentsNew[len(parentsNew)-1] = sDig
|
|
if opt.callback != nil {
|
|
opt.callback(types.CallbackManifest, d.Digest.String(), types.CallbackStarted, 0, d.Size)
|
|
}
|
|
// process entries in an index
|
|
if mSrcIndex, ok := mSrc.(manifest.Indexer); ok && mSrc.IsSet() && !ref.EqualRepository(refSrc, refTgt) {
|
|
// manifest lists need to recursively copy nested images by digest
|
|
dList, err := mSrcIndex.GetManifestList()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, dEntry := range dList {
|
|
// skip copy of platforms not specifically included
|
|
if len(opt.platforms) > 0 {
|
|
match, err := imagePlatformInList(dEntry.Platform, opt.platforms)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !match {
|
|
rc.log.WithFields(logrus.Fields{
|
|
"platform": dEntry.Platform,
|
|
}).Debug("Platform excluded from copy")
|
|
continue
|
|
}
|
|
}
|
|
dEntry := dEntry
|
|
waitCount++
|
|
go func() {
|
|
var err error
|
|
rc.log.WithFields(logrus.Fields{
|
|
"platform": dEntry.Platform,
|
|
"digest": dEntry.Digest.String(),
|
|
}).Debug("Copy platform")
|
|
entrySrc := refSrc.SetDigest(dEntry.Digest.String())
|
|
entryTgt := refTgt.SetDigest(dEntry.Digest.String())
|
|
switch dEntry.MediaType {
|
|
case mediatype.Docker1Manifest, mediatype.Docker1ManifestSigned,
|
|
mediatype.Docker2Manifest, mediatype.Docker2ManifestList,
|
|
mediatype.OCI1Manifest, mediatype.OCI1ManifestList:
|
|
// known manifest media type
|
|
err = rc.imageCopyOpt(ctx, entrySrc, entryTgt, dEntry, true, parentsNew, opt)
|
|
case mediatype.Docker2ImageConfig, mediatype.OCI1ImageConfig,
|
|
mediatype.Docker2LayerGzip, mediatype.OCI1Layer, mediatype.OCI1LayerGzip,
|
|
mediatype.BuildkitCacheConfig:
|
|
// known blob media type
|
|
err = rc.imageCopyBlob(ctx, entrySrc, entryTgt, dEntry, opt, bOpt...)
|
|
default:
|
|
// unknown media type, first try an image copy
|
|
err = rc.imageCopyOpt(ctx, entrySrc, entryTgt, dEntry, true, parentsNew, opt)
|
|
if err != nil {
|
|
// fall back to trying to copy a blob
|
|
err = rc.imageCopyBlob(ctx, entrySrc, entryTgt, dEntry, opt, bOpt...)
|
|
}
|
|
}
|
|
waitCh <- err
|
|
}()
|
|
}
|
|
}
|
|
|
|
// copy referrers
|
|
referrerTags := []string{}
|
|
if opt.referrerConfs != nil {
|
|
rl, err := rc.ReferrerList(ctx, refSrc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
referrerTags = append(referrerTags, rl.Tags...)
|
|
descList := []descriptor.Descriptor{}
|
|
if len(opt.referrerConfs) == 0 {
|
|
descList = rl.Descriptors
|
|
} else {
|
|
for _, rConf := range opt.referrerConfs {
|
|
rlFilter := scheme.ReferrerFilter(rConf, rl)
|
|
descList = append(descList, rlFilter.Descriptors...)
|
|
}
|
|
}
|
|
for _, rDesc := range descList {
|
|
opt.mu.Lock()
|
|
seen := opt.seen[":"+rDesc.Digest.String()]
|
|
opt.mu.Unlock()
|
|
if seen != nil {
|
|
continue // skip referrers that have been seen
|
|
}
|
|
referrerSrc := refSrc.SetDigest(rDesc.Digest.String())
|
|
referrerTgt := refTgt.SetDigest(rDesc.Digest.String())
|
|
rDesc := rDesc
|
|
waitCount++
|
|
go func() {
|
|
err := rc.imageCopyOpt(ctx, referrerSrc, referrerTgt, rDesc, true, parentsNew, opt)
|
|
if errors.Is(err, errs.ErrLoopDetected) {
|
|
// if a loop is detected, push the referrers copy to the end
|
|
opt.mu.Lock()
|
|
opt.finalFn = append(opt.finalFn, func(ctx context.Context) error {
|
|
return rc.imageCopyOpt(ctx, referrerSrc, referrerTgt, rDesc, true, []digest.Digest{}, opt)
|
|
})
|
|
opt.mu.Unlock()
|
|
waitCh <- nil
|
|
} else {
|
|
if err != nil && !errors.Is(err, context.Canceled) {
|
|
rc.log.WithFields(logrus.Fields{
|
|
"digest": rDesc.Digest.String(),
|
|
"src": referrerSrc.CommonName(),
|
|
"tgt": referrerTgt.CommonName(),
|
|
}).Warn("Failed to copy referrer")
|
|
}
|
|
waitCh <- err
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
// lookup digest tags to include artifacts with image
|
|
if opt.digestTags {
|
|
// load tag listing for digest tag copy
|
|
opt.mu.Lock()
|
|
if opt.tagList == nil {
|
|
tl, err := rc.TagList(ctx, refSrc)
|
|
if err != nil {
|
|
opt.mu.Unlock()
|
|
rc.log.WithFields(logrus.Fields{
|
|
"source": refSrc.Reference,
|
|
"err": err,
|
|
}).Warn("Failed to list tags for digest-tag copy")
|
|
return err
|
|
}
|
|
tags, err := tl.GetTags()
|
|
if err != nil {
|
|
opt.mu.Unlock()
|
|
rc.log.WithFields(logrus.Fields{
|
|
"source": refSrc.Reference,
|
|
"err": err,
|
|
}).Warn("Failed to list tags for digest-tag copy")
|
|
return err
|
|
}
|
|
if tags == nil {
|
|
tags = []string{}
|
|
}
|
|
opt.tagList = tags
|
|
}
|
|
opt.mu.Unlock()
|
|
prefix := fmt.Sprintf("%s-%s", sDig.Algorithm(), sDig.Encoded())
|
|
for _, tag := range opt.tagList {
|
|
if strings.HasPrefix(tag, prefix) {
|
|
// skip referrers that were copied above
|
|
found := false
|
|
for _, referrerTag := range referrerTags {
|
|
if referrerTag == tag {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if found {
|
|
continue
|
|
}
|
|
refTagSrc := refSrc.SetTag(tag)
|
|
refTagTgt := refTgt.SetTag(tag)
|
|
tag := tag
|
|
waitCount++
|
|
go func() {
|
|
err := rc.imageCopyOpt(ctx, refTagSrc, refTagTgt, descriptor.Descriptor{}, false, parentsNew, opt)
|
|
if errors.Is(err, errs.ErrLoopDetected) {
|
|
// if a loop is detected, push the digest tag copy back to the end
|
|
opt.mu.Lock()
|
|
opt.finalFn = append(opt.finalFn, func(ctx context.Context) error {
|
|
return rc.imageCopyOpt(ctx, refTagSrc, refTagTgt, descriptor.Descriptor{}, false, []digest.Digest{}, opt)
|
|
})
|
|
opt.mu.Unlock()
|
|
waitCh <- nil
|
|
} else {
|
|
if err != nil && !errors.Is(err, context.Canceled) {
|
|
rc.log.WithFields(logrus.Fields{
|
|
"tag": tag,
|
|
"src": refTagSrc.CommonName(),
|
|
"tgt": refTagTgt.CommonName(),
|
|
}).Warn("Failed to copy digest-tag")
|
|
}
|
|
waitCh <- err
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
}
|
|
|
|
// check for any errors and abort early if found
|
|
err = nil
|
|
done := false
|
|
for !done && waitCount > 0 {
|
|
if err == nil {
|
|
select {
|
|
case err = <-waitCh:
|
|
if err != nil {
|
|
cancel()
|
|
}
|
|
default:
|
|
done = true // happy path
|
|
}
|
|
} else {
|
|
if errors.Is(err, context.Canceled) {
|
|
// try to find a better error message than context canceled
|
|
err = <-waitCh
|
|
} else {
|
|
<-waitCh
|
|
}
|
|
}
|
|
if !done {
|
|
waitCount--
|
|
}
|
|
}
|
|
if err != nil {
|
|
rc.log.WithFields(logrus.Fields{
|
|
"err": err,
|
|
"sDig": sDig,
|
|
}).Debug("child manifest copy failed")
|
|
return err
|
|
}
|
|
|
|
// If source is image, copy blobs
|
|
if mSrcImg, ok := mSrc.(manifest.Imager); ok && mSrc.IsSet() && !ref.EqualRepository(refSrc, refTgt) {
|
|
// copy the config
|
|
cd, err := mSrcImg.GetConfig()
|
|
if err != nil {
|
|
// docker schema v1 does not have a config object, ignore if it's missing
|
|
if !errors.Is(err, errs.ErrUnsupportedMediaType) {
|
|
rc.log.WithFields(logrus.Fields{
|
|
"ref": refSrc.Reference,
|
|
"err": err,
|
|
}).Warn("Failed to get config digest from manifest")
|
|
return fmt.Errorf("failed to get config digest for %s: %w", refSrc.CommonName(), err)
|
|
}
|
|
} else {
|
|
waitCount++
|
|
go func() {
|
|
rc.log.WithFields(logrus.Fields{
|
|
"source": refSrc.Reference,
|
|
"target": refTgt.Reference,
|
|
"digest": cd.Digest.String(),
|
|
}).Info("Copy config")
|
|
err := rc.imageCopyBlob(ctx, refSrc, refTgt, cd, opt, bOpt...)
|
|
if err != nil && !errors.Is(err, context.Canceled) {
|
|
rc.log.WithFields(logrus.Fields{
|
|
"source": refSrc.Reference,
|
|
"target": refTgt.Reference,
|
|
"digest": cd.Digest.String(),
|
|
"err": err,
|
|
}).Warn("Failed to copy config")
|
|
}
|
|
waitCh <- err
|
|
}()
|
|
}
|
|
|
|
// copy filesystem layers
|
|
l, err := mSrcImg.GetLayers()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, layerSrc := range l {
|
|
if len(layerSrc.URLs) > 0 && !opt.includeExternal {
|
|
// skip blobs where the URLs are defined, these aren't hosted and won't be pulled from the source
|
|
rc.log.WithFields(logrus.Fields{
|
|
"source": refSrc.Reference,
|
|
"target": refTgt.Reference,
|
|
"layer": layerSrc.Digest.String(),
|
|
"external-urls": layerSrc.URLs,
|
|
}).Debug("Skipping external layer")
|
|
continue
|
|
}
|
|
waitCount++
|
|
layerSrc := layerSrc
|
|
go func() {
|
|
rc.log.WithFields(logrus.Fields{
|
|
"source": refSrc.Reference,
|
|
"target": refTgt.Reference,
|
|
"layer": layerSrc.Digest.String(),
|
|
}).Info("Copy layer")
|
|
err := rc.imageCopyBlob(ctx, refSrc, refTgt, layerSrc, opt, bOpt...)
|
|
if err != nil && !errors.Is(err, context.Canceled) {
|
|
rc.log.WithFields(logrus.Fields{
|
|
"source": refSrc.Reference,
|
|
"target": refTgt.Reference,
|
|
"layer": layerSrc.Digest.String(),
|
|
"err": err,
|
|
}).Warn("Failed to copy layer")
|
|
}
|
|
waitCh <- err
|
|
}()
|
|
}
|
|
}
|
|
|
|
// wait for background tasks to finish
|
|
err = nil
|
|
for waitCount > 0 {
|
|
if err == nil {
|
|
err = <-waitCh
|
|
if err != nil {
|
|
cancel()
|
|
}
|
|
} else {
|
|
if errors.Is(err, context.Canceled) {
|
|
// try to find a better error message than context canceled
|
|
err = <-waitCh
|
|
} else {
|
|
<-waitCh
|
|
}
|
|
}
|
|
waitCount--
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// push manifest
|
|
if mTgt == nil || sDig != mTgt.GetDescriptor().Digest || opt.forceRecursive {
|
|
err = rc.ManifestPut(ctx, refTgt, mSrc, mOpts...)
|
|
if err != nil {
|
|
if !errors.Is(err, context.Canceled) {
|
|
rc.log.WithFields(logrus.Fields{
|
|
"target": refTgt.Reference,
|
|
"err": err,
|
|
}).Warn("Failed to push manifest")
|
|
}
|
|
return err
|
|
}
|
|
if opt.callback != nil {
|
|
opt.callback(types.CallbackManifest, d.Digest.String(), types.CallbackFinished, d.Size, d.Size)
|
|
}
|
|
} else {
|
|
if opt.callback != nil {
|
|
opt.callback(types.CallbackManifest, d.Digest.String(), types.CallbackSkipped, d.Size, d.Size)
|
|
}
|
|
}
|
|
if seenCB != nil {
|
|
seenCB(nil)
|
|
seenCB = nil
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (rc *RegClient) imageCopyBlob(ctx context.Context, refSrc ref.Ref, refTgt ref.Ref, d descriptor.Descriptor, opt *imageOpt, bOpt ...BlobOpts) error {
|
|
seenCB, err := imageSeenOrWait(ctx, opt, "", d.Digest, []digest.Digest{})
|
|
if seenCB == nil {
|
|
return err
|
|
}
|
|
err = rc.BlobCopy(ctx, refSrc, refTgt, d, bOpt...)
|
|
seenCB(err)
|
|
return err
|
|
}
|
|
|
|
// imageSeenOrWait returns either a callback to report the error when the digest hasn't been seen before
|
|
// or it will wait for the previous copy to run and return the error from that copy
|
|
func imageSeenOrWait(ctx context.Context, opt *imageOpt, tag string, dig digest.Digest, parents []digest.Digest) (func(error), error) {
|
|
var seenNew *imageSeen
|
|
key := tag + ":" + dig.String()
|
|
opt.mu.Lock()
|
|
seen := opt.seen[key]
|
|
if seen == nil {
|
|
seenNew = &imageSeen{
|
|
done: make(chan struct{}),
|
|
}
|
|
opt.seen[key] = seenNew
|
|
}
|
|
opt.mu.Unlock()
|
|
if seen != nil {
|
|
// quick check for the previous copy already done
|
|
select {
|
|
case <-seen.done:
|
|
return nil, seen.err
|
|
default:
|
|
}
|
|
// look for loops in parents
|
|
for _, p := range parents {
|
|
if key == tag+":"+p.String() {
|
|
return nil, errs.ErrLoopDetected
|
|
}
|
|
}
|
|
// wait for copy to finish or context to cancel
|
|
done := ctx.Done()
|
|
select {
|
|
case <-seen.done:
|
|
return nil, seen.err
|
|
case <-done:
|
|
return nil, ctx.Err()
|
|
}
|
|
} else {
|
|
return func(err error) {
|
|
seenNew.err = err
|
|
close(seenNew.done)
|
|
// on failures, delete the history to allow a retry
|
|
if err != nil {
|
|
opt.mu.Lock()
|
|
delete(opt.seen, key)
|
|
opt.mu.Unlock()
|
|
}
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
// ImageExport exports an image to an output stream.
|
|
// The format is compatible with "docker load" if a single image is selected and not a manifest list.
|
|
// The ref must include a tag for exporting to docker (defaults to latest), and may also include a digest.
|
|
// The export is also formatted according to [OCI Layout] which supports multi-platform images.
|
|
// A tar file will be sent to outStream.
|
|
//
|
|
// Resulting filesystem:
|
|
// - oci-layout: created at top level, can be done at the start
|
|
// - index.json: created at top level, single descriptor with org.opencontainers.image.ref.name annotation pointing to the tag
|
|
// - manifest.json: created at top level, based on every layer added, only works for a single arch image
|
|
// - blobs/$algo/$hash: each content addressable object (manifest, config, or layer), created recursively
|
|
//
|
|
// [OCI Layout]: https://github.com/opencontainers/image-spec/blob/master/image-layout.md
|
|
func (rc *RegClient) ImageExport(ctx context.Context, r ref.Ref, outStream io.Writer, opts ...ImageOpts) error {
|
|
if !r.IsSet() {
|
|
return fmt.Errorf("ref is not set: %s%.0w", r.CommonName(), errs.ErrInvalidReference)
|
|
}
|
|
var ociIndex v1.Index
|
|
|
|
var opt imageOpt
|
|
for _, optFn := range opts {
|
|
optFn(&opt)
|
|
}
|
|
if opt.exportRef.IsZero() {
|
|
opt.exportRef = r
|
|
}
|
|
|
|
// create tar writer object
|
|
out := outStream
|
|
if opt.exportCompress {
|
|
gzOut := gzip.NewWriter(out)
|
|
defer gzOut.Close()
|
|
out = gzOut
|
|
}
|
|
tw := tar.NewWriter(out)
|
|
defer tw.Close()
|
|
twd := &tarWriteData{
|
|
tw: tw,
|
|
dirs: map[string]bool{},
|
|
files: map[string]bool{},
|
|
mode: 0644,
|
|
}
|
|
|
|
// retrieve image manifest
|
|
m, err := rc.ManifestGet(ctx, r)
|
|
if err != nil {
|
|
rc.log.WithFields(logrus.Fields{
|
|
"ref": r.CommonName(),
|
|
"err": err,
|
|
}).Warn("Failed to get manifest")
|
|
return err
|
|
}
|
|
|
|
// build/write oci-layout
|
|
ociLayout := v1.ImageLayout{Version: ociLayoutVersion}
|
|
err = twd.tarWriteFileJSON(ociLayoutFilename, ociLayout)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// create a manifest descriptor
|
|
mDesc := m.GetDescriptor()
|
|
if mDesc.Annotations == nil {
|
|
mDesc.Annotations = map[string]string{}
|
|
}
|
|
mDesc.Annotations[annotationImageName] = opt.exportRef.CommonName()
|
|
mDesc.Annotations[annotationRefName] = opt.exportRef.Tag
|
|
|
|
// generate/write an OCI index
|
|
ociIndex.Versioned = v1.IndexSchemaVersion
|
|
ociIndex.Manifests = []descriptor.Descriptor{mDesc} // initialize with the descriptor to the manifest list
|
|
err = twd.tarWriteFileJSON(ociIndexFilename, ociIndex)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// append to docker manifest with tag, config filename, each layer filename, and layer descriptors
|
|
if mi, ok := m.(manifest.Imager); ok {
|
|
conf, err := mi.GetConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
refTag := opt.exportRef.ToReg()
|
|
if refTag.Digest != "" {
|
|
refTag.Digest = ""
|
|
}
|
|
if refTag.Tag == "" {
|
|
refTag.Tag = "latest"
|
|
}
|
|
dockerManifest := dockerTarManifest{
|
|
RepoTags: []string{refTag.CommonName()},
|
|
Config: tarOCILayoutDescPath(conf),
|
|
Layers: []string{},
|
|
LayerSources: map[digest.Digest]descriptor.Descriptor{},
|
|
}
|
|
dl, err := mi.GetLayers()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, d := range dl {
|
|
dockerManifest.Layers = append(dockerManifest.Layers, tarOCILayoutDescPath(d))
|
|
dockerManifest.LayerSources[d.Digest] = d
|
|
}
|
|
|
|
// marshal manifest and write manifest.json
|
|
err = twd.tarWriteFileJSON(dockerManifestFilename, []dockerTarManifest{dockerManifest})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// recursively include manifests and nested blobs
|
|
err = rc.imageExportDescriptor(ctx, r, mDesc, twd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// imageExportDescriptor pulls a manifest or blob, outputs to a tar file, and recursively processes any nested manifests or blobs
|
|
func (rc *RegClient) imageExportDescriptor(ctx context.Context, r ref.Ref, desc descriptor.Descriptor, twd *tarWriteData) error {
|
|
tarFilename := tarOCILayoutDescPath(desc)
|
|
if twd.files[tarFilename] {
|
|
// blob has already been imported into tar, skip
|
|
return nil
|
|
}
|
|
switch desc.MediaType {
|
|
case mediatype.Docker1Manifest, mediatype.Docker1ManifestSigned, mediatype.Docker2Manifest, mediatype.OCI1Manifest:
|
|
// Handle single platform manifests
|
|
// retrieve manifest
|
|
m, err := rc.ManifestGet(ctx, r, WithManifestDesc(desc))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mi, ok := m.(manifest.Imager)
|
|
if !ok {
|
|
return fmt.Errorf("manifest doesn't support image methods%.0w", errs.ErrUnsupportedMediaType)
|
|
}
|
|
// write manifest body by digest
|
|
mBody, err := m.RawBody()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = twd.tarWriteHeader(tarFilename, int64(len(mBody)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = twd.tw.Write(mBody)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// add config
|
|
confD, err := mi.GetConfig()
|
|
// ignore unsupported media type errors
|
|
if err != nil && !errors.Is(err, errs.ErrUnsupportedMediaType) {
|
|
return err
|
|
}
|
|
if err == nil {
|
|
err = rc.imageExportDescriptor(ctx, r, confD, twd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// loop over layers
|
|
layerDL, err := mi.GetLayers()
|
|
// ignore unsupported media type errors
|
|
if err != nil && !errors.Is(err, errs.ErrUnsupportedMediaType) {
|
|
return err
|
|
}
|
|
if err == nil {
|
|
for _, layerD := range layerDL {
|
|
err = rc.imageExportDescriptor(ctx, r, layerD, twd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
case mediatype.Docker2ManifestList, mediatype.OCI1ManifestList:
|
|
// handle OCI index and Docker manifest list
|
|
// retrieve manifest
|
|
m, err := rc.ManifestGet(ctx, r, WithManifestDesc(desc))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mi, ok := m.(manifest.Indexer)
|
|
if !ok {
|
|
return fmt.Errorf("manifest doesn't support index methods%.0w", errs.ErrUnsupportedMediaType)
|
|
}
|
|
// write manifest body by digest
|
|
mBody, err := m.RawBody()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = twd.tarWriteHeader(tarFilename, int64(len(mBody)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = twd.tw.Write(mBody)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// recurse over entries in the list/index
|
|
mdl, err := mi.GetManifestList()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, md := range mdl {
|
|
err = rc.imageExportDescriptor(ctx, r, md, twd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
default:
|
|
// get blob
|
|
blobR, err := rc.BlobGet(ctx, r, desc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer blobR.Close()
|
|
// write blob by digest
|
|
err = twd.tarWriteHeader(tarFilename, int64(desc.Size))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
size, err := io.Copy(twd.tw, blobR)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to export blob %s: %w", desc.Digest.String(), err)
|
|
}
|
|
if size != desc.Size {
|
|
return fmt.Errorf("blob size mismatch, descriptor %d, received %d", desc.Size, size)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ImageImport pushes an image from a tar file (ImageExport) to a registry.
|
|
func (rc *RegClient) ImageImport(ctx context.Context, r ref.Ref, rs io.ReadSeeker, opts ...ImageOpts) error {
|
|
if !r.IsSetRepo() {
|
|
return fmt.Errorf("ref is not set: %s%.0w", r.CommonName(), errs.ErrInvalidReference)
|
|
}
|
|
var opt imageOpt
|
|
for _, optFn := range opts {
|
|
optFn(&opt)
|
|
}
|
|
|
|
trd := &tarReadData{
|
|
name: opt.importName,
|
|
handlers: map[string]tarFileHandler{},
|
|
links: map[string][]string{},
|
|
processed: map[string]bool{},
|
|
finish: []func() error{},
|
|
manifests: map[digest.Digest]manifest.Manifest{},
|
|
}
|
|
|
|
// add handler for oci-layout, index.json, and manifest.json
|
|
rc.imageImportOCIAddHandler(ctx, r, trd)
|
|
rc.imageImportDockerAddHandler(trd)
|
|
|
|
// process tar file looking for oci-layout and index.json, load manifests/blobs on success
|
|
err := trd.tarReadAll(rs)
|
|
|
|
if err != nil && errors.Is(err, errs.ErrNotFound) && trd.dockerManifestFound {
|
|
// import failed but manifest.json found, fall back to manifest.json processing
|
|
// add handlers for the docker manifest layers
|
|
rc.imageImportDockerAddLayerHandlers(ctx, r, trd)
|
|
// reprocess the tar looking for manifest.json files
|
|
err = trd.tarReadAll(rs)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to import layers from docker tar: %w", err)
|
|
}
|
|
// push docker manifest
|
|
m, err := manifest.New(manifest.WithOrig(trd.dockerManifest))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = rc.ManifestPut(ctx, r, m)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else if err != nil {
|
|
// unhandled error from tar read
|
|
return err
|
|
} else {
|
|
// successful load of OCI blobs, now push manifest and tag
|
|
err = rc.imageImportOCIPushManifests(ctx, r, trd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (rc *RegClient) imageImportBlob(ctx context.Context, r ref.Ref, desc descriptor.Descriptor, trd *tarReadData) error {
|
|
// skip if blob already exists
|
|
_, err := rc.BlobHead(ctx, r, desc)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
// upload blob
|
|
_, err = rc.BlobPut(ctx, r, desc, trd.tr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// imageImportDockerAddHandler processes tar files generated by docker.
|
|
func (rc *RegClient) imageImportDockerAddHandler(trd *tarReadData) {
|
|
trd.handlers[dockerManifestFilename] = func(header *tar.Header, trd *tarReadData) error {
|
|
err := trd.tarReadFileJSON(&trd.dockerManifestList)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
trd.dockerManifestFound = true
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// imageImportDockerAddLayerHandlers imports the docker layers when OCI import fails and docker manifest found.
|
|
func (rc *RegClient) imageImportDockerAddLayerHandlers(ctx context.Context, r ref.Ref, trd *tarReadData) {
|
|
// remove handlers for OCI
|
|
delete(trd.handlers, ociLayoutFilename)
|
|
delete(trd.handlers, ociIndexFilename)
|
|
|
|
index := 0
|
|
if trd.name != "" {
|
|
found := false
|
|
tags := []string{}
|
|
for i, entry := range trd.dockerManifestList {
|
|
tags = append(tags, entry.RepoTags...)
|
|
for _, tag := range entry.RepoTags {
|
|
if tag == trd.name {
|
|
index = i
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if found {
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
rc.log.WithFields(logrus.Fields{
|
|
"tags": tags,
|
|
"name": trd.name,
|
|
}).Warn("Could not find requested name")
|
|
return
|
|
}
|
|
}
|
|
|
|
// make a docker v2 manifest from first json array entry (can only tag one image)
|
|
trd.dockerManifest.SchemaVersion = 2
|
|
trd.dockerManifest.MediaType = mediatype.Docker2Manifest
|
|
trd.dockerManifest.Layers = make([]descriptor.Descriptor, len(trd.dockerManifestList[index].Layers))
|
|
|
|
// add handler for config
|
|
trd.handlers[filepath.Clean(trd.dockerManifestList[index].Config)] = func(header *tar.Header, trd *tarReadData) error {
|
|
// upload blob, digest is unknown
|
|
d, err := rc.BlobPut(ctx, r, descriptor.Descriptor{Size: header.Size}, trd.tr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// save the resulting descriptor to the manifest
|
|
if od, ok := trd.dockerManifestList[index].LayerSources[d.Digest]; ok {
|
|
trd.dockerManifest.Config = od
|
|
} else {
|
|
d.MediaType = mediatype.Docker2ImageConfig
|
|
trd.dockerManifest.Config = d
|
|
}
|
|
return nil
|
|
}
|
|
// add handlers for each layer
|
|
for i, layerFile := range trd.dockerManifestList[index].Layers {
|
|
func(i int) {
|
|
trd.handlers[filepath.Clean(layerFile)] = func(header *tar.Header, trd *tarReadData) error {
|
|
// ensure blob is compressed with gzip to match media type
|
|
gzipR, err := archive.Compress(trd.tr, archive.CompressGzip)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// upload blob, digest and size is unknown
|
|
d, err := rc.BlobPut(ctx, r, descriptor.Descriptor{}, gzipR)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// save the resulting descriptor in the appropriate layer
|
|
if od, ok := trd.dockerManifestList[index].LayerSources[d.Digest]; ok {
|
|
trd.dockerManifest.Layers[i] = od
|
|
} else {
|
|
d.MediaType = mediatype.Docker2LayerGzip
|
|
trd.dockerManifest.Layers[i] = d
|
|
}
|
|
return nil
|
|
}
|
|
}(i)
|
|
}
|
|
trd.handleAdded = true
|
|
}
|
|
|
|
// imageImportOCIAddHandler adds handlers for oci-layout and index.json found in OCI layout tar files.
|
|
func (rc *RegClient) imageImportOCIAddHandler(ctx context.Context, r ref.Ref, trd *tarReadData) {
|
|
// add handler for oci-layout, index.json, and manifest.json
|
|
var err error
|
|
var foundLayout, foundIndex bool
|
|
|
|
// common handler code when both oci-layout and index.json have been processed
|
|
ociHandler := func(trd *tarReadData) error {
|
|
// no need to process docker manifest.json when OCI layout is available
|
|
delete(trd.handlers, dockerManifestFilename)
|
|
// create a manifest from the index
|
|
trd.ociManifest, err = manifest.New(manifest.WithOrig(trd.ociIndex))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// start recursively processing manifests starting with the index
|
|
// there's no need to push the index.json by digest, it will be pushed by tag if needed
|
|
err = rc.imageImportOCIHandleManifest(ctx, r, trd.ociManifest, trd, false, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
trd.handlers[ociLayoutFilename] = func(header *tar.Header, trd *tarReadData) error {
|
|
var ociLayout v1.ImageLayout
|
|
err := trd.tarReadFileJSON(&ociLayout)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ociLayout.Version != ociLayoutVersion {
|
|
// unknown version, ignore
|
|
rc.log.WithFields(logrus.Fields{
|
|
"version": ociLayout.Version,
|
|
}).Warn("Unsupported oci-layout version")
|
|
return nil
|
|
}
|
|
foundLayout = true
|
|
if foundIndex {
|
|
err = ociHandler(trd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
trd.handlers[ociIndexFilename] = func(header *tar.Header, trd *tarReadData) error {
|
|
err := trd.tarReadFileJSON(&trd.ociIndex)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
foundIndex = true
|
|
if foundLayout {
|
|
err = ociHandler(trd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// imageImportOCIHandleManifest recursively processes index and manifest entries from an OCI layout tar.
|
|
func (rc *RegClient) imageImportOCIHandleManifest(ctx context.Context, r ref.Ref, m manifest.Manifest, trd *tarReadData, push bool, child bool) error {
|
|
// cache the manifest to avoid needing to pull again later, this is used if index.json is a wrapper around some other manifest
|
|
trd.manifests[m.GetDescriptor().Digest] = m
|
|
|
|
handleManifest := func(d descriptor.Descriptor, child bool) {
|
|
filename := tarOCILayoutDescPath(d)
|
|
if !trd.processed[filename] && trd.handlers[filename] == nil {
|
|
trd.handlers[filename] = func(header *tar.Header, trd *tarReadData) error {
|
|
b, err := io.ReadAll(trd.tr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch d.MediaType {
|
|
case mediatype.Docker1Manifest, mediatype.Docker1ManifestSigned,
|
|
mediatype.Docker2Manifest, mediatype.Docker2ManifestList,
|
|
mediatype.OCI1Manifest, mediatype.OCI1ManifestList:
|
|
// known manifest media types
|
|
md, err := manifest.New(manifest.WithDesc(d), manifest.WithRaw(b))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return rc.imageImportOCIHandleManifest(ctx, r, md, trd, true, child)
|
|
case mediatype.Docker2ImageConfig, mediatype.OCI1ImageConfig,
|
|
mediatype.Docker2LayerGzip, mediatype.OCI1Layer, mediatype.OCI1LayerGzip,
|
|
mediatype.BuildkitCacheConfig:
|
|
// known blob media types
|
|
return rc.imageImportBlob(ctx, r, d, trd)
|
|
default:
|
|
// attempt manifest import, fall back to blob import
|
|
md, err := manifest.New(manifest.WithDesc(d), manifest.WithRaw(b))
|
|
if err == nil {
|
|
return rc.imageImportOCIHandleManifest(ctx, r, md, trd, true, child)
|
|
}
|
|
return rc.imageImportBlob(ctx, r, d, trd)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !push {
|
|
mi, ok := m.(manifest.Indexer)
|
|
if !ok {
|
|
return fmt.Errorf("manifest doesn't support image methods%.0w", errs.ErrUnsupportedMediaType)
|
|
}
|
|
// for root index, add handler for matching reference (or only reference)
|
|
dl, err := mi.GetManifestList()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// locate the digest in the index
|
|
var d descriptor.Descriptor
|
|
if len(dl) == 1 {
|
|
d = dl[0]
|
|
} else if r.Digest != "" {
|
|
d.Digest = digest.Digest(r.Digest)
|
|
} else if trd.name != "" {
|
|
for _, cur := range dl {
|
|
if cur.Annotations[annotationRefName] == trd.name {
|
|
d = cur
|
|
break
|
|
}
|
|
}
|
|
if d.Digest.String() == "" {
|
|
return fmt.Errorf("could not find requested tag in index.json, %s", trd.name)
|
|
}
|
|
} else {
|
|
if r.Tag == "" {
|
|
r.Tag = "latest"
|
|
}
|
|
// if more than one digest is in the index, use the first matching tag
|
|
for _, cur := range dl {
|
|
if cur.Annotations[annotationRefName] == r.Tag {
|
|
d = cur
|
|
break
|
|
}
|
|
}
|
|
if d.Digest.String() == "" {
|
|
return fmt.Errorf("could not find requested tag in index.json, %s", r.Tag)
|
|
}
|
|
}
|
|
handleManifest(d, false)
|
|
// add a finish step to tag the selected digest
|
|
trd.finish = append(trd.finish, func() error {
|
|
mRef, ok := trd.manifests[d.Digest]
|
|
if !ok {
|
|
return fmt.Errorf("could not find manifest to tag, ref: %s, digest: %s", r.CommonName(), d.Digest)
|
|
}
|
|
return rc.ManifestPut(ctx, r, mRef)
|
|
})
|
|
} else if m.IsList() {
|
|
// for index/manifest lists, add handlers for each embedded manifest
|
|
mi, ok := m.(manifest.Indexer)
|
|
if !ok {
|
|
return fmt.Errorf("manifest doesn't support index methods%.0w", errs.ErrUnsupportedMediaType)
|
|
}
|
|
dl, err := mi.GetManifestList()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, d := range dl {
|
|
handleManifest(d, true)
|
|
}
|
|
} else {
|
|
// else if a single image/manifest
|
|
mi, ok := m.(manifest.Imager)
|
|
if !ok {
|
|
return fmt.Errorf("manifest doesn't support image methods%.0w", errs.ErrUnsupportedMediaType)
|
|
}
|
|
// add handler for the config descriptor if it's defined
|
|
cd, err := mi.GetConfig()
|
|
if err == nil {
|
|
filename := tarOCILayoutDescPath(cd)
|
|
if !trd.processed[filename] && trd.handlers[filename] == nil {
|
|
func(cd descriptor.Descriptor) {
|
|
trd.handlers[filename] = func(header *tar.Header, trd *tarReadData) error {
|
|
return rc.imageImportBlob(ctx, r, cd, trd)
|
|
}
|
|
}(cd)
|
|
}
|
|
}
|
|
// add handlers for each layer
|
|
layers, err := mi.GetLayers()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, d := range layers {
|
|
filename := tarOCILayoutDescPath(d)
|
|
if !trd.processed[filename] && trd.handlers[filename] == nil {
|
|
func(d descriptor.Descriptor) {
|
|
trd.handlers[filename] = func(header *tar.Header, trd *tarReadData) error {
|
|
return rc.imageImportBlob(ctx, r, d, trd)
|
|
}
|
|
}(d)
|
|
}
|
|
}
|
|
}
|
|
// add a finish func to push the manifest, this gets skipped for the index.json
|
|
if push {
|
|
trd.finish = append(trd.finish, func() error {
|
|
mRef := r
|
|
mRef.Digest = string(m.GetDescriptor().Digest)
|
|
_, err := rc.ManifestHead(ctx, mRef)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
opts := []ManifestOpts{}
|
|
if child {
|
|
opts = append(opts, WithManifestChild())
|
|
}
|
|
return rc.ManifestPut(ctx, mRef, m, opts...)
|
|
})
|
|
}
|
|
trd.handleAdded = true
|
|
return nil
|
|
}
|
|
|
|
// imageImportOCIPushManifests uploads manifests after OCI blobs were successfully loaded.
|
|
func (rc *RegClient) imageImportOCIPushManifests(ctx context.Context, r ref.Ref, trd *tarReadData) error {
|
|
// run finish handlers in reverse order to upload nested manifests
|
|
for i := len(trd.finish) - 1; i >= 0; i-- {
|
|
err := trd.finish[i]()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func imagePlatformInList(target *platform.Platform, list []string) (bool, error) {
|
|
// special case for an unset platform
|
|
if target == nil || target.OS == "" {
|
|
for _, entry := range list {
|
|
if entry == "" {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
for _, entry := range list {
|
|
if entry == "" {
|
|
continue
|
|
}
|
|
plat, err := platform.Parse(entry)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if platform.Match(*target, plat) {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// tarReadAll processes the tar file in a loop looking for matching filenames in the list of handlers.
|
|
// Handlers for filenames are added at the top level, and by manifest imports.
|
|
func (trd *tarReadData) tarReadAll(rs io.ReadSeeker) error {
|
|
// return immediately if nothing to do
|
|
if len(trd.handlers) == 0 {
|
|
return nil
|
|
}
|
|
for {
|
|
// reset back to beginning of tar file
|
|
_, err := rs.Seek(0, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dr, err := archive.Decompress(rs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
trd.tr = tar.NewReader(dr)
|
|
trd.handleAdded = false
|
|
// loop over each entry of the tar file
|
|
for {
|
|
header, err := trd.tr.Next()
|
|
if err == io.EOF {
|
|
break
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
name := filepath.Clean(header.Name)
|
|
// track symlinks
|
|
if header.Typeflag == tar.TypeSymlink || header.Typeflag == tar.TypeLink {
|
|
// normalize target relative to root of tar
|
|
target := header.Linkname
|
|
if !filepath.IsAbs(target) {
|
|
target, err = filepath.Rel(filepath.Dir(name), target)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
target = filepath.Clean("/" + target)[1:]
|
|
// track and set handleAdded if an existing handler points to the target
|
|
if trd.linkAdd(name, target) && !trd.handleAdded {
|
|
list, err := trd.linkList(target)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, src := range append(list, name) {
|
|
if trd.handlers[src] != nil {
|
|
trd.handleAdded = true
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// loop through filename and symlinks to file in search of handlers
|
|
list, err := trd.linkList(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
list = append(list, name)
|
|
trdUsed := false
|
|
for _, entry := range list {
|
|
if trd.handlers[entry] != nil {
|
|
// trd cannot be reused, force the loop to run again
|
|
if trdUsed {
|
|
trd.handleAdded = true
|
|
break
|
|
}
|
|
trdUsed = true
|
|
// run handler
|
|
err = trd.handlers[entry](header, trd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
delete(trd.handlers, entry)
|
|
trd.processed[entry] = true
|
|
// return if last handler processed
|
|
if len(trd.handlers) == 0 {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// if entire file read without adding a new handler, fail
|
|
if !trd.handleAdded {
|
|
return fmt.Errorf("unable to read all files from tar: %w", errs.ErrNotFound)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (trd *tarReadData) linkAdd(src, tgt string) bool {
|
|
for _, entry := range trd.links[tgt] {
|
|
if entry == src {
|
|
return false
|
|
}
|
|
}
|
|
trd.links[tgt] = append(trd.links[tgt], src)
|
|
return true
|
|
}
|
|
|
|
func (trd *tarReadData) linkList(tgt string) ([]string, error) {
|
|
list := trd.links[tgt]
|
|
for _, entry := range list {
|
|
if entry == tgt {
|
|
return nil, fmt.Errorf("symlink loop encountered for %s", tgt)
|
|
}
|
|
list = append(list, trd.links[entry]...)
|
|
}
|
|
return list, nil
|
|
}
|
|
|
|
// tarReadFileJSON reads the current tar entry and unmarshals json into provided interface.
|
|
func (trd *tarReadData) tarReadFileJSON(data interface{}) error {
|
|
b, err := io.ReadAll(trd.tr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = json.Unmarshal(b, data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var errTarFileExists = errors.New("tar file already exists")
|
|
|
|
func (td *tarWriteData) tarWriteHeader(filename string, size int64) error {
|
|
dirname := filepath.Dir(filename)
|
|
if !td.dirs[dirname] && dirname != "." {
|
|
header := tar.Header{
|
|
Format: tar.FormatPAX,
|
|
Typeflag: tar.TypeDir,
|
|
Name: dirname,
|
|
Size: 0,
|
|
Mode: td.mode | 0511,
|
|
ModTime: td.timestamp,
|
|
AccessTime: td.timestamp,
|
|
ChangeTime: td.timestamp,
|
|
}
|
|
err := td.tw.WriteHeader(&header)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
td.dirs[dirname] = true
|
|
}
|
|
if td.files[filename] {
|
|
return fmt.Errorf("%w: %s", errTarFileExists, filename)
|
|
}
|
|
td.files[filename] = true
|
|
header := tar.Header{
|
|
Format: tar.FormatPAX,
|
|
Typeflag: tar.TypeReg,
|
|
Name: filename,
|
|
Size: size,
|
|
Mode: td.mode | 0400,
|
|
ModTime: td.timestamp,
|
|
AccessTime: td.timestamp,
|
|
ChangeTime: td.timestamp,
|
|
}
|
|
return td.tw.WriteHeader(&header)
|
|
}
|
|
|
|
func (td *tarWriteData) tarWriteFileJSON(filename string, data interface{}) error {
|
|
dataJSON, err := json.Marshal(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = td.tarWriteHeader(filename, int64(len(dataJSON)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = td.tw.Write(dataJSON)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func tarOCILayoutDescPath(d descriptor.Descriptor) string {
|
|
return filepath.Clean(fmt.Sprintf("blobs/%s/%s", d.Digest.Algorithm(), d.Digest.Encoded()))
|
|
}
|