diff --git a/README.md b/README.md index 3484cfd..4443b53 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,43 @@ # kubelwagen *Getting your dev environment to the front lines* -It's great to have a consistent environment in containers, and it's great to have services run on Kubernetes, but it's a pain to build dev containers to be run as pods on Kubernetes. +It's great to have a consistent environment in containers, and it's great to have services run on Kubernetes, but it's a pain to build dev containers to be run as pods on Kubernetes. `git pull` on pod creation helps, but it still requires you commit and push the code, and restart the pod. -It would be great if your live development working directory could be mounted into a running pod on the powerful metal in the cloud. Plus, it would obviate the need for workaround services such as (localhost.dev or something?) to share a running WIP instance as it would have a valid service on the dev cluster, ingressable and everything. +It would be great if your live development working directory could be mounted into a running pod on the powerful metal in the cloud. Plus, it would obviate the need for workaround services such as [ngrok](https://ngrok.com) to share a running WIP instance, as it would have a valid service on the dev cluster, complete with Ingress resources and everything. Plus, it's running in-cluster; so all the configurations and access controls and service discoveries are available to your dev instance. With the absolute latest, WIP code from your laptop. -Kubelwagen is a sidecar container that provides a FUSE directory to the running pod that is mounted in from a client connecting in over HTTP. +Use it in tandem with automatically refreshing dev environments -- JS auto-reloaders for frontend devs, [entr](https://entrproject.org) for general reloads, or any other tooling you like. + +Kubelwagen is a sidecar (hah) container that provides a FUSE directory to the running pod that is mounted in from a client connecting in over an HTTPS websocket. + +## FUSE over Websockets? Are you insane? + +It's a dev tool, HTTPS is pretty good, relax. Yeah, it'll be slow... but with appropriate caching, a few extra automatic reload milliseconds of the code you just changed is preferable to rebuilding a container or rescheduling a pod. Faster iteration times == happier devs. Closer parity to your laptop and production == happier ops. ## Setting it up ``` -kubelwagen serve [LISTEN HOSTPORT] [TARGET DIRECTORY] +kubelwagen serve [TARGET DIRECTORY] ``` ``` kubelwagen connect [TARGET ADDRESS] [SOURCE DIRECTORY] ``` +## Status & Todo + +#### Status +First mounting! `ls` over the internet to your heart's content! + +#### Todo +* Implement *all* the methods! + * XAttrs +* File handles +* Caching +* INotify +* Overlay (serve local directory when not connected) + + ## Licensing Distributed under the GPL, as it's a dev tool and not meant as a product. Use it internally. Share alike. diff --git a/filesystem.go b/filesystem.go index fbaa0da..e045ade 100644 --- a/filesystem.go +++ b/filesystem.go @@ -39,17 +39,22 @@ func (fs *WsFs) String() string { return "kubelwagen" } +func (fs *WsFs) getResponse(r *Request) (Response, bool) { + c := getChannel() + fs.req <- RequestCallback{ + message: *r, + response: c, + } + resp, ok := <-c + return resp, ok +} + func (fs *WsFs) OpenDir(name string, context *fuse.Context) ([]fuse.DirEntry, fuse.Status) { r := Request{ Method: MethodOpenDir, Path: name, } - c := getChannel() - fs.req <- RequestCallback{ - message: r, - response: c, - } - resp, ok := <-c + resp, ok := fs.getResponse(&r) if !ok { logrus.Errorln("Response to request channel closed") return fs.FileSystem.OpenDir(name, context) @@ -62,16 +67,24 @@ func (fs *WsFs) GetAttr(name string, context *fuse.Context) (*fuse.Attr, fuse.St Method: MethodGetAttr, Path: name, } - c := getChannel() - fs.req <- RequestCallback{ - message: r, - response: c, - } - resp, ok := <-c + resp, ok := fs.getResponse(&r) if !ok { logrus.Errorln("Response to request channel closed") return fs.FileSystem.GetAttr(name, context) } return resp.Stat, resp.Code +} + +func (fs *WsFs) StatFs(name string) *fuse.StatfsOut { + r := Request{ + Method: MethodStatFs, + Path: name, + } + resp, ok := fs.getResponse(&r) + if !ok { + logrus.Errorln("Response to request channel closed") + return fs.FileSystem.StatFs(name) + } + return resp.Statfs } diff --git a/fs/local.go b/fs/local.go index 3cbf76b..3516898 100644 --- a/fs/local.go +++ b/fs/local.go @@ -4,6 +4,7 @@ import ( "io" "os" "path/filepath" + "syscall" "github.com/barakmich/kubelwagen" "github.com/hanwen/go-fuse/fuse" @@ -28,6 +29,38 @@ func (fs *LocalFs) Handle(r *kubelwagen.Request) *kubelwagen.Response { return fs.openDir(r) case kubelwagen.MethodGetAttr: return fs.getAttr(r) + case kubelwagen.MethodStatFs: + return fs.statFs(r) + case kubelwagen.MethodChmod: + return fs.serveFromErr(r, os.Chmod(fs.getPath(r), os.FileMode(r.Mode))) + case kubelwagen.MethodChown: + return fs.serveFromErr(r, os.Chown(fs.getPath(r), int(r.UID), int(r.GID))) + case kubelwagen.MethodTruncate: + return fs.serveFromErr(r, os.Truncate(fs.getPath(r), r.Offset)) + case kubelwagen.MethodMknod: + return fs.serveFromErr(r, syscall.Mknod(fs.getPath(r), r.Mode, int(r.Dev))) + case kubelwagen.MethodMkdir: + return fs.serveFromErr(r, os.Mkdir(fs.getPath(r), os.FileMode(r.Mode))) + case kubelwagen.MethodUnlink: + return fs.serveFromErr(r, syscall.Unlink(fs.getPath(r))) + case kubelwagen.MethodRmdir: + return fs.serveFromErr(r, syscall.Rmdir(fs.getPath(r))) + case kubelwagen.MethodSymlink: + return fs.serveFromErr(r, os.Symlink(r.NewPath, fs.getPath(r))) + case kubelwagen.MethodRename: + return fs.serveFromErr(r, os.Rename(fs.getPath(r), filepath.Join(fs.base, r.NewPath))) + case kubelwagen.MethodLink: + return fs.serveFromErr(r, os.Link(fs.getPath(r), filepath.Join(fs.base, r.NewPath))) + case kubelwagen.MethodReadLink: + f, err := os.Readlink(fs.getPath(r)) + if err != nil { + return kubelwagen.ErrorResp(r, nil) + } + out := fs.serveFromErr(r, err) + out.LinkStr = f + return out + case kubelwagen.MethodAccess: + return fs.serveFromErr(r, syscall.Access(fs.getPath(r), r.Mode)) } return &kubelwagen.Response{ ID: r.ID, @@ -99,3 +132,31 @@ func (fs *LocalFs) getAttr(r *kubelwagen.Request) *kubelwagen.Response { out.Stat = fuse.ToAttr(fi) return out } + +func (fs *LocalFs) statFs(r *kubelwagen.Request) *kubelwagen.Response { + out := &kubelwagen.Response{ + ID: r.ID, + Code: fuse.OK, + } + s := syscall.Statfs_t{} + err := syscall.Statfs(fs.getPath(r), &s) + if err != nil { + return kubelwagen.ErrorResp(r, err) + } + g := &fuse.StatfsOut{} + g.FromStatfsT(&s) + out.Statfs = g + return out +} + +func (fs *LocalFs) serveFromErr(r *kubelwagen.Request, err error) *kubelwagen.Response { + out := &kubelwagen.Response{ + ID: r.ID, + Code: fuse.OK, + } + if err != nil { + return kubelwagen.ErrorResp(r, err) + } + return out + +} diff --git a/fuse.go b/fuse.go index 5d68f29..1b90c37 100644 --- a/fuse.go +++ b/fuse.go @@ -29,6 +29,8 @@ func mkMountOpts(opts WsFsOpts) *fuse.MountOptions { } mountOpts := &fuse.MountOptions{ Options: fusermountopts, + FsName: "kubelwagen", + Name: "wsfs", } return mountOpts } diff --git a/request.go b/request.go index fa963ba..bb39bd5 100644 --- a/request.go +++ b/request.go @@ -8,18 +8,32 @@ const ( MethodInvalid Method = iota MethodOpenDir MethodGetAttr + MethodStatFs + MethodChmod + MethodChown + MethodTruncate + MethodMknod + MethodMkdir + MethodUnlink + MethodRmdir + MethodSymlink + MethodRename + MethodLink + MethodReadLink + MethodAccess ) type Request struct { ID int `json:"id"` Method Method `json:"method"` Path string `json:"path"` - Mode int32 + Mode uint32 UID int32 GID int32 Size int32 Dev int32 Flags int32 + Offset int64 NewPath string `json:"newpath,omitempty"` Attr string `json:"attr,omitempty"` Data []byte `json:"data,omitempty"` @@ -38,4 +52,5 @@ type Response struct { Attrs []string `json:"attrs,omitempty"` Dirents []fuse.DirEntry `json:"dirents,omitempty"` LinkStr string `json:"linkstr,omitempty"` + Statfs *fuse.StatfsOut `json:"statfs,omitempty"` } diff --git a/vendor/github.com/hanwen/go-fuse/fuse/poll.go b/vendor/github.com/hanwen/go-fuse/fuse/poll.go new file mode 100644 index 0000000..d158a14 --- /dev/null +++ b/vendor/github.com/hanwen/go-fuse/fuse/poll.go @@ -0,0 +1,34 @@ +package fuse + +// Go 1.9 introduces polling for file I/O. The implementation causes +// the runtime's epoll to take up the last GOMAXPROCS slot, and if +// that happens, we won't have any threads left to service FUSE's +// _OP_POLL request. Prevent this by forcing _OP_POLL to happen, so we +// can say ENOSYS and prevent further _OP_POLL requests. +const pollHackName = ".go-fuse-epoll-hack" +const pollHackInode = ^uint64(0) + +func doPollHackLookup(ms *Server, req *request) { + switch req.inHeader.Opcode { + case _OP_CREATE: + out := (*CreateOut)(req.outData()) + out.EntryOut = EntryOut{ + NodeId: pollHackInode, + Attr: Attr{ + Ino: pollHackInode, + Mode: S_IFREG | 0644, + Nlink: 1, + }, + } + out.OpenOut = OpenOut{ + Fh: pollHackInode, + } + req.status = OK + case _OP_LOOKUP: + out := (*EntryOut)(req.outData()) + *out = EntryOut{} + req.status = ENOENT + default: + req.status = EIO + } +} diff --git a/vendor/github.com/hanwen/go-fuse/fuse/poll_darwin.go b/vendor/github.com/hanwen/go-fuse/fuse/poll_darwin.go new file mode 100644 index 0000000..84cc980 --- /dev/null +++ b/vendor/github.com/hanwen/go-fuse/fuse/poll_darwin.go @@ -0,0 +1,49 @@ +package fuse + +import ( + "path/filepath" + "syscall" + "unsafe" +) + +type pollFd struct { + Fd int32 + Events int16 + Revents int16 +} + +func sysPoll(fds []pollFd, timeout int) (n int, err error) { + r0, _, e1 := syscall.Syscall(syscall.SYS_POLL, uintptr(unsafe.Pointer(&fds[0])), + uintptr(len(fds)), uintptr(timeout)) + n = int(r0) + if e1 != 0 { + err = syscall.Errno(e1) + } + return n, err +} + +func pollHack(mountPoint string) error { + const ( + POLLIN = 0x1 + POLLPRI = 0x2 + POLLOUT = 0x4 + POLLRDHUP = 0x2000 + POLLERR = 0x8 + POLLHUP = 0x10 + ) + + fd, err := syscall.Open(filepath.Join(mountPoint, pollHackName), syscall.O_CREAT|syscall.O_TRUNC|syscall.O_RDWR, 0644) + if err != nil { + return err + } + pollData := []pollFd{{ + Fd: int32(fd), + Events: POLLIN | POLLPRI | POLLOUT, + }} + + // Trigger _OP_POLL, so we can say ENOSYS. We don't care about + // the return value. + sysPoll(pollData, 0) + syscall.Close(fd) + return nil +} diff --git a/vendor/github.com/hanwen/go-fuse/fuse/poll_linux.go b/vendor/github.com/hanwen/go-fuse/fuse/poll_linux.go new file mode 100644 index 0000000..6b8924a --- /dev/null +++ b/vendor/github.com/hanwen/go-fuse/fuse/poll_linux.go @@ -0,0 +1,25 @@ +package fuse + +import ( + "path/filepath" + "syscall" + + "golang.org/x/sys/unix" +) + +func pollHack(mountPoint string) error { + fd, err := syscall.Creat(filepath.Join(mountPoint, pollHackName), syscall.O_CREAT) + if err != nil { + return err + } + pollData := []unix.PollFd{{ + Fd: int32(fd), + Events: unix.POLLIN | unix.POLLPRI | unix.POLLOUT, + }} + + // Trigger _OP_POLL, so we can say ENOSYS. We don't care about + // the return value. + unix.Poll(pollData, 0) + syscall.Close(fd) + return nil +}