Skip to content

Commit

Permalink
crfs, stargz: basics of read-only FUSE filesystem, directory support
Browse files Browse the repository at this point in the history
No network support yet. But this implements the basic FUSE support
reading from a local stargz file.

Updates golang/go#30829

Change-Id: I342e957b3b36cded5aec8b1cdca65c3f5e788db3
Reviewed-on: https://go-review.googlesource.com/c/build/+/168799
Reviewed-by: Maisem Ali <[email protected]>
Reviewed-by: Brad Fitzpatrick <[email protected]>
  • Loading branch information
bradfitz committed Mar 22, 2019
1 parent 8a5a4d2 commit 9019790
Show file tree
Hide file tree
Showing 5 changed files with 346 additions and 1 deletion.
223 changes: 223 additions & 0 deletions crfs/crfs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// The crfs command runs the Container Registry Filesystem, providing a read-only
// FUSE filesystem for container images.
//
// For purposes of documentation, we'll assume you've mounted this at /crfs.
//
// Currently (as of 2019-03-21) it only mounts a single layer at the top level.
// In the future it'll have paths like:
//
// /crfs/image/gcr.io/foo-proj/image/latest
// /crfs/layer/gcr.io/foo-proj/image/latest/xxxxxxxxxxxxxx
//
// For mounting a squashed image and a layer, respectively, with the
// host, owner, image name, and version encoded in the path
// components.
package main

import (
"context"
"errors"
"flag"
"fmt"
"io"
"log"
"os"
"sort"
"syscall"
"time"
"unsafe"

"bazil.org/fuse"
fspkg "bazil.org/fuse/fs"
"golang.org/x/build/crfs/stargz"
)

const debug = false

func usage() {
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
fmt.Fprintf(os.Stderr, " %s <MOUNT_POINT> (defaults to /crfs)\n", os.Args[0])
flag.PrintDefaults()
}

var stargzFile = flag.String("test_stargz", "", "local stargz file for testing a single layer mount, without hitting a container registry")

func main() {
flag.Parse()
mntPoint := "/crfs"
if flag.NArg() > 1 {
usage()
os.Exit(2)
}
if flag.NArg() == 1 {
mntPoint = flag.Arg(0)
}

if *stargzFile == "" {
log.Fatalf("TODO: network mode not done yet. Use --test_stargz for now")
}
fs, err := NewLocalStargzFileFS(*stargzFile)
if err != nil {
log.Fatal(err)
}

c, err := fuse.Mount(mntPoint, fuse.FSName("crfs"), fuse.Subtype("crfs"))
if err != nil {
log.Fatal(err)
}
defer c.Close()

err = fspkg.Serve(c, fs)
if err != nil {
log.Fatal(err)
}

// check if the mount process has an error to report
<-c.Ready
if err := c.MountError; err != nil {
log.Fatal(err)
}
}

// FS is the CRFS filesystem.
// It implements https://godoc.org/bazil.org/fuse/fs#FS
type FS struct {
r *stargz.Reader
}

func NewLocalStargzFileFS(file string) (*FS, error) {
f, err := os.Open(file)
if err != nil {
return nil, err
}
fi, err := f.Stat()
if err != nil {
return nil, err
}
r, err := stargz.Open(io.NewSectionReader(f, 0, fi.Size()))
if err != nil {
return nil, err
}
return &FS{r: r}, nil
}

// Root returns the root filesystem node for the CRFS filesystem.
// See https://godoc.org/bazil.org/fuse/fs#FS
func (fs *FS) Root() (fspkg.Node, error) {
te, ok := fs.r.Lookup("")
if !ok {
return nil, errors.New("failed to find root in stargz")
}
return &node{fs, te}, nil
}

func inodeOfEnt(ent *stargz.TOCEntry) uint64 {
return uint64(uintptr(unsafe.Pointer(ent)))
}

func direntType(ent *stargz.TOCEntry) fuse.DirentType {
switch ent.Type {
case "dir":
return fuse.DT_Dir
case "reg":
return fuse.DT_File
case "symlink":
return fuse.DT_Link
}
// TODO: socket, block, char, fifo as needed
return fuse.DT_Unknown
}

// node is a CRFS node in the FUSE filesystem.
// See https://godoc.org/bazil.org/fuse/fs#Node
type node struct {
fs *FS
te *stargz.TOCEntry
}

var (
_ fspkg.HandleReadDirAller = (*node)(nil)
_ fspkg.Node = (*node)(nil)
_ fspkg.NodeStringLookuper = (*node)(nil)
_ fspkg.NodeReadlinker = (*node)(nil)
_ fspkg.HandleReader = (*node)(nil)
)

// Attr populates a with the attributes of n.
// See https://godoc.org/bazil.org/fuse/fs#Node
func (n *node) Attr(ctx context.Context, a *fuse.Attr) error {
fi := n.te.Stat()
a.Valid = 30 * 24 * time.Hour
a.Inode = inodeOfEnt(n.te)
a.Size = uint64(fi.Size())
a.Blocks = a.Size / 512
a.Mtime = fi.ModTime()
a.Mode = fi.Mode()
a.Uid = uint32(n.te.Uid)
a.Gid = uint32(n.te.Gid)
if debug {
log.Printf("attr of %s: %s", n.te.Name, *a)
}
return nil
}

// ReadDirAll returns all directory entries in the directory node n.
//
// https://godoc.org/bazil.org/fuse/fs#HandleReadDirAller
func (n *node) ReadDirAll(ctx context.Context) (ents []fuse.Dirent, err error) {
n.te.ForeachChild(func(baseName string, ent *stargz.TOCEntry) bool {
ents = append(ents, fuse.Dirent{
Inode: inodeOfEnt(ent),
Type: direntType(ent),
Name: baseName,
})
return true
})
sort.Slice(ents, func(i, j int) bool { return ents[i].Name < ents[j].Name })
return ents, nil
}

// Lookup looks up a child entry of the directory node n.
//
// See https://godoc.org/bazil.org/fuse/fs#NodeStringLookuper
func (n *node) Lookup(ctx context.Context, name string) (fspkg.Node, error) {
e, ok := n.te.LookupChild(name)
if !ok {
return nil, syscall.ENOENT
}
return &node{n.fs, e}, nil
}

// Readlink reads the target of a symlink.
//
// See https://godoc.org/bazil.org/fuse/fs#NodeReadlinker
func (n *node) Readlink(ctx context.Context, req *fuse.ReadlinkRequest) (string, error) {
if n.te.Type != "symlink" {
return "", syscall.EINVAL
}
return n.te.LinkName, nil
}

// Read reads data from a regular file n.
//
// See https://godoc.org/bazil.org/fuse/fs#HandleReader
func (n *node) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error {
sr, err := n.fs.r.OpenFile(n.te.Name)
if err != nil {
return err
}

resp.Data = make([]byte, req.Size)
nr, err := sr.ReadAt(resp.Data, req.Offset)
if nr < req.Size {
resp.Data = resp.Data[:nr]
}
if debug {
log.Printf("Read response: size=%d @ %d, read %d", req.Size, req.Offset, nr)
}
return nil
}
9 changes: 9 additions & 0 deletions crfs/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module golang.org/x/build/crfs

go 1.12

require (
bazil.org/fuse v0.0.0-20180421153158-65cc252bf669
golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53 // indirect
golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54 // indirect
)
9 changes: 9 additions & 0 deletions crfs/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
bazil.org/fuse v0.0.0-20180421153158-65cc252bf669 h1:FNCRpXiquG1aoyqcIWVFmpTSKVcx2bQD38uZZeGtdlw=
bazil.org/fuse v0.0.0-20180421153158-65cc252bf669/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53 h1:kcXqo9vE6fsZY5X5Rd7R1l7fTgnWaDCVmln65REefiE=
golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54 h1:xe1/2UUJRmA9iDglQSlkx8c5n3twv58+K0mPpC2zmhA=
golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
79 changes: 78 additions & 1 deletion crfs/stargz/stargz.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,18 +160,44 @@ type TOCEntry struct {
ChunkOffset int64 `json:"chunkOffset,omitempty"`
ChunkSize int64 `json:"chunkSize,omitempty"`

children []*TOCEntry // TODO: populate; add TOCEntry.Readdir
children map[string]*TOCEntry
}

// ModTime returns the entry's modification time.
func (e *TOCEntry) ModTime() time.Time { return e.modTime }

func (e *TOCEntry) addChild(baseName string, child *TOCEntry) {
if e.children == nil {
e.children = make(map[string]*TOCEntry)
}
e.children[baseName] = child
}

// jtoc is the JSON-serialized table of contents index of the files in the stargz file.
type jtoc struct {
Version int `json:"version"`
Entries []*TOCEntry `json:"entries"`
}

// Stat returns a FileInfo value representing e.
func (e *TOCEntry) Stat() os.FileInfo { return fileInfo{e} }

// ForeachChild calls f for each child item. If f returns false, iteration ends.
// If e is not a directory, f is not called.
func (e *TOCEntry) ForeachChild(f func(baseName string, ent *TOCEntry) bool) {
for name, ent := range e.children {
if !f(name, ent) {
return
}
}
}

// LookupChild returns the directory e's child by its base name.
func (e *TOCEntry) LookupChild(baseName string) (child *TOCEntry, ok bool) {
child, ok = e.children[baseName]
return
}

// fileInfo implements os.FileInfo using the wrapped *TOCEntry.
type fileInfo struct{ e *TOCEntry }

Expand Down Expand Up @@ -232,9 +258,60 @@ func (r *Reader) initFields() {
r.chunks[ent.Name] = append(r.chunks[ent.Name], ent)
}
}

// Populate children, add implicit directories:
for _, ent := range r.toc.Entries {
if ent.Type == "chunk" {
continue
}
// add "foo/":
// add "foo" child to "" (creating "" if necessary)
//
// add "foo/bar/":
// add "bar" child to "foo" (creating "foo" if necessary)
//
// add "foo/bar.txt":
// add "bar.txt" child to "foo" (creating "foo" if necessary)
//
// add "a/b/c/d/e/f.txt":
// create "a/b/c/d/e" node
// add "f.txt" child to "e"

name := ent.Name
if ent.Type == "dir" {
name = strings.TrimSuffix(name, "/")
}
pdir := r.getOrCreateDir(parentDir(name))
pdir.addChild(path.Base(name), ent)
}

}

func parentDir(p string) string {
dir, _ := path.Split(p)
return strings.TrimSuffix(dir, "/")
}

func (r *Reader) getOrCreateDir(d string) *TOCEntry {
e, ok := r.m[d]
if !ok {
e = &TOCEntry{
Name: d,
Type: "dir",
Mode: 0755,
}
r.m[d] = e
if d != "" {
pdir := r.getOrCreateDir(parentDir(d))
pdir.addChild(path.Base(d), e)
}
}
return e
}

// Lookup returns the Table of Contents entry for the given path.
//
// To get the root directory, use the empty string.
func (r *Reader) Lookup(path string) (e *TOCEntry, ok bool) {
if r == nil {
return
Expand Down
Loading

0 comments on commit 9019790

Please sign in to comment.