1
0
mirror of https://github.com/regclient/regclient.git synced 2025-04-18 22:44:00 +03:00
regclient/mod/dag.go
Brandon Mitchell 95c9152941
Chore: Modernize Go to the 1.22 specs
- 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>
2025-02-18 14:32:06 -05:00

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
}