...

Source file src/github.com/google/go-containerregistry/pkg/v1/mutate/mutate.go

Documentation: github.com/google/go-containerregistry/pkg/v1/mutate

     1  // Copyright 2018 Google LLC All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //    http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package mutate
    16  
    17  import (
    18  	"archive/tar"
    19  	"bytes"
    20  	"encoding/json"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"path/filepath"
    25  	"strings"
    26  	"time"
    27  
    28  	"github.com/google/go-containerregistry/internal/gzip"
    29  	v1 "github.com/google/go-containerregistry/pkg/v1"
    30  	"github.com/google/go-containerregistry/pkg/v1/empty"
    31  	"github.com/google/go-containerregistry/pkg/v1/match"
    32  	"github.com/google/go-containerregistry/pkg/v1/partial"
    33  	"github.com/google/go-containerregistry/pkg/v1/tarball"
    34  	"github.com/google/go-containerregistry/pkg/v1/types"
    35  )
    36  
    37  const whiteoutPrefix = ".wh."
    38  
    39  // Addendum contains layers and history to be appended
    40  // to a base image
    41  type Addendum struct {
    42  	Layer       v1.Layer
    43  	History     v1.History
    44  	URLs        []string
    45  	Annotations map[string]string
    46  	MediaType   types.MediaType
    47  }
    48  
    49  // AppendLayers applies layers to a base image.
    50  func AppendLayers(base v1.Image, layers ...v1.Layer) (v1.Image, error) {
    51  	additions := make([]Addendum, 0, len(layers))
    52  	for _, layer := range layers {
    53  		additions = append(additions, Addendum{Layer: layer})
    54  	}
    55  
    56  	return Append(base, additions...)
    57  }
    58  
    59  // Append will apply the list of addendums to the base image
    60  func Append(base v1.Image, adds ...Addendum) (v1.Image, error) {
    61  	if len(adds) == 0 {
    62  		return base, nil
    63  	}
    64  	if err := validate(adds); err != nil {
    65  		return nil, err
    66  	}
    67  
    68  	return &image{
    69  		base: base,
    70  		adds: adds,
    71  	}, nil
    72  }
    73  
    74  // Appendable is an interface that represents something that can be appended
    75  // to an ImageIndex. We need to be able to construct a v1.Descriptor in order
    76  // to append something, and this is the minimum required information for that.
    77  type Appendable interface {
    78  	MediaType() (types.MediaType, error)
    79  	Digest() (v1.Hash, error)
    80  	Size() (int64, error)
    81  }
    82  
    83  // IndexAddendum represents an appendable thing and all the properties that
    84  // we may want to override in the resulting v1.Descriptor.
    85  type IndexAddendum struct {
    86  	Add Appendable
    87  	v1.Descriptor
    88  }
    89  
    90  // AppendManifests appends a manifest to the ImageIndex.
    91  func AppendManifests(base v1.ImageIndex, adds ...IndexAddendum) v1.ImageIndex {
    92  	return &index{
    93  		base: base,
    94  		adds: adds,
    95  	}
    96  }
    97  
    98  // RemoveManifests removes any descriptors that match the match.Matcher.
    99  func RemoveManifests(base v1.ImageIndex, matcher match.Matcher) v1.ImageIndex {
   100  	return &index{
   101  		base:   base,
   102  		remove: matcher,
   103  	}
   104  }
   105  
   106  // Config mutates the provided v1.Image to have the provided v1.Config
   107  func Config(base v1.Image, cfg v1.Config) (v1.Image, error) {
   108  	cf, err := base.ConfigFile()
   109  	if err != nil {
   110  		return nil, err
   111  	}
   112  
   113  	cf.Config = cfg
   114  
   115  	return ConfigFile(base, cf)
   116  }
   117  
   118  // Subject mutates the subject on an image or index manifest.
   119  //
   120  // The input is expected to be a v1.Image or v1.ImageIndex, and
   121  // returns the same type. You can type-assert the result like so:
   122  //
   123  //	img := Subject(empty.Image, subj).(v1.Image)
   124  //
   125  // Or for an index:
   126  //
   127  //	idx := Subject(empty.Index, subj).(v1.ImageIndex)
   128  //
   129  // If the input is not an Image or ImageIndex, the result will
   130  // attempt to lazily annotate the raw manifest.
   131  func Subject(f partial.WithRawManifest, subject v1.Descriptor) partial.WithRawManifest {
   132  	if img, ok := f.(v1.Image); ok {
   133  		return &image{
   134  			base:    img,
   135  			subject: &subject,
   136  		}
   137  	}
   138  	if idx, ok := f.(v1.ImageIndex); ok {
   139  		return &index{
   140  			base:    idx,
   141  			subject: &subject,
   142  		}
   143  	}
   144  	return arbitraryRawManifest{a: f, subject: &subject}
   145  }
   146  
   147  // Annotations mutates the annotations on an annotatable image or index manifest.
   148  //
   149  // The annotatable input is expected to be a v1.Image or v1.ImageIndex, and
   150  // returns the same type. You can type-assert the result like so:
   151  //
   152  //	img := Annotations(empty.Image, map[string]string{
   153  //	    "foo": "bar",
   154  //	}).(v1.Image)
   155  //
   156  // Or for an index:
   157  //
   158  //	idx := Annotations(empty.Index, map[string]string{
   159  //	    "foo": "bar",
   160  //	}).(v1.ImageIndex)
   161  //
   162  // If the input Annotatable is not an Image or ImageIndex, the result will
   163  // attempt to lazily annotate the raw manifest.
   164  func Annotations(f partial.WithRawManifest, anns map[string]string) partial.WithRawManifest {
   165  	if img, ok := f.(v1.Image); ok {
   166  		return &image{
   167  			base:        img,
   168  			annotations: anns,
   169  		}
   170  	}
   171  	if idx, ok := f.(v1.ImageIndex); ok {
   172  		return &index{
   173  			base:        idx,
   174  			annotations: anns,
   175  		}
   176  	}
   177  	return arbitraryRawManifest{a: f, anns: anns}
   178  }
   179  
   180  type arbitraryRawManifest struct {
   181  	a       partial.WithRawManifest
   182  	anns    map[string]string
   183  	subject *v1.Descriptor
   184  }
   185  
   186  func (a arbitraryRawManifest) RawManifest() ([]byte, error) {
   187  	b, err := a.a.RawManifest()
   188  	if err != nil {
   189  		return nil, err
   190  	}
   191  	var m map[string]any
   192  	if err := json.Unmarshal(b, &m); err != nil {
   193  		return nil, err
   194  	}
   195  	if ann, ok := m["annotations"]; ok {
   196  		if annm, ok := ann.(map[string]string); ok {
   197  			for k, v := range a.anns {
   198  				annm[k] = v
   199  			}
   200  		} else {
   201  			return nil, fmt.Errorf(".annotations is not a map: %T", ann)
   202  		}
   203  	} else {
   204  		m["annotations"] = a.anns
   205  	}
   206  	if a.subject != nil {
   207  		m["subject"] = a.subject
   208  	}
   209  	return json.Marshal(m)
   210  }
   211  
   212  // ConfigFile mutates the provided v1.Image to have the provided v1.ConfigFile
   213  func ConfigFile(base v1.Image, cfg *v1.ConfigFile) (v1.Image, error) {
   214  	m, err := base.Manifest()
   215  	if err != nil {
   216  		return nil, err
   217  	}
   218  
   219  	image := &image{
   220  		base:       base,
   221  		manifest:   m.DeepCopy(),
   222  		configFile: cfg,
   223  	}
   224  
   225  	return image, nil
   226  }
   227  
   228  // CreatedAt mutates the provided v1.Image to have the provided v1.Time
   229  func CreatedAt(base v1.Image, created v1.Time) (v1.Image, error) {
   230  	cf, err := base.ConfigFile()
   231  	if err != nil {
   232  		return nil, err
   233  	}
   234  
   235  	cfg := cf.DeepCopy()
   236  	cfg.Created = created
   237  
   238  	return ConfigFile(base, cfg)
   239  }
   240  
   241  // Extract takes an image and returns an io.ReadCloser containing the image's
   242  // flattened filesystem.
   243  //
   244  // Callers can read the filesystem contents by passing the reader to
   245  // tar.NewReader, or io.Copy it directly to some output.
   246  //
   247  // If a caller doesn't read the full contents, they should Close it to free up
   248  // resources used during extraction.
   249  func Extract(img v1.Image) io.ReadCloser {
   250  	pr, pw := io.Pipe()
   251  
   252  	go func() {
   253  		// Close the writer with any errors encountered during
   254  		// extraction. These errors will be returned by the reader end
   255  		// on subsequent reads. If err == nil, the reader will return
   256  		// EOF.
   257  		pw.CloseWithError(extract(img, pw))
   258  	}()
   259  
   260  	return pr
   261  }
   262  
   263  // Adapted from https://github.com/google/containerregistry/blob/da03b395ccdc4e149e34fbb540483efce962dc64/client/v2_2/docker_image_.py#L816
   264  func extract(img v1.Image, w io.Writer) error {
   265  	tarWriter := tar.NewWriter(w)
   266  	defer tarWriter.Close()
   267  
   268  	fileMap := map[string]bool{}
   269  
   270  	layers, err := img.Layers()
   271  	if err != nil {
   272  		return fmt.Errorf("retrieving image layers: %w", err)
   273  	}
   274  
   275  	// we iterate through the layers in reverse order because it makes handling
   276  	// whiteout layers more efficient, since we can just keep track of the removed
   277  	// files as we see .wh. layers and ignore those in previous layers.
   278  	for i := len(layers) - 1; i >= 0; i-- {
   279  		layer := layers[i]
   280  		layerReader, err := layer.Uncompressed()
   281  		if err != nil {
   282  			return fmt.Errorf("reading layer contents: %w", err)
   283  		}
   284  		defer layerReader.Close()
   285  		tarReader := tar.NewReader(layerReader)
   286  		for {
   287  			header, err := tarReader.Next()
   288  			if errors.Is(err, io.EOF) {
   289  				break
   290  			}
   291  			if err != nil {
   292  				return fmt.Errorf("reading tar: %w", err)
   293  			}
   294  
   295  			// Some tools prepend everything with "./", so if we don't Clean the
   296  			// name, we may have duplicate entries, which angers tar-split.
   297  			header.Name = filepath.Clean(header.Name)
   298  			// force PAX format to remove Name/Linkname length limit of 100 characters
   299  			// required by USTAR and to not depend on internal tar package guess which
   300  			// prefers USTAR over PAX
   301  			header.Format = tar.FormatPAX
   302  
   303  			basename := filepath.Base(header.Name)
   304  			dirname := filepath.Dir(header.Name)
   305  			tombstone := strings.HasPrefix(basename, whiteoutPrefix)
   306  			if tombstone {
   307  				basename = basename[len(whiteoutPrefix):]
   308  			}
   309  
   310  			// check if we have seen value before
   311  			// if we're checking a directory, don't filepath.Join names
   312  			var name string
   313  			if header.Typeflag == tar.TypeDir {
   314  				name = header.Name
   315  			} else {
   316  				name = filepath.Join(dirname, basename)
   317  			}
   318  
   319  			if _, ok := fileMap[name]; ok {
   320  				continue
   321  			}
   322  
   323  			// check for a whited out parent directory
   324  			if inWhiteoutDir(fileMap, name) {
   325  				continue
   326  			}
   327  
   328  			// mark file as handled. non-directory implicitly tombstones
   329  			// any entries with a matching (or child) name
   330  			fileMap[name] = tombstone || !(header.Typeflag == tar.TypeDir)
   331  			if !tombstone {
   332  				if err := tarWriter.WriteHeader(header); err != nil {
   333  					return err
   334  				}
   335  				if header.Size > 0 {
   336  					if _, err := io.CopyN(tarWriter, tarReader, header.Size); err != nil {
   337  						return err
   338  					}
   339  				}
   340  			}
   341  		}
   342  	}
   343  	return nil
   344  }
   345  
   346  func inWhiteoutDir(fileMap map[string]bool, file string) bool {
   347  	for {
   348  		if file == "" {
   349  			break
   350  		}
   351  		dirname := filepath.Dir(file)
   352  		if file == dirname {
   353  			break
   354  		}
   355  		if val, ok := fileMap[dirname]; ok && val {
   356  			return true
   357  		}
   358  		file = dirname
   359  	}
   360  	return false
   361  }
   362  
   363  func max(a, b int) int {
   364  	if a > b {
   365  		return a
   366  	}
   367  	return b
   368  }
   369  
   370  // Time sets all timestamps in an image to the given timestamp.
   371  func Time(img v1.Image, t time.Time) (v1.Image, error) {
   372  	newImage := empty.Image
   373  
   374  	layers, err := img.Layers()
   375  	if err != nil {
   376  		return nil, fmt.Errorf("getting image layers: %w", err)
   377  	}
   378  
   379  	ocf, err := img.ConfigFile()
   380  	if err != nil {
   381  		return nil, fmt.Errorf("getting original config file: %w", err)
   382  	}
   383  
   384  	addendums := make([]Addendum, max(len(ocf.History), len(layers)))
   385  	var historyIdx, addendumIdx int
   386  	for layerIdx := 0; layerIdx < len(layers); addendumIdx, layerIdx = addendumIdx+1, layerIdx+1 {
   387  		newLayer, err := layerTime(layers[layerIdx], t)
   388  		if err != nil {
   389  			return nil, fmt.Errorf("setting layer times: %w", err)
   390  		}
   391  
   392  		// try to search for the history entry that corresponds to this layer
   393  		for ; historyIdx < len(ocf.History); historyIdx++ {
   394  			addendums[addendumIdx].History = ocf.History[historyIdx]
   395  			// if it's an EmptyLayer, do not set the Layer and have the Addendum with just the History
   396  			// and move on to the next History entry
   397  			if ocf.History[historyIdx].EmptyLayer {
   398  				addendumIdx++
   399  				continue
   400  			}
   401  			// otherwise, we can exit from the cycle
   402  			historyIdx++
   403  			break
   404  		}
   405  		if addendumIdx < len(addendums) {
   406  			addendums[addendumIdx].Layer = newLayer
   407  		}
   408  	}
   409  
   410  	// add all leftover History entries
   411  	for ; historyIdx < len(ocf.History); historyIdx, addendumIdx = historyIdx+1, addendumIdx+1 {
   412  		addendums[addendumIdx].History = ocf.History[historyIdx]
   413  	}
   414  
   415  	newImage, err = Append(newImage, addendums...)
   416  	if err != nil {
   417  		return nil, fmt.Errorf("appending layers: %w", err)
   418  	}
   419  
   420  	cf, err := newImage.ConfigFile()
   421  	if err != nil {
   422  		return nil, fmt.Errorf("setting config file: %w", err)
   423  	}
   424  
   425  	cfg := cf.DeepCopy()
   426  
   427  	// Copy basic config over
   428  	cfg.Architecture = ocf.Architecture
   429  	cfg.OS = ocf.OS
   430  	cfg.OSVersion = ocf.OSVersion
   431  	cfg.Config = ocf.Config
   432  
   433  	// Strip away timestamps from the config file
   434  	cfg.Created = v1.Time{Time: t}
   435  
   436  	for i, h := range cfg.History {
   437  		h.Created = v1.Time{Time: t}
   438  		h.CreatedBy = ocf.History[i].CreatedBy
   439  		h.Comment = ocf.History[i].Comment
   440  		h.EmptyLayer = ocf.History[i].EmptyLayer
   441  		// Explicitly ignore Author field; which hinders reproducibility
   442  		h.Author = ""
   443  		cfg.History[i] = h
   444  	}
   445  
   446  	return ConfigFile(newImage, cfg)
   447  }
   448  
   449  func layerTime(layer v1.Layer, t time.Time) (v1.Layer, error) {
   450  	layerReader, err := layer.Uncompressed()
   451  	if err != nil {
   452  		return nil, fmt.Errorf("getting layer: %w", err)
   453  	}
   454  	defer layerReader.Close()
   455  	w := new(bytes.Buffer)
   456  	tarWriter := tar.NewWriter(w)
   457  	defer tarWriter.Close()
   458  
   459  	tarReader := tar.NewReader(layerReader)
   460  	for {
   461  		header, err := tarReader.Next()
   462  		if errors.Is(err, io.EOF) {
   463  			break
   464  		}
   465  		if err != nil {
   466  			return nil, fmt.Errorf("reading layer: %w", err)
   467  		}
   468  
   469  		header.ModTime = t
   470  
   471  		//PAX and GNU Format support additional timestamps in the header
   472  		if header.Format == tar.FormatPAX || header.Format == tar.FormatGNU {
   473  			header.AccessTime = t
   474  			header.ChangeTime = t
   475  		}
   476  
   477  		if err := tarWriter.WriteHeader(header); err != nil {
   478  			return nil, fmt.Errorf("writing tar header: %w", err)
   479  		}
   480  
   481  		if header.Typeflag == tar.TypeReg {
   482  			// TODO(#1168): This should be lazy, and not buffer the entire layer contents.
   483  			if _, err = io.CopyN(tarWriter, tarReader, header.Size); err != nil {
   484  				return nil, fmt.Errorf("writing layer file: %w", err)
   485  			}
   486  		}
   487  	}
   488  
   489  	if err := tarWriter.Close(); err != nil {
   490  		return nil, err
   491  	}
   492  
   493  	b := w.Bytes()
   494  	// gzip the contents, then create the layer
   495  	opener := func() (io.ReadCloser, error) {
   496  		return gzip.ReadCloser(io.NopCloser(bytes.NewReader(b))), nil
   497  	}
   498  	layer, err = tarball.LayerFromOpener(opener)
   499  	if err != nil {
   500  		return nil, fmt.Errorf("creating layer: %w", err)
   501  	}
   502  
   503  	return layer, nil
   504  }
   505  
   506  // Canonical is a helper function to combine Time and configFile
   507  // to remove any randomness during a docker build.
   508  func Canonical(img v1.Image) (v1.Image, error) {
   509  	// Set all timestamps to 0
   510  	created := time.Time{}
   511  	img, err := Time(img, created)
   512  	if err != nil {
   513  		return nil, err
   514  	}
   515  
   516  	cf, err := img.ConfigFile()
   517  	if err != nil {
   518  		return nil, err
   519  	}
   520  
   521  	// Get rid of host-dependent random config
   522  	cfg := cf.DeepCopy()
   523  
   524  	cfg.Container = ""
   525  	cfg.Config.Hostname = ""
   526  	cfg.DockerVersion = ""
   527  
   528  	return ConfigFile(img, cfg)
   529  }
   530  
   531  // MediaType modifies the MediaType() of the given image.
   532  func MediaType(img v1.Image, mt types.MediaType) v1.Image {
   533  	return &image{
   534  		base:      img,
   535  		mediaType: &mt,
   536  	}
   537  }
   538  
   539  // ConfigMediaType modifies the MediaType() of the given image's Config.
   540  //
   541  // If !mt.IsConfig(), this will be the image's artifactType in any indexes it's a part of.
   542  func ConfigMediaType(img v1.Image, mt types.MediaType) v1.Image {
   543  	return &image{
   544  		base:            img,
   545  		configMediaType: &mt,
   546  	}
   547  }
   548  
   549  // IndexMediaType modifies the MediaType() of the given index.
   550  func IndexMediaType(idx v1.ImageIndex, mt types.MediaType) v1.ImageIndex {
   551  	return &index{
   552  		base:      idx,
   553  		mediaType: &mt,
   554  	}
   555  }
   556  

View as plain text