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
|
# kubelwagen
|
||||||
*Getting your dev environment to the front lines*
|
*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
|
## Setting it up
|
||||||
|
|
||||||
```
|
```
|
||||||
kubelwagen serve [LISTEN HOSTPORT] [TARGET DIRECTORY]
|
kubelwagen serve [TARGET DIRECTORY]
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
kubelwagen connect [TARGET ADDRESS] [SOURCE 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
|
## Licensing
|
||||||
|
|
||||||
Distributed under the GPL, as it's a dev tool and not meant as a product. Use it internally. Share alike.
|
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"
|
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) {
|
func (fs *WsFs) OpenDir(name string, context *fuse.Context) ([]fuse.DirEntry, fuse.Status) {
|
||||||
r := Request{
|
r := Request{
|
||||||
Method: MethodOpenDir,
|
Method: MethodOpenDir,
|
||||||
Path: name,
|
Path: name,
|
||||||
}
|
}
|
||||||
c := getChannel()
|
resp, ok := fs.getResponse(&r)
|
||||||
fs.req <- RequestCallback{
|
|
||||||
message: r,
|
|
||||||
response: c,
|
|
||||||
}
|
|
||||||
resp, ok := <-c
|
|
||||||
if !ok {
|
if !ok {
|
||||||
logrus.Errorln("Response to request channel closed")
|
logrus.Errorln("Response to request channel closed")
|
||||||
return fs.FileSystem.OpenDir(name, context)
|
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,
|
Method: MethodGetAttr,
|
||||||
Path: name,
|
Path: name,
|
||||||
}
|
}
|
||||||
c := getChannel()
|
resp, ok := fs.getResponse(&r)
|
||||||
fs.req <- RequestCallback{
|
|
||||||
message: r,
|
|
||||||
response: c,
|
|
||||||
}
|
|
||||||
resp, ok := <-c
|
|
||||||
if !ok {
|
if !ok {
|
||||||
logrus.Errorln("Response to request channel closed")
|
logrus.Errorln("Response to request channel closed")
|
||||||
return fs.FileSystem.GetAttr(name, context)
|
return fs.FileSystem.GetAttr(name, context)
|
||||||
}
|
}
|
||||||
return resp.Stat, resp.Code
|
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"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"github.com/barakmich/kubelwagen"
|
"github.com/barakmich/kubelwagen"
|
||||||
"github.com/hanwen/go-fuse/fuse"
|
"github.com/hanwen/go-fuse/fuse"
|
||||||
|
|
@ -28,6 +29,38 @@ func (fs *LocalFs) Handle(r *kubelwagen.Request) *kubelwagen.Response {
|
||||||
return fs.openDir(r)
|
return fs.openDir(r)
|
||||||
case kubelwagen.MethodGetAttr:
|
case kubelwagen.MethodGetAttr:
|
||||||
return fs.getAttr(r)
|
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{
|
return &kubelwagen.Response{
|
||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
|
|
@ -99,3 +132,31 @@ func (fs *LocalFs) getAttr(r *kubelwagen.Request) *kubelwagen.Response {
|
||||||
out.Stat = fuse.ToAttr(fi)
|
out.Stat = fuse.ToAttr(fi)
|
||||||
return out
|
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{
|
mountOpts := &fuse.MountOptions{
|
||||||
Options: fusermountopts,
|
Options: fusermountopts,
|
||||||
|
FsName: "kubelwagen",
|
||||||
|
Name: "wsfs",
|
||||||
}
|
}
|
||||||
return mountOpts
|
return mountOpts
|
||||||
}
|
}
|
||||||
|
|
|
||||||
17
request.go
17
request.go
|
|
@ -8,18 +8,32 @@ const (
|
||||||
MethodInvalid Method = iota
|
MethodInvalid Method = iota
|
||||||
MethodOpenDir
|
MethodOpenDir
|
||||||
MethodGetAttr
|
MethodGetAttr
|
||||||
|
MethodStatFs
|
||||||
|
MethodChmod
|
||||||
|
MethodChown
|
||||||
|
MethodTruncate
|
||||||
|
MethodMknod
|
||||||
|
MethodMkdir
|
||||||
|
MethodUnlink
|
||||||
|
MethodRmdir
|
||||||
|
MethodSymlink
|
||||||
|
MethodRename
|
||||||
|
MethodLink
|
||||||
|
MethodReadLink
|
||||||
|
MethodAccess
|
||||||
)
|
)
|
||||||
|
|
||||||
type Request struct {
|
type Request struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Method Method `json:"method"`
|
Method Method `json:"method"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Mode int32
|
Mode uint32
|
||||||
UID int32
|
UID int32
|
||||||
GID int32
|
GID int32
|
||||||
Size int32
|
Size int32
|
||||||
Dev int32
|
Dev int32
|
||||||
Flags int32
|
Flags int32
|
||||||
|
Offset int64
|
||||||
NewPath string `json:"newpath,omitempty"`
|
NewPath string `json:"newpath,omitempty"`
|
||||||
Attr string `json:"attr,omitempty"`
|
Attr string `json:"attr,omitempty"`
|
||||||
Data []byte `json:"data,omitempty"`
|
Data []byte `json:"data,omitempty"`
|
||||||
|
|
@ -38,4 +52,5 @@ type Response struct {
|
||||||
Attrs []string `json:"attrs,omitempty"`
|
Attrs []string `json:"attrs,omitempty"`
|
||||||
Dirents []fuse.DirEntry `json:"dirents,omitempty"`
|
Dirents []fuse.DirEntry `json:"dirents,omitempty"`
|
||||||
LinkStr string `json:"linkstr,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