mirror of
https://github.com/regclient/regclient.git
synced 2025-04-18 22:44:00 +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>
513 lines
14 KiB
Go
513 lines
14 KiB
Go
package mod
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"cmp"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"slices"
|
|
|
|
"github.com/opencontainers/go-digest"
|
|
|
|
"github.com/regclient/regclient"
|
|
"github.com/regclient/regclient/types/blob"
|
|
"github.com/regclient/regclient/types/descriptor"
|
|
"github.com/regclient/regclient/types/errs"
|
|
"github.com/regclient/regclient/types/manifest"
|
|
v1 "github.com/regclient/regclient/types/oci/v1"
|
|
"github.com/regclient/regclient/types/ref"
|
|
)
|
|
|
|
type changes int
|
|
|
|
const (
|
|
unchanged changes = iota
|
|
added
|
|
replaced
|
|
deleted
|
|
)
|
|
|
|
type dagConfig struct {
|
|
stepsManifest []func(context.Context, *regclient.RegClient, ref.Ref, ref.Ref, *dagManifest) error
|
|
stepsOCIConfig []func(context.Context, *regclient.RegClient, ref.Ref, ref.Ref, *dagOCIConfig) error
|
|
stepsLayer []func(context.Context, *regclient.RegClient, ref.Ref, ref.Ref, *dagLayer, io.ReadCloser) (io.ReadCloser, error)
|
|
stepsLayerFile []func(context.Context, *regclient.RegClient, ref.Ref, ref.Ref, *dagLayer, *tar.Header, io.Reader) (*tar.Header, io.Reader, changes, error)
|
|
maxDataSize int64
|
|
rTgt ref.Ref
|
|
forceLayerWalk bool
|
|
}
|
|
|
|
type dagManifest struct {
|
|
mod changes
|
|
top bool // indicates the top level manifest (needed for manifest lists)
|
|
origDesc descriptor.Descriptor
|
|
newDesc descriptor.Descriptor
|
|
m manifest.Manifest
|
|
config *dagOCIConfig
|
|
layers []*dagLayer
|
|
manifests []*dagManifest
|
|
referrers []*dagManifest
|
|
}
|
|
|
|
type dagOCIConfig struct {
|
|
modified bool
|
|
newDesc descriptor.Descriptor
|
|
oc blob.OCIConfig
|
|
}
|
|
|
|
type dagLayer struct {
|
|
mod changes
|
|
newDesc descriptor.Descriptor
|
|
ucDigest digest.Digest // uncompressed descriptor
|
|
desc descriptor.Descriptor
|
|
rSrc ref.Ref
|
|
}
|
|
|
|
func dagGet(ctx context.Context, rc *regclient.RegClient, rSrc ref.Ref, d descriptor.Descriptor) (*dagManifest, error) {
|
|
var err error
|
|
getOpts := []regclient.ManifestOpts{}
|
|
if d.Digest != "" {
|
|
getOpts = append(getOpts, regclient.WithManifestDesc(d))
|
|
}
|
|
dm := dagManifest{}
|
|
dm.m, err = rc.ManifestGet(ctx, rSrc, getOpts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dm.origDesc = dm.m.GetDescriptor()
|
|
if mi, ok := dm.m.(manifest.Indexer); ok {
|
|
dl, err := mi.GetManifestList()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, desc := range dl {
|
|
rGet := rSrc.SetDigest(desc.Digest.String())
|
|
curMM, err := dagGet(ctx, rc, rGet, desc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dm.manifests = append(dm.manifests, curMM)
|
|
}
|
|
}
|
|
if mi, ok := dm.m.(manifest.Imager); ok {
|
|
// pull config
|
|
doc := dagOCIConfig{}
|
|
cd, err := mi.GetConfig()
|
|
if err != nil && !errors.Is(err, errs.ErrUnsupportedMediaType) {
|
|
return nil, err
|
|
} else if err == nil && slices.Contains(mtKnownConfig, cd.MediaType) {
|
|
oc, err := rc.BlobGetOCIConfig(ctx, rSrc, cd)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
doc.oc = oc
|
|
dm.config = &doc
|
|
}
|
|
// init layers
|
|
layers, err := mi.GetLayers()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, layer := range layers {
|
|
dl := dagLayer{
|
|
desc: layer,
|
|
}
|
|
dm.layers = append(dm.layers, &dl)
|
|
}
|
|
}
|
|
// get a list of referrers
|
|
rl, err := rc.ReferrerList(ctx, rSrc)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get referrers: %w", err)
|
|
}
|
|
for _, desc := range rl.Descriptors {
|
|
// strip referrers metadata from descriptor (annotations and artifact type)
|
|
desc.ArtifactType = ""
|
|
if len(desc.Annotations) > 0 {
|
|
desc.Annotations = nil
|
|
}
|
|
rGet := rSrc.SetDigest(desc.Digest.String())
|
|
curMM, err := dagGet(ctx, rc, rGet, desc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dm.referrers = append(dm.referrers, curMM)
|
|
}
|
|
return &dm, nil
|
|
}
|
|
|
|
func dagPut(ctx context.Context, rc *regclient.RegClient, mc dagConfig, rSrc, rTgt ref.Ref, dm *dagManifest) error {
|
|
var err error
|
|
// recursively push children to get new digests to include in the modified manifest
|
|
om := dm.m.GetOrig()
|
|
changed := false
|
|
if dm.m.IsList() {
|
|
ociI, err := manifest.OCIIndexFromAny(om)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// two passes through manifests, first to add/update entries
|
|
for i, child := range dm.manifests {
|
|
if i >= len(ociI.Manifests) && child.mod != added {
|
|
return fmt.Errorf("manifest does not have enough child manifests")
|
|
}
|
|
if child.mod == deleted {
|
|
continue
|
|
}
|
|
// recursively make changes
|
|
err = dagPut(ctx, rc, mc, rSrc, rTgt, child)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// handle data field
|
|
d := child.m.GetDescriptor()
|
|
if child.mod != unchanged && child.newDesc.Digest != "" {
|
|
d = child.newDesc
|
|
}
|
|
if d.Size <= mc.maxDataSize || (mc.maxDataSize < 0 && len(d.Data) > 0) {
|
|
// if data field should be set
|
|
// retrieve the body
|
|
mBytes, err := dm.m.RawBody()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// set data field
|
|
d.Data = mBytes
|
|
} else if mc.maxDataSize >= 0 && d.Size > mc.maxDataSize && len(d.Data) > 0 {
|
|
// strip data fields if above max size
|
|
d.Data = []byte{}
|
|
}
|
|
// update the descriptor list
|
|
if child.mod == added {
|
|
// TODO: need to set the platform and any annotations
|
|
if len(ociI.Manifests) == i {
|
|
ociI.Manifests = append(ociI.Manifests, d)
|
|
} else {
|
|
ociI.Manifests = append(ociI.Manifests[:i+1], ociI.Manifests[i:]...)
|
|
ociI.Manifests[i] = d
|
|
}
|
|
changed = true
|
|
} else if child.mod == replaced || !bytes.Equal(ociI.Manifests[i].Data, d.Data) {
|
|
ociI.Manifests[i].Digest = d.Digest
|
|
ociI.Manifests[i].Size = d.Size
|
|
ociI.Manifests[i].MediaType = d.MediaType
|
|
ociI.Manifests[i].Data = d.Data
|
|
changed = true
|
|
}
|
|
}
|
|
// second pass in reverse to delete entries
|
|
for i := len(dm.manifests) - 1; i >= 0; i-- {
|
|
child := dm.manifests[i]
|
|
if child.mod != deleted {
|
|
continue
|
|
}
|
|
ociI.Manifests = slices.Delete(ociI.Manifests, i, i+1)
|
|
changed = true
|
|
}
|
|
err = manifest.OCIIndexToAny(ociI, &om)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else { // !mm.m.IsList()
|
|
ociM, err := manifest.OCIManifestFromAny(om)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
oc := v1.Image{}
|
|
iConfig := -1
|
|
if dm.config != nil {
|
|
oc = dm.config.oc.GetConfig()
|
|
if oc.History != nil {
|
|
iConfig = 0
|
|
}
|
|
}
|
|
|
|
// first pass to add/modify layers
|
|
for i, layer := range dm.layers {
|
|
if i >= len(ociM.Layers) && layer.mod != added {
|
|
return fmt.Errorf("manifest does not have enough layers")
|
|
}
|
|
// keep config index aligned
|
|
for iConfig >= 0 && iConfig < len(oc.History) && oc.History[iConfig].EmptyLayer {
|
|
iConfig++
|
|
if iConfig >= len(oc.History) && layer.mod != added {
|
|
return fmt.Errorf("config history does not have enough entries")
|
|
}
|
|
}
|
|
if layer.mod == deleted {
|
|
if iConfig >= 0 {
|
|
iConfig++
|
|
}
|
|
continue
|
|
}
|
|
// handle data field
|
|
d := layer.desc
|
|
if layer.mod != unchanged && layer.newDesc.Digest != "" {
|
|
d = layer.newDesc
|
|
}
|
|
if d.Size <= mc.maxDataSize || (mc.maxDataSize < 0 && len(d.Data) > 0) {
|
|
// if data field should be set
|
|
// retrieve the body
|
|
br, err := rc.BlobGet(ctx, rTgt, d)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bBytes, err := io.ReadAll(br)
|
|
_ = br.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// set data field
|
|
d.Data = bBytes
|
|
} else if mc.maxDataSize >= 0 && d.Size > mc.maxDataSize && len(d.Data) > 0 {
|
|
// strip data fields if above max size
|
|
d.Data = []byte{}
|
|
}
|
|
if layer.mod == added {
|
|
if len(ociM.Layers) == i {
|
|
ociM.Layers = append(ociM.Layers, d)
|
|
if oc.RootFS.DiffIDs != nil && len(oc.RootFS.DiffIDs) == i {
|
|
oc.RootFS.DiffIDs = append(oc.RootFS.DiffIDs, layer.ucDigest)
|
|
}
|
|
} else {
|
|
ociM.Layers = append(ociM.Layers[:i+1], ociM.Layers[i:]...)
|
|
ociM.Layers[i] = d
|
|
if oc.RootFS.DiffIDs != nil && len(oc.RootFS.DiffIDs) >= i {
|
|
oc.RootFS.DiffIDs = append(oc.RootFS.DiffIDs[:i+1], oc.RootFS.DiffIDs[i:]...)
|
|
oc.RootFS.DiffIDs[i] = layer.ucDigest
|
|
}
|
|
}
|
|
newHistory := v1.History{
|
|
Created: &timeStart,
|
|
Comment: "regclient",
|
|
}
|
|
if iConfig < 0 {
|
|
// noop
|
|
} else if iConfig >= len(oc.History) {
|
|
oc.History = append(oc.History, newHistory)
|
|
} else {
|
|
oc.History = append(oc.History[:iConfig+1], oc.History[iConfig:]...)
|
|
oc.History[iConfig] = newHistory
|
|
}
|
|
changed = true
|
|
} else if layer.mod == replaced || !bytes.Equal(ociM.Layers[i].Data, d.Data) {
|
|
ociM.Layers[i] = d
|
|
if oc.RootFS.DiffIDs != nil && len(oc.RootFS.DiffIDs) >= i+1 && layer.ucDigest != "" {
|
|
oc.RootFS.DiffIDs[i] = layer.ucDigest
|
|
}
|
|
changed = true
|
|
}
|
|
if iConfig >= 0 {
|
|
iConfig++
|
|
}
|
|
}
|
|
// second pass in reverse to delete entries
|
|
iConfig = len(oc.History) - 1
|
|
for i := len(dm.layers) - 1; i >= 0; i-- {
|
|
layer := dm.layers[i]
|
|
for iConfig >= 0 && oc.History[iConfig].EmptyLayer {
|
|
iConfig--
|
|
}
|
|
if layer.mod != deleted {
|
|
if iConfig >= 0 {
|
|
iConfig--
|
|
}
|
|
continue
|
|
}
|
|
ociM.Layers = slices.Delete(ociM.Layers, i, i+1)
|
|
if oc.RootFS.DiffIDs != nil && len(oc.RootFS.DiffIDs) >= i+1 {
|
|
oc.RootFS.DiffIDs = slices.Delete(oc.RootFS.DiffIDs, i, i+1)
|
|
}
|
|
if iConfig >= 0 {
|
|
oc.History = slices.Delete(oc.History, iConfig, iConfig+1)
|
|
iConfig--
|
|
}
|
|
changed = true
|
|
}
|
|
if changed && dm.config != nil {
|
|
dm.config.oc.SetConfig(oc)
|
|
dm.config.modified = true
|
|
}
|
|
var cBytes []byte
|
|
if dm.config != nil {
|
|
dm.config.newDesc = dm.config.oc.GetDescriptor()
|
|
cBytes, err = dm.config.oc.RawBody()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if dm.config.modified {
|
|
cRdr := bytes.NewReader(cBytes)
|
|
_, err = rc.BlobPut(ctx, rTgt, dm.config.newDesc, cRdr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ociM.Config.MediaType = dm.config.newDesc.MediaType
|
|
ociM.Config.Digest = dm.config.newDesc.Digest
|
|
ociM.Config.Size = dm.config.newDesc.Size
|
|
changed = true
|
|
} else if !ref.EqualRepository(rSrc, rTgt) {
|
|
err = rc.BlobCopy(ctx, rSrc, rTgt, dm.config.oc.GetDescriptor())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if dm.config == nil && ociM.Config.Digest != "" && !ref.EqualRepository(rSrc, rTgt) {
|
|
err = rc.BlobCopy(ctx, rSrc, rTgt, ociM.Config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// handle config data field
|
|
if ociM.Config.Size <= mc.maxDataSize || (mc.maxDataSize < 0 && len(ociM.Config.Data) > 0) {
|
|
// if config was not loaded into memory (e.g. artifact), load it now
|
|
if cBytes == nil {
|
|
cRdr, err := rc.BlobGet(ctx, rTgt, ociM.Config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cBytes, err = io.ReadAll(cRdr)
|
|
_ = cRdr.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !bytes.Equal(ociM.Config.Data, cBytes) {
|
|
ociM.Config.Data = cBytes
|
|
changed = true
|
|
}
|
|
} else if mc.maxDataSize >= 0 && ociM.Config.Size > mc.maxDataSize && len(ociM.Config.Data) > 0 {
|
|
// strip data fields if above max size
|
|
ociM.Config.Data = []byte{}
|
|
changed = true
|
|
}
|
|
|
|
if changed {
|
|
err = manifest.OCIManifestToAny(ociM, &om)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if changed {
|
|
dm.mod = replaced
|
|
err = dm.m.SetOrig(om)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// update descriptor and update subject descriptor on all referrers
|
|
if dm.mod == replaced || dm.mod == added {
|
|
dm.newDesc = dm.m.GetDescriptor()
|
|
}
|
|
if ref.EqualRepository(rSrc, rTgt) {
|
|
// only update referrers when modifying a manifest in the same repository
|
|
for i := range dm.referrers {
|
|
if dm.referrers[i].mod == deleted || !(dm.mod == replaced || dm.mod == added || dm.referrers[i].mod == added) {
|
|
continue
|
|
}
|
|
sm, ok := dm.referrers[i].m.(manifest.Subjecter)
|
|
if !ok {
|
|
return fmt.Errorf("referrer does not support subject field, mt=%s", dm.referrers[i].m.GetDescriptor().MediaType)
|
|
}
|
|
d := dm.m.GetDescriptor()
|
|
err = sm.SetSubject(&d)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set subject: %w", err)
|
|
}
|
|
if dm.referrers[i].mod == unchanged {
|
|
dm.referrers[i].mod = replaced
|
|
}
|
|
dm.referrers[i].newDesc = dm.referrers[i].m.GetDescriptor()
|
|
}
|
|
// recursively push referrers
|
|
for _, child := range dm.referrers {
|
|
err = dagPut(ctx, rc, mc, rSrc, rTgt, child)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
// push manifest
|
|
if dm.mod == replaced || dm.mod == added || (dm.mod == unchanged && !ref.EqualRepository(rSrc, rTgt)) {
|
|
mpOpts := []regclient.ManifestOpts{}
|
|
rPut := rTgt
|
|
if !dm.top {
|
|
mpOpts = append(mpOpts, regclient.WithManifestChild())
|
|
rPut = rPut.SetDigest(cmp.Or(dm.newDesc.Digest.String(), dm.origDesc.Digest.String()))
|
|
} else if rPut.Tag == "" || rPut.Digest != "" {
|
|
// update digest, prefer newDesc if set
|
|
rPut = rPut.AddDigest(cmp.Or(dm.newDesc.Digest.String(), dm.origDesc.Digest.String()))
|
|
}
|
|
err = rc.ManifestPut(ctx, rPut, dm.m, mpOpts...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func dagWalkManifests(dm *dagManifest, fn func(*dagManifest) (*dagManifest, error)) error {
|
|
if dm.manifests != nil {
|
|
for _, child := range dm.manifests {
|
|
err := dagWalkManifests(child, fn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
mmNew, err := fn(dm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*dm = *mmNew
|
|
return nil
|
|
}
|
|
|
|
func dagWalkOCIConfig(dm *dagManifest, fn func(*dagOCIConfig) (*dagOCIConfig, error)) error {
|
|
if dm.manifests != nil {
|
|
for _, child := range dm.manifests {
|
|
err := dagWalkOCIConfig(child, fn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if dm.config != nil {
|
|
docNew, err := fn(dm.config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dm.config = docNew
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func dagWalkLayers(dm *dagManifest, fn func(*dagLayer) (*dagLayer, error)) error {
|
|
var err error
|
|
if dm.manifests != nil {
|
|
for _, child := range dm.manifests {
|
|
err = dagWalkLayers(child, fn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if dm.layers != nil {
|
|
for i, layer := range dm.layers {
|
|
if layer.mod == deleted {
|
|
continue
|
|
}
|
|
mlNew, err := fn(layer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dm.layers[i] = mlNew
|
|
}
|
|
}
|
|
return nil
|
|
}
|