579 lines
13 KiB
Go
579 lines
13 KiB
Go
|
package continuity
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"os"
|
||
|
"reflect"
|
||
|
"sort"
|
||
|
|
||
|
"github.com/opencontainers/go-digest"
|
||
|
pb "github.com/stevvooe/continuity/proto"
|
||
|
)
|
||
|
|
||
|
// TODO(stevvooe): A record based model, somewhat sketched out at the bottom
|
||
|
// of this file, will be more flexible. Another possibly is to tie the package
|
||
|
// interface directly to the protobuf type. This will have efficiency
|
||
|
// advantages at the cost coupling the nasty codegen types to the exported
|
||
|
// interface.
|
||
|
|
||
|
type Resource interface {
|
||
|
// Path provides the primary resource path relative to the bundle root. In
|
||
|
// cases where resources have more than one path, such as with hard links,
|
||
|
// this will return the primary path, which is often just the first entry.
|
||
|
Path() string
|
||
|
|
||
|
// Mode returns the
|
||
|
Mode() os.FileMode
|
||
|
|
||
|
UID() string
|
||
|
GID() string
|
||
|
}
|
||
|
|
||
|
// ByPath provides the canonical sort order for a set of resources. Use with
|
||
|
// sort.Stable for deterministic sorting.
|
||
|
type ByPath []Resource
|
||
|
|
||
|
func (bp ByPath) Len() int { return len(bp) }
|
||
|
func (bp ByPath) Swap(i, j int) { bp[i], bp[j] = bp[j], bp[i] }
|
||
|
func (bp ByPath) Less(i, j int) bool { return bp[i].Path() < bp[j].Path() }
|
||
|
|
||
|
type XAttrer interface {
|
||
|
XAttrs() map[string][]byte
|
||
|
}
|
||
|
|
||
|
// Hardlinkable is an interface that a resource type satisfies if it can be a
|
||
|
// hardlink target.
|
||
|
type Hardlinkable interface {
|
||
|
// Paths returns all paths of the resource, including the primary path
|
||
|
// returned by Resource.Path. If len(Paths()) > 1, the resource is a hard
|
||
|
// link.
|
||
|
Paths() []string
|
||
|
}
|
||
|
|
||
|
type RegularFile interface {
|
||
|
Resource
|
||
|
XAttrer
|
||
|
Hardlinkable
|
||
|
|
||
|
Size() int64
|
||
|
Digests() []digest.Digest
|
||
|
}
|
||
|
|
||
|
// Merge two or more Resources into new file. Typically, this should be
|
||
|
// used to merge regular files as hardlinks. If the files are not identical,
|
||
|
// other than Paths and Digests, the merge will fail and an error will be
|
||
|
// returned.
|
||
|
func Merge(fs ...Resource) (Resource, error) {
|
||
|
if len(fs) < 1 {
|
||
|
return nil, fmt.Errorf("please provide a resource to merge")
|
||
|
}
|
||
|
|
||
|
if len(fs) == 1 {
|
||
|
return fs[0], nil
|
||
|
}
|
||
|
|
||
|
var paths []string
|
||
|
var digests []digest.Digest
|
||
|
bypath := map[string][]Resource{}
|
||
|
|
||
|
// The attributes are all compared against the first to make sure they
|
||
|
// agree before adding to the above collections. If any of these don't
|
||
|
// correctly validate, the merge fails.
|
||
|
prototype := fs[0]
|
||
|
xattrs := make(map[string][]byte)
|
||
|
|
||
|
// initialize xattrs for use below. All files must have same xattrs.
|
||
|
if prototypeXAttrer, ok := prototype.(XAttrer); ok {
|
||
|
for attr, value := range prototypeXAttrer.XAttrs() {
|
||
|
xattrs[attr] = value
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for _, f := range fs {
|
||
|
h, isHardlinkable := f.(Hardlinkable)
|
||
|
if !isHardlinkable {
|
||
|
return nil, errNotAHardLink
|
||
|
}
|
||
|
|
||
|
if f.Mode() != prototype.Mode() {
|
||
|
return nil, fmt.Errorf("modes do not match: %v != %v", f.Mode(), prototype.Mode())
|
||
|
}
|
||
|
|
||
|
if f.UID() != prototype.UID() {
|
||
|
return nil, fmt.Errorf("uid does not match: %v != %v", f.UID(), prototype.UID())
|
||
|
}
|
||
|
|
||
|
if f.GID() != prototype.GID() {
|
||
|
return nil, fmt.Errorf("gid does not match: %v != %v", f.GID(), prototype.GID())
|
||
|
}
|
||
|
|
||
|
if xattrer, ok := f.(XAttrer); ok {
|
||
|
fxattrs := xattrer.XAttrs()
|
||
|
if !reflect.DeepEqual(fxattrs, xattrs) {
|
||
|
return nil, fmt.Errorf("resource %q xattrs do not match: %v != %v", fxattrs, xattrs)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for _, p := range h.Paths() {
|
||
|
pfs, ok := bypath[p]
|
||
|
if !ok {
|
||
|
// ensure paths are unique by only appending on a new path.
|
||
|
paths = append(paths, p)
|
||
|
}
|
||
|
|
||
|
bypath[p] = append(pfs, f)
|
||
|
}
|
||
|
|
||
|
if regFile, isRegFile := f.(RegularFile); isRegFile {
|
||
|
prototypeRegFile, prototypeIsRegFile := prototype.(RegularFile)
|
||
|
if !prototypeIsRegFile {
|
||
|
return nil, errors.New("prototype is not a regular file")
|
||
|
}
|
||
|
|
||
|
if regFile.Size() != prototypeRegFile.Size() {
|
||
|
return nil, fmt.Errorf("size does not match: %v != %v", regFile.Size(), prototypeRegFile.Size())
|
||
|
}
|
||
|
|
||
|
digests = append(digests, regFile.Digests()...)
|
||
|
} else if device, isDevice := f.(Device); isDevice {
|
||
|
prototypeDevice, prototypeIsDevice := prototype.(Device)
|
||
|
if !prototypeIsDevice {
|
||
|
return nil, errors.New("prototype is not a device")
|
||
|
}
|
||
|
|
||
|
if device.Major() != prototypeDevice.Major() {
|
||
|
return nil, fmt.Errorf("major number does not match: %v != %v", device.Major(), prototypeDevice.Major())
|
||
|
}
|
||
|
if device.Minor() != prototypeDevice.Minor() {
|
||
|
return nil, fmt.Errorf("minor number does not match: %v != %v", device.Minor(), prototypeDevice.Minor())
|
||
|
}
|
||
|
} else if _, isNamedPipe := f.(NamedPipe); isNamedPipe {
|
||
|
_, prototypeIsNamedPipe := prototype.(NamedPipe)
|
||
|
if !prototypeIsNamedPipe {
|
||
|
return nil, errors.New("prototype is not a named pipe")
|
||
|
}
|
||
|
} else {
|
||
|
return nil, errNotAHardLink
|
||
|
}
|
||
|
}
|
||
|
|
||
|
sort.Stable(sort.StringSlice(paths))
|
||
|
|
||
|
// Choose a "canonical" file. Really, it is just the first file to sort
|
||
|
// against. We also effectively select the very first digest as the
|
||
|
// "canonical" one for this file.
|
||
|
first := bypath[paths[0]][0]
|
||
|
|
||
|
resource := resource{
|
||
|
paths: paths,
|
||
|
mode: first.Mode(),
|
||
|
uid: first.UID(),
|
||
|
gid: first.GID(),
|
||
|
xattrs: xattrs,
|
||
|
}
|
||
|
|
||
|
switch typedF := first.(type) {
|
||
|
case RegularFile:
|
||
|
var err error
|
||
|
digests, err = uniqifyDigests(digests...)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return ®ularFile{
|
||
|
resource: resource,
|
||
|
size: typedF.Size(),
|
||
|
digests: digests,
|
||
|
}, nil
|
||
|
case Device:
|
||
|
return &device{
|
||
|
resource: resource,
|
||
|
major: typedF.Major(),
|
||
|
minor: typedF.Minor(),
|
||
|
}, nil
|
||
|
|
||
|
case NamedPipe:
|
||
|
return &namedPipe{
|
||
|
resource: resource,
|
||
|
}, nil
|
||
|
|
||
|
default:
|
||
|
return nil, errNotAHardLink
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type Directory interface {
|
||
|
Resource
|
||
|
XAttrer
|
||
|
|
||
|
// Directory is a no-op method to identify directory objects by interface.
|
||
|
Directory()
|
||
|
}
|
||
|
|
||
|
type SymLink interface {
|
||
|
Resource
|
||
|
|
||
|
// Target returns the target of the symlink contained in the .
|
||
|
Target() string
|
||
|
}
|
||
|
|
||
|
type NamedPipe interface {
|
||
|
Resource
|
||
|
Hardlinkable
|
||
|
XAttrer
|
||
|
|
||
|
// Pipe is a no-op method to allow consistent resolution of NamedPipe
|
||
|
// interface.
|
||
|
Pipe()
|
||
|
}
|
||
|
|
||
|
type Device interface {
|
||
|
Resource
|
||
|
Hardlinkable
|
||
|
XAttrer
|
||
|
|
||
|
Major() uint64
|
||
|
Minor() uint64
|
||
|
}
|
||
|
|
||
|
type resource struct {
|
||
|
paths []string
|
||
|
mode os.FileMode
|
||
|
uid, gid string
|
||
|
xattrs map[string][]byte
|
||
|
}
|
||
|
|
||
|
var _ Resource = &resource{}
|
||
|
|
||
|
// newBaseResource returns a *resource, populated with data from p and fi,
|
||
|
// where p will be populated directly.
|
||
|
func newBaseResource(p string, mode os.FileMode, uid, gid string) (*resource, error) {
|
||
|
return &resource{
|
||
|
paths: []string{p},
|
||
|
mode: mode,
|
||
|
|
||
|
uid: uid,
|
||
|
gid: gid,
|
||
|
|
||
|
// NOTE(stevvooe): Population of shared xattrs field is deferred to
|
||
|
// the resource types that populate it. Since they are a property of
|
||
|
// the context, they must set there.
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
func (r *resource) Path() string {
|
||
|
if len(r.paths) < 1 {
|
||
|
return ""
|
||
|
}
|
||
|
|
||
|
return r.paths[0]
|
||
|
}
|
||
|
|
||
|
func (r *resource) Mode() os.FileMode {
|
||
|
return r.mode
|
||
|
}
|
||
|
|
||
|
func (r *resource) UID() string {
|
||
|
return r.uid
|
||
|
}
|
||
|
|
||
|
func (r *resource) GID() string {
|
||
|
return r.gid
|
||
|
}
|
||
|
|
||
|
type regularFile struct {
|
||
|
resource
|
||
|
size int64
|
||
|
digests []digest.Digest
|
||
|
}
|
||
|
|
||
|
var _ RegularFile = ®ularFile{}
|
||
|
|
||
|
// newRegularFile returns the RegularFile, using the populated base resource
|
||
|
// and one or more digests of the content.
|
||
|
func newRegularFile(base resource, paths []string, size int64, dgsts ...digest.Digest) (RegularFile, error) {
|
||
|
if !base.Mode().IsRegular() {
|
||
|
return nil, fmt.Errorf("not a regular file")
|
||
|
}
|
||
|
|
||
|
base.paths = make([]string, len(paths))
|
||
|
copy(base.paths, paths)
|
||
|
|
||
|
// make our own copy of digests
|
||
|
ds := make([]digest.Digest, len(dgsts))
|
||
|
copy(ds, dgsts)
|
||
|
|
||
|
return ®ularFile{
|
||
|
resource: base,
|
||
|
size: size,
|
||
|
digests: ds,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
func (rf *regularFile) Paths() []string {
|
||
|
paths := make([]string, len(rf.paths))
|
||
|
copy(paths, rf.paths)
|
||
|
return paths
|
||
|
}
|
||
|
|
||
|
func (rf *regularFile) Size() int64 {
|
||
|
return rf.size
|
||
|
}
|
||
|
|
||
|
func (rf *regularFile) Digests() []digest.Digest {
|
||
|
digests := make([]digest.Digest, len(rf.digests))
|
||
|
copy(digests, rf.digests)
|
||
|
return digests
|
||
|
}
|
||
|
|
||
|
func (rf *regularFile) XAttrs() map[string][]byte {
|
||
|
xattrs := make(map[string][]byte, len(rf.xattrs))
|
||
|
|
||
|
for attr, value := range rf.xattrs {
|
||
|
xattrs[attr] = append(xattrs[attr], value...)
|
||
|
}
|
||
|
|
||
|
return xattrs
|
||
|
}
|
||
|
|
||
|
type directory struct {
|
||
|
resource
|
||
|
}
|
||
|
|
||
|
var _ Directory = &directory{}
|
||
|
|
||
|
func newDirectory(base resource) (Directory, error) {
|
||
|
if !base.Mode().IsDir() {
|
||
|
return nil, fmt.Errorf("not a directory")
|
||
|
}
|
||
|
|
||
|
return &directory{
|
||
|
resource: base,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
func (d *directory) Directory() {}
|
||
|
|
||
|
func (d *directory) XAttrs() map[string][]byte {
|
||
|
xattrs := make(map[string][]byte, len(d.xattrs))
|
||
|
|
||
|
for attr, value := range d.xattrs {
|
||
|
xattrs[attr] = append(xattrs[attr], value...)
|
||
|
}
|
||
|
|
||
|
return xattrs
|
||
|
}
|
||
|
|
||
|
type symLink struct {
|
||
|
resource
|
||
|
target string
|
||
|
}
|
||
|
|
||
|
var _ SymLink = &symLink{}
|
||
|
|
||
|
func newSymLink(base resource, target string) (SymLink, error) {
|
||
|
if base.Mode()&os.ModeSymlink == 0 {
|
||
|
return nil, fmt.Errorf("not a symlink")
|
||
|
}
|
||
|
|
||
|
return &symLink{
|
||
|
resource: base,
|
||
|
target: target,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
func (l *symLink) Target() string {
|
||
|
return l.target
|
||
|
}
|
||
|
|
||
|
type namedPipe struct {
|
||
|
resource
|
||
|
}
|
||
|
|
||
|
var _ NamedPipe = &namedPipe{}
|
||
|
|
||
|
func newNamedPipe(base resource, paths []string) (NamedPipe, error) {
|
||
|
if base.Mode()&os.ModeNamedPipe == 0 {
|
||
|
return nil, fmt.Errorf("not a namedpipe")
|
||
|
}
|
||
|
|
||
|
base.paths = make([]string, len(paths))
|
||
|
copy(base.paths, paths)
|
||
|
|
||
|
return &namedPipe{
|
||
|
resource: base,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
func (np *namedPipe) Pipe() {}
|
||
|
|
||
|
func (np *namedPipe) Paths() []string {
|
||
|
paths := make([]string, len(np.paths))
|
||
|
copy(paths, np.paths)
|
||
|
return paths
|
||
|
}
|
||
|
|
||
|
func (np *namedPipe) XAttrs() map[string][]byte {
|
||
|
xattrs := make(map[string][]byte, len(np.xattrs))
|
||
|
|
||
|
for attr, value := range np.xattrs {
|
||
|
xattrs[attr] = append(xattrs[attr], value...)
|
||
|
}
|
||
|
|
||
|
return xattrs
|
||
|
}
|
||
|
|
||
|
type device struct {
|
||
|
resource
|
||
|
major, minor uint64
|
||
|
}
|
||
|
|
||
|
var _ Device = &device{}
|
||
|
|
||
|
func newDevice(base resource, paths []string, major, minor uint64) (Device, error) {
|
||
|
if base.Mode()&os.ModeDevice == 0 {
|
||
|
return nil, fmt.Errorf("not a device")
|
||
|
}
|
||
|
|
||
|
base.paths = make([]string, len(paths))
|
||
|
copy(base.paths, paths)
|
||
|
|
||
|
return &device{
|
||
|
resource: base,
|
||
|
major: major,
|
||
|
minor: minor,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
func (d *device) Paths() []string {
|
||
|
paths := make([]string, len(d.paths))
|
||
|
copy(paths, d.paths)
|
||
|
return paths
|
||
|
}
|
||
|
|
||
|
func (d *device) XAttrs() map[string][]byte {
|
||
|
xattrs := make(map[string][]byte, len(d.xattrs))
|
||
|
|
||
|
for attr, value := range d.xattrs {
|
||
|
xattrs[attr] = append(xattrs[attr], value...)
|
||
|
}
|
||
|
|
||
|
return xattrs
|
||
|
}
|
||
|
|
||
|
func (d device) Major() uint64 {
|
||
|
return d.major
|
||
|
}
|
||
|
|
||
|
func (d device) Minor() uint64 {
|
||
|
return d.minor
|
||
|
}
|
||
|
|
||
|
// toProto converts a resource to a protobuf record. We'd like to push this
|
||
|
// the individual types but we want to keep this all together during
|
||
|
// prototyping.
|
||
|
func toProto(resource Resource) *pb.Resource {
|
||
|
b := &pb.Resource{
|
||
|
Path: []string{resource.Path()},
|
||
|
Mode: uint32(resource.Mode()),
|
||
|
Uid: resource.UID(),
|
||
|
Gid: resource.GID(),
|
||
|
}
|
||
|
|
||
|
if xattrer, ok := resource.(XAttrer); ok {
|
||
|
b.Xattr = xattrer.XAttrs()
|
||
|
}
|
||
|
|
||
|
switch r := resource.(type) {
|
||
|
case RegularFile:
|
||
|
b.Path = r.Paths()
|
||
|
b.Size = uint64(r.Size())
|
||
|
|
||
|
for _, dgst := range r.Digests() {
|
||
|
b.Digest = append(b.Digest, dgst.String())
|
||
|
}
|
||
|
case SymLink:
|
||
|
b.Target = r.Target()
|
||
|
case Device:
|
||
|
b.Major, b.Minor = r.Major(), r.Minor()
|
||
|
b.Path = r.Paths()
|
||
|
case NamedPipe:
|
||
|
b.Path = r.Paths()
|
||
|
}
|
||
|
|
||
|
// enforce a few stability guarantees that may not be provided by the
|
||
|
// resource implementation.
|
||
|
sort.Strings(b.Path)
|
||
|
|
||
|
return b
|
||
|
}
|
||
|
|
||
|
// fromProto converts from a protobuf Resource to a Resource interface.
|
||
|
func fromProto(b *pb.Resource) (Resource, error) {
|
||
|
base, err := newBaseResource(b.Path[0], os.FileMode(b.Mode), b.Uid, b.Gid)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
base.xattrs = make(map[string][]byte, len(b.Xattr))
|
||
|
|
||
|
for attr, value := range b.Xattr {
|
||
|
base.xattrs[attr] = append(base.xattrs[attr], value...)
|
||
|
}
|
||
|
|
||
|
switch {
|
||
|
case base.Mode().IsRegular():
|
||
|
dgsts := make([]digest.Digest, len(b.Digest))
|
||
|
for i, dgst := range b.Digest {
|
||
|
// TODO(stevvooe): Should we be validating at this point?
|
||
|
dgsts[i] = digest.Digest(dgst)
|
||
|
}
|
||
|
|
||
|
return newRegularFile(*base, b.Path, int64(b.Size), dgsts...)
|
||
|
case base.Mode().IsDir():
|
||
|
return newDirectory(*base)
|
||
|
case base.Mode()&os.ModeSymlink != 0:
|
||
|
return newSymLink(*base, b.Target)
|
||
|
case base.Mode()&os.ModeNamedPipe != 0:
|
||
|
return newNamedPipe(*base, b.Path)
|
||
|
case base.Mode()&os.ModeDevice != 0:
|
||
|
return newDevice(*base, b.Path, b.Major, b.Minor)
|
||
|
}
|
||
|
|
||
|
return nil, fmt.Errorf("unknown resource record (%#v): %s", b, base.Mode())
|
||
|
}
|
||
|
|
||
|
// NOTE(stevvooe): An alternative model that supports inline declaration.
|
||
|
// Convenient for unit testing where inline declarations may be desirable but
|
||
|
// creates an awkward API for the standard use case.
|
||
|
|
||
|
// type ResourceKind int
|
||
|
|
||
|
// const (
|
||
|
// ResourceRegularFile = iota + 1
|
||
|
// ResourceDirectory
|
||
|
// ResourceSymLink
|
||
|
// Resource
|
||
|
// )
|
||
|
|
||
|
// type Resource struct {
|
||
|
// Kind ResourceKind
|
||
|
// Paths []string
|
||
|
// Mode os.FileMode
|
||
|
// UID string
|
||
|
// GID string
|
||
|
// Size int64
|
||
|
// Digests []digest.Digest
|
||
|
// Target string
|
||
|
// Major, Minor int
|
||
|
// XAttrs map[string][]byte
|
||
|
// }
|
||
|
|
||
|
// type RegularFile struct {
|
||
|
// Paths []string
|
||
|
// Size int64
|
||
|
// Digests []digest.Digest
|
||
|
// Perm os.FileMode // os.ModePerm + sticky, setuid, setgid
|
||
|
// }
|