├── .dockerignore ├── .github └── workflows │ └── build_image.yaml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── conf ├── .gitignore ├── config.default.ini └── config.go ├── docker-compose.yml ├── e3ch └── e3ch.go ├── go.mod ├── go.sum ├── images ├── kv.png ├── members.png ├── roles.png ├── setting.png └── users.png ├── main.go ├── routers ├── errors.go ├── key.go ├── member.go ├── resp.go ├── roles.go ├── routers.go ├── users.go └── utils.go └── static ├── .gitignore ├── package.json ├── src ├── components │ ├── App.jsx │ ├── AuthPanel.jsx │ ├── KeyValue.jsx │ ├── KeyValueCreate.jsx │ ├── KeyValueItem.jsx │ ├── KeyValueSetting.jsx │ ├── Members.jsx │ ├── Roles.jsx │ ├── RolesSetting.jsx │ ├── Setting.jsx │ ├── Users.jsx │ ├── UsersSetting.jsx │ ├── request.jsx │ └── utils.jsx ├── css │ └── patch.css ├── entry.jsx └── index.html └── webpack.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | images 2 | static/dist 3 | static/node_modules 4 | Dockerfile 5 | -------------------------------------------------------------------------------- /.github/workflows/build_image.yaml: -------------------------------------------------------------------------------- 1 | name: e3w-build-image 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | tags: 8 | - v* 9 | workflow_dispatch: 10 | branches: ["*"] 11 | 12 | jobs: 13 | docker: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v3 18 | - name: Set up QEMU 19 | uses: docker/setup-qemu-action@v2 20 | - name: Set up Docker Buildx 21 | uses: docker/setup-buildx-action@v2 22 | - name: Login to DockerHub 23 | uses: docker/login-action@v2 24 | with: 25 | username: ${{ secrets.DOCKERHUB_USERNAME }} 26 | password: ${{ secrets.DOCKERHUB_TOKEN }} 27 | - name: Build and push 28 | uses: docker/build-push-action@v4 29 | with: 30 | context: . 31 | platforms: linux/amd64,linux/arm64 32 | push: true 33 | tags: soyking/e3w:${{ github.ref_name }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Go template 3 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 4 | *.o 5 | *.a 6 | *.so 7 | 8 | # Folders 9 | _obj 10 | _test 11 | 12 | # Architecture specific extensions/prefixes 13 | *.[568vq] 14 | [568vq].out 15 | 16 | *.cgo1.go 17 | *.cgo2.c 18 | _cgo_defun.c 19 | _cgo_gotypes.go 20 | _cgo_export.* 21 | 22 | _testmain.go 23 | 24 | *.exe 25 | *.test 26 | *.prof 27 | 28 | e3w 29 | vendor 30 | .idea 31 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # docker build -t soyking/e3w . 2 | FROM golang:1.17 as backend 3 | RUN mkdir -p /e3w 4 | ADD . /e3w 5 | WORKDIR /e3w 6 | RUN CGO_ENABLED=0 GOPROXY=https://goproxy.io go build 7 | 8 | FROM node:8 as frontend 9 | RUN mkdir /app 10 | ADD static /app 11 | WORKDIR /app 12 | RUN npm --registry=https://registry.npmmirror.com \ 13 | --cache=$HOME/.npm/.cache/cnpm \ 14 | --disturl=https://npmmirror.com/mirrors/node \ 15 | --userconfig=$HOME/.cnpmrc install && npm run publish 16 | 17 | FROM alpine:latest 18 | RUN mkdir -p /app/static/dist /app/conf 19 | COPY --from=backend /e3w/e3w /app 20 | COPY --from=frontend /app/dist /app/static/dist 21 | COPY conf/config.default.ini /app/conf 22 | EXPOSE 8080 23 | WORKDIR /app 24 | CMD ["./e3w"] 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 soyking 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | e3w 2 | === 3 | 4 | etcd v3 Web UI based on [Golang](https://golang.org/) && [React](https://facebook.github.io/react/), copy from [consul ui](https://github.com/hashicorp/consul/tree/master/ui) :) 5 | 6 | supporting hierarchy on etcd v3, based on [e3ch](https://github.com/soyking/e3ch) 7 | 8 | ## Quick Start 9 | 10 | ``` 11 | git clone https://github.com/soyking/e3w.git 12 | cd e3w 13 | docker-compose up 14 | # open http://localhost:8080 15 | ``` 16 | 17 | Or use docker image by `docker pull soyking/e3w`, more details in `Dockerfile` and `docker-compose.yml` 18 | 19 | ## Overview 20 | 21 | KEY/VALUE 22 | 23 | ![](./images/kv.png) 24 | 25 | MEMBERS 26 | 27 | ![](./images/members.png) 28 | 29 | ROLES 30 | 31 | ![](./images/roles.png) 32 | 33 | USERS 34 | 35 | ![](./images/users.png) 36 | 37 | SETTING 38 | 39 | ![](./images/setting.png) 40 | 41 | ## Usage 42 | 43 | 1.Fetch the project `go get github.com/soyking/e3w` 44 | 45 | 46 | 2.frontend 47 | 48 | ``` 49 | cd static 50 | npm install 51 | npm run publish 52 | ``` 53 | 54 | 3.backend 55 | 56 | a. Start etcd, such as [goreman](https://github.com/coreos/etcd/#running-a-local-etcd-cluster) 57 | 58 | b. Edit conf/config.default.ini if needed, `go build && ./e3w` 59 | 60 | c. For auth: 61 | 62 | ``` 63 | ETCDCTL_API=3 etcdctl auth enable 64 | # edit conf/config.default.ini[app]#auth 65 | ./e3w 66 | # you could set your username and password in SETTING page 67 | ``` 68 | 69 | 4.build image 70 | 71 | Install dependencies in 3.b, then run `docker build -t soyking/e3w .` 72 | 73 | ## Notice 74 | 75 | - When you want to add some permissions in directories to a user, the implement of hierarchy in [e3ch](https://github.com/soyking/e3ch) requires you to set a directory's READ permission. For example, if role `roleA` has the permission of directory `dir1/dir2`, then it should have permissions: 76 | 77 | ``` 78 | KV Read: 79 | dir1/dir2 80 | [dir1/dir2/, dir1/dir20) (prefix dir1/dir2/) 81 | KV Write: 82 | [dir1/dir2/, dir1/dir20) (prefix dir1/dir2/) 83 | ``` 84 | 85 | When `userA` was granted `roleA`, `userA` could open the by `http://e3w-address.com/#/kv/dir1/dir2` to view and edit the key/value 86 | 87 | - Access key/value by etcdctl, [issue](https://github.com/soyking/e3w/issues/3). But the best way to access key/value is using [e3ch](https://github.com/soyking/e3ch). 88 | -------------------------------------------------------------------------------- /conf/.gitignore: -------------------------------------------------------------------------------- 1 | *.ini 2 | !config.default.ini -------------------------------------------------------------------------------- /conf/config.default.ini: -------------------------------------------------------------------------------- 1 | [app] 2 | port=8080 3 | auth=false 4 | 5 | [etcd] 6 | root_key=e3w_test 7 | dir_value= 8 | addr=etcd:2379,etcd:22379,etcd:32379 9 | dial_timeout=10s 10 | username= 11 | password= 12 | cert_file= 13 | key_file= 14 | ca_file= 15 | skip_verify_tls=false 16 | -------------------------------------------------------------------------------- /conf/config.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "time" 5 | 6 | "gopkg.in/ini.v1" 7 | ) 8 | 9 | const ( 10 | EtcdTimeout = time.Second * 10 11 | ) 12 | 13 | type Config struct { 14 | Port string 15 | Auth bool 16 | EtcdRootKey string 17 | DirValue string 18 | EtcdEndPoints []string 19 | EtcdUsername string 20 | EtcdPassword string 21 | EtcdDialTimeout time.Duration 22 | CertFile string 23 | KeyFile string 24 | CAFile string 25 | SkipVerifyTLS bool 26 | } 27 | 28 | func Init(filepath string) (*Config, error) { 29 | cfg, err := ini.Load(filepath) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | c := &Config{} 35 | 36 | appSec := cfg.Section("app") 37 | c.Port = appSec.Key("port").Value() 38 | c.Auth = appSec.Key("auth").MustBool() 39 | 40 | etcdSec := cfg.Section("etcd") 41 | c.EtcdRootKey = etcdSec.Key("root_key").Value() 42 | c.DirValue = etcdSec.Key("dir_value").Value() 43 | c.EtcdEndPoints = etcdSec.Key("addr").Strings(",") 44 | c.EtcdDialTimeout = etcdSec.Key("dial_timeout").MustDuration(EtcdTimeout) 45 | c.EtcdUsername = etcdSec.Key("username").Value() 46 | c.EtcdPassword = etcdSec.Key("password").Value() 47 | c.CertFile = etcdSec.Key("cert_file").Value() 48 | c.KeyFile = etcdSec.Key("key_file").Value() 49 | c.CAFile = etcdSec.Key("ca_file").Value() 50 | c.SkipVerifyTLS = etcdSec.Key("skip_verify_tls").MustBool() 51 | 52 | return c, nil 53 | } 54 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | etcd: 5 | image: soyking/etcd-goreman:3.2.7 6 | environment: 7 | - CLIENT_ADDR=etcd 8 | e3w: 9 | image: soyking/e3w:master 10 | volumes: 11 | - ./conf/config.default.ini:/app/conf/config.default.ini 12 | ports: 13 | - "8080:8080" 14 | depends_on: 15 | - etcd 16 | -------------------------------------------------------------------------------- /e3ch/e3ch.go: -------------------------------------------------------------------------------- 1 | package e3ch 2 | 3 | import ( 4 | "crypto/tls" 5 | 6 | client "github.com/soyking/e3ch" 7 | "github.com/soyking/e3w/conf" 8 | "go.etcd.io/etcd/client/pkg/v3/transport" 9 | clientv3 "go.etcd.io/etcd/client/v3" 10 | ) 11 | 12 | func NewE3chClient(config *conf.Config) (*client.EtcdHRCHYClient, error) { 13 | var tlsConfig *tls.Config 14 | var err error 15 | if config.CertFile != "" && config.KeyFile != "" && config.CAFile != "" { 16 | tlsInfo := transport.TLSInfo{ 17 | CertFile: config.CertFile, 18 | KeyFile: config.KeyFile, 19 | TrustedCAFile: config.CAFile, 20 | InsecureSkipVerify: config.SkipVerifyTLS, 21 | } 22 | tlsConfig, err = tlsInfo.ClientConfig() 23 | if err != nil { 24 | return nil, err 25 | } 26 | } 27 | 28 | clt, err := clientv3.New(clientv3.Config{ 29 | Endpoints: config.EtcdEndPoints, 30 | Username: config.EtcdUsername, 31 | Password: config.EtcdPassword, 32 | TLS: tlsConfig, 33 | DialTimeout: config.EtcdDialTimeout, 34 | }) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | client, err := client.New(clt, config.EtcdRootKey, config.DirValue) 40 | if err != nil { 41 | return nil, err 42 | } 43 | return client, client.FormatRootKey() 44 | } 45 | 46 | func CloneE3chClient(username, password string, client *client.EtcdHRCHYClient) (*client.EtcdHRCHYClient, error) { 47 | clt, err := clientv3.New(clientv3.Config{ 48 | Endpoints: client.EtcdClient().Endpoints(), 49 | Username: username, 50 | Password: password, 51 | }) 52 | if err != nil { 53 | return nil, err 54 | } 55 | return client.Clone(clt), nil 56 | } 57 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/soyking/e3w 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.7.7 7 | github.com/smartystreets/goconvey v1.6.4 // indirect 8 | github.com/soyking/e3ch v1.1.1 9 | go.etcd.io/etcd/api/v3 v3.5.2 10 | go.etcd.io/etcd/client/pkg/v3 v3.5.2 11 | go.etcd.io/etcd/client/v3 v3.5.2 12 | golang.org/x/net v0.7.0 13 | gopkg.in/ini.v1 v1.61.0 14 | ) 15 | 16 | require ( 17 | github.com/coreos/go-semver v0.3.0 // indirect 18 | github.com/coreos/go-systemd/v22 v22.3.2 // indirect 19 | github.com/gin-contrib/sse v0.1.0 // indirect 20 | github.com/go-playground/locales v0.13.0 // indirect 21 | github.com/go-playground/universal-translator v0.17.0 // indirect 22 | github.com/go-playground/validator/v10 v10.4.1 // indirect 23 | github.com/gogo/protobuf v1.3.2 // indirect 24 | github.com/golang/protobuf v1.5.2 // indirect 25 | github.com/json-iterator/go v1.1.11 // indirect 26 | github.com/leodido/go-urn v1.2.0 // indirect 27 | github.com/mattn/go-isatty v0.0.12 // indirect 28 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 29 | github.com/modern-go/reflect2 v1.0.1 // indirect 30 | github.com/ugorji/go/codec v1.1.7 // indirect 31 | go.uber.org/atomic v1.7.0 // indirect 32 | go.uber.org/multierr v1.6.0 // indirect 33 | go.uber.org/zap v1.17.0 // indirect 34 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect 35 | golang.org/x/sys v0.5.0 // indirect 36 | golang.org/x/text v0.7.0 // indirect 37 | google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect 38 | google.golang.org/grpc v1.38.0 // indirect 39 | google.golang.org/protobuf v1.26.0 // indirect 40 | gopkg.in/yaml.v2 v2.4.0 // indirect 41 | ) 42 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 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/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 6 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 7 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 8 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 9 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 10 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 11 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 12 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 13 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 14 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 15 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 16 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 17 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 18 | github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= 19 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 20 | github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= 21 | github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 22 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 24 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 26 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 27 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 28 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 29 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 30 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 31 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 32 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 33 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 34 | github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs= 35 | github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= 36 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 37 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 38 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 39 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 40 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 41 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 42 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 43 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 44 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 45 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 46 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 47 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 48 | github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= 49 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 50 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 51 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 52 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 53 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 54 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 55 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 56 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 57 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 58 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 59 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 60 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 61 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 62 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 63 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 64 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 65 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 66 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 67 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 68 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 69 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 70 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 71 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 72 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 73 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 74 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 75 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 76 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 77 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 78 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 79 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 80 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 81 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 82 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 83 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 84 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 85 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 86 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 87 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 88 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 89 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 90 | github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= 91 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 92 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 93 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 94 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 95 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 96 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 97 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 98 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 99 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 100 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 101 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 102 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 103 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 104 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 105 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 106 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 107 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 108 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 109 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 110 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 111 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 112 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 113 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 114 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 115 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 116 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 117 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 118 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 119 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 120 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 121 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 122 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 123 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 124 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 125 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 126 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 127 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 128 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 129 | github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= 130 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 131 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 132 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 133 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 134 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 135 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 136 | github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= 137 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 138 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 139 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 140 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 141 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 142 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 143 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 144 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 145 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 146 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 147 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 148 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 149 | github.com/soyking/e3ch v1.1.1 h1:ow0afNCmPAYyPxdiCg5WdETvfYfQNDQbMNf2WEyqjzk= 150 | github.com/soyking/e3ch v1.1.1/go.mod h1:yqwbBijL3SBRT82TojQ8oI5w3MkPjYnGTEWZd43SvMc= 151 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 152 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 153 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 154 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 155 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 156 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 157 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 158 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 159 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 160 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 161 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 162 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 163 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 164 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 165 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 166 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 167 | go.etcd.io/etcd/api/v3 v3.5.2 h1:tXok5yLlKyuQ/SXSjtqHc4uzNaMqZi2XsoSPr/LlJXI= 168 | go.etcd.io/etcd/api/v3 v3.5.2/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= 169 | go.etcd.io/etcd/client/pkg/v3 v3.5.2 h1:4hzqQ6hIb3blLyQ8usCU4h3NghkqcsohEQ3o3VetYxE= 170 | go.etcd.io/etcd/client/pkg/v3 v3.5.2/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= 171 | go.etcd.io/etcd/client/v3 v3.5.2 h1:WdnejrUtQC4nCxK0/dLTMqKOB+U5TP/2Ya0BJL+1otA= 172 | go.etcd.io/etcd/client/v3 v3.5.2/go.mod h1:kOOaWFFgHygyT0WlSmL8TJiXmMysO/nNUlEsSsN6W4o= 173 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 174 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 175 | go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= 176 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 177 | go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= 178 | go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= 179 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 180 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 181 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 182 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 183 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= 184 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 185 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 186 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 187 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 188 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 189 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 190 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 191 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 192 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 193 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 194 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 195 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 196 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 197 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 198 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 199 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 200 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 201 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 202 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 203 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 204 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 205 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 206 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 207 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 208 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 209 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 210 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 211 | golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= 212 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 213 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 214 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 215 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 216 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 217 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 218 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 219 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 220 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 221 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 222 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 223 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 224 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 225 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 226 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 227 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 228 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 229 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 230 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 231 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 232 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 233 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 234 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 235 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 236 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 237 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 238 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 239 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 240 | golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 241 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 242 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 243 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 244 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 245 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 246 | golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= 247 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 248 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 249 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 250 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 251 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 252 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 253 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 254 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 255 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 256 | golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= 257 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 258 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 259 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 260 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 261 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 262 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 263 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 264 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 265 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 266 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 267 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 268 | golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 269 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 270 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 271 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 272 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 273 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 274 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 275 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 276 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 277 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 278 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 279 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 280 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 281 | google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c h1:wtujag7C+4D6KMoulW9YauvK2lgdvCMS260jsqqBXr0= 282 | google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= 283 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 284 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 285 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 286 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 287 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 288 | google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0= 289 | google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= 290 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 291 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 292 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 293 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 294 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 295 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 296 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 297 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 298 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 299 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 300 | google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= 301 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 302 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 303 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 304 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 305 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 306 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 307 | gopkg.in/ini.v1 v1.61.0 h1:LBCdW4FmFYL4s/vDZD1RQYX7oAR6IjujCYgMdbHBR10= 308 | gopkg.in/ini.v1 v1.61.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 309 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 310 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 311 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 312 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 313 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 314 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 315 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 316 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 317 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 318 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 319 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 320 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 321 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 322 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 323 | sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= 324 | -------------------------------------------------------------------------------- /images/kv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soyking/e3w/e69ab7ff3b587f1940fd973aad0bbcda0177c7bb/images/kv.png -------------------------------------------------------------------------------- /images/members.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soyking/e3w/e69ab7ff3b587f1940fd973aad0bbcda0177c7bb/images/members.png -------------------------------------------------------------------------------- /images/roles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soyking/e3w/e69ab7ff3b587f1940fd973aad0bbcda0177c7bb/images/roles.png -------------------------------------------------------------------------------- /images/setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soyking/e3w/e69ab7ff3b587f1940fd973aad0bbcda0177c7bb/images/setting.png -------------------------------------------------------------------------------- /images/users.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soyking/e3w/e69ab7ff3b587f1940fd973aad0bbcda0177c7bb/images/users.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/soyking/e3w/conf" 10 | "github.com/soyking/e3w/e3ch" 11 | "github.com/soyking/e3w/routers" 12 | "go.etcd.io/etcd/api/v3/version" 13 | ) 14 | 15 | const ( 16 | PROGRAM_NAME = "e3w" 17 | PROGRAM_VERSION = "0.1.0" 18 | ) 19 | 20 | var configFilepath string 21 | 22 | func init() { 23 | flag.StringVar(&configFilepath, "conf", "conf/config.default.ini", "config file path") 24 | rev := flag.Bool("rev", false, "print rev") 25 | flag.Parse() 26 | 27 | if *rev { 28 | fmt.Printf("[%s v%s]\n[etcd %s]\n", 29 | PROGRAM_NAME, PROGRAM_VERSION, 30 | version.Version, 31 | ) 32 | os.Exit(0) 33 | } 34 | } 35 | 36 | func main() { 37 | config, err := conf.Init(configFilepath) 38 | if err != nil { 39 | panic(err) 40 | } 41 | 42 | client, err := e3ch.NewE3chClient(config) 43 | if err != nil { 44 | panic(err) 45 | } 46 | 47 | router := gin.Default() 48 | router.UseRawPath = true 49 | routers.InitRouters(router, config, client) 50 | if err := router.Run(":" + config.Port); err != nil { 51 | panic(err) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /routers/errors.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import "errors" 4 | 5 | var ( 6 | errRoleName = errors.New("role's name should not be empty") 7 | errInvalidPermType = errors.New("perm type should be READ | WRITE | READWRITE") 8 | 9 | errUserName = errors.New("user's name should not be empty") 10 | ) 11 | -------------------------------------------------------------------------------- /routers/key.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/soyking/e3ch" 6 | ) 7 | 8 | type Node struct { 9 | Key string `json:"key"` 10 | Value string `json:"value"` 11 | IsDir bool `json:"is_dir"` 12 | } 13 | 14 | func parseNode(node *client.Node) *Node { 15 | return &Node{ 16 | Key: string(node.Key), 17 | Value: string(node.Value), 18 | IsDir: node.IsDir, 19 | } 20 | } 21 | 22 | func getKeyHandler(c *gin.Context, client *client.EtcdHRCHYClient) (interface{}, error) { 23 | _, list := c.GetQuery("list") 24 | key := c.Param("key") 25 | 26 | if list { 27 | nodes, err := client.List(key) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | realNodes := []*Node{} 33 | for _, node := range nodes { 34 | realNodes = append(realNodes, parseNode(node)) 35 | } 36 | return realNodes, nil 37 | } else { 38 | node, err := client.Get(key) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | return parseNode(node), nil 44 | } 45 | } 46 | 47 | type postRequest struct { 48 | Value string `json:"value"` 49 | } 50 | 51 | func postKeyHandler(c *gin.Context, client *client.EtcdHRCHYClient) (interface{}, error) { 52 | _, dir := c.GetQuery("dir") 53 | key := c.Param("key") 54 | 55 | if dir { 56 | return nil, client.CreateDir(key) 57 | } else { 58 | r := new(postRequest) 59 | err := parseBody(c, r) 60 | if err != nil { 61 | return nil, err 62 | } 63 | return nil, client.Create(key, r.Value) 64 | } 65 | } 66 | 67 | func putKeyHandler(c *gin.Context, client *client.EtcdHRCHYClient) (interface{}, error) { 68 | key := c.Param("key") 69 | r := new(postRequest) 70 | err := parseBody(c, r) 71 | if err != nil { 72 | return nil, err 73 | } 74 | return nil, client.Put(key, r.Value) 75 | } 76 | 77 | func delKeyHandler(c *gin.Context, client *client.EtcdHRCHYClient) (interface{}, error) { 78 | return nil, client.Delete(c.Param("key")) 79 | } 80 | -------------------------------------------------------------------------------- /routers/member.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "go.etcd.io/etcd/api/v3/etcdserverpb" 6 | clientv3 "go.etcd.io/etcd/client/v3" 7 | ) 8 | 9 | const ( 10 | ROLE_LEADER = "leader" 11 | ROLE_FOLLOWER = "follower" 12 | 13 | STATUS_HEALTHY = "healthy" 14 | STATUS_UNHEALTHY = "unhealthy" 15 | ) 16 | 17 | type Member struct { 18 | *etcdserverpb.Member 19 | Role string `json:"role"` 20 | Status string `json:"status"` 21 | DbSize int64 `json:"db_size"` 22 | } 23 | 24 | func getMembersHandler(c *gin.Context, client *clientv3.Client) (interface{}, error) { 25 | resp, err := client.MemberList(newEtcdCtx()) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | members := []*Member{} 31 | for _, member := range resp.Members { 32 | if len(member.ClientURLs) > 0 { 33 | m := &Member{Member: member, Role: ROLE_FOLLOWER, Status: STATUS_UNHEALTHY} 34 | resp, err := client.Status(newEtcdCtx(), m.ClientURLs[0]) 35 | if err == nil { 36 | m.Status = STATUS_HEALTHY 37 | m.DbSize = resp.DbSize 38 | if resp.Leader == resp.Header.MemberId { 39 | m.Role = ROLE_LEADER 40 | } 41 | } 42 | members = append(members, m) 43 | } 44 | } 45 | 46 | return members, nil 47 | } 48 | -------------------------------------------------------------------------------- /routers/resp.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net/http" 6 | ) 7 | 8 | type response struct { 9 | Result interface{} `json:"result"` 10 | Err string `json:"err"` 11 | } 12 | 13 | type respHandler func(c *gin.Context) (interface{}, error) 14 | 15 | func resp(handler respHandler) gin.HandlerFunc { 16 | return func(c *gin.Context) { 17 | result, err := handler(c) 18 | r := &response{} 19 | if err != nil { 20 | r.Err = err.Error() 21 | } else { 22 | r.Result = result 23 | } 24 | c.JSON(http.StatusOK, r) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /routers/roles.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | client "github.com/soyking/e3ch" 6 | "go.etcd.io/etcd/api/v3/authpb" 7 | clientv3 "go.etcd.io/etcd/client/v3" 8 | ) 9 | 10 | func getRolesHandler(c *gin.Context, client *clientv3.Client) (interface{}, error) { 11 | resp, err := client.RoleList(newEtcdCtx()) 12 | if err != nil { 13 | return nil, err 14 | } else { 15 | return resp.Roles, nil 16 | } 17 | } 18 | 19 | type createRoleRequest struct { 20 | Name string `json:"name"` 21 | } 22 | 23 | func createRoleHandler(c *gin.Context, client *clientv3.Client) (interface{}, error) { 24 | r := new(createRoleRequest) 25 | err := parseBody(c, r) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | if r.Name == "" { 31 | return nil, errRoleName 32 | } 33 | 34 | _, err = client.RoleAdd(newEtcdCtx(), r.Name) 35 | return nil, err 36 | } 37 | 38 | func getRolePermsHandler(c *gin.Context, client *client.EtcdHRCHYClient) (interface{}, error) { 39 | name := c.Param("name") 40 | if name == "" { 41 | return nil, errRoleName 42 | } 43 | 44 | return client.GetRolePerms(name) 45 | } 46 | 47 | func deleteRoleHandler(c *gin.Context, client *clientv3.Client) (interface{}, error) { 48 | name := c.Param("name") 49 | if name == "" { 50 | return nil, errRoleName 51 | } 52 | 53 | _, err := client.RoleDelete(newEtcdCtx(), name) 54 | return nil, err 55 | } 56 | 57 | type createRolePermRequest struct { 58 | Key string `json:"key"` 59 | RangeEnd string `json:"range_end"` 60 | PermType string `json:"perm_type"` 61 | } 62 | 63 | func createRolePermHandler(c *gin.Context, client *client.EtcdHRCHYClient) (interface{}, error) { 64 | name := c.Param("name") 65 | if name == "" { 66 | return nil, errRoleName 67 | } 68 | 69 | r := new(createRolePermRequest) 70 | err := parseBody(c, r) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | tp, ok := authpb.Permission_Type_value[r.PermType] 76 | if !ok { 77 | return nil, errInvalidPermType 78 | } 79 | 80 | _, withPrefix := c.GetQuery("prefix") 81 | if withPrefix { 82 | r.RangeEnd = clientv3.GetPrefixRangeEnd(r.Key) 83 | } 84 | 85 | return nil, client.RoleGrantPermission(name, r.Key, r.RangeEnd, clientv3.PermissionType(tp)) 86 | } 87 | 88 | type deleteRolePermRequest struct { 89 | Key string `json:"key"` 90 | RangeEnd string `json:"range_end"` 91 | } 92 | 93 | func deleteRolePermHandler(c *gin.Context, client *client.EtcdHRCHYClient) (interface{}, error) { 94 | name := c.Param("name") 95 | if name == "" { 96 | return nil, errRoleName 97 | } 98 | 99 | r := new(deleteRolePermRequest) 100 | err := parseBody(c, r) 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | _, withPrefix := c.GetQuery("prefix") 106 | if withPrefix { 107 | r.RangeEnd = clientv3.GetPrefixRangeEnd(r.Key) 108 | } 109 | 110 | return nil, client.RoleRevokePermission(name, r.Key, r.RangeEnd) 111 | } 112 | -------------------------------------------------------------------------------- /routers/routers.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | client "github.com/soyking/e3ch" 6 | "github.com/soyking/e3w/conf" 7 | "github.com/soyking/e3w/e3ch" 8 | clientv3 "go.etcd.io/etcd/client/v3" 9 | ) 10 | 11 | const ( 12 | ETCD_USERNAME_HEADER = "X-Etcd-Username" 13 | ETCD_PASSWORD_HEADER = "X-Etcd-Password" 14 | ) 15 | 16 | type e3chHandler func(*gin.Context, *client.EtcdHRCHYClient) (interface{}, error) 17 | 18 | type groupHandler func(e3chHandler) respHandler 19 | 20 | func withE3chGroup(e3chClt *client.EtcdHRCHYClient, config *conf.Config) groupHandler { 21 | return func(h e3chHandler) respHandler { 22 | return func(c *gin.Context) (interface{}, error) { 23 | clt := e3chClt 24 | if config.Auth { 25 | var err error 26 | username := c.Request.Header.Get(ETCD_USERNAME_HEADER) 27 | password := c.Request.Header.Get(ETCD_PASSWORD_HEADER) 28 | clt, err = e3ch.CloneE3chClient(username, password, e3chClt) 29 | if err != nil { 30 | return nil, err 31 | } 32 | defer clt.EtcdClient().Close() 33 | } 34 | return h(c, clt) 35 | } 36 | } 37 | } 38 | 39 | type etcdHandler func(*gin.Context, *clientv3.Client) (interface{}, error) 40 | 41 | func etcdWrapper(h etcdHandler) e3chHandler { 42 | return func(c *gin.Context, e3chClt *client.EtcdHRCHYClient) (interface{}, error) { 43 | return h(c, e3chClt.EtcdClient()) 44 | } 45 | } 46 | 47 | func InitRouters(g *gin.Engine, config *conf.Config, e3chClt *client.EtcdHRCHYClient) { 48 | g.GET("/", func(c *gin.Context) { 49 | c.File("./static/dist/index.html") 50 | }) 51 | g.Static("/public", "./static/dist") 52 | 53 | e3chGroup := withE3chGroup(e3chClt, config) 54 | 55 | // key/value actions 56 | g.GET("/kv/*key", resp(e3chGroup(getKeyHandler))) 57 | g.POST("/kv/*key", resp(e3chGroup(postKeyHandler))) 58 | g.PUT("/kv/*key", resp(e3chGroup(putKeyHandler))) 59 | g.DELETE("/kv/*key", resp(e3chGroup(delKeyHandler))) 60 | 61 | // members actions 62 | g.GET("/members", resp(e3chGroup(etcdWrapper(getMembersHandler)))) 63 | 64 | // roles actions 65 | g.GET("/roles", resp(e3chGroup(etcdWrapper(getRolesHandler)))) 66 | g.POST("/role", resp(e3chGroup(etcdWrapper(createRoleHandler)))) 67 | g.GET("/role/:name", resp(e3chGroup(getRolePermsHandler))) 68 | g.DELETE("/role/:name", resp(e3chGroup(etcdWrapper(deleteRoleHandler)))) 69 | g.POST("/role/:name/permission", resp(e3chGroup(createRolePermHandler))) 70 | g.DELETE("/role/:name/permission", resp(e3chGroup(deleteRolePermHandler))) 71 | 72 | // users actions 73 | g.GET("/users", resp(e3chGroup(etcdWrapper(getUsersHandler)))) 74 | g.POST("/user", resp(e3chGroup(etcdWrapper(createUserHandler)))) 75 | g.GET("/user/:name", resp(e3chGroup(etcdWrapper(getUserRolesHandler)))) 76 | g.DELETE("/user/:name", resp(e3chGroup(etcdWrapper(deleteUserHandler)))) 77 | g.PUT("/user/:name/password", resp(e3chGroup(etcdWrapper(setUserPasswordHandler)))) 78 | g.PUT("/user/:name/role/:role", resp(e3chGroup(etcdWrapper(grantUserRoleHandler)))) 79 | g.DELETE("/user/:name/role/:role", resp(e3chGroup(etcdWrapper(revokeUserRoleHandler)))) 80 | } 81 | -------------------------------------------------------------------------------- /routers/users.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | clientv3 "go.etcd.io/etcd/client/v3" 6 | ) 7 | 8 | func getUsersHandler(c *gin.Context, client *clientv3.Client) (interface{}, error) { 9 | resp, err := client.UserList(newEtcdCtx()) 10 | if err != nil { 11 | return nil, err 12 | } else { 13 | return resp.Users, nil 14 | } 15 | } 16 | 17 | type createUserRequest struct { 18 | Name string `json:"name"` 19 | Password string `json:"password"` 20 | } 21 | 22 | func createUserHandler(c *gin.Context, client *clientv3.Client) (interface{}, error) { 23 | r := new(createUserRequest) 24 | err := parseBody(c, r) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | if r.Name == "" { 30 | return nil, errUserName 31 | } 32 | 33 | _, err = client.UserAdd(newEtcdCtx(), r.Name, r.Password) 34 | return nil, err 35 | } 36 | 37 | func getUserRolesHandler(c *gin.Context, client *clientv3.Client) (interface{}, error) { 38 | name := c.Param("name") 39 | resp, err := client.UserGet(newEtcdCtx(), name) 40 | if err != nil { 41 | return nil, err 42 | } else { 43 | return resp.Roles, nil 44 | } 45 | } 46 | 47 | func deleteUserHandler(c *gin.Context, client *clientv3.Client) (interface{}, error) { 48 | name := c.Param("name") 49 | _, err := client.UserDelete(newEtcdCtx(), name) 50 | return nil, err 51 | } 52 | 53 | type setUserPasswordRequest struct { 54 | Password string `json:"password"` 55 | } 56 | 57 | func setUserPasswordHandler(c *gin.Context, client *clientv3.Client) (interface{}, error) { 58 | r := new(setUserPasswordRequest) 59 | err := parseBody(c, r) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | name := c.Param("name") 65 | _, err = client.UserChangePassword(newEtcdCtx(), name, r.Password) 66 | return nil, err 67 | } 68 | 69 | func grantUserRoleHandler(c *gin.Context, client *clientv3.Client) (interface{}, error) { 70 | name := c.Param("name") 71 | role := c.Param("role") 72 | 73 | _, err := client.UserGrantRole(newEtcdCtx(), name, role) 74 | return nil, err 75 | } 76 | 77 | func revokeUserRoleHandler(c *gin.Context, client *clientv3.Client) (interface{}, error) { 78 | name := c.Param("name") 79 | role := c.Param("role") 80 | 81 | _, err := client.UserRevokeRole(newEtcdCtx(), name, role) 82 | return nil, err 83 | } 84 | -------------------------------------------------------------------------------- /routers/utils.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/gin-gonic/gin" 6 | "golang.org/x/net/context" 7 | "io/ioutil" 8 | "time" 9 | ) 10 | 11 | const ( 12 | ETCD_CLIENT_TIMEOUT = 3 * time.Second 13 | ) 14 | 15 | func parseBody(c *gin.Context, t interface{}) error { 16 | defer c.Request.Body.Close() 17 | body, err := ioutil.ReadAll(c.Request.Body) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | return json.Unmarshal(body, t) 23 | } 24 | 25 | func newEtcdCtx() context.Context { 26 | ctx, _ := context.WithTimeout(context.Background(), ETCD_CLIENT_TIMEOUT) 27 | return ctx 28 | } 29 | -------------------------------------------------------------------------------- /static/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | *.log -------------------------------------------------------------------------------- /static/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "babel-core": "^6.14.0", 4 | "babel-loader": "^6.2.5", 5 | "babel-plugin-antd": "^0.5.1", 6 | "babel-preset-es2015": "^6.14.0", 7 | "babel-preset-react": "^6.11.1", 8 | "css-loader": "^0.25.0", 9 | "html-webpack-plugin": "^2.22.0", 10 | "style-loader": "^0.13.1", 11 | "webpack": "^1.13.2" 12 | }, 13 | "dependencies": { 14 | "antd": "^1.11.0", 15 | "react": "15.1.0", 16 | "react-dom": "15.1.0", 17 | "react-polymer-layout": "^0.2.17", 18 | "react-router": "^2.8.1", 19 | "xhr": "^2.2.2" 20 | }, 21 | "scripts": { 22 | "build": "./node_modules/webpack/bin/webpack.js", 23 | "watch": "npm run build -- --watch", 24 | "publish": "NODE_ENV=production npm run build -- --optimize-minimize" 25 | } 26 | } -------------------------------------------------------------------------------- /static/src/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Menu, MenuItemGroup, Icon } from 'antd' 3 | import { Box } from 'react-polymer-layout' 4 | 5 | const App = React.createClass({ 6 | getInitialState() { 7 | return { menu: "" } 8 | }, 9 | 10 | _getMenu() { 11 | let parts = window.location.hash.split("/") 12 | let menu = "kv" 13 | if (parts.length > 1) { 14 | // cut ?_k=hash 15 | menu = parts[1].split("?")[0] 16 | } 17 | return menu 18 | }, 19 | 20 | _changeMenu() { 21 | this.setState({ menu: this._getMenu() }) 22 | }, 23 | 24 | componentDidMount() { 25 | this._changeMenu() 26 | }, 27 | 28 | componentWillReceiveProps(nextProps) { 29 | if (this.state.menu !== this._getMenu()) { 30 | this._changeMenu() 31 | } 32 | }, 33 | 34 | handleClick(e) { 35 | window.location.hash = "#" + e.key 36 | }, 37 | 38 | render() { 39 | return ( 40 | 41 | 42 | 43 | { window.location.hash = "#/" } } 44 | style={{ 45 | fontSize: 25, fontWeight: 700, marginRight: 20, paddingRight: 20, 46 | borderStyle: "solid", borderWidth: "0px 2px 0px 0px", borderColor: "#ddd", 47 | cursor: "pointer" 48 | }}> 49 | E·3·W 50 | 51 | 56 | 57 | KEY / VALUE 58 | 59 | 60 | MEMBERS 61 | 62 | AUTH}> 63 | ROLES 64 | USERS 65 | 66 | 67 | SETTING 68 | 69 | 70 | 71 |
72 | {this.props.children} 73 |
74 |
75 |
76 | ); 77 | }, 78 | }) 79 | 80 | module.exports = App -------------------------------------------------------------------------------- /static/src/components/AuthPanel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Box } from 'react-polymer-layout' 3 | import { Input, Button } from 'antd' 4 | import { CommonPanel } from './utils' 5 | 6 | const AuthItem = React.createClass({ 7 | render() { 8 | let item = this.props.item 9 | let bColor = item.selected ? "#95ccf5" : "#c3c3c3" 10 | return ( 11 | 15 |
20 | {item.name || ""} 21 |
22 | ) 23 | } 24 | }) 25 | 26 | const AuthCreate = React.createClass({ 27 | _clean() { 28 | this.setState({ name: "" }) 29 | }, 30 | 31 | componentDidMount() { 32 | this._clean() 33 | }, 34 | 35 | componentWillReceiveProps(nextProps) { 36 | this._clean() 37 | }, 38 | 39 | getInitialState() { 40 | return { name: "" } 41 | }, 42 | 43 | render() { 44 | return ( 45 | 46 |
47 | this.setState({ name: e.target.value })} /> 48 |
49 | 50 |
51 | 52 |
53 |
54 |
55 | ) 56 | } 57 | }) 58 | 59 | const AuthPanel = React.createClass({ 60 | _prepareItems(props) { 61 | let rawItems = props.items || [] 62 | let items = [] 63 | rawItems.forEach(i => { items.push({ name: i, selected: false }) }) 64 | this.setState({ items: items }) 65 | }, 66 | 67 | _selectItem(name) { 68 | let items = this.state.items 69 | let unset = false 70 | items.forEach(i => { 71 | if (i.name === name) { 72 | if (i.selected) { 73 | unset = true 74 | } 75 | i.selected = !i.selected 76 | } 77 | else { 78 | i.selected = false 79 | } 80 | }) 81 | this.setState({ items: items, selectedItem: unset ? "" : name }) 82 | }, 83 | 84 | _createItem(name) { 85 | this.props.create(name) 86 | }, 87 | 88 | _deleteItem() { 89 | this.props.delete(this.state.selectedItem) 90 | this.setState({ selectedItem: "" }) 91 | }, 92 | 93 | componentDidMount() { 94 | this._prepareItems(this.props) 95 | }, 96 | 97 | componentWillReceiveProps(nextProps) { 98 | this._prepareItems(nextProps) 99 | }, 100 | 101 | getInitialState() { 102 | return { items: [], selectedItem: "" } 103 | }, 104 | 105 | render() { 106 | let title = this.props.title || "" 107 | let panelHint = "CREATE " + title 108 | let sidePanel = null 109 | let withDelete = false 110 | if (this.state.selectedItem) { 111 | if (this.props.setting) { 112 | sidePanel = this.props.setting(this.state.selectedItem) 113 | panelHint = "SETTING" 114 | withDelete = true 115 | } 116 | } else { 117 | sidePanel = 118 | } 119 | return ( 120 | 121 |
126 | {this.props.title || ""} 127 |
128 | 129 | 133 | { 134 | this.state.items.map( 135 | i => ( this._selectItem(i.name)} item={i} />) 136 | ) 137 | } 138 | 139 | 140 | {sidePanel} 141 | 142 | 143 |
144 | ) 145 | } 146 | }) 147 | 148 | module.exports = AuthPanel -------------------------------------------------------------------------------- /static/src/components/KeyValue.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Box } from 'react-polymer-layout' 3 | import { Breadcrumb } from 'antd' 4 | import { KVList } from './request' 5 | import KeyValueCreate from './KeyValueCreate' 6 | import KeyValueItem from './KeyValueItem' 7 | import KeyValueSetting from './KeyValueSetting' 8 | import { CommonPanel } from './utils' 9 | 10 | const KeyValue = React.createClass({ 11 | // states: 12 | // - dir: the full path of current dir, eg. / or /abc/def 13 | // - menus: components of Breadcrumb, including path (to another dir, using in url hash) and name 14 | // - list: the key under the dir, get from api 15 | 16 | _isRoot() { 17 | return this.state.dir === "/" 18 | }, 19 | 20 | _parseList(list) { 21 | list = list || [] 22 | // sorted dir and normal kv 23 | list.sort((l1, l2) => { return l1.is_dir === l2.is_dir ? l1.key > l2.key : l1.is_dir ? -1 : 1 }) 24 | // trim prefix of dir, get the relative path, +1 for / 25 | let prefixLen = this.state.dir.length + (this._isRoot() ? 0 : 1) 26 | list.forEach(l => { 27 | l.key = l.key.slice(prefixLen) 28 | }) 29 | this.setState({ list: list }) 30 | }, 31 | 32 | // dir should be / or /abc/def 33 | _ParseDir(dir) { 34 | let menus = [{ path: "/", name: "ROOT" }] 35 | if (dir !== "/") { 36 | let keys = dir.split("/") 37 | for (let i = 1; i < keys.length; i++) { 38 | // get the full path of every component 39 | menus.push({ path: keys.slice(0, i + 1).join("/"), name: keys[i] }) 40 | } 41 | } 42 | KVList(dir, this._parseList) 43 | return { dir: dir, menus: menus } 44 | }, 45 | 46 | // list current dir and using KeyValueSetting 47 | _fetch(dir) { 48 | this.setState(this._ParseDir(dir)) 49 | this.setState({ setting: false }) 50 | }, 51 | 52 | // change url 53 | _redirect(dir) { 54 | window.location.hash = "#kv" + dir 55 | }, 56 | 57 | _fullKey(subKey) { 58 | return (this._isRoot() ? "/" : this.state.dir + "/") + subKey 59 | }, 60 | 61 | // callback for clicking KeyValueItem to enter a new dir 62 | _enter(subKey) { 63 | this._redirect(this._fullKey(subKey)) 64 | }, 65 | 66 | // callback for clicking KeyValueItem to set the kv 67 | _set(subKey) { 68 | let list = this.state.list 69 | list.forEach(l => { 70 | if (l.key === subKey) { l.selected = true } else { l.selected = false } 71 | }) 72 | this.setState({ setting: true, currentKey: this._fullKey(subKey), list: list }) 73 | }, 74 | 75 | // call back for clicking KeyValueItem again 76 | _unset(subKey) { 77 | let list = this.state.list 78 | list.forEach(l => { 79 | l.selected = false 80 | }) 81 | this.setState({ setting: false, list: list }) 82 | }, 83 | 84 | // callback for deleting a key in KeyValueItem 85 | _delete() { 86 | this._fetch(this.state.dir) 87 | }, 88 | 89 | // callback for creating kv or dir 90 | _update() { 91 | this._fetch(this.state.dir) 92 | }, 93 | 94 | // callback for delete currentDir and enter previous dir 95 | _back() { 96 | let menus = this.state.menus 97 | let targetPath = (menus[menus.length - 2] || menus[0]).path 98 | this._redirect(targetPath) 99 | }, 100 | 101 | // refresh the page with new path in url 102 | _refresh(props) { 103 | this._fetch("/" + (props.params.splat || "")) 104 | }, 105 | 106 | componentDidMount() { 107 | this._refresh(this.props) 108 | }, 109 | 110 | componentWillReceiveProps(nextProps) { 111 | if (this.props.params.splat !== nextProps.params.splat) { 112 | this._refresh(nextProps) 113 | } 114 | }, 115 | 116 | getInitialState() { 117 | return { dir: "", menus: [], list: [], setting: false, currentKey: "" } 118 | }, 119 | 120 | render() { 121 | let currentKey = this.state.currentKey 122 | return ( 123 | 124 | 125 | 126 | { 127 | this.state.menus.map( 128 | m => ( this._redirect(m.path) }>{m.name}) 129 | ) 130 | } 131 | 132 | 133 | 134 | 135 | 136 | { 137 | this.state.list.map( 138 | l => () 139 | ) 140 | } 141 | 142 | 143 | 144 | {this.state.setting ? 145 | () : 146 | () } 147 | 148 | 149 | ) 150 | } 151 | }) 152 | 153 | module.exports = KeyValue -------------------------------------------------------------------------------- /static/src/components/KeyValueCreate.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Input, Button } from 'antd' 3 | import { Box } from 'react-polymer-layout' 4 | import { KVPost, KVDelete } from './request' 5 | import { DeleteButton } from './utils' 6 | 7 | const KeyValueCreate = React.createClass({ 8 | _createDone(result) { 9 | this.setState({ key: "", value: "" }) 10 | this.props.update() 11 | }, 12 | 13 | _createKey(e) { 14 | KVPost(this.props.fullKey(this.state.key), this.state.value, this._createDone) 15 | }, 16 | 17 | _createDir(e) { 18 | KVPost(this.props.fullKey(this.state.key) + "?dir", null, this._createDone) 19 | }, 20 | 21 | _deleteDone(result) { 22 | this.props.back() 23 | }, 24 | 25 | _deleteDir() { 26 | KVDelete(this.state.dir, this._deleteDone) 27 | }, 28 | 29 | getInitialState() { 30 | return { dir: "", key: "", value: "" } 31 | }, 32 | 33 | _updateDir(props) { 34 | this.setState({ dir: props.dir, key: "", value: "" }) 35 | }, 36 | 37 | componentDidMount() { 38 | this._updateDir(this.props) 39 | }, 40 | 41 | componentWillReceiveProps(nextProps) { 42 | if (this.props.dir !== nextProps.dir) { 43 | this._updateDir(nextProps) 44 | } 45 | }, 46 | 47 | render() { 48 | let cantClick = this.state.key === "" 49 | return ( 50 | 51 | 52 | this.setState({ key: e.target.value }) } /> 53 | 54 |
55 | this.setState({ value: e.target.value }) } /> 56 |
57 | 58 | { 59 | 60 |
61 |
62 |
63 | } 64 | { 65 | this.state.dir === "/" ? null : 66 | (
) 67 | } 68 |
69 |
70 | ) 71 | } 72 | }) 73 | 74 | module.exports = KeyValueCreate -------------------------------------------------------------------------------- /static/src/components/KeyValueItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Box } from 'react-polymer-layout' 3 | import { Icon } from 'antd' 4 | 5 | const KeyValueItem = React.createClass({ 6 | _enter() { 7 | let info = this.props.info 8 | if (info.is_dir) { 9 | this.props.enter(info.key) 10 | } else if (info.selected) { 11 | this.props.unset(info.key) 12 | } else { 13 | this.props.set(info.key) 14 | } 15 | }, 16 | 17 | render() { 18 | let info = this.props.info 19 | let icon = info.is_dir ? : () 20 | let bColor = info.is_dir ? "#c3c3c3" : info.selected ? "#95ccf5" : "#ddd" 21 | return ( 22 | 26 | {icon} 31 | {info.key} 32 | 33 | ) 34 | } 35 | }) 36 | 37 | module.exports = KeyValueItem -------------------------------------------------------------------------------- /static/src/components/KeyValueSetting.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Input, Button } from 'antd' 3 | import { Box } from 'react-polymer-layout' 4 | import { message } from 'antd' 5 | import { KVGet, KVPut, KVDelete } from './request' 6 | import { DeleteButton } from './utils' 7 | 8 | const KeyValueSetting = React.createClass({ 9 | _getDone(result) { 10 | this.setState({ value: result.value }) 11 | }, 12 | 13 | _get(key) { 14 | KVGet(key || this.props.currentKey, this._getDone) 15 | }, 16 | 17 | _updateDone(result) { 18 | message.info("update successfully.") 19 | }, 20 | 21 | _update() { 22 | KVPut(this.props.currentKey, this.state.value, this._updateDone) 23 | }, 24 | 25 | _deleteDone(result) { 26 | this.props.delete() 27 | }, 28 | 29 | _delete() { 30 | KVDelete(this.props.currentKey, this._deleteDone) 31 | }, 32 | 33 | getInitialState() { 34 | return { value: "" } 35 | }, 36 | 37 | _fetch(key) { 38 | this.setState({ value: "" }) 39 | this._get(key) 40 | }, 41 | 42 | componentDidMount() { 43 | this._fetch() 44 | }, 45 | 46 | componentWillReceiveProps(nextProps) { 47 | if (this.props.currentKey !== nextProps.currentKey) { 48 | this._fetch(nextProps.currentKey) 49 | } 50 | }, 51 | 52 | render() { 53 | return ( 54 | 55 |
56 | this.setState({ value: e.target.value }) } /> 57 |
58 | 59 |
60 |
61 | 62 |
63 |
64 |
65 | ) 66 | } 67 | }) 68 | 69 | module.exports = KeyValueSetting -------------------------------------------------------------------------------- /static/src/components/Members.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Box } from 'react-polymer-layout' 3 | import { MembersGet } from './request' 4 | import { Icon } from 'antd' 5 | 6 | const Member = React.createClass({ 7 | render() { 8 | let member = this.props.member 9 | let size = 100 10 | let healthy = member.status === "healthy" 11 | let mainColor = healthy ? "#60be29" : "#ff6100" 12 | let targetStyle = { fontWeight: 700 } 13 | let roleColor = member.role === "leader" ? "#8867d2" : "#ccc" 14 | return ( 15 | 16 | 22 | {member.name} 23 | 24 | 29 | 30 | 31 | 32 |
Status
33 |
34 | { 35 | healthy ? 36 |
: 37 |
38 | } 39 |
40 |
41 | 42 |
DB Size
43 | { member.db_size} Bytes 44 |
45 |
46 | 47 |
PeerURLs
48 |
{member.peerURLs}
49 |
50 |
51 | 52 | {member.role.toUpperCase() } 53 | 54 |
55 |
56 | ) 57 | } 58 | }) 59 | 60 | const Members = React.createClass({ 61 | _getDone(result) { 62 | this.setState({ members: result }) 63 | }, 64 | 65 | _get() { 66 | MembersGet(this._getDone) 67 | }, 68 | 69 | getInitialState() { 70 | return { members: [] } 71 | }, 72 | 73 | componentDidMount() { 74 | this._get() 75 | }, 76 | 77 | componentWillReceiveProps(nextProps) { 78 | this._get() 79 | }, 80 | 81 | render() { 82 | return ( 83 | 84 | 85 | { 86 | this.state.members.map(m => { 87 | return 88 | }) 89 | } 90 | 91 | 92 | ) 93 | } 94 | }) 95 | 96 | module.exports = Members -------------------------------------------------------------------------------- /static/src/components/Roles.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import AuthPanel from './AuthPanel' 3 | import { RolesAll, RolesPost, RolesDelete } from './request' 4 | import RolesSetting from './RolesSetting' 5 | 6 | const Roles = React.createClass({ 7 | _getRolesDone(result) { 8 | this.setState({ roles: result || [] }) 9 | }, 10 | 11 | _getRoles() { 12 | RolesAll(this._getRolesDone) 13 | }, 14 | 15 | _createRoleDone(result) { 16 | this._getRoles() 17 | }, 18 | 19 | _createRole(name) { 20 | RolesPost(name, this._createRoleDone) 21 | }, 22 | 23 | _deleteRoleDone(result) { 24 | this._getRoles() 25 | }, 26 | 27 | _deleteRole(name) { 28 | RolesDelete(name, this._deleteRoleDone) 29 | }, 30 | 31 | componentDidMount() { 32 | this._getRoles() 33 | }, 34 | 35 | componentWillReceiveProps(nextProps) { 36 | this._getRoles() 37 | }, 38 | 39 | getInitialState() { 40 | return { roles: [] } 41 | }, 42 | 43 | _setting(name) { 44 | return 45 | }, 46 | 47 | render() { 48 | return ( 49 | 50 | ) 51 | } 52 | }) 53 | 54 | module.exports = Roles -------------------------------------------------------------------------------- /static/src/components/RolesSetting.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Box } from 'react-polymer-layout' 3 | import { RolesGet, RolesAddPerm, RolesDeletePerm } from './request' 4 | import { Radio, Input, Button, Tooltip, Icon } from 'antd' 5 | 6 | const RadioButton = Radio.Button 7 | const RadioGroup = Radio.Group 8 | const PermTypes = ["READWRITE", "READ", "WRITE"] 9 | const KeyTypes = ["RANGE", "PREFIX"] 10 | 11 | const PermItem = React.createClass({ 12 | render() { 13 | let perm = this.props.perm 14 | let typeColor = "#ccc" 15 | switch (perm.perm_type) { 16 | case "READWRITE": 17 | typeColor = "#f60" 18 | break 19 | case "READ": 20 | typeColor = "#5fbc29" 21 | break 22 | case "WRITE": 23 | typeColor = "#01b3ca" 24 | break 25 | } 26 | return ( 27 | 28 | 33 | 34 | {perm.perm_type} 35 | 36 | 37 | 38 | {perm.key} 39 | 40 | 41 | 42 | 43 | {perm.range_end} 44 | 45 | 46 | 47 | 50 | 51 | ) 52 | } 53 | }) 54 | 55 | const RolesSetting = React.createClass({ 56 | _getRoleDone(result) { 57 | this.setState({ perms: result || [] }) 58 | }, 59 | 60 | _getRole(props) { 61 | if (props.name) { 62 | RolesGet(props.name, this._getRoleDone) 63 | } 64 | }, 65 | 66 | _selectPermType(e) { 67 | this.setState({ permType: e.target.value }) 68 | }, 69 | 70 | _selectKeyType(e) { 71 | this.setState({ keyType: e.target.value }) 72 | }, 73 | 74 | _addPermDone(result) { 75 | this._getRole(this.props) 76 | }, 77 | 78 | _addPerm() { 79 | let state = this.state 80 | RolesAddPerm(this.props.name, state.permType, state.key, state.rangeEnd, state.keyType === "PREFIX", this._addPermDone) 81 | }, 82 | 83 | _deletePermDone() { 84 | this._getRole(this.props) 85 | }, 86 | 87 | _deletePerm(p) { 88 | RolesDeletePerm(this.props.name, p.key, p.range_end, this._deletePermDone) 89 | }, 90 | 91 | componentDidMount() { 92 | this._getRole(this.props) 93 | }, 94 | 95 | componentWillReceiveProps(nextProps) { 96 | if (nextProps.name !== this.props.name) { 97 | this._getRole(nextProps) 98 | } 99 | }, 100 | 101 | getInitialState() { 102 | return { name: "", perms: [], permType: PermTypes[0], keyType: KeyTypes[0], key: "", rangeEnd: "" } 103 | }, 104 | 105 | render() { 106 | let radioStyle = { padding: "5px 0px 5px 0px" } 107 | let typeStyle = { width: 120, paddingLeft: 3 } 108 | let cantClick = this.state.key === "" 109 | return ( 110 | 111 | 112 | {this.state.perms.map(p => { 113 | return
{ this._deletePerm(p) } }/>
114 | }) } 115 |
116 | 117 | 118 | 119 | Perm Type 120 | 121 | {PermTypes.map(t => { return ({t}) }) } 122 | 123 | 124 | 125 |
Key Type
126 | 127 | {KeyTypes.map(t => { return ({t}) }) } 128 | 129 |
130 | 131 | Key 132 | this.setState({ key: e.target.value }) } /> 133 | 134 | 135 | RangeEnd 136 | this.setState({ rangeEnd: e.target.value }) } /> 137 | 138 | 139 | 142 | 143 |
144 |
145 |
146 | ) 147 | } 148 | }) 149 | 150 | module.exports = RolesSetting -------------------------------------------------------------------------------- /static/src/components/Setting.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Box } from 'react-polymer-layout' 3 | import { Input } from 'antd' 4 | 5 | const Setting = React.createClass({ 6 | _loadSetting() { 7 | this.setState({ username: localStorage.etcdUsername, password: localStorage.etcdPassword }) 8 | }, 9 | 10 | _saveUsername(e) { 11 | let username = e.target.value 12 | this.setState({ username: username }) 13 | localStorage.etcdUsername = username 14 | }, 15 | 16 | _savePassword(e) { 17 | let password = e.target.value 18 | this.setState({ password: password }) 19 | localStorage.etcdPassword = password 20 | }, 21 | 22 | componentDidMount() { 23 | this._loadSetting() 24 | }, 25 | 26 | componentWillReceiveProps(nextProps) { 27 | this._loadSetting() 28 | }, 29 | 30 | getInitialState() { 31 | return { username: "", password: "" } 32 | }, 33 | 34 | render() { 35 | let inputStyle = { margin: "10px 0px 10px" } 36 | let settingItemStyle = { width: 100, color: "#939393", fontSize: 14, fontWeight: 700 } 37 | return ( 38 | 39 | 40 |

Setting

41 |

Setting username and password for accessing etcd by Web UI.Everything is saved to localStorage.

42 | 43 | 44 | USERNAME 45 | 46 | 47 | 48 | 49 | 50 | PASSWORD 51 | 52 | 53 | 54 |
55 |
56 | ) 57 | } 58 | }) 59 | 60 | module.exports = Setting -------------------------------------------------------------------------------- /static/src/components/Users.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import AuthPanel from './AuthPanel' 3 | import { UsersAll, UsersPost, UsersDelete } from './request' 4 | import UsersSetting from './UsersSetting' 5 | 6 | const Users = React.createClass({ 7 | _getUsersDone(result) { 8 | this.setState({ users: result || [] }) 9 | }, 10 | 11 | _getUsers() { 12 | UsersAll(this._getUsersDone) 13 | }, 14 | 15 | _createUserDone(result) { 16 | this._getUsers() 17 | }, 18 | 19 | _createUser(name) { 20 | UsersPost(name, this._createUserDone) 21 | }, 22 | 23 | _deleteUserDone(result) { 24 | this._getUsers() 25 | }, 26 | 27 | _deleteUser(name) { 28 | UsersDelete(name, this._deleteUserDone) 29 | }, 30 | 31 | componentDidMount() { 32 | this._getUsers() 33 | }, 34 | 35 | componentWillReceiveProps(nextProps) { 36 | this._getUsers() 37 | }, 38 | 39 | getInitialState() { 40 | return { users: [] } 41 | }, 42 | 43 | _setting(name) { 44 | return 45 | }, 46 | 47 | render() { 48 | return ( 49 | 50 | ) 51 | } 52 | }) 53 | 54 | module.exports = Users -------------------------------------------------------------------------------- /static/src/components/UsersSetting.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Box } from 'react-polymer-layout' 3 | import { UsersGet, User, UsersGrantRole, UsersRovokeRole, UsersChangePassword, RolesAll } from './request' 4 | import { Tag, Select, Button, Input } from 'antd' 5 | 6 | const Option = Select.Option 7 | const roleColors = ["blue", "green", "yellow", "red"] 8 | 9 | const RoleItem = React.createClass({ 10 | render() { 11 | let color = roleColors[(this.props.index || 0) % roleColors.length] 12 | return ( 13 | 14 | 15 | {this.props.name} 16 | 17 | 18 | ) 19 | } 20 | }) 21 | 22 | const UsersSetting = React.createClass({ 23 | _getUserDone(result) { 24 | this.setState({ roles: result || [] }) 25 | }, 26 | 27 | _getUser(props) { 28 | if (props.name) { 29 | UsersGet(props.name, this._getUserDone) 30 | } 31 | }, 32 | 33 | _getAllRolesDone(result) { 34 | this.setState({ allRoles: result || [] }) 35 | }, 36 | 37 | _getAllRoles() { 38 | RolesAll(this._getAllRolesDone) 39 | }, 40 | 41 | _enter(props) { 42 | this._getUser(props) 43 | this._getAllRoles() 44 | this.setState({ selectedRole: "", password: "" }) 45 | }, 46 | 47 | _refresh() { 48 | this._getUser(this.props) 49 | }, 50 | 51 | _revokeRoleDone() { 52 | _refresh() 53 | }, 54 | 55 | _revokeRole(role) { 56 | if (this.props.name && role) { 57 | UsersRovokeRole(this.props.name, role, this._revokeRoleDone) 58 | } 59 | }, 60 | 61 | _grantRoleDone(result) { 62 | this._refresh() 63 | }, 64 | 65 | _grantRole() { 66 | if (this.props.name && this.state.selectedRole) { 67 | UsersGrantRole(this.props.name, this.state.selectedRole, this._grantRoleDone) 68 | } 69 | }, 70 | 71 | _selectRole(value) { 72 | this.setState({ selectedRole: value }) 73 | }, 74 | 75 | _changePassword() { 76 | UsersChangePassword(this.props.name, this.state.password, () => { }) 77 | }, 78 | 79 | componentDidMount() { 80 | this._enter(this.props) 81 | }, 82 | 83 | componentWillReceiveProps(nextProps) { 84 | if (nextProps.name !== this.props.name) { 85 | this._enter(nextProps) 86 | } 87 | }, 88 | 89 | getInitialState() { 90 | return { roles: [], allRoles: [], selectedRole: "", password: "" } 91 | }, 92 | 93 | render() { 94 | let boxStyle = { padding: 10, fontSize: 16, fontWeight: 700 } 95 | let moduleStyle = Object.assign({}, boxStyle, { borderTop: "1px solid #ddd" }) 96 | return ( 97 | 98 | 99 | ROLES 100 | 101 | { 102 | this.state.roles.map((r, index) => { 103 | return this._revokeRole(r) } index={index}/> 104 | }) 105 | } 106 | 107 | 108 | 109 | GRANT 110 | 111 | 118 | 121 | 122 | 123 | 124 | CHANGE PASSWORD 125 | 126 | { this.setState({ password: e.target.value }) } 128 | } /> 129 | 132 | 133 | 134 | 135 | ) 136 | } 137 | }) 138 | 139 | module.exports = UsersSetting -------------------------------------------------------------------------------- /static/src/components/request.jsx: -------------------------------------------------------------------------------- 1 | import xhr from 'xhr' 2 | import { message } from 'antd' 3 | 4 | function handler(callback) { 5 | return function (err, response) { 6 | if (err) { 7 | message.error(err); 8 | } else { 9 | if (response && response.body) { 10 | let resp = JSON.parse(response.body) 11 | if (resp.err) { 12 | message.error(resp.err) 13 | } else if (callback) { 14 | callback(resp.result) 15 | } 16 | } 17 | } 18 | } 19 | } 20 | 21 | function withAuth(options) { 22 | return Object.assign( 23 | options || {}, 24 | { 25 | "headers": { 26 | "X-Etcd-Username": localStorage.etcdUsername, 27 | "X-Etcd-Password": localStorage.etcdPassword 28 | } 29 | } 30 | ) 31 | } 32 | 33 | function KVList(path, callback) { 34 | xhr.get("kv" + path + "?list", withAuth(), handler(callback)) 35 | } 36 | 37 | function KVGet(path, callback) { 38 | xhr.get("kv" + path, withAuth(), handler(callback)) 39 | } 40 | 41 | function KVPost(path, value, callback) { 42 | let bodyStr = JSON.stringify({ value: value }) 43 | xhr.post("kv" + path, withAuth({ body: bodyStr }), handler(callback)) 44 | } 45 | 46 | function KVPut(path, value, callback) { 47 | let bodyStr = JSON.stringify({ value: value }) 48 | xhr.put("kv" + path, withAuth({ body: bodyStr }), handler(callback)) 49 | } 50 | 51 | function KVDelete(path, callback) { 52 | xhr.del("kv" + path, withAuth(), handler(callback)) 53 | } 54 | 55 | function MembersGet(callback) { 56 | xhr.get("members", withAuth(), handler(callback)) 57 | } 58 | 59 | function RolesAll(callback) { 60 | xhr.get("roles", withAuth(), handler(callback)) 61 | } 62 | 63 | function RolesPost(name, callback) { 64 | let bodyStr = JSON.stringify({ name: name }) 65 | xhr.post("role", withAuth({ body: bodyStr }), handler(callback)) 66 | } 67 | 68 | function RolesGet(name, callback) { 69 | xhr.get("role/" + encodeURIComponent(name), withAuth(), handler(callback)) 70 | } 71 | 72 | function RolesDelete(name, callback) { 73 | xhr.del("role/" + encodeURIComponent(name), withAuth(), handler(callback)) 74 | } 75 | 76 | function RolesAddPerm(name, permType, key, rangeEnd, prefix, callback) { 77 | let bodyStr = JSON.stringify({ perm_type: permType, key: key, range_end: rangeEnd }) 78 | xhr.post("role/" + encodeURIComponent(name) + "/permission" + (prefix ? "?prefix" : ""), withAuth({ body: bodyStr }), handler(callback)) 79 | } 80 | 81 | function RolesDeletePerm(name, key, rangeEnd, callback) { 82 | let bodyStr = JSON.stringify({ key: key, range_end: rangeEnd }) 83 | xhr.del("role/" + encodeURIComponent(name) + "/permission", withAuth({ body: bodyStr }), handler(callback)) 84 | } 85 | 86 | function UsersAll(callback) { 87 | xhr.get("users", withAuth(), handler(callback)) 88 | } 89 | 90 | function UsersPost(name, callback) { 91 | let bodyStr = JSON.stringify({ name: name }) 92 | xhr.post("user", withAuth({ body: bodyStr }), handler(callback)) 93 | } 94 | 95 | function UsersGet(name, callback) { 96 | xhr.get("user/" + encodeURIComponent(name), withAuth(), handler(callback)) 97 | } 98 | 99 | function UsersDelete(name, callback) { 100 | xhr.del("user/" + encodeURIComponent(name), withAuth(), handler(callback)) 101 | } 102 | 103 | function UsersGrantRole(name, role, callback) { 104 | xhr.put("user/" + encodeURIComponent(name) + "/role/" + encodeURIComponent(role), withAuth(), handler(callback)) 105 | } 106 | 107 | function UsersRovokeRole(name, role, callback) { 108 | xhr.del("user/" + encodeURIComponent(name) + "/role/" + encodeURIComponent(role), withAuth(), handler(callback)) 109 | } 110 | 111 | function UsersChangePassword(name, password, callback) { 112 | let bodyStr = JSON.stringify({ password: password }) 113 | xhr.put("user/" + encodeURIComponent(name) + "/password", withAuth({ body: bodyStr }), handler(callback)) 114 | } 115 | 116 | module.exports = { 117 | KVList, KVPut, KVDelete, KVGet, KVPost, 118 | MembersGet, 119 | RolesAll, RolesPost, RolesGet, RolesDelete, RolesAddPerm, RolesDeletePerm, 120 | UsersAll, UsersPost, UsersGet, UsersDelete, UsersGrantRole, UsersRovokeRole, UsersChangePassword 121 | } -------------------------------------------------------------------------------- /static/src/components/utils.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Popconfirm, Button } from 'antd' 3 | import { Box } from 'react-polymer-layout' 4 | 5 | const DeleteButton = React.createClass({ 6 | render() { 7 | return ( 8 | 9 | 10 | 11 | ) 12 | } 13 | }) 14 | 15 | const CommonPanel = React.createClass({ 16 | render() { 17 | let bColor = this.props.color ? this.props.color : "#ddd" 18 | return ( 19 | 20 | 21 |
22 | 23 | 24 | {this.props.hint || ""} 25 | 26 | {this.props.withDelete ? : null} 27 | 28 | {this.props.children} 29 |
30 |
31 | ) 32 | } 33 | }) 34 | 35 | module.exports = { DeleteButton, CommonPanel } -------------------------------------------------------------------------------- /static/src/css/patch.css: -------------------------------------------------------------------------------- 1 | .ant-breadcrumb { 2 | color: #939393; 3 | font-size: 18px; 4 | } 5 | 6 | .ant-btn-ghost:focus, .ant-btn-ghost:hover{ 7 | color: red; 8 | border-color: red; 9 | } 10 | 11 | .kv-create-button{ 12 | padding: 15px 15px 12px 0px; 13 | } 14 | 15 | .ant-tag{ 16 | min-width: 80px; 17 | height: 30px; 18 | line-height: 26px; 19 | font-size: 16px; 20 | } -------------------------------------------------------------------------------- /static/src/entry.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { hashHistory, Router, Route, IndexRedirect } from 'react-router' 4 | import App from './components/App' 5 | import KeyValue from './components/KeyValue' 6 | import Members from './components/Members' 7 | import Roles from './components/Roles' 8 | import Users from './components/Users' 9 | import Setting from "./components/Setting" 10 | import 'antd/dist/antd.min.css' 11 | import './css/patch.css' 12 | 13 | ReactDOM.render(( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ), document.querySelector(".root")) -------------------------------------------------------------------------------- /static/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ETCD V3 WEB UI 4 |
5 | -------------------------------------------------------------------------------- /static/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin') 3 | var webpack = require('webpack') 4 | 5 | var plugins = [ 6 | new HtmlWebpackPlugin({ 7 | filename: 'index.html', 8 | template: './src/index.html', 9 | inject: false, 10 | }) 11 | ] 12 | 13 | process.env.NODE_ENV === 'production' ? plugins.push(new webpack.DefinePlugin({ 14 | "process.env": { 15 | NODE_ENV: JSON.stringify("production") 16 | } 17 | })) : null 18 | 19 | module.exports = { 20 | devtool: process.env.NODE_ENV === 'production' ? '' : 'inline-source-map', 21 | entry: './src/entry.jsx', 22 | output: { 23 | path: path.join(__dirname, '/dist'), 24 | filename: 'bundle.js' 25 | }, 26 | resolve: { 27 | extensions: ['', '.js', '.jsx'] 28 | }, 29 | module: { 30 | loaders: [{ 31 | test: /.jsx$/, 32 | loader: 'babel', 33 | exclude: /node_modules/, 34 | query: { 35 | presets: ['react', 'es2015'], 36 | plugins: ['antd'] 37 | } 38 | }, { 39 | test: /\.css$/, 40 | loader: 'style-loader!css-loader' 41 | }] 42 | }, 43 | plugins: plugins 44 | } --------------------------------------------------------------------------------