From 732aec52fffa8634548629a6b0f2c9e9ad50147f Mon Sep 17 00:00:00 2001 From: Adam Ahrens Date: Mon, 25 Oct 2021 16:01:19 -0700 Subject: [PATCH] Fuse Over RPC (#222) * commiting to switch * basics working * remove added flag * added in wrong ui * change back go version * reset build * update gazelle and run Close() on file * gazelle * fix recursive test from enkit //... * changes for PR, renaming to be cleaner * add in rename to fusepb * net.JoinHostPort * separate types * change package name * rename interfaces * remove allowother in test --- bazel/go_repositories.bzl | 47 +++++++- enkit/BUILD.bazel | 1 + enkit/main.go | 5 +- go.mod | 3 +- go.sum | 11 ++ proxy/enfuse/BUILD.bazel | 36 ++++++ proxy/enfuse/client.go | 41 +++++++ proxy/enfuse/config.go | 69 +++++++++++ proxy/enfuse/e2e_test.go | 112 ++++++++++++++++++ proxy/enfuse/files.go | 139 +++++++++++++++++++++++ proxy/enfuse/fusecmd/BUILD.bazel | 32 ++++++ proxy/enfuse/fusecmd/commands_darwin.go | 16 +++ proxy/enfuse/fusecmd/commands_linux.go | 54 +++++++++ proxy/enfuse/fusecmd/commands_windows.go | 16 +++ proxy/enfuse/rpc/BUILD.bazel | 24 ++++ proxy/enfuse/rpc/fuse.proto | 43 +++++++ proxy/enfuse/server.go | 102 +++++++++++++++++ 17 files changed, 747 insertions(+), 4 deletions(-) create mode 100644 proxy/enfuse/BUILD.bazel create mode 100644 proxy/enfuse/client.go create mode 100644 proxy/enfuse/config.go create mode 100644 proxy/enfuse/e2e_test.go create mode 100644 proxy/enfuse/files.go create mode 100644 proxy/enfuse/fusecmd/BUILD.bazel create mode 100644 proxy/enfuse/fusecmd/commands_darwin.go create mode 100644 proxy/enfuse/fusecmd/commands_linux.go create mode 100644 proxy/enfuse/fusecmd/commands_windows.go create mode 100644 proxy/enfuse/rpc/BUILD.bazel create mode 100644 proxy/enfuse/rpc/fuse.proto create mode 100644 proxy/enfuse/server.go diff --git a/bazel/go_repositories.bzl b/bazel/go_repositories.bzl index 1bb24edb..15d4f182 100644 --- a/bazel/go_repositories.bzl +++ b/bazel/go_repositories.bzl @@ -395,6 +395,18 @@ def go_repositories(): sum = "h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=", version = "v1.0.0", ) + go_repository( + name = "com_github_dvyukov_go_fuzz", + importpath = "github.com/dvyukov/go-fuzz", + sum = "h1:NgO45/5mBLRVfiXerEFzH6ikcZ7DNRPS639xFg3ENzU=", + version = "v0.0.0-20200318091601-be3528f3a813", + ) + go_repository( + name = "com_github_elazarl_go_bindata_assetfs", + importpath = "github.com/elazarl/go-bindata-assetfs", + sum = "h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk=", + version = "v1.0.0", + ) go_repository( name = "com_github_emirpasic_gods", @@ -1127,6 +1139,13 @@ def go_repositories(): sum = "h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g=", version = "v1.2.0", ) + go_repository( + name = "com_github_julusian_godocdown", + importpath = "github.com/Julusian/godocdown", + sum = "h1:n3F+mWm+b4D7uNbx1syN/uQTVDwt2sWfk23Mhzwzec4=", + version = "v0.0.0-20170816220326-6d19f8ff2df8", + ) + go_repository( name = "com_github_julz_importas", importpath = "github.com/julz/importas", @@ -1643,6 +1662,12 @@ def go_repositories(): sum = "h1:L8QM9bvf68pVdQ3bCFZMDmnt9yqcMBro1pC7F+IPYMY=", version = "v0.0.0-20200407221936-30656e2c4a95", ) + go_repository( + name = "com_github_robertkrimen_godocdown", + importpath = "github.com/robertkrimen/godocdown", + sum = "h1:jMxcLa+VjJKhpCwbLUXAD15wJ+hhvXMLujCl3MkXpfM=", + version = "v0.0.0-20130622164427-0bfa04905481", + ) go_repository( name = "com_github_rogpeppe_fastuuid", @@ -1853,6 +1878,12 @@ def go_repositories(): sum = "h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=", version = "v0.0.0-20190523213315-cbe66965904d", ) + go_repository( + name = "com_github_stephens2424_writerset", + importpath = "github.com/stephens2424/writerset", + sum = "h1:znRLgU6g8RS5euYRcy004XeE4W+Tu44kALzy7ghPif8=", + version = "v1.0.2", + ) go_repository( name = "com_github_stretchr_objx", @@ -1916,6 +1947,12 @@ def go_repositories(): sum = "h1:a1S4+4HSXDJMgeODJH/t0EEKxcVla6Tasw+Zx9JJMog=", version = "v2.3.1", ) + go_repository( + name = "com_github_tv42_httpunix", + importpath = "github.com/tv42/httpunix", + sum = "h1:u6SKchux2yDvFQnDHS3lPnIRmfVJ5Sxy3ao2SIdysLQ=", + version = "v0.0.0-20191220191345-2ba4b9c3382c", + ) go_repository( name = "com_github_ugorji_go", @@ -2191,6 +2228,12 @@ def go_repositories(): sum = "h1:xwwDQW5We85NaTk2APgoN9202w/l0DVGp+GZMfsrh7s=", version = "v0.0.0-20210223155950-e043a3d3c984", ) + go_repository( + name = "org_bazil_fuse", + importpath = "bazil.org/fuse", + sum = "h1:UrYe9YkT4Wpm6D+zByEyCJQzDqTPXqTDUI7bZ41i9VE=", + version = "v0.0.0-20200524192727-fb710f7dfd05", + ) go_repository( name = "org_golang_google_api", @@ -2286,8 +2329,8 @@ def go_repositories(): go_repository( name = "org_golang_x_sys", importpath = "golang.org/x/sys", - sum = "h1:46ULzRKLh1CwgRq2dC5SlBzEqqNCi8rreOZnNrbqcIY=", - version = "v0.0.0-20210309074719-68d13333faf2", + sum = "h1:xrCZDmdtoloIiooiA9q0OQb9r8HejIHYoHGhGCe1pGg=", + version = "v0.0.0-20210910150752-751e447fb3d0", ) go_repository( name = "org_golang_x_term", diff --git a/enkit/BUILD.bazel b/enkit/BUILD.bazel index 66e287c0..6f74cd02 100644 --- a/enkit/BUILD.bazel +++ b/enkit/BUILD.bazel @@ -12,6 +12,7 @@ go_library( "//lib/client/commands:go_default_library", "//lib/kflags/kcobra:go_default_library", "//lib/srand:go_default_library", + "//proxy/enfuse/fusecmd:go_default_library", "//proxy/ptunnel/commands:go_default_library", "@com_github_spf13_cobra//:go_default_library", ], diff --git a/enkit/main.go b/enkit/main.go index dca334b9..c1135e1e 100644 --- a/enkit/main.go +++ b/enkit/main.go @@ -2,10 +2,11 @@ package main import ( "github.com/enfabrica/enkit/lib/client" + "github.com/enfabrica/enkit/proxy/enfuse/fusecmd" acommands "github.com/enfabrica/enkit/astore/client/commands" - bcommands "github.com/enfabrica/enkit/lib/client/commands" bazelcmds "github.com/enfabrica/enkit/lib/bazel/commands" + bcommands "github.com/enfabrica/enkit/lib/client/commands" tcommands "github.com/enfabrica/enkit/proxy/ptunnel/commands" "github.com/enfabrica/enkit/lib/kflags/kcobra" @@ -48,5 +49,7 @@ func main() { bazel := bazelcmds.New(base) root.AddCommand(bazel.Command) + root.AddCommand(fusecmd.New()) + base.Run(kcobra.HideFlags(set), populator, runner) } diff --git a/go.mod b/go.mod index bc74781c..390fcc87 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/enfabrica/enkit go 1.14 require ( + bazil.org/fuse v0.0.0-20200524192727-fb710f7dfd05 // indirect cloud.google.com/go/datastore v1.1.0 cloud.google.com/go/storage v1.10.0 github.com/Microsoft/go-winio v0.4.16 // indirect @@ -69,7 +70,7 @@ require ( golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 - golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 // indirect + golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 // indirect google.golang.org/api v0.30.0 google.golang.org/grpc v1.37.0 google.golang.org/protobuf v1.26.0 diff --git a/go.sum b/go.sum index 1e141420..8273c991 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ 4d63.com/gochecknoglobals v0.0.0-20201008074935-acfc0b28355a h1:wFEQiK85fRsEVF0CRrPAos5LoAryUsIX1kPW/WrIqFw= 4d63.com/gochecknoglobals v0.0.0-20201008074935-acfc0b28355a/go.mod h1:wfdC5ZjKSPr7CybKEcgJhUOgeAQW1+7WcyK8OvUilfo= +bazil.org/fuse v0.0.0-20200524192727-fb710f7dfd05 h1:UrYe9YkT4Wpm6D+zByEyCJQzDqTPXqTDUI7bZ41i9VE= +bazil.org/fuse v0.0.0-20200524192727-fb710f7dfd05/go.mod h1:h0h5FBYpXThbvSfTqthw+0I4nmHnhTHkO5BoOHsBWqg= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -56,6 +58,7 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOC github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM= github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= +github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk= @@ -183,6 +186,8 @@ github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= +github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -702,6 +707,7 @@ github.com/quasilyte/go-ruleguard/rules v0.0.0-20201231183845-9e62ed36efe1 h1:PX github.com/quasilyte/go-ruleguard/rules v0.0.0-20201231183845-9e62ed36efe1/go.mod h1:7JTjp89EGyU1d6XfBiXihJNG37wB2VRkd125Q1u7Plc= github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95 h1:L8QM9bvf68pVdQ3bCFZMDmnt9yqcMBro1pC7F+IPYMY= github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= +github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af h1:gu+uRPtBe88sKxUCEXRoeCvVG90TJmwhiqRpvdhQFng= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= @@ -782,6 +788,7 @@ github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/ssgreg/nlreturn/v2 v2.1.0 h1:6/s4Rc49L6Uo6RLjhWZGBpWWjfzk2yrf1nIW8m4wgVA= github.com/ssgreg/nlreturn/v2 v2.1.0/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= +github.com/stephens2424/writerset v1.0.2/go.mod h1:aS2JhsMn6eA7e82oNmW4rfsgAOp9COBTTl8mzkwADnc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -810,6 +817,7 @@ github.com/tomarrell/wrapcheck v0.0.0-20201130113247-1683564d9756 h1:zV5mu0ESwb+ github.com/tomarrell/wrapcheck v0.0.0-20201130113247-1683564d9756/go.mod h1:yiFB6fFoV7saXirUGfuK+cPtUh4NX/Hf5y2WC2lehu0= github.com/tommy-muehle/go-mnd/v2 v2.3.1 h1:a1S4+4HSXDJMgeODJH/t0EEKxcVla6Tasw+Zx9JJMog= github.com/tommy-muehle/go-mnd/v2 v2.3.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= +github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM= github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ulikunitz/xz v0.5.8 h1:ERv8V6GKqVi23rgu5cj9pVfVzJbOqAY2Ntl88O6c2nQ= @@ -1026,6 +1034,7 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1067,6 +1076,7 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7s golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1139,6 +1149,7 @@ golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWc golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200414032229-332987a829c3/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200422022333-3d57cf2e726e/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200423201157-2723c5de0d66/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= diff --git a/proxy/enfuse/BUILD.bazel b/proxy/enfuse/BUILD.bazel new file mode 100644 index 00000000..53e0af30 --- /dev/null +++ b/proxy/enfuse/BUILD.bazel @@ -0,0 +1,36 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "client.go", + "config.go", + "files.go", + "server.go", + ], + importpath = "github.com/enfabrica/enkit/proxy/enfuse", + visibility = ["//visibility:public"], + deps = [ + "//proxy/enfuse/rpc:go_default_library", + "@org_bazil_fuse//:go_default_library", + "@org_bazil_fuse//fs:go_default_library", + "@org_golang_google_grpc//:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["e2e_test.go"], + tags = [ + "no-sandbox", + ], + deps = [ + ":go_default_library", + "//lib/knetwork:go_default_library", + "//lib/srand:go_default_library", + "//proxy/enfuse/rpc:go_default_library", + "@com_github_stretchr_testify//assert:go_default_library", + "@org_bazil_fuse//fs/fstestutil:go_default_library", + "@org_golang_google_grpc//:go_default_library", + ], +) diff --git a/proxy/enfuse/client.go b/proxy/enfuse/client.go new file mode 100644 index 00000000..37590e0a --- /dev/null +++ b/proxy/enfuse/client.go @@ -0,0 +1,41 @@ +package enfuse + +import ( + "bazil.org/fuse" + "bazil.org/fuse/fs" + fusepb "github.com/enfabrica/enkit/proxy/enfuse/rpc" + "google.golang.org/grpc" + "net" + "strconv" +) + +var ( + _ fs.FS = &FuseClient{} +) + +func NewClient(config *ConnectConfig) (*FuseClient, error) { + conn, err := grpc.Dial(net.JoinHostPort(config.Url, strconv.Itoa(config.Port)), grpc.WithInsecure()) + if err != nil { + return nil, err + } + return &FuseClient{fusepb.NewFuseControllerClient(conn)}, nil +} + +func MountDirectory(mountPath string, client *FuseClient) error { + c, err := fuse.Mount( + mountPath, + ) + if err != nil { + return err + } + srv := fs.New(c, nil) + return srv.Serve(client) +} + +type FuseClient struct { + ConnClient fusepb.FuseControllerClient +} + +func (f *FuseClient) Root() (fs.Node, error) { + return &Dir{Dir: "", Client: f.ConnClient}, nil +} diff --git a/proxy/enfuse/config.go b/proxy/enfuse/config.go new file mode 100644 index 00000000..fb34995b --- /dev/null +++ b/proxy/enfuse/config.go @@ -0,0 +1,69 @@ +package enfuse + +import ( + "net" +) + +type ConnectConfig struct { + Port int + Url string + L net.Listener + } + +type ConnectMod func(c *ConnectConfig) + +var ( + WithPort = func(p int) ConnectMod { + return func(c *ConnectConfig) { + c.Port = p + } + } + WithInterface = func(u string) ConnectMod { + return func(c *ConnectConfig) { + c.Url = u + } + } + WithListener = func(l net.Listener) ConnectMod { + return func(c *ConnectConfig) { + c.L = l + } + } + WithConnectConfig = func(c1 *ConnectConfig) ConnectMod { + return func(c *ConnectConfig) { + *c = *c1 + } + } +) + +type ( + ServerConfig struct { + *ConnectConfig + Dir string + } + ServerConfigMod = func(sc *ServerConfig) +) + +var ( + WithConnectMods = func(c ...ConnectMod) ServerConfigMod { + return func(sc *ServerConfig) { + for _, m := range c { + m(sc.ConnectConfig) + } + } + } + WithDir = func(d string) ServerConfigMod { + return func(sc *ServerConfig) { + sc.Dir = d + } + } +) + +func NewServerConfig(mods ...ServerConfigMod) *ServerConfig { + sc := &ServerConfig{ + ConnectConfig: &ConnectConfig{}, + } + for _, m := range mods { + m(sc) + } + return sc +} diff --git a/proxy/enfuse/e2e_test.go b/proxy/enfuse/e2e_test.go new file mode 100644 index 00000000..685380d1 --- /dev/null +++ b/proxy/enfuse/e2e_test.go @@ -0,0 +1,112 @@ +package enfuse_test + +import ( + "bazil.org/fuse/fs/fstestutil" + "fmt" + "github.com/enfabrica/enkit/lib/knetwork" + "github.com/enfabrica/enkit/lib/srand" + "github.com/enfabrica/enkit/proxy/enfuse" + fusepb "github.com/enfabrica/enkit/proxy/enfuse/rpc" + "github.com/stretchr/testify/assert" + "google.golang.org/grpc" + "io/fs" + "io/ioutil" + "math/rand" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + "time" +) + +func TestNewFuseShareCommand(t *testing.T) { + d, generatedFiles := CreateSeededTmpDir(t, 2) + p, err := knetwork.AllocatePort() + assert.Nil(t, err) + a, err := p.Address() + assert.Nil(t, err) + s := enfuse.NewServer( + enfuse.NewServerConfig( + enfuse.WithDir(d), + enfuse.WithConnectMods( + enfuse.WithListener(p), + ), + ), + ) + go func() { + assert.Nil(t, s.Serve()) + }() + time.Sleep(5 * time.Millisecond) + conn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%d", a.Port), grpc.WithInsecure()) + assert.Nil(t, err) + defer conn.Close() + c := enfuse.FuseClient{ConnClient: fusepb.NewFuseControllerClient(conn)} + m, err := fstestutil.MountedT(t, &c, nil) + assert.NoError(t, err) + defer m.Close() + + var fusePaths []string + assert.NoError(t, filepath.Walk(m.Dir, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + fusePaths = append(fusePaths, path) + assert.Greater(t, int(info.Size()), 0) + } + return err + })) + + assert.Equal(t, len(generatedFiles), len(fusePaths)) + for _, genFile := range generatedFiles { + for _, realFile := range fusePaths { + if realFile == genFile.Name { + btes, err := ioutil.ReadFile(realFile) + assert.NoError(t, err) + assert.Equal(t, len(genFile.Data), len(btes)) + assert.Truef(t, reflect.DeepEqual(btes, genFile.Data), "dta returned by fs equal") + } + } + } +} + +type TmpFile struct { + Name string + Data []byte +} + +func CreateSeededTmpDir(t *testing.T, num int) (string, []TmpFile) { + tmpDirName, err := os.MkdirTemp(os.TempDir(), "*") + assert.Nil(t, err) + var tts []TmpFile + for i := 0; i < num; i++ { + tts = append(tts, createTmpFile(t, tmpDirName)) + } + return tmpDirName, tts +} + +func createTmpFile(t *testing.T, tmpDirName string) TmpFile { + rng := rand.New(srand.Source) + cwd := tmpDirName + for i := 0; i < rng.Intn(5); i++ { + name, err := os.MkdirTemp(cwd, "*") + assert.NoError(t, err) + cwd = name + } + f, err := os.CreateTemp(cwd, "*.txt") + assert.Nil(t, err) + filename := f.Name() + sizeOfFile := 1024 * 1024 * (rng.Intn(2) + 1) // size of the file is greater than rpc data. + content := make([]byte, sizeOfFile) + i, err := rng.Read(content) + assert.NoError(t, err) + assert.Equal(t, sizeOfFile, i) + _, err = f.Write(content) + assert.NoError(t, err) + assert.NoError(t, f.Close()) + return TmpFile{ + Name: strings.ReplaceAll(filename, tmpDirName, ""), + Data: content, + } +} diff --git a/proxy/enfuse/files.go b/proxy/enfuse/files.go new file mode 100644 index 00000000..0bf41283 --- /dev/null +++ b/proxy/enfuse/files.go @@ -0,0 +1,139 @@ +package enfuse + +import ( + "bazil.org/fuse" + "bazil.org/fuse/fs" + "context" + fusepb "github.com/enfabrica/enkit/proxy/enfuse/rpc" + "os" + "path/filepath" + "sync" + "syscall" + "time" +) + +var ( + _ fs.NodeRequestLookuper = &Dir{} + _ fs.Node = &Dir{} + + _ fs.Node = &File{} + _ fs.HandleReader = &File{} +) + +func ConvertToDirent(info []*fusepb.FileInfo) []fuse.Dirent { + var fdir []fuse.Dirent + for _, i := range info { + dt := fuse.DT_File + inode := uint64(3) + if i.IsDir { + inode = 4 + dt = fuse.DT_Dir + } + fdir = append(fdir, fuse.Dirent{ + Inode: inode, + Type: dt, + Name: i.Name, + }) + } + return fdir +} + +// Dir represents a directory node. It contains a pass by value reference to the grpc client for fetching data. +// It contains a path (including self) of its parent directory +type Dir struct { + Client fusepb.FuseControllerClient + Data []*fusepb.FileInfo + Dir string + LastFetch time.Time + mu sync.Mutex +} + +func (f *Dir) Lookup(ctx context.Context, req *fuse.LookupRequest, resp *fuse.LookupResponse) (fs.Node, error) { + if err := f.fetchData(); err != nil { + return nil, err + } + for _, d := range f.Data { + if d.Name == req.Name { + if d.IsDir { + return &Dir{Dir: filepath.Join(f.Dir, d.Name), Client: f.Client}, nil + } else { + return &File{FileName: filepath.Join(f.Dir, d.Name), Client: f.Client}, nil + } + } + } + return nil, syscall.ENOENT +} + +func (f *Dir) Attr(ctx context.Context, attr *fuse.Attr) error { + attr.Inode = 1 + attr.Mode = os.ModeDir | 0o555 + return nil +} + +func (f *Dir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { + if err := f.fetchData(); err != nil { + return nil, err + } + return ConvertToDirent(f.Data), nil +} + +func (f *Dir) fetchData() error { + f.mu.Lock() + defer f.mu.Unlock() + if time.Since(f.LastFetch) < 5*time.Second { + return nil + } + r, err := f.Client.FileInfo(context.Background(), &fusepb.FileInfoRequest{Dir: f.Dir}) + if err != nil { + return err + } + f.Data = r.Files + f.LastFetch = time.Now() + return nil +} + +// File represents a single file. It contains the path to itself and the grpc client. +type File struct { + FileName string + Client fusepb.FuseControllerClient + Info *fusepb.FileInfo + FetchTime time.Time + mu sync.Mutex +} + +func (f *File) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error { + if err := f.getData(); err != nil { + return err + } + res, err := f.Client.FileContent(ctx, &fusepb.RequestContent{ + Offset: uint64(req.Offset), + Path: f.FileName, + Size: uint64(req.Size), + }) + if err != nil { + return err + } + resp.Data = res.Content + return nil +} + +func (f *File) Attr(ctx context.Context, attr *fuse.Attr) error { + if err := f.getData(); err != nil { + return err + } + attr.Inode = 6 + attr.Mode = 0o444 + attr.Size = uint64(f.Info.Size) + return nil +} + +func (f *File) getData() error { + f.mu.Lock() + defer f.mu.Unlock() + res, err := f.Client.SingleFileInfo(context.Background(), &fusepb.SingleFileInfoRequest{Path: f.FileName}) + if err != nil { + return err + } + f.Info = res.Info + return nil +} diff --git a/proxy/enfuse/fusecmd/BUILD.bazel b/proxy/enfuse/fusecmd/BUILD.bazel new file mode 100644 index 00000000..d6dbe5b8 --- /dev/null +++ b/proxy/enfuse/fusecmd/BUILD.bazel @@ -0,0 +1,32 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "commands_darwin.go", + "commands_linux.go", + "commands_windows.go", + ], + importpath = "github.com/enfabrica/enkit/proxy/enfuse/fusecmd", + visibility = ["//visibility:public"], + deps = select({ + "@io_bazel_rules_go//go/platform:android": [ + "//proxy/enfuse:go_default_library", + "@com_github_spf13_cobra//:go_default_library", + ], + "@io_bazel_rules_go//go/platform:darwin": [ + "@com_github_spf13_cobra//:go_default_library", + ], + "@io_bazel_rules_go//go/platform:ios": [ + "@com_github_spf13_cobra//:go_default_library", + ], + "@io_bazel_rules_go//go/platform:linux": [ + "//proxy/enfuse:go_default_library", + "@com_github_spf13_cobra//:go_default_library", + ], + "@io_bazel_rules_go//go/platform:windows": [ + "@com_github_spf13_cobra//:go_default_library", + ], + "//conditions:default": [], + }), +) diff --git a/proxy/enfuse/fusecmd/commands_darwin.go b/proxy/enfuse/fusecmd/commands_darwin.go new file mode 100644 index 00000000..54bd6442 --- /dev/null +++ b/proxy/enfuse/fusecmd/commands_darwin.go @@ -0,0 +1,16 @@ +package fusecmd + +import ( + "errors" + "github.com/spf13/cobra" +) + +func New() *cobra.Command { + c := &cobra.Command{ + Use: "fuse", + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("FUSE is not supported on this platform") + }, + } + return c +} diff --git a/proxy/enfuse/fusecmd/commands_linux.go b/proxy/enfuse/fusecmd/commands_linux.go new file mode 100644 index 00000000..8fa7e36e --- /dev/null +++ b/proxy/enfuse/fusecmd/commands_linux.go @@ -0,0 +1,54 @@ +package fusecmd + +import ( + "github.com/enfabrica/enkit/proxy/enfuse" + "github.com/spf13/cobra" +) + +func NewFuseShareCommand() *cobra.Command { + cc := &enfuse.ConnectConfig{} + var dir string + c := &cobra.Command{ + Use: `share`, + RunE: func(cmd *cobra.Command, args []string) error { + return enfuse.ServeDirectory( + enfuse.WithConnectMods( + enfuse.WithConnectConfig(cc), + ), + enfuse.WithDir(dir), + ) + }, + } + c.Flags().StringVar(&dir, "dir", ".", "the directory to share") + c.Flags().IntVarP(&cc.Port, "port", "p", 9999, "the port to serve the rpc from") + c.Flags().StringVarP(&cc.Url, "interface", "i", "127.0.0.1", "the interface to bind") + return c +} + +func NewFuseMountDirectory() *cobra.Command { + cc := &enfuse.ConnectConfig{} + var cwd string + c := &cobra.Command{ + Use: `mount`, + RunE: func(cmd *cobra.Command, args []string) error { + fc, err := enfuse.NewClient(cc) + if err != nil { + return err + } + return enfuse.MountDirectory(cwd, fc) + }, + } + c.Flags().StringVar(&cwd, "dir", ".", "the mount point for the FUSE directory") + c.Flags().IntVarP(&cc.Port, "port", "p", 9999, "the port to serve the rpc from") + c.Flags().StringVarP(&cc.Url, "interface", "i", "127.0.0.1", "the interface to bind to") + return c +} + +func New() *cobra.Command { + c := &cobra.Command{ + Use: "fuse", + } + c.AddCommand(NewFuseMountDirectory()) + c.AddCommand(NewFuseShareCommand()) + return c +} diff --git a/proxy/enfuse/fusecmd/commands_windows.go b/proxy/enfuse/fusecmd/commands_windows.go new file mode 100644 index 00000000..54bd6442 --- /dev/null +++ b/proxy/enfuse/fusecmd/commands_windows.go @@ -0,0 +1,16 @@ +package fusecmd + +import ( + "errors" + "github.com/spf13/cobra" +) + +func New() *cobra.Command { + c := &cobra.Command{ + Use: "fuse", + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("FUSE is not supported on this platform") + }, + } + return c +} diff --git a/proxy/enfuse/rpc/BUILD.bazel b/proxy/enfuse/rpc/BUILD.bazel new file mode 100644 index 00000000..8523bd08 --- /dev/null +++ b/proxy/enfuse/rpc/BUILD.bazel @@ -0,0 +1,24 @@ +load("@rules_proto//proto:defs.bzl", "proto_library") +load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") + +proto_library( + name = "enfuse_proto", + srcs = ["fuse.proto"], + visibility = ["//visibility:public"], +) + +go_proto_library( + name = "enfuse_go_proto", + compilers = ["@io_bazel_rules_go//proto:go_grpc"], + importpath = "github.com/enfabrica/enkit/proxy/enfuse/rpc", + proto = ":enfuse_proto", + visibility = ["//visibility:public"], +) + +go_library( + name = "go_default_library", + embed = [":enfuse_go_proto"], + importpath = "github.com/enfabrica/enkit/proxy/enfuse/rpc", + visibility = ["//visibility:public"], +) diff --git a/proxy/enfuse/rpc/fuse.proto b/proxy/enfuse/rpc/fuse.proto new file mode 100644 index 00000000..50d272e1 --- /dev/null +++ b/proxy/enfuse/rpc/fuse.proto @@ -0,0 +1,43 @@ +syntax = "proto3"; + +package fusepb; + + +message FileInfo { + string name = 1; // Full path from shared content root. + bool isDir = 2; + int64 size = 3; // Size of the file is the real size in bytes. Directory size is 0. +} + +message FileInfoRequest{ + string dir = 1; // directory is the list of files to get from which dir. If empty, should default to the root. +} + +message FileInfoResponse { + repeated FileInfo files = 1; +} + +message RequestContent { + string path = 1; // path is a fully qualified path. + uint64 offset = 2; // file data offset. + uint64 size = 3; // size of the returned byte array. +} + +message ResponseContent { + bytes content = 1; +} + +message SingleFileInfoRequest { + string path = 1; // Can either be a file or directory. +} + +message SingleFileInfoResponse { + FileInfo info = 1; +} + +// FuseController is a wrapper around a restful single file buffer and os.Stat for directories and files. +service FuseController { + rpc FileContent(RequestContent) returns (ResponseContent){} // Remote buffer reader of a file. + rpc FileInfo(FileInfoRequest) returns (FileInfoResponse){} // Returns all the File Infos for a given directory. + rpc SingleFileInfo(SingleFileInfoRequest) returns (SingleFileInfoResponse){} // File Info for a single file. +} \ No newline at end of file diff --git a/proxy/enfuse/server.go b/proxy/enfuse/server.go new file mode 100644 index 00000000..a0648fea --- /dev/null +++ b/proxy/enfuse/server.go @@ -0,0 +1,102 @@ +package enfuse + +import ( + "context" + "fmt" + fusepb "github.com/enfabrica/enkit/proxy/enfuse/rpc" + "google.golang.org/grpc" + "io" + "io/ioutil" + "net" + "os" + "path/filepath" +) + +func ServeDirectory(mods ...ServerConfigMod) error { + s := NewServer(NewServerConfig(mods...)) + return s.Serve() +} + +var _ fusepb.FuseControllerServer = &FuseServer{} + +type FuseServer struct { + cfg *ServerConfig +} + +func (s *FuseServer) SingleFileInfo(ctx context.Context, request *fusepb.SingleFileInfoRequest) (*fusepb.SingleFileInfoResponse, error) { + des, err := os.Open(filepath.Join(s.cfg.Dir, request.Path)) + if err != nil { + return nil, err + } + defer des.Close() + st, err := des.Stat() + if err != nil { + return nil, err + } + return &fusepb.SingleFileInfoResponse{ + Info: &fusepb.FileInfo{ + Name: filepath.Base(request.Path), + Size: st.Size(), + IsDir: false, + }, + }, nil +} + +func (s *FuseServer) FileContent(ctx context.Context, rf *fusepb.RequestContent) (*fusepb.ResponseContent, error) { + f, err := os.Open(filepath.Join(s.cfg.Dir, rf.Path)) + if err != nil { + return nil, err + } + defer f.Close() + data := make([]byte, rf.Size) // default to sending 1mb at max, client side is 4mb max possible but this is to be safe + i, err := f.ReadAt(data, int64(rf.Offset)) + if err != nil && err != io.EOF { + return nil, err + } + if i != len(data) { + data = data[:i] + } + return &fusepb.ResponseContent{Content: data}, nil +} + +func (s *FuseServer) FileInfo(ctx context.Context, request *fusepb.FileInfoRequest) (*fusepb.FileInfoResponse, error) { + var dir string + if request.Dir == "" { + dir = s.cfg.Dir + } else { + dir = filepath.Join(s.cfg.Dir, request.Dir) + } + var fis []*fusepb.FileInfo + outs, err := ioutil.ReadDir(dir) + if err != nil { + return nil, err + } + for _, info := range outs { + e := &fusepb.FileInfo{ + Name: info.Name(), + IsDir: info.IsDir(), + Size: info.Size(), + } + fis = append(fis, e) + } + return &fusepb.FileInfoResponse{ + Files: fis, + }, err +} + +func (s *FuseServer) Serve() error { + grpcs := grpc.NewServer() + fusepb.RegisterFuseControllerServer(grpcs, s) + if s.cfg.L == nil { + l, err := net.Listen("tcp", fmt.Sprintf("%s:%d", s.cfg.Url, s.cfg.Port)) + if err != nil { + return err + } + s.cfg.L = l + } + return grpcs.Serve(s.cfg.L) +} + +func NewServer(cfg *ServerConfig) *FuseServer { + return &FuseServer{cfg: cfg} +}