add a bunch of methods to the client end
This commit is contained in:
parent
a5d237df2a
commit
5ab2cbd265
8 changed files with 236 additions and 17 deletions
28
README.md
28
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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
|||
61
fs/local.go
61
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
|
||||
|
||||
}
|
||||
|
|
|
|||
2
fuse.go
2
fuse.go
|
|
@ -29,6 +29,8 @@ func mkMountOpts(opts WsFsOpts) *fuse.MountOptions {
|
|||
}
|
||||
mountOpts := &fuse.MountOptions{
|
||||
Options: fusermountopts,
|
||||
FsName: "kubelwagen",
|
||||
Name: "wsfs",
|
||||
}
|
||||
return mountOpts
|
||||
}
|
||||
|
|
|
|||
17
request.go
17
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"`
|
||||
}
|
||||
|
|
|
|||
34
vendor/github.com/hanwen/go-fuse/fuse/poll.go
generated
vendored
Normal file
34
vendor/github.com/hanwen/go-fuse/fuse/poll.go
generated
vendored
Normal 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
49
vendor/github.com/hanwen/go-fuse/fuse/poll_darwin.go
generated
vendored
Normal 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
25
vendor/github.com/hanwen/go-fuse/fuse/poll_linux.go
generated
vendored
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue