1
0
mirror of https://github.com/regclient/regclient.git synced 2025-04-18 22:44:00 +03:00
regclient/mod/mod.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

390 lines
11 KiB
Go

// Package mod changes an image according to the requested modifications.
package mod
import (
"archive/tar"
"compress/gzip"
"context"
"fmt"
"io"
"os"
"slices"
"time"
"github.com/klauspost/compress/zstd"
"github.com/opencontainers/go-digest"
"github.com/regclient/regclient"
"github.com/regclient/regclient/pkg/archive"
"github.com/regclient/regclient/types/descriptor"
"github.com/regclient/regclient/types/mediatype"
"github.com/regclient/regclient/types/ref"
"github.com/regclient/regclient/types/warning"
)
// Opts defines options for Apply
type Opts func(*dagConfig, *dagManifest) error
// OptTime defines time settings for [WithConfigTimestamp] and [WithLayerTimestamp].
type OptTime struct {
Set time.Time // time to set, this or FromLabel are required
FromLabel string // label from which to extract set time
After time.Time // only change times that are after this
BaseRef ref.Ref // define base image, do not alter timestamps from base layers
BaseLayers int // define a number of layers to not modify (count of the layers in a base image)
}
var (
// known tar media types
mtKnownTar = []string{
mediatype.Docker2Layer,
mediatype.Docker2LayerGzip,
mediatype.Docker2LayerZstd,
mediatype.OCI1Layer,
mediatype.OCI1LayerGzip,
mediatype.OCI1LayerZstd,
}
// known config media types
mtKnownConfig = []string{
mediatype.Docker2ImageConfig,
mediatype.OCI1ImageConfig,
}
)
// Apply applies a set of modifications to an image (manifest, configs, and layers).
func Apply(ctx context.Context, rc *regclient.RegClient, rSrc ref.Ref, opts ...Opts) (ref.Ref, error) {
// dedup warnings
if w := warning.FromContext(ctx); w == nil {
ctx = warning.NewContext(ctx, &warning.Warning{Hook: warning.DefaultHook()})
}
// pull the image metadata into a DAG
dm, err := dagGet(ctx, rc, rSrc, descriptor.Descriptor{})
if err != nil {
return rSrc, err
}
dm.top = true
// load the options
rTgt := rSrc.SetTag("")
dc := dagConfig{
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: -1, // unchanged, if a data field exists, preserve it
rTgt: rTgt,
}
for _, opt := range opts {
if err := opt(&dc, dm); err != nil {
return rSrc, err
}
}
rTgt = dc.rTgt
// perform manifest changes
if len(dc.stepsManifest) > 0 {
err = dagWalkManifests(dm, func(dm *dagManifest) (*dagManifest, error) {
for _, fn := range dc.stepsManifest {
err := fn(ctx, rc, rSrc, rTgt, dm)
if err != nil {
return nil, err
}
}
return dm, nil
})
if err != nil {
return rTgt, err
}
}
// perform config changes
if len(dc.stepsOCIConfig) > 0 {
err = dagWalkOCIConfig(dm, func(doc *dagOCIConfig) (*dagOCIConfig, error) {
for _, fn := range dc.stepsOCIConfig {
err := fn(ctx, rc, rSrc, rTgt, doc)
if err != nil {
return nil, err
}
}
return doc, nil
})
if err != nil {
return rTgt, err
}
}
// perform layer changes and copy layers to target repository
if len(dc.stepsLayer) > 0 || len(dc.stepsLayerFile) > 0 || !ref.EqualRepository(rSrc, rTgt) || dc.forceLayerWalk {
err = dagWalkLayers(dm, func(dl *dagLayer) (*dagLayer, error) {
var rdr io.ReadCloser
defer func() {
if rdr != nil {
_ = rdr.Close()
}
}()
var err error
rSrc := rSrc
if dl.rSrc.IsSet() {
rSrc = dl.rSrc
}
if dl.mod == deleted || len(dl.desc.URLs) > 0 {
// skip deleted and external layers
return dl, nil
}
// changes for the entire layer
if len(dc.stepsLayer) > 0 {
bRdr, err := rc.BlobGet(ctx, rSrc, dl.desc)
if err != nil {
return nil, err
}
rdr = bRdr
for _, sl := range dc.stepsLayer {
rdrNext, err := sl(ctx, rc, rSrc, rTgt, dl, rdr)
if err != nil {
return nil, err
}
rdr = rdrNext
}
}
// changes for files within layers require extracting the tar and then repackaging it
if len(dc.stepsLayerFile) > 0 && slices.Contains(mtKnownTar, dl.desc.MediaType) {
if dl.mod == deleted {
return dl, nil
}
if rdr == nil {
bRdr, err := rc.BlobGet(ctx, rSrc, dl.desc)
if err != nil {
return nil, err
}
rdr = bRdr
}
changed := false
empty := true
desc := dl.desc
if dl.newDesc.MediaType != "" {
desc = dl.newDesc
}
// if compressed, setup a decompressing reader that passes through the close
if desc.MediaType != mediatype.OCI1Layer && desc.MediaType != mediatype.Docker2Layer {
dr, err := archive.Decompress(rdr)
if err != nil {
_ = rdr.Close()
return nil, err
}
rdr = readCloserFn{Reader: dr, closeFn: rdr.Close}
}
// setup tar reader to process layer
tr := tar.NewReader(rdr)
// create temp file and setup tar writer
fh, err := os.CreateTemp("", "regclient-mod-")
if err != nil {
_ = rdr.Close()
return nil, err
}
defer func() {
_ = fh.Close()
_ = os.Remove(fh.Name())
}()
var tw *tar.Writer
var gw *gzip.Writer
var zw *zstd.Encoder
digRaw := desc.DigestAlgo().Digester() // raw/compressed digest
digUC := desc.DigestAlgo().Digester() // uncompressed digest
if dl.desc.MediaType == mediatype.Docker2LayerGzip || dl.desc.MediaType == mediatype.OCI1LayerGzip {
cw := io.MultiWriter(fh, digRaw.Hash())
gw = gzip.NewWriter(cw)
defer gw.Close()
ucw := io.MultiWriter(gw, digUC.Hash())
tw = tar.NewWriter(ucw)
} else if dl.desc.MediaType == mediatype.Docker2LayerZstd || dl.desc.MediaType == mediatype.OCI1LayerZstd {
cw := io.MultiWriter(fh, digRaw.Hash())
zw, err = zstd.NewWriter(cw)
if err != nil {
_ = rdr.Close()
return nil, err
}
defer zw.Close()
ucw := io.MultiWriter(zw, digUC.Hash())
tw = tar.NewWriter(ucw)
} else {
dw := io.MultiWriter(fh, digRaw.Hash(), digUC.Hash())
tw = tar.NewWriter(dw)
}
// iterate over files in the layer
for {
th, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
changeFile := unchanged
var fileRdr io.Reader
fileRdr = tr
for _, slf := range dc.stepsLayerFile {
var changeCur changes
th, fileRdr, changeCur, err = slf(ctx, rc, rSrc, rTgt, dl, th, fileRdr)
if err != nil {
_ = rdr.Close()
return nil, err
}
if changeCur != unchanged {
changed = true
}
if changeCur == deleted {
changeFile = deleted
break
}
}
// copy th and tr to temp tar writer file
if changeFile != deleted {
empty = false
err = tw.WriteHeader(th)
if err != nil {
_ = rdr.Close()
return nil, err
}
if th.Typeflag == tar.TypeReg && th.Size > 0 {
_, err := io.CopyN(tw, fileRdr, th.Size)
if err != nil {
_ = rdr.Close()
return nil, err
}
}
}
}
if empty {
dl.mod = deleted
return dl, nil
}
if changed {
// close to flush remaining content
err = tw.Close()
if err != nil {
_ = rdr.Close()
return nil, fmt.Errorf("failed to close temporary tar layer: %w", err)
}
if gw != nil {
err = gw.Close()
if err != nil {
_ = rdr.Close()
return nil, fmt.Errorf("failed to close gzip writer: %w", err)
}
}
if zw != nil {
err = zw.Close()
if err != nil {
_ = rdr.Close()
return nil, fmt.Errorf("failed to close zstd writer: %w", err)
}
}
err = rdr.Close()
if err != nil {
return nil, fmt.Errorf("failed to close layer reader: %w", err)
}
// replace the current reader and save the digests
l, err := fh.Seek(0, 1)
if err != nil {
return nil, err
}
_, err = fh.Seek(0, 0)
if err != nil {
return nil, err
}
rdr = fh
desc.Digest = digRaw.Digest()
desc.Size = l
dl.newDesc = desc
dl.ucDigest = digUC.Digest()
if dl.mod == unchanged {
dl.mod = replaced
}
}
}
// if added or replaced, and reader not nil, push blob
if (dl.mod == added || dl.mod == replaced) && rdr != nil {
// push the blob and verify the results
dNew, err := rc.BlobPut(ctx, rTgt, dl.newDesc, rdr)
if err != nil {
return nil, err
}
err = rdr.Close()
if err != nil {
return nil, err
}
if dl.newDesc.Digest == "" {
dl.newDesc.Digest = dNew.Digest
} else if dl.newDesc.Digest != dNew.Digest {
return nil, fmt.Errorf("layer digest mismatch, pushed %s, expected %s", dNew.Digest.String(), dl.newDesc.Digest.String())
}
if dl.newDesc.Size == 0 {
dl.newDesc.Size = dNew.Size
} else if dl.newDesc.Size != dNew.Size {
return nil, fmt.Errorf("layer size mismatch, pushed %d, expected %d", dNew.Size, dl.newDesc.Size)
}
}
// for unchanged layers, if the repository is different, copy the blob
if dl.mod == unchanged && !ref.EqualRepository(rSrc, rTgt) {
err = rc.BlobCopy(ctx, rSrc, rTgt, dl.desc)
if err != nil {
return nil, err
}
}
return dl, nil
})
if err != nil {
return rTgt, err
}
}
// push the resulting changed content, both manifests and configs
err = dagPut(ctx, rc, dc, rSrc, rTgt, dm)
if err != nil {
return rTgt, err
}
if rTgt.Tag == "" || rTgt.Digest != "" {
rTgt = rTgt.AddDigest(dm.m.GetDescriptor().Digest.String())
}
return rTgt, nil
}
// WithRefTgt sets the target manifest.
// Apply will default to pushing to the same name by digest.
func WithRefTgt(rTgt ref.Ref) Opts {
return func(dc *dagConfig, dm *dagManifest) error {
dc.rTgt = rTgt
return nil
}
}
// WithData sets the descriptor data field max size.
// This also strips the data field off descriptors above the max size.
func WithData(maxDataSize int64) Opts {
return func(dc *dagConfig, dm *dagManifest) error {
dc.maxDataSize = maxDataSize
return nil
}
}
// WithDigestAlgo sets the digest algorithm for both manifests and layers.
func WithDigestAlgo(algo digest.Algorithm) Opts {
layerOpt := WithLayerDigestAlgo(algo)
configOpt := WithConfigDigestAlgo(algo)
manOpt := WithManifestDigestAlgo(algo)
return func(dc *dagConfig, dm *dagManifest) error {
err := layerOpt(dc, dm)
if err != nil {
return err
}
err = configOpt(dc, dm)
if err != nil {
return err
}
err = manOpt(dc, dm)
if err != nil {
return err
}
return nil
}
}