...

Source file src/github.com/containerd/continuity/context.go

Documentation: github.com/containerd/continuity

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package continuity
    18  
    19  import (
    20  	"bytes"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"os"
    25  	"path/filepath"
    26  	"strings"
    27  
    28  	"github.com/containerd/continuity/devices"
    29  	driverpkg "github.com/containerd/continuity/driver"
    30  	"github.com/containerd/continuity/pathdriver"
    31  
    32  	"github.com/opencontainers/go-digest"
    33  )
    34  
    35  var (
    36  	// ErrNotFound represents the resource not found
    37  	ErrNotFound = fmt.Errorf("not found")
    38  	// ErrNotSupported represents the resource not supported
    39  	ErrNotSupported = fmt.Errorf("not supported")
    40  )
    41  
    42  // Context represents a file system context for accessing resources. The
    43  // responsibility of the context is to convert system specific resources to
    44  // generic Resource objects. Most of this is safe path manipulation, as well
    45  // as extraction of resource details.
    46  type Context interface {
    47  	Apply(Resource) error
    48  	Verify(Resource) error
    49  	Resource(string, os.FileInfo) (Resource, error)
    50  	Walk(filepath.WalkFunc) error
    51  }
    52  
    53  // SymlinkPath is intended to give the symlink target value
    54  // in a root context. Target and linkname are absolute paths
    55  // not under the given root.
    56  type SymlinkPath func(root, linkname, target string) (string, error)
    57  
    58  // ContextOptions represents options to create a new context.
    59  type ContextOptions struct {
    60  	Digester   Digester
    61  	Driver     driverpkg.Driver
    62  	PathDriver pathdriver.PathDriver
    63  	Provider   ContentProvider
    64  }
    65  
    66  // context represents a file system context for accessing resources.
    67  // Generally, all path qualified access and system considerations should land
    68  // here.
    69  type context struct {
    70  	driver     driverpkg.Driver
    71  	pathDriver pathdriver.PathDriver
    72  	root       string
    73  	digester   Digester
    74  	provider   ContentProvider
    75  }
    76  
    77  // NewContext returns a Context associated with root. The default driver will
    78  // be used, as returned by NewDriver.
    79  func NewContext(root string) (Context, error) {
    80  	return NewContextWithOptions(root, ContextOptions{})
    81  }
    82  
    83  // NewContextWithOptions returns a Context associate with the root.
    84  func NewContextWithOptions(root string, options ContextOptions) (Context, error) {
    85  	// normalize to absolute path
    86  	pathDriver := options.PathDriver
    87  	if pathDriver == nil {
    88  		pathDriver = pathdriver.LocalPathDriver
    89  	}
    90  
    91  	root = pathDriver.FromSlash(root)
    92  	root, err := pathDriver.Abs(pathDriver.Clean(root))
    93  	if err != nil {
    94  		return nil, err
    95  	}
    96  
    97  	driver := options.Driver
    98  	if driver == nil {
    99  		driver, err = driverpkg.NewSystemDriver()
   100  		if err != nil {
   101  			return nil, err
   102  		}
   103  	}
   104  
   105  	digester := options.Digester
   106  	if digester == nil {
   107  		digester = simpleDigester{digest.Canonical}
   108  	}
   109  
   110  	// Check the root directory. Need to be a little careful here. We are
   111  	// allowing a link for now, but this may have odd behavior when
   112  	// canonicalizing paths. As long as all files are opened through the link
   113  	// path, this should be okay.
   114  	fi, err := driver.Stat(root)
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  
   119  	if !fi.IsDir() {
   120  		return nil, &os.PathError{Op: "NewContext", Path: root, Err: os.ErrInvalid}
   121  	}
   122  
   123  	return &context{
   124  		root:       root,
   125  		driver:     driver,
   126  		pathDriver: pathDriver,
   127  		digester:   digester,
   128  		provider:   options.Provider,
   129  	}, nil
   130  }
   131  
   132  // Resource returns the resource as path p, populating the entry with info
   133  // from fi. The path p should be the path of the resource in the context,
   134  // typically obtained through Walk or from the value of Resource.Path(). If fi
   135  // is nil, it will be resolved.
   136  func (c *context) Resource(p string, fi os.FileInfo) (Resource, error) {
   137  	fp, err := c.fullpath(p)
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  
   142  	if fi == nil {
   143  		fi, err = c.driver.Lstat(fp)
   144  		if err != nil {
   145  			return nil, err
   146  		}
   147  	}
   148  
   149  	base, err := newBaseResource(p, fi)
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  
   154  	base.xattrs, err = c.resolveXAttrs(fp, fi, base)
   155  	if err != nil && !errors.Is(err, ErrNotSupported) {
   156  		return nil, err
   157  	}
   158  
   159  	// TODO(stevvooe): Handle windows alternate data streams.
   160  
   161  	if fi.Mode().IsRegular() {
   162  		dgst, err := c.digest(p)
   163  		if err != nil {
   164  			return nil, err
   165  		}
   166  
   167  		return newRegularFile(*base, base.paths, fi.Size(), dgst)
   168  	}
   169  
   170  	if fi.Mode().IsDir() {
   171  		return newDirectory(*base)
   172  	}
   173  
   174  	if fi.Mode()&os.ModeSymlink != 0 {
   175  		// We handle relative links vs absolute links by including a
   176  		// beginning slash for absolute links. Effectively, the bundle's
   177  		// root is treated as the absolute link anchor.
   178  		target, err := c.driver.Readlink(fp)
   179  		if err != nil {
   180  			return nil, err
   181  		}
   182  
   183  		return newSymLink(*base, target)
   184  	}
   185  
   186  	if fi.Mode()&os.ModeNamedPipe != 0 {
   187  		return newNamedPipe(*base, base.paths)
   188  	}
   189  
   190  	if fi.Mode()&os.ModeDevice != 0 {
   191  		deviceDriver, ok := c.driver.(driverpkg.DeviceInfoDriver)
   192  		if !ok {
   193  			return nil, fmt.Errorf("device extraction is not supported for %s: %w", fp, ErrNotSupported)
   194  		}
   195  
   196  		// character and block devices merely need to recover the
   197  		// major/minor device number.
   198  		major, minor, err := deviceDriver.DeviceInfo(fi)
   199  		if err != nil {
   200  			return nil, err
   201  		}
   202  
   203  		return newDevice(*base, base.paths, major, minor)
   204  	}
   205  
   206  	return nil, fmt.Errorf("%q (%v) is not supported: %w", fp, fi.Mode(), ErrNotFound)
   207  }
   208  
   209  func (c *context) verifyMetadata(resource, target Resource) error {
   210  	if target.Mode() != resource.Mode() {
   211  		return fmt.Errorf("resource %q has incorrect mode: %v != %v", target.Path(), target.Mode(), resource.Mode())
   212  	}
   213  
   214  	if target.UID() != resource.UID() {
   215  		return fmt.Errorf("unexpected uid for %q: %v != %v", target.Path(), target.UID(), resource.GID())
   216  	}
   217  
   218  	if target.GID() != resource.GID() {
   219  		return fmt.Errorf("unexpected gid for %q: %v != %v", target.Path(), target.GID(), target.GID())
   220  	}
   221  
   222  	if xattrer, ok := resource.(XAttrer); ok {
   223  		txattrer, tok := target.(XAttrer)
   224  		if !tok {
   225  			return fmt.Errorf("resource %q has xattrs but target does not support them", resource.Path())
   226  		}
   227  
   228  		// For xattrs, only ensure that we have those defined in the resource
   229  		// and their values match. We can ignore other xattrs. In other words,
   230  		// we only verify that target has the subset defined by resource.
   231  		txattrs := txattrer.XAttrs()
   232  		for attr, value := range xattrer.XAttrs() {
   233  			tvalue, ok := txattrs[attr]
   234  			if !ok {
   235  				return fmt.Errorf("resource %q target missing xattr %q", resource.Path(), attr)
   236  			}
   237  
   238  			if !bytes.Equal(value, tvalue) {
   239  				return fmt.Errorf("xattr %q value differs for resource %q", attr, resource.Path())
   240  			}
   241  		}
   242  	}
   243  
   244  	switch r := resource.(type) {
   245  	case RegularFile:
   246  		// TODO(stevvooe): Another reason to use a record-based approach. We
   247  		// have to do another type switch to get this to work. This could be
   248  		// fixed with an Equal function, but let's study this a little more to
   249  		// be sure.
   250  		t, ok := target.(RegularFile)
   251  		if !ok {
   252  			return fmt.Errorf("resource %q target not a regular file", r.Path())
   253  		}
   254  
   255  		if t.Size() != r.Size() {
   256  			return fmt.Errorf("resource %q target has incorrect size: %v != %v", t.Path(), t.Size(), r.Size())
   257  		}
   258  	case Directory:
   259  		t, ok := target.(Directory)
   260  		if !ok {
   261  			return fmt.Errorf("resource %q target not a directory", t.Path())
   262  		}
   263  	case SymLink:
   264  		t, ok := target.(SymLink)
   265  		if !ok {
   266  			return fmt.Errorf("resource %q target not a symlink", t.Path())
   267  		}
   268  
   269  		if t.Target() != r.Target() {
   270  			return fmt.Errorf("resource %q target has mismatched target: %q != %q", t.Path(), t.Target(), r.Target())
   271  		}
   272  	case Device:
   273  		t, ok := target.(Device)
   274  		if !ok {
   275  			return fmt.Errorf("resource %q is not a device", t.Path())
   276  		}
   277  
   278  		if t.Major() != r.Major() || t.Minor() != r.Minor() {
   279  			return fmt.Errorf("resource %q has mismatched major/minor numbers: %d,%d != %d,%d", t.Path(), t.Major(), t.Minor(), r.Major(), r.Minor())
   280  		}
   281  	case NamedPipe:
   282  		t, ok := target.(NamedPipe)
   283  		if !ok {
   284  			return fmt.Errorf("resource %q is not a named pipe", t.Path())
   285  		}
   286  	default:
   287  		return fmt.Errorf("cannot verify resource: %v", resource)
   288  	}
   289  
   290  	return nil
   291  }
   292  
   293  // Verify the resource in the context. An error will be returned a discrepancy
   294  // is found.
   295  func (c *context) Verify(resource Resource) error {
   296  	fp, err := c.fullpath(resource.Path())
   297  	if err != nil {
   298  		return err
   299  	}
   300  
   301  	fi, err := c.driver.Lstat(fp)
   302  	if err != nil {
   303  		return err
   304  	}
   305  
   306  	target, err := c.Resource(resource.Path(), fi)
   307  	if err != nil {
   308  		return err
   309  	}
   310  
   311  	if target.Path() != resource.Path() {
   312  		return fmt.Errorf("resource paths do not match: %q != %q", target.Path(), resource.Path())
   313  	}
   314  
   315  	if err := c.verifyMetadata(resource, target); err != nil {
   316  		return err
   317  	}
   318  
   319  	if h, isHardlinkable := resource.(Hardlinkable); isHardlinkable {
   320  		hardlinkKey, err := newHardlinkKey(fi)
   321  		if err == errNotAHardLink {
   322  			if len(h.Paths()) > 1 {
   323  				return fmt.Errorf("%q is not a hardlink to %q", h.Paths()[1], resource.Path())
   324  			}
   325  		} else if err != nil {
   326  			return err
   327  		}
   328  
   329  		for _, path := range h.Paths()[1:] {
   330  			fpLink, err := c.fullpath(path)
   331  			if err != nil {
   332  				return err
   333  			}
   334  
   335  			fiLink, err := c.driver.Lstat(fpLink)
   336  			if err != nil {
   337  				return err
   338  			}
   339  
   340  			targetLink, err := c.Resource(path, fiLink)
   341  			if err != nil {
   342  				return err
   343  			}
   344  
   345  			hardlinkKeyLink, err := newHardlinkKey(fiLink)
   346  			if err != nil {
   347  				return err
   348  			}
   349  
   350  			if hardlinkKeyLink != hardlinkKey {
   351  				return fmt.Errorf("%q is not a hardlink to %q", path, resource.Path())
   352  			}
   353  
   354  			if err := c.verifyMetadata(resource, targetLink); err != nil {
   355  				return err
   356  			}
   357  		}
   358  	}
   359  
   360  	switch r := resource.(type) {
   361  	case RegularFile:
   362  		t, ok := target.(RegularFile)
   363  		if !ok {
   364  			return fmt.Errorf("resource %q target not a regular file", r.Path())
   365  		}
   366  
   367  		// TODO(stevvooe): This may need to get a little more sophisticated
   368  		// for digest comparison. We may want to actually calculate the
   369  		// provided digests, rather than the implementations having an
   370  		// overlap.
   371  		if !digestsMatch(t.Digests(), r.Digests()) {
   372  			return fmt.Errorf("digests for resource %q do not match: %v != %v", t.Path(), t.Digests(), r.Digests())
   373  		}
   374  	}
   375  
   376  	return nil
   377  }
   378  
   379  func (c *context) checkoutFile(fp string, rf RegularFile) error {
   380  	if c.provider == nil {
   381  		return fmt.Errorf("no file provider")
   382  	}
   383  	var (
   384  		r   io.ReadCloser
   385  		err error
   386  	)
   387  	for _, dgst := range rf.Digests() {
   388  		r, err = c.provider.Reader(dgst)
   389  		if err == nil {
   390  			break
   391  		}
   392  	}
   393  	if err != nil {
   394  		return fmt.Errorf("file content could not be provided: %w", err)
   395  	}
   396  	defer r.Close()
   397  
   398  	return atomicWriteFile(fp, r, rf.Size(), rf.Mode())
   399  }
   400  
   401  // Apply the resource to the contexts. An error will be returned if the
   402  // operation fails. Depending on the resource type, the resource may be
   403  // created. For resource that cannot be resolved, an error will be returned.
   404  func (c *context) Apply(resource Resource) error {
   405  	fp, err := c.fullpath(resource.Path())
   406  	if err != nil {
   407  		return err
   408  	}
   409  
   410  	if !strings.HasPrefix(fp, c.root) {
   411  		return fmt.Errorf("resource %v escapes root", resource)
   412  	}
   413  
   414  	chmod := true
   415  	fi, err := c.driver.Lstat(fp)
   416  	if err != nil {
   417  		if !os.IsNotExist(err) {
   418  			return err
   419  		}
   420  	}
   421  
   422  	switch r := resource.(type) {
   423  	case RegularFile:
   424  		if fi == nil {
   425  			if err := c.checkoutFile(fp, r); err != nil {
   426  				return fmt.Errorf("error checking out file %q: %w", resource.Path(), err)
   427  			}
   428  			chmod = false
   429  		} else {
   430  			if !fi.Mode().IsRegular() {
   431  				return fmt.Errorf("file %q should be a regular file, but is not", resource.Path())
   432  			}
   433  			if fi.Size() != r.Size() {
   434  				if err := c.checkoutFile(fp, r); err != nil {
   435  					return fmt.Errorf("error checking out file %q: %w", resource.Path(), err)
   436  				}
   437  			} else {
   438  				for _, dgst := range r.Digests() {
   439  					f, err := os.Open(fp)
   440  					if err != nil {
   441  						return fmt.Errorf("failure opening file for read %q: %w", resource.Path(), err)
   442  					}
   443  					compared, err := dgst.Algorithm().FromReader(f)
   444  					if err == nil && dgst != compared {
   445  						if err := c.checkoutFile(fp, r); err != nil {
   446  							return fmt.Errorf("error checking out file %q: %w", resource.Path(), err)
   447  						}
   448  						break
   449  					}
   450  					if err1 := f.Close(); err == nil {
   451  						err = err1
   452  					}
   453  					if err != nil {
   454  						return fmt.Errorf("error checking digest for %q: %w", resource.Path(), err)
   455  					}
   456  				}
   457  			}
   458  		}
   459  	case Directory:
   460  		if fi == nil {
   461  			if err := c.driver.Mkdir(fp, resource.Mode()); err != nil {
   462  				return err
   463  			}
   464  		} else if !fi.Mode().IsDir() {
   465  			return fmt.Errorf("%q should be a directory, but is not", resource.Path())
   466  		}
   467  
   468  	case SymLink:
   469  		var target string // only possibly set if target resource is a symlink
   470  
   471  		if fi != nil {
   472  			if fi.Mode()&os.ModeSymlink != 0 {
   473  				target, err = c.driver.Readlink(fp)
   474  				if err != nil {
   475  					return err
   476  				}
   477  			}
   478  		}
   479  
   480  		if target != r.Target() {
   481  			if fi != nil {
   482  				if err := c.driver.Remove(fp); err != nil { // RemoveAll in case of directory?
   483  					return err
   484  				}
   485  			}
   486  
   487  			if err := c.driver.Symlink(r.Target(), fp); err != nil {
   488  				return err
   489  			}
   490  		}
   491  
   492  	case Device:
   493  		if fi == nil {
   494  			if err := c.driver.Mknod(fp, resource.Mode(), int(r.Major()), int(r.Minor())); err != nil {
   495  				return err
   496  			}
   497  		} else if (fi.Mode() & os.ModeDevice) == 0 {
   498  			return fmt.Errorf("%q should be a device, but is not", resource.Path())
   499  		} else {
   500  			major, minor, err := devices.DeviceInfo(fi)
   501  			if err != nil {
   502  				return err
   503  			}
   504  			if major != r.Major() || minor != r.Minor() {
   505  				if err := c.driver.Remove(fp); err != nil {
   506  					return err
   507  				}
   508  
   509  				if err := c.driver.Mknod(fp, resource.Mode(), int(r.Major()), int(r.Minor())); err != nil {
   510  					return err
   511  				}
   512  			}
   513  		}
   514  
   515  	case NamedPipe:
   516  		if fi == nil {
   517  			if err := c.driver.Mkfifo(fp, resource.Mode()); err != nil {
   518  				return err
   519  			}
   520  		} else if (fi.Mode() & os.ModeNamedPipe) == 0 {
   521  			return fmt.Errorf("%q should be a named pipe, but is not", resource.Path())
   522  		}
   523  	}
   524  
   525  	if h, isHardlinkable := resource.(Hardlinkable); isHardlinkable {
   526  		for _, path := range h.Paths() {
   527  			if path == resource.Path() {
   528  				continue
   529  			}
   530  
   531  			lp, err := c.fullpath(path)
   532  			if err != nil {
   533  				return err
   534  			}
   535  
   536  			if _, fi := c.driver.Lstat(lp); fi == nil {
   537  				c.driver.Remove(lp)
   538  			}
   539  			if err := c.driver.Link(fp, lp); err != nil {
   540  				return err
   541  			}
   542  		}
   543  	}
   544  
   545  	// Update filemode if file was not created
   546  	if chmod {
   547  		if err := c.driver.Lchmod(fp, resource.Mode()); err != nil {
   548  			return err
   549  		}
   550  	}
   551  
   552  	if err := c.driver.Lchown(fp, resource.UID(), resource.GID()); err != nil {
   553  		return err
   554  	}
   555  
   556  	if xattrer, ok := resource.(XAttrer); ok {
   557  		// For xattrs, only ensure that we have those defined in the resource
   558  		// and their values are set. We can ignore other xattrs. In other words,
   559  		// we only set xattres defined by resource but never remove.
   560  
   561  		if _, ok := resource.(SymLink); ok {
   562  			lxattrDriver, ok := c.driver.(driverpkg.LXAttrDriver)
   563  			if !ok {
   564  				return fmt.Errorf("unsupported symlink xattr for resource %q", resource.Path())
   565  			}
   566  			if err := lxattrDriver.LSetxattr(fp, xattrer.XAttrs()); err != nil {
   567  				return err
   568  			}
   569  		} else {
   570  			xattrDriver, ok := c.driver.(driverpkg.XAttrDriver)
   571  			if !ok {
   572  				return fmt.Errorf("unsupported xattr for resource %q", resource.Path())
   573  			}
   574  			if err := xattrDriver.Setxattr(fp, xattrer.XAttrs()); err != nil {
   575  				return err
   576  			}
   577  		}
   578  	}
   579  
   580  	return nil
   581  }
   582  
   583  // Walk provides a convenience function to call filepath.Walk correctly for
   584  // the context. Otherwise identical to filepath.Walk, the path argument is
   585  // corrected to be contained within the context.
   586  func (c *context) Walk(fn filepath.WalkFunc) error {
   587  	root := c.root
   588  	fi, err := c.driver.Lstat(c.root)
   589  	if err == nil && fi.Mode()&os.ModeSymlink != 0 {
   590  		root, err = c.driver.Readlink(c.root)
   591  		if err != nil {
   592  			return err
   593  		}
   594  	}
   595  	return c.pathDriver.Walk(root, func(p string, fi os.FileInfo, _ error) error {
   596  		contained, err := c.containWithRoot(p, root)
   597  		return fn(contained, fi, err)
   598  	})
   599  }
   600  
   601  // fullpath returns the system path for the resource, joined with the context
   602  // root. The path p must be a part of the context.
   603  func (c *context) fullpath(p string) (string, error) {
   604  	p = c.pathDriver.Join(c.root, p)
   605  	if !strings.HasPrefix(p, c.root) {
   606  		return "", fmt.Errorf("invalid context path")
   607  	}
   608  
   609  	return p, nil
   610  }
   611  
   612  // containWithRoot cleans and santizes the filesystem path p to be an absolute path,
   613  // effectively relative to the passed root. Extra care should be used when calling this
   614  // instead of contain. This is needed for Walk, as if context root is a symlink,
   615  // it must be evaluated prior to the Walk
   616  func (c *context) containWithRoot(p string, root string) (string, error) {
   617  	sanitized, err := c.pathDriver.Rel(root, p)
   618  	if err != nil {
   619  		return "", err
   620  	}
   621  
   622  	// ZOMBIES(stevvooe): In certain cases, we may want to remap these to a
   623  	// "containment error", so the caller can decide what to do.
   624  	return c.pathDriver.Join("/", c.pathDriver.Clean(sanitized)), nil
   625  }
   626  
   627  // digest returns the digest of the file at path p, relative to the root.
   628  func (c *context) digest(p string) (digest.Digest, error) {
   629  	f, err := c.driver.Open(c.pathDriver.Join(c.root, p))
   630  	if err != nil {
   631  		return "", err
   632  	}
   633  	defer f.Close()
   634  
   635  	return c.digester.Digest(f)
   636  }
   637  
   638  // resolveXAttrs attempts to resolve the extended attributes for the resource
   639  // at the path fp, which is the full path to the resource. If the resource
   640  // cannot have xattrs, nil will be returned.
   641  func (c *context) resolveXAttrs(fp string, fi os.FileInfo, base *resource) (map[string][]byte, error) {
   642  	if fi.Mode().IsRegular() || fi.Mode().IsDir() {
   643  		xattrDriver, ok := c.driver.(driverpkg.XAttrDriver)
   644  		if !ok {
   645  			return nil, fmt.Errorf("xattr extraction is not supported: %w", ErrNotSupported)
   646  		}
   647  
   648  		return xattrDriver.Getxattr(fp)
   649  	}
   650  
   651  	if fi.Mode()&os.ModeSymlink != 0 {
   652  		lxattrDriver, ok := c.driver.(driverpkg.LXAttrDriver)
   653  		if !ok {
   654  			return nil, fmt.Errorf("xattr extraction for symlinks is not supported: %w", ErrNotSupported)
   655  		}
   656  
   657  		return lxattrDriver.LGetxattr(fp)
   658  	}
   659  
   660  	return nil, nil
   661  }
   662  

View as plain text