diff --git a/README.md b/README.md index 44e15a4..c8a4d0a 100644 --- a/README.md +++ b/README.md @@ -67,3 +67,7 @@ Creating of new files/directories, setting attributes is going to be done later. - Mkdir and file crating support. - Other FS features... + +- Daemonization + +- diff --git a/go.mod b/go.mod index 19739f5..4903dbf 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,8 @@ go 1.13 require ( github.com/hanwen/go-fuse/v2 v2.0.2 + github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect + github.com/manifoldco/promptui v0.8.0 + github.com/sevlyar/go-daemon v0.1.5 golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c // indirect ) diff --git a/go.sum b/go.sum index 1b8834c..573b280 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,33 @@ +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/hanwen/go-fuse v1.0.0 h1:GxS9Zrn6c35/BnfiVsZVWmsG803xwE7eVRDvcf/BEVc= github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok= github.com/hanwen/go-fuse/v2 v2.0.2 h1:BtsqKI5RXOqDMnTgpCb0IWgvRgGLJdqYVZ/Hm6KgKto= github.com/hanwen/go-fuse/v2 v2.0.2/go.mod h1:HH3ygZOoyRbP9y2q7y3+JM6hPL+Epe29IbWaS0UA81o= +github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= +github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= +github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo= +github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/sevlyar/go-daemon v0.1.5 h1:Zy/6jLbM8CfqJ4x4RPr7MJlSKt90f00kNM1D401C+Qk= +github.com/sevlyar/go-daemon v0.1.5/go.mod h1:6dJpPatBT9eUwM5VCw9Bt6CdX9Tk6UWvhW3MebLDRKE= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522 h1:Ve1ORMCxvRmSXBwJK+t3Oy+V2vRW2OetUQBq4rJIkZE= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c h1:jceGD5YNJGgGMkJz79agzOln1K9TaZUjv5ird16qniQ= golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/lib/dockerfs/container.go b/lib/dockerfs/container.go new file mode 100644 index 0000000..6dfec2a --- /dev/null +++ b/lib/dockerfs/container.go @@ -0,0 +1,17 @@ +package dockerfs + +import ( + "fmt" + "strings" +) + +type Container struct { + Id string + Names []string + Image string + Command string +} + +func (c *Container) String() string { + return fmt.Sprintf("%v %v (from %v): %v", c.Id[:8], strings.Join(c.Names, ", "), c.Image, c.Command) +} diff --git a/lib/dockerfs/docker.go b/lib/dockerfs/docker.go index 0f035b3..9fcb5d8 100644 --- a/lib/dockerfs/docker.go +++ b/lib/dockerfs/docker.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "io" + "io/ioutil" "net/http" "path/filepath" "strings" @@ -26,6 +27,9 @@ type dockerMng interface { // Save file SaveFile(path string, data []byte, stat *ContainerPathStat) (err error) + + // List containers + ContainersList() ([]Container, error) } var _ = (dockerMng)((*dockerMngImpl)(nil)) @@ -100,6 +104,24 @@ func (d *dockerMngImpl) GetFile(path string) (io.ReadCloser, error) { }, nil } +func (d *dockerMngImpl) ContainersList() ([]Container, error) { + url := "/containers/json" + resp, err := d.httpc.Get(url) + if err != nil { + return nil, fmt.Errorf("Get request to %q failed: %w", url, err) + } + defer resp.Body.Close() + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + var cts []Container + if err := json.Unmarshal(data, &cts); err != nil { + return nil, err + } + return cts, nil +} + type readCloser struct { reader io.Reader close func() error diff --git a/lib/dockerfs/docker_mock.go b/lib/dockerfs/docker_mock.go index 92d5852..449dc0a 100644 --- a/lib/dockerfs/docker_mock.go +++ b/lib/dockerfs/docker_mock.go @@ -184,3 +184,7 @@ func (d *dockerMngMock) SaveFile(path string, data []byte, stat *ContainerPathSt _, err = f.Write(data) return err } + +func (d *dockerMngMock) ContainersList() ([]Container, error) { + return nil, nil +} diff --git a/lib/manager/container.go b/lib/manager/container.go new file mode 100644 index 0000000..5288360 --- /dev/null +++ b/lib/manager/container.go @@ -0,0 +1,36 @@ +package manager + +import ( + "fmt" + "strings" + + "github.com/plesk/docker-fs/lib/dockerfs" +) + +type Container struct { + Id string + Names []string + Image string + Command string + + // + MountPoint string + Mounted bool + ShortId string + Name string +} + +func FromContainer(c *dockerfs.Container) Container { + return Container{ + Id: c.Id, + Names: c.Names, + Image: c.Image, + Command: c.Command, + ShortId: c.Id[:8], + Name: strings.TrimLeft(c.Names[0], "/"), + } +} + +func (c *Container) String() string { + return fmt.Sprintf("%v %v (from %v): %v", c.Id[:8], strings.Join(c.Names, ", "), c.Image, c.Command) +} diff --git a/lib/manager/manager.go b/lib/manager/manager.go new file mode 100644 index 0000000..a65f90e --- /dev/null +++ b/lib/manager/manager.go @@ -0,0 +1,166 @@ +package manager + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "os/exec" + "os/signal" + "path/filepath" + "syscall" + + "github.com/plesk/docker-fs/lib/log" + + "github.com/plesk/docker-fs/lib/dockerfs" + + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" + + daemon "github.com/sevlyar/go-daemon" +) + +type Manager struct { + statusPath string +} + +func New() *Manager { + home, err := os.UserHomeDir() + if err != nil { + log.Printf("[warning] Cannot detect user home directory. Use /tmp.") + home = "/tmp" + } + return &Manager{ + statusPath: filepath.Join(home, ".dockerfs.status.json"), + } +} + +func (m *Manager) ListContainers() ([]Container, error) { + httpc, err := dockerfs.NewClient("unix:/var/run/docker.sock") + if err != nil { + return nil, err + } + dmng := dockerfs.NewDockerMng(httpc, "") + list, err := dmng.ContainersList() + if err != nil { + return nil, err + } + status, err := m.readStatus() + if err != nil { + return nil, err + } + var result []Container + for _, c := range list { + ct := FromContainer(&c) + if s, ok := status[ct.Id]; ok { + ct.MountPoint = s + ct.Mounted = ct.MountPoint != "" + } + result = append(result, ct) + } + return result, nil +} + +func (m *Manager) MountContainer(containerId, mountPoint string, daemonize bool) error { + if err := m.writeStatus(containerId, mountPoint); err != nil { + return err + } + + if daemonize { + ctx := daemon.Context{} + child, err := ctx.Reborn() + if err != nil { + return fmt.Errorf("Daemonization failed: %w", err) + } + if child != nil { + // parent process + return nil + } + } + + log.Printf("[info] Check if mount directory exists (%v)...", mountPoint) + if err := os.MkdirAll(mountPoint, 0755); err != nil { + return err + } + log.Printf("[info] Fetching content of container %v...", containerId) + dockerMng := dockerfs.NewMng(containerId) + if err := dockerMng.Init(); err != nil { + return fmt.Errorf("dockerMng.Init() failed: %w", err) + } + + root := dockerMng.Root() + + log.Printf("[info] Mounting FS to %v...", mountPoint) + server, err := fs.Mount(mountPoint, root, &fs.Options{}) + if err != nil { + return fmt.Errorf("Mount failed: %w", err) + } + + log.Printf("[info] Setting up signal handler...") + osSignalChannel := make(chan os.Signal, 1) + signal.Notify(osSignalChannel, syscall.SIGTERM, syscall.SIGINT) + go shutdown(server, osSignalChannel) + + log.Printf("[info] OK!") + server.Wait() + log.Printf("[info] Server finished.") + + return m.writeStatus(containerId, "") +} + +func (m *Manager) UnmountContainer(id, path string) error { + cmd := exec.Command("umount", path) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + _ = cmd.Run() + return m.writeStatus(id, "") +} + +func (m *Manager) writeStatus(id, path string) error { + fmt.Printf("write status: %q = %q\n", id, path) + status, err := m.readStatus() + if err != nil { + return err + } + if path != "" { + absPath, err := filepath.Abs(path) + if err != nil { + return err + } + status[id] = absPath + } else { + delete(status, id) + } + data, err := json.Marshal(status) + if err != nil { + return err + } + fmt.Printf("status => %s\n", data) + return ioutil.WriteFile(m.statusPath, data, 0644) +} + +func (m *Manager) readStatus() (map[string]string, error) { + data, err := ioutil.ReadFile(m.statusPath) + if os.IsNotExist(err) { + return map[string]string{}, nil + } + if err != nil { + return nil, err + } + status := map[string]string{} + if err := json.Unmarshal(data, &status); err != nil { + return nil, err + } + return status, nil +} + +func shutdown(server *fuse.Server, signals <-chan os.Signal) { + <-signals + if err := server.Unmount(); err != nil { + log.Printf("[warning] server unmount failed: %v", err) + os.Exit(1) + } + + log.Printf("[info] Unmount successful.") + os.Exit(0) +} diff --git a/lib/tui/templates.go b/lib/tui/templates.go new file mode 100644 index 0000000..e56f367 --- /dev/null +++ b/lib/tui/templates.go @@ -0,0 +1,24 @@ +package tui + +import ( + "github.com/manifoldco/promptui" +) + +var listTemplates = &promptui.SelectTemplates{ + Label: "Select container to mount/unmount. {{ \"(use ^C to exit)\" | faint }}", + Active: "\U0000261E {{ if .Mounted }}{{ .ShortId | blue | bold }} {{ .Name | blue | bold }} (mounted){{ else }}{{ .ShortId | bold }} {{ .Name | bold }}{{ end }}", + Inactive: " {{ if .Mounted }}{{ .ShortId | blue }} {{ .Name | blue }} (mounted){{else}}{{ .ShortId }} {{ .Name }}{{ end }}", + Details: ` +------ Container ------ +Id: {{ .Id }} +Name: {{ .Names }} +Image: {{ .Image }} +Command: {{ .Command }} +{{ if .Mounted }}MountPoint: {{ .MountPoint }}{{ end }}`, +} + +var confirmUnmountTemplates = &promptui.SelectTemplates{ + Label: "{{ \"Unmount\" | red }} container {{ .Id | bold}} from {{ .Mp | bold }}", + Active: "\U0000261E {{ . | bold }}", + Inactive: " {{ . }}", +} diff --git a/lib/tui/tui.go b/lib/tui/tui.go new file mode 100644 index 0000000..efb98a8 --- /dev/null +++ b/lib/tui/tui.go @@ -0,0 +1,108 @@ +package tui + +import ( + "fmt" + "log" + "os" + "os/exec" + + "github.com/manifoldco/promptui" + "github.com/plesk/docker-fs/lib/manager" +) + +type State int + +const ( + ChooseAction State = iota + List +) + +type Tui struct { + state State + mng *manager.Manager +} + +func NewTui(mng *manager.Manager) *Tui { + return &Tui{ + mng: mng, + } +} + +func (t *Tui) Run(state State) error { + t.state = state + for { + if err := t.list(); err != nil { + return err + } + } +} + +func (t *Tui) list() error { + cts, err := t.mng.ListContainers() + if err != nil { + return err + } + + sel := promptui.Select{ + Label: "Label", + Items: cts, + Templates: listTemplates, + } + i, _, err := sel.Run() + if err != nil { + return err + } + ct := cts[i] + if ct.Mounted { + // ask to unmount + sel := promptui.Select{ + Label: struct { + Id string + Mp string + }{ + Id: ct.ShortId, + Mp: ct.MountPoint, + }, + Items: []string{ + "Yes", + "No", + }, + Templates: confirmUnmountTemplates, + } + i, _, err := sel.Run() + if err != nil { + return err + } + if i == 1 { + // No + return nil + } + // unmounting + if err := t.mng.UnmountContainer(ct.Id, ct.MountPoint); err != nil { + return err + } + } else { + // Mounting + promptPath := promptui.Prompt{ + Label: "Choose path to mount docker container", + Default: fmt.Sprintf("./mount-%v", cts[i].Name), + AllowEdit: true, + } + + mountPoint, err := promptPath.Run() + if err != nil { + log.Fatal(err) + } + + executable, err := os.Executable() + if err != nil { + return fmt.Errorf("Cannot detect executable path: %w", err) + } + + cmd := exec.Command(executable, "-id", cts[i].Id, "-mount", mountPoint, "-daemonize") + if err := cmd.Run(); err != nil { + return fmt.Errorf("Mount command failed: %w", err) + } + } + return nil +} diff --git a/main.go b/main.go index 6ee6012..fa805c3 100644 --- a/main.go +++ b/main.go @@ -4,14 +4,12 @@ import ( "flag" "fmt" "os" - "os/signal" - "syscall" "github.com/plesk/docker-fs/lib/log" + "github.com/plesk/docker-fs/lib/tui" - "github.com/plesk/docker-fs/lib/dockerfs" + "github.com/plesk/docker-fs/lib/manager" - "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" ) @@ -25,6 +23,8 @@ var ( // dockerSocketAddr string + daemonize bool + logLevel string verbose, quiet bool ) @@ -36,6 +36,9 @@ func init() { flag.StringVar(&mountPoint, "mount", "", "Mount point for containter FS") flag.StringVar(&mountPoint, "m", "", "Mount point for containter FS") + flag.BoolVar(&daemonize, "daemonize", false, "Daemonize fuse process") + flag.BoolVar(&daemonize, "d", false, "Daemonize fuse process") + // TODO make http support flag.StringVar(&dockerSocketAddr, "docker-socket", "/var/run/docker.sock", "Docker socket") @@ -49,16 +52,17 @@ func init() { func main() { flag.Parse() - if containerId == "" { - fmt.Fprintf(os.Stderr, "Container id is not specified.\n") - flag.Usage() - os.Exit(2) - } - - if mountPoint == "" { - fmt.Fprintf(os.Stderr, "Mount point is not specified.\n") - flag.Usage() - os.Exit(2) + if containerId != "" { + if mountPoint == "" { + fmt.Fprintf(os.Stderr, "Mount point is not specified.\n") + flag.Usage() + os.Exit(2) + } + mng := manager.New() + if err := mng.MountContainer(containerId, mountPoint, daemonize); err != nil { + log.Fatal(err) + } + return } if verbose && quiet { @@ -76,34 +80,12 @@ func main() { log.Printf("[warning] cannot set log level: %q (%v)", logLevel, err) } - log.Printf("[info] Check if mount directory exists (%v)...", mountPoint) - if err := os.MkdirAll(mountPoint, 0755); err != nil { - log.Fatal(err) - } - - log.Printf("[info] Fetching content of container %v...", containerId) - dockerMng := dockerfs.NewMng(containerId) - if err := dockerMng.Init(); err != nil { - log.Fatalf("dockerMng.Init() failed: %v", err) - } + mng := manager.New() + ui := tui.NewTui(mng) - root := dockerMng.Root() - - log.Printf("Mounting FS to %v...", mountPoint) - server, err := fs.Mount(mountPoint, root, &fs.Options{}) - if err != nil { - log.Fatalf("Mount failed: %v", err) + if err := ui.Run(tui.List); err != nil { + log.Fatal(err) } - - log.Printf("[info] Setting up signal handler...") - osSignalChannel := make(chan os.Signal, 1) - signal.Notify(osSignalChannel, syscall.SIGTERM, syscall.SIGINT) - go shutdown(server, osSignalChannel) - - log.Printf("OK!") - log.Printf("Press CTRL-C to unmount docker FS") - server.Wait() - log.Printf("[info] Server finished.") } func shutdown(server *fuse.Server, signals <-chan os.Signal) {