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 eea06e2a5c
Refactoring the type package
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>
2024-03-04 15:43:18 -05:00

504 lines
13 KiB
Go

package mod
import (
"archive/tar"
"bytes"
"context"
"errors"
"fmt"
"io"
"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
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 && inListStr(cd.MediaType, mtWLConfig) {
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 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 = append(ociI.Manifests[:i], ociI.Manifests[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 && oc.History[iConfig].EmptyLayer {
iConfig++
if iConfig >= len(oc.History) {
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)
if err != nil {
return err
}
// set data field
d.Data = bBytes
} else if 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 len(oc.History) == iConfig {
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 = append(ociM.Layers[:i], ociM.Layers[i+1:]...)
if oc.RootFS.DiffIDs != nil && len(oc.RootFS.DiffIDs) >= i+1 {
oc.RootFS.DiffIDs = append(oc.RootFS.DiffIDs[:i], oc.RootFS.DiffIDs[i+1:]...)
}
if iConfig >= 0 {
oc.History = append(oc.History[:iConfig], oc.History[iConfig+1:]...)
iConfig--
}
changed = true
}
if changed && dm.config != nil {
dm.config.oc.SetConfig(oc)
dm.config.modified = true
}
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
}
}
// handle config data field
if ociM.Config.Size <= mc.maxDataSize || (mc.maxDataSize < 0 && len(ociM.Config.Data) > 0) {
if !bytes.Equal(ociM.Config.Data, cBytes) {
ociM.Config.Data = cBytes
changed = true
}
} else if ociM.Config.Size > mc.maxDataSize && len(ociM.Config.Data) > 0 {
// strip data fields if above max size
ociM.Config.Data = []byte{}
changed = true
}
}
if dm.config == nil && ociM.Config.Digest != "" && !ref.EqualRepository(rSrc, rTgt) {
err = rc.BlobCopy(ctx, rSrc, rTgt, ociM.Config)
if err != nil {
return err
}
}
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.Tag = ""
}
if rPut.Tag == "" {
// push by digest
if dm.newDesc.Digest != "" {
rPut.Digest = dm.newDesc.Digest.String()
} else {
rPut.Digest = dm.origDesc.Digest.String()
}
} else {
// push by tag
rPut.Digest = ""
}
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
}