github.com/flavio/docker@v0.1.3-0.20170117145210-f63d1a6eec47/plugin/backend_linux.go (about)

     1  // +build linux
     2  
     3  package plugin
     4  
     5  import (
     6  	"archive/tar"
     7  	"compress/gzip"
     8  	"encoding/json"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"net/http"
    13  	"os"
    14  	"path"
    15  	"path/filepath"
    16  	"strings"
    17  
    18  	"github.com/Sirupsen/logrus"
    19  	"github.com/docker/distribution/manifest/schema2"
    20  	"github.com/docker/docker/api/types"
    21  	"github.com/docker/docker/distribution"
    22  	progressutils "github.com/docker/docker/distribution/utils"
    23  	"github.com/docker/docker/distribution/xfer"
    24  	"github.com/docker/docker/image"
    25  	"github.com/docker/docker/layer"
    26  	"github.com/docker/docker/pkg/chrootarchive"
    27  	"github.com/docker/docker/pkg/pools"
    28  	"github.com/docker/docker/pkg/progress"
    29  	"github.com/docker/docker/plugin/v2"
    30  	"github.com/docker/docker/reference"
    31  	"github.com/opencontainers/go-digest"
    32  	"github.com/pkg/errors"
    33  	"golang.org/x/net/context"
    34  )
    35  
    36  // Disable deactivates a plugin. This means resources (volumes, networks) cant use them.
    37  func (pm *Manager) Disable(refOrID string, config *types.PluginDisableConfig) error {
    38  	p, err := pm.config.Store.GetV2Plugin(refOrID)
    39  	if err != nil {
    40  		return err
    41  	}
    42  	pm.mu.RLock()
    43  	c := pm.cMap[p]
    44  	pm.mu.RUnlock()
    45  
    46  	if !config.ForceDisable && p.GetRefCount() > 0 {
    47  		return fmt.Errorf("plugin %s is in use", p.Name())
    48  	}
    49  
    50  	if err := pm.disable(p, c); err != nil {
    51  		return err
    52  	}
    53  	pm.config.LogPluginEvent(p.GetID(), refOrID, "disable")
    54  	return nil
    55  }
    56  
    57  // Enable activates a plugin, which implies that they are ready to be used by containers.
    58  func (pm *Manager) Enable(refOrID string, config *types.PluginEnableConfig) error {
    59  	p, err := pm.config.Store.GetV2Plugin(refOrID)
    60  	if err != nil {
    61  		return err
    62  	}
    63  
    64  	c := &controller{timeoutInSecs: config.Timeout}
    65  	if err := pm.enable(p, c, false); err != nil {
    66  		return err
    67  	}
    68  	pm.config.LogPluginEvent(p.GetID(), refOrID, "enable")
    69  	return nil
    70  }
    71  
    72  // Inspect examines a plugin config
    73  func (pm *Manager) Inspect(refOrID string) (tp *types.Plugin, err error) {
    74  	p, err := pm.config.Store.GetV2Plugin(refOrID)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	return &p.PluginObj, nil
    80  }
    81  
    82  func (pm *Manager) pull(ctx context.Context, ref reference.Named, config *distribution.ImagePullConfig, outStream io.Writer) error {
    83  	if outStream != nil {
    84  		// Include a buffer so that slow client connections don't affect
    85  		// transfer performance.
    86  		progressChan := make(chan progress.Progress, 100)
    87  
    88  		writesDone := make(chan struct{})
    89  
    90  		defer func() {
    91  			close(progressChan)
    92  			<-writesDone
    93  		}()
    94  
    95  		var cancelFunc context.CancelFunc
    96  		ctx, cancelFunc = context.WithCancel(ctx)
    97  
    98  		go func() {
    99  			progressutils.WriteDistributionProgress(cancelFunc, outStream, progressChan)
   100  			close(writesDone)
   101  		}()
   102  
   103  		config.ProgressOutput = progress.ChanOutput(progressChan)
   104  	} else {
   105  		config.ProgressOutput = progress.DiscardOutput()
   106  	}
   107  	return distribution.Pull(ctx, ref, config)
   108  }
   109  
   110  type tempConfigStore struct {
   111  	config       []byte
   112  	configDigest digest.Digest
   113  }
   114  
   115  func (s *tempConfigStore) Put(c []byte) (digest.Digest, error) {
   116  	dgst := digest.FromBytes(c)
   117  
   118  	s.config = c
   119  	s.configDigest = dgst
   120  
   121  	return dgst, nil
   122  }
   123  
   124  func (s *tempConfigStore) Get(d digest.Digest) ([]byte, error) {
   125  	if d != s.configDigest {
   126  		return nil, fmt.Errorf("digest not found")
   127  	}
   128  	return s.config, nil
   129  }
   130  
   131  func (s *tempConfigStore) RootFSFromConfig(c []byte) (*image.RootFS, error) {
   132  	return configToRootFS(c)
   133  }
   134  
   135  func computePrivileges(c types.PluginConfig) (types.PluginPrivileges, error) {
   136  	var privileges types.PluginPrivileges
   137  	if c.Network.Type != "null" && c.Network.Type != "bridge" && c.Network.Type != "" {
   138  		privileges = append(privileges, types.PluginPrivilege{
   139  			Name:        "network",
   140  			Description: "permissions to access a network",
   141  			Value:       []string{c.Network.Type},
   142  		})
   143  	}
   144  	for _, mount := range c.Mounts {
   145  		if mount.Source != nil {
   146  			privileges = append(privileges, types.PluginPrivilege{
   147  				Name:        "mount",
   148  				Description: "host path to mount",
   149  				Value:       []string{*mount.Source},
   150  			})
   151  		}
   152  	}
   153  	for _, device := range c.Linux.Devices {
   154  		if device.Path != nil {
   155  			privileges = append(privileges, types.PluginPrivilege{
   156  				Name:        "device",
   157  				Description: "host device to access",
   158  				Value:       []string{*device.Path},
   159  			})
   160  		}
   161  	}
   162  	if c.Linux.AllowAllDevices {
   163  		privileges = append(privileges, types.PluginPrivilege{
   164  			Name:        "allow-all-devices",
   165  			Description: "allow 'rwm' access to all devices",
   166  			Value:       []string{"true"},
   167  		})
   168  	}
   169  	if len(c.Linux.Capabilities) > 0 {
   170  		privileges = append(privileges, types.PluginPrivilege{
   171  			Name:        "capabilities",
   172  			Description: "list of additional capabilities required",
   173  			Value:       c.Linux.Capabilities,
   174  		})
   175  	}
   176  
   177  	return privileges, nil
   178  }
   179  
   180  // Privileges pulls a plugin config and computes the privileges required to install it.
   181  func (pm *Manager) Privileges(ctx context.Context, ref reference.Named, metaHeader http.Header, authConfig *types.AuthConfig) (types.PluginPrivileges, error) {
   182  	// create image store instance
   183  	cs := &tempConfigStore{}
   184  
   185  	// DownloadManager not defined because only pulling configuration.
   186  	pluginPullConfig := &distribution.ImagePullConfig{
   187  		Config: distribution.Config{
   188  			MetaHeaders:      metaHeader,
   189  			AuthConfig:       authConfig,
   190  			RegistryService:  pm.config.RegistryService,
   191  			ImageEventLogger: func(string, string, string) {},
   192  			ImageStore:       cs,
   193  		},
   194  		Schema2Types: distribution.PluginTypes,
   195  	}
   196  
   197  	if err := pm.pull(ctx, ref, pluginPullConfig, nil); err != nil {
   198  		return nil, err
   199  	}
   200  
   201  	if cs.config == nil {
   202  		return nil, errors.New("no configuration pulled")
   203  	}
   204  	var config types.PluginConfig
   205  	if err := json.Unmarshal(cs.config, &config); err != nil {
   206  		return nil, err
   207  	}
   208  
   209  	return computePrivileges(config)
   210  }
   211  
   212  // Pull pulls a plugin, check if the correct privileges are provided and install the plugin.
   213  func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *types.AuthConfig, privileges types.PluginPrivileges, outStream io.Writer) (err error) {
   214  	pm.muGC.RLock()
   215  	defer pm.muGC.RUnlock()
   216  
   217  	// revalidate because Pull is public
   218  	nameref, err := reference.ParseNamed(name)
   219  	if err != nil {
   220  		return errors.Wrapf(err, "failed to parse %q", name)
   221  	}
   222  	name = reference.WithDefaultTag(nameref).String()
   223  
   224  	if err := pm.config.Store.validateName(name); err != nil {
   225  		return err
   226  	}
   227  
   228  	tmpRootFSDir, err := ioutil.TempDir(pm.tmpDir(), ".rootfs")
   229  	defer os.RemoveAll(tmpRootFSDir)
   230  
   231  	dm := &downloadManager{
   232  		tmpDir:    tmpRootFSDir,
   233  		blobStore: pm.blobStore,
   234  	}
   235  
   236  	pluginPullConfig := &distribution.ImagePullConfig{
   237  		Config: distribution.Config{
   238  			MetaHeaders:      metaHeader,
   239  			AuthConfig:       authConfig,
   240  			RegistryService:  pm.config.RegistryService,
   241  			ImageEventLogger: pm.config.LogPluginEvent,
   242  			ImageStore:       dm,
   243  		},
   244  		DownloadManager: dm, // todo: reevaluate if possible to substitute distribution/xfer dependencies instead
   245  		Schema2Types:    distribution.PluginTypes,
   246  	}
   247  
   248  	err = pm.pull(ctx, ref, pluginPullConfig, outStream)
   249  	if err != nil {
   250  		go pm.GC()
   251  		return err
   252  	}
   253  
   254  	if _, err := pm.createPlugin(name, dm.configDigest, dm.blobs, tmpRootFSDir, &privileges); err != nil {
   255  		return err
   256  	}
   257  
   258  	return nil
   259  }
   260  
   261  // List displays the list of plugins and associated metadata.
   262  func (pm *Manager) List() ([]types.Plugin, error) {
   263  	plugins := pm.config.Store.GetAll()
   264  	out := make([]types.Plugin, 0, len(plugins))
   265  	for _, p := range plugins {
   266  		out = append(out, p.PluginObj)
   267  	}
   268  	return out, nil
   269  }
   270  
   271  // Push pushes a plugin to the store.
   272  func (pm *Manager) Push(ctx context.Context, name string, metaHeader http.Header, authConfig *types.AuthConfig, outStream io.Writer) error {
   273  	p, err := pm.config.Store.GetV2Plugin(name)
   274  	if err != nil {
   275  		return err
   276  	}
   277  
   278  	ref, err := reference.ParseNamed(p.Name())
   279  	if err != nil {
   280  		return errors.Wrapf(err, "plugin has invalid name %v for push", p.Name())
   281  	}
   282  
   283  	var po progress.Output
   284  	if outStream != nil {
   285  		// Include a buffer so that slow client connections don't affect
   286  		// transfer performance.
   287  		progressChan := make(chan progress.Progress, 100)
   288  
   289  		writesDone := make(chan struct{})
   290  
   291  		defer func() {
   292  			close(progressChan)
   293  			<-writesDone
   294  		}()
   295  
   296  		var cancelFunc context.CancelFunc
   297  		ctx, cancelFunc = context.WithCancel(ctx)
   298  
   299  		go func() {
   300  			progressutils.WriteDistributionProgress(cancelFunc, outStream, progressChan)
   301  			close(writesDone)
   302  		}()
   303  
   304  		po = progress.ChanOutput(progressChan)
   305  	} else {
   306  		po = progress.DiscardOutput()
   307  	}
   308  
   309  	// TODO: replace these with manager
   310  	is := &pluginConfigStore{
   311  		pm:     pm,
   312  		plugin: p,
   313  	}
   314  	ls := &pluginLayerProvider{
   315  		pm:     pm,
   316  		plugin: p,
   317  	}
   318  	rs := &pluginReference{
   319  		name:     ref,
   320  		pluginID: p.Config,
   321  	}
   322  
   323  	uploadManager := xfer.NewLayerUploadManager(3)
   324  
   325  	imagePushConfig := &distribution.ImagePushConfig{
   326  		Config: distribution.Config{
   327  			MetaHeaders:      metaHeader,
   328  			AuthConfig:       authConfig,
   329  			ProgressOutput:   po,
   330  			RegistryService:  pm.config.RegistryService,
   331  			ReferenceStore:   rs,
   332  			ImageEventLogger: pm.config.LogPluginEvent,
   333  			ImageStore:       is,
   334  			RequireSchema2:   true,
   335  		},
   336  		ConfigMediaType: schema2.MediaTypePluginConfig,
   337  		LayerStore:      ls,
   338  		UploadManager:   uploadManager,
   339  	}
   340  
   341  	return distribution.Push(ctx, ref, imagePushConfig)
   342  }
   343  
   344  type pluginReference struct {
   345  	name     reference.Named
   346  	pluginID digest.Digest
   347  }
   348  
   349  func (r *pluginReference) References(id digest.Digest) []reference.Named {
   350  	if r.pluginID != id {
   351  		return nil
   352  	}
   353  	return []reference.Named{r.name}
   354  }
   355  
   356  func (r *pluginReference) ReferencesByName(ref reference.Named) []reference.Association {
   357  	return []reference.Association{
   358  		{
   359  			Ref: r.name,
   360  			ID:  r.pluginID,
   361  		},
   362  	}
   363  }
   364  
   365  func (r *pluginReference) Get(ref reference.Named) (digest.Digest, error) {
   366  	if r.name.String() != ref.String() {
   367  		return digest.Digest(""), reference.ErrDoesNotExist
   368  	}
   369  	return r.pluginID, nil
   370  }
   371  
   372  func (r *pluginReference) AddTag(ref reference.Named, id digest.Digest, force bool) error {
   373  	// Read only, ignore
   374  	return nil
   375  }
   376  func (r *pluginReference) AddDigest(ref reference.Canonical, id digest.Digest, force bool) error {
   377  	// Read only, ignore
   378  	return nil
   379  }
   380  func (r *pluginReference) Delete(ref reference.Named) (bool, error) {
   381  	// Read only, ignore
   382  	return false, nil
   383  }
   384  
   385  type pluginConfigStore struct {
   386  	pm     *Manager
   387  	plugin *v2.Plugin
   388  }
   389  
   390  func (s *pluginConfigStore) Put([]byte) (digest.Digest, error) {
   391  	return digest.Digest(""), errors.New("cannot store config on push")
   392  }
   393  
   394  func (s *pluginConfigStore) Get(d digest.Digest) ([]byte, error) {
   395  	if s.plugin.Config != d {
   396  		return nil, errors.New("plugin not found")
   397  	}
   398  	rwc, err := s.pm.blobStore.Get(d)
   399  	if err != nil {
   400  		return nil, err
   401  	}
   402  	defer rwc.Close()
   403  	return ioutil.ReadAll(rwc)
   404  }
   405  
   406  func (s *pluginConfigStore) RootFSFromConfig(c []byte) (*image.RootFS, error) {
   407  	return configToRootFS(c)
   408  }
   409  
   410  type pluginLayerProvider struct {
   411  	pm     *Manager
   412  	plugin *v2.Plugin
   413  }
   414  
   415  func (p *pluginLayerProvider) Get(id layer.ChainID) (distribution.PushLayer, error) {
   416  	rootFS := rootFSFromPlugin(p.plugin.PluginObj.Config.Rootfs)
   417  	var i int
   418  	for i = 1; i <= len(rootFS.DiffIDs); i++ {
   419  		if layer.CreateChainID(rootFS.DiffIDs[:i]) == id {
   420  			break
   421  		}
   422  	}
   423  	if i > len(rootFS.DiffIDs) {
   424  		return nil, errors.New("layer not found")
   425  	}
   426  	return &pluginLayer{
   427  		pm:      p.pm,
   428  		diffIDs: rootFS.DiffIDs[:i],
   429  		blobs:   p.plugin.Blobsums[:i],
   430  	}, nil
   431  }
   432  
   433  type pluginLayer struct {
   434  	pm      *Manager
   435  	diffIDs []layer.DiffID
   436  	blobs   []digest.Digest
   437  }
   438  
   439  func (l *pluginLayer) ChainID() layer.ChainID {
   440  	return layer.CreateChainID(l.diffIDs)
   441  }
   442  
   443  func (l *pluginLayer) DiffID() layer.DiffID {
   444  	return l.diffIDs[len(l.diffIDs)-1]
   445  }
   446  
   447  func (l *pluginLayer) Parent() distribution.PushLayer {
   448  	if len(l.diffIDs) == 1 {
   449  		return nil
   450  	}
   451  	return &pluginLayer{
   452  		pm:      l.pm,
   453  		diffIDs: l.diffIDs[:len(l.diffIDs)-1],
   454  		blobs:   l.blobs[:len(l.diffIDs)-1],
   455  	}
   456  }
   457  
   458  func (l *pluginLayer) Open() (io.ReadCloser, error) {
   459  	return l.pm.blobStore.Get(l.blobs[len(l.diffIDs)-1])
   460  }
   461  
   462  func (l *pluginLayer) Size() (int64, error) {
   463  	return l.pm.blobStore.Size(l.blobs[len(l.diffIDs)-1])
   464  }
   465  
   466  func (l *pluginLayer) MediaType() string {
   467  	return schema2.MediaTypeLayer
   468  }
   469  
   470  func (l *pluginLayer) Release() {
   471  	// Nothing needs to be release, no references held
   472  }
   473  
   474  // Remove deletes plugin's root directory.
   475  func (pm *Manager) Remove(name string, config *types.PluginRmConfig) error {
   476  	p, err := pm.config.Store.GetV2Plugin(name)
   477  	pm.mu.RLock()
   478  	c := pm.cMap[p]
   479  	pm.mu.RUnlock()
   480  
   481  	if err != nil {
   482  		return err
   483  	}
   484  
   485  	if !config.ForceRemove {
   486  		if p.GetRefCount() > 0 {
   487  			return fmt.Errorf("plugin %s is in use", p.Name())
   488  		}
   489  		if p.IsEnabled() {
   490  			return fmt.Errorf("plugin %s is enabled", p.Name())
   491  		}
   492  	}
   493  
   494  	if p.IsEnabled() {
   495  		if err := pm.disable(p, c); err != nil {
   496  			logrus.Errorf("failed to disable plugin '%s': %s", p.Name(), err)
   497  		}
   498  	}
   499  
   500  	defer func() {
   501  		go pm.GC()
   502  	}()
   503  
   504  	id := p.GetID()
   505  	pm.config.Store.Remove(p)
   506  	pluginDir := filepath.Join(pm.config.Root, id)
   507  	if err := os.RemoveAll(pluginDir); err != nil {
   508  		logrus.Warnf("unable to remove %q from plugin remove: %v", pluginDir, err)
   509  	}
   510  	pm.config.LogPluginEvent(id, name, "remove")
   511  	return nil
   512  }
   513  
   514  // Set sets plugin args
   515  func (pm *Manager) Set(name string, args []string) error {
   516  	p, err := pm.config.Store.GetV2Plugin(name)
   517  	if err != nil {
   518  		return err
   519  	}
   520  	if err := p.Set(args); err != nil {
   521  		return err
   522  	}
   523  	return pm.save(p)
   524  }
   525  
   526  // CreateFromContext creates a plugin from the given pluginDir which contains
   527  // both the rootfs and the config.json and a repoName with optional tag.
   528  func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.ReadCloser, options *types.PluginCreateOptions) (err error) {
   529  	pm.muGC.RLock()
   530  	defer pm.muGC.RUnlock()
   531  
   532  	ref, err := reference.ParseNamed(options.RepoName)
   533  	if err != nil {
   534  		return errors.Wrapf(err, "failed to parse reference %v", options.RepoName)
   535  	}
   536  	if _, ok := ref.(reference.Canonical); ok {
   537  		return errors.Errorf("canonical references are not permitted")
   538  	}
   539  	name := reference.WithDefaultTag(ref).String()
   540  
   541  	if err := pm.config.Store.validateName(name); err != nil { // fast check, real check is in createPlugin()
   542  		return err
   543  	}
   544  
   545  	tmpRootFSDir, err := ioutil.TempDir(pm.tmpDir(), ".rootfs")
   546  	defer os.RemoveAll(tmpRootFSDir)
   547  	if err != nil {
   548  		return errors.Wrap(err, "failed to create temp directory")
   549  	}
   550  	var configJSON []byte
   551  	rootFS := splitConfigRootFSFromTar(tarCtx, &configJSON)
   552  
   553  	rootFSBlob, err := pm.blobStore.New()
   554  	if err != nil {
   555  		return err
   556  	}
   557  	defer rootFSBlob.Close()
   558  	gzw := gzip.NewWriter(rootFSBlob)
   559  	layerDigester := digest.Canonical.Digester()
   560  	rootFSReader := io.TeeReader(rootFS, io.MultiWriter(gzw, layerDigester.Hash()))
   561  
   562  	if err := chrootarchive.Untar(rootFSReader, tmpRootFSDir, nil); err != nil {
   563  		return err
   564  	}
   565  	if err := rootFS.Close(); err != nil {
   566  		return err
   567  	}
   568  
   569  	if configJSON == nil {
   570  		return errors.New("config not found")
   571  	}
   572  
   573  	if err := gzw.Close(); err != nil {
   574  		return errors.Wrap(err, "error closing gzip writer")
   575  	}
   576  
   577  	var config types.PluginConfig
   578  	if err := json.Unmarshal(configJSON, &config); err != nil {
   579  		return errors.Wrap(err, "failed to parse config")
   580  	}
   581  
   582  	if err := pm.validateConfig(config); err != nil {
   583  		return err
   584  	}
   585  
   586  	pm.mu.Lock()
   587  	defer pm.mu.Unlock()
   588  
   589  	rootFSBlobsum, err := rootFSBlob.Commit()
   590  	if err != nil {
   591  		return err
   592  	}
   593  	defer func() {
   594  		if err != nil {
   595  			go pm.GC()
   596  		}
   597  	}()
   598  
   599  	config.Rootfs = &types.PluginConfigRootfs{
   600  		Type:    "layers",
   601  		DiffIds: []string{layerDigester.Digest().String()},
   602  	}
   603  
   604  	configBlob, err := pm.blobStore.New()
   605  	if err != nil {
   606  		return err
   607  	}
   608  	defer configBlob.Close()
   609  	if err := json.NewEncoder(configBlob).Encode(config); err != nil {
   610  		return errors.Wrap(err, "error encoding json config")
   611  	}
   612  	configBlobsum, err := configBlob.Commit()
   613  	if err != nil {
   614  		return err
   615  	}
   616  
   617  	p, err := pm.createPlugin(name, configBlobsum, []digest.Digest{rootFSBlobsum}, tmpRootFSDir, nil)
   618  	if err != nil {
   619  		return err
   620  	}
   621  
   622  	pm.config.LogPluginEvent(p.PluginObj.ID, name, "create")
   623  
   624  	return nil
   625  }
   626  
   627  func (pm *Manager) validateConfig(config types.PluginConfig) error {
   628  	return nil // TODO:
   629  }
   630  
   631  func splitConfigRootFSFromTar(in io.ReadCloser, config *[]byte) io.ReadCloser {
   632  	pr, pw := io.Pipe()
   633  	go func() {
   634  		tarReader := tar.NewReader(in)
   635  		tarWriter := tar.NewWriter(pw)
   636  		defer in.Close()
   637  
   638  		hasRootFS := false
   639  
   640  		for {
   641  			hdr, err := tarReader.Next()
   642  			if err == io.EOF {
   643  				if !hasRootFS {
   644  					pw.CloseWithError(errors.Wrap(err, "no rootfs found"))
   645  					return
   646  				}
   647  				// Signals end of archive.
   648  				tarWriter.Close()
   649  				pw.Close()
   650  				return
   651  			}
   652  			if err != nil {
   653  				pw.CloseWithError(errors.Wrap(err, "failed to read from tar"))
   654  				return
   655  			}
   656  
   657  			content := io.Reader(tarReader)
   658  			name := path.Clean(hdr.Name)
   659  			if path.IsAbs(name) {
   660  				name = name[1:]
   661  			}
   662  			if name == configFileName {
   663  				dt, err := ioutil.ReadAll(content)
   664  				if err != nil {
   665  					pw.CloseWithError(errors.Wrapf(err, "failed to read %s", configFileName))
   666  					return
   667  				}
   668  				*config = dt
   669  			}
   670  			if parts := strings.Split(name, "/"); len(parts) != 0 && parts[0] == rootFSFileName {
   671  				hdr.Name = path.Clean(path.Join(parts[1:]...))
   672  				if hdr.Typeflag == tar.TypeLink && strings.HasPrefix(strings.ToLower(hdr.Linkname), rootFSFileName+"/") {
   673  					hdr.Linkname = hdr.Linkname[len(rootFSFileName)+1:]
   674  				}
   675  				if err := tarWriter.WriteHeader(hdr); err != nil {
   676  					pw.CloseWithError(errors.Wrap(err, "error writing tar header"))
   677  					return
   678  				}
   679  				if _, err := pools.Copy(tarWriter, content); err != nil {
   680  					pw.CloseWithError(errors.Wrap(err, "error copying tar data"))
   681  					return
   682  				}
   683  				hasRootFS = true
   684  			} else {
   685  				io.Copy(ioutil.Discard, content)
   686  			}
   687  		}
   688  	}()
   689  	return pr
   690  }