├── .travis.yml ├── .gitignore ├── version └── version.go ├── main.go ├── continuityutil └── continuityutil.go ├── oci-runtime-bundle.template ├── prepare.sh ├── README.md ├── config.runc-1.0.0-rc3-default.json └── config.json ├── puller ├── puller.go ├── local.go └── cacher.go ├── image ├── imageutil │ └── imageutil.go └── image.go ├── go.mod ├── commands ├── main.go ├── mount.go └── build.go ├── lazyfs ├── tree_test.go ├── continuitytree.go ├── tree.go ├── resource.go ├── lazyfile.go └── lazyfs.go ├── go.sum ├── README.md └── LICENSE /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.8 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | filegrain 2 | oci-runtime-bundle.template/rootfs 3 | oci-runtime-bundle.template/volumes 4 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | const ( 4 | VersionAnnotation = "filegrain.version" 5 | Version = "20170501" 6 | ) 7 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/AkihiroSuda/filegrain/commands" 5 | ) 6 | 7 | func main() { 8 | commands.MainCmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /continuityutil/continuityutil.go: -------------------------------------------------------------------------------- 1 | package continuityutil 2 | 3 | const ( 4 | MediaTypeManifestV0Protobuf = "application/vnd.continuity.manifest.v0+pb" // TODO: define in upstream continuity 5 | ) 6 | -------------------------------------------------------------------------------- /oci-runtime-bundle.template/prepare.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -x 3 | set -e 4 | mkdir -p rootfs 5 | mkdir -p volumes/root 6 | chown 0:0 volumes/root 7 | mkdir -p volumes/home 8 | chown 0:0 volumes/home 9 | -------------------------------------------------------------------------------- /puller/puller.go: -------------------------------------------------------------------------------- 1 | package puller 2 | 3 | import ( 4 | "github.com/opencontainers/go-digest" 5 | spec "github.com/opencontainers/image-spec/specs-go/v1" 6 | 7 | "github.com/AkihiroSuda/filegrain/image" 8 | ) 9 | 10 | type Puller interface { 11 | PullBlob(img string, d digest.Digest) (image.BlobReader, error) 12 | PullIndex(img string) (*spec.Index, error) 13 | } 14 | -------------------------------------------------------------------------------- /image/imageutil/imageutil.go: -------------------------------------------------------------------------------- 1 | package imageutil 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | spec "github.com/opencontainers/image-spec/specs-go/v1" 7 | 8 | "github.com/AkihiroSuda/filegrain/image" 9 | ) 10 | 11 | func WriteJSONBlob(img string, x interface{}, mediaType string) (*spec.Descriptor, error) { 12 | b, err := json.Marshal(x) 13 | if err != nil { 14 | return nil, err 15 | } 16 | d, err := image.WriteBlob(img, b) 17 | if err != nil { 18 | return nil, err 19 | } 20 | return &spec.Descriptor{ 21 | MediaType: mediaType, 22 | Digest: d, 23 | Size: int64(len(b)), 24 | }, nil 25 | } 26 | -------------------------------------------------------------------------------- /oci-runtime-bundle.template/README.md: -------------------------------------------------------------------------------- 1 | # Example OCI bundle for FILEgrain 2 | 3 | Usage: 4 | - Run `sudo ./prepare.sh` to create directories (`rootfs`, `volumes/{root, home}`). 5 | - Run `sudo filegrain mount /tmp/your-filegrain-image $(pwd)/rootfs` to mount `rootfs`. 6 | - Run `sudo runc run foo` to run a container. 7 | 8 | Differences from the runc v1.0.0-rc3 default `config.json`: 9 | - netns: host 10 | - hostname: "filegrain" 11 | - tmpfs: `/tmp`, `/run`. and `/var/log` 12 | - bind-mount: `/etc/{hosts, hostname, resolv.conf}` and `/tmp/.X11-unix` 13 | - persistent volumes: `volumes/{root, home}` on `/root` and `/home` 14 | -------------------------------------------------------------------------------- /puller/local.go: -------------------------------------------------------------------------------- 1 | package puller 2 | 3 | import ( 4 | "github.com/opencontainers/go-digest" 5 | spec "github.com/opencontainers/image-spec/specs-go/v1" 6 | 7 | "github.com/AkihiroSuda/filegrain/image" 8 | ) 9 | 10 | // LocalPuller lacks caching. Use with BlobCacher. 11 | type LocalPuller struct { 12 | } 13 | 14 | func NewLocalPuller() *LocalPuller { 15 | return &LocalPuller{} 16 | } 17 | 18 | func (p *LocalPuller) PullBlob(img string, d digest.Digest) (image.BlobReader, error) { 19 | return image.GetBlobReader(img, d) 20 | } 21 | 22 | func (p *LocalPuller) PullIndex(img string) (*spec.Index, error) { 23 | return image.ReadIndex(img) 24 | } 25 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/AkihiroSuda/filegrain 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/Sirupsen/logrus v0.11.5 7 | github.com/cheggaaa/pb v1.0.13 8 | github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc 9 | github.com/docker/go-units v0.3.2 10 | github.com/golang/protobuf v0.0.0-20170427213220-18c9bb326172 11 | github.com/hanwen/go-fuse v0.0.0-20170424203904-5404bf0e372d 12 | github.com/mattn/go-runewidth v0.0.2 13 | github.com/opencontainers/go-digest v1.0.0-rc0 14 | github.com/opencontainers/image-spec v0.0.0-20170501194034-c87455c1b399 15 | github.com/pkg/errors v0.8.0 16 | github.com/spf13/cobra v0.0.0-20170501210834-69f86e6d5d7a 17 | github.com/spf13/pflag v0.0.0-20170427125145-f1d95a35e132 18 | golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /commands/main.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | _ "crypto/sha256" 5 | _ "crypto/sha512" 6 | 7 | "github.com/Sirupsen/logrus" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var ( 12 | mainCmdConfig struct { 13 | debug bool 14 | } 15 | 16 | MainCmd = &cobra.Command{ 17 | Use: "filegrain ", 18 | Short: "FILEgrain: transport-agnostic, fine-grained content-addressable container image layout", 19 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 20 | if mainCmdConfig.debug { 21 | logrus.SetLevel(logrus.DebugLevel) 22 | logrus.Debug("running in debug mode") 23 | } 24 | return nil 25 | }, 26 | } 27 | ) 28 | 29 | func init() { 30 | MainCmd.PersistentFlags().BoolVar(&mainCmdConfig.debug, "debug", false, "debug") 31 | MainCmd.AddCommand(MountCmd) 32 | MainCmd.AddCommand(BuildCmd) 33 | } 34 | -------------------------------------------------------------------------------- /lazyfs/tree_test.go: -------------------------------------------------------------------------------- 1 | package lazyfs 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNode(t *testing.T) { 8 | nm := newNodeManager("/") 9 | nm.insert("/usr/bin/foo", "apple") 10 | nm.insert("/usr/bin/bar", "banana") 11 | nm.insert("/usr/bin", "chocolate") 12 | nm.insert("/bin", "doughnut") 13 | nm.insert("/usr/lib", "elderberry") 14 | // TODO: add assertion 15 | t.Logf("nm.root: %#v", nm.root) 16 | t.Logf("nm.lookup(\"\"): %#v", nm.lookup("")) 17 | t.Logf("nm.lookup(\"/\"): %#v", nm.lookup("/")) 18 | t.Logf("nm.lookup(\"/usr\"): %#v", nm.lookup("/usr")) 19 | t.Logf("nm.lookup(\"/usr/bin\"): %#v", nm.lookup("/usr/bin")) 20 | t.Logf("nm.lookup(\"/usr/bin/foo\"): %#v", nm.lookup("/usr/bin/foo")) 21 | t.Logf("nm.lookup(\"/usr/bin/bar\"): %#v", nm.lookup("/usr/bin/bar")) 22 | t.Logf("nm.lookup(\"/usr/bin/NX\"): %#v", nm.lookup("/usr/bin/NX")) 23 | t.Logf("nm.lookup(\"/NX\"): %#v", nm.lookup("/NX")) 24 | } 25 | -------------------------------------------------------------------------------- /lazyfs/continuitytree.go: -------------------------------------------------------------------------------- 1 | package lazyfs 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/AkihiroSuda/filegrain/continuityutil" 8 | continuitypb "github.com/containerd/continuity/proto" 9 | ) 10 | 11 | func loadTree(opts Options) (*nodeManager, error) { 12 | imageManifest, err := loadImageManifest(opts) 13 | if err != nil { 14 | return nil, err 15 | } 16 | nm := newNodeManager("/") // "/" = path sep (not root dir) 17 | nm.root.x = &continuitypb.Resource{ // set root content (unlikely to appear in the manifest) 18 | Mode: uint32(os.ModeDir | 0755), 19 | } 20 | for _, layer := range imageManifest.Layers { 21 | // TODO: support mixing up tar layers and continutiy layers.. 22 | if layer.MediaType != continuityutil.MediaTypeManifestV0Protobuf { 23 | return nil, fmt.Errorf("unsupported layer mediaType: %s", layer.MediaType) 24 | } 25 | pb, err := loadContinuityPBManifest(opts, &layer) 26 | if err != nil { 27 | return nil, err 28 | } 29 | for _, resource := range pb.Resource { 30 | for _, path := range resource.Path { 31 | nm.insert(path, resource) 32 | } 33 | } 34 | } 35 | return nm, nil 36 | } 37 | -------------------------------------------------------------------------------- /lazyfs/tree.go: -------------------------------------------------------------------------------- 1 | package lazyfs 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type node struct { 9 | m map[string]*node // key: basename 10 | x interface{} 11 | } 12 | 13 | func newNode() *node { 14 | return &node{ 15 | m: make(map[string]*node, 0), 16 | x: nil, 17 | } 18 | } 19 | 20 | type nodeManager struct { 21 | root *node 22 | sep string 23 | } 24 | 25 | func newNodeManager(sep string) *nodeManager { 26 | return &nodeManager{ 27 | root: newNode(), 28 | sep: sep, 29 | } 30 | } 31 | 32 | func (nm *nodeManager) insert(path string, x interface{}) { 33 | n := nm.root 34 | for _, s := range strings.Split(path, nm.sep) { 35 | if s == "" { 36 | continue 37 | } 38 | if s == "." || s == ".." { 39 | panic(fmt.Errorf("disallowed path element: %q", s)) 40 | } 41 | nn, ok := n.m[s] 42 | if !ok { 43 | nn = newNode() 44 | n.m[s] = nn 45 | } 46 | n = nn 47 | } 48 | n.x = x 49 | } 50 | 51 | // lookup returns node if found or nil. 52 | // note that lookup("") returns nm.root (== lookup(nm.sep)) 53 | func (nm *nodeManager) lookup(path string) *node { 54 | n := nm.root 55 | for _, s := range strings.Split(path, nm.sep) { 56 | if s == "" { 57 | continue 58 | } 59 | if s == "." || s == ".." { 60 | panic(fmt.Errorf("disallowed path element: %q", s)) 61 | } 62 | nn, ok := n.m[s] 63 | if !ok { 64 | return nil 65 | } 66 | n = nn 67 | } 68 | return n 69 | } 70 | -------------------------------------------------------------------------------- /lazyfs/resource.go: -------------------------------------------------------------------------------- 1 | package lazyfs 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | 8 | "github.com/golang/protobuf/proto" 9 | spec "github.com/opencontainers/image-spec/specs-go/v1" 10 | continuitypb "github.com/containerd/continuity/proto" 11 | 12 | "github.com/AkihiroSuda/filegrain/image" 13 | ) 14 | 15 | func loadContinuityPBManifest(opts Options, desc *spec.Descriptor) (*continuitypb.Manifest, error) { 16 | manifestBlob, err := loadBlobWithDescriptor(opts, desc) 17 | if err != nil { 18 | return nil, err 19 | } 20 | var bm continuitypb.Manifest 21 | 22 | if err := proto.Unmarshal(manifestBlob, &bm); err != nil { 23 | return nil, err 24 | } 25 | return &bm, nil 26 | } 27 | 28 | func loadImageManifest(opts Options) (*spec.Manifest, error) { 29 | idx, err := opts.Puller.PullIndex(opts.Image) 30 | if err != nil { 31 | return nil, err 32 | } 33 | var imageManifestDesc *spec.Descriptor 34 | for _, m := range idx.Manifests { 35 | mRefName, ok := m.Annotations[image.RefNameAnnotation] 36 | if ok && mRefName == opts.RefName { 37 | imageManifestDesc = &m 38 | break 39 | } 40 | } 41 | if imageManifestDesc == nil { 42 | return nil, fmt.Errorf("unknown reference name: %q", opts.RefName) 43 | } 44 | imageManifestBlob, err := loadBlobWithDescriptor(opts, imageManifestDesc) 45 | if err != nil { 46 | return nil, err 47 | } 48 | var imageManifest spec.Manifest 49 | if err := json.Unmarshal(imageManifestBlob, &imageManifest); err != nil { 50 | return nil, err 51 | } 52 | return &imageManifest, nil 53 | } 54 | 55 | func loadBlobWithDescriptor(opts Options, desc *spec.Descriptor) ([]byte, error) { 56 | r, err := opts.Puller.PullBlob(opts.Image, desc.Digest) 57 | if err != nil { 58 | return nil, err 59 | } 60 | b, err := ioutil.ReadAll(r) 61 | if err != nil { 62 | return nil, err 63 | } 64 | return b, r.Close() 65 | } 66 | -------------------------------------------------------------------------------- /lazyfs/lazyfile.go: -------------------------------------------------------------------------------- 1 | package lazyfs 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/Sirupsen/logrus" 7 | "github.com/hanwen/go-fuse/fuse" 8 | "github.com/hanwen/go-fuse/fuse/nodefs" 9 | "github.com/opencontainers/go-digest" 10 | continuitypb "github.com/containerd/continuity/proto" 11 | ) 12 | 13 | type file struct { 14 | opts Options 15 | res *continuitypb.Resource 16 | nodefs.File 17 | } 18 | 19 | func newFile(opts Options, res *continuitypb.Resource) nodefs.File { 20 | f := new(file) 21 | f.opts = opts 22 | f.res = res 23 | f.File = nodefs.NewDefaultFile() 24 | cached := &nodefs.WithFlags{ 25 | File: f, 26 | FuseFlags: fuse.FOPEN_KEEP_CACHE, 27 | } 28 | return cached 29 | } 30 | 31 | func (f *file) GetAttr(out *fuse.Attr) fuse.Status { 32 | *out = *continuityResourceToFuseAttr(f.res) 33 | return fuse.OK 34 | } 35 | 36 | func (f *file) Read(buf []byte, off int64) (res fuse.ReadResult, code fuse.Status) { 37 | if len(f.res.Digest) == 0 { 38 | logrus.Errorf("no digest for %#v", f.res) 39 | return nil, fuse.EIO 40 | } 41 | dgst := digest.Digest(f.res.Digest[0]) 42 | br, err := f.opts.Puller.PullBlob(f.opts.Image, dgst) 43 | if err != nil { 44 | logrus.Errorf("error while pulling %s: %v", dgst, err) 45 | return nil, fuse.EIO 46 | } 47 | if _, err := br.Seek(off, 0); err != nil { 48 | logrus.Errorf("error while seeking %s to %d: %v", dgst, off, err) 49 | return nil, fuse.EIO 50 | } 51 | if n, err := br.Read(buf); err == io.EOF { 52 | buf = buf[:n] 53 | } else if err != nil { 54 | logrus.Errorf("error while reading %d bytes at %d for %s: %v", 55 | len(buf), off, dgst, err) 56 | return nil, fuse.EIO 57 | } 58 | if err := br.Close(); err != nil { 59 | logrus.Errorf("error while closing after reading %d bytes at %d for %s: %v", 60 | len(buf), off, dgst, err) 61 | return nil, fuse.EIO 62 | } 63 | return fuse.ReadResultData(buf), fuse.OK 64 | } 65 | -------------------------------------------------------------------------------- /commands/mount.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "os" 7 | "os/signal" 8 | 9 | "github.com/Sirupsen/logrus" 10 | "github.com/spf13/cobra" 11 | 12 | "github.com/AkihiroSuda/filegrain/lazyfs" 13 | "github.com/AkihiroSuda/filegrain/puller" 14 | ) 15 | 16 | var ( 17 | mountCmdConfig struct { 18 | debugFUSE bool 19 | refName string 20 | } 21 | 22 | MountCmd = &cobra.Command{ 23 | Use: "mount ", 24 | Short: "Mount with lazy fs", 25 | RunE: func(cmd *cobra.Command, args []string) error { 26 | if len(args) != 2 { 27 | return errors.New("must specify image and mountpoint") 28 | } 29 | img, mountpoint := args[0], args[1] 30 | cachePath, err := ioutil.TempDir("", "filegrain-blobcache") 31 | if err != nil { 32 | return err 33 | } 34 | logrus.Infof("Blob cache (ephemeral): %s", cachePath) 35 | defer os.RemoveAll(cachePath) // FIXME 36 | pvller, err := puller.NewBlobCacher(cachePath, 37 | puller.NewLocalPuller()) 38 | if err != nil { 39 | return err 40 | } 41 | opts := lazyfs.Options{ 42 | Mountpoint: mountpoint, 43 | Puller: pvller, 44 | Image: img, 45 | RefName: mountCmdConfig.refName, 46 | } 47 | return serve(opts) 48 | }, 49 | } 50 | ) 51 | 52 | func init() { 53 | MountCmd.Flags().StringVar(&mountCmdConfig.refName, "tag", "latest", "tag (aka reference name)") 54 | MountCmd.Flags().BoolVar(&mountCmdConfig.debugFUSE, "debug-fuse", false, "debug FUSE") 55 | } 56 | 57 | func serve(opts lazyfs.Options) error { 58 | fs, err := lazyfs.NewFS(opts) 59 | if err != nil { 60 | return err 61 | } 62 | sv, err := lazyfs.NewServer(fs) 63 | if err != nil { 64 | return err 65 | } 66 | if mountCmdConfig.debugFUSE { 67 | fs.SetDebug(true) 68 | sv.SetDebug(true) 69 | } 70 | go sv.Serve() 71 | logrus.Infof("Mounting on %s", opts.Mountpoint) 72 | if err := sv.WaitMount(); err != nil { 73 | return err 74 | } 75 | logrus.Infof("Mounted on %s", opts.Mountpoint) 76 | defer func() { 77 | logrus.Infof("Unmounting %s", opts.Mountpoint) 78 | if err := sv.Unmount(); err != nil { 79 | panic(err) // FIXME 80 | } 81 | logrus.Infof("Unmounted %s", opts.Mountpoint) 82 | }() 83 | waitForSIGINT() 84 | return nil 85 | } 86 | 87 | func waitForSIGINT() { 88 | c := make(chan os.Signal, 1) 89 | signal.Notify(c, os.Interrupt) 90 | <-c 91 | } 92 | -------------------------------------------------------------------------------- /commands/build.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/Sirupsen/logrus" 10 | "github.com/spf13/cobra" 11 | 12 | "github.com/AkihiroSuda/filegrain/builder" 13 | ) 14 | 15 | var ( 16 | buildCmdConfig struct { 17 | refName string 18 | sourceType string 19 | target string 20 | } 21 | 22 | BuildCmd = &cobra.Command{ 23 | Use: "build -o ", 24 | Short: "Build a FILEgrain image", 25 | RunE: func(cmd *cobra.Command, args []string) error { 26 | if buildCmdConfig.target == "" { 27 | return errors.New("must specify target output (-o)") 28 | } 29 | if len(args) != 1 { 30 | return errors.New("must specify source") 31 | } 32 | source := args[0] 33 | b, err := newBuilder(buildCmdConfig.sourceType, source) 34 | if err != nil { 35 | return err 36 | } 37 | if err := b.Build(buildCmdConfig.target, buildCmdConfig.refName); err != nil { 38 | return err 39 | } 40 | logrus.Info("Done") 41 | return nil 42 | }, 43 | } 44 | ) 45 | 46 | func init() { 47 | BuildCmd.Flags().StringVarP(&buildCmdConfig.target, "output", "o", "", "target output path") 48 | BuildCmd.Flags().StringVar(&buildCmdConfig.refName, "tag", "latest", "tag (aka reference name)") 49 | BuildCmd.Flags().StringVar(&buildCmdConfig.sourceType, "source-type", "auto", "source type (auto, oci-image, docker-image, rootfs)") 50 | } 51 | 52 | func newBuilder(sourceType, source string) (builder.Builder, error) { 53 | if sourceType == "auto" || sourceType == "" { 54 | sourceType = guessSourceType(source) 55 | if sourceType != "" { 56 | logrus.Infof("Detected source type %q for %s", sourceType, source) 57 | } else { 58 | return nil, fmt.Errorf("could not detect source type for %s", source) 59 | } 60 | } 61 | switch sourceType { 62 | case "oci-image": 63 | return builder.NewBuilderWithOCIImage(source) 64 | case "docker-image": 65 | return builder.NewBuilderWithDockerImage(source) 66 | case "rootfs": 67 | return builder.NewBuilderWithRootFS(source) 68 | } 69 | return nil, fmt.Errorf("unknown source type: %s", sourceType) 70 | } 71 | 72 | func guessSourceType(source string) string { 73 | fi, err := os.Stat(source) 74 | if err == nil && fi.IsDir() { 75 | _, err := os.Stat(filepath.Join(source, "oci-layout")) 76 | if err == nil { 77 | return "oci-image" 78 | } 79 | return "rootfs" 80 | } 81 | // FIXME: not accurate 82 | return "docker-image" 83 | } 84 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Sirupsen/logrus v0.11.5 h1:aIMrrsnipdTlAieMe7FC/iiuJ0+ELiXCT4YiVQiK9j8= 2 | github.com/Sirupsen/logrus v0.11.5/go.mod h1:rmk17hk6i8ZSAJkSDa7nOxamrG+SP4P0mm+DAvExv4U= 3 | github.com/cheggaaa/pb v1.0.13 h1:vSXdStIZ5JH9knGF+zbK1gpOJZPQ+CvU24ETBe7LyS0= 4 | github.com/cheggaaa/pb v1.0.13/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= 5 | github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc h1:TP+534wVlf61smEIq1nwLLAjQVEK2EADoW3CX9AuT+8= 6 | github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= 7 | github.com/docker/go-units v0.3.2 h1:Kjm80apys7gTtfVmCvVY8gwu10uofaFSrmAKOVrtueE= 8 | github.com/docker/go-units v0.3.2/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 9 | github.com/golang/protobuf v0.0.0-20170427213220-18c9bb326172 h1:ib1Vbb6/KliPKsRcZdmCUnFGP7/BcCWgW9+gR+sUQk0= 10 | github.com/golang/protobuf v0.0.0-20170427213220-18c9bb326172/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 11 | github.com/hanwen/go-fuse v0.0.0-20170424203904-5404bf0e372d h1:06w1Xwq+3gQbm76tgdC8QL+2B6aBxSEMCRH73aSv+fw= 12 | github.com/hanwen/go-fuse v0.0.0-20170424203904-5404bf0e372d/go.mod h1:4ZJ05v9yt5k/mcFkGvSPKJB5T8G/6nuumL63ZqlrPvI= 13 | github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o= 14 | github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 15 | github.com/opencontainers/go-digest v1.0.0-rc0 h1:YHPGfp+qlmg7loi376Jk5jNEgjgUUIdXGFsel8aFHnA= 16 | github.com/opencontainers/go-digest v1.0.0-rc0/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= 17 | github.com/opencontainers/image-spec v0.0.0-20170501194034-c87455c1b399 h1:sU4Mza5IE9ERvtzoCj7p4cOJcfQ2dgfbZQRJE17Ewxo= 18 | github.com/opencontainers/image-spec v0.0.0-20170501194034-c87455c1b399/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= 19 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 20 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 21 | github.com/spf13/cobra v0.0.0-20170501210834-69f86e6d5d7a h1:9OzjTlfCOEYy/+ivO76kJxif+t9vmOjlXZDL0cnxdvg= 22 | github.com/spf13/cobra v0.0.0-20170501210834-69f86e6d5d7a/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 23 | github.com/spf13/pflag v0.0.0-20170427125145-f1d95a35e132 h1:Z9FpVnwd+lo1kp/Mf5kL1XW57L+Vcu/40wC3/aFGCHY= 24 | github.com/spf13/pflag v0.0.0-20170427125145-f1d95a35e132/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 25 | github.com/stevvooe/continuity v0.0.0-20190426062206-aaeac12a7ffc h1:1GLiICsIP1hnDnXS8MFG17DyorbYXMTFipmAfAOrTKk= 26 | github.com/stevvooe/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:AXZEju8Nky8hvQW5KS9REMxcmvWpBFJRqnhQ6W/7S6E= 27 | golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444 h1:/d2cWp6PSamH4jDPFLyO150psQdqvtoNX8Zjg3AQ31g= 28 | golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 29 | -------------------------------------------------------------------------------- /puller/cacher.go: -------------------------------------------------------------------------------- 1 | package puller 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "sync" 8 | "sync/atomic" 9 | 10 | "github.com/Sirupsen/logrus" 11 | "github.com/docker/go-units" 12 | "github.com/opencontainers/go-digest" 13 | spec "github.com/opencontainers/image-spec/specs-go/v1" 14 | 15 | "github.com/AkihiroSuda/filegrain/image" 16 | ) 17 | 18 | type pullStatus int 19 | 20 | const ( 21 | pullStatusUnknown pullStatus = iota 22 | pullStatusPulling 23 | pullStatusPulled 24 | ) 25 | 26 | type BlobCacher struct { 27 | cachePath string 28 | puller Puller 29 | 30 | pullStatus map[digest.Digest]pullStatus 31 | pullStatusCond *sync.Cond 32 | 33 | pulledBlobBytes uint64 // atomic 34 | pulledBlobs uint64 // atomic 35 | } 36 | 37 | func NewBlobCacher(cachePath string, puller Puller) (*BlobCacher, error) { 38 | if _, err := os.Stat(cachePath); err != nil { 39 | return nil, err 40 | } 41 | cacher := &BlobCacher{ 42 | cachePath: cachePath, 43 | puller: puller, 44 | pullStatus: make(map[digest.Digest]pullStatus, 0), 45 | pullStatusCond: sync.NewCond(&sync.Mutex{}), 46 | pulledBlobBytes: 0, 47 | pulledBlobs: 0, 48 | } 49 | // currently, cachePath needs to be empty. 50 | // TODO: load cacher.pulled 51 | return cacher, nil 52 | } 53 | 54 | func (p *BlobCacher) PullBlob(img string, d digest.Digest) (image.BlobReader, error) { 55 | if err := p.cacheBlobIfNotYet(img, d); err != nil { 56 | return nil, err 57 | } 58 | return p.openCachedBlob(img, d) 59 | } 60 | 61 | func (p *BlobCacher) cacheBlobIfNotYet(img string, d digest.Digest) error { 62 | alreadyCached := false 63 | for { 64 | p.pullStatusCond.L.Lock() 65 | st, ok := p.pullStatus[d] 66 | if ok && st == pullStatusPulling { 67 | p.pullStatusCond.Wait() 68 | p.pullStatusCond.L.Unlock() 69 | } else { 70 | p.pullStatusCond.L.Unlock() 71 | alreadyCached = st == pullStatusPulled 72 | break 73 | } 74 | } 75 | if !alreadyCached { 76 | return p.cacheBlob(img, d) 77 | } 78 | return nil 79 | } 80 | 81 | func (p *BlobCacher) cacheBlob(img string, d digest.Digest) error { 82 | // logrus.Debugf("Caching blob: %s", d) 83 | p.pullStatusCond.L.Lock() 84 | p.pullStatus[d] = pullStatusPulling 85 | p.pullStatusCond.L.Unlock() 86 | r, err := p.puller.PullBlob(img, d) 87 | if err != nil { 88 | return err 89 | } 90 | w, err := image.NewBlobWriter(p.cachePath, d.Algorithm()) 91 | if err != nil { 92 | return err 93 | } 94 | copied, err := io.Copy(w, r) 95 | if err != nil { 96 | return err 97 | } 98 | if err := r.Close(); err != nil { 99 | return err 100 | } 101 | if err := w.Close(); err != nil { 102 | return err 103 | } 104 | if dd := w.Digest(); dd == nil || *dd != d { 105 | return fmt.Errorf("expected %q, got %q", d, dd) 106 | } 107 | totalCopied := atomic.AddUint64(&p.pulledBlobBytes, uint64(copied)) 108 | totalCachedBlobs := atomic.AddUint64(&p.pulledBlobs, uint64(1)) 109 | logrus.Infof("Cache: %d blobs, %s", totalCachedBlobs, units.BytesSize(float64(totalCopied))) 110 | p.pullStatusCond.L.Lock() 111 | p.pullStatus[d] = pullStatusPulled 112 | p.pullStatusCond.L.Unlock() 113 | p.pullStatusCond.Broadcast() 114 | return nil 115 | } 116 | 117 | func (p *BlobCacher) openCachedBlob(img string, d digest.Digest) (image.BlobReader, error) { 118 | return image.GetBlobReader(p.cachePath, d) 119 | } 120 | 121 | func (p *BlobCacher) PullIndex(img string) (*spec.Index, error) { 122 | return p.puller.PullIndex(img) 123 | } 124 | -------------------------------------------------------------------------------- /lazyfs/lazyfs.go: -------------------------------------------------------------------------------- 1 | package lazyfs 2 | 3 | import ( 4 | "os" 5 | "syscall" 6 | 7 | "github.com/Sirupsen/logrus" 8 | "github.com/hanwen/go-fuse/fuse" 9 | "github.com/hanwen/go-fuse/fuse/nodefs" 10 | "github.com/hanwen/go-fuse/fuse/pathfs" 11 | continuitypb "github.com/containerd/continuity/proto" 12 | 13 | "github.com/AkihiroSuda/filegrain/puller" 14 | ) 15 | 16 | // FS is a READ-ONLY filesystem with lazy-pull feature. 17 | // 18 | // FS implements github.com/hanwen/go-fuse/fuse/pathfs.FileSystem 19 | // 20 | // Supported objects: 21 | // - directories 22 | // - regular files (including hardlinks) (excepts XAttrs) 23 | // - symbolic links 24 | type FS struct { 25 | pathfs.FileSystem 26 | opts Options 27 | tree *nodeManager 28 | } 29 | 30 | func continuityResourceToFuseAttr(res *continuitypb.Resource) *fuse.Attr { 31 | mode := res.Mode & uint32(os.ModePerm) 32 | siz := res.Size 33 | switch res.Mode & uint32(os.ModeType) { 34 | case uint32(os.ModeDir): 35 | mode |= syscall.S_IFDIR 36 | case uint32(os.ModeSymlink): 37 | mode |= syscall.S_IFLNK 38 | case 0: 39 | mode |= syscall.S_IFREG 40 | } 41 | return &fuse.Attr{ 42 | Mode: mode, 43 | Size: siz, 44 | // Times are not supported in current continuity 45 | } 46 | } 47 | 48 | func (fs *FS) lookup(name string) (*continuitypb.Resource, fuse.Status) { 49 | n := fs.tree.lookup(name) 50 | if n == nil { 51 | return nil, fuse.ENOENT 52 | } 53 | res, ok := n.x.(*continuitypb.Resource) 54 | if !ok { 55 | logrus.Errorf("can't convert %#v to *continuitypb.Resource while looking up %q", n.x, name) 56 | return nil, fuse.EIO 57 | } 58 | return res, fuse.OK 59 | } 60 | 61 | func (fs *FS) GetAttr(name string, fc *fuse.Context) (*fuse.Attr, fuse.Status) { 62 | res, st := fs.lookup(name) 63 | if st != fuse.OK { 64 | return nil, st 65 | } 66 | attr := continuityResourceToFuseAttr(res) 67 | return attr, fuse.OK 68 | } 69 | 70 | func (fs *FS) OpenDir(name string, fc *fuse.Context) ([]fuse.DirEntry, fuse.Status) { 71 | n := fs.tree.lookup(name) 72 | if n == nil { 73 | return nil, fuse.ENOENT 74 | } 75 | var ents []fuse.DirEntry 76 | for k, v := range n.m { 77 | res, ok := v.x.(*continuitypb.Resource) 78 | if !ok { 79 | logrus.Errorf("can't convert %#v to *continuitypb.Resource while opendir %q, %q", n.x, name, k) 80 | return nil, fuse.EIO 81 | } 82 | mode := res.Mode // FIXME? 83 | ents = append(ents, fuse.DirEntry{ 84 | Name: k, 85 | Mode: mode, 86 | }) 87 | } 88 | return ents, fuse.OK 89 | } 90 | 91 | func (fs *FS) Open(name string, flags uint32, fc *fuse.Context) (nodefs.File, fuse.Status) { 92 | res, st := fs.lookup(name) 93 | if st != fuse.OK { 94 | return nil, st 95 | } 96 | return newFile(fs.opts, res), fuse.OK 97 | } 98 | 99 | func (fs *FS) Readlink(name string, fc *fuse.Context) (string, fuse.Status) { 100 | res, st := fs.lookup(name) 101 | if st != fuse.OK { 102 | return "", st 103 | } 104 | return res.Target, fuse.OK 105 | } 106 | 107 | type Options struct { 108 | Mountpoint string 109 | Puller puller.Puller 110 | Image string 111 | RefName string 112 | } 113 | 114 | func NewFS(opts Options) (*FS, error) { 115 | tree, err := loadTree(opts) 116 | if err != nil { 117 | return nil, err 118 | } 119 | fs := &FS{ 120 | FileSystem: pathfs.NewReadonlyFileSystem(pathfs.NewDefaultFileSystem()), 121 | opts: opts, 122 | tree: tree, 123 | } 124 | return fs, nil 125 | } 126 | 127 | func NewServer(fs *FS) (*fuse.Server, error) { 128 | nfs := pathfs.NewPathNodeFs(pathfs.NewReadonlyFileSystem(fs), nil) 129 | conn := nodefs.NewFileSystemConnector(nfs.Root(), nil) 130 | return fuse.NewServer(conn.RawFS(), fs.opts.Mountpoint, 131 | &fuse.MountOptions{ 132 | FsName: fs.opts.Mountpoint + ":" + fs.opts.RefName, 133 | Name: "filegrain.lazyfs", 134 | }) 135 | } 136 | -------------------------------------------------------------------------------- /oci-runtime-bundle.template/config.runc-1.0.0-rc3-default.json: -------------------------------------------------------------------------------- 1 | { 2 | "ociVersion": "1.0.0-rc5-dev", 3 | "process": { 4 | "terminal": true, 5 | "user": { 6 | "uid": 0, 7 | "gid": 0 8 | }, 9 | "args": [ 10 | "sh" 11 | ], 12 | "env": [ 13 | "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", 14 | "TERM=xterm" 15 | ], 16 | "cwd": "/", 17 | "capabilities": { 18 | "bounding": [ 19 | "CAP_AUDIT_WRITE", 20 | "CAP_KILL", 21 | "CAP_NET_BIND_SERVICE" 22 | ], 23 | "effective": [ 24 | "CAP_AUDIT_WRITE", 25 | "CAP_KILL", 26 | "CAP_NET_BIND_SERVICE" 27 | ], 28 | "inheritable": [ 29 | "CAP_AUDIT_WRITE", 30 | "CAP_KILL", 31 | "CAP_NET_BIND_SERVICE" 32 | ], 33 | "permitted": [ 34 | "CAP_AUDIT_WRITE", 35 | "CAP_KILL", 36 | "CAP_NET_BIND_SERVICE" 37 | ], 38 | "ambient": [ 39 | "CAP_AUDIT_WRITE", 40 | "CAP_KILL", 41 | "CAP_NET_BIND_SERVICE" 42 | ] 43 | }, 44 | "rlimits": [ 45 | { 46 | "type": "RLIMIT_NOFILE", 47 | "hard": 1024, 48 | "soft": 1024 49 | } 50 | ], 51 | "noNewPrivileges": true 52 | }, 53 | "root": { 54 | "path": "rootfs", 55 | "readonly": true 56 | }, 57 | "hostname": "runc", 58 | "mounts": [ 59 | { 60 | "destination": "/proc", 61 | "type": "proc", 62 | "source": "proc" 63 | }, 64 | { 65 | "destination": "/dev", 66 | "type": "tmpfs", 67 | "source": "tmpfs", 68 | "options": [ 69 | "nosuid", 70 | "strictatime", 71 | "mode=755", 72 | "size=65536k" 73 | ] 74 | }, 75 | { 76 | "destination": "/dev/pts", 77 | "type": "devpts", 78 | "source": "devpts", 79 | "options": [ 80 | "nosuid", 81 | "noexec", 82 | "newinstance", 83 | "ptmxmode=0666", 84 | "mode=0620", 85 | "gid=5" 86 | ] 87 | }, 88 | { 89 | "destination": "/dev/shm", 90 | "type": "tmpfs", 91 | "source": "shm", 92 | "options": [ 93 | "nosuid", 94 | "noexec", 95 | "nodev", 96 | "mode=1777", 97 | "size=65536k" 98 | ] 99 | }, 100 | { 101 | "destination": "/dev/mqueue", 102 | "type": "mqueue", 103 | "source": "mqueue", 104 | "options": [ 105 | "nosuid", 106 | "noexec", 107 | "nodev" 108 | ] 109 | }, 110 | { 111 | "destination": "/sys", 112 | "type": "sysfs", 113 | "source": "sysfs", 114 | "options": [ 115 | "nosuid", 116 | "noexec", 117 | "nodev", 118 | "ro" 119 | ] 120 | }, 121 | { 122 | "destination": "/sys/fs/cgroup", 123 | "type": "cgroup", 124 | "source": "cgroup", 125 | "options": [ 126 | "nosuid", 127 | "noexec", 128 | "nodev", 129 | "relatime", 130 | "ro" 131 | ] 132 | } 133 | ], 134 | "linux": { 135 | "resources": { 136 | "devices": [ 137 | { 138 | "allow": false, 139 | "access": "rwm" 140 | } 141 | ] 142 | }, 143 | "namespaces": [ 144 | { 145 | "type": "pid" 146 | }, 147 | { 148 | "type": "network" 149 | }, 150 | { 151 | "type": "ipc" 152 | }, 153 | { 154 | "type": "uts" 155 | }, 156 | { 157 | "type": "mount" 158 | } 159 | ], 160 | "maskedPaths": [ 161 | "/proc/kcore", 162 | "/proc/latency_stats", 163 | "/proc/timer_list", 164 | "/proc/timer_stats", 165 | "/proc/sched_debug", 166 | "/sys/firmware" 167 | ], 168 | "readonlyPaths": [ 169 | "/proc/asound", 170 | "/proc/bus", 171 | "/proc/fs", 172 | "/proc/irq", 173 | "/proc/sys", 174 | "/proc/sysrq-trigger" 175 | ] 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /image/image.go: -------------------------------------------------------------------------------- 1 | package image 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/opencontainers/go-digest" 12 | "github.com/opencontainers/image-spec/specs-go" 13 | spec "github.com/opencontainers/image-spec/specs-go/v1" 14 | ) 15 | 16 | const ( 17 | RefNameAnnotation = "org.opencontainers.image.ref.name" // should it be defined in image-spec? 18 | ) 19 | 20 | func Init(img string) error { 21 | // Create the directory 22 | if err := os.RemoveAll(img); err != nil { 23 | return err 24 | } 25 | if err := os.MkdirAll(img, 0755); err != nil { 26 | return err 27 | } 28 | // Create blobs/sha256 29 | if err := os.MkdirAll( 30 | filepath.Join(img, "blobs", string(digest.Canonical)), 31 | 0755); err != nil { 32 | return nil 33 | } 34 | // Create oci-layout 35 | if err := WriteImageLayout(img, &spec.ImageLayout{Version: spec.ImageLayoutVersion}); err != nil { 36 | return err 37 | } 38 | // Create index.json 39 | return WriteIndex(img, &spec.Index{Versioned: specs.Versioned{SchemaVersion: 2}}) 40 | } 41 | 42 | func blobPath(img string, d digest.Digest) string { 43 | return filepath.Join(img, "blobs", d.Algorithm().String(), d.Hex()) 44 | } 45 | 46 | func indexPath(img string) string { 47 | return filepath.Join(img, "index.json") 48 | } 49 | 50 | type BlobReader interface { 51 | io.ReadSeeker 52 | io.Closer 53 | } 54 | 55 | func GetBlobReader(img string, d digest.Digest) (BlobReader, error) { 56 | return os.Open(blobPath(img, d)) 57 | } 58 | 59 | func ReadBlob(img string, d digest.Digest) ([]byte, error) { 60 | r, err := GetBlobReader(img, d) 61 | if err != nil { 62 | return nil, err 63 | } 64 | return ioutil.ReadAll(r) 65 | } 66 | 67 | func WriteBlob(img string, b []byte) (digest.Digest, error) { 68 | d := digest.FromBytes(b) 69 | return d, ioutil.WriteFile(blobPath(img, d), b, 0444) 70 | } 71 | 72 | type BlobWriter struct { 73 | img string 74 | digester digest.Digester 75 | f *os.File 76 | closed bool 77 | } 78 | 79 | func NewBlobWriter(img string, algo digest.Algorithm) (*BlobWriter, error) { 80 | // use img rather than the default tmp, so as to make sure rename(2) can be applied 81 | f, err := ioutil.TempFile(img, "tmp.blobwriter") 82 | if err != nil { 83 | return nil, err 84 | } 85 | return &BlobWriter{ 86 | img: img, 87 | digester: algo.Digester(), 88 | f: f, 89 | }, nil 90 | } 91 | 92 | func (bw *BlobWriter) Write(b []byte) (int, error) { 93 | n, err := bw.f.Write(b) 94 | if err != nil { 95 | return n, err 96 | } 97 | return bw.digester.Hash().Write(b) 98 | } 99 | 100 | func (bw *BlobWriter) Close() error { 101 | oldPath := bw.f.Name() 102 | if err := bw.f.Close(); err != nil { 103 | return err 104 | } 105 | newPath := blobPath(bw.img, bw.digester.Digest()) 106 | if err := os.MkdirAll(filepath.Dir(newPath), 0755); err != nil { 107 | return err 108 | } 109 | if err := os.Rename(oldPath, newPath); err != nil { 110 | return err 111 | } 112 | bw.closed = true 113 | return nil 114 | } 115 | 116 | // Digest returns nil if unclosed 117 | func (bw *BlobWriter) Digest() *digest.Digest { 118 | if !bw.closed { 119 | return nil 120 | } 121 | d := bw.digester.Digest() 122 | return &d 123 | } 124 | 125 | func DeleteBlob(img string, d digest.Digest) error { 126 | return os.Remove(blobPath(img, d)) 127 | } 128 | 129 | func ReadImageLayout(img string) (*spec.ImageLayout, error) { 130 | b, err := ioutil.ReadFile(filepath.Join(img, spec.ImageLayoutFile)) 131 | if err != nil { 132 | return nil, err 133 | } 134 | var layout spec.ImageLayout 135 | if err := json.Unmarshal(b, &layout); err != nil { 136 | return nil, err 137 | } 138 | return &layout, nil 139 | } 140 | 141 | func WriteImageLayout(img string, layout *spec.ImageLayout) error { 142 | b, err := json.Marshal(layout) 143 | if err != nil { 144 | return err 145 | } 146 | return ioutil.WriteFile(filepath.Join(img, spec.ImageLayoutFile), b, 0644) 147 | } 148 | 149 | func ReadIndex(img string) (*spec.Index, error) { 150 | b, err := ioutil.ReadFile(indexPath(img)) 151 | if err != nil { 152 | return nil, err 153 | } 154 | var idx spec.Index 155 | if err := json.Unmarshal(b, &idx); err != nil { 156 | return nil, err 157 | } 158 | return &idx, nil 159 | } 160 | 161 | func WriteIndex(img string, idx *spec.Index) error { 162 | b, err := json.Marshal(idx) 163 | if err != nil { 164 | return err 165 | } 166 | return ioutil.WriteFile(indexPath(img), b, 0644) 167 | } 168 | 169 | // RemoveManifestDescriptorFromIndex removes the manifest descriptor from the index. 170 | // Returns nil error when the entry not found. 171 | func RemoveManifestDescriptorFromIndex(img string, refName string) error { 172 | if refName == "" { 173 | return errors.New("empty refName specified") 174 | } 175 | src, err := ReadIndex(img) 176 | if err != nil { 177 | return err 178 | } 179 | dst := *src 180 | dst.Manifests = nil 181 | for _, m := range src.Manifests { 182 | mRefName, ok := m.Annotations[RefNameAnnotation] 183 | if ok && mRefName == refName { 184 | continue 185 | } 186 | dst.Manifests = append(dst.Manifests, m) 187 | } 188 | return WriteIndex(img, &dst) 189 | } 190 | 191 | // PutManifestDescriptorToIndex puts a manifest descriptor to the index. 192 | // If ref name is set and conflicts with the existing descriptors, the old ones are removed. 193 | func PutManifestDescriptorToIndex(img string, desc *spec.Descriptor) error { 194 | refName, ok := desc.Annotations[RefNameAnnotation] 195 | if ok && refName != "" { 196 | if err := RemoveManifestDescriptorFromIndex(img, refName); err != nil { 197 | return err 198 | } 199 | } 200 | idx, err := ReadIndex(img) 201 | if err != nil { 202 | return err 203 | } 204 | idx.Manifests = append(idx.Manifests, *desc) 205 | return WriteIndex(img, idx) 206 | } 207 | -------------------------------------------------------------------------------- /oci-runtime-bundle.template/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ociVersion": "1.0.0-rc5-dev", 3 | "process": { 4 | "terminal": true, 5 | "user": { 6 | "uid": 0, 7 | "gid": 0 8 | }, 9 | "args": [ 10 | "sh" 11 | ], 12 | "env": [ 13 | "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", 14 | "TERM=xterm" 15 | ], 16 | "cwd": "/", 17 | "capabilities": { 18 | "bounding": [ 19 | "CAP_AUDIT_WRITE", 20 | "CAP_KILL", 21 | "CAP_NET_BIND_SERVICE" 22 | ], 23 | "effective": [ 24 | "CAP_AUDIT_WRITE", 25 | "CAP_KILL", 26 | "CAP_NET_BIND_SERVICE" 27 | ], 28 | "inheritable": [ 29 | "CAP_AUDIT_WRITE", 30 | "CAP_KILL", 31 | "CAP_NET_BIND_SERVICE" 32 | ], 33 | "permitted": [ 34 | "CAP_AUDIT_WRITE", 35 | "CAP_KILL", 36 | "CAP_NET_BIND_SERVICE" 37 | ], 38 | "ambient": [ 39 | "CAP_AUDIT_WRITE", 40 | "CAP_KILL", 41 | "CAP_NET_BIND_SERVICE" 42 | ] 43 | }, 44 | "rlimits": [ 45 | { 46 | "type": "RLIMIT_NOFILE", 47 | "hard": 1024, 48 | "soft": 1024 49 | } 50 | ], 51 | "noNewPrivileges": true 52 | }, 53 | "root": { 54 | "path": "rootfs", 55 | "readonly": true 56 | }, 57 | "hostname": "filegrain", 58 | "mounts": [ 59 | { 60 | "destination" : "/tmp", 61 | "source" : "tmpfs", 62 | "type" : "tmpfs" 63 | }, 64 | { 65 | "destination" : "/run", 66 | "source" : "tmpfs", 67 | "type" : "tmpfs" 68 | }, 69 | { 70 | "destination" : "/var/log", 71 | "source" : "tmpfs", 72 | "type" : "tmpfs" 73 | }, 74 | { 75 | "type" : "bind", 76 | "source" : "/etc/hosts", 77 | "options" : [ 78 | "rbind", 79 | "ro" 80 | ], 81 | "destination" : "/etc/hosts" 82 | }, 83 | { 84 | "type" : "bind", 85 | "source" : "/etc/hostname", 86 | "options" : [ 87 | "rbind", 88 | "ro" 89 | ], 90 | "destination" : "/etc/hostname" 91 | }, 92 | { 93 | "type" : "bind", 94 | "source" : "/etc/resolv.conf", 95 | "options" : [ 96 | "rbind", 97 | "ro" 98 | ], 99 | "destination" : "/etc/resolv.conf" 100 | }, 101 | { 102 | "type" : "bind", 103 | "source" : "/tmp/.X11-unix", 104 | "options" : [ 105 | "rbind", 106 | "rw" 107 | ], 108 | "destination" : "/tmp/.X11-unix" 109 | }, 110 | { 111 | "type" : "bind", 112 | "source" : "volumes/root", 113 | "destination" : "/root", 114 | "options" : [ 115 | "rbind", 116 | "rw" 117 | ] 118 | }, 119 | { 120 | "type" : "bind", 121 | "source" : "volumes/home", 122 | "destination" : "/home", 123 | "options" : [ 124 | "rbind", 125 | "rw" 126 | ] 127 | }, 128 | { 129 | "destination": "/proc", 130 | "type": "proc", 131 | "source": "proc" 132 | }, 133 | { 134 | "destination": "/dev", 135 | "type": "tmpfs", 136 | "source": "tmpfs", 137 | "options": [ 138 | "nosuid", 139 | "strictatime", 140 | "mode=755", 141 | "size=65536k" 142 | ] 143 | }, 144 | { 145 | "destination": "/dev/pts", 146 | "type": "devpts", 147 | "source": "devpts", 148 | "options": [ 149 | "nosuid", 150 | "noexec", 151 | "newinstance", 152 | "ptmxmode=0666", 153 | "mode=0620", 154 | "gid=5" 155 | ] 156 | }, 157 | { 158 | "destination": "/dev/shm", 159 | "type": "tmpfs", 160 | "source": "shm", 161 | "options": [ 162 | "nosuid", 163 | "noexec", 164 | "nodev", 165 | "mode=1777", 166 | "size=65536k" 167 | ] 168 | }, 169 | { 170 | "destination": "/dev/mqueue", 171 | "type": "mqueue", 172 | "source": "mqueue", 173 | "options": [ 174 | "nosuid", 175 | "noexec", 176 | "nodev" 177 | ] 178 | }, 179 | { 180 | "destination": "/sys", 181 | "type": "sysfs", 182 | "source": "sysfs", 183 | "options": [ 184 | "nosuid", 185 | "noexec", 186 | "nodev", 187 | "ro" 188 | ] 189 | }, 190 | { 191 | "destination": "/sys/fs/cgroup", 192 | "type": "cgroup", 193 | "source": "cgroup", 194 | "options": [ 195 | "nosuid", 196 | "noexec", 197 | "nodev", 198 | "relatime", 199 | "ro" 200 | ] 201 | } 202 | ], 203 | "linux": { 204 | "resources": { 205 | "devices": [ 206 | { 207 | "allow": false, 208 | "access": "rwm" 209 | } 210 | ] 211 | }, 212 | "namespaces": [ 213 | { 214 | "type": "pid" 215 | }, 216 | { 217 | "type": "ipc" 218 | }, 219 | { 220 | "type": "uts" 221 | }, 222 | { 223 | "type": "mount" 224 | } 225 | ], 226 | "maskedPaths": [ 227 | "/proc/kcore", 228 | "/proc/latency_stats", 229 | "/proc/timer_list", 230 | "/proc/timer_stats", 231 | "/proc/sched_debug", 232 | "/sys/firmware" 233 | ], 234 | "readonlyPaths": [ 235 | "/proc/asound", 236 | "/proc/bus", 237 | "/proc/fs", 238 | "/proc/irq", 239 | "/proc/sys", 240 | "/proc/sysrq-trigger" 241 | ] 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :warning: FILEgrain is abandoned in favor of stargz/CRFS. See [containerd#3731](https://github.com/containerd/containerd/issues/3731) and https://github.com/ktock/remote-snapshotter 2 | 3 | - - - 4 | # FILEgrain: transport-agnostic, fine-grained content-addressable container image layout 5 | 6 | [![Build Status](https://travis-ci.org/AkihiroSuda/filegrain.svg)](https://travis-ci.org/AkihiroSuda/filegrain) 7 | [![GoDoc](https://godoc.org/github.com/AkihiroSuda/filegrain?status.svg)](https://godoc.org/github.com/AkihiroSuda/filegrain) 8 | 9 | FILEgrain is a (long-term) proposal to extend [OCI Image Format](https://github.com/opencontainers/image-spec) to support CAS in the granularity of file, in a transport-agnostic way. 10 | 11 | **Your feedback is welcome.** 12 | 13 | ## Talks 14 | 15 | - [Open Source Summit North America (September 11, 2017, Los Angeles)](https://ossna2017.sched.com/event/BDpM/filegrain-transport-agnostic-fine-grained-content-addressable-container-image-layout-akihiro-suda-ntt) 16 | 17 | ## Pros and Cons 18 | 19 | Pros: 20 | * Higher concurrency in pulling image, in a transport-agnostic way 21 | * Files can be lazy-pulled. i.e. Files can appear at the filesystem before it is actually pulled. 22 | * Finer deduplication granularity 23 | 24 | Cons: 25 | * The `blobs` directory in the image can contain a large number of files. So, `readdir()` for the directory is likely to become slow. This could be mitigated by using [external blob stores](#future-support-for-ipfs-blob-store) though. 26 | 27 | ## Format 28 | 29 | FILEgrain defines the image manifest which is almost identical to the OCI image manifest, but different in the following points: 30 | 31 | * FILEgrain image manifest supports [continuity manifest](https://github.com/containerd/continuity) (`application/vnd.continuity.manifest.v0+pb` and `...+json`) as an [Image Layer Filesystem Changeset](https://github.com/opencontainers/image-spec/blob/master/layer.md). Regular files in an image are stored as OCI blob and accessed via the digest value recorded in the continuity manifest. FILEgrain still supports tar layers (`application/vnd.oci.image.layer.v1.tar` and its families), and it is even possible to put a continuity layer on top of tar layers, and vice versa. Tar layers might be useful for enforcing a lot of small files to be downloaded in batch (as a single tar file). 32 | * FILEgrain image manifest SHOULD have an annotation `filegrain.version=20170501`, in both the manifest JSON itself and the image index JSON. This annotation WILL change in future versions. 33 | 34 | It is possible and recommended to put both a FILEgrain manifest file and an OCI manifest file in a single image. 35 | 36 | ## Example 37 | [image index](https://github.com/opencontainers/image-spec/blob/latest/image-index.md): 38 | (The second entry is a FILEgrain manifest) 39 | ```json 40 | { 41 | "schemaVersion": 2, 42 | "manifests": [ 43 | { 44 | "mediaType": "application/vnd.oci.image.manifest.v1+json", 45 | ... 46 | }, 47 | { 48 | "mediaType": "application/vnd.oci.image.manifest.v1+json", 49 | ..., 50 | "annotations": { 51 | "filegrain.version": "20170501" 52 | } 53 | } 54 | ] 55 | } 56 | ``` 57 | 58 | [image manifest](https://github.com/opencontainers/image-spec/blob/latest/image-manifest.md): 59 | (a continuity layer on top of a tar layer) 60 | ```json 61 | { 62 | "schemaVersion": 2, 63 | "layers": [ 64 | { 65 | "mediaType": "application/vnd.continuity.manifest.v0+json", 66 | ... 67 | }, 68 | { 69 | "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", 70 | .., 71 | } 72 | ], 73 | "annotations": { 74 | "filegrain.version": "20170501" 75 | } 76 | } 77 | ``` 78 | 79 | ## Distribution 80 | 81 | FILEgrain is designed agnostic to transportation and hence can be distribeted in any way. 82 | 83 | My personal recommendation is to just put the image directory to [IPFS](https://ipfs.io). 84 | However, I intentionally designed FILEgrain _not_ to use IPFS multiaddr/multihash. 85 | 86 | ### Future support for IPFS blob store 87 | 88 | So as to avoid putting a lot file into a single OCI blob directory, it might be good to consider using IPFS as an additional blob store. 89 | 90 | IPFS store support is not yet undertaken, but it would be like this: 91 | 92 | ```json 93 | { 94 | "schemaVersion": 2, 95 | "layers": [ 96 | { 97 | "mediaType": "application/vnd.continuity.manifest.v0+json", 98 | ..., 99 | "annotations": { 100 | "filegrain.ipfs": "QmFooBar" 101 | } 102 | } 103 | ], 104 | "annotations": { 105 | "filegrain.version": "2017XXXX" 106 | } 107 | } 108 | ``` 109 | 110 | In this case, the layer SHOULD be fetch via IPFS multihash, rather than the digest values specified in the continuity manifest. 111 | Also, the continuity manifest MAY omit digest values, since IPFS provides them redundantly. 112 | 113 | Note that this is different from just putting the `blobs` directory onto IPFS, which would still create a lot of files on a single directory, when pulled from non-FILEgrain implementation. 114 | 115 | 116 | ## POC 117 | 118 | Builder: 119 | 120 | - [ ] Build a FILEgrain image from an existing OCI image (`--source-type oci-image`) 121 | - [X] Build a FILEgrain image from an existing Docker image (`--source-type docker-image`) 122 | - [X] Build a FILEgrain image from a raw rootfs directory (`--source-type rootfs`) 123 | 124 | Lazy Puller: 125 | 126 | - [X] OCI-style directory on a generic filesystem (`blobs/sha256/deadbeef..`) 127 | - [ ] Docker registry 128 | - [ ] IPFS multihash (See [Future support for IPFS blob store](#future-support-for-ipfs-blob-store) section) 129 | 130 | Mounter: 131 | 132 | - [X] Read-only mount using FUSE (Linux) 133 | 134 | Writable mount is not planned at the moment, as FILEgrain is optimized for "cattles" rather than "pets". 135 | Users should use bind-mount or some union filesystems for `/tmp`, `/run`, and `/home`. 136 | 137 | ### POC Usage 138 | 139 | Install FILEgrain binary: 140 | 141 | ```console 142 | $ go get github.com/AkihiroSuda/filegrain 143 | ``` 144 | 145 | Convert a Docker image (e.g. `java:8`) to a FILEgrain image `/tmp/filegrain-image`: 146 | 147 | ```console 148 | # filegrain build -o /tmp/filegrain-image --source-type docker-image java:8 149 | ``` 150 | 151 | Prepare an OCI bundle `/tmp/bundle.sh `from [`./oci-runtime-bundle.template`](./oci-runtime-bundle.template/README.md): 152 | ```console 153 | # cp -r ./oci-runtime-bundle.template /tmp/bundle 154 | # cd /tmp/bundle 155 | # ./prepare.sh 156 | ``` 157 | 158 | Mount the local FILEgrain image `/tmp/filegrain-image` on `/tmp/bundle/rootfs`: 159 | ```console 160 | # filegrain mount /tmp/filegrain-image /tmp/bundle/rootfs 161 | ``` 162 | In future, `filegrain mount` should support mounting remote images over Docker Registry HTTP API as well. 163 | 164 | Open another terminal, and start runC with the bundle `/tmp/bundle`: 165 | ```console 166 | # cd /tmp/bundle 167 | # runc run foo 168 | ``` 169 | Instead of runc, you will be able to use `docker run` as well when Docker supports running an arbitrary OCI runtime bundle. 170 | 171 | The container starts without pulling all the blobs. 172 | Pulled blobs can be found on `/tmp/filegrain-blobcacheXXXXX`: 173 | 174 | ```console 175 | # du -hs /tmp/filegrain-blobcache* 176 | ``` 177 | 178 | This directory grows as you `read(2)` files within the container rootfs. 179 | 180 | ### POC Benchmark 181 | 182 | Please refer to [#17](https://github.com/AkihiroSuda/filegrain/issues/17). 183 | 184 | e.g. Pulling 352MB of blobs is enough for using NLTK with 8.3GB `kaggle/python` image. 185 | 186 | ## Similar work 187 | 188 | ### Lazy distribution 189 | - [Harter, Tyler, et al. "Slacker: Fast Distribution with Lazy Docker Containers." FAST. 2016.](https://www.usenix.org/conference/fast16/technical-sessions/presentation/harter) 190 | - [Lestaris, George. "Alternatives to layer-based image distribution: using CERN filesystem for images." Container Camp UK. 2016.](http://www.slideshare.net/glestaris/alternatives-to-layerbased-image-distribution-using-cern-filesystem-for-images) 191 | - [Blomer, Jakob, et al. "A Novel Approach for Distributing and Managing Container Images: Integrating CernVM File System and Mesos." MesosCon NA. 2016.](https://mesosconna2016.sched.com/event/6jtr/a-novel-approach-for-distributing-and-managing-container-images-integrating-cernvm-file-system-and-mesos-jakob-blomer-cern-jie-yu-artem-harutyunyan-mesosphere) 192 | 193 | ## FAQ 194 | 195 | **Q. Why not just use IPFS directory? It is CAS in the granularity of file.** 196 | 197 | A. Because IPFS does not support metadata of files. Also, this way is not transport-agnostic. 198 | 199 | **Q. Usecases for lazy-pulling?** 200 | 201 | A. Here are some examples I can come up with: 202 | 203 | - Huge web application composed of a lot of static HTML and graphic files 204 | - Huge scientific data (a content-addressable image with full code and full data would be great for reproducible research) 205 | - Huge OS image (e.g. Windows Server, Linux with VNC desktop) 206 | - Huge runtime (e.g. Java, dotNET) 207 | - Huge image that is composed of multiple software stack for integration testing 208 | 209 | Please also refer to the list of [similar work about lazy distribution](#similar-work). 210 | 211 | **Q. Isn't it a bad idea to put a lot of file into a single blobs directory?** 212 | 213 | A. This could be mitigated by avoid putting file into the OCI blob store, and use an external blob store instead e.g. IPFS. (go-ipfs supports [sharding](https://github.com/ipfs/go-ipfs/pull/3042)), although not transport-agnostic. 214 | See also [an idea about future support for IPFS blob store](#future-support-for-ipfs-blob-store). 215 | 216 | Also, there is an idea to implement sharding to the OCI native blob store: [opencontainers/image-spec#449](https://github.com/opencontainers/image-spec/issues/449). 217 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------