...

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

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

     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 tarball
    16  
    17  import (
    18  	"archive/tar"
    19  	"bytes"
    20  	"encoding/json"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"os"
    25  	"path"
    26  	"path/filepath"
    27  	"sync"
    28  
    29  	comp "github.com/google/go-containerregistry/internal/compression"
    30  	"github.com/google/go-containerregistry/pkg/compression"
    31  	"github.com/google/go-containerregistry/pkg/name"
    32  	v1 "github.com/google/go-containerregistry/pkg/v1"
    33  	"github.com/google/go-containerregistry/pkg/v1/partial"
    34  	"github.com/google/go-containerregistry/pkg/v1/types"
    35  )
    36  
    37  type image struct {
    38  	opener        Opener
    39  	manifest      *Manifest
    40  	config        []byte
    41  	imgDescriptor *Descriptor
    42  
    43  	tag *name.Tag
    44  }
    45  
    46  type uncompressedImage struct {
    47  	*image
    48  }
    49  
    50  type compressedImage struct {
    51  	*image
    52  	manifestLock sync.Mutex // Protects manifest
    53  	manifest     *v1.Manifest
    54  }
    55  
    56  var _ partial.UncompressedImageCore = (*uncompressedImage)(nil)
    57  var _ partial.CompressedImageCore = (*compressedImage)(nil)
    58  
    59  // Opener is a thunk for opening a tar file.
    60  type Opener func() (io.ReadCloser, error)
    61  
    62  func pathOpener(path string) Opener {
    63  	return func() (io.ReadCloser, error) {
    64  		return os.Open(path)
    65  	}
    66  }
    67  
    68  // ImageFromPath returns a v1.Image from a tarball located on path.
    69  func ImageFromPath(path string, tag *name.Tag) (v1.Image, error) {
    70  	return Image(pathOpener(path), tag)
    71  }
    72  
    73  // LoadManifest load manifest
    74  func LoadManifest(opener Opener) (Manifest, error) {
    75  	m, err := extractFileFromTar(opener, "manifest.json")
    76  	if err != nil {
    77  		return nil, err
    78  	}
    79  	defer m.Close()
    80  
    81  	var manifest Manifest
    82  
    83  	if err := json.NewDecoder(m).Decode(&manifest); err != nil {
    84  		return nil, err
    85  	}
    86  	return manifest, nil
    87  }
    88  
    89  // Image exposes an image from the tarball at the provided path.
    90  func Image(opener Opener, tag *name.Tag) (v1.Image, error) {
    91  	img := &image{
    92  		opener: opener,
    93  		tag:    tag,
    94  	}
    95  	if err := img.loadTarDescriptorAndConfig(); err != nil {
    96  		return nil, err
    97  	}
    98  
    99  	// Peek at the first layer and see if it's compressed.
   100  	if len(img.imgDescriptor.Layers) > 0 {
   101  		compressed, err := img.areLayersCompressed()
   102  		if err != nil {
   103  			return nil, err
   104  		}
   105  		if compressed {
   106  			c := compressedImage{
   107  				image: img,
   108  			}
   109  			return partial.CompressedToImage(&c)
   110  		}
   111  	}
   112  
   113  	uc := uncompressedImage{
   114  		image: img,
   115  	}
   116  	return partial.UncompressedToImage(&uc)
   117  }
   118  
   119  func (i *image) MediaType() (types.MediaType, error) {
   120  	return types.DockerManifestSchema2, nil
   121  }
   122  
   123  // Descriptor stores the manifest data for a single image inside a `docker save` tarball.
   124  type Descriptor struct {
   125  	Config   string
   126  	RepoTags []string
   127  	Layers   []string
   128  
   129  	// Tracks foreign layer info. Key is DiffID.
   130  	LayerSources map[v1.Hash]v1.Descriptor `json:",omitempty"`
   131  }
   132  
   133  // Manifest represents the manifests of all images as the `manifest.json` file in a `docker save` tarball.
   134  type Manifest []Descriptor
   135  
   136  func (m Manifest) findDescriptor(tag *name.Tag) (*Descriptor, error) {
   137  	if tag == nil {
   138  		if len(m) != 1 {
   139  			return nil, errors.New("tarball must contain only a single image to be used with tarball.Image")
   140  		}
   141  		return &(m)[0], nil
   142  	}
   143  	for _, img := range m {
   144  		for _, tagStr := range img.RepoTags {
   145  			repoTag, err := name.NewTag(tagStr)
   146  			if err != nil {
   147  				return nil, err
   148  			}
   149  
   150  			// Compare the resolved names, since there are several ways to specify the same tag.
   151  			if repoTag.Name() == tag.Name() {
   152  				return &img, nil
   153  			}
   154  		}
   155  	}
   156  	return nil, fmt.Errorf("tag %s not found in tarball", tag)
   157  }
   158  
   159  func (i *image) areLayersCompressed() (bool, error) {
   160  	if len(i.imgDescriptor.Layers) == 0 {
   161  		return false, errors.New("0 layers found in image")
   162  	}
   163  	layer := i.imgDescriptor.Layers[0]
   164  	blob, err := extractFileFromTar(i.opener, layer)
   165  	if err != nil {
   166  		return false, err
   167  	}
   168  	defer blob.Close()
   169  
   170  	cp, _, err := comp.PeekCompression(blob)
   171  	if err != nil {
   172  		return false, err
   173  	}
   174  
   175  	return cp != compression.None, nil
   176  }
   177  
   178  func (i *image) loadTarDescriptorAndConfig() error {
   179  	m, err := extractFileFromTar(i.opener, "manifest.json")
   180  	if err != nil {
   181  		return err
   182  	}
   183  	defer m.Close()
   184  
   185  	if err := json.NewDecoder(m).Decode(&i.manifest); err != nil {
   186  		return err
   187  	}
   188  
   189  	if i.manifest == nil {
   190  		return errors.New("no valid manifest.json in tarball")
   191  	}
   192  
   193  	i.imgDescriptor, err = i.manifest.findDescriptor(i.tag)
   194  	if err != nil {
   195  		return err
   196  	}
   197  
   198  	cfg, err := extractFileFromTar(i.opener, i.imgDescriptor.Config)
   199  	if err != nil {
   200  		return err
   201  	}
   202  	defer cfg.Close()
   203  
   204  	i.config, err = io.ReadAll(cfg)
   205  	if err != nil {
   206  		return err
   207  	}
   208  	return nil
   209  }
   210  
   211  func (i *image) RawConfigFile() ([]byte, error) {
   212  	return i.config, nil
   213  }
   214  
   215  // tarFile represents a single file inside a tar. Closing it closes the tar itself.
   216  type tarFile struct {
   217  	io.Reader
   218  	io.Closer
   219  }
   220  
   221  func extractFileFromTar(opener Opener, filePath string) (io.ReadCloser, error) {
   222  	f, err := opener()
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  	needClose := true
   227  	defer func() {
   228  		if needClose {
   229  			f.Close()
   230  		}
   231  	}()
   232  
   233  	tf := tar.NewReader(f)
   234  	for {
   235  		hdr, err := tf.Next()
   236  		if errors.Is(err, io.EOF) {
   237  			break
   238  		}
   239  		if err != nil {
   240  			return nil, err
   241  		}
   242  		if hdr.Name == filePath {
   243  			if hdr.Typeflag == tar.TypeSymlink || hdr.Typeflag == tar.TypeLink {
   244  				currentDir := filepath.Dir(filePath)
   245  				return extractFileFromTar(opener, path.Join(currentDir, path.Clean(hdr.Linkname)))
   246  			}
   247  			needClose = false
   248  			return tarFile{
   249  				Reader: tf,
   250  				Closer: f,
   251  			}, nil
   252  		}
   253  	}
   254  	return nil, fmt.Errorf("file %s not found in tar", filePath)
   255  }
   256  
   257  // uncompressedLayerFromTarball implements partial.UncompressedLayer
   258  type uncompressedLayerFromTarball struct {
   259  	diffID    v1.Hash
   260  	mediaType types.MediaType
   261  	opener    Opener
   262  	filePath  string
   263  }
   264  
   265  // foreignUncompressedLayer implements partial.UncompressedLayer but returns
   266  // a custom descriptor. This allows the foreign layer URLs to be included in
   267  // the generated image manifest for uncompressed layers.
   268  type foreignUncompressedLayer struct {
   269  	uncompressedLayerFromTarball
   270  	desc v1.Descriptor
   271  }
   272  
   273  func (fl *foreignUncompressedLayer) Descriptor() (*v1.Descriptor, error) {
   274  	return &fl.desc, nil
   275  }
   276  
   277  // DiffID implements partial.UncompressedLayer
   278  func (ulft *uncompressedLayerFromTarball) DiffID() (v1.Hash, error) {
   279  	return ulft.diffID, nil
   280  }
   281  
   282  // Uncompressed implements partial.UncompressedLayer
   283  func (ulft *uncompressedLayerFromTarball) Uncompressed() (io.ReadCloser, error) {
   284  	return extractFileFromTar(ulft.opener, ulft.filePath)
   285  }
   286  
   287  func (ulft *uncompressedLayerFromTarball) MediaType() (types.MediaType, error) {
   288  	return ulft.mediaType, nil
   289  }
   290  
   291  func (i *uncompressedImage) LayerByDiffID(h v1.Hash) (partial.UncompressedLayer, error) {
   292  	cfg, err := partial.ConfigFile(i)
   293  	if err != nil {
   294  		return nil, err
   295  	}
   296  	for idx, diffID := range cfg.RootFS.DiffIDs {
   297  		if diffID == h {
   298  			// Technically the media type should be 'application/tar' but given that our
   299  			// v1.Layer doesn't force consumers to care about whether the layer is compressed
   300  			// we should be fine returning the DockerLayer media type
   301  			mt := types.DockerLayer
   302  			bd, ok := i.imgDescriptor.LayerSources[h]
   303  			if ok {
   304  				// This is janky, but we don't want to implement Descriptor for
   305  				// uncompressed layers because it breaks a bunch of assumptions in partial.
   306  				// See https://github.com/google/go-containerregistry/issues/1870
   307  				docker25workaround := bd.MediaType == types.DockerUncompressedLayer || bd.MediaType == types.OCIUncompressedLayer
   308  
   309  				if !docker25workaround {
   310  					// Overwrite the mediaType for foreign layers.
   311  					return &foreignUncompressedLayer{
   312  						uncompressedLayerFromTarball: uncompressedLayerFromTarball{
   313  							diffID:    diffID,
   314  							mediaType: bd.MediaType,
   315  							opener:    i.opener,
   316  							filePath:  i.imgDescriptor.Layers[idx],
   317  						},
   318  						desc: bd,
   319  					}, nil
   320  				}
   321  
   322  				// Intentional fall through.
   323  			}
   324  
   325  			return &uncompressedLayerFromTarball{
   326  				diffID:    diffID,
   327  				mediaType: mt,
   328  				opener:    i.opener,
   329  				filePath:  i.imgDescriptor.Layers[idx],
   330  			}, nil
   331  		}
   332  	}
   333  	return nil, fmt.Errorf("diff id %q not found", h)
   334  }
   335  
   336  func (c *compressedImage) Manifest() (*v1.Manifest, error) {
   337  	c.manifestLock.Lock()
   338  	defer c.manifestLock.Unlock()
   339  	if c.manifest != nil {
   340  		return c.manifest, nil
   341  	}
   342  
   343  	b, err := c.RawConfigFile()
   344  	if err != nil {
   345  		return nil, err
   346  	}
   347  
   348  	cfgHash, cfgSize, err := v1.SHA256(bytes.NewReader(b))
   349  	if err != nil {
   350  		return nil, err
   351  	}
   352  
   353  	c.manifest = &v1.Manifest{
   354  		SchemaVersion: 2,
   355  		MediaType:     types.DockerManifestSchema2,
   356  		Config: v1.Descriptor{
   357  			MediaType: types.DockerConfigJSON,
   358  			Size:      cfgSize,
   359  			Digest:    cfgHash,
   360  		},
   361  	}
   362  
   363  	for i, p := range c.imgDescriptor.Layers {
   364  		cfg, err := partial.ConfigFile(c)
   365  		if err != nil {
   366  			return nil, err
   367  		}
   368  		diffid := cfg.RootFS.DiffIDs[i]
   369  		if d, ok := c.imgDescriptor.LayerSources[diffid]; ok {
   370  			// If it's a foreign layer, just append the descriptor so we can avoid
   371  			// reading the entire file.
   372  			c.manifest.Layers = append(c.manifest.Layers, d)
   373  		} else {
   374  			l, err := extractFileFromTar(c.opener, p)
   375  			if err != nil {
   376  				return nil, err
   377  			}
   378  			defer l.Close()
   379  			sha, size, err := v1.SHA256(l)
   380  			if err != nil {
   381  				return nil, err
   382  			}
   383  			c.manifest.Layers = append(c.manifest.Layers, v1.Descriptor{
   384  				MediaType: types.DockerLayer,
   385  				Size:      size,
   386  				Digest:    sha,
   387  			})
   388  		}
   389  	}
   390  	return c.manifest, nil
   391  }
   392  
   393  func (c *compressedImage) RawManifest() ([]byte, error) {
   394  	return partial.RawManifest(c)
   395  }
   396  
   397  // compressedLayerFromTarball implements partial.CompressedLayer
   398  type compressedLayerFromTarball struct {
   399  	desc     v1.Descriptor
   400  	opener   Opener
   401  	filePath string
   402  }
   403  
   404  // Digest implements partial.CompressedLayer
   405  func (clft *compressedLayerFromTarball) Digest() (v1.Hash, error) {
   406  	return clft.desc.Digest, nil
   407  }
   408  
   409  // Compressed implements partial.CompressedLayer
   410  func (clft *compressedLayerFromTarball) Compressed() (io.ReadCloser, error) {
   411  	return extractFileFromTar(clft.opener, clft.filePath)
   412  }
   413  
   414  // MediaType implements partial.CompressedLayer
   415  func (clft *compressedLayerFromTarball) MediaType() (types.MediaType, error) {
   416  	return clft.desc.MediaType, nil
   417  }
   418  
   419  // Size implements partial.CompressedLayer
   420  func (clft *compressedLayerFromTarball) Size() (int64, error) {
   421  	return clft.desc.Size, nil
   422  }
   423  
   424  func (c *compressedImage) LayerByDigest(h v1.Hash) (partial.CompressedLayer, error) {
   425  	m, err := c.Manifest()
   426  	if err != nil {
   427  		return nil, err
   428  	}
   429  	for i, l := range m.Layers {
   430  		if l.Digest == h {
   431  			fp := c.imgDescriptor.Layers[i]
   432  			return &compressedLayerFromTarball{
   433  				desc:     l,
   434  				opener:   c.opener,
   435  				filePath: fp,
   436  			}, nil
   437  		}
   438  	}
   439  	return nil, fmt.Errorf("blob %v not found", h)
   440  }
   441  

View as plain text