mirror of
https://github.com/regclient/regclient.git
synced 2025-04-17 11:37:11 +03:00
- Use "any" instead of an empty interface. - Use range over an integer for for loops. - Remove shadow variables in loops now that Go no longer reuses the variable. - Use "slices.Contains", "slices.Delete", "slices.Equal", "slices.Index", "slices.SortFunc". - Use "cmp.Or", "min", and "max". - Use "fmt.Appendf" instead of "Sprintf" for generating a byte slice. - Use "errors.Join" or "fmt.Errorf" with multiple "%w" for multiple errors. Additionally, use modern regclient features: - Use "ref.SetTag", "ref.SetDigest", and "ref.AddDigest". - Call "regclient.ManifestGet" using "WithManifestDesc" instead of setting the digest on the reference. Signed-off-by: Brandon Mitchell <git@bmitch.net>
1889 lines
55 KiB
Go
1889 lines
55 KiB
Go
package regclient
|
|
|
|
import (
|
|
"archive/tar"
|
|
"cmp"
|
|
"compress/gzip"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/url"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
// crypto libraries included for go-digest
|
|
_ "crypto/sha256"
|
|
_ "crypto/sha512"
|
|
|
|
digest "github.com/opencontainers/go-digest"
|
|
|
|
"github.com/regclient/regclient/pkg/archive"
|
|
"github.com/regclient/regclient/scheme"
|
|
"github.com/regclient/regclient/types"
|
|
"github.com/regclient/regclient/types/blob"
|
|
"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
|
|
referrerSrc ref.Ref
|
|
referrerTgt ref.Ref
|
|
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)
|
|
}
|
|
}
|
|
|
|
// ImageWithReferrerSrc specifies an alternate repository to pull referrers from.
|
|
func ImageWithReferrerSrc(src ref.Ref) ImageOpts {
|
|
return func(opts *imageOpt) {
|
|
opts.referrerSrc = src
|
|
}
|
|
}
|
|
|
|
// ImageWithReferrerTgt specifies an alternate repository to pull referrers from.
|
|
func ImageWithReferrerTgt(tgt ref.Ref) ImageOpts {
|
|
return func(opts *imageOpt) {
|
|
opts.referrerTgt = tgt
|
|
}
|
|
}
|
|
|
|
// 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
|
|
|
|
// dedup warnings
|
|
if w := warning.FromContext(ctx); w == nil {
|
|
ctx = warning.NewContext(ctx, &warning.Warning{Hook: warning.DefaultHook()})
|
|
}
|
|
// 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.slog.Debug("base image digest matches",
|
|
slog.String("name", baseR.CommonName()),
|
|
slog.String("digest", baseMH.GetDescriptor().Digest.String()))
|
|
return nil
|
|
} else {
|
|
rc.slog.Debug("base image digest changed",
|
|
slog.String("name", baseR.CommonName()),
|
|
slog.String("digest", baseMH.GetDescriptor().Digest.String()),
|
|
slog.String("expected", expectDig.String()))
|
|
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.AddDigest(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
|
|
}
|
|
for _, d := range dl {
|
|
rp := r.AddDigest(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
|
|
}
|
|
baseM, err = rc.ManifestGet(ctx, baseR, WithManifestDesc(*d))
|
|
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.slog.Debug("image layer changed",
|
|
slog.Int("layer", i),
|
|
slog.String("expected", layers[i].Digest.String()),
|
|
slog.String("digest", baseLayers[i].Digest.String()))
|
|
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.slog.Debug("image history changed",
|
|
slog.Int("index", i),
|
|
slog.Any("expected", confOCI.History[i]),
|
|
slog.Any("history", baseConfOCI.History[i]))
|
|
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.slog.Debug("base image layers and history matches",
|
|
slog.String("base", baseR.CommonName()))
|
|
return nil
|
|
}
|
|
|
|
// ImageConfig returns the OCI config of a given image.
|
|
// Use [ImageWithPlatform] to select a platform from an Index or Manifest List.
|
|
func (rc *RegClient) ImageConfig(ctx context.Context, r ref.Ref, opts ...ImageOpts) (*blob.BOCIConfig, error) {
|
|
opt := imageOpt{
|
|
platform: "local",
|
|
}
|
|
for _, optFn := range opts {
|
|
optFn(&opt)
|
|
}
|
|
// dedup warnings
|
|
if w := warning.FromContext(ctx); w == nil {
|
|
ctx = warning.NewContext(ctx, &warning.Warning{Hook: warning.DefaultHook()})
|
|
}
|
|
p, err := platform.Parse(opt.platform)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse platform %s: %w", opt.platform, err)
|
|
}
|
|
m, err := rc.ManifestGet(ctx, r, WithManifestPlatform(p))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get manifest: %w", err)
|
|
}
|
|
for m.IsList() {
|
|
mi, ok := m.(manifest.Indexer)
|
|
if !ok {
|
|
return nil, fmt.Errorf("unsupported manifest type: %s", m.GetDescriptor().MediaType)
|
|
}
|
|
ml, err := mi.GetManifestList()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get manifest list: %w", err)
|
|
}
|
|
d, err := descriptor.DescriptorListSearch(ml, descriptor.MatchOpt{Platform: &p})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to find platform in manifest list: %w", err)
|
|
}
|
|
m, err = rc.ManifestGet(ctx, r, WithManifestDesc(d))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get manifest: %w", err)
|
|
}
|
|
}
|
|
mi, ok := m.(manifest.Imager)
|
|
if !ok {
|
|
return nil, fmt.Errorf("unsupported manifest type: %s", m.GetDescriptor().MediaType)
|
|
}
|
|
d, err := mi.GetConfig()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get image config: %w", err)
|
|
}
|
|
if d.MediaType != mediatype.OCI1ImageConfig && d.MediaType != mediatype.Docker2ImageConfig {
|
|
return nil, fmt.Errorf("unsupported config media type %s: %w", d.MediaType, errs.ErrUnsupportedMediaType)
|
|
}
|
|
return rc.BlobGetOCIConfig(ctx, r, d)
|
|
}
|
|
|
|
// 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
|
|
refTgtRepo := refTgt.SetTag("").CommonName()
|
|
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, refTgtRepo, 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, refTgtRepo, 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, refTgtRepo, 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, refTgtRepo, 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.slog.Debug("Platform excluded from copy",
|
|
slog.Any("platform", dEntry.Platform))
|
|
continue
|
|
}
|
|
}
|
|
waitCount++
|
|
go func() {
|
|
var err error
|
|
rc.slog.Debug("Copy platform",
|
|
slog.Any("platform", dEntry.Platform),
|
|
slog.String("digest", dEntry.Digest.String()))
|
|
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.Docker2Layer, mediatype.Docker2LayerGzip, mediatype.Docker2LayerZstd,
|
|
mediatype.OCI1Layer, mediatype.OCI1LayerGzip, mediatype.OCI1LayerZstd,
|
|
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
|
|
}()
|
|
}
|
|
}
|
|
|
|
// 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.slog.Warn("Failed to get config digest from manifest",
|
|
slog.String("ref", refSrc.Reference),
|
|
slog.String("err", err.Error()))
|
|
return fmt.Errorf("failed to get config digest for %s: %w", refSrc.CommonName(), err)
|
|
}
|
|
} else {
|
|
waitCount++
|
|
go func() {
|
|
rc.slog.Info("Copy config",
|
|
slog.String("source", refSrc.Reference),
|
|
slog.String("target", refTgt.Reference),
|
|
slog.String("digest", cd.Digest.String()))
|
|
err := rc.imageCopyBlob(ctx, refSrc, refTgt, cd, opt, bOpt...)
|
|
if err != nil && !errors.Is(err, context.Canceled) {
|
|
rc.slog.Warn("Failed to copy config",
|
|
slog.String("source", refSrc.Reference),
|
|
slog.String("target", refTgt.Reference),
|
|
slog.String("digest", cd.Digest.String()),
|
|
slog.String("err", err.Error()))
|
|
}
|
|
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.slog.Debug("Skipping external layer",
|
|
slog.String("source", refSrc.Reference),
|
|
slog.String("target", refTgt.Reference),
|
|
slog.String("layer", layerSrc.Digest.String()),
|
|
slog.Any("external-urls", layerSrc.URLs))
|
|
continue
|
|
}
|
|
waitCount++
|
|
go func() {
|
|
rc.slog.Info("Copy layer",
|
|
slog.String("source", refSrc.Reference),
|
|
slog.String("target", refTgt.Reference),
|
|
slog.String("layer", layerSrc.Digest.String()))
|
|
err := rc.imageCopyBlob(ctx, refSrc, refTgt, layerSrc, opt, bOpt...)
|
|
if err != nil && !errors.Is(err, context.Canceled) {
|
|
rc.slog.Warn("Failed to copy layer",
|
|
slog.String("source", refSrc.Reference),
|
|
slog.String("target", refTgt.Reference),
|
|
slog.String("layer", layerSrc.Digest.String()),
|
|
slog.String("err", err.Error()))
|
|
}
|
|
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.slog.Debug("child manifest copy failed",
|
|
slog.String("err", err.Error()),
|
|
slog.String("sDig", sDig.String()))
|
|
return err
|
|
}
|
|
|
|
// copy referrers
|
|
referrerTags := []string{}
|
|
if opt.referrerConfs != nil {
|
|
referrerOpts := []scheme.ReferrerOpts{}
|
|
rSubject := refSrc
|
|
referrerSrc := refSrc
|
|
referrerTgt := refTgt
|
|
if opt.referrerSrc.IsSet() {
|
|
referrerOpts = append(referrerOpts, scheme.WithReferrerSource(opt.referrerSrc))
|
|
referrerSrc = opt.referrerSrc
|
|
}
|
|
if opt.referrerTgt.IsSet() {
|
|
referrerTgt = opt.referrerTgt
|
|
}
|
|
if sDig != "" {
|
|
rSubject = rSubject.SetDigest(sDig.String())
|
|
}
|
|
rl, err := rc.ReferrerList(ctx, rSubject, referrerOpts...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !rl.Source.IsSet() || ref.EqualRepository(refSrc, rl.Source) {
|
|
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 := referrerSrc.SetDigest(rDesc.Digest.String())
|
|
referrerTgt := referrerTgt.SetDigest(rDesc.Digest.String())
|
|
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.slog.Warn("Failed to copy referrer",
|
|
slog.String("digest", rDesc.Digest.String()),
|
|
slog.String("src", referrerSrc.CommonName()),
|
|
slog.String("tgt", referrerTgt.CommonName()))
|
|
}
|
|
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.slog.Warn("Failed to list tags for digest-tag copy",
|
|
slog.String("source", refSrc.Reference),
|
|
slog.String("err", err.Error()))
|
|
return err
|
|
}
|
|
tags, err := tl.GetTags()
|
|
if err != nil {
|
|
opt.mu.Unlock()
|
|
rc.slog.Warn("Failed to list tags for digest-tag copy",
|
|
slog.String("source", refSrc.Reference),
|
|
slog.String("err", err.Error()))
|
|
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
|
|
if slices.Contains(referrerTags, tag) {
|
|
continue
|
|
}
|
|
refTagSrc := refSrc.SetTag(tag)
|
|
refTagTgt := refTgt.SetTag(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.slog.Warn("Failed to copy digest-tag",
|
|
slog.String("tag", tag),
|
|
slog.String("src", refTagSrc.CommonName()),
|
|
slog.String("tgt", refTagTgt.CommonName()))
|
|
}
|
|
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.slog.Warn("Failed to push manifest",
|
|
slog.String("target", refTgt.Reference),
|
|
slog.String("err", err.Error()))
|
|
}
|
|
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, refTgt.SetTag("").CommonName(), "", 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, repo, tag string, dig digest.Digest, parents []digest.Digest) (func(error), error) {
|
|
var seenNew *imageSeen
|
|
key := repo + "/" + 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 == repo+"/"+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
|
|
}
|
|
|
|
// dedup warnings
|
|
if w := warning.FromContext(ctx); w == nil {
|
|
ctx = warning.NewContext(ctx, &warning.Warning{Hook: warning.DefaultHook()})
|
|
}
|
|
// 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.slog.Warn("Failed to get manifest",
|
|
slog.String("ref", r.CommonName()),
|
|
slog.String("err", err.Error()))
|
|
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
|
|
}
|
|
if err = conf.Digest.Validate(); err != nil {
|
|
return err
|
|
}
|
|
refTag := opt.exportRef.ToReg()
|
|
refTag = refTag.SetTag(cmp.Or(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 {
|
|
if err = d.Digest.Validate(); err != nil {
|
|
return err
|
|
}
|
|
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 {
|
|
if err := desc.Digest.Validate(); err != nil {
|
|
return err
|
|
}
|
|
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)
|
|
}
|
|
|
|
// dedup warnings
|
|
if w := warning.FromContext(ctx); w == nil {
|
|
ctx = warning.NewContext(ctx, &warning.Warning{Hook: warning.DefaultHook()})
|
|
}
|
|
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...)
|
|
if slices.Contains(entry.RepoTags, trd.name) {
|
|
index = i
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
rc.slog.Warn("Could not find requested name",
|
|
slog.Any("tags", tags),
|
|
slog.String("name", trd.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.ToSlash(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.ToSlash(filepath.Clean(layerFile))] = func(header *tar.Header, trd *tarReadData) error {
|
|
// ensure blob is compressed
|
|
rdrUC, err := archive.Decompress(trd.tr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
gzipR, err := archive.Compress(rdrUC, archive.CompressGzip)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer gzipR.Close()
|
|
// 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.slog.Warn("Unsupported oci-layout version",
|
|
slog.String("version", ociLayout.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) error {
|
|
if err := d.Digest.Validate(); err != nil {
|
|
return err
|
|
}
|
|
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.Docker2Layer, mediatype.Docker2LayerGzip, mediatype.Docker2LayerZstd,
|
|
mediatype.OCI1Layer, mediatype.OCI1LayerGzip, mediatype.OCI1LayerZstd,
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
err = handleManifest(d, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// 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 {
|
|
err = handleManifest(d, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} 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 {
|
|
if err = cd.Digest.Validate(); err != nil {
|
|
return err
|
|
}
|
|
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 {
|
|
if err = d.Digest.Validate(); err != nil {
|
|
return err
|
|
}
|
|
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.SetDigest(m.GetDescriptor().Digest.String())
|
|
_, 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(_ context.Context, _ 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 == "" {
|
|
if slices.Contains(list, "") {
|
|
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.ToSlash(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.ToSlash(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 {
|
|
if slices.Contains(trd.links[tgt], 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 any) 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.ToSlash(filepath.Dir(filename))
|
|
if !td.dirs[dirName] && dirName != "." {
|
|
dirSplit := strings.Split(dirName, "/")
|
|
for i := range dirSplit {
|
|
dirJoin := strings.Join(dirSplit[:i+1], "/")
|
|
if !td.dirs[dirJoin] && dirJoin != "" {
|
|
header := tar.Header{
|
|
Format: tar.FormatPAX,
|
|
Typeflag: tar.TypeDir,
|
|
Name: dirJoin + "/",
|
|
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[dirJoin] = 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 any) 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 fmt.Sprintf("blobs/%s/%s", d.Digest.Algorithm(), d.Digest.Encoded())
|
|
}
|