Files
dockerfs_research/main.go
2024-11-14 19:56:55 -05:00

285 lines
6.8 KiB
Go

package main
import (
"errors"
"strconv"
"context"
"os/user"
"io"
"sync"
"github.com/knusbaum/go9p"
"github.com/knusbaum/go9p/fs"
"github.com/knusbaum/go9p/proto"
dtypes "github.com/docker/docker/api/types"
containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
)
///
///
/// Dynamic Tree Implementation
///
///
type NewStat func (name, uid, gid string, mode uint32) *proto.Stat
type Mappable interface {
MapFS(t *Tree) []fs.FSNode
}
type NodeGenerator func (t *Tree) []fs.FSNode;
func (ng NodeGenerator) MapFS(t *Tree) []fs.FSNode { return ng(t) };
type Tree struct {
tStat proto.Stat
tParent fs.Dir
//tcache []fs.FSNode
NodeNS NewStat
//NodeGen NodeGenerator
NodeGen Mappable
}
func (t *Tree) Stat() proto.Stat { return t.tStat }
func (t *Tree) WriteStat(s *proto.Stat) error { return errors.New("attributes are read only") }
func (t *Tree) SetParent(p fs.Dir) {}
func (t *Tree) Parent() fs.Dir { return t.tParent }
func (t *Tree) Children() map[string]fs.FSNode {
ret := make(map[string]fs.FSNode)
for _, n := range t.NodeGen.MapFS(t) { //t.tcache { //t.NodeGen.MapFS(t) { //t.NodeGen(t) {
ret[n.Stat().Name] = n
}
return ret
}
//func (t *Tree) Update() {
// t.tcache = t.NodeGen.MapFS(t)
//}
//func NewTree(s *proto.Stat, p fs.Dir, ns NewStat, ng NodeGenerator) Tree {
func NewTree(s *proto.Stat, p fs.Dir, ns NewStat, ng Mappable) *Tree {
t := &Tree{
tStat: *s,
tParent: p,
//tcache: make([]fs.FSNode, 0),
NodeNS: ns,
NodeGen: ng,
}
t.tStat.Mode |= proto.DMDIR
t.tStat.Qid.Qtype = uint8(t.tStat.Mode >> 24)
//t.Update()
return t
}
// This is just a helper for merging Mappables/NodeGenerators together
// Allows for making distinct node generators for sections of a Dynamic Tree
// without much hassle
func ConcatMapFS(m ...Mappable) NodeGenerator {
return func(t *Tree) []fs.FSNode {
ret := make([]fs.FSNode, 0)
for _, n := range m {
ret = append(ret, n.MapFS(t)...)
}
return ret
}
}
///
///
/// DockerFS Implementation
///
///
type ContainerNode dtypes.Container
func (cn ContainerNode) MapFS(t *Tree) []fs.FSNode {
hnewstat := func(name string) *proto.Stat { return t.NodeNS(name, t.Stat().Uid, t.Stat().Gid, 0444) }
ID_Node := fs.NewStaticFile(hnewstat("ID"), []byte(cn.ID))
Names_Node_ng := NodeGenerator(func(t *Tree) []fs.FSNode {
ret := make([]fs.FSNode, 0)
for i, n := range cn.Names {
ret = append(ret, fs.NewStaticFile(hnewstat(strconv.Itoa(i)), []byte(n)))
}
return ret
})
Names_Node := NewTree(t.NodeNS("Names", t.Stat().Uid, t.Stat().Gid, 0777), t, t.NodeNS, Names_Node_ng)
Image_Node := fs.NewStaticFile(hnewstat("Image"), []byte(cn.Image))
ImageID_Node := fs.NewStaticFile(hnewstat("ImageID"), []byte(cn.ImageID))
Command_Node := fs.NewStaticFile(hnewstat("Command"), []byte(cn.Command))
Created_Node := fs.NewStaticFile(hnewstat("Created"), []byte(strconv.Itoa(int(cn.Created))))
return []fs.FSNode{ID_Node, Names_Node, Image_Node, ImageID_Node, Command_Node, Created_Node}
}
type ClientNode struct {
*client.Client
ctx context.Context
}
// Alot of this was hacked together with ideas from fs.DynamicFile
type ContainerLogsNode struct {
nStat proto.Stat
nParent fs.Dir
sync.RWMutex
fidContent map[uint64]io.ReadCloser
cli *ClientNode
containerNode ContainerNode
}
func (n *ContainerLogsNode) Stat() proto.Stat { return n.nStat }
func (n *ContainerLogsNode) WriteStat(s *proto.Stat) error { return errors.New("attributes are read only") }
func (n *ContainerLogsNode) SetParent(p fs.Dir) {}
func (n *ContainerLogsNode) Parent() fs.Dir { return n.nParent }
func (n *ContainerLogsNode) Open(fid uint64, omode proto.Mode) error {
n.Lock()
defer n.Unlock()
out, err := n.cli.ContainerLogs(
//n.cli.ctx, n.nParent.Stat().Name,
n.cli.ctx, n.containerNode.ID,
// Tail is set to 0, but a default 1024 lines is planned
// Follow is set to true so the file can be read with a
// stream of the latest logs
// Options structs will later just be incorporated as
// part of either the ClientNode or ContainerNode with
// a ctl file for changing the options
containertypes.LogsOptions{ShowStdout: true, ShowStderr: true, Tail: "0", Follow: true},
)
if err != nil {
panic(err)
}
n.fidContent[fid] = out
return nil
}
func (n *ContainerLogsNode) Read(fid uint64, offset uint64, count uint64) ([]byte, error) {
n.Lock()
defer n.Unlock()
bs := make([]byte, count)
switch _, err := n.fidContent[fid].Read(bs); err {
case nil:
return bs, nil
case io.EOF:
return []byte{}, nil
default:
println(err.Error())
return []byte{}, err
}
}
func (n *ContainerLogsNode) Write(fid uint64, offset uint64, data []byte) (uint32, error) {
return 0, errors.New("Cannot write to file.")
}
func (n *ContainerLogsNode) Close(fid uint64) error { delete(n.fidContent, fid); return nil }
// This wasn't really that clever, idk maybe we'll see
/*
func (n *ContainerLogsNode) MapFS(t *Tree) []fs.FSNode {
n.nStat = *t.NodeNS("Logs", t.Stat().Uid, t.Stat().Gid, 0777)
n.nParent = t
n.fidContent = make(map[uint64]io.ReadCloser)
return []fs.FSNode{n}
}
*/
//func (n *ContainerLogsNode) MapFS(t *Tree)
func NewContainerLogsNode(s *proto.Stat, p fs.Dir, cliNode *ClientNode, containerNode ContainerNode) *ContainerLogsNode {
return &ContainerLogsNode{
nStat: *s,
nParent: p,
fidContent: make(map[uint64]io.ReadCloser),
cli: cliNode,
containerNode: containerNode,
}
}
func (cn *ClientNode) MapRunningContainers(t *Tree) []fs.FSNode {
ret := make([]fs.FSNode, 0)
containers, err := cn.ContainerList(cn.ctx, containertypes.ListOptions{})
if err != nil {
panic(err)
}
for _, c := range containers {
//cl := &ContainerLogsNode{cli: cn}
containerNode := ContainerNode(c)
api_ng := func(t *Tree) []fs.FSNode {
return []fs.FSNode{
NewContainerLogsNode(
t.NodeNS("Logs", t.Stat().Uid, t.Stat().Gid, 0777), t,
cn, containerNode,
),
}
}
ctree := NewTree(
t.NodeNS(c.ID, t.Stat().Uid, t.Stat().Gid, 0777),
t, t.NodeNS,
ConcatMapFS(
//ContainerNode(c),
containerNode,
NodeGenerator(api_ng),
),
)
ret = append(ret, ctree)
}
return ret
}
func (cn *ClientNode) MapFS(t *Tree) []fs.FSNode {
ret := make([]fs.FSNode, 0)
running_ng := NodeGenerator(cn.MapRunningContainers)
ret = append(ret, NewTree(
t.NodeNS("running", t.Stat().Uid, t.Stat().Gid, 0777),
t, t.NodeNS, running_ng,
))
return ret
}
func InitFS() *fs.FS {
var rfs fs.FS
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
panic(err)
}
clinode := &ClientNode{cli, ctx}
u, _ := user.Current()
s := rfs.NewStat("/", u.Username, u.Username, 0777)
t := NewTree(s, nil, rfs.NewStat, clinode)
rfs.Root = t
return &rfs
}
func main() {
rfs := InitFS()
go9p.Serve("localhost:9999", rfs.Server())
}