package continuity import ( "bytes" "fmt" "io" "log" "os" "path/filepath" "strings" "syscall" "github.com/opencontainers/go-digest" ) var ( ErrNotFound = fmt.Errorf("not found") ErrNotSupported = fmt.Errorf("not supported") ) // Context represents a file system context for accessing resources. The // responsibility of the context is to convert system specific resources to // generic Resource objects. Most of this is safe path manipulation, as well // as extraction of resource details. type Context interface { Apply(Resource) error Verify(Resource) error Resource(string, os.FileInfo) (Resource, error) Walk(filepath.WalkFunc) error } // SymlinkPath is intended to give the symlink target value // in a root context. Target and linkname are absolute paths // not under the given root. type SymlinkPath func(root, linkname, target string) (string, error) type ContextOptions struct { Digester Digester Driver Driver Provider ContentProvider } // context represents a file system context for accessing resources. // Generally, all path qualified access and system considerations should land // here. type context struct { driver Driver root string digester Digester provider ContentProvider } // NewContext returns a Context associated with root. The default driver will // be used, as returned by NewDriver. func NewContext(root string) (Context, error) { return NewContextWithOptions(root, ContextOptions{}) } // NewContextWithOptions returns a Context associate with the root. func NewContextWithOptions(root string, options ContextOptions) (Context, error) { // normalize to absolute path root, err := filepath.Abs(filepath.Clean(root)) if err != nil { return nil, err } driver := options.Driver if driver == nil { driver, err = NewSystemDriver() if err != nil { return nil, err } } digester := options.Digester if digester == nil { digester = simpleDigester{digest.Canonical} } // Check the root directory. Need to be a little careful here. We are // allowing a link for now, but this may have odd behavior when // canonicalizing paths. As long as all files are opened through the link // path, this should be okay. fi, err := driver.Stat(root) if err != nil { return nil, err } if !fi.IsDir() { return nil, &os.PathError{Op: "NewContext", Path: root, Err: os.ErrInvalid} } return &context{ root: root, driver: driver, digester: digester, provider: options.Provider, }, nil } // Resource returns the resource as path p, populating the entry with info // from fi. The path p should be the path of the resource in the context, // typically obtained through Walk or from the value of Resource.Path(). If fi // is nil, it will be resolved. func (c *context) Resource(p string, fi os.FileInfo) (Resource, error) { fp, err := c.fullpath(p) if err != nil { return nil, err } if fi == nil { fi, err = c.driver.Lstat(fp) if err != nil { return nil, err } } // TODO(stevvooe): This need to be resolved for the container's root, // where here we are really getting the host OS's value. We need to allow // this be passed in and fixed up to make these uid/gid mappings portable. // Either this can be part of the driver or we can achieve it through some // other mechanism. sys, ok := fi.Sys().(*syscall.Stat_t) if !ok { // TODO(stevvooe): This may not be a hard error for all platforms. We // may want to move this to the driver. return nil, fmt.Errorf("unable to resolve syscall.Stat_t from (os.FileInfo).Sys(): %#v", fi) } base, err := newBaseResource(p, fi.Mode(), fmt.Sprint(sys.Uid), fmt.Sprint(sys.Gid)) if err != nil { return nil, err } base.xattrs, err = c.resolveXAttrs(fp, fi, base) if err == ErrNotSupported { log.Printf("resolving xattrs on %s not supported", fp) } else if err != nil { return nil, err } // TODO(stevvooe): Handle windows alternate data streams. if fi.Mode().IsRegular() { dgst, err := c.digest(p) if err != nil { return nil, err } return newRegularFile(*base, base.paths, fi.Size(), dgst) } if fi.Mode().IsDir() { return newDirectory(*base) } if fi.Mode()&os.ModeSymlink != 0 { // We handle relative links vs absolute links by including a // beginning slash for absolute links. Effectively, the bundle's // root is treated as the absolute link anchor. target, err := c.driver.Readlink(fp) if err != nil { return nil, err } return newSymLink(*base, target) } if fi.Mode()&os.ModeNamedPipe != 0 { return newNamedPipe(*base, base.paths) } if fi.Mode()&os.ModeDevice != 0 { deviceDriver, ok := c.driver.(DeviceInfoDriver) if !ok { log.Printf("device extraction not supported %s", fp) return nil, ErrNotSupported } // character and block devices merely need to recover the // major/minor device number. major, minor, err := deviceDriver.DeviceInfo(fi) if err != nil { return nil, err } return newDevice(*base, base.paths, major, minor) } log.Printf("%q (%v) is not supported", fp, fi.Mode()) return nil, ErrNotFound } func (c *context) verifyMetadata(resource, target Resource) error { if target.Mode() != resource.Mode() { return fmt.Errorf("resource %q has incorrect mode: %v != %v", target.Path(), target.Mode(), resource.Mode()) } if target.UID() != resource.UID() { return fmt.Errorf("unexpected uid for %q: %v != %v", target.Path(), target.UID(), resource.GID()) } if target.GID() != resource.GID() { return fmt.Errorf("unexpected gid for %q: %v != %v", target.Path(), target.GID(), target.GID()) } if xattrer, ok := resource.(XAttrer); ok { txattrer, tok := target.(XAttrer) if !tok { return fmt.Errorf("resource %q has xattrs but target does not support them", resource.Path()) } // For xattrs, only ensure that we have those defined in the resource // and their values match. We can ignore other xattrs. In other words, // we only verify that target has the subset defined by resource. txattrs := txattrer.XAttrs() for attr, value := range xattrer.XAttrs() { tvalue, ok := txattrs[attr] if !ok { return fmt.Errorf("resource %q target missing xattr %q", resource.Path(), attr) } if !bytes.Equal(value, tvalue) { return fmt.Errorf("xattr %q value differs for resource %q", attr, resource.Path()) } } } switch r := resource.(type) { case RegularFile: // TODO(stevvooe): Another reason to use a record-based approach. We // have to do another type switch to get this to work. This could be // fixed with an Equal function, but let's study this a little more to // be sure. t, ok := target.(RegularFile) if !ok { return fmt.Errorf("resource %q target not a regular file", r.Path()) } if t.Size() != r.Size() { return fmt.Errorf("resource %q target has incorrect size: %v != %v", t.Path(), t.Size(), r.Size()) } case Directory: t, ok := target.(Directory) if !ok { return fmt.Errorf("resource %q target not a directory", t.Path()) } case SymLink: t, ok := target.(SymLink) if !ok { return fmt.Errorf("resource %q target not a symlink", t.Path()) } if t.Target() != r.Target() { return fmt.Errorf("resource %q target has mismatched target: %q != %q", t.Path(), t.Target(), r.Target()) } case Device: t, ok := target.(Device) if !ok { return fmt.Errorf("resource %q is not a device", t.Path()) } if t.Major() != r.Major() || t.Minor() != r.Minor() { return fmt.Errorf("resource %q has mismatched major/minor numbers: %d,%d != %d,%d", t.Path(), t.Major(), t.Minor(), r.Major(), r.Minor()) } case NamedPipe: t, ok := target.(NamedPipe) if !ok { return fmt.Errorf("resource %q is not a named pipe", t.Path()) } default: return fmt.Errorf("cannot verify resource: %v", resource) } return nil } // Verify the resource in the context. An error will be returned a discrepancy // is found. func (c *context) Verify(resource Resource) error { fp, err := c.fullpath(resource.Path()) if err != nil { return err } fi, err := c.driver.Lstat(fp) if err != nil { return err } target, err := c.Resource(resource.Path(), fi) if err != nil { return err } if target.Path() != resource.Path() { return fmt.Errorf("resource paths do not match: %q != %q", target.Path(), resource.Path()) } if err := c.verifyMetadata(resource, target); err != nil { return err } if h, isHardlinkable := resource.(Hardlinkable); isHardlinkable { hardlinkKey, err := newHardlinkKey(fi) if err == errNotAHardLink { if len(h.Paths()) > 1 { return fmt.Errorf("%q is not a hardlink to %q", h.Paths()[1], resource.Path()) } } else if err != nil { return err } for _, path := range h.Paths()[1:] { fpLink, err := c.fullpath(path) if err != nil { return err } fiLink, err := c.driver.Lstat(fpLink) if err != nil { return err } targetLink, err := c.Resource(path, fiLink) if err != nil { return err } hardlinkKeyLink, err := newHardlinkKey(fiLink) if err != nil { return err } if hardlinkKeyLink != hardlinkKey { return fmt.Errorf("%q is not a hardlink to %q", path, resource.Path()) } if err := c.verifyMetadata(resource, targetLink); err != nil { return err } } } switch r := resource.(type) { case RegularFile: t, ok := target.(RegularFile) if !ok { return fmt.Errorf("resource %q target not a regular file", r.Path()) } // TODO(stevvooe): This may need to get a little more sophisticated // for digest comparison. We may want to actually calculate the // provided digests, rather than the implementations having an // overlap. if !digestsMatch(t.Digests(), r.Digests()) { return fmt.Errorf("digests for resource %q do not match: %v != %v", t.Path(), t.Digests(), r.Digests()) } } return nil } func (c *context) checkoutFile(fp string, rf RegularFile) error { if c.provider == nil { return fmt.Errorf("no file provider") } var ( r io.ReadCloser err error ) for _, dgst := range rf.Digests() { r, err = c.provider.Reader(dgst) if err == nil { break } } if err != nil { return fmt.Errorf("file content could not be provided: %v", err) } defer r.Close() return atomicWriteFile(fp, r, rf) } // Apply the resource to the contexts. An error will be returned if the // operation fails. Depending on the resource type, the resource may be // created. For resource that cannot be resolved, an error will be returned. func (c *context) Apply(resource Resource) error { fp, err := c.fullpath(resource.Path()) if err != nil { return err } if !strings.HasPrefix(fp, c.root) { return fmt.Errorf("resource %v escapes root", resource) } var chmod = true fi, err := c.driver.Lstat(fp) if err != nil { if !os.IsNotExist(err) { return err } } switch r := resource.(type) { case RegularFile: if fi == nil { if err := c.checkoutFile(fp, r); err != nil { return fmt.Errorf("error checking out file %q: %v", resource.Path(), err) } chmod = false } else { if !fi.Mode().IsRegular() { return fmt.Errorf("file %q should be a regular file, but is not", resource.Path()) } if fi.Size() != r.Size() { if err := c.checkoutFile(fp, r); err != nil { return fmt.Errorf("error checking out file %q: %v", resource.Path(), err) } } else { for _, dgst := range r.Digests() { f, err := os.Open(fp) if err != nil { return fmt.Errorf("failure opening file for read %q: %v", resource.Path(), err) } compared, err := dgst.Algorithm().FromReader(f) if err == nil && dgst != compared { if err := c.checkoutFile(fp, r); err != nil { return fmt.Errorf("error checking out file %q: %v", resource.Path(), err) } break } if err1 := f.Close(); err == nil { err = err1 } if err != nil { return fmt.Errorf("error checking digest for %q: %v", resource.Path(), err) } } } } case Directory: if fi == nil { if err := c.driver.Mkdir(fp, resource.Mode()); err != nil { return err } } else if !fi.Mode().IsDir() { return fmt.Errorf("%q should be a directory, but is not", resource.Path()) } case SymLink: var target string // only possibly set if target resource is a symlink if fi != nil { if fi.Mode()&os.ModeSymlink != 0 { target, err = c.driver.Readlink(fp) if err != nil { return err } } } if target != r.Target() { if err := c.driver.Remove(fp); err != nil { // RemoveAll? return err } if err := c.driver.Symlink(r.Target(), fp); err != nil { return err } } // NOTE(stevvooe): Chmod on symlink is not supported on linux. We // may want to maintain support for other platforms that have it. chmod = false case Device: if fi == nil { if err := c.driver.Mknod(fp, resource.Mode(), int(r.Major()), int(r.Minor())); err != nil { return err } } else if (fi.Mode() & os.ModeDevice) == 0 { return fmt.Errorf("%q should be a device, but is not", resource.Path()) } else { major, minor, err := deviceInfo(fi) if err != nil { return err } if major != r.Major() || minor != r.Minor() { if err := c.driver.Remove(fp); err != nil { return err } if err := c.driver.Mknod(fp, resource.Mode(), int(r.Major()), int(r.Minor())); err != nil { return err } } } case NamedPipe: if fi == nil { if err := c.driver.Mkfifo(fp, resource.Mode()); err != nil { return err } } else if (fi.Mode() & os.ModeNamedPipe) == 0 { return fmt.Errorf("%q should be a named pipe, but is not", resource.Path()) } } if h, isHardlinkable := resource.(Hardlinkable); isHardlinkable { for _, path := range h.Paths() { if path == resource.Path() { continue } lp, err := c.fullpath(path) if err != nil { return err } if _, fi := c.driver.Lstat(lp); fi == nil { c.driver.Remove(lp) } if err := c.driver.Link(fp, lp); err != nil { return err } } } // Update filemode if file was not created if chmod { if err := c.driver.Lchmod(fp, resource.Mode()); err != nil { return err } } if err := c.driver.Lchown(fp, resource.UID(), resource.GID()); err != nil { return err } if xattrer, ok := resource.(XAttrer); ok { // For xattrs, only ensure that we have those defined in the resource // and their values are set. We can ignore other xattrs. In other words, // we only set xattres defined by resource but never remove. if _, ok := resource.(SymLink); ok { lxattrDriver, ok := c.driver.(LXAttrDriver) if !ok { return fmt.Errorf("unsupported symlink xattr for resource %q", resource.Path()) } if err := lxattrDriver.LSetxattr(fp, xattrer.XAttrs()); err != nil { return err } } else { xattrDriver, ok := c.driver.(XAttrDriver) if !ok { return fmt.Errorf("unsupported xattr for resource %q", resource.Path()) } if err := xattrDriver.Setxattr(fp, xattrer.XAttrs()); err != nil { return err } } } return nil } // Walk provides a convenience function to call filepath.Walk correctly for // the context. Otherwise identical to filepath.Walk, the path argument is // corrected to be contained within the context. func (c *context) Walk(fn filepath.WalkFunc) error { return filepath.Walk(c.root, func(p string, fi os.FileInfo, err error) error { contained, err := c.contain(p) return fn(contained, fi, err) }) } // fullpath returns the system path for the resource, joined with the context // root. The path p must be a part of the context. func (c *context) fullpath(p string) (string, error) { p = filepath.Join(c.root, p) if !strings.HasPrefix(p, c.root) { return "", fmt.Errorf("invalid context path") } return p, nil } // contain cleans and santizes the filesystem path p to be an absolute path, // effectively relative to the context root. func (c *context) contain(p string) (string, error) { sanitized, err := filepath.Rel(c.root, p) if err != nil { return "", err } // ZOMBIES(stevvooe): In certain cases, we may want to remap these to a // "containment error", so the caller can decide what to do. return filepath.Join("/", filepath.Clean(sanitized)), nil } // digest returns the digest of the file at path p, relative to the root. func (c *context) digest(p string) (digest.Digest, error) { f, err := c.driver.Open(filepath.Join(c.root, p)) if err != nil { return "", err } defer f.Close() return c.digester.Digest(f) } // resolveXAttrs attempts to resolve the extended attributes for the resource // at the path fp, which is the full path to the resource. If the resource // cannot have xattrs, nil will be returned. func (c *context) resolveXAttrs(fp string, fi os.FileInfo, base *resource) (map[string][]byte, error) { if fi.Mode().IsRegular() || fi.Mode().IsDir() { xattrDriver, ok := c.driver.(XAttrDriver) if !ok { log.Println("xattr extraction not supported") return nil, ErrNotSupported } return xattrDriver.Getxattr(fp) } if fi.Mode()&os.ModeSymlink != 0 { lxattrDriver, ok := c.driver.(LXAttrDriver) if !ok { log.Println("xattr extraction for symlinks not supported") return nil, ErrNotSupported } return lxattrDriver.LGetxattr(fp) } return nil, nil }