├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .godownloader.sh ├── .goreleaser.yml ├── LICENSE ├── README.md ├── config └── config.go ├── etcd └── client.go ├── fs ├── file.go ├── folder.go ├── folder_test.go └── server.go ├── go.mod ├── go.sum └── main.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | os: 8 | - ubuntu-latest 9 | # - macOS-latest # mac doesn't have docker pre-installed? 10 | runs-on: ${{ matrix.os }} 11 | services: 12 | mysql: 13 | image: bitnami/etcd:3 14 | env: 15 | ALLOW_NONE_AUTHENTICATION: "yes" 16 | ports: 17 | - 2379:2379 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@master 22 | with: 23 | fetch-depth: 0 24 | 25 | - name: Set Up Go 26 | uses: actions/setup-go@v2 27 | with: 28 | go-version: '^1' 29 | 30 | - name: Unit test 31 | run: go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... 32 | 33 | - name: Integration test 34 | run: | 35 | uname -a 36 | fusermount -V 37 | go build . 38 | mkdir mnt 39 | ./etcdfs 40 | ./etcdfs mnt & 41 | ls -al 42 | timeout 20 sh -c 'until mountpoint mnt; do echo "not started yet"; sleep 3s; done' 43 | ls -al mnt 44 | echo content >mnt/aa 45 | ls -al mnt 46 | cat mnt/aa | grep content 47 | stat mnt/aa 48 | rm mnt/aa 49 | sudo umount mnt 50 | git clean -fd # GoReleaser needs a clean workspace 51 | 52 | - name: GoReleaser 53 | uses: goreleaser/goreleaser-action@v2 54 | if: startsWith(github.ref, 'refs/tags/') && startsWith(matrix.os, 'ubuntu') 55 | with: 56 | version: latest 57 | args: release --rm-dist 58 | env: 59 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Prject configuration by Goland 15 | .idea/ 16 | .vscode/ 17 | 18 | vendor/ 19 | .DS_Store 20 | coverage.txt 21 | dist/ 22 | -------------------------------------------------------------------------------- /.godownloader.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | # Code generated by godownloader on 2020-08-02T15:52:40Z. DO NOT EDIT. 4 | # 5 | 6 | usage() { 7 | this=$1 8 | cat </dev/null 117 | } 118 | echoerr() { 119 | echo "$@" 1>&2 120 | } 121 | log_prefix() { 122 | echo "$0" 123 | } 124 | _logp=6 125 | log_set_priority() { 126 | _logp="$1" 127 | } 128 | log_priority() { 129 | if test -z "$1"; then 130 | echo "$_logp" 131 | return 132 | fi 133 | [ "$1" -le "$_logp" ] 134 | } 135 | log_tag() { 136 | case $1 in 137 | 0) echo "emerg" ;; 138 | 1) echo "alert" ;; 139 | 2) echo "crit" ;; 140 | 3) echo "err" ;; 141 | 4) echo "warning" ;; 142 | 5) echo "notice" ;; 143 | 6) echo "info" ;; 144 | 7) echo "debug" ;; 145 | *) echo "$1" ;; 146 | esac 147 | } 148 | log_debug() { 149 | log_priority 7 || return 0 150 | echoerr "$(log_prefix)" "$(log_tag 7)" "$@" 151 | } 152 | log_info() { 153 | log_priority 6 || return 0 154 | echoerr "$(log_prefix)" "$(log_tag 6)" "$@" 155 | } 156 | log_err() { 157 | log_priority 3 || return 0 158 | echoerr "$(log_prefix)" "$(log_tag 3)" "$@" 159 | } 160 | log_crit() { 161 | log_priority 2 || return 0 162 | echoerr "$(log_prefix)" "$(log_tag 2)" "$@" 163 | } 164 | uname_os() { 165 | os=$(uname -s | tr '[:upper:]' '[:lower:]') 166 | case "$os" in 167 | cygwin_nt*) os="windows" ;; 168 | mingw*) os="windows" ;; 169 | msys_nt*) os="windows" ;; 170 | esac 171 | echo "$os" 172 | } 173 | uname_arch() { 174 | arch=$(uname -m) 175 | case $arch in 176 | x86_64) arch="amd64" ;; 177 | x86) arch="386" ;; 178 | i686) arch="386" ;; 179 | i386) arch="386" ;; 180 | aarch64) arch="arm64" ;; 181 | armv5*) arch="armv5" ;; 182 | armv6*) arch="armv6" ;; 183 | armv7*) arch="armv7" ;; 184 | esac 185 | echo ${arch} 186 | } 187 | uname_os_check() { 188 | os=$(uname_os) 189 | case "$os" in 190 | darwin) return 0 ;; 191 | dragonfly) return 0 ;; 192 | freebsd) return 0 ;; 193 | linux) return 0 ;; 194 | android) return 0 ;; 195 | nacl) return 0 ;; 196 | netbsd) return 0 ;; 197 | openbsd) return 0 ;; 198 | plan9) return 0 ;; 199 | solaris) return 0 ;; 200 | windows) return 0 ;; 201 | esac 202 | log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" 203 | return 1 204 | } 205 | uname_arch_check() { 206 | arch=$(uname_arch) 207 | case "$arch" in 208 | 386) return 0 ;; 209 | amd64) return 0 ;; 210 | arm64) return 0 ;; 211 | armv5) return 0 ;; 212 | armv6) return 0 ;; 213 | armv7) return 0 ;; 214 | ppc64) return 0 ;; 215 | ppc64le) return 0 ;; 216 | mips) return 0 ;; 217 | mipsle) return 0 ;; 218 | mips64) return 0 ;; 219 | mips64le) return 0 ;; 220 | s390x) return 0 ;; 221 | amd64p32) return 0 ;; 222 | esac 223 | log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" 224 | return 1 225 | } 226 | untar() { 227 | tarball=$1 228 | case "${tarball}" in 229 | *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" ;; 230 | *.tar) tar --no-same-owner -xf "${tarball}" ;; 231 | *.zip) unzip "${tarball}" ;; 232 | *) 233 | log_err "untar unknown archive format for ${tarball}" 234 | return 1 235 | ;; 236 | esac 237 | } 238 | http_download_curl() { 239 | local_file=$1 240 | source_url=$2 241 | header=$3 242 | if [ -z "$header" ]; then 243 | code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url") 244 | else 245 | code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url") 246 | fi 247 | if [ "$code" != "200" ]; then 248 | log_debug "http_download_curl received HTTP status $code" 249 | return 1 250 | fi 251 | return 0 252 | } 253 | http_download_wget() { 254 | local_file=$1 255 | source_url=$2 256 | header=$3 257 | if [ -z "$header" ]; then 258 | wget -q -O "$local_file" "$source_url" 259 | else 260 | wget -q --header "$header" -O "$local_file" "$source_url" 261 | fi 262 | } 263 | http_download() { 264 | log_debug "http_download $2" 265 | if is_command curl; then 266 | http_download_curl "$@" 267 | return 268 | elif is_command wget; then 269 | http_download_wget "$@" 270 | return 271 | fi 272 | log_crit "http_download unable to find wget or curl" 273 | return 1 274 | } 275 | http_copy() { 276 | tmp=$(mktemp) 277 | http_download "${tmp}" "$1" "$2" || return 1 278 | body=$(cat "$tmp") 279 | rm -f "${tmp}" 280 | echo "$body" 281 | } 282 | github_release() { 283 | owner_repo=$1 284 | version=$2 285 | test -z "$version" && version="latest" 286 | giturl="https://github.com/${owner_repo}/releases/${version}" 287 | json=$(http_copy "$giturl" "Accept:application/json") 288 | test -z "$json" && return 1 289 | version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') 290 | test -z "$version" && return 1 291 | echo "$version" 292 | } 293 | hash_sha256() { 294 | TARGET=${1:-/dev/stdin} 295 | if is_command gsha256sum; then 296 | hash=$(gsha256sum "$TARGET") || return 1 297 | echo "$hash" | cut -d ' ' -f 1 298 | elif is_command sha256sum; then 299 | hash=$(sha256sum "$TARGET") || return 1 300 | echo "$hash" | cut -d ' ' -f 1 301 | elif is_command shasum; then 302 | hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1 303 | echo "$hash" | cut -d ' ' -f 1 304 | elif is_command openssl; then 305 | hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1 306 | echo "$hash" | cut -d ' ' -f a 307 | else 308 | log_crit "hash_sha256 unable to find command to compute sha-256 hash" 309 | return 1 310 | fi 311 | } 312 | hash_sha256_verify() { 313 | TARGET=$1 314 | checksums=$2 315 | if [ -z "$checksums" ]; then 316 | log_err "hash_sha256_verify checksum file not specified in arg2" 317 | return 1 318 | fi 319 | BASENAME=${TARGET##*/} 320 | want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1) 321 | if [ -z "$want" ]; then 322 | log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'" 323 | return 1 324 | fi 325 | got=$(hash_sha256 "$TARGET") 326 | if [ "$want" != "$got" ]; then 327 | log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got" 328 | return 1 329 | fi 330 | } 331 | cat /dev/null <:/etc/kubernetes/pki/etcd . 23 | $ # mount to a local directory 24 | $ etcdfs --endpoints=:2379 --cacert etcd/ca.crt --key etcd/server.key --cert etcd/server.crt mnt 25 | $ # open it in VS code 26 | $ code mnt 27 | ``` 28 | 29 | ## Installation 30 | 31 | #### Homebrew 32 | 33 | ```bash 34 | # WIP 35 | ``` 36 | 37 | #### `curl | bash` style downloads to `/usr/local/bin` 38 | ```bash 39 | $ curl -sfL https://raw.githubusercontent.com/polyrabbit/etcdfs/master/.godownloader.sh | bash -s -- -d -b /usr/local/bin 40 | ``` 41 | 42 | #### Using [Go](https://golang.org/) 43 | ```bash 44 | $ go get -u github.com/polyrabbit/etcdfs 45 | ``` 46 | 47 | ## Usage 48 | 49 | ```bash 50 | $ etcdfs 51 | Mount etcd to local file system - find help/update from https://github.com/polyrabbit/etcdfs 52 | 53 | Usage: 54 | etcdfs [mount-point] [flags] 55 | 56 | Flags: 57 | --endpoints strings etcd endpoints (default [127.0.0.1:2379]) 58 | --dial-timeout duration dial timeout for client connections (default 2s) 59 | --read-timeout duration timeout for reading and writing to etcd (default 3s) 60 | -v, --verbose verbose output 61 | --enable-pprof enable runtime profiling data via HTTP server. Address is at "http://localhost:9327/debug/pprof" 62 | --cert string identify secure client using this TLS certificate file 63 | --key string identify secure client using this TLS key file 64 | --cacert string verify certificates of TLS-enabled secure servers using this CA bundle 65 | --mount-options strings options are passed as -o string to fusermount (default [nonempty]) 66 | -h, --help help for etcdfs 67 | ``` 68 | 69 | _Notice: `etcdfs` has a very similar CLI syntax to `etcdctl`._ 70 | 71 | ## Limitations 72 | 73 | * Etcdfs depends on the [FUSE](https://en.wikipedia.org/wiki/Filesystem_in_Userspace) kernel module which only supports Linux and macOS(?). It needs to be installed first: 74 | * Linux: `yum/apt-get install -y fuse` 75 | * macOS: install [OSXFUSE](https://osxfuse.github.io/) 76 | * Keys in etcd should have a hierarchical structure to fit the filesystem tree model. And currently the only supported hierarchy separator is `/` (the same as *nix), more will be supported in the future. 77 | * Currently only etcd v3 is supported. 78 | 79 | ## Supported Operations 80 | 81 | Most commonly used POSIX operations are supported: 82 | 83 | * Readdir 84 | * Lookup 85 | * Getattr 86 | * Open 87 | * Read 88 | * Write 89 | * Create 90 | * Flush 91 | * Fsync 92 | * Unlink 93 | * Setattr 94 | 95 | ## TODO 96 | 97 | - [x] ~~When building a directory, all keys belonging to that directory can be skipped~~ 98 | - [ ] Support hierarchy separators other than `/` in etcd 99 | - [ ] Watch for file/directory changes 100 | 101 | ## Credits 102 | 103 | * Inspired by [rssfs](https://github.com/dertuxmalwieder/rssfs) 104 | * Based on [go-fuse](https://github.com/hanwen/go-fuse) binding 105 | 106 | ## License 107 | 108 | The MIT License (MIT) - see [LICENSE.md](https://github.com/polyrabbit/etcdfs/blob/master/LICENSE) for more details 109 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "time" 8 | 9 | "github.com/sirupsen/logrus" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var ( 14 | MountPoint string 15 | MountOptions []string 16 | Endpoints []string 17 | Verbose bool 18 | DialTimeout time.Duration 19 | CommandTimeOut time.Duration 20 | EnablePprof bool 21 | // Secure config 22 | CertFile string 23 | KeyFile string 24 | TrustedCAFile string 25 | 26 | // Will be set by go-build 27 | Version string 28 | Rev string 29 | ) 30 | 31 | const ( 32 | defaultDialTimeout = 2 * time.Second 33 | defaultCommandTimeOut = 3 * time.Second 34 | defaultPprofAddress = "localhost:9327" 35 | ) 36 | 37 | var ( 38 | rootCmd = &cobra.Command{ 39 | Use: fmt.Sprintf("%s [mount-point]", os.Args[0]), 40 | Short: "Mount etcd to local file system - find help/update at https://github.com/polyrabbit/etcdfs", 41 | RunE: func(cmd *cobra.Command, args []string) error { 42 | if len(args) != 1 { 43 | return cmd.Help() 44 | } 45 | MountPoint = args[0] 46 | return nil 47 | }, 48 | } 49 | ) 50 | 51 | func init() { 52 | logrus.SetFormatter(&logrus.TextFormatter{TimestampFormat: "15:04:05", FullTimestamp: true}) 53 | 54 | version := Version 55 | if version != "" && Rev != "" { 56 | version = fmt.Sprintf("%s, build %s", version, Rev) 57 | } 58 | rootCmd.Version = version 59 | 60 | // We use the same flags as etcd 61 | rootCmd.Flags().StringSliceVar(&Endpoints, "endpoints", []string{"127.0.0.1:2379"}, "etcd endpoints") 62 | rootCmd.Flags().DurationVar(&DialTimeout, "dial-timeout", defaultDialTimeout, "dial timeout for client connections") 63 | rootCmd.Flags().DurationVar(&CommandTimeOut, "read-timeout", defaultCommandTimeOut, "timeout for reading and writing to etcd") 64 | rootCmd.Flags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output") 65 | rootCmd.Flags().BoolVar(&EnablePprof, "enable-pprof", false, fmt.Sprintf("enable runtime profiling data via HTTP server. Address is at %q", "http://"+defaultPprofAddress+"/debug/pprof")) 66 | 67 | rootCmd.Flags().StringVar(&CertFile, "cert", "", "identify secure client using this TLS certificate file") 68 | rootCmd.Flags().StringVar(&KeyFile, "key", "", "identify secure client using this TLS key file") 69 | rootCmd.Flags().StringVar(&TrustedCAFile, "cacert", "", "verify certificates of TLS-enabled secure servers using this CA bundle") 70 | 71 | rootCmd.Flags().StringSliceVarP(&MountOptions, "mount-options", "o", nil, "options are passed as -o string to fusermount") 72 | 73 | rootCmd.Flags().SortFlags = false 74 | rootCmd.SilenceErrors = true 75 | } 76 | 77 | func Execute() bool { 78 | if err := rootCmd.Execute(); err != nil { 79 | logrus.Errorln(err) 80 | return false 81 | } 82 | if len(MountPoint) == 0 { 83 | return false 84 | } 85 | 86 | if Verbose { 87 | logrus.SetLevel(logrus.DebugLevel) 88 | } 89 | if EnablePprof { 90 | go func() { 91 | if err := http.ListenAndServe(defaultPprofAddress, nil); err != nil { 92 | logrus.WithError(err).Error("Failed to serve pprof") 93 | } 94 | }() 95 | } 96 | return true 97 | } 98 | -------------------------------------------------------------------------------- /etcd/client.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "strings" 7 | "time" 8 | 9 | "github.com/polyrabbit/etcdfs/config" 10 | "github.com/sirupsen/logrus" 11 | v3 "go.etcd.io/etcd/v3/clientv3" 12 | "go.etcd.io/etcd/v3/pkg/transport" 13 | "go.uber.org/zap" 14 | ) 15 | 16 | type Client struct { 17 | client *v3.Client 18 | } 19 | 20 | func MustNew() *Client { 21 | // Disable noisy zap log 22 | disabledZapConfig := zap.NewDevelopmentConfig() 23 | disabledZapConfig.OutputPaths = []string{"/dev/null"} 24 | disabledZapConfig.ErrorOutputPaths = []string{"/dev/null"} 25 | 26 | tlsinfo := transport.TLSInfo{} 27 | tlsinfo.CertFile = config.CertFile 28 | tlsinfo.KeyFile = config.KeyFile 29 | tlsinfo.TrustedCAFile = config.TrustedCAFile 30 | var tlsConf *tls.Config 31 | if !tlsinfo.Empty() { 32 | tlsConf, _ = tlsinfo.ClientConfig() 33 | } 34 | 35 | etcdClient, err := v3.New(v3.Config{ 36 | Endpoints: config.Endpoints, 37 | DialTimeout: config.DialTimeout, 38 | AutoSyncInterval: time.Minute, 39 | LogConfig: &disabledZapConfig, 40 | TLS: tlsConf, 41 | }) 42 | if err != nil { 43 | logrus.WithError(err).Fatal("Failed to new etcd client") 44 | } 45 | c := &Client{etcdClient} 46 | // Get a random key. As long as we can get the response without an error, the server is health. 47 | // Otherwise we fail fast to avoid propagating errors to filesystem layer. 48 | _, err = c.GetValue(context.TODO(), "_ping") 49 | if err != nil { 50 | logrus.WithError(err).WithField("endpoints", strings.Join(config.Endpoints, ",")).Fatal("etcd server does not respond") 51 | } 52 | return c 53 | } 54 | 55 | // List keys beginning at certain prefix, with ability to specify range-end 56 | func (c *Client) ListKeys(ctx context.Context, prefix string, opts ...v3.OpOption) ([]string, bool, error) { 57 | defaultOpts := []v3.OpOption{v3.WithPrefix(), v3.WithKeysOnly(), v3.WithSerializable()} 58 | defaultOpts = append(defaultOpts, opts...) 59 | ctx, cancel := context.WithTimeout(ctx, config.CommandTimeOut) 60 | defer cancel() 61 | resp, err := c.client.Get(ctx, prefix, defaultOpts...) 62 | if err != nil { 63 | return nil, false, err 64 | } 65 | keys := make([]string, len(resp.Kvs)) 66 | for i, kv := range resp.Kvs { 67 | keys[i] = string(kv.Key) 68 | } 69 | return keys, resp.More, nil 70 | } 71 | 72 | func (c *Client) GetValue(ctx context.Context, key string, opts ...v3.OpOption) ([]byte, error) { 73 | defaultOpts := []v3.OpOption{v3.WithSerializable()} 74 | defaultOpts = append(defaultOpts, opts...) 75 | ctx, cancel := context.WithTimeout(ctx, config.CommandTimeOut) 76 | defer cancel() 77 | resp, err := c.client.Get(ctx, key, defaultOpts...) 78 | if err != nil { 79 | return nil, err 80 | } 81 | if len(resp.Kvs) == 0 { 82 | return nil, nil 83 | } 84 | return resp.Kvs[0].Value, nil 85 | } 86 | 87 | func (c *Client) PutValue(ctx context.Context, key string, value []byte, opts ...v3.OpOption) error { 88 | ctx, cancel := context.WithTimeout(ctx, config.CommandTimeOut) 89 | defer cancel() 90 | _, err := c.client.Put(ctx, key, string(value), opts...) 91 | return err 92 | } 93 | 94 | func (c *Client) DeleteKey(ctx context.Context, key string, opts ...v3.OpOption) error { 95 | ctx, cancel := context.WithTimeout(ctx, config.CommandTimeOut) 96 | defer cancel() 97 | _, err := c.client.Delete(ctx, key, opts...) 98 | return err 99 | } 100 | 101 | func (c *Client) Close() error { 102 | return c.client.Close() 103 | } 104 | -------------------------------------------------------------------------------- /fs/file.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "context" 5 | "path/filepath" 6 | "syscall" 7 | 8 | "github.com/hanwen/go-fuse/v2/fs" 9 | "github.com/hanwen/go-fuse/v2/fuse" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // Callers should have lock held 14 | func (n *Node) resizeUnlocked(sz uint64) { 15 | if sz > uint64(cap(n.content)) { 16 | buf := make([]byte, sz) 17 | copy(buf, n.content) 18 | n.content = buf 19 | } else { 20 | n.content = n.content[:sz] 21 | } 22 | } 23 | 24 | // Open gets value from etcd, and saves it in "content" for later read 25 | func (n *Node) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { 26 | if n.content == nil { 27 | if rc, err := n.client.GetValue(ctx, n.path); err != nil { 28 | logrus.WithError(err).WithField("path", n.path).Errorf("Failed to get value from etcd") 29 | return nil, 0, syscall.EIO 30 | } else { 31 | n.rwMu.Lock() 32 | n.content = rc 33 | n.rwMu.Unlock() 34 | } 35 | } 36 | logrus.WithField("path", n.path).WithField("length", len(n.content)).Debug("Node Open") 37 | return n, fuse.FOPEN_DIRECT_IO, fs.OK 38 | } 39 | 40 | // Read returns bytes from "content", which should be filled by a prior Open operation 41 | func (n *Node) Read(ctx context.Context, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) { 42 | n.rwMu.RLock() 43 | defer n.rwMu.RUnlock() 44 | logrus.WithField("path", n.path).Debug("Node Read") 45 | 46 | end := int(off) + len(dest) 47 | if end > len(n.content) { 48 | end = len(n.content) 49 | } 50 | // We could copy to the `dest` buffer, but since we have a 51 | // []byte already, return that. 52 | return fuse.ReadResultData(n.content[off:end]), fs.OK 53 | } 54 | 55 | // Write saves to the internal "content" buffer 56 | func (n *Node) Write(ctx context.Context, fh fs.FileHandle, buf []byte, off int64) (uint32, syscall.Errno) { 57 | n.rwMu.Lock() 58 | defer n.rwMu.Unlock() 59 | logrus.WithField("path", n.path).WithField("length", len(buf)).Debug("Node Write") 60 | sz := int64(len(buf)) 61 | if off+sz > int64(len(n.content)) { 62 | n.resizeUnlocked(uint64(off + sz)) 63 | } 64 | copy(n.content[off:], buf) 65 | return uint32(sz), 0 66 | } 67 | 68 | // Create actually writes an empty value into etcd (as a placeholder) 69 | func (n *Node) Create(ctx context.Context, name string, flags uint32, mode uint32, out *fuse.EntryOut) (*fs.Inode, fs.FileHandle, uint32, syscall.Errno) { 70 | fullPath := filepath.Join(n.path, string(filepath.Separator), name) 71 | logrus.WithField("path", fullPath).Debug("Node Create") 72 | child := Node{ 73 | path: fullPath, 74 | client: n.client, 75 | isLeaf: true, 76 | } 77 | _, err := child.Write(ctx, nil, []byte{}, 0) 78 | return n.NewInode(ctx, &child, fs.StableAttr{Mode: child.getMode(child.isLeaf), Ino: n.inodeHash(child.path)}), nil, 0, err 79 | } 80 | 81 | // Flush puts file content into etcd 82 | func (n *Node) Flush(ctx context.Context, fh fs.FileHandle) syscall.Errno { 83 | logrus.WithField("path", n.path).Debug("Node Flush") 84 | n.rwMu.RLock() 85 | defer n.rwMu.RUnlock() 86 | if err := n.client.PutValue(ctx, n.path, n.content); err != nil { 87 | logrus.WithError(err).WithField("path", n.path).Errorf("Failed to put value into etcd") 88 | return syscall.EIO 89 | } 90 | return fs.OK 91 | } 92 | 93 | // Some editors (eg. Vim) need to call Fsync, so implement it here as a no-op 94 | func (n *Node) Fsync(ctx context.Context, f fs.FileHandle, flags uint32) syscall.Errno { 95 | logrus.WithField("path", n.path).Debug("Node Fsync") 96 | return fs.OK 97 | } 98 | 99 | // Unlink removes a key from etcd 100 | func (n *Node) Unlink(ctx context.Context, name string) syscall.Errno { 101 | fullPath := filepath.Join(n.path, string(filepath.Separator), name) 102 | logrus.WithField("path", fullPath).Debug("Node Unlink") 103 | if err := n.client.DeleteKey(ctx, fullPath); err != nil { 104 | logrus.WithError(err).WithField("path", fullPath).Errorf("Failed to delete key from etcd") 105 | return syscall.EIO 106 | } 107 | return fs.OK 108 | } 109 | 110 | // Implement Setattr to support truncation 111 | func (n *Node) Setattr(ctx context.Context, fh fs.FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) syscall.Errno { 112 | if sz, ok := in.GetSize(); ok { 113 | n.rwMu.Lock() 114 | n.resizeUnlocked(sz) 115 | n.rwMu.Unlock() 116 | } 117 | if errno := n.Flush(ctx, nil); errno != fs.OK { 118 | return errno 119 | } 120 | return n.Getattr(ctx, fh, out) 121 | } 122 | 123 | var ( 124 | _ fs.NodeUnlinker = &Node{} 125 | _ fs.NodeCreater = &Node{} 126 | _ fs.NodeOpener = &Node{} 127 | _ fs.FileReader = &Node{} 128 | _ fs.NodeWriter = &Node{} 129 | _ fs.NodeFlusher = &Node{} 130 | _ fs.NodeFsyncer = &Node{} 131 | _ fs.NodeSetattrer = &Node{} 132 | ) 133 | -------------------------------------------------------------------------------- /fs/folder.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "context" 5 | "hash/fnv" 6 | "os/user" 7 | "path/filepath" 8 | "strconv" 9 | "strings" 10 | "sync" 11 | "syscall" 12 | "time" 13 | 14 | "github.com/hanwen/go-fuse/v2/fs" 15 | "github.com/hanwen/go-fuse/v2/fuse" 16 | "github.com/polyrabbit/etcdfs/etcd" 17 | "github.com/sirupsen/logrus" 18 | v3 "go.etcd.io/etcd/v3/clientv3" 19 | ) 20 | 21 | // Set file owners to the current user, 22 | // otherwise in OSX, we will fail to start. 23 | var uid, gid uint32 24 | 25 | func init() { 26 | u, err := user.Current() 27 | if err != nil { 28 | panic(err) 29 | } 30 | uid32, _ := strconv.ParseUint(u.Uid, 10, 32) 31 | gid32, _ := strconv.ParseUint(u.Gid, 10, 32) 32 | uid = uint32(uid32) 33 | gid = uint32(gid32) 34 | } 35 | 36 | // A tree node in filesystem, it acts as both a directory and file 37 | type Node struct { 38 | fs.Inode 39 | client *etcd.Client 40 | isLeaf bool // A leaf of the filesystem tree means it's a file 41 | path string // File path to get to the current file 42 | 43 | rwMu sync.RWMutex // Protect file content 44 | content []byte // Internal buffer to hold the current file content 45 | } 46 | 47 | // NewRoot returns a file node - acting as a root, with inode sets to 1 and leaf sets to false 48 | func NewRoot(client *etcd.Client) *Node { 49 | return &Node{ 50 | client: client, 51 | isLeaf: false, 52 | } 53 | } 54 | 55 | // List keys under a certain prefix from etcd, and output the next hierarchy level 56 | func (n *Node) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) { 57 | parent := n.resolve("") 58 | logrus.WithField("path", parent).Debug("Node Readdir") 59 | 60 | entrySet := make(map[string]fuse.DirEntry) 61 | // We will have a fixed end. 62 | // A little tricky here that we use WithRange to override 'End' set by WithPrefix 63 | opts := []v3.OpOption{v3.WithRange(v3.GetPrefixRangeEnd(parent)), 64 | v3.WithSort(v3.SortByKey, v3.SortAscend), v3.WithLimit(500)} 65 | nextGroup := parent 66 | for { 67 | keys, moreKeys, err := n.client.ListKeys(ctx, nextGroup, opts...) 68 | if err != nil { 69 | logrus.WithError(err).WithField("path", parent).Errorf("Failed to list keys from etcd") 70 | return nil, syscall.EIO 71 | } 72 | 73 | var lastName string 74 | for _, key := range keys { 75 | nextLevel, hasMore := n.nextHierarchyLevel(key, parent) 76 | lastName = nextLevel 77 | if _, exist := entrySet[nextLevel]; exist { 78 | continue 79 | } 80 | entrySet[nextLevel] = fuse.DirEntry{ 81 | Mode: n.getMode(!hasMore), 82 | Name: nextLevel, 83 | Ino: n.inodeHash(nextLevel), 84 | } 85 | } 86 | // For a flat kv structure, we dont need to iterate all, eg. 87 | // /foo/1, /foo/2, /foo/3, /foo/4, /foo/5, /bar/1, /bar/2 88 | // when we find "/foo/1", we can skip all "/foo/xxx" folders and jump directly to "/bar/1" 89 | nextGroup = v3.GetPrefixRangeEnd(n.resolve(lastName)) // TODO: new path should end with "/"? 90 | 91 | if !moreKeys || len(keys) == 0 { 92 | break 93 | } 94 | if len(entrySet) > 1000 { 95 | logrus.Warn("Already fetched more than 1000 entries, skipping the rest for performance reason...") 96 | break 97 | } 98 | } 99 | 100 | entries := make([]fuse.DirEntry, 0, len(entrySet)) 101 | for _, e := range entrySet { 102 | entries = append(entries, e) 103 | } 104 | return fs.NewListDirStream(entries), fs.OK 105 | } 106 | 107 | // Returns next hierarchy level and tells if we have more hierarchies 108 | // path "/foo", parent "/" => "foo" 109 | func (n *Node) nextHierarchyLevel(path, parent string) (string, bool) { 110 | baseName := strings.TrimPrefix(path, parent) 111 | hierarchies := strings.SplitN(baseName, string(filepath.Separator), 2) 112 | return filepath.Clean(hierarchies[0]), len(hierarchies) >= 2 113 | } 114 | 115 | // resolve acts as `filepath.Join`, but we want the '/' separator always 116 | func (n *Node) resolve(fileName string) string { 117 | return n.path + string(filepath.Separator) + fileName 118 | } 119 | 120 | // Lookup finds a file under the current node(directory) 121 | func (n *Node) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { 122 | fullPath := n.resolve(name) 123 | logrus.WithField("path", fullPath).Debug("Node Lookup") 124 | keys, _, err := n.client.ListKeys(ctx, fullPath, v3.WithLimit(1)) 125 | if err != nil { 126 | logrus.WithError(err).WithField("path", fullPath).Errorf("Failed to list keys from etcd") 127 | return nil, syscall.EIO 128 | } 129 | if len(keys) == 0 { 130 | return nil, syscall.ENOENT 131 | } 132 | key := keys[0] 133 | child := Node{ 134 | path: fullPath, 135 | client: n.client, 136 | } 137 | if key == fullPath { 138 | child.isLeaf = true 139 | } else if strings.HasPrefix(key, fullPath+string(filepath.Separator)) { 140 | child.isLeaf = false 141 | } else { 142 | return nil, syscall.ENOENT 143 | } 144 | return n.NewInode(ctx, &child, fs.StableAttr{Mode: child.getMode(child.isLeaf), Ino: n.inodeHash(child.path)}), fs.OK 145 | } 146 | 147 | func (n *Node) getMode(isLeaf bool) uint32 { 148 | if isLeaf { 149 | return 0644 | uint32(syscall.S_IFREG) 150 | } else { 151 | return 0755 | uint32(syscall.S_IFDIR) 152 | } 153 | } 154 | 155 | // Getattr outputs file attributes 156 | // TODO: how to invalidate them? 157 | func (n *Node) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno { 158 | out.Mode = n.getMode(n.isLeaf) 159 | out.Size = uint64(len(n.content)) 160 | out.Ino = n.inodeHash(n.path) 161 | now := time.Now() 162 | out.SetTimes(&now, &now, &now) 163 | out.Uid = uid 164 | out.Gid = gid 165 | return fs.OK 166 | } 167 | 168 | // Hash file path into inode number, so we can ensure the same file always gets the same inode number 169 | func (n *Node) inodeHash(path string) uint64 { 170 | h := fnv.New64a() 171 | _, _ = h.Write([]byte(path)) 172 | return h.Sum64() 173 | } 174 | 175 | var ( 176 | _ fs.NodeGetattrer = &Node{} 177 | _ fs.NodeReaddirer = &Node{} 178 | _ fs.NodeLookuper = &Node{} 179 | ) 180 | -------------------------------------------------------------------------------- /fs/folder_test.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNode_nextHierarchyLevel(t *testing.T) { 8 | type args struct { 9 | path string 10 | parent string 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | want string 16 | want1 bool 17 | }{ 18 | { 19 | name: "root", 20 | args: args{path: "/foo", parent: "/"}, 21 | want: "foo", 22 | want1: false, 23 | }, { 24 | name: "root - has more", 25 | args: args{path: "/foo/bar", parent: "/"}, 26 | want: "foo", 27 | want1: true, 28 | }, 29 | } 30 | for _, tt := range tests { 31 | t.Run(tt.name, func(t *testing.T) { 32 | n := &Node{} 33 | level, hasMore := n.nextHierarchyLevel(tt.args.path, tt.args.parent) 34 | if level != tt.want { 35 | t.Errorf("nextHierarchyLevel() level = %v, want %v", level, tt.want) 36 | } 37 | if hasMore != tt.want1 { 38 | t.Errorf("nextHierarchyLevel() hasMore = %v, want %v", hasMore, tt.want1) 39 | } 40 | }) 41 | } 42 | } 43 | 44 | func TestNode_absPath(t *testing.T) { 45 | type args struct { 46 | parent string 47 | fileName string 48 | } 49 | tests := []struct { 50 | name string 51 | args args 52 | want string 53 | }{ 54 | { 55 | name: "normal case", 56 | args: args{"", "aaa"}, 57 | want: "/aaa", 58 | }, { 59 | name: "no file name", 60 | args: args{"", ""}, 61 | want: "/", 62 | }, { 63 | name: "under parent", 64 | args: args{"/aweme", ""}, 65 | want: "/aweme/", 66 | }, 67 | } 68 | for _, tt := range tests { 69 | t.Run(tt.name, func(t *testing.T) { 70 | n := &Node{path: tt.args.parent} 71 | if got := n.resolve(tt.args.fileName); got != tt.want { 72 | t.Errorf("resolve() = %v, want %v", got, tt.want) 73 | } 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /fs/server.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | "time" 8 | 9 | "github.com/hanwen/go-fuse/v2/fs" 10 | "github.com/hanwen/go-fuse/v2/fuse" 11 | "github.com/polyrabbit/etcdfs/config" 12 | "github.com/polyrabbit/etcdfs/etcd" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | type Server struct { 17 | *fuse.Server 18 | mountPoint string 19 | } 20 | 21 | // 200ms is enough for an operation to complete 22 | var cacheDuration = 200 * time.Millisecond 23 | 24 | func MustMount(mountPoint string, client *etcd.Client) *Server { 25 | opts := &fs.Options{ 26 | AttrTimeout: &cacheDuration, 27 | EntryTimeout: &cacheDuration, 28 | MountOptions: fuse.MountOptions{ 29 | Options: config.MountOptions, 30 | Debug: false, 31 | FsName: "etcdfs", 32 | }, 33 | } 34 | server, err := fs.Mount(mountPoint, NewRoot(client), opts) 35 | if err != nil { 36 | logrus.WithError(err).Fatal("Failed to mount") 37 | return nil 38 | } 39 | return &Server{ 40 | Server: server, 41 | mountPoint: mountPoint, 42 | } 43 | } 44 | 45 | func (s *Server) ListenForUnmount() { 46 | c := make(chan os.Signal, 1) 47 | signal.Notify(c, syscall.SIGTERM, syscall.SIGINT) 48 | sig := <-c 49 | logrus.Infof("Got %s signal, unmounting %q...", sig, s.mountPoint) 50 | err := s.Unmount() 51 | if err != nil { 52 | logrus.WithError(err).Errorf("Failed to unmount, try %q manually.", "umount "+s.mountPoint) 53 | } 54 | <-c // Double ctrl+c 55 | logrus.Warn("Force exiting...") 56 | os.Exit(1) 57 | } 58 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/polyrabbit/etcdfs 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/hanwen/go-fuse/v2 v2.1.0 7 | github.com/sirupsen/logrus v1.6.0 8 | github.com/spf13/cobra v0.0.3 9 | go.etcd.io/etcd/v3 v3.3.0-rc.0.0.20200707003333-58bb8ae09f8e 10 | go.uber.org/zap v1.14.1 11 | golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 5 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 6 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 7 | github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= 8 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 9 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 10 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 11 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 12 | github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa h1:OaNxuTZr7kxeODyLWsRMC+OD03aFUH+mW6r2d+MWa5Y= 13 | github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= 14 | github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY= 15 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 16 | github.com/coreos/go-systemd/v22 v22.0.0 h1:XJIw/+VlJ+87J+doOxznsAWIdmWuViOVhkQamW5YV28= 17 | github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= 18 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 19 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 21 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 22 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 23 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 24 | github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4 h1:qk/FSDDxo05wdJH28W+p5yivv7LuLYLRXPPD8KQCtZs= 25 | github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 26 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 27 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 28 | github.com/etcd-io/gofail v0.0.0-20190801230047-ad7f989257ca/go.mod h1:49H/RkXP8pKaZy4h0d+NW16rSLhyVBt4o6VLJbmOqDE= 29 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 30 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 31 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 32 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 33 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 34 | github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 35 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 36 | github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= 37 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 38 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 39 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 40 | github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903 h1:LbsanbbD6LieFkXbj9YNNBupiGHJgFeLpO0j0Fza1h8= 41 | github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 42 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 43 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 44 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 45 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 46 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 47 | github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= 48 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 49 | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= 50 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 51 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 52 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 53 | github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA= 54 | github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 55 | github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c h1:Lh2aW+HnU2Nbe1gqD9SOJLJxW1jBMmQOktN2acDyJk8= 56 | github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 57 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4 h1:z53tR0945TRRQO/fLEVPI6SMv7ZflF0TEaTAoU7tOzg= 58 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 59 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= 60 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 61 | github.com/grpc-ecosystem/grpc-gateway v1.9.5 h1:UImYN5qQ8tuGpGE16ZmjvcTtTw24zw1QAp/SlnNrZhI= 62 | github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 63 | github.com/hanwen/go-fuse v1.0.0 h1:GxS9Zrn6c35/BnfiVsZVWmsG803xwE7eVRDvcf/BEVc= 64 | github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok= 65 | github.com/hanwen/go-fuse/v2 v2.1.0 h1:+32ffteETaLYClUj0a3aHjZ1hOPxxaNEHiZiujuDaek= 66 | github.com/hanwen/go-fuse/v2 v2.1.0/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO00GgRLGryc= 67 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 68 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 69 | github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= 70 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 71 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 72 | github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= 73 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 74 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 75 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 76 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 77 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 78 | github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= 79 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 80 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 81 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 82 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 83 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 84 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 85 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 86 | github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= 87 | github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= 88 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 89 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 90 | github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 91 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 92 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 93 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 94 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 95 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 96 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 97 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 98 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 99 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 100 | github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= 101 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 102 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 103 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 104 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 105 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 106 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 107 | github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM= 108 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 109 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 110 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 111 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= 112 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 113 | github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw= 114 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 115 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 116 | github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs= 117 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 118 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 119 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 120 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 121 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 122 | github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= 123 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 124 | github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E= 125 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 126 | github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= 127 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 128 | github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4= 129 | github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 130 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 131 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 132 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 133 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 134 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 135 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 136 | github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8 h1:ndzgwNDnKIqyCvHTXaCqh9KlOWKvBry6nuXMJmonVsE= 137 | github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 138 | github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= 139 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= 140 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 141 | go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= 142 | go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= 143 | go.etcd.io/etcd/v3 v3.3.0-rc.0.0.20200707003333-58bb8ae09f8e h1:HZQLoe71Q24wVyDrGBRcVuogx32U+cPlcm/WoSLUI6c= 144 | go.etcd.io/etcd/v3 v3.3.0-rc.0.0.20200707003333-58bb8ae09f8e/go.mod h1:UENlOa05tkNvLx9VnNziSerG4Ro74upGK6Apd4v6M/Y= 145 | go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= 146 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 147 | go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= 148 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 149 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= 150 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 151 | go.uber.org/zap v1.14.1 h1:nYDKopTbvAPq/NrUVZwT15y2lpROBiLLyoRTbXOYWOo= 152 | go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= 153 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 154 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 155 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 156 | golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc h1:c0o/qxkaO2LF5t6fQrT4b5hzyggAkLLlCUjqfRxd8Q4= 157 | golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 158 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 159 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 160 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 161 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 162 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 163 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 164 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 165 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 166 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 167 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 168 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 169 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 170 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 171 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 172 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 173 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= 174 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 175 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 176 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 177 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 178 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 179 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 180 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 181 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 182 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 183 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 184 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 185 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 186 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 187 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 188 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 189 | golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666 h1:gVCS+QOncANNPlmlO1AhlU3oxs4V9z+gTtPwIk3p2N8= 190 | golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 191 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 192 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 193 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 h1:+DCIGbF/swA92ohVg0//6X2IVY3KZs6p9mix0ziNYJM= 194 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 195 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 196 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 197 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 198 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 199 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 200 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 201 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 202 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= 203 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 204 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 205 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 206 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 207 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 208 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= 209 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 210 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 211 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 212 | google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg= 213 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 214 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 215 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 216 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 217 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 218 | gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= 219 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 220 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 221 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 222 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 223 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 224 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 225 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 226 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 227 | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= 228 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 229 | sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= 230 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 231 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "net/http/pprof" 5 | "path/filepath" 6 | 7 | "github.com/polyrabbit/etcdfs/config" 8 | "github.com/polyrabbit/etcdfs/etcd" 9 | "github.com/polyrabbit/etcdfs/fs" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | func main() { 14 | if !config.Execute() { 15 | return 16 | } 17 | client := etcd.MustNew() 18 | defer client.Close() 19 | mountPoint, err := filepath.Abs(config.MountPoint) 20 | if err != nil { 21 | logrus.WithError(err).WithField("mountPoint", mountPoint).Fatal("Failed to get abs file path") 22 | return 23 | } 24 | server := fs.MustMount(mountPoint, client) 25 | go server.ListenForUnmount() 26 | logrus.Infof("Mounted to %q, use ctrl+c to terminate.", mountPoint) 27 | server.Wait() 28 | } 29 | --------------------------------------------------------------------------------