add a bunch of methods to the client end

This commit is contained in:
Barak Michener 2018-03-30 21:34:51 -07:00
parent a5d237df2a
commit 5ab2cbd265
8 changed files with 236 additions and 17 deletions

View file

@ -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.

View file

@ -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
}

View file

@ -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
}

View file

@ -29,6 +29,8 @@ func mkMountOpts(opts WsFsOpts) *fuse.MountOptions {
}
mountOpts := &fuse.MountOptions{
Options: fusermountopts,
FsName: "kubelwagen",
Name: "wsfs",
}
return mountOpts
}

View file

@ -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"`
}

34
vendor/github.com/hanwen/go-fuse/fuse/poll.go generated vendored Normal file
View file

@ -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
}
}

49
vendor/github.com/hanwen/go-fuse/fuse/poll_darwin.go generated vendored Normal file
View file

@ -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
}

25
vendor/github.com/hanwen/go-fuse/fuse/poll_linux.go generated vendored Normal file
View file

@ -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
}