├── conf
├── .gitignore
├── config.default.ini
└── config.go
├── static
├── .gitignore
├── src
│ ├── index.html
│ ├── css
│ │ └── patch.css
│ ├── entry.jsx
│ └── components
│ │ ├── Roles.jsx
│ │ ├── Users.jsx
│ │ ├── KeyValueItem.jsx
│ │ ├── utils.jsx
│ │ ├── KeyValueSetting.jsx
│ │ ├── Setting.jsx
│ │ ├── KeyValueCreate.jsx
│ │ ├── App.jsx
│ │ ├── Members.jsx
│ │ ├── request.jsx
│ │ ├── UsersSetting.jsx
│ │ ├── AuthPanel.jsx
│ │ ├── KeyValue.jsx
│ │ └── RolesSetting.jsx
├── package.json
└── webpack.config.js
├── images
├── kv.png
├── members.png
├── roles.png
├── setting.png
└── users.png
├── .dockerignore
├── routers
├── errors.go
├── resp.go
├── utils.go
├── member.go
├── key.go
├── users.go
├── roles.go
└── routers.go
├── docker-compose.yml
├── .gitignore
├── Dockerfile
├── .github
└── workflows
│ └── build_image.yaml
├── LICENSE
├── main.go
├── e3ch
└── e3ch.go
├── go.mod
├── README.md
└── go.sum
/conf/.gitignore:
--------------------------------------------------------------------------------
1 | *.ini
2 | !config.default.ini
--------------------------------------------------------------------------------
/static/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 | node_modules/
3 | *.log
--------------------------------------------------------------------------------
/images/kv.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soyking/e3w/HEAD/images/kv.png
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | images
2 | static/dist
3 | static/node_modules
4 | Dockerfile
5 |
--------------------------------------------------------------------------------
/images/members.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soyking/e3w/HEAD/images/members.png
--------------------------------------------------------------------------------
/images/roles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soyking/e3w/HEAD/images/roles.png
--------------------------------------------------------------------------------
/images/setting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soyking/e3w/HEAD/images/setting.png
--------------------------------------------------------------------------------
/images/users.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soyking/e3w/HEAD/images/users.png
--------------------------------------------------------------------------------
/static/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
ETCD V3 WEB UI
4 |
5 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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"))
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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/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/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | 
24 |
25 | MEMBERS
26 |
27 | 
28 |
29 | ROLES
30 |
31 | 
32 |
33 | USERS
34 |
35 | 
36 |
37 | SETTING
38 |
39 | 
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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
70 |
71 |
72 | {this.props.children}
73 |
74 |
75 |
76 | );
77 | },
78 | })
79 |
80 | module.exports = App
--------------------------------------------------------------------------------
/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/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/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/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/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------