420 lines
9.5 KiB
Go
420 lines
9.5 KiB
Go
// +build !windows
|
|
|
|
package continuity
|
|
|
|
import (
|
|
"bytes"
|
|
_ "crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"math/rand"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"syscall"
|
|
"testing"
|
|
|
|
"github.com/containerd/continuity/devices"
|
|
"github.com/opencontainers/go-digest"
|
|
)
|
|
|
|
// Hard things:
|
|
// 1. Groups/gid - no standard library support.
|
|
// 2. xattrs - must choose package to provide this.
|
|
// 3. ADS - no clue where to start.
|
|
|
|
func TestWalkFS(t *testing.T) {
|
|
rand.Seed(1)
|
|
|
|
// Testing:
|
|
// 1. Setup different files:
|
|
// - links
|
|
// - sibling directory - relative
|
|
// - sibling directory - absolute
|
|
// - parent directory - absolute
|
|
// - parent directory - relative
|
|
// - illegal links
|
|
// - parent directory - relative, out of root
|
|
// - parent directory - absolute, out of root
|
|
// - regular files
|
|
// - character devices
|
|
// - what about sticky bits?
|
|
// 2. Build the manifest.
|
|
// 3. Verify expected result.
|
|
testResources := []dresource{
|
|
{
|
|
path: "a",
|
|
mode: 0644,
|
|
},
|
|
{
|
|
kind: rhardlink,
|
|
path: "a-hardlink",
|
|
target: "a",
|
|
},
|
|
{
|
|
kind: rdirectory,
|
|
path: "b",
|
|
mode: 0755,
|
|
},
|
|
{
|
|
kind: rhardlink,
|
|
path: "b/a-hardlink",
|
|
target: "a",
|
|
},
|
|
{
|
|
path: "b/a",
|
|
mode: 0600 | os.ModeSticky,
|
|
},
|
|
{
|
|
kind: rdirectory,
|
|
path: "c",
|
|
mode: 0755,
|
|
},
|
|
{
|
|
path: "c/a",
|
|
mode: 0644,
|
|
},
|
|
{
|
|
kind: rrelsymlink,
|
|
path: "c/ca-relsymlink",
|
|
mode: 0600,
|
|
target: "a",
|
|
},
|
|
{
|
|
kind: rrelsymlink,
|
|
path: "c/a-relsymlink",
|
|
mode: 0600,
|
|
target: "../a",
|
|
},
|
|
{
|
|
kind: rabssymlink,
|
|
path: "c/a-abssymlink",
|
|
mode: 0600,
|
|
target: "a",
|
|
},
|
|
// TODO(stevvooe): Make sure we can test this case and get proper
|
|
// errors when it is encountered.
|
|
// {
|
|
// // create a bad symlink and make sure we don't include it.
|
|
// kind: relsymlink,
|
|
// path: "c/a-badsymlink",
|
|
// mode: 0600,
|
|
// target: "../../..",
|
|
// },
|
|
|
|
// TODO(stevvooe): Must add tests for xattrs, with symlinks,
|
|
// directorys and regular files.
|
|
|
|
{
|
|
kind: rnamedpipe,
|
|
path: "fifo",
|
|
mode: 0666 | os.ModeNamedPipe,
|
|
},
|
|
|
|
{
|
|
kind: rdirectory,
|
|
path: "/dev",
|
|
mode: 0755,
|
|
},
|
|
|
|
// NOTE(stevvooe): Below here, we add a few simple character devices.
|
|
// Block devices are untested but should be nearly the same as
|
|
// character devices.
|
|
// devNullResource,
|
|
// devZeroResource,
|
|
}
|
|
|
|
root, err := ioutil.TempDir("", "continuity-test-")
|
|
if err != nil {
|
|
t.Fatalf("error creating temporary directory: %v", err)
|
|
}
|
|
|
|
defer os.RemoveAll(root)
|
|
|
|
generateTestFiles(t, root, testResources)
|
|
|
|
ctx, err := NewContext(root)
|
|
if err != nil {
|
|
t.Fatalf("error getting context: %v", err)
|
|
}
|
|
|
|
m, err := BuildManifest(ctx)
|
|
if err != nil {
|
|
t.Fatalf("error building manifest: %v", err)
|
|
}
|
|
|
|
var b bytes.Buffer
|
|
MarshalText(&b, m)
|
|
t.Log(b.String())
|
|
|
|
// TODO(dmcgowan): always verify, currently hard links not supported
|
|
//if err := VerifyManifest(ctx, m); err != nil {
|
|
// t.Fatalf("error verifying manifest: %v")
|
|
//}
|
|
|
|
expectedResources, err := expectedResourceList(root, testResources)
|
|
if err != nil {
|
|
// TODO(dmcgowan): update function to panic, this would mean test setup error
|
|
t.Fatalf("error creating resource list: %v", err)
|
|
}
|
|
|
|
// Diff resources
|
|
diff := diffResourceList(expectedResources, m.Resources)
|
|
if diff.HasDiff() {
|
|
t.Log("Resource list difference")
|
|
for _, a := range diff.Additions {
|
|
t.Logf("Unexpected resource: %#v", a)
|
|
}
|
|
for _, d := range diff.Deletions {
|
|
t.Logf("Missing resource: %#v", d)
|
|
}
|
|
for _, u := range diff.Updates {
|
|
t.Logf("Changed resource:\n\tExpected: %#v\n\tActual: %#v", u.Original, u.Updated)
|
|
}
|
|
|
|
t.FailNow()
|
|
}
|
|
}
|
|
|
|
// TODO(stevvooe): At this time, we have a nice testing framework to define
|
|
// and build resources. This will likely be a pre-cursor to the packages
|
|
// public interface.
|
|
type kind int
|
|
|
|
func (k kind) String() string {
|
|
switch k {
|
|
case rfile:
|
|
return "file"
|
|
case rdirectory:
|
|
return "directory"
|
|
case rhardlink:
|
|
return "hardlink"
|
|
case rchardev:
|
|
return "chardev"
|
|
case rnamedpipe:
|
|
return "namedpipe"
|
|
}
|
|
|
|
panic(fmt.Sprintf("unknown kind: %v", int(k)))
|
|
}
|
|
|
|
const (
|
|
rfile kind = iota
|
|
rdirectory
|
|
rhardlink
|
|
rrelsymlink
|
|
rabssymlink
|
|
rchardev
|
|
rnamedpipe
|
|
)
|
|
|
|
type dresource struct {
|
|
kind kind
|
|
path string
|
|
mode os.FileMode
|
|
target string // hard/soft link target
|
|
digest digest.Digest
|
|
size int
|
|
uid int64
|
|
gid int64
|
|
major, minor int
|
|
}
|
|
|
|
func generateTestFiles(t *testing.T, root string, resources []dresource) {
|
|
for i, resource := range resources {
|
|
p := filepath.Join(root, resource.path)
|
|
switch resource.kind {
|
|
case rfile:
|
|
size := rand.Intn(4 << 20)
|
|
d := make([]byte, size)
|
|
randomBytes(d)
|
|
dgst := digest.FromBytes(d)
|
|
resources[i].digest = dgst
|
|
resources[i].size = size
|
|
|
|
// this relies on the proper directory parent being defined.
|
|
if err := ioutil.WriteFile(p, d, resource.mode); err != nil {
|
|
t.Fatalf("error writing %q: %v", p, err)
|
|
}
|
|
case rdirectory:
|
|
if err := os.Mkdir(p, resource.mode); err != nil {
|
|
t.Fatalf("error creating directory %q: %v", p, err)
|
|
}
|
|
case rhardlink:
|
|
target := filepath.Join(root, resource.target)
|
|
if err := os.Link(target, p); err != nil {
|
|
t.Fatalf("error creating hardlink: %v", err)
|
|
}
|
|
case rrelsymlink:
|
|
if err := os.Symlink(resource.target, p); err != nil {
|
|
t.Fatalf("error creating symlink: %v", err)
|
|
}
|
|
case rabssymlink:
|
|
// for absolute links, we join with root.
|
|
target := filepath.Join(root, resource.target)
|
|
|
|
if err := os.Symlink(target, p); err != nil {
|
|
t.Fatalf("error creating symlink: %v", err)
|
|
}
|
|
case rchardev, rnamedpipe:
|
|
if err := devices.Mknod(p, resource.mode, resource.major, resource.minor); err != nil {
|
|
t.Fatalf("error creating device %q: %v", p, err)
|
|
}
|
|
default:
|
|
t.Fatalf("unknown resource type: %v", resource.kind)
|
|
}
|
|
|
|
st, err := os.Lstat(p)
|
|
if err != nil {
|
|
t.Fatalf("error statting after creation: %v", err)
|
|
}
|
|
resources[i].uid = int64(st.Sys().(*syscall.Stat_t).Uid)
|
|
resources[i].gid = int64(st.Sys().(*syscall.Stat_t).Gid)
|
|
resources[i].mode = st.Mode()
|
|
|
|
// TODO: Readback and join xattr
|
|
}
|
|
|
|
// log the test root for future debugging
|
|
if err := filepath.Walk(root, func(p string, fi os.FileInfo, err error) error {
|
|
if fi.Mode()&os.ModeSymlink != 0 {
|
|
target, err := os.Readlink(p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
t.Log(fi.Mode(), p, "->", target)
|
|
} else {
|
|
t.Log(fi.Mode(), p)
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatalf("error walking created root: %v", err)
|
|
}
|
|
|
|
var b bytes.Buffer
|
|
if err := tree(&b, root); err != nil {
|
|
t.Fatalf("error running tree: %v", err)
|
|
}
|
|
t.Logf("\n%s", b.String())
|
|
}
|
|
|
|
func randomBytes(p []byte) {
|
|
for i := range p {
|
|
p[i] = byte(rand.Intn(1<<8 - 1))
|
|
}
|
|
}
|
|
|
|
// expectedResourceList sorts the set of resources into the order
|
|
// expected in the manifest and collapses hardlinks
|
|
func expectedResourceList(root string, resources []dresource) ([]Resource, error) {
|
|
resourceMap := map[string]Resource{}
|
|
paths := []string{}
|
|
for _, r := range resources {
|
|
absPath := r.path
|
|
if !filepath.IsAbs(absPath) {
|
|
absPath = "/" + absPath
|
|
}
|
|
switch r.kind {
|
|
case rfile:
|
|
f := ®ularFile{
|
|
resource: resource{
|
|
paths: []string{absPath},
|
|
mode: r.mode,
|
|
uid: r.uid,
|
|
gid: r.gid,
|
|
},
|
|
size: int64(r.size),
|
|
digests: []digest.Digest{r.digest},
|
|
}
|
|
resourceMap[absPath] = f
|
|
paths = append(paths, absPath)
|
|
case rdirectory:
|
|
d := &directory{
|
|
resource: resource{
|
|
paths: []string{absPath},
|
|
mode: r.mode,
|
|
uid: r.uid,
|
|
gid: r.gid,
|
|
},
|
|
}
|
|
resourceMap[absPath] = d
|
|
paths = append(paths, absPath)
|
|
case rhardlink:
|
|
targetPath := r.target
|
|
if !filepath.IsAbs(targetPath) {
|
|
targetPath = "/" + targetPath
|
|
}
|
|
target, ok := resourceMap[targetPath]
|
|
if !ok {
|
|
return nil, errors.New("must specify target before hardlink for test resources")
|
|
}
|
|
rf, ok := target.(*regularFile)
|
|
if !ok {
|
|
return nil, errors.New("hardlink target must be regular file")
|
|
}
|
|
// TODO(dmcgowan): full merge
|
|
rf.paths = append(rf.paths, absPath)
|
|
// TODO(dmcgowan): check if first path is now different, changes source order and should update
|
|
// resource map key, to avoid canonically ordered first should be regular file
|
|
sort.Stable(sort.StringSlice(rf.paths))
|
|
case rrelsymlink, rabssymlink:
|
|
targetPath := r.target
|
|
if r.kind == rabssymlink && !filepath.IsAbs(r.target) {
|
|
// for absolute links, we join with root.
|
|
targetPath = filepath.Join(root, targetPath)
|
|
}
|
|
s := &symLink{
|
|
resource: resource{
|
|
paths: []string{absPath},
|
|
mode: r.mode,
|
|
uid: r.uid,
|
|
gid: r.gid,
|
|
},
|
|
target: targetPath,
|
|
}
|
|
resourceMap[absPath] = s
|
|
paths = append(paths, absPath)
|
|
case rchardev:
|
|
d := &device{
|
|
resource: resource{
|
|
paths: []string{absPath},
|
|
mode: r.mode,
|
|
uid: r.uid,
|
|
gid: r.gid,
|
|
},
|
|
major: uint64(r.major),
|
|
minor: uint64(r.minor),
|
|
}
|
|
resourceMap[absPath] = d
|
|
paths = append(paths, absPath)
|
|
case rnamedpipe:
|
|
p := &namedPipe{
|
|
resource: resource{
|
|
paths: []string{absPath},
|
|
mode: r.mode,
|
|
uid: r.uid,
|
|
gid: r.gid,
|
|
},
|
|
}
|
|
resourceMap[absPath] = p
|
|
paths = append(paths, absPath)
|
|
default:
|
|
return nil, fmt.Errorf("unknown resource type: %v", r.kind)
|
|
}
|
|
}
|
|
|
|
if len(resourceMap) < len(paths) {
|
|
return nil, errors.New("resource list has duplicated paths")
|
|
}
|
|
|
|
sort.Strings(paths)
|
|
|
|
manifestResources := make([]Resource, len(paths))
|
|
for i, p := range paths {
|
|
manifestResources[i] = resourceMap[p]
|
|
}
|
|
|
|
return manifestResources, nil
|
|
}
|