├── .gitignore ├── .idea ├── .gitignore ├── etcd-manage-server.iml ├── misc.xml ├── modules.xml ├── vcs.xml └── watcherTasks.xml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── bin └── config │ └── cfg.toml ├── data ├── README.md └── etcd-manage.sql ├── docker ├── cluster-ssl │ ├── docker-compose.yml │ └── tlskey │ │ ├── ca.pem │ │ ├── server.key │ │ └── server.pem └── default │ ├── docker-compose.yml │ └── mysqld.cnf ├── etcd-manage-ui ├── .babelrc ├── .editorconfig ├── .postcssrc.js ├── LICENSE ├── Makefile ├── README.md ├── build │ ├── build.js │ ├── check-versions.js │ ├── logo.png │ ├── utils.js │ ├── vue-loader.conf.js │ ├── webpack.base.conf.js │ ├── webpack.dev.conf.js │ └── webpack.prod.conf.js ├── config │ ├── dev.env.js │ ├── index.js │ └── prod.env.js ├── go.mod ├── go.sum ├── index.html ├── package-lock.json ├── package.json ├── src │ ├── App.vue │ ├── api │ │ ├── api.js │ │ ├── kv.js │ │ ├── logs.js │ │ ├── passport.js │ │ ├── role.js │ │ ├── server.js │ │ └── user.js │ ├── assets │ │ ├── css │ │ │ └── base.css │ │ ├── favicon.png │ │ ├── img │ │ │ ├── narrow-hover.png │ │ │ ├── narrow.png │ │ │ ├── unfold-hover.png │ │ │ └── unfold.png │ │ └── imgs │ │ │ ├── file.png │ │ │ └── folder.png │ ├── common │ │ ├── CloudContainer.vue │ │ ├── CloudHeader.vue │ │ └── CloudSideBar.vue │ ├── components │ │ ├── KvCard.vue │ │ ├── KvGrid.vue │ │ ├── KvList.vue │ │ └── MonacoEditor.vue │ ├── config │ │ └── index.js │ ├── i18n │ │ ├── en-US.js │ │ └── zh-CN.js │ ├── main.js │ ├── page │ │ ├── CloudHome.vue │ │ ├── EtcdServers.vue │ │ ├── KV.vue │ │ ├── Logs.vue │ │ ├── Members.vue │ │ ├── Role.vue │ │ ├── User.vue │ │ └── bus.js │ └── router │ │ └── index.js ├── static │ ├── .gitkeep │ ├── etcdui.png │ └── favicon.ico └── tpls │ ├── compile.sh │ └── tpls.go ├── etcdsdk ├── etcdsdk.go ├── main │ └── main.go └── model │ ├── config.go │ ├── error.go │ ├── interface.go │ └── model.go ├── go.mod ├── go.sum ├── logs └── access.log ├── main.go ├── program ├── api │ ├── api.go │ └── v1 │ │ ├── keys │ │ └── keys.go │ │ ├── passport │ │ └── passport.go │ │ ├── router.go │ │ ├── server │ │ └── server.go │ │ ├── setings │ │ ├── role │ │ │ └── role.go │ │ └── user │ │ │ └── user.go │ │ └── upload │ │ └── upload.go ├── cache │ ├── cache.go │ └── default.go ├── common │ └── common.go ├── config │ └── config.go ├── http_ui.go ├── logger │ └── logger.go ├── middleware.go ├── models │ ├── client.go │ ├── etcdServers.go │ ├── roleEtcdServers.go │ ├── roles.go │ ├── time.go │ └── users.go ├── program.go └── publicFunc │ └── publicFunc.go └── tupian ├── etcd key树以及value展示图.png ├── etcd-key树图.png ├── etcd管理页面.png ├── 多个etcd集群健康图.png └── 添加etcd界面.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | default.etcd 15 | bin/ems* 16 | bin/logs/* 17 | bin/etcd* 18 | /ca.pem 19 | /cert.pem 20 | /key.pem 21 | 22 | etcd-manage-ui/dist/ 23 | etcd-manage-ui/node_modules/ 24 | etcd-manage-ui/*.log 25 | etcd-manage-ui/.DS_Store 26 | 27 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /.idea/etcd-manage-server.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/watcherTasks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | 28 | 29 | 40 | 52 | 53 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | # 解决go 时区和https请求证书错误问题 3 | RUN apk update \ 4 | && apk add ca-certificates \ 5 | && update-ca-certificates \ 6 | && apk add tzdata 7 | COPY ["./bin", "/app/"] 8 | EXPOSE 10280 9 | WORKDIR /app 10 | CMD ["./ems"] 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 etcd-manage 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: 2 | @echo 'Usage of make: [ build | linux_build | windows_build | docker_build | docker_run | clean ]' 3 | 4 | build: 5 | @go build -o ./bin/ems ./ 6 | 7 | linux_build: 8 | @CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./bin/ems ./ 9 | 10 | windows_build: 11 | @CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o ./bin/ems.exe ./ 12 | 13 | docker_build: linux_build 14 | docker build -t shiguanghuxian/etcd-manage . 15 | 16 | docker_run: docker_build 17 | docker-compose up --force-recreate 18 | 19 | run: build 20 | @./bin/ems 21 | 22 | install: build 23 | @mv ./bin/ems $(GOPATH)/bin/ems 24 | 25 | clean: 26 | @rm -f ./bin/ems* 27 | @rm -f ./bin/logs/* 28 | 29 | .PHONY: default build linux_build windows_build docker_build docker_run clean -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # k8s Etcd manage Server 2 | 3 | 4 | ## 功能介绍 5 | etcd-manage-server 是一个用go编写的etcd可视化管理工具,具有友好的界面,管理key就像管理本地文件一样方便。支持简单权限管理区分只读和读写权限。 6 | 7 | **备注** 8 | 9 | 1. 本项目适配k8s-etcd的乱码问题 10 | 2. 将sql文件导入到mysql数据库(sql文件路径 ./data/etcd-manage.sql),默认用户 admin/111111 11 | 3. 当前只实现了etcd v3 api管理 12 | 4. 在使用时可直接修改默认的两个etcd连接地址为真实可用地址即可开始体验。 13 | 14 | ## 使用 15 | ``` 16 | # 1. sql数据导入到mysql数据库 17 | 18 | # 2. 打包、运行 19 | go mod tidy 20 | go mod download 21 | go build . 22 | 23 | ./etcd-manage-server 24 | ``` 25 | ## 效果演示 26 | 27 | etcd管理页面 28 | ![](https://github.com/qixiang-liu/k8s-etcd-ui/blob/master/tupian/etcd%E7%AE%A1%E7%90%86%E9%A1%B5%E9%9D%A2.png) 29 | 30 | 多个etcd集群健康图 31 | ![](https://github.com/qixiang-liu/k8s-etcd-ui/blob/master/tupian/%E5%A4%9A%E4%B8%AAetcd%E9%9B%86%E7%BE%A4%E5%81%A5%E5%BA%B7%E5%9B%BE.png) 32 | 33 | key树以及value展示图 34 | ![](https://github.com/qixiang-liu/k8s-etcd-ui/blob/master/tupian/etcd%20%20key%E6%A0%91%E4%BB%A5%E5%8F%8Avalue%E5%B1%95%E7%A4%BA%E5%9B%BE.png) 35 | 36 | 添加etcd界面 37 | ![](https://github.com/qixiang-liu/k8s-etcd-ui/blob/master/tupian/%E6%B7%BB%E5%8A%A0etcd%E7%95%8C%E9%9D%A2.png) 38 | 39 | etcd-key树图 40 | ![](https://github.com/qixiang-liu/k8s-etcd-ui/blob/master/tupian/etcd-key%E6%A0%91%E5%9B%BE.png) 41 | 42 | 43 | -------------------------------------------------------------------------------- /bin/config/cfg.toml: -------------------------------------------------------------------------------- 1 | # debug模式 2 | debug = false 3 | # 日志文件路径 4 | log_path = "" 5 | 6 | # http 监听端口 7 | [http] 8 | # 监听地址 9 | address = "127.0.0.1" 10 | # 监听端口 11 | port = 10280 12 | 13 | # 使用 Let's Encrypt 证书 - tls_enable为true优先使用本地证书模式 14 | tls_encrypt_enable = false 15 | # 域名列表 16 | tls_encrypt_domain_names = ["your-domain.com"] 17 | 18 | # 是否启用tls 19 | tls_enable = false 20 | # tls证书文件 21 | [http.tls_config] 22 | cert_file = "cert_file" 23 | key_file = "key_file" 24 | 25 | 26 | ## mysql 配置 27 | [db] 28 | ## 是否调试模式 29 | debug = true 30 | ## 数据库连接地址 - make docker_run 不可以是127.0.0.1 31 | address = "127.0.0.1" 32 | ## 数据库端口 33 | port = 3306 34 | ## 连接池最大连接数 35 | max_idle_conns = 64 36 | ## 默认打开连接数 37 | max_open_conns = 24 38 | ## 数据库用户名 39 | user = "root" 40 | ## 数据库密码 41 | passwd = "123456" 42 | ## 数据库名 43 | db_name = "etcd_servers" 44 | -------------------------------------------------------------------------------- /data/README.md: -------------------------------------------------------------------------------- 1 | 2 | # mysql相关命令 3 | 4 | ## mysql数据备份语句 5 | ``` 6 | mysqldump -u root -p123456 -h127.0.0.1 -B --add-drop-database --column-statistics=0 etcd_servers > etcd-manage.sql 7 | ``` 8 | ## mysql导入语句 9 | ``` 10 | mysql -uroot -p123456 -h127.0.0.1 < etcd-manage.sql 11 | ``` 12 | 13 | -------------------------------------------------------------------------------- /data/etcd-manage.sql: -------------------------------------------------------------------------------- 1 | -- MySQL dump 10.13 Distrib 8.0.17, for osx10.14 (x86_64) 2 | -- 3 | -- Host: 127.0.0.1 Database: etcd_servers 4 | -- ------------------------------------------------------ 5 | -- Server version 5.7.30 6 | 7 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 8 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 9 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 10 | /*!50503 SET NAMES utf8mb4 */; 11 | /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; 12 | /*!40103 SET TIME_ZONE='+00:00' */; 13 | /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; 14 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 15 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 16 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 17 | 18 | -- 19 | -- Current Database: `etcd_servers` 20 | -- 21 | 22 | /*!40000 DROP DATABASE IF EXISTS `etcd_servers`*/; 23 | 24 | CREATE DATABASE /*!32312 IF NOT EXISTS*/ `etcd_servers` /*!40100 DEFAULT CHARACTER SET latin1 */; 25 | 26 | USE `etcd_servers`; 27 | 28 | -- 29 | -- Table structure for table `etcd_servers` 30 | -- 31 | 32 | DROP TABLE IF EXISTS `etcd_servers`; 33 | /*!40101 SET @saved_cs_client = @@character_set_client */; 34 | /*!50503 SET character_set_client = utf8mb4 */; 35 | CREATE TABLE `etcd_servers` ( 36 | `id` int(11) NOT NULL AUTO_INCREMENT, 37 | `version` varchar(3) NOT NULL DEFAULT 'v3' COMMENT 'etcd版本', 38 | `name` varchar(30) NOT NULL DEFAULT '' COMMENT 'etcd服务名字', 39 | `address` varchar(600) NOT NULL COMMENT 'etcd地址列表', 40 | `prefix` varchar(100) NOT NULL DEFAULT '' COMMENT 'key前缀,建议不为空,防止大量key', 41 | `tls_enable` varchar(5) NOT NULL DEFAULT 'true' COMMENT '是否启用tls连接', 42 | `cert_file` text NOT NULL COMMENT '证书', 43 | `key_file` text NOT NULL COMMENT '证书', 44 | `ca_file` text NOT NULL COMMENT '证书', 45 | `username` varchar(60) NOT NULL DEFAULT '' COMMENT '用户名', 46 | `password` varchar(60) NOT NULL DEFAULT '' COMMENT '密码', 47 | `desc` varchar(300) NOT NULL COMMENT '描述信息', 48 | `created_at` datetime DEFAULT NULL COMMENT '添加时间', 49 | `updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 50 | PRIMARY KEY (`id`) USING BTREE 51 | ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COMMENT='etched server列表'; 52 | /*!40101 SET character_set_client = @saved_cs_client */; 53 | 54 | -- 55 | -- Dumping data for table `etcd_servers` 56 | -- 57 | 58 | LOCK TABLES `etcd_servers` WRITE; 59 | /*!40000 ALTER TABLE `etcd_servers` DISABLE KEYS */; 60 | INSERT INTO `etcd_servers` VALUES (8,'v3','lqx','https://10.20.3.102:2379','','true','LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURJRENDQWdpZ0F3SUJBZ0lJQ1dRWENPM1A3QUF3RFFZSktvWklodmNOQVFFTEJRQXdFakVRTUE0R0ExVUUKQXhNSFpYUmpaQzFqWVRBZUZ3MHlNREE0TURRd01qVXlNelZhRncweU1UQTRNRFF3TWpVeU16VmFNQmd4RmpBVQpCZ05WQkFNVERXeHhlQzF0WVhOMFpYSXRNREV3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUURoNWJqUDNXN2NzVlhEaHdKWHlrWUR6RjRLaldrYXFQc0xNNmVFRGhYZWJXMGVaZi9RT3QrcUFxaWgKbTdPZ3BISFp6VFhFODVVSHV5QkFTM3daQU0wZDRqODY1bHFwaTJuTU41Vm5GTkNMOEpmdlRiTVdocFpleS9DVApPc1J3amhLQ0JHc2kvbHE5SEt0WnJtdmd0TjRNVlhyanB4cE5Td1BaeDFjNENBUXJPOFk1MkFpRlU5cFJoNmtHCmZ1TC82NlFGaDlpTnN2eExOUnFmVFpLY3oxUWxrZEdyWUdqVXE1blVrV3MwQnM3NmpmU3hQRFBydG1XN1lhdnEKbHo5RjVQSzZaSTY3Mm84MWNzOTVQR0VKV2JRTWpBaHRnckpKMGdoenVoTU15YlRYQWFmUCtQZkUxMEdQWlRMUgpvWit5c1U2dWI2eHNwVm1NZzR0b1lxMWhVWW4zQWdNQkFBR2pkREJ5TUE0R0ExVWREd0VCL3dRRUF3SUZvREFkCkJnTlZIU1VFRmpBVUJnZ3JCZ0VGQlFjREFRWUlLd1lCQlFVSEF3SXdRUVlEVlIwUkJEb3dPSUlOYkhGNExXMWgKYzNSbGNpMHdNWUlKYkc5allXeG9iM04waHdRS0ZBTm1od1IvQUFBQmh4QUFBQUFBQUFBQUFBQUFBQUFBQUFBQgpNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUFOODNxZFJ2eWl1ZVZkd09ZOWozVG0raFVxdFN5anV3eWI3aGJSCnR2UDNJb3RLbm50ZzNpRndYZVpNQkxTWUhvWU9jVXJ6ZndCUzZHOUgrdjBkRFh0Vi8vR3ljKzQ4QUVLbjhjZU8KT2xIRnJJQkRUMkE5VVBZTUhkVmNMcEZjOHJvdGhPQmJjd3RGSFB6aStqUlI5dE9oYjhQY0l0WFhTL29nOXVNMQpqaE5zU282VHlhYlA0U3BVSUhVNXpkbEMxaHBVTXk4b2V0SnI0d1c3SEpvK00xd2pjWmNNTU5VTEE4UEllb2xICndtR1dFa1g2OFZVQzhGU0FDK1ZyeUlLM20vTWVjQkEwMFFseGtBZmprbWt2b2NqbHhvVTJ1ZzVLTHYyWUthb0YKeC9mVjFuOUNuTFlpdmU3VWI5T2pGTmxjazJZOHpMNjRhTXlnVzZvOWRWTzBDcFVNCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K','LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBNGVXNHo5MXUzTEZWdzRjQ1Y4cEdBOHhlQ28xcEdxajdDek9uaEE0VjNtMXRIbVgvCjBEcmZxZ0tvb1p1em9LUngyYzAxeFBPVkI3c2dRRXQ4R1FETkhlSS9PdVphcVl0cHpEZVZaeFRRaS9DWDcwMnoKRm9hV1hzdndrenJFY0k0U2dnUnJJdjVhdlJ5cldhNXI0TFRlREZWNjQ2Y2FUVXNEMmNkWE9BZ0VLenZHT2RnSQpoVlBhVVllcEJuN2kvK3VrQllmWWpiTDhTelVhbjAyU25NOVVKWkhScTJCbzFLdVoxSkZyTkFiTytvMzBzVHd6CjY3Wmx1MkdyNnBjL1JlVHl1bVNPdTlxUE5YTFBlVHhoQ1ZtMERJd0liWUt5U2RJSWM3b1RETW0wMXdHbnovajMKeE5kQmoyVXkwYUdmc3JGT3JtK3NiS1ZaaklPTGFHS3RZVkdKOXdJREFRQUJBb0lCQUFuQzg0bUgrQkp4VjFOcgpzaDQ1RDIxNmwxVzlacDFRVUFqYjRwRkNTbytpQ3VVVlkwaU1RcjRGLzJOOFp2YTZKSEZVL00zVitNcXN1MmdMCjJ3RDVsK09DczFqSU80SzRFNHBQbkpVbndSdEsra1hOQmNBamNMd0g5QTFvckxSd2J6eFBGSkllaTYxQVgrY3cKTWxremQ2VHd6SzZwdWVrOUpKMTBqOEJNR0dJRnpuSnlBMm5EYjU4WFJCM2UySnFRQlFEQ1BaM1BJaHE3UlJrNQp3Q1ExMG9jbXdKbUQwTGkydUlqRW5JOTU4ZHRzVVdBSmI5Z1FoNTVIeFphSEU2R05pTkRQdlpxZCtyc1ZJSGRsClFqR2MvQTY0NVpvSnNIQWczeGVyWnArdytvT0pmc253SEJkQ1ZpZlFTR0lWa1g0a243QXVzZFZrSDFWN1JqYjEKT2NFWWd3RUNnWUVBNzM3YnpVT3RDRWUvNnNsaHhMYm9JYXVLQXk3bDFDdk16MWNYMXJDaDV2Z2V3bW4yd3RzRAowZE9xSWdGcXZ1MVIvTTltVkpsZzNYeTRRc01oc2Vabk9BRTJIeGZIcFByVkoyOGJlMm1paWdZQjI3ZTBnY0FpCnhKb3h2L2pSazlZRTZzZnJYN3lOU2pIQWJBWGszWjh0Y1BnU0pxRktDUUp3YmgyR2QrcTdBZWtDZ1lFQThYYjMKWE9kY05aczhteEFDZjhqMUZjNTlRZGNqOGJlQ1YrU0ViM09YMkhxMlZobjJkbzltbFcyaTA2ejFuVTU3NVdqUQpHUW43NDNpUjYzb3pqSmZLNU5EKzJidWRSMnZOeVo2VkxzUHlySlZIUUxHSDBhT1pvL3RMdTZoQStCazJZWm83ClZOWFBReHFIMVF1UG1UNWFIbHpXMVBrWnpZQndFWVVnSHpKbjROOENnWUI3QnczZW1mOVBHNXlJL2I1TmtUQVoKNjhiUDc4MThlcXVSYjBuOXJXcmQvV08vdHpOZDBhaGFwNExrU1JvT3psYXBxZGtGYUcwTUdqK0ZmRHZZNldUbwpyaWFoUGxQK2VpUDVSK2cwTTRXZHBZeGExRG5UMVdHRzRUYmhTTzVRSlVjTlhIbWJDbjhDT0NDQzNWdytSTURSCklYNGhmZ1ZNTDRhVjZuRGpOUit2MFFLQmdRQ3BGTGdEK3hJTGk0ZDF3VkV1cjhaR25kQUNBYWR1eENSbWJXTDcKTkFNNUdEeURzQ3h3T3R3SGVMMFM0a01mQXUwbzZDc0h6WUR2ZU9jYzcvWVcxZGZDUUVLa3JvWmtrNjJIS0IrbwpucGRZbUROTHJzUy9YSUxpVzc1ZFNtVXNGV09LRnRqQy8vRGhPVHV5U0NVbWxvMitReDVBQmFvMngyQXlOSGtZCnYxQVVHd0tCZ0JNMklPWDIzc3VSd0o1UHU1eGowQzFkVzh1QzFaYjEzeWVoZm93ZjdueFFxWmhtRHk4cGlOUmUKUnFKQlFhaFpnYTdKWi9uZWtKZTh5S3hueGhtQWdJSy9hTmxJMkQ3NThEL1d5NENQZVluQ2hKS0VZTzh5bHFwMgovVk5JeXpIOHNLNWs1M1E0UzNmdThlNlVyM29KcndFQWtkVmZSeGFsbzU5V1dwWjY2bkp0Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==','LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN3akNDQWFxZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFTTVJBd0RnWURWUVFERXdkbGRHTmsKTFdOaE1CNFhEVEl3TURnd05EQXlOVEl6TlZvWERUTXdNRGd3TWpBeU5USXpOVm93RWpFUU1BNEdBMVVFQXhNSApaWFJqWkMxallUQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQU5sN0J6YzZzcWFNCjJvbnAwOVMxTndVK0xKRkFFeXdLN2lqUHV4TlRSV2VQSzFwajF5TmxiUVlDV1pHZUExMUFQRVFUdEVzc0c1dlUKeWJTNHUydzFkM3Q2WEpwb2FpZ3pOVkVPaWVlUkgxOE9lb2pVNWdWM2hZTlNUTVByQUIrZXJ0YVcxNkMzQmhTRgprVmFrc2I5UHIxNU0ramE3WUpCQnhnL2hNOEtIbzU2NVl3Z0hJUXoxM253R3JqMUVPc2x0YkVRN0xValFRdU9wCkpmYU82WElCZWVhUlFJWm9yZjZLTVdNY29ScG9nbFFwcXRXWVh1cTUwOUNWd3NqTWNmZDEwV3dQUHE0aWt3RE0KYVFYS0JXTWhwczR4azY4SWcrVVNLRGkxZXdzOThrOFo5UlQ5b001Z3FBVVQ1RzRyTGpWNzZSZDQrR2xiblFpdwpkUHFxdzdMMkwrOENBd0VBQWFNak1DRXdEZ1lEVlIwUEFRSC9CQVFEQWdLa01BOEdBMVVkRXdFQi93UUZNQU1CCkFmOHdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBSDdkRXNiSWZWN3RLNTlEZjBIRFNLd1ZLS3dkWGlzbTlqTkYKYUI2SG5ST1RvaE5lb2NNYXFWRlo5N0RWZytlTzh0UUJVdFhrVlg1N08vVzY2dFMrckNIcW04cVJYNzJFMVRzRQpmQzI2QW9tdXdEbnBHOGxvUXdLV2x4L2YrY1BEdkVhZ0MrLytDZVI3dmZrc09TbktGbVVhS0l4M0MxSWVYT1RKCk1jOWpVQ1Z0N05xbzBjY3dDOUplMkVqd3lnYkVXUEVOZnczMGRtZ1B4UTNKdXJRSmhLZlhFU21lZ0hOTUw4R08KQnFHY3YzY1A5TWQrbzhJdUVpby9BUkdCWjE2QXFrSmozTDY2TGc1bjBlVldLVWZpOE9hd3BvL2F3Q2JuaXhCMgoyQ3lZRkFiNHR0VkVZT3dpT2FjTkRiaXQvc0xuejFMcHpkT2xlQy96WjZTd2c4eWVBbE09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K','','','','2020-08-05 11:18:41','2020-08-05 11:18:41'),(9,'v3','local','http://127.0.0.1:2379','','false','','','','','','','2020-08-06 15:07:59','2020-08-06 15:16:35'); 61 | /*!40000 ALTER TABLE `etcd_servers` ENABLE KEYS */; 62 | UNLOCK TABLES; 63 | 64 | -- 65 | -- Table structure for table `role_etcd_servers` 66 | -- 67 | 68 | DROP TABLE IF EXISTS `role_etcd_servers`; 69 | /*!40101 SET @saved_cs_client = @@character_set_client */; 70 | /*!50503 SET character_set_client = utf8mb4 */; 71 | CREATE TABLE `role_etcd_servers` ( 72 | `id` int(11) NOT NULL AUTO_INCREMENT, 73 | `etcd_server_id` int(11) NOT NULL DEFAULT '0' COMMENT 'etcd服务id', 74 | `role_id` int(11) NOT NULL COMMENT '角色id', 75 | `type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0读 1写 -1无任何权限', 76 | `created_at` datetime NOT NULL COMMENT '添加时间', 77 | `updated_at` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 78 | PRIMARY KEY (`id`), 79 | KEY `idx_role_id` (`role_id`), 80 | KEY `idx_etcd_server_id` (`etcd_server_id`) 81 | ) ENGINE=InnoDB AUTO_INCREMENT=39 DEFAULT CHARSET=utf8mb4 COMMENT='角色权限表'; 82 | /*!40101 SET character_set_client = @saved_cs_client */; 83 | 84 | -- 85 | -- Dumping data for table `role_etcd_servers` 86 | -- 87 | 88 | LOCK TABLES `role_etcd_servers` WRITE; 89 | /*!40000 ALTER TABLE `role_etcd_servers` DISABLE KEYS */; 90 | INSERT INTO `role_etcd_servers` VALUES (37,8,1,1,'2020-08-05 11:18:41','2020-08-05 11:18:41'),(38,9,1,1,'2020-08-06 15:07:59','2020-08-06 15:07:59'); 91 | /*!40000 ALTER TABLE `role_etcd_servers` ENABLE KEYS */; 92 | UNLOCK TABLES; 93 | 94 | -- 95 | -- Table structure for table `roles` 96 | -- 97 | 98 | DROP TABLE IF EXISTS `roles`; 99 | /*!40101 SET @saved_cs_client = @@character_set_client */; 100 | /*!50503 SET character_set_client = utf8mb4 */; 101 | CREATE TABLE `roles` ( 102 | `id` int(11) NOT NULL AUTO_INCREMENT, 103 | `name` varchar(60) NOT NULL DEFAULT '' COMMENT '角色名', 104 | `created_at` datetime NOT NULL COMMENT '添加时间', 105 | `updated_at` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 106 | PRIMARY KEY (`id`) USING BTREE 107 | ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='角色表'; 108 | /*!40101 SET character_set_client = @saved_cs_client */; 109 | 110 | -- 111 | -- Dumping data for table `roles` 112 | -- 113 | 114 | LOCK TABLES `roles` WRITE; 115 | /*!40000 ALTER TABLE `roles` DISABLE KEYS */; 116 | INSERT INTO `roles` VALUES (1,'高级管理员','2019-08-15 03:43:44','2020-07-10 18:18:37'),(2,'开发只读','2019-08-18 04:14:42','2019-08-18 04:32:02'),(3,'开发管理','2019-08-18 04:25:05','2019-08-18 04:32:21'); 117 | /*!40000 ALTER TABLE `roles` ENABLE KEYS */; 118 | UNLOCK TABLES; 119 | 120 | -- 121 | -- Table structure for table `users` 122 | -- 123 | 124 | DROP TABLE IF EXISTS `users`; 125 | /*!40101 SET @saved_cs_client = @@character_set_client */; 126 | /*!50503 SET character_set_client = utf8mb4 */; 127 | CREATE TABLE `users` ( 128 | `id` int(11) NOT NULL AUTO_INCREMENT, 129 | `username` varchar(60) NOT NULL DEFAULT '' COMMENT '用户名', 130 | `password` char(32) NOT NULL DEFAULT '' COMMENT '密码', 131 | `email` varchar(300) NOT NULL DEFAULT '' COMMENT '邮箱', 132 | `role_id` int(11) NOT NULL DEFAULT '0' COMMENT '角色id', 133 | `created_at` datetime NOT NULL COMMENT '添加时间', 134 | `updated_at` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 135 | PRIMARY KEY (`id`) 136 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; 137 | /*!40101 SET character_set_client = @saved_cs_client */; 138 | 139 | -- 140 | -- Dumping data for table `users` 141 | -- 142 | 143 | LOCK TABLES `users` WRITE; 144 | /*!40000 ALTER TABLE `users` DISABLE KEYS */; 145 | INSERT INTO `users` VALUES (1,'admin','94d9484ada1e5639810258b515fa61f1','',1,'2019-08-12 20:19:15','2020-07-10 18:18:43'); 146 | /*!40000 ALTER TABLE `users` ENABLE KEYS */; 147 | UNLOCK TABLES; 148 | /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; 149 | 150 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 151 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 152 | /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; 153 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 154 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 155 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 156 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 157 | 158 | -- Dump completed on 2020-08-06 23:40:32 159 | -------------------------------------------------------------------------------- /docker/cluster-ssl/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | etcd0: 4 | image: quay.io/coreos/etcd:v3.3 5 | ports: 6 | - 2379:2379 7 | - 2380 8 | volumes: 9 | - ./tlskey:/etc/etcd/etcdSSL 10 | environment: 11 | - ETCD_NAME=etcd0 12 | - ETCD_CLIENT_CERT_AUTH=true 13 | - ETCD_TRUSTED_CA_FILE=/etc/etcd/etcdSSL/ca.pem 14 | - ETCD_CERT_FILE=/etc/etcd/etcdSSL/server.pem 15 | - ETCD_KEY_FILE=/etc/etcd/etcdSSL/server.key 16 | - ETCD_ADVERTISE_CLIENT_URLS=https://127.0.0.1:2379 # 此地址告诉客户端访问此服务url 17 | - ETCD_LISTEN_CLIENT_URLS=https://0.0.0.0:2379 18 | - ETCD_LISTEN_PEER_URLS=https://0.0.0.0:2380 19 | - ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster 20 | - ETCD_INITIAL_CLUSTER=etcd0=https://etcd0:2380,etcd1=https://etcd1:2380,etcd2=https://etcd2:2380 21 | - ETCD_INITIAL_CLUSTER_STATE=new 22 | - ETCD_INITIAL_ADVERTISE_PEER_URLS=https://etcd0:2380 23 | - ETCD_PEER_CERT_FILE=/etc/etcd/etcdSSL/server.pem 24 | - ETCD_PEER_KEY_FILE=/etc/etcd/etcdSSL/server.key 25 | - ETCD_PEER_TRUSTED_CA_FILE=/etc/etcd/etcdSSL/ca.pem 26 | 27 | etcd1: 28 | image: quay.io/coreos/etcd:v3.3 29 | ports: 30 | - 12379:2379 31 | - 2380 32 | volumes: 33 | - ./tlskey:/etc/etcd/etcdSSL 34 | environment: 35 | - ETCD_NAME=etcd1 36 | - ETCD_CLIENT_CERT_AUTH=true 37 | - ETCD_TRUSTED_CA_FILE=/etc/etcd/etcdSSL/ca.pem 38 | - ETCD_CERT_FILE=/etc/etcd/etcdSSL/server.pem 39 | - ETCD_KEY_FILE=/etc/etcd/etcdSSL/server.key 40 | - ETCD_ADVERTISE_CLIENT_URLS=https://127.0.0.1:12379 # 此地址告诉客户端访问此服务url 41 | - ETCD_LISTEN_CLIENT_URLS=https://0.0.0.0:2379 42 | - ETCD_LISTEN_PEER_URLS=https://0.0.0.0:2380 43 | - ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster 44 | - ETCD_INITIAL_CLUSTER=etcd0=https://etcd0:2380,etcd1=https://etcd1:2380,etcd2=https://etcd2:2380 45 | - ETCD_INITIAL_CLUSTER_STATE=new 46 | - ETCD_INITIAL_ADVERTISE_PEER_URLS=https://etcd1:2380 47 | - ETCD_PEER_CERT_FILE=/etc/etcd/etcdSSL/server.pem 48 | - ETCD_PEER_KEY_FILE=/etc/etcd/etcdSSL/server.key 49 | - ETCD_PEER_TRUSTED_CA_FILE=/etc/etcd/etcdSSL/ca.pem 50 | 51 | etcd2: 52 | image: quay.io/coreos/etcd:v3.3 53 | ports: 54 | - 22379:2379 55 | - 2380 56 | volumes: 57 | - ./tlskey:/etc/etcd/etcdSSL 58 | environment: 59 | - ETCD_NAME=etcd2 60 | - ETCD_CLIENT_CERT_AUTH=true 61 | - ETCD_TRUSTED_CA_FILE=/etc/etcd/etcdSSL/ca.pem 62 | - ETCD_CERT_FILE=/etc/etcd/etcdSSL/server.pem 63 | - ETCD_KEY_FILE=/etc/etcd/etcdSSL/server.key 64 | - ETCD_ADVERTISE_CLIENT_URLS=https://127.0.0.1:22379 # 此地址告诉客户端访问此服务url 65 | - ETCD_LISTEN_CLIENT_URLS=https://0.0.0.0:2379 66 | - ETCD_LISTEN_PEER_URLS=https://0.0.0.0:2380 67 | - ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster 68 | - ETCD_INITIAL_CLUSTER=etcd0=https://etcd0:2380,etcd1=https://etcd1:2380,etcd2=https://etcd2:2380 69 | - ETCD_INITIAL_CLUSTER_STATE=new 70 | - ETCD_INITIAL_ADVERTISE_PEER_URLS=https://etcd2:2380 71 | - ETCD_PEER_CERT_FILE=/etc/etcd/etcdSSL/server.pem 72 | - ETCD_PEER_KEY_FILE=/etc/etcd/etcdSSL/server.key 73 | - ETCD_PEER_TRUSTED_CA_FILE=/etc/etcd/etcdSSL/ca.pem 74 | -------------------------------------------------------------------------------- /docker/cluster-ssl/tlskey/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFrjCCA5agAwIBAgIUGRyBFWSb/XUQg/l4wyv23RrOLyowDQYJKoZIhvcNAQEN 3 | BQAwbzELMAkGA1UEBhMCQ04xEDAOBgNVBAgTB0JlaWppbmcxEDAOBgNVBAcTB0Jl 4 | aWppbmcxDTALBgNVBAoTBGV0Y2QxFjAUBgNVBAsTDWV0Y2QgU2VjdXJpdHkxFTAT 5 | BgNVBAMTDGV0Y2Qtcm9vdC1jYTAeFw0xOTAxMDcwODMxMDBaFw0yNDAxMDYwODMx 6 | MDBaMG8xCzAJBgNVBAYTAkNOMRAwDgYDVQQIEwdCZWlqaW5nMRAwDgYDVQQHEwdC 7 | ZWlqaW5nMQ0wCwYDVQQKEwRldGNkMRYwFAYDVQQLEw1ldGNkIFNlY3VyaXR5MRUw 8 | EwYDVQQDEwxldGNkLXJvb3QtY2EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK 9 | AoICAQD5FFYDkXRixCbQH7V7mFeRhq/NjvSTybS8pv17R9TnTafYkWrBmnfnjO6Q 10 | W/g6KU3AuWedG7WJQZvSd8wrUVmR2GQqdqKZlD1hmSXwf0Bxjb7+aZGyIsoTdRs2 11 | 3DelwIxWKnRxB3g2WL8c36TdT/Fx/lODd0k5QxMzUQ4bfwuq0aeJH/yzsQRm114E 12 | z/mEopwsPSLlSOK9UXRCqesPTigw9Q9Ic7oY3rwePJ4fI/zG+FbAAIv+KOALPiCi 13 | TneH9087lY1msTpFyewczQaUAhwpVEEVTMLNby1GrWfyeTe2fHTKW+4iP56Bv/Po 14 | VbKW017SO4UCYJy/sARXQr4TKHz8cZl1fdznjiy3pKxmQYRck8sZ4/PGj613ql0Y 15 | mhrPuEWHSwSoL2MB4/jhyV/ou+hw8q1TFCtewSD7u1Hr1AH7xEE0FIPCCsHAN1r9 16 | LRzLpf4esZEEI+ly83TZe1g27VX8i+SwrRJYEtYf7xjsESnpXEtCNdO17vFfJgc2 17 | KXnFH9UygrLRP8RhggiTElivT89zpD0tFEF89DIHM3e+nnw29sMoidAs9tlRF9cO 18 | YoQpEyYtwGdAebAGXXXfaO+hSuyQekpfWlhKhASIyXw98/169EIm/Zz8S87q8oxQ 19 | yAThKCeeMFgv8h4Xi2dzJzfwJBZ4nXpnos84V42B9jcwnRsdIQIDAQABo0IwQDAO 20 | BgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeUqZZev 21 | gwjmuuQtcA5zasrXnJIwDQYJKoZIhvcNAQENBQADggIBADyIvQHVDFnAfQMIK4YK 22 | oFmlIX0uzw9B2hPTg9dmoPHSohP+qW1Wqp6fuHdavXzGCL1GCbLNCWZhg67COWUe 23 | LPYznE0HE8qDDpsXReZli+u12e5LEZfWRW/U+dOzB4Y21u75GqrUDcdsVmFRl3it 24 | tyu8YIVMHYrM28ZtpJ2aJsRVVvCGC3WOBApZ2ayBRr4KSRXfrt0BisVMLFkDklSk 25 | 1P3FdawnCrIvCxK5AOJhTbs+iSjTOK1OvYUGvfkJSpuvyIGcJssl7O1u/7qTg/Cx 26 | 7EMTS3TymryndLRvMCJLQTPzVfnyihrfAeqEmLPILzxgfUJpTvAk1QcXvzy2RQjy 27 | +HIIxo+Lr+wjGSxULD6kMmM5mcLJOAOlHTaPsxabGAtVUNRvr18BZhNoyob0lUUg 28 | +khrRzPG7uRqbCWa3lCFUe2bRD3wfiIjBTgZiCA1X0GA87epCcyFjqiBCgoyHCpy 29 | lahIL+RFrDh6PukV7KxoGlRZ7cAOZGgBTVlKfqoSOvl4q319nPkTjEmirI2cp4qh 30 | rJjv/mChJGIwePeEwFw1coFWr2pIdlMnhndxbp0CnbmFyejK31ESpdCjKc/Ul5MW 31 | ChMqk4Y+8tWJCLUS7gmNCBSmPklXfY0H6B/RaMfpsq31xt4SmJtADHyxtDXuXvMK 32 | Tv9qqO+FUmzVK4MPJG4USAR9 33 | -----END CERTIFICATE----- 34 | -------------------------------------------------------------------------------- /docker/cluster-ssl/tlskey/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | 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 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /docker/cluster-ssl/tlskey/server.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 34 | LAV+fqGI6tQ8L3gwFoy9S11dIsSM9f1vvjU= 35 | -----END CERTIFICATE----- 36 | -------------------------------------------------------------------------------- /docker/default/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | etcd: 5 | image: "quay.azk8s.cn/coreos/etcd:v3.3" 6 | container_name: "db-etcd" 7 | environment: 8 | ETCD_ADVERTISE_CLIENT_URLS: "http://0.0.0.0:2379" 9 | ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2379" 10 | ETCDCTL_API: "3" 11 | volumes: 12 | - ./default.etcd:/default.etcd 13 | ports: 14 | - 2379:2379 15 | - 2380:2380 16 | - 4001:4001 17 | mysql: 18 | image: mysql:5.7 19 | container_name: db-mysql 20 | ports: 21 | - 3306:3306 22 | volumes: 23 | - ./mysql/data:/var/lib/mysql 24 | - ./mysql/conf/mysqld.cnf:/etc/mysql/mysql.conf.d/mysqld.cnf 25 | environment: 26 | MYSQL_ROOT_PASSWORD: "123456" 27 | -------------------------------------------------------------------------------- /docker/default/mysqld.cnf: -------------------------------------------------------------------------------- 1 | [mysqld_safe] 2 | socket = /var/run/mysqld/mysqld.sock 3 | nice = 0 4 | [mysqld] 5 | user = mysql 6 | pid-file = /var/run/mysqld/mysqld.pid 7 | socket = /var/run/mysqld/mysqld.sock 8 | port = 3306 9 | basedir = /usr 10 | datadir = /var/lib/mysql 11 | tmpdir = /tmp 12 | lc-messages-dir = /usr/share/mysql 13 | skip-external-locking 14 | bind-address = 0.0.0.0 15 | key_buffer_size = 16M 16 | max_allowed_packet = 16M 17 | thread_stack = 192K 18 | thread_cache_size = 8 19 | myisam-recover-options = BACKUP 20 | query_cache_limit = 1M 21 | query_cache_size = 16M 22 | log_error = /var/log/mysql/error.log 23 | expire_logs_days = 10 24 | max_binlog_size = 100M 25 | -------------------------------------------------------------------------------- /etcd-manage-ui/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"] 12 | } 13 | -------------------------------------------------------------------------------- /etcd-manage-ui/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /etcd-manage-ui/.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /etcd-manage-ui/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 etcd-manage 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 | -------------------------------------------------------------------------------- /etcd-manage-ui/Makefile: -------------------------------------------------------------------------------- 1 | default: 2 | @echo 'Usage of make: [ build | run | go | clean ]' 3 | 4 | build: 5 | npm run build 6 | 7 | run: 8 | npm run dev 9 | 10 | go: build 11 | @cp -r dist tpls && cd tpls && ./compile.sh 12 | 13 | clean: 14 | @rm -f ./dist 15 | @rm -f ./tpls/dist 16 | @rm -f ./tpls/tpls.go 17 | 18 | .PHONY: default build run go clean -------------------------------------------------------------------------------- /etcd-manage-ui/README.md: -------------------------------------------------------------------------------- 1 | # Etcd UI 2 | 3 | 4 | ## 功能介绍 5 | 6 | etcd-manage 是一个用go编写的etcd管理工具,具有友好的界面(类似阿里云后台),管理key就像管理本地文件一样方便。支持简单权限管理区分只读和读写权限。 7 | 8 | 开源地址: [https://github.com/etcd-manage](https://github.com/etcd-manage) 9 | 10 | **备注** 11 | 12 | 1. 用helm部署时注意修改mysql连接相关信息,可不修改使用默认安装直接使用 13 | 2. 将sql文件导入到mysql数据库,默认用户 admin/111111 [etcd-manage.sql](https://raw.githubusercontent.com/cloudnativeapp/charts/master/submitted/etcd-manage/sql/etcd-manage.sql) 14 | 3. 此程序为2.0版本,实现1.0功能 1.0项目地址 [https://github.com/shiguanghuxian/etcd-manage](https://github.com/shiguanghuxian/etcd-manage) 15 | 4. 下一步开发对中英双语言做全面支持,当前对中文支持友好。 16 | 5. 当前只实现了etcd v3 api管理key v2在路上。 17 | 6. 在使用时可直接修改默认的两个etcd连接地址为真实可用地址即可开始体验。 18 | 19 | 20 | ## HELM 安装使用 21 | 22 | 提示:安装后通过kubectl get pods看到两个pod专题都是Running表示服务可用,首次mysql需要初始化会慢一些,大概1分钟。 23 | 24 | ```shell 25 | helm install my-etcd-manage etcd-manage 26 | 或 27 | cd path/etcd-manage 28 | helm package . 29 | helm install my-etcd-manage etcd-manage-1.0.0.tgz 30 | 31 | ``` 32 | 33 | 运行后看到输出: 34 | 35 | ```shell 36 | NAME: my-etcd-manage 37 | LAST DEPLOYED: 2019-08-26 20:16:23.182943 +0800 CST m=+0.068774555 38 | NAMESPACE: default 39 | STATUS: deployed 40 | 41 | NOTES: 42 | 1. Get the application URL by running these commands: 43 | export POD_NAME=$(kubectl get pods -l "app=etcd-manage,release=my-etcd-manage" -o jsonpath="{.items[0].metadata.name}") 44 | echo "Visit http://127.0.0.1:10280 to use your application" 45 | kubectl port-forward $POD_NAME 10280:10280 46 | 47 | # kubectl get pods 48 | NAME READY STATUS RESTARTS AGE 49 | my-etcd-manage-f4bc496f5-bpg99 1/1 Running 2 25s 50 | my-etcd-manage-mysql-5577cd9b-4nqr2 1/1 Running 0 25s 51 | 52 | ``` 53 | 54 | 执行完 NOTES 中提示命令的命令即可在浏览器中访问 `http://127.0.0.1:10280/ui` 查看。注意url端口后边路径为/ui 55 | 56 | 默认用户 admin/111111 57 | 58 | 如果NOTES命令执行错误可执行 59 | 60 | ```shell 61 | kubectl port-forward my-etcd-manage-f4bc496f5-bpg99 10280:10280 // my-etcd-manage-f4bc496f5-bpg99 为 kubectl get pods 中获取的值 62 | ``` 63 | 64 | ## HELM 使用参数 65 | 66 | 使用数据库参数可使用自己mysql服务,默认使用依赖的charts中mysql服务,如果使用自己mysql请导入sql文件 [etcd-manage.sql](https://raw.githubusercontent.com/cloudnativeapp/charts/master/submitted/etcd-manage/sql/etcd-manage.sql) 67 | 68 | ```shell 69 | helm install my-etcd-manage etcd-manage --set database.address="你的数据库ip地址" --set database.port=3306 --set database.user="user" --set database.passwd="密码" --set database.db_name="etcd-manage" 70 | ``` 71 | 72 | **参数介绍** 73 | 74 | | 参数名 | 简述 | 示例 | 75 | | ----- | ----- | ---| 76 | | database.address | mysql数据库地址 | 192.168.1.88 | 77 | | database.port | mysql数据库端口 | 3306 | 78 | | database.user | mysql用户名 | root | 79 | | database.passwd | mysql用户密码 | z123456 | 80 | | database.db_name | 导入etcd-manage.sql的数据库 | etcd-manage | 81 | 82 | 83 | ## 效果演示 84 | 85 | etcd服务列表管理 86 | 87 | ![](https://raw.githubusercontent.com/cloudnativeapp/charts/master/submitted/etcd-manage/imgs/etcd-server.png) 88 | 89 | key 管理 90 | 91 | ![](https://raw.githubusercontent.com/cloudnativeapp/charts/master/submitted/etcd-manage/imgs/keys.png) 92 | 93 | key 编辑 94 | 95 | ![](https://raw.githubusercontent.com/cloudnativeapp/charts/master/submitted/etcd-manage/imgs/key-edit.png) 96 | 97 | key 查看 98 | 99 | ![](https://raw.githubusercontent.com/cloudnativeapp/charts/master/submitted/etcd-manage/imgs/key-show.png) 100 | 101 | 用户管理 102 | 103 | ![](https://raw.githubusercontent.com/cloudnativeapp/charts/master/submitted/etcd-manage/imgs/user.png) 104 | -------------------------------------------------------------------------------- /etcd-manage-ui/build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, (err, stats) => { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build. 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | if (stats.hasErrors()) { 31 | console.log(chalk.red(' Build failed with errors.\n')) 32 | process.exit(1) 33 | } 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /etcd-manage-ui/build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | const semver = require('semver') 4 | const packageConfig = require('../package.json') 5 | const shell = require('shelljs') 6 | 7 | function exec (cmd) { 8 | return require('child_process').execSync(cmd).toString().trim() 9 | } 10 | 11 | const versionRequirements = [ 12 | { 13 | name: 'node', 14 | currentVersion: semver.clean(process.version), 15 | versionRequirement: packageConfig.engines.node 16 | } 17 | ] 18 | 19 | if (shell.which('npm')) { 20 | versionRequirements.push({ 21 | name: 'npm', 22 | currentVersion: exec('npm --version'), 23 | versionRequirement: packageConfig.engines.npm 24 | }) 25 | } 26 | 27 | module.exports = function () { 28 | const warnings = [] 29 | 30 | for (let i = 0; i < versionRequirements.length; i++) { 31 | const mod = versionRequirements[i] 32 | 33 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 34 | warnings.push(mod.name + ': ' + 35 | chalk.red(mod.currentVersion) + ' should be ' + 36 | chalk.green(mod.versionRequirement) 37 | ) 38 | } 39 | } 40 | 41 | if (warnings.length) { 42 | console.log('') 43 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 44 | console.log() 45 | 46 | for (let i = 0; i < warnings.length; i++) { 47 | const warning = warnings[i] 48 | console.log(' ' + warning) 49 | } 50 | 51 | console.log() 52 | process.exit(1) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /etcd-manage-ui/build/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comqx/k8s-etcd-ui/e860824e994da8c48aea5cd3e9283f374828a8c9/etcd-manage-ui/build/logo.png -------------------------------------------------------------------------------- /etcd-manage-ui/build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const config = require('../config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | const packageConfig = require('../package.json') 6 | 7 | exports.assetsPath = function (_path) { 8 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 9 | ? config.build.assetsSubDirectory 10 | : config.dev.assetsSubDirectory 11 | 12 | return path.posix.join(assetsSubDirectory, _path) 13 | } 14 | 15 | exports.cssLoaders = function (options) { 16 | options = options || {} 17 | 18 | const cssLoader = { 19 | loader: 'css-loader', 20 | options: { 21 | sourceMap: options.sourceMap 22 | } 23 | } 24 | 25 | const postcssLoader = { 26 | loader: 'postcss-loader', 27 | options: { 28 | sourceMap: options.sourceMap 29 | } 30 | } 31 | 32 | // generate loader string to be used with extract text plugin 33 | function generateLoaders (loader, loaderOptions) { 34 | const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] 35 | 36 | if (loader) { 37 | loaders.push({ 38 | loader: loader + '-loader', 39 | options: Object.assign({}, loaderOptions, { 40 | sourceMap: options.sourceMap 41 | }) 42 | }) 43 | } 44 | 45 | // Extract CSS when that option is specified 46 | // (which is the case during production build) 47 | if (options.extract) { 48 | return ExtractTextPlugin.extract({ 49 | use: loaders, 50 | fallback: 'vue-style-loader' 51 | }) 52 | } else { 53 | return ['vue-style-loader'].concat(loaders) 54 | } 55 | } 56 | 57 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 58 | return { 59 | css: generateLoaders(), 60 | postcss: generateLoaders(), 61 | less: generateLoaders('less'), 62 | sass: generateLoaders('sass', { indentedSyntax: true }), 63 | scss: generateLoaders('sass'), 64 | stylus: generateLoaders('stylus'), 65 | styl: generateLoaders('stylus') 66 | } 67 | } 68 | 69 | // Generate loaders for standalone style files (outside of .vue) 70 | exports.styleLoaders = function (options) { 71 | const output = [] 72 | const loaders = exports.cssLoaders(options) 73 | 74 | for (const extension in loaders) { 75 | const loader = loaders[extension] 76 | output.push({ 77 | test: new RegExp('\\.' + extension + '$'), 78 | use: loader 79 | }) 80 | } 81 | 82 | return output 83 | } 84 | 85 | exports.createNotifierCallback = () => { 86 | const notifier = require('node-notifier') 87 | 88 | return (severity, errors) => { 89 | if (severity !== 'error') return 90 | 91 | const error = errors[0] 92 | const filename = error.file && error.file.split('!').pop() 93 | 94 | notifier.notify({ 95 | title: packageConfig.name, 96 | message: severity + ': ' + error.name, 97 | subtitle: filename || '', 98 | icon: path.join(__dirname, 'logo.png') 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /etcd-manage-ui/build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | const sourceMapEnabled = isProduction 6 | ? config.build.productionSourceMap 7 | : config.dev.cssSourceMap 8 | 9 | module.exports = { 10 | loaders: utils.cssLoaders({ 11 | sourceMap: sourceMapEnabled, 12 | extract: isProduction 13 | }), 14 | cssSourceMap: sourceMapEnabled, 15 | cacheBusting: config.dev.cacheBusting, 16 | transformToRequire: { 17 | video: ['src', 'poster'], 18 | source: 'src', 19 | img: 'src', 20 | image: 'xlink:href' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /etcd-manage-ui/build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const config = require('../config') 5 | const vueLoaderConfig = require('./vue-loader.conf') 6 | const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); 7 | 8 | 9 | function resolve (dir) { 10 | return path.join(__dirname, '..', dir) 11 | } 12 | 13 | 14 | module.exports = { 15 | context: path.resolve(__dirname, '../'), 16 | entry: { 17 | app: './src/main.js' 18 | }, 19 | output: { 20 | path: config.build.assetsRoot, 21 | filename: '[name].js', 22 | publicPath: process.env.NODE_ENV === 'production' 23 | ? config.build.assetsPublicPath 24 | : config.dev.assetsPublicPath 25 | }, 26 | resolve: { 27 | extensions: ['.js', '.vue', '.json'], 28 | alias: { 29 | 'vue$': 'vue/dist/vue.esm.js', 30 | '@': resolve('src'), 31 | } 32 | }, 33 | plugins: [ 34 | new MonacoWebpackPlugin() 35 | ], 36 | module: { 37 | rules: [ 38 | { 39 | test: /\.vue$/, 40 | loader: 'vue-loader', 41 | options: vueLoaderConfig 42 | }, 43 | { 44 | test: /\.js$/, 45 | loader: 'babel-loader', 46 | include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] 47 | }, 48 | { 49 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 50 | loader: 'url-loader', 51 | options: { 52 | limit: 10000, 53 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 54 | } 55 | }, 56 | { 57 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 58 | loader: 'url-loader', 59 | options: { 60 | limit: 10000, 61 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 62 | } 63 | }, 64 | { 65 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 66 | loader: 'url-loader', 67 | options: { 68 | limit: 10000, 69 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 70 | } 71 | }, 72 | { 73 | test: /\.sass$/, 74 | loaders: ['style', 'css', 'sass'] 75 | } 76 | ] 77 | }, 78 | node: { 79 | // prevent webpack from injecting useless setImmediate polyfill because Vue 80 | // source contains it (although only uses it if it's native). 81 | setImmediate: false, 82 | // prevent webpack from injecting mocks to Node native modules 83 | // that does not make sense for the client 84 | dgram: 'empty', 85 | fs: 'empty', 86 | net: 'empty', 87 | tls: 'empty', 88 | child_process: 'empty' 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /etcd-manage-ui/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const webpack = require('webpack') 4 | const config = require('../config') 5 | const merge = require('webpack-merge') 6 | const path = require('path') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 11 | const portfinder = require('portfinder') 12 | 13 | const HOST = process.env.HOST 14 | const PORT = process.env.PORT && Number(process.env.PORT) 15 | 16 | const devWebpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) 19 | }, 20 | // cheap-module-eval-source-map is faster for development 21 | devtool: config.dev.devtool, 22 | 23 | // these devServer options should be customized in /config/index.js 24 | devServer: { 25 | clientLogLevel: 'warning', 26 | historyApiFallback: { 27 | rewrites: [ 28 | { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, 29 | ], 30 | }, 31 | hot: true, 32 | contentBase: false, // since we use CopyWebpackPlugin. 33 | compress: true, 34 | host: HOST || config.dev.host, 35 | port: PORT || config.dev.port, 36 | open: config.dev.autoOpenBrowser, 37 | overlay: config.dev.errorOverlay 38 | ? { warnings: false, errors: true } 39 | : false, 40 | publicPath: config.dev.assetsPublicPath, 41 | proxy: config.dev.proxyTable, 42 | quiet: true, // necessary for FriendlyErrorsPlugin 43 | watchOptions: { 44 | poll: config.dev.poll, 45 | } 46 | }, 47 | plugins: [ 48 | new webpack.DefinePlugin({ 49 | 'process.env': require('../config/dev.env') 50 | }), 51 | new webpack.HotModuleReplacementPlugin(), 52 | new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. 53 | new webpack.NoEmitOnErrorsPlugin(), 54 | // https://github.com/ampedandwired/html-webpack-plugin 55 | new HtmlWebpackPlugin({ 56 | filename: 'index.html', 57 | template: 'index.html', 58 | inject: true 59 | }), 60 | // copy custom static assets 61 | new CopyWebpackPlugin([ 62 | { 63 | from: path.resolve(__dirname, '../static'), 64 | to: config.dev.assetsSubDirectory, 65 | ignore: ['.*'] 66 | } 67 | ]) 68 | ] 69 | }) 70 | 71 | module.exports = new Promise((resolve, reject) => { 72 | portfinder.basePort = process.env.PORT || config.dev.port 73 | portfinder.getPort((err, port) => { 74 | if (err) { 75 | reject(err) 76 | } else { 77 | // publish the new Port, necessary for e2e tests 78 | process.env.PORT = port 79 | // add port to devServer config 80 | devWebpackConfig.devServer.port = port 81 | 82 | // Add FriendlyErrorsPlugin 83 | devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ 84 | compilationSuccessInfo: { 85 | messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], 86 | }, 87 | onErrors: config.dev.notifyOnErrors 88 | ? utils.createNotifierCallback() 89 | : undefined 90 | })) 91 | 92 | resolve(devWebpackConfig) 93 | } 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /etcd-manage-ui/build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const webpack = require('webpack') 5 | const config = require('../config') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 12 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 13 | 14 | const env = require('../config/prod.env') 15 | 16 | const webpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ 19 | sourceMap: config.build.productionSourceMap, 20 | extract: true, 21 | usePostCSS: true 22 | }) 23 | }, 24 | devtool: config.build.productionSourceMap ? config.build.devtool : false, 25 | output: { 26 | path: config.build.assetsRoot, 27 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 28 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 29 | }, 30 | plugins: [ 31 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 32 | new webpack.DefinePlugin({ 33 | 'process.env': env 34 | }), 35 | new UglifyJsPlugin({ 36 | uglifyOptions: { 37 | compress: { 38 | warnings: false 39 | } 40 | }, 41 | sourceMap: config.build.productionSourceMap, 42 | parallel: true 43 | }), 44 | // extract css into its own file 45 | new ExtractTextPlugin({ 46 | filename: utils.assetsPath('css/[name].[contenthash].css'), 47 | // Setting the following option to `false` will not extract CSS from codesplit chunks. 48 | // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack. 49 | // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, 50 | // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110 51 | allChunks: true, 52 | }), 53 | // Compress extracted CSS. We are using this plugin so that possible 54 | // duplicated CSS from different components can be deduped. 55 | new OptimizeCSSPlugin({ 56 | cssProcessorOptions: config.build.productionSourceMap 57 | ? { safe: true, map: { inline: false } } 58 | : { safe: true } 59 | }), 60 | // generate dist index.html with correct asset hash for caching. 61 | // you can customize output by editing /index.html 62 | // see https://github.com/ampedandwired/html-webpack-plugin 63 | new HtmlWebpackPlugin({ 64 | filename: config.build.index, 65 | template: 'index.html', 66 | inject: true, 67 | minify: { 68 | removeComments: true, 69 | collapseWhitespace: true, 70 | removeAttributeQuotes: true 71 | // more options: 72 | // https://github.com/kangax/html-minifier#options-quick-reference 73 | }, 74 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 75 | chunksSortMode: 'dependency' 76 | }), 77 | // keep module.id stable when vendor modules does not change 78 | new webpack.HashedModuleIdsPlugin(), 79 | // enable scope hoisting 80 | new webpack.optimize.ModuleConcatenationPlugin(), 81 | // split vendor js into its own file 82 | new webpack.optimize.CommonsChunkPlugin({ 83 | name: 'vendor', 84 | minChunks (module) { 85 | // any required modules inside node_modules are extracted to vendor 86 | return ( 87 | module.resource && 88 | /\.js$/.test(module.resource) && 89 | module.resource.indexOf( 90 | path.join(__dirname, '../node_modules') 91 | ) === 0 92 | ) 93 | } 94 | }), 95 | // extract webpack runtime and module manifest to its own file in order to 96 | // prevent vendor hash from being updated whenever app bundle is updated 97 | new webpack.optimize.CommonsChunkPlugin({ 98 | name: 'manifest', 99 | minChunks: Infinity 100 | }), 101 | // This instance extracts shared chunks from code splitted chunks and bundles them 102 | // in a separate chunk, similar to the vendor chunk 103 | // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk 104 | new webpack.optimize.CommonsChunkPlugin({ 105 | name: 'app', 106 | async: 'vendor-async', 107 | children: true, 108 | minChunks: 3 109 | }), 110 | 111 | // copy custom static assets 112 | new CopyWebpackPlugin([ 113 | { 114 | from: path.resolve(__dirname, '../static'), 115 | to: config.build.assetsSubDirectory, 116 | ignore: ['.*'] 117 | } 118 | ]) 119 | ] 120 | }) 121 | 122 | if (config.build.productionGzip) { 123 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 124 | 125 | webpackConfig.plugins.push( 126 | new CompressionWebpackPlugin({ 127 | asset: '[path].gz[query]', 128 | algorithm: 'gzip', 129 | test: new RegExp( 130 | '\\.(' + 131 | config.build.productionGzipExtensions.join('|') + 132 | ')$' 133 | ), 134 | threshold: 10240, 135 | minRatio: 0.8 136 | }) 137 | ) 138 | } 139 | 140 | if (config.build.bundleAnalyzerReport) { 141 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 142 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 143 | } 144 | 145 | module.exports = webpackConfig 146 | -------------------------------------------------------------------------------- /etcd-manage-ui/config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /etcd-manage-ui/config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.3.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | 7 | module.exports = { 8 | dev: { 9 | 10 | // Paths 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/', 13 | proxyTable: {}, 14 | 15 | // Various Dev Server settings 16 | host: 'localhost', // can be overwritten by process.env.HOST 17 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 18 | autoOpenBrowser: false, 19 | errorOverlay: true, 20 | notifyOnErrors: true, 21 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 22 | 23 | 24 | /** 25 | * Source Maps 26 | */ 27 | 28 | // https://webpack.js.org/configuration/devtool/#development 29 | devtool: 'cheap-module-eval-source-map', 30 | 31 | // If you have problems debugging vue-files in devtools, 32 | // set this to false - it *may* help 33 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 34 | cacheBusting: true, 35 | 36 | cssSourceMap: true 37 | }, 38 | 39 | build: { 40 | // Template for index.html 41 | index: path.resolve(__dirname, '../dist/index.html'), 42 | 43 | // Paths 44 | assetsRoot: path.resolve(__dirname, '../dist'), 45 | assetsSubDirectory: 'static', 46 | assetsPublicPath: '/ui/', 47 | 48 | /** 49 | * Source Maps 50 | */ 51 | 52 | productionSourceMap: true, 53 | // https://webpack.js.org/configuration/devtool/#production 54 | devtool: '#source-map', 55 | 56 | // Gzip off by default as many popular static hosts such as 57 | // Surge or Netlify already gzip all static assets for you. 58 | // Before setting to `true`, make sure to: 59 | // npm install --save-dev compression-webpack-plugin 60 | productionGzip: false, 61 | productionGzipExtensions: ['js', 'css'], 62 | 63 | // Run the build command with an extra argument to 64 | // View the bundle analyzer report after build finishes: 65 | // `npm run build --report` 66 | // Set to `true` or `false` to always turn it on or off 67 | bundleAnalyzerReport: process.env.npm_config_report 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /etcd-manage-ui/config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /etcd-manage-ui/go.mod: -------------------------------------------------------------------------------- 1 | module etcd-manage-ui 2 | 3 | go 1.13 4 | 5 | require github.com/jteeuwen/go-bindata v3.0.7+incompatible // indirect 6 | -------------------------------------------------------------------------------- /etcd-manage-ui/go.sum: -------------------------------------------------------------------------------- 1 | github.com/jteeuwen/go-bindata v3.0.7+incompatible h1:91Uy4d9SYVr1kyTJ15wJsog+esAZZl7JmEfTkwmhJts= 2 | github.com/jteeuwen/go-bindata v3.0.7+incompatible/go.mod h1:JVvhzYOiGBnFSYRyV00iY8q7/0PThjIYav1p9h5dmKs= 3 | -------------------------------------------------------------------------------- /etcd-manage-ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Etcd-Manager 8 | 9 | 55 | 56 | 57 | 58 | 59 |
60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /etcd-manage-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "navbar", 3 | "version": "1.0.0", 4 | "description": "A Vue.js project", 5 | "author": "曾杰霖 <248717607@qq.com>", 6 | "private": true, 7 | "scripts": { 8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", 9 | "start": "npm run dev", 10 | "build": "node build/build.js" 11 | }, 12 | "dependencies": { 13 | "axios": "^0.18.0", 14 | "iview": "^3.2.2", 15 | "monaco-editor": "^0.20.0", 16 | "vue": "^2.5.2", 17 | "vue-i18n": "^8.8.2", 18 | "vue-router": "^3.0.1" 19 | }, 20 | "devDependencies": { 21 | "autoprefixer": "^7.1.2", 22 | "babel-core": "^6.22.1", 23 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 24 | "babel-loader": "^7.1.1", 25 | "babel-plugin-syntax-jsx": "^6.18.0", 26 | "babel-plugin-transform-runtime": "^6.22.0", 27 | "babel-plugin-transform-vue-jsx": "^3.5.0", 28 | "babel-preset-env": "^1.3.2", 29 | "babel-preset-stage-2": "^6.22.0", 30 | "chalk": "^2.0.1", 31 | "copy-webpack-plugin": "^4.0.1", 32 | "css-loader": "^0.28.0", 33 | "extract-text-webpack-plugin": "^3.0.0", 34 | "file-loader": "^1.1.4", 35 | "friendly-errors-webpack-plugin": "^1.6.1", 36 | "html-webpack-plugin": "^2.30.1", 37 | "monaco-editor-webpack-plugin": "^1.7.0", 38 | "node-notifier": "^5.1.2", 39 | "node-sass": "^4.9.0", 40 | "optimize-css-assets-webpack-plugin": "^3.2.0", 41 | "ora": "^1.2.0", 42 | "portfinder": "^1.0.13", 43 | "postcss-import": "^11.0.0", 44 | "postcss-loader": "^2.0.8", 45 | "postcss-url": "^7.2.1", 46 | "rimraf": "^2.6.0", 47 | "sass-loader": "^7.0.3", 48 | "semver": "^5.3.0", 49 | "shelljs": "^0.7.6", 50 | "uglifyjs-webpack-plugin": "^1.1.1", 51 | "url-loader": "^0.5.8", 52 | "vue-loader": "^13.3.0", 53 | "vue-style-loader": "^3.0.1", 54 | "vue-template-compiler": "^2.5.2", 55 | "webpack": "^3.6.0", 56 | "webpack-bundle-analyzer": "^2.9.0", 57 | "webpack-dev-server": "^2.9.1", 58 | "webpack-merge": "^4.1.0" 59 | }, 60 | "engines": { 61 | "node": ">= 6.0.0", 62 | "npm": ">= 3.0.0" 63 | }, 64 | "browserslist": [ 65 | "> 1%", 66 | "last 2 versions", 67 | "not ie <= 8" 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /etcd-manage-ui/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 23 | -------------------------------------------------------------------------------- /etcd-manage-ui/src/api/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import iView from 'iview' 3 | import { Message } from 'iview' 4 | import { bus } from "@/page/bus.js"; 5 | import { Config } from "@/config" 6 | 7 | // 请求前缀 8 | axios.defaults.baseURL = Config.BaseUrl 9 | 10 | // 请求之前拦截 11 | axios.interceptors.request.use( 12 | config => { 13 | // 添加用户认证信息 14 | let loginInfoStr = sessionStorage.getItem('login-info'); 15 | if(loginInfoStr){ 16 | let loginInfo = JSON.parse(loginInfoStr); 17 | config.headers.Token = loginInfo.token; 18 | } 19 | // etcd服务 20 | let etcdID = localStorage.getItem('EtcdID') || ''; // 读取当前选中的etcd server 21 | config.headers.EtcdID = etcdID; 22 | iView.LoadingBar.start(); 23 | return config 24 | }, 25 | err => { 26 | return Promise.reject(err) 27 | }) 28 | 29 | 30 | // 请求相应拦截器 31 | axios.interceptors.response.use( 32 | response => { 33 | // console.log(response) 34 | // 未登录情况 35 | if (response.status == 401) { 36 | ShowLogin(); 37 | } 38 | if (response.status == 400) { 39 | Message.error('req err') 40 | } 41 | iView.LoadingBar.finish(); 42 | return response 43 | }, 44 | error => { 45 | iView.LoadingBar.error(); 46 | // console.log(error.response); 47 | if (error.response) { 48 | if (error.response.status == 400) { 49 | Message.error(error.response.data.msg); 50 | } 51 | } else { 52 | Message.error('请求错误' + JSON.stringify(error)); 53 | } 54 | 55 | if (error.response) { 56 | switch (error.response.status) { 57 | case 401: 58 | ShowLogin(); 59 | break 60 | } 61 | } 62 | 63 | return Promise.reject(error) 64 | }); 65 | 66 | const ShowLogin = () => { 67 | // todo 删除本地用户缓存 68 | sessionStorage.removeItem('login-info'); 69 | // window.location.href = '/login' 70 | bus.$emit('show-login', true); 71 | Message.error('未登录,或登录失效,请登录') 72 | } 73 | 74 | export default axios -------------------------------------------------------------------------------- /etcd-manage-ui/src/api/kv.js: -------------------------------------------------------------------------------- 1 | import axios from './api' 2 | 3 | const KV = { 4 | /** 5 | * 查询指定目录的key列表 6 | * @param {string} dir 查询key的目录 7 | */ 8 | GetKeyList(dir) { 9 | return axios.get(`/v1/keys?path=${dir}`); 10 | }, 11 | 12 | /** 13 | * 获取一个key的详细信息 14 | * @param {string} dir 15 | */ 16 | GetKeyInfo(dir) { 17 | return axios.get(`/v1/keys/val?path=${dir}`) 18 | }, 19 | 20 | /** 21 | * 添加key 22 | * @param {*} data 添加keybody信息 23 | */ 24 | AddKey(data){ 25 | return axios.post('/v1/keys', data) 26 | }, 27 | 28 | /** 29 | * 修改key 30 | * @param {} data 修改时的body 31 | */ 32 | SaveKey(data){ 33 | return axios.put(`/v1/keys`, data) 34 | }, 35 | 36 | /** 37 | * 删除key 38 | * @param {string} dir 要删除的key 39 | */ 40 | DelKey(dir){ 41 | return axios.delete(`/v1/keys?path=${dir}`) 42 | } 43 | } 44 | 45 | // 获取指定目录的key列表 46 | export { 47 | KV 48 | } -------------------------------------------------------------------------------- /etcd-manage-ui/src/api/logs.js: -------------------------------------------------------------------------------- 1 | import axios from './api' 2 | 3 | const Logs = { 4 | /** 5 | * 日志列表 6 | */ 7 | GetLogsList(dateStr, page, pageSize, user, logType) { 8 | return axios.get(`/v1/logs?date=${dateStr}&page=${page}&page_size=${pageSize}&user=${user}&log_type=${logType}`); 9 | }, 10 | 11 | /** 12 | * 获取所有用户列表 13 | */ 14 | GetAllUser(){ 15 | return axios.get(`/v1/users`); 16 | }, 17 | 18 | /** 19 | * 获取所有日志类型 20 | */ 21 | GetLogTypes(){ 22 | return axios.get(`/v1/logtypes`); 23 | } 24 | 25 | } 26 | 27 | export { 28 | Logs 29 | } 30 | -------------------------------------------------------------------------------- /etcd-manage-ui/src/api/passport.js: -------------------------------------------------------------------------------- 1 | import axios from './api' 2 | 3 | const Passport = { 4 | 5 | /** 6 | * 登录 7 | * @param {*} username 用户名 8 | * @param {*} password 密码 9 | */ 10 | Login(username, password) { 11 | return axios.post(`/v1/passport/login`, { 12 | username: username, 13 | password: password 14 | }); 15 | }, 16 | 17 | } 18 | 19 | // 获取指定目录的key列表 20 | export { 21 | Passport 22 | } -------------------------------------------------------------------------------- /etcd-manage-ui/src/api/role.js: -------------------------------------------------------------------------------- 1 | import axios from './api' 2 | 3 | const Role = { 4 | /** 5 | * 全部角色 6 | */ 7 | GetAll() { 8 | return axios.get(`/v1/role`); 9 | }, 10 | 11 | /** 12 | * 添加 13 | * @param {*} data 添加角色信息 14 | */ 15 | Add(data){ 16 | return axios.post('/v1/role', data) 17 | }, 18 | 19 | /** 20 | * 删除角色 21 | * @param {*} id 22 | */ 23 | Del(id){ 24 | return axios.delete(`/v1/role?id=${id}`) 25 | }, 26 | 27 | /** 28 | * 修改角色 29 | * @param {*} data 30 | */ 31 | Save(data){ 32 | return axios.put('/v1/role', data) 33 | }, 34 | 35 | } 36 | 37 | export { 38 | Role 39 | } 40 | -------------------------------------------------------------------------------- /etcd-manage-ui/src/api/server.js: -------------------------------------------------------------------------------- 1 | import axios from './api' 2 | 3 | const SERVER = { 4 | /** 5 | * 查询指定目录的key列表 6 | */ 7 | GetMemberList() { 8 | return axios.get(`/v1/keys/members`); 9 | }, 10 | 11 | /** 12 | * 获取所有etcd服务列表 13 | */ 14 | GetEtcdServerList(name){ 15 | name = name || ''; 16 | return axios.get(`/v1/server?name=${name}`); 17 | }, 18 | 19 | /** 20 | * 添加 21 | * @param {Object} info 添加详情 22 | */ 23 | AddEtcdServer(info){ 24 | return axios.post(`/v1/server`, info); 25 | }, 26 | 27 | /** 28 | * 修改 29 | * @param {Object} edit 修改详情 30 | */ 31 | UpdateEtcdServer(edit){ 32 | return axios.put(`/v1/server`, edit); 33 | }, 34 | 35 | /** 36 | * 修复etcd key目录结构 37 | */ 38 | RestoreEtcdServer(id){ 39 | if (!id){ 40 | return 41 | } 42 | return axios.get(`/v1/server/restore?etcd_id=${id}`); 43 | }, 44 | 45 | /** 46 | * 获取权限列表 47 | * @param {*} id etcd服务id 48 | */ 49 | GetRoles(id){ 50 | if (!id){ 51 | return 52 | } 53 | return axios.get(`/v1/server/roles?etcd_id=${id}`); 54 | }, 55 | 56 | /** 57 | * 设置etcd服务权限配置 58 | * @param {*} data 59 | */ 60 | SetRoles(data){ 61 | if (!data){ 62 | return 63 | } 64 | return axios.post(`/v1/server/roles`, data); 65 | }, 66 | 67 | /** 68 | * 删除 69 | * @param {*} id 70 | */ 71 | Del(id){ 72 | return axios.delete(`/v1/server?id=${id}`) 73 | }, 74 | 75 | 76 | } 77 | 78 | export { 79 | SERVER 80 | } 81 | -------------------------------------------------------------------------------- /etcd-manage-ui/src/api/user.js: -------------------------------------------------------------------------------- 1 | import axios from './api' 2 | 3 | const User = { 4 | /** 5 | * 全部角色 6 | */ 7 | GetList(userId, username, roleId, page, pageSize) { 8 | return axios.get(`/v1/user?user_id=${userId}&name=${username}&role_id=${roleId}&page=${page}&page_size=${pageSize}`); 9 | }, 10 | 11 | /** 12 | * 添加 13 | * @param {*} data 添加角色信息 14 | */ 15 | Add(data){ 16 | return axios.post('/v1/user', data) 17 | }, 18 | 19 | /** 20 | * 删除角色 21 | * @param {*} id 22 | */ 23 | Del(id){ 24 | return axios.delete(`/v1/user?id=${id}`) 25 | }, 26 | 27 | /** 28 | * 修改角色 29 | * @param {*} data 30 | */ 31 | Save(data){ 32 | return axios.put('/v1/user', data) 33 | }, 34 | 35 | } 36 | 37 | export { 38 | User 39 | } 40 | -------------------------------------------------------------------------------- /etcd-manage-ui/src/assets/css/base.css: -------------------------------------------------------------------------------- 1 | body,dl,dd,ul,ol,h1,h2,h3,h4,h5,h6,pre,form,input,textarea,p,hr,thead,tbody,tfoot,th,td{margin:0;padding:0;} 2 | ul,ol{list-style:none;} 3 | a{text-decoration:none;} 4 | html{-ms-text-size-adjust:none;-webkit-text-size-adjust:none;text-size-adjust:none;} 5 | body{line-height:1.5; font-size:14px;} 6 | body,button,input,select,textarea{font-family:'helvetica neue',tahoma,'hiragino sans gb',stheiti,'wenquanyi micro hei',\5FAE\8F6F\96C5\9ED1,\5B8B\4F53,sans-serif;} 7 | b,strong{font-weight:bold;} 8 | i,em{font-style:normal;} 9 | table{border-collapse:collapse;border-spacing:0;} 10 | /* table th,table td{border:1px solid #ddd;padding:5px;} */ 11 | /* table th{font-weight:inherit;border-bottom-width:2px;border-bottom-color:#ccc;} */ 12 | /* img{border:0 none;width:auto\9;max-width:100%;vertical-align:top; height:auto;} */ 13 | button,input,select,textarea{font-family:inherit;font-size:100%;margin:0;vertical-align:baseline;} 14 | button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;} 15 | button[disabled],input[disabled]{cursor:default;} 16 | input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;} 17 | input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;} 18 | input[type="search"]::-webkit-search-decoration{-webkit-appearance:none;} 19 | input:focus{outline:none;} 20 | select[size],select[multiple],select[size][multiple]{border:1px solid #AAA;padding:0;} 21 | article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block;} 22 | audio,canvas,video,progress{display:inline-block;} 23 | html{width: 100%;height: 100%;} 24 | body{background:#fff;width: 100%;height: 100%;} 25 | input::-webkit-input-speech-button {display: none} 26 | button,input,textarea{ 27 | -webkit-tap-highlight-color: rgba(0,0,0,0); 28 | } 29 | input::-webkit-input-placeholder, textarea::-webkit-input-placeholder { 30 | color: #CECDD0; 31 | font-size: 14px; 32 | } 33 | input:-moz-placeholder, textarea:-moz-placeholder { 34 | font-size: 14px; 35 | color: #CECDD0; 36 | } 37 | input::-moz-placeholder, textarea::-moz-placeholder { 38 | font-size: 14px; 39 | color: #CECDD0; 40 | } 41 | input:-ms-input-placeholder, textarea:-ms-input-placeholder { 42 | font-size: 14px; 43 | color: #CECDD0; 44 | } -------------------------------------------------------------------------------- /etcd-manage-ui/src/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comqx/k8s-etcd-ui/e860824e994da8c48aea5cd3e9283f374828a8c9/etcd-manage-ui/src/assets/favicon.png -------------------------------------------------------------------------------- /etcd-manage-ui/src/assets/img/narrow-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comqx/k8s-etcd-ui/e860824e994da8c48aea5cd3e9283f374828a8c9/etcd-manage-ui/src/assets/img/narrow-hover.png -------------------------------------------------------------------------------- /etcd-manage-ui/src/assets/img/narrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comqx/k8s-etcd-ui/e860824e994da8c48aea5cd3e9283f374828a8c9/etcd-manage-ui/src/assets/img/narrow.png -------------------------------------------------------------------------------- /etcd-manage-ui/src/assets/img/unfold-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comqx/k8s-etcd-ui/e860824e994da8c48aea5cd3e9283f374828a8c9/etcd-manage-ui/src/assets/img/unfold-hover.png -------------------------------------------------------------------------------- /etcd-manage-ui/src/assets/img/unfold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comqx/k8s-etcd-ui/e860824e994da8c48aea5cd3e9283f374828a8c9/etcd-manage-ui/src/assets/img/unfold.png -------------------------------------------------------------------------------- /etcd-manage-ui/src/assets/imgs/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comqx/k8s-etcd-ui/e860824e994da8c48aea5cd3e9283f374828a8c9/etcd-manage-ui/src/assets/imgs/file.png -------------------------------------------------------------------------------- /etcd-manage-ui/src/assets/imgs/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comqx/k8s-etcd-ui/e860824e994da8c48aea5cd3e9283f374828a8c9/etcd-manage-ui/src/assets/imgs/folder.png -------------------------------------------------------------------------------- /etcd-manage-ui/src/common/CloudContainer.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 22 | -------------------------------------------------------------------------------- /etcd-manage-ui/src/common/CloudHeader.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 162 | 163 | 208 | -------------------------------------------------------------------------------- /etcd-manage-ui/src/common/CloudSideBar.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 209 | 210 | 302 | -------------------------------------------------------------------------------- /etcd-manage-ui/src/components/KvCard.vue: -------------------------------------------------------------------------------- 1 | 51 | 96 | 97 | 145 | -------------------------------------------------------------------------------- /etcd-manage-ui/src/components/KvGrid.vue: -------------------------------------------------------------------------------- 1 | 19 | 64 | 65 | 96 | -------------------------------------------------------------------------------- /etcd-manage-ui/src/components/KvList.vue: -------------------------------------------------------------------------------- 1 | 21 | 226 | 227 | 232 | -------------------------------------------------------------------------------- /etcd-manage-ui/src/components/MonacoEditor.vue: -------------------------------------------------------------------------------- 1 | 19 | 127 | -------------------------------------------------------------------------------- /etcd-manage-ui/src/config/index.js: -------------------------------------------------------------------------------- 1 | // 开发环境配置 2 | const DEV = { 3 | BaseUrl: 'http://127.0.0.1:10280' 4 | } 5 | 6 | // 正式环境 7 | const PRO = { 8 | BaseUrl: '' 9 | } 10 | 11 | export const Config = process.env.NODE_ENV === 'development' ? DEV : PRO 12 | -------------------------------------------------------------------------------- /etcd-manage-ui/src/i18n/en-US.js: -------------------------------------------------------------------------------- 1 | const lang = { 2 | sideBar: { 3 | oneEtcd: 'One Etcd Server', 4 | KV: 'Key / Value', 5 | MEMBERS: 'MEMBERS', 6 | Other: 'Other', 7 | EtcdServers: 'Etcd Servers', 8 | Logs: 'Logs', 9 | Setings: 'Setings', 10 | Role: 'Role', 11 | User: 'User' 12 | }, 13 | login:{ 14 | title: 'Login', 15 | username: 'Username', 16 | password: 'Password', 17 | loginBtn: 'Login' 18 | }, 19 | public:{ 20 | add:'Add', 21 | edit:"Edit", 22 | confirmDelete:'Confirm delete', 23 | delete:"Delete", 24 | multiDelete:'Multi Delete', 25 | hide:'Hide', 26 | save:'Save', 27 | close:'Close', 28 | screen:'Local screening', 29 | role: "Role" 30 | }, 31 | header:{ 32 | 'personalCenter':'Personal Center', 33 | 'logout':'Logout', 34 | }, 35 | key:{ 36 | currentPathTips:'Key can only be a path that cannot be a value.', 37 | delConfirm:'Are you sure you want to delete the selected key?', 38 | notSubKey:'No child level list!', 39 | editKey:'Editor Key', 40 | addKey:'Add Key', 41 | open:'Open', 42 | show:'Show', 43 | keyNotEmpty:'Key can not be empty', 44 | addSuccessfully:'Added successfully!', 45 | saveSuccessfully:'Saved successfully!', 46 | showTree: 'Show tree', 47 | keyPlaceholder: "Key path, which can contain multiple'/'", 48 | keyNotPrefixBar: "Key must begin with '/'" 49 | }, 50 | logs:{ 51 | date:'Date', 52 | user:'User', 53 | typeof:'Type', 54 | filter:'Filter', 55 | selectDate:'Please select a date', 56 | selectDateShowLog:'Select date to view the log', 57 | }, 58 | etcdServer:{ 59 | repairDirectory:'Repair directory', 60 | determineRepairDirectory:'Determine the directory problem for repairing the etcd key', 61 | } 62 | 63 | } 64 | 65 | export default lang; -------------------------------------------------------------------------------- /etcd-manage-ui/src/i18n/zh-CN.js: -------------------------------------------------------------------------------- 1 | const lang = { 2 | sideBar: { 3 | oneEtcd: '一个Etcd服务', 4 | KV: '键 / 值', 5 | MEMBERS: '集群信息', 6 | Other: '其它', 7 | EtcdServers: 'Etcd 服务列表', 8 | Logs: '日志', 9 | Setings: '设置', 10 | Role: '角色', 11 | User: '用户' 12 | }, 13 | login:{ 14 | title: '登 录', 15 | username: '用户名', 16 | password: '密 码', 17 | loginBtn: '登 录' 18 | }, 19 | public:{ 20 | add:'添加', 21 | edit:"编辑", 22 | confirmDelete:'确定删除', 23 | delete:"删除", 24 | multiDelete:'批删除', 25 | hide:'隐藏', 26 | save:'保存', 27 | close:'关闭', 28 | screen:'本地筛选', 29 | role: '权限' 30 | }, 31 | header:{ 32 | 'personalCenter':'个人中心', 33 | 'logout':'退出', 34 | }, 35 | key:{ 36 | currentPathTips:'key路径,只能是路径不能是值', 37 | delConfirm:'确定删除选中的key?', 38 | notSubKey:'没有子级列表!', 39 | addKey:'添加key', 40 | editKey:'编辑key', 41 | open:'打开', 42 | show:'查看', 43 | keyNotEmpty:'key不能为空', 44 | addSuccessfully:'添加成功!', 45 | saveSuccessfully:'保存成功!', 46 | showTree: '显示树形', 47 | keyPlaceholder: "key路径可以包含多个'/'", 48 | keyNotPrefixBar: "key必须以 '/' 开头" 49 | }, 50 | logs:{ 51 | date:'日期', 52 | user:'用户', 53 | typeof:'类型', 54 | filter:'筛选', 55 | selectDate:'请选择日期', 56 | selectDateShowLog:'选择日期查看日志', 57 | }, 58 | etcdServer:{ 59 | repairDirectory:'修复目录', 60 | determineRepairDirectory:'确定修复etcd key的目录问题', 61 | } 62 | } 63 | 64 | export default lang; -------------------------------------------------------------------------------- /etcd-manage-ui/src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | import router from './router' 6 | import iView from 'iview'; 7 | import 'iview/dist/styles/iview.css'; 8 | import './assets/css/base.css' 9 | 10 | import VueI18n from 'vue-i18n'; 11 | import en from 'iview/dist/locale/en-US'; 12 | import zh from 'iview/dist/locale/zh-CN'; 13 | 14 | import myen from './i18n/en-US'; 15 | import myzh from './i18n/zh-CN'; 16 | 17 | 18 | Vue.use(VueI18n); 19 | 20 | Vue.config.productionTip = false 21 | 22 | Vue.use(iView, { 23 | transfer: true 24 | }); 25 | 26 | 27 | // 语言 28 | Vue.locale = () => {}; 29 | 30 | const messages = { 31 | en: Object.assign(myen, en), 32 | zh: Object.assign(myzh, zh) 33 | }; 34 | 35 | let mylang = localStorage.getItem("etcd-language") || 'en'; 36 | 37 | // Create VueI18n instance with options 38 | const i18n = new VueI18n({ 39 | locale: mylang, // set locale 40 | messages // set locale messages 41 | }); 42 | 43 | // 判断字符串是否是以xx为前缀 44 | Vue.prototype.HasPrefix = (str, prefix) => { 45 | console.log(str, prefix,str.substring(str.length - prefix.length)); 46 | 47 | if(prefix == null || prefix == "" || str.length == 0 || prefix.length > str.length) 48 | return false 49 | if(str.substring(0, prefix.length) == prefix) 50 | return true 51 | else 52 | return false 53 | } 54 | 55 | 56 | /* eslint-disable no-new */ 57 | new Vue({ 58 | el: '#app', 59 | router, 60 | i18n, 61 | components: { 62 | App 63 | }, 64 | template: '' 65 | }) 66 | -------------------------------------------------------------------------------- /etcd-manage-ui/src/page/CloudHome.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 84 | 85 | 106 | -------------------------------------------------------------------------------- /etcd-manage-ui/src/page/Logs.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 45 | 46 | 153 | -------------------------------------------------------------------------------- /etcd-manage-ui/src/page/Members.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 79 | 85 | -------------------------------------------------------------------------------- /etcd-manage-ui/src/page/Role.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 180 | 181 | -------------------------------------------------------------------------------- /etcd-manage-ui/src/page/User.vue: -------------------------------------------------------------------------------- 1 | 2 | 97 | 287 | 288 | -------------------------------------------------------------------------------- /etcd-manage-ui/src/page/bus.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | export const bus = new Vue() 3 | -------------------------------------------------------------------------------- /etcd-manage-ui/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import CloudHome from '@/page/CloudHome' 4 | 5 | Vue.use(Router) 6 | 7 | export default new Router({ 8 | routes: [{ 9 | path: '/', 10 | name: 'CloudHome', 11 | component: CloudHome, 12 | children: [ 13 | { 14 | path: '/key/kv', 15 | name: 'KV', 16 | component: () => import('@/page/KV'), 17 | }, 18 | { 19 | path: '/server/members', 20 | name: 'Members', 21 | component: () => import('@/page/Members'), 22 | }, 23 | { 24 | path: '/setings/EtcdServers', 25 | name: 'EtcdServers', 26 | component: () => import('@/page/EtcdServers'), 27 | }, 28 | { 29 | path: '/setings/user', 30 | name: 'User', 31 | component: () => import('@/page/User'), 32 | }, 33 | { 34 | path: '/setings/role', 35 | name: 'Role', 36 | component: () => import('@/page/Role'), 37 | } 38 | 39 | ] 40 | }] 41 | }) 42 | -------------------------------------------------------------------------------- /etcd-manage-ui/static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comqx/k8s-etcd-ui/e860824e994da8c48aea5cd3e9283f374828a8c9/etcd-manage-ui/static/.gitkeep -------------------------------------------------------------------------------- /etcd-manage-ui/static/etcdui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comqx/k8s-etcd-ui/e860824e994da8c48aea5cd3e9283f374828a8c9/etcd-manage-ui/static/etcdui.png -------------------------------------------------------------------------------- /etcd-manage-ui/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comqx/k8s-etcd-ui/e860824e994da8c48aea5cd3e9283f374828a8c9/etcd-manage-ui/static/favicon.ico -------------------------------------------------------------------------------- /etcd-manage-ui/tpls/compile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | npm run build 4 | go-bindata -o tpls.go ../dist/... 5 | 6 | sed -i "" "s/package main/package tpls/g" tpls.go 7 | -------------------------------------------------------------------------------- /etcdsdk/main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/etcd-manage/etcd-manage-server/etcdsdk/model" 9 | "go.etcd.io/etcd/clientv3" 10 | ) 11 | 12 | func main() { 13 | var ( 14 | client3Config clientv3.Config 15 | //cert tls.Certificate 16 | resp *clientv3.GetResponse 17 | //err error 18 | ) 19 | cfg := model.Config{ 20 | EtcdId: 8, 21 | Version: "v3", 22 | Address: []string{"http://127.0.0.1:2379"}, 23 | TlsEnable: "false", 24 | CertFile: nil, 25 | KeyFile: nil, 26 | CaFile: nil, 27 | } 28 | //cfg.CertFile = common.Base64Decode("LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURJRENDQWdpZ0F3SUJBZ0lJQ1dRWENPM1A3QUF3RFFZSktvWklodmNOQVFFTEJRQXdFakVRTUE0R0ExVUUKQXhNSFpYUmpaQzFqWVRBZUZ3MHlNREE0TURRd01qVXlNelZhRncweU1UQTRNRFF3TWpVeU16VmFNQmd4RmpBVQpCZ05WQkFNVERXeHhlQzF0WVhOMFpYSXRNREV3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUURoNWJqUDNXN2NzVlhEaHdKWHlrWUR6RjRLaldrYXFQc0xNNmVFRGhYZWJXMGVaZi9RT3QrcUFxaWgKbTdPZ3BISFp6VFhFODVVSHV5QkFTM3daQU0wZDRqODY1bHFwaTJuTU41Vm5GTkNMOEpmdlRiTVdocFpleS9DVApPc1J3amhLQ0JHc2kvbHE5SEt0WnJtdmd0TjRNVlhyanB4cE5Td1BaeDFjNENBUXJPOFk1MkFpRlU5cFJoNmtHCmZ1TC82NlFGaDlpTnN2eExOUnFmVFpLY3oxUWxrZEdyWUdqVXE1blVrV3MwQnM3NmpmU3hQRFBydG1XN1lhdnEKbHo5RjVQSzZaSTY3Mm84MWNzOTVQR0VKV2JRTWpBaHRnckpKMGdoenVoTU15YlRYQWFmUCtQZkUxMEdQWlRMUgpvWit5c1U2dWI2eHNwVm1NZzR0b1lxMWhVWW4zQWdNQkFBR2pkREJ5TUE0R0ExVWREd0VCL3dRRUF3SUZvREFkCkJnTlZIU1VFRmpBVUJnZ3JCZ0VGQlFjREFRWUlLd1lCQlFVSEF3SXdRUVlEVlIwUkJEb3dPSUlOYkhGNExXMWgKYzNSbGNpMHdNWUlKYkc5allXeG9iM04waHdRS0ZBTm1od1IvQUFBQmh4QUFBQUFBQUFBQUFBQUFBQUFBQUFBQgpNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUFOODNxZFJ2eWl1ZVZkd09ZOWozVG0raFVxdFN5anV3eWI3aGJSCnR2UDNJb3RLbm50ZzNpRndYZVpNQkxTWUhvWU9jVXJ6ZndCUzZHOUgrdjBkRFh0Vi8vR3ljKzQ4QUVLbjhjZU8KT2xIRnJJQkRUMkE5VVBZTUhkVmNMcEZjOHJvdGhPQmJjd3RGSFB6aStqUlI5dE9oYjhQY0l0WFhTL29nOXVNMQpqaE5zU282VHlhYlA0U3BVSUhVNXpkbEMxaHBVTXk4b2V0SnI0d1c3SEpvK00xd2pjWmNNTU5VTEE4UEllb2xICndtR1dFa1g2OFZVQzhGU0FDK1ZyeUlLM20vTWVjQkEwMFFseGtBZmprbWt2b2NqbHhvVTJ1ZzVLTHYyWUthb0YKeC9mVjFuOUNuTFlpdmU3VWI5T2pGTmxjazJZOHpMNjRhTXlnVzZvOWRWTzBDcFVNCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K") 29 | //cfg.KeyFile = common.Base64Decode("LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBNGVXNHo5MXUzTEZWdzRjQ1Y4cEdBOHhlQ28xcEdxajdDek9uaEE0VjNtMXRIbVgvCjBEcmZxZ0tvb1p1em9LUngyYzAxeFBPVkI3c2dRRXQ4R1FETkhlSS9PdVphcVl0cHpEZVZaeFRRaS9DWDcwMnoKRm9hV1hzdndrenJFY0k0U2dnUnJJdjVhdlJ5cldhNXI0TFRlREZWNjQ2Y2FUVXNEMmNkWE9BZ0VLenZHT2RnSQpoVlBhVVllcEJuN2kvK3VrQllmWWpiTDhTelVhbjAyU25NOVVKWkhScTJCbzFLdVoxSkZyTkFiTytvMzBzVHd6CjY3Wmx1MkdyNnBjL1JlVHl1bVNPdTlxUE5YTFBlVHhoQ1ZtMERJd0liWUt5U2RJSWM3b1RETW0wMXdHbnovajMKeE5kQmoyVXkwYUdmc3JGT3JtK3NiS1ZaaklPTGFHS3RZVkdKOXdJREFRQUJBb0lCQUFuQzg0bUgrQkp4VjFOcgpzaDQ1RDIxNmwxVzlacDFRVUFqYjRwRkNTbytpQ3VVVlkwaU1RcjRGLzJOOFp2YTZKSEZVL00zVitNcXN1MmdMCjJ3RDVsK09DczFqSU80SzRFNHBQbkpVbndSdEsra1hOQmNBamNMd0g5QTFvckxSd2J6eFBGSkllaTYxQVgrY3cKTWxremQ2VHd6SzZwdWVrOUpKMTBqOEJNR0dJRnpuSnlBMm5EYjU4WFJCM2UySnFRQlFEQ1BaM1BJaHE3UlJrNQp3Q1ExMG9jbXdKbUQwTGkydUlqRW5JOTU4ZHRzVVdBSmI5Z1FoNTVIeFphSEU2R05pTkRQdlpxZCtyc1ZJSGRsClFqR2MvQTY0NVpvSnNIQWczeGVyWnArdytvT0pmc253SEJkQ1ZpZlFTR0lWa1g0a243QXVzZFZrSDFWN1JqYjEKT2NFWWd3RUNnWUVBNzM3YnpVT3RDRWUvNnNsaHhMYm9JYXVLQXk3bDFDdk16MWNYMXJDaDV2Z2V3bW4yd3RzRAowZE9xSWdGcXZ1MVIvTTltVkpsZzNYeTRRc01oc2Vabk9BRTJIeGZIcFByVkoyOGJlMm1paWdZQjI3ZTBnY0FpCnhKb3h2L2pSazlZRTZzZnJYN3lOU2pIQWJBWGszWjh0Y1BnU0pxRktDUUp3YmgyR2QrcTdBZWtDZ1lFQThYYjMKWE9kY05aczhteEFDZjhqMUZjNTlRZGNqOGJlQ1YrU0ViM09YMkhxMlZobjJkbzltbFcyaTA2ejFuVTU3NVdqUQpHUW43NDNpUjYzb3pqSmZLNU5EKzJidWRSMnZOeVo2VkxzUHlySlZIUUxHSDBhT1pvL3RMdTZoQStCazJZWm83ClZOWFBReHFIMVF1UG1UNWFIbHpXMVBrWnpZQndFWVVnSHpKbjROOENnWUI3QnczZW1mOVBHNXlJL2I1TmtUQVoKNjhiUDc4MThlcXVSYjBuOXJXcmQvV08vdHpOZDBhaGFwNExrU1JvT3psYXBxZGtGYUcwTUdqK0ZmRHZZNldUbwpyaWFoUGxQK2VpUDVSK2cwTTRXZHBZeGExRG5UMVdHRzRUYmhTTzVRSlVjTlhIbWJDbjhDT0NDQzNWdytSTURSCklYNGhmZ1ZNTDRhVjZuRGpOUit2MFFLQmdRQ3BGTGdEK3hJTGk0ZDF3VkV1cjhaR25kQUNBYWR1eENSbWJXTDcKTkFNNUdEeURzQ3h3T3R3SGVMMFM0a01mQXUwbzZDc0h6WUR2ZU9jYzcvWVcxZGZDUUVLa3JvWmtrNjJIS0IrbwpucGRZbUROTHJzUy9YSUxpVzc1ZFNtVXNGV09LRnRqQy8vRGhPVHV5U0NVbWxvMitReDVBQmFvMngyQXlOSGtZCnYxQVVHd0tCZ0JNMklPWDIzc3VSd0o1UHU1eGowQzFkVzh1QzFaYjEzeWVoZm93ZjdueFFxWmhtRHk4cGlOUmUKUnFKQlFhaFpnYTdKWi9uZWtKZTh5S3hueGhtQWdJSy9hTmxJMkQ3NThEL1d5NENQZVluQ2hKS0VZTzh5bHFwMgovVk5JeXpIOHNLNWs1M1E0UzNmdThlNlVyM29KcndFQWtkVmZSeGFsbzU5V1dwWjY2bkp0Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==") 30 | //cfg.CaFile = common.Base64Decode("LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN3akNDQWFxZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFTTVJBd0RnWURWUVFERXdkbGRHTmsKTFdOaE1CNFhEVEl3TURnd05EQXlOVEl6TlZvWERUTXdNRGd3TWpBeU5USXpOVm93RWpFUU1BNEdBMVVFQXhNSApaWFJqWkMxallUQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQU5sN0J6YzZzcWFNCjJvbnAwOVMxTndVK0xKRkFFeXdLN2lqUHV4TlRSV2VQSzFwajF5TmxiUVlDV1pHZUExMUFQRVFUdEVzc0c1dlUKeWJTNHUydzFkM3Q2WEpwb2FpZ3pOVkVPaWVlUkgxOE9lb2pVNWdWM2hZTlNUTVByQUIrZXJ0YVcxNkMzQmhTRgprVmFrc2I5UHIxNU0ramE3WUpCQnhnL2hNOEtIbzU2NVl3Z0hJUXoxM253R3JqMUVPc2x0YkVRN0xValFRdU9wCkpmYU82WElCZWVhUlFJWm9yZjZLTVdNY29ScG9nbFFwcXRXWVh1cTUwOUNWd3NqTWNmZDEwV3dQUHE0aWt3RE0KYVFYS0JXTWhwczR4azY4SWcrVVNLRGkxZXdzOThrOFo5UlQ5b001Z3FBVVQ1RzRyTGpWNzZSZDQrR2xiblFpdwpkUHFxdzdMMkwrOENBd0VBQWFNak1DRXdEZ1lEVlIwUEFRSC9CQVFEQWdLa01BOEdBMVVkRXdFQi93UUZNQU1CCkFmOHdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBSDdkRXNiSWZWN3RLNTlEZjBIRFNLd1ZLS3dkWGlzbTlqTkYKYUI2SG5ST1RvaE5lb2NNYXFWRlo5N0RWZytlTzh0UUJVdFhrVlg1N08vVzY2dFMrckNIcW04cVJYNzJFMVRzRQpmQzI2QW9tdXdEbnBHOGxvUXdLV2x4L2YrY1BEdkVhZ0MrLytDZVI3dmZrc09TbktGbVVhS0l4M0MxSWVYT1RKCk1jOWpVQ1Z0N05xbzBjY3dDOUplMkVqd3lnYkVXUEVOZnczMGRtZ1B4UTNKdXJRSmhLZlhFU21lZ0hOTUw4R08KQnFHY3YzY1A5TWQrbzhJdUVpby9BUkdCWjE2QXFrSmozTDY2TGc1bjBlVldLVWZpOE9hd3BvL2F3Q2JuaXhCMgoyQ3lZRkFiNHR0VkVZT3dpT2FjTkRiaXQvc0xuejFMcHpkT2xlQy96WjZTd2c4eWVBbE09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K") 31 | 32 | //if cert, err = tls.X509KeyPair(cfg.CertFile, cfg.KeyFile); err != nil { 33 | // return 34 | //} 35 | //pool := x509.NewCertPool() 36 | //pool.AppendCertsFromPEM(cfg.CaFile) 37 | //_tlsConfig := &tls.Config{ 38 | // Certificates: []tls.Certificate{cert}, 39 | // RootCAs: pool, 40 | //} 41 | client3Config = clientv3.Config{ 42 | Endpoints: cfg.Address, 43 | DialTimeout: 5 * time.Second, 44 | } 45 | cliNew, _ := clientv3.New(client3Config) 46 | resp, _ = clientv3.NewKV(cliNew).Get(context.Background(), "/", clientv3.WithFromKey(), clientv3.WithKeysOnly()) 47 | fmt.Println(resp) 48 | } 49 | -------------------------------------------------------------------------------- /etcdsdk/model/config.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | const ( 8 | ETCD_VERSION_V3 = "v3" 9 | ) 10 | 11 | // Config etcd 连接配置 12 | type Config struct { 13 | EtcdId int32 `json:"etcd_id,omitempty"` 14 | Version string `json:"version,omitempty"` 15 | Address []string `json:"address,omitempty"` 16 | TlsEnable string `json:"tls_enable,omitempty"` 17 | CertFile []byte `json:"cert_file,omitempty"` 18 | KeyFile []byte `json:"key_file,omitempty"` 19 | CaFile []byte `json:"ca_file,omitempty"` 20 | Username string `json:"username,omitempty"` 21 | Password string `json:"password,omitempty"` 22 | } 23 | 24 | func (c *Config) String() string { 25 | return strconv.Itoa(int(c.EtcdId)) 26 | } 27 | -------------------------------------------------------------------------------- /etcdsdk/model/error.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "errors" 4 | 5 | // etcd 错误定义 6 | var ( 7 | ERR_CONFIG_ISNIL = errors.New("Config is nil") 8 | ERR_TLS_CONFIG_ISNIL = errors.New("TLSConfig is nil") 9 | ERR_ETCD_ADDRESS_EMPTY = errors.New("Etcd connection address cannot be empty") 10 | ERR_UNSUPPORTED_VERSION = errors.New("Unsupported etcd version") 11 | 12 | ERR_ADD_KEY = errors.New("Add key error") 13 | ERR_KEY_NOT_FOUND = errors.New("Key does not exist") 14 | ERR_KEY_NOT_DIR = errors.New("Key is not a directory") 15 | ) 16 | -------------------------------------------------------------------------------- /etcdsdk/model/interface.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // EtcdSdk 统一操作etcd接口 4 | type EtcdSdk interface { 5 | List(path string) (list []*Node, err error) // 显示当前path下所有key 6 | Val(path string) (data *Node, err error) // 获取path的值 7 | Add(path string, data []byte) (err error) // 添加key 8 | Put(path string, data []byte) (err error) // 修改key 9 | Del(path string) (err error) // 删除key 10 | Members() (members []*Member, err error) // 获取节点列表 11 | Close() (err error) // 关闭连接 12 | } 13 | -------------------------------------------------------------------------------- /etcdsdk/model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // 这里保存sdk使用到的所有结构图 4 | 5 | type ListNodes struct { 6 | IsDir bool `json:"is_dir"` 7 | Path string `json:"path"` 8 | Name string `json:"name"` 9 | Version int64 `json:"version"` 10 | Lease int64 `json:"lease"` 11 | } 12 | 13 | // Node 一个key 目录或文件 14 | type Node struct { 15 | IsDir bool `json:"is_dir"` 16 | Path string `json:"path"` 17 | Name string `json:"name"` 18 | Value string `json:"value"` 19 | Version int64 `json:"version"` 20 | Lease int64 `json:"lease"` 21 | } 22 | 23 | const ( 24 | ROLE_LEADER = "leader" 25 | ROLE_FOLLOWER = "follower" 26 | 27 | STATUS_HEALTHY = "healthy" 28 | STATUS_UNHEALTHY = "unhealthy" 29 | ) 30 | 31 | // Member 节点信息 32 | type Member struct { 33 | // *etcdserverpb.Member 34 | ID string `json:"ID"` 35 | Name string `json:"name"` 36 | PeerURLs []string `json:"peerURLs"` 37 | ClientURLs []string `json:"clientURLs"` 38 | Role string `json:"role"` 39 | Status string `json:"status"` 40 | DbSize int64 `json:"db_size"` 41 | } 42 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/etcd-manage/etcd-manage-server 2 | 3 | go 1.13 4 | 5 | replace ( 6 | github.com/coreos/bbolt v1.3.5 => go.etcd.io/bbolt v1.3.5 7 | github.com/etcd-manage/etcd-manage-ui v0.0.0-00010101000000-000000000000 => ./etcd-manage-ui 8 | go.etcd.io/bbolt v1.3.5 => github.com/coreos/bbolt v1.3.5 9 | google.golang.org/grpc => google.golang.org/grpc v1.26.0 10 | ) 11 | 12 | require ( 13 | github.com/coreos/bbolt v1.3.5 // indirect 14 | github.com/coreos/etcd v3.3.22+incompatible // indirect 15 | github.com/coreos/go-semver v0.3.0 // indirect 16 | github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect 17 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect 18 | github.com/docker/go-metrics v0.0.1 // indirect 19 | github.com/dustin/go-humanize v1.0.0 // indirect 20 | github.com/etcd-manage/etcd-manage-ui v0.0.0-00010101000000-000000000000 21 | github.com/gin-gonic/autotls v0.0.0-20200518075542-45033372a9ad 22 | github.com/gin-gonic/gin v1.6.3 23 | github.com/go-sql-driver/mysql v1.5.0 24 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect 25 | github.com/google/uuid v1.1.1 26 | github.com/gorilla/mux v1.7.4 // indirect 27 | github.com/gorilla/websocket v1.4.2 // indirect 28 | github.com/grpc-ecosystem/go-grpc-middleware v1.2.0 // indirect 29 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect 30 | github.com/grpc-ecosystem/grpc-gateway v1.14.6 // indirect 31 | github.com/jinzhu/gorm v1.9.14 32 | github.com/jonboulle/clockwork v0.2.0 // indirect 33 | github.com/kylelemons/godebug v1.1.0 // indirect 34 | github.com/naoina/go-stringutil v0.1.0 // indirect 35 | github.com/naoina/toml v0.1.1 36 | github.com/opencontainers/image-spec v1.0.1 // indirect 37 | github.com/patrickmn/go-cache v2.1.0+incompatible 38 | github.com/prometheus/client_golang v1.7.1 // indirect 39 | github.com/shiguanghuxian/etcd-manage v2.0.0-beta+incompatible 40 | github.com/soheilhy/cmux v0.1.4 // indirect 41 | github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966 // indirect 42 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect 43 | go.etcd.io/bbolt v1.3.5 // indirect 44 | go.etcd.io/etcd v3.3.22+incompatible 45 | go.uber.org/zap v1.15.0 46 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect 47 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f // indirect 48 | k8s.io/apimachinery v0.18.6 49 | k8s.io/kubectl v0.18.6 50 | ) 51 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/etcd-manage/etcd-manage-server/program" 8 | "os/signal" 9 | ) 10 | 11 | func main() { 12 | // 系统日志显示文件和行号 13 | log.SetFlags(log.Lshortfile | log.LstdFlags) 14 | // 服务对象 15 | p, err := program.New() 16 | if err != nil { 17 | log.Println(err) 18 | os.Exit(1) 19 | } 20 | err = p.Run() 21 | if err != nil { 22 | log.Println(err) 23 | os.Exit(1) 24 | } 25 | 26 | // 监听退出信号 27 | c := make(chan os.Signal) 28 | signal.Notify(c, os.Interrupt, os.Kill) // , syscall.SIGUSR1, syscall.SIGUSR2 29 | <-c 30 | log.Println("Exit") 31 | } 32 | -------------------------------------------------------------------------------- /program/api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | gin "github.com/gin-gonic/gin" 5 | ) 6 | 7 | // API api 路由注册 8 | type API interface { 9 | Register(router *gin.RouterGroup) 10 | } 11 | -------------------------------------------------------------------------------- /program/api/v1/keys/keys.go: -------------------------------------------------------------------------------- 1 | package keys 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/etcd-manage/etcd-manage-server/etcdsdk/model" 10 | 11 | "github.com/etcd-manage/etcd-manage-server/etcdsdk" 12 | "github.com/etcd-manage/etcd-manage-server/program/publicFunc" 13 | 14 | "github.com/gin-gonic/gin" 15 | ) 16 | 17 | // KeyController key控制器 18 | type KeyController struct { 19 | publicFunc.PublicF 20 | client etcdsdk.EtcdV3 21 | etcdIdNum int 22 | } 23 | 24 | // List 获取目录下key列表 25 | // /v1/keys?path= 26 | func (api *KeyController) List(c *gin.Context) { 27 | var ( 28 | path string 29 | err error 30 | list []*model.Node 31 | ) 32 | defer func() { 33 | api.client.Close() 34 | if err != nil { 35 | c.JSON(http.StatusBadRequest, gin.H{ 36 | "msg": err.Error(), 37 | }) 38 | } 39 | }() 40 | 41 | path = c.DefaultQuery("path", "/") 42 | if api.etcdIdNum, err = strconv.Atoi(c.GetHeader("EtcdID")); err != nil { 43 | fmt.Printf("获取etcdid失败:%s\n", err.Error()) 44 | } 45 | 46 | if api.client, err = api.GetEtcdClient(api.etcdIdNum); err != nil { 47 | fmt.Println("获取client失败,err", err.Error()) 48 | return 49 | } 50 | 51 | if list, err = api.client.List(path); err != nil { 52 | fmt.Println("获取etcd 数据失败,err", err.Error()) 53 | return 54 | } 55 | c.JSON(http.StatusOK, list) 56 | } 57 | 58 | // Val 获取一个key的值 59 | func (api *KeyController) Val(c *gin.Context) { 60 | var ( 61 | err error 62 | list *model.Node 63 | ) 64 | defer func() { 65 | api.client.Close() 66 | if err != nil { 67 | c.JSON(http.StatusBadRequest, gin.H{ 68 | "msg": err.Error(), 69 | }) 70 | } 71 | }() 72 | path := c.Query("path") 73 | if api.etcdIdNum, err = strconv.Atoi(c.GetHeader("EtcdID")); err != nil { 74 | fmt.Printf("获取etcdId 失败:%s\n", err.Error()) 75 | } 76 | 77 | if api.client, err = api.GetEtcdClient(api.etcdIdNum); err != nil { 78 | fmt.Println("获取etcd 数据失败,err", err.Error()) 79 | return 80 | } 81 | 82 | if list, err = api.client.Val(path); err != nil { 83 | fmt.Println("获取etcd 数据失败,err", err.Error()) 84 | return 85 | } 86 | c.JSON(http.StatusOK, list) 87 | } 88 | 89 | // ReqKeyBody 添加和修改key请求body 90 | type ReqKeyBody struct { 91 | Path string `json:"path"` 92 | Value string `json:"value"` 93 | } 94 | 95 | // Add 添加key 96 | func (api *KeyController) Add(c *gin.Context) { 97 | var ( 98 | req ReqKeyBody 99 | err error 100 | ) 101 | defer func() { 102 | api.client.Close() 103 | if err != nil { 104 | c.JSON(http.StatusBadRequest, gin.H{ 105 | "msg": err.Error(), 106 | }) 107 | } 108 | }() 109 | if err = c.Bind(req); err != nil { 110 | return 111 | } 112 | if api.etcdIdNum, err = strconv.Atoi(c.GetHeader("EtcdID")); err != nil { 113 | fmt.Printf("获取etcdId 失败:%s\n", err.Error()) 114 | } 115 | 116 | if api.client, err = api.GetEtcdClient(api.etcdIdNum); err != nil { 117 | fmt.Println("获取etcd 数据失败,err", err.Error()) 118 | return 119 | } 120 | 121 | if err = api.client.Add(req.Path, []byte(req.Value)); err != nil { 122 | return 123 | } 124 | c.JSON(http.StatusOK, "ok") 125 | } 126 | 127 | // Put 修改key 128 | func (api *KeyController) Put(c *gin.Context) { 129 | var ( 130 | req ReqKeyBody 131 | err error 132 | ) 133 | defer func() { 134 | api.client.Close() 135 | if err != nil { 136 | c.JSON(http.StatusBadRequest, gin.H{ 137 | "msg": err.Error(), 138 | }) 139 | } 140 | }() 141 | 142 | if err = c.Bind(req); err != nil { 143 | return 144 | } 145 | if api.etcdIdNum, err = strconv.Atoi(c.GetHeader("EtcdID")); err != nil { 146 | fmt.Printf("获取etcdId 失败:%s\n", err.Error()) 147 | } 148 | 149 | if api.client, err = api.GetEtcdClient(api.etcdIdNum); err != nil { 150 | fmt.Println("获取etcd 数据失败,err", err.Error()) 151 | return 152 | } 153 | 154 | if err = api.client.Put(req.Path, []byte(req.Value)); err != nil { 155 | return 156 | } 157 | c.JSON(http.StatusOK, "ok") 158 | } 159 | 160 | // Del 删除key 161 | func (api *KeyController) Del(c *gin.Context) { 162 | var ( 163 | err error 164 | path string 165 | ) 166 | defer func() { 167 | api.client.Close() 168 | if err != nil { 169 | c.JSON(http.StatusBadRequest, gin.H{ 170 | "msg": err.Error(), 171 | }) 172 | } 173 | }() 174 | if path = c.Query("path"); path == "" { 175 | err = errors.New("Path cannot be empty") 176 | return 177 | } 178 | if api.etcdIdNum, err = strconv.Atoi(c.GetHeader("EtcdID")); err != nil { 179 | fmt.Printf("获取etcdId 失败:%s\n", err.Error()) 180 | return 181 | } 182 | 183 | if api.client, err = api.GetEtcdClient(api.etcdIdNum); err != nil { 184 | fmt.Println("获取etcd 数据失败,err", err.Error()) 185 | return 186 | } 187 | if err = api.client.Del(path); err != nil { 188 | return 189 | } 190 | c.JSON(http.StatusOK, "ok") 191 | } 192 | 193 | // Members 获取etcd服务节点 194 | func (api *KeyController) Members(c *gin.Context) { 195 | var ( 196 | err error 197 | members []*model.Member 198 | ) 199 | defer func() { 200 | api.client.Close() 201 | if err != nil { 202 | c.JSON(http.StatusBadRequest, gin.H{ 203 | "msg": err.Error(), 204 | }) 205 | } 206 | }() 207 | 208 | if api.etcdIdNum, err = strconv.Atoi(c.GetHeader("EtcdID")); err != nil { 209 | fmt.Printf("获取etcdId 失败:%s\n", err.Error()) 210 | return 211 | } 212 | 213 | if api.client, err = api.GetEtcdClient(api.etcdIdNum); err != nil { 214 | fmt.Println("获取etcd 数据失败,err", err.Error()) 215 | return 216 | } 217 | 218 | if members, err = api.client.Members(); err != nil { 219 | return 220 | } 221 | c.JSON(http.StatusOK, members) 222 | } 223 | -------------------------------------------------------------------------------- /program/api/v1/passport/passport.go: -------------------------------------------------------------------------------- 1 | package passport 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/etcd-manage/etcd-manage-server/program/cache" 10 | "github.com/etcd-manage/etcd-manage-server/program/common" 11 | "github.com/etcd-manage/etcd-manage-server/program/logger" 12 | "github.com/etcd-manage/etcd-manage-server/program/models" 13 | "github.com/gin-gonic/gin" 14 | "github.com/google/uuid" 15 | ) 16 | 17 | // PassportController 登录退出 18 | type PassportController struct { 19 | } 20 | 21 | // LoginReq 登录请求参数 22 | type LoginReq struct { 23 | Username string `json:"username,omitempty"` 24 | Password string `json:"password,omitempty"` 25 | } 26 | 27 | func (api *PassportController) Login(c *gin.Context) { 28 | req := new(LoginReq) 29 | err := c.Bind(req) 30 | if err != nil { 31 | c.JSON(http.StatusBadRequest, gin.H{ 32 | "msg": err.Error(), 33 | }) 34 | return 35 | } 36 | if req.Username == "" || req.Password == "" { 37 | c.JSON(http.StatusBadRequest, gin.H{ 38 | "msg": "参数错误", 39 | }) 40 | return 41 | } 42 | // 查询数据库登录 43 | req.Password = common.Md5Password(req.Password) 44 | log.Println(req.Password) 45 | user := new(models.UsersModel) 46 | err = user.FirstByUsernameAndPassword(req.Username, req.Password) 47 | if err != nil { 48 | logger.Log.Errorw("登录查询用户错误", "err", err) 49 | c.JSON(http.StatusBadRequest, gin.H{ 50 | "msg": "用户名或密码错误", 51 | }) 52 | return 53 | } 54 | user.Password = "" 55 | user.Token = uuid.New().String() 56 | // 存储Token 57 | jsUser, _ := json.Marshal(user) 58 | key := cache.GetLoginKey(user.Token) 59 | cache.DefaultMemCache.Set(key, string(jsUser), 7*24*time.Hour) 60 | 61 | c.JSON(http.StatusOK, user) 62 | } 63 | -------------------------------------------------------------------------------- /program/api/v1/router.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/etcd-manage/etcd-manage-server/program/api/v1/keys" 5 | "github.com/etcd-manage/etcd-manage-server/program/api/v1/passport" 6 | "github.com/etcd-manage/etcd-manage-server/program/api/v1/server" 7 | "github.com/etcd-manage/etcd-manage-server/program/api/v1/setings/role" 8 | "github.com/etcd-manage/etcd-manage-server/program/api/v1/setings/user" 9 | "github.com/etcd-manage/etcd-manage-server/program/api/v1/upload" 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | // APIV1 v1版接口 14 | type APIV1 struct { 15 | } 16 | 17 | // Register 注册路由 18 | func (v1 *APIV1) Register(router *gin.RouterGroup) { 19 | // etcd key 管理 20 | gx := router.Group("/keys") 21 | keysController := new(keys.KeyController) 22 | gx.GET("", keysController.List) 23 | gx.GET("/val", keysController.Val) 24 | gx.POST("", keysController.Add) 25 | gx.PUT("", keysController.Put) 26 | gx.DELETE("", keysController.Del) 27 | 28 | // 一个etcd服务集群信息 29 | gx.GET("/members", keysController.Members) 30 | 31 | // etcd服务列表 32 | serverController := new(server.ServerController) 33 | gs := router.Group("/server") 34 | gs.GET("", serverController.List) 35 | gs.POST("", serverController.Add) 36 | gs.PUT("", serverController.Update) 37 | gs.DELETE("", serverController.Del) 38 | gs.GET("/restore", serverController.Restore) 39 | gs.POST("/roles", serverController.SetRoles) 40 | gs.GET("/roles", serverController.GetRoles) 41 | 42 | // 认证中心 43 | passportController := new(passport.PassportController) 44 | gp := router.Group("/passport") 45 | gp.POST("/login", passportController.Login) 46 | 47 | // 角色 48 | roleController := new(role.RoleController) 49 | rs := router.Group("/role") 50 | rs.GET("", roleController.All) 51 | rs.POST("", roleController.Add) 52 | rs.PUT("", roleController.Update) 53 | rs.DELETE("", roleController.Del) 54 | 55 | // 用户 56 | userController := new(user.UserController) 57 | us := router.Group("/user") 58 | us.GET("", userController.List) 59 | us.POST("", userController.Add) 60 | us.PUT("", userController.Update) 61 | us.DELETE("", userController.Del) 62 | 63 | // 文件上传 64 | uploadController := new(upload.UploadController) 65 | uus := router.Group("/upload") 66 | uus.POST("/content", uploadController.UploadOutContent) 67 | 68 | } 69 | -------------------------------------------------------------------------------- /program/api/v1/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/etcd-manage/etcd-manage-server/etcdsdk" 11 | "github.com/etcd-manage/etcd-manage-server/etcdsdk/model" 12 | "github.com/etcd-manage/etcd-manage-server/program/common" 13 | "github.com/etcd-manage/etcd-manage-server/program/logger" 14 | "github.com/etcd-manage/etcd-manage-server/program/models" 15 | "github.com/gin-gonic/gin" 16 | 17 | //"strings" 18 | "time" 19 | ) 20 | 21 | // ServerController etcd服务列表相关操作 22 | type ServerController struct { 23 | } 24 | 25 | // List 获取etcd服务列表,全部 26 | func (api *ServerController) List(c *gin.Context) { 27 | name := c.Query("name") 28 | // 查询当前角色权限 29 | userinfoObj, exist := c.Get("userinfo") 30 | if exist == false { 31 | logger.Log.Warnw("用户登录信息不存在") 32 | c.AbortWithStatus(http.StatusUnauthorized) 33 | return 34 | } 35 | userinfo := userinfoObj.(*models.UsersModel) 36 | 37 | list, err := new(models.EtcdServersModel).All(name, userinfo.RoleId) 38 | if err != nil { 39 | c.JSON(http.StatusBadRequest, gin.H{ 40 | "msg": err.Error(), 41 | }) 42 | } 43 | c.JSON(http.StatusOK, list) 44 | } 45 | 46 | // Add 添加服务 47 | func (api *ServerController) Add(c *gin.Context) { 48 | var err error 49 | defer func() { 50 | if err != nil { 51 | c.JSON(http.StatusBadRequest, gin.H{ 52 | "msg": err.Error(), 53 | }) 54 | } 55 | }() 56 | // 添加 57 | req := new(models.EtcdServersModel) 58 | err = c.Bind(req) 59 | if err != nil { 60 | return 61 | } 62 | now := models.JSONTime(time.Now()) 63 | req.CreatedAt = now 64 | err = req.Insert() 65 | if err != nil { 66 | return 67 | } 68 | // 添加超级管理员权限 69 | re := &models.RoleEtcdServersModel{ 70 | EtcdServerId: req.ID, 71 | Type: 1, 72 | RoleId: 1, 73 | CreatedAt: now, 74 | UpdatedAt: now, 75 | } 76 | 77 | if err = re.Save(); err != nil { 78 | return 79 | } 80 | c.JSON(http.StatusOK, "ok") 81 | } 82 | 83 | // Update 修改服务 84 | func (api *ServerController) Update(c *gin.Context) { 85 | var err error 86 | if err != nil { 87 | c.JSON(http.StatusBadRequest, gin.H{ 88 | "msg": err.Error(), 89 | }) 90 | } 91 | // 添加 92 | req := new(models.EtcdServersModel) 93 | err = c.Bind(req) 94 | if err != nil { 95 | return 96 | } 97 | req.UpdatedAt = models.JSONTime(time.Now()) 98 | err = req.Update() 99 | if err != nil { 100 | return 101 | } 102 | 103 | c.JSON(http.StatusOK, "ok") 104 | } 105 | 106 | // Restore 修复v1版本或e3w对目录的标记 107 | // /v1/server/restore?etcd_id=6" 108 | func (api *ServerController) Restore(c *gin.Context) { 109 | etcdId := c.Query("etcd_id") 110 | var err error 111 | defer func() { 112 | if err != nil { 113 | c.JSON(http.StatusBadRequest, gin.H{ 114 | "msg": err.Error(), 115 | }) 116 | } 117 | }() 118 | 119 | etcdIdNum, _ := strconv.Atoi(etcdId) 120 | etcdOne := new(models.EtcdServersModel) 121 | etcdOne, err = etcdOne.FirstById(int32(etcdIdNum)) 122 | if err != nil { 123 | return 124 | } 125 | if etcdOne.Version != model.ETCD_VERSION_V3 { 126 | err = errors.New("Only V3 version is allowed to be repaired") 127 | return 128 | } 129 | // 连接etcd 130 | cfg := model.Config{ 131 | Version: etcdOne.Version, 132 | Address: strings.Split(etcdOne.Address, ","), 133 | TlsEnable: etcdOne.TlsEnable, 134 | CertFile: common.Base64Decode(etcdOne.CaFile), 135 | KeyFile: common.Base64Decode(etcdOne.KeyFile), 136 | CaFile: common.Base64Decode(etcdOne.CaFile), 137 | Username: etcdOne.Username, 138 | Password: etcdOne.Password, 139 | } 140 | //cfg := &model.Config{ 141 | // Version: "v3", 142 | // Address: []string{"https://127.0.0.1:2379"}, 143 | // TlsEnable: "true", 144 | // CertFile: "/Users/liuqixiang/project/goStudyProject/etcd-ui/etcd-manage-server/0_cert.pem", 145 | // KeyFile: "/Users/liuqixiang/project/goStudyProject/etcd-ui/etcd-manage-server/0_key.pem", 146 | // CaFile: "/Users/liuqixiang/project/goStudyProject/etcd-ui/etcd-manage-server/0_ca.pem", 147 | //} 148 | 149 | if _, err = etcdsdk.NewClient(cfg); err != nil { 150 | fmt.Println("etcdv3.NewClient(cfg)>>>", err) 151 | return 152 | } 153 | //clientV3, ok := client.(*etcdsdk.EtcdV3) 154 | //if ok == false { 155 | // err = errors.New("Connecting etcd V3 service error") 156 | // return 157 | //} 158 | //err = client.Restore() 159 | //if err != nil { 160 | // fmt.Println("clientV3.Restore()>>>", err) 161 | // return 162 | //} 163 | c.JSON(http.StatusOK, "ok") 164 | } 165 | 166 | // SetRoles 设置etcd服务角色 167 | func (api *ServerController) SetRoles(c *gin.Context) { 168 | req := make([]*models.AllByEtcdIdData, 0) 169 | err := c.Bind(&req) 170 | if err != nil { 171 | c.JSON(http.StatusBadRequest, gin.H{ 172 | "msg": err.Error(), 173 | }) 174 | return 175 | } 176 | if len(req) == 0 { 177 | c.JSON(http.StatusBadRequest, gin.H{ 178 | "msg": "未设置任何权限", 179 | }) 180 | return 181 | } 182 | m := new(models.RoleEtcdServersModel) 183 | err = m.UpByEtcdId(req) 184 | if err != nil { 185 | c.JSON(http.StatusBadRequest, gin.H{ 186 | "msg": err.Error(), 187 | }) 188 | return 189 | } 190 | 191 | c.JSON(http.StatusOK, "ok") 192 | } 193 | 194 | // GetRoles 获取etcd服务权限列表 195 | func (api *ServerController) GetRoles(c *gin.Context) { 196 | etcdId := common.GetHttpToInt(c, "etcd_id") 197 | if etcdId <= 0 { 198 | c.JSON(http.StatusBadRequest, gin.H{ 199 | "msg": "参数错误", 200 | }) 201 | return 202 | } 203 | list, err := new(models.RoleEtcdServersModel).AllByEtcdId(int32(etcdId)) 204 | if err != nil { 205 | c.JSON(http.StatusBadRequest, gin.H{ 206 | "msg": err.Error(), 207 | }) 208 | return 209 | } 210 | 211 | c.JSON(http.StatusOK, list) 212 | } 213 | 214 | // Del 删除 215 | func (s *ServerController) Del(c *gin.Context) { 216 | id := c.Query("id") 217 | idNum, _ := strconv.Atoi(id) 218 | if idNum == 0 { 219 | c.JSON(http.StatusBadRequest, gin.H{ 220 | "msg": "参数错误", 221 | }) 222 | return 223 | } 224 | err := new(models.EtcdServersModel).Del(int32(idNum)) 225 | if err != nil { 226 | c.JSON(http.StatusBadRequest, gin.H{ 227 | "msg": err.Error(), 228 | }) 229 | return 230 | } 231 | 232 | c.JSON(http.StatusOK, nil) 233 | } 234 | -------------------------------------------------------------------------------- /program/api/v1/setings/role/role.go: -------------------------------------------------------------------------------- 1 | package role 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/etcd-manage/etcd-manage-server/program/models" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | /* 角色管理 */ 13 | 14 | // RoleController 系统管理设置相关 15 | type RoleController struct { 16 | } 17 | 18 | // All 获取所有角色 19 | func (s *RoleController) All(c *gin.Context) { 20 | list, err := new(models.RolesModel).All() 21 | if err != nil { 22 | c.JSON(http.StatusBadRequest, gin.H{ 23 | "msg": err.Error(), 24 | }) 25 | return 26 | } 27 | c.JSON(http.StatusOK, list) 28 | } 29 | 30 | // Add 添加角色 31 | func (s *RoleController) Add(c *gin.Context) { 32 | req := new(models.RolesModel) 33 | err := c.Bind(req) 34 | if err != nil { 35 | c.JSON(http.StatusBadRequest, gin.H{ 36 | "msg": err.Error(), 37 | }) 38 | return 39 | } 40 | now := models.JSONTime(time.Now()) 41 | req.CreatedAt = now 42 | req.UpdatedAt = now 43 | req.Id = 0 44 | err = req.Save() 45 | if err != nil { 46 | c.JSON(http.StatusBadRequest, gin.H{ 47 | "msg": err.Error(), 48 | }) 49 | return 50 | } 51 | 52 | c.JSON(http.StatusOK, nil) 53 | } 54 | 55 | // Del 删除角色 56 | func (s *RoleController) Del(c *gin.Context) { 57 | id := c.Query("id") 58 | idNum, _ := strconv.Atoi(id) 59 | if idNum == 0 { 60 | c.JSON(http.StatusBadRequest, gin.H{ 61 | "msg": "参数错误", 62 | }) 63 | return 64 | } 65 | err := new(models.RolesModel).Del(int32(idNum)) 66 | if err != nil { 67 | c.JSON(http.StatusBadRequest, gin.H{ 68 | "msg": err.Error(), 69 | }) 70 | return 71 | } 72 | c.JSON(http.StatusOK, nil) 73 | } 74 | 75 | // Up 修改角色 76 | func (s *RoleController) Update(c *gin.Context) { 77 | req := new(models.RolesModel) 78 | err := c.Bind(req) 79 | if err != nil { 80 | c.JSON(http.StatusBadRequest, gin.H{ 81 | "msg": err.Error(), 82 | }) 83 | return 84 | } 85 | if req.Id <= 0 { 86 | c.JSON(http.StatusBadRequest, gin.H{ 87 | "msg": "参数错误", 88 | }) 89 | return 90 | } 91 | now := models.JSONTime(time.Now()) 92 | req.UpdatedAt = now 93 | err = req.Save() 94 | if err != nil { 95 | c.JSON(http.StatusBadRequest, gin.H{ 96 | "msg": err.Error(), 97 | }) 98 | return 99 | } 100 | 101 | c.JSON(http.StatusOK, nil) 102 | } 103 | -------------------------------------------------------------------------------- /program/api/v1/setings/user/user.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/etcd-manage/etcd-manage-server/program/common" 10 | "github.com/etcd-manage/etcd-manage-server/program/logger" 11 | "github.com/etcd-manage/etcd-manage-server/program/models" 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | /* 用户管理 */ 16 | 17 | // UserController 用户管理 18 | type UserController struct { 19 | } 20 | 21 | // List 分页获取 22 | func (s *UserController) List(c *gin.Context) { 23 | var err error 24 | defer func() { 25 | if err != nil { 26 | c.JSON(http.StatusBadRequest, gin.H{ 27 | "msg": err.Error(), 28 | }) 29 | } 30 | }() 31 | // 读取条件 32 | name := c.Query("name") 33 | userId := common.GetHttpToInt(c, "user_id") 34 | roleId := common.GetHttpToInt(c, "role_id") 35 | page := common.GetHttpToInt(c, "page") 36 | pageSize := common.GetHttpToInt(c, "page_size") 37 | if page == 0 { 38 | page = 1 39 | } 40 | offset := (page - 1) * pageSize 41 | // 查询列表 42 | user := new(models.UsersModel) 43 | list, err := user.List(int32(userId), int32(roleId), name, offset, pageSize) 44 | if err != nil { 45 | logger.Log.Errorw("查询用户列表错误", "err", err) 46 | return 47 | } 48 | _c, err := user.ListCount(int32(userId), int32(roleId), name, offset, pageSize) 49 | if err != nil { 50 | logger.Log.Errorw("查询用户总数错误", "err", err) 51 | return 52 | } 53 | // 去除密码 54 | for _, v := range list { 55 | v.Password = "" 56 | } 57 | 58 | c.JSON(http.StatusOK, gin.H{ 59 | "list": list, 60 | "total": _c, 61 | }) 62 | } 63 | 64 | // Add 添加 65 | func (s *UserController) Add(c *gin.Context) { 66 | req := new(models.UsersModel) 67 | err := c.Bind(req) 68 | if err != nil { 69 | c.JSON(http.StatusBadRequest, gin.H{ 70 | "msg": err.Error(), 71 | }) 72 | return 73 | } 74 | if req.RoleId <= 0 { 75 | c.JSON(http.StatusBadRequest, gin.H{ 76 | "msg": "请选择角色角色", 77 | }) 78 | return 79 | } 80 | req.Password = strings.TrimSpace(req.Password) 81 | if len(req.Password) < 6 { 82 | c.JSON(http.StatusBadRequest, gin.H{ 83 | "msg": "密码长度不能小于6", 84 | }) 85 | return 86 | } 87 | req.Password = common.Md5Password(req.Password) 88 | now := models.JSONTime(time.Now()) 89 | req.CreatedAt = now 90 | req.UpdatedAt = now 91 | req.Id = 0 92 | err = req.Save() 93 | if err != nil { 94 | c.JSON(http.StatusBadRequest, gin.H{ 95 | "msg": err.Error(), 96 | }) 97 | return 98 | } 99 | 100 | c.JSON(http.StatusOK, nil) 101 | } 102 | 103 | // Del 删除 104 | func (s *UserController) Del(c *gin.Context) { 105 | id := c.Query("id") 106 | idNum, _ := strconv.Atoi(id) 107 | if idNum == 0 { 108 | c.JSON(http.StatusBadRequest, gin.H{ 109 | "msg": "参数错误", 110 | }) 111 | return 112 | } 113 | err := new(models.UsersModel).Del(int32(idNum)) 114 | if err != nil { 115 | c.JSON(http.StatusBadRequest, gin.H{ 116 | "msg": err.Error(), 117 | }) 118 | return 119 | } 120 | c.JSON(http.StatusOK, nil) 121 | } 122 | 123 | // Up 修改 124 | func (s *UserController) Update(c *gin.Context) { 125 | req := new(models.UsersModel) 126 | err := c.Bind(req) 127 | if err != nil { 128 | c.JSON(http.StatusBadRequest, gin.H{ 129 | "msg": err.Error(), 130 | }) 131 | return 132 | } 133 | if req.Id <= 0 { 134 | c.JSON(http.StatusBadRequest, gin.H{ 135 | "msg": "参数错误", 136 | }) 137 | return 138 | } 139 | req.Password = strings.TrimSpace(req.Password) 140 | omit := make([]string, 0) 141 | omit = append(omit, "created_at") 142 | if req.Password != "" { 143 | if len(req.Password) < 6 { 144 | c.JSON(http.StatusBadRequest, gin.H{ 145 | "msg": "密码长度不能小于6", 146 | }) 147 | return 148 | } 149 | req.Password = common.Md5Password(req.Password) 150 | } else { 151 | omit = append(omit, "password") 152 | } 153 | 154 | now := models.JSONTime(time.Now()) 155 | req.UpdatedAt = now 156 | err = req.Save(omit...) 157 | if err != nil { 158 | c.JSON(http.StatusBadRequest, gin.H{ 159 | "msg": err.Error(), 160 | }) 161 | return 162 | } 163 | 164 | c.JSON(http.StatusOK, nil) 165 | } 166 | -------------------------------------------------------------------------------- /program/api/v1/upload/upload.go: -------------------------------------------------------------------------------- 1 | package upload 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | type UploadController struct { 12 | } 13 | 14 | // UploadOutContent 上传文件,返回内容 15 | func (api *UploadController) UploadOutContent(c *gin.Context) { 16 | var err error 17 | defer func() { 18 | if err != nil { 19 | c.JSON(http.StatusBadRequest, gin.H{ 20 | "msg": err.Error(), 21 | }) 22 | } 23 | }() 24 | 25 | fileInfo, err := c.FormFile("file") 26 | if err != nil { 27 | return 28 | } 29 | if fileInfo.Size > 1*1024*1024 { 30 | err = errors.New("上传文件大于1Mb") 31 | return 32 | } 33 | f, err := fileInfo.Open() 34 | if err != nil { 35 | return 36 | } 37 | 38 | content, err := ioutil.ReadAll(f) 39 | if err != nil { 40 | return 41 | } 42 | c.JSON(http.StatusOK, gin.H{ 43 | "content": content, 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /program/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | /* 缓存对象 */ 9 | 10 | // Cache 缓存接口 - 用户缓存登录信息 11 | type Cache interface { 12 | // Get 获取一个缓存 13 | Get(key string) (val string, exist bool) 14 | // Set 设置一个值 15 | Set(key, val string, expiration time.Duration) 16 | // Del 删除知道keys 17 | Del(key ...string) (err error) 18 | } 19 | 20 | // 常量 21 | const ( 22 | LoginKey = "login:%s" 23 | ) 24 | 25 | // GetLoginKey 获取登录key 26 | func GetLoginKey(token string) string { 27 | return fmt.Sprintf(LoginKey, token) 28 | } 29 | -------------------------------------------------------------------------------- /program/cache/default.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/patrickmn/go-cache" 7 | ) 8 | 9 | /* 默认缓存 - 内存 */ 10 | 11 | var ( 12 | // DefaultMemCache 默认缓存对象 13 | DefaultMemCache Cache 14 | ) 15 | 16 | func init() { 17 | cli := cache.New(7*24*time.Hour, 10*time.Second) 18 | DefaultMemCache = &MemCache{ 19 | cli: cli, 20 | } 21 | } 22 | 23 | // MemCache 内存缓存 https://github.com/patrickmn/go-cache 24 | type MemCache struct { 25 | cli *cache.Cache 26 | } 27 | 28 | // Get 获取一个缓存 29 | func (mem *MemCache) Get(key string) (val string, exist bool) { 30 | valI, exist := mem.cli.Get(key) 31 | if exist == true { 32 | val = valI.(string) 33 | } 34 | return 35 | } 36 | 37 | // Set 设置一个值 38 | func (mem *MemCache) Set(key, val string, expiration time.Duration) { 39 | mem.cli.Set(key, val, expiration) 40 | } 41 | 42 | // Del 删除知道keys 43 | func (mem *MemCache) Del(key ...string) (err error) { 44 | for _, k := range key { 45 | mem.cli.Delete(k) 46 | } 47 | return 48 | } 49 | -------------------------------------------------------------------------------- /program/common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/base64" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | "os" 10 | "path/filepath" 11 | "strconv" 12 | 13 | "github.com/etcd-manage/etcd-manage-server/etcdsdk/model" 14 | "github.com/gin-gonic/gin" 15 | ) 16 | 17 | // GetRootDir 获取执行路径 18 | func GetRootDir() string { 19 | // 文件不存在获取执行路径 20 | file, err := filepath.Abs(filepath.Dir(os.Args[0])) 21 | if err != nil { 22 | file = fmt.Sprintf(".%s", string(os.PathSeparator)) 23 | } else { 24 | file = fmt.Sprintf("%s%s", file, string(os.PathSeparator)) 25 | } 26 | return file 27 | } 28 | 29 | // PathExists 判断文件或目录是否存在 30 | func PathExists(path string) (bool, error) { 31 | _, err := os.Stat(path) 32 | if err == nil { 33 | return true, nil 34 | } 35 | if os.IsNotExist(err) { 36 | return false, nil 37 | } 38 | return false, err 39 | } 40 | 41 | // GetEtcdClientByGinContext 获取一个etcd客户端 从gin请求上下文 42 | func GetEtcdClientByGinContext(c *gin.Context) (client model.EtcdSdk, err error) { 43 | clientI, exists := c.Get("CLIENT") 44 | 45 | if exists == false || clientI == nil { 46 | err = errors.New("Etcd service connection error") 47 | return 48 | } 49 | client = clientI.(model.EtcdSdk) 50 | return 51 | } 52 | 53 | // Md5Password 密码生成 54 | func Md5Password(password string) string { 55 | salt := "etcd-manage" 56 | return Md5(Md5(password) + Md5(salt)) 57 | } 58 | 59 | // Md5 计算字符串md5值 60 | func Md5(s string) string { 61 | h := md5.New() 62 | h.Write([]byte(s)) 63 | return hex.EncodeToString(h.Sum(nil)) 64 | } 65 | 66 | // GetHttpToInt 获取请求参数,转为int 67 | func GetHttpToInt(c *gin.Context, name string) int { 68 | valStr := c.Query(name) 69 | val, _ := strconv.Atoi(valStr) 70 | return val 71 | } 72 | 73 | func Base64Decode(base64String string) []byte { 74 | var ( 75 | decoded []byte 76 | err error 77 | ) 78 | if decoded, err = base64.StdEncoding.DecodeString(base64String); err != nil { 79 | fmt.Printf("解密失败:%s\n", err.Error()) 80 | } 81 | return decoded 82 | } 83 | -------------------------------------------------------------------------------- /program/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | 6 | //"github.com/etcd-manage/etcd-manage-server/program/common" 7 | "github.com/naoina/toml" 8 | ) 9 | 10 | // Config 配置 11 | type Config struct { 12 | Debug bool `toml:"debug"` 13 | LogPath string `toml:"log_path"` 14 | HTTP *HTTP `toml:"http"` 15 | DB *MySQLConfig `toml:"db"` 16 | } 17 | 18 | // HTTP http 件套配置 19 | type HTTP struct { 20 | Address string `toml:"address"` 21 | Port int `toml:"port"` 22 | TLSEnable bool `toml:"tls_enable"` // 是否启用tls连接 23 | TLSConfig *HTTPTls `toml:"tls_config"` // 启用tls时必须配置此内容 24 | TLSEncryptEnable bool `toml:"tls_encrypt_enable"` // 是否启用 Let's Encrypt tls 25 | TLSEncryptDomainNames []string `toml:"tls_encrypt_domain_names"` // 启用 Let's Encrypt 时的域名列表 26 | } 27 | 28 | // HTTPTls http tls配置 29 | type HTTPTls struct { 30 | CertFile string `toml:"cert_file"` 31 | KeyFile string `toml:"key_file"` 32 | } 33 | 34 | // MySQLConfig 数据库配置 35 | type MySQLConfig struct { 36 | Debug bool `toml:"debug"` // 是否调试模式 37 | Address string `toml:"address"` // 数据库连接地址 38 | Port int `toml:"port"` // 数据库端口 39 | MaxIdleConns int `toml:"max_idle_conns"` // 连接池最大连接数 40 | MaxOpenConns int `toml:"max_open_conns"` // 默认打开连接数 41 | User string `toml:"user"` // 数据库用户名 42 | Passwd string `toml:"passwd"` // 数据库密码 43 | DbName string `toml:"db_name"` // 数据库名 44 | } 45 | 46 | var ( 47 | cfg *Config 48 | ) 49 | 50 | // GetCfg 获取配置 51 | func GetCfg() *Config { 52 | if cfg == nil { 53 | LoadConfig("") 54 | } 55 | return cfg 56 | } 57 | 58 | // LoadConfig 读取配置 59 | func LoadConfig(cfgPath string) (*Config, error) { 60 | cfgPath = getCfgPath(cfgPath) 61 | f, err := os.Open(cfgPath) 62 | if err != nil { 63 | return nil, err 64 | } 65 | defer f.Close() 66 | cfg = new(Config) 67 | if err := toml.NewDecoder(f).Decode(cfg); err != nil { 68 | return nil, err 69 | } 70 | 71 | return cfg, nil 72 | } 73 | 74 | func getCfgPath(cfgPath string) string { 75 | if cfgPath == "" { 76 | cfgPath="/Users/liuqixiang/project/goStudyProject/etcd-ui/etcd-manage-server/bin/config/cfg.toml" 77 | //cfgPath = common.GetRootDir() + "bin/config/cfg.toml" 78 | } 79 | return cfgPath 80 | } 81 | -------------------------------------------------------------------------------- /program/http_ui.go: -------------------------------------------------------------------------------- 1 | package program 2 | 3 | import ( 4 | "log" 5 | "mime" 6 | "net/http" 7 | "path" 8 | "strings" 9 | 10 | "github.com/etcd-manage/etcd-manage-server/program/logger" 11 | "github.com/etcd-manage/etcd-manage-ui/tpls" 12 | gin "github.com/gin-gonic/gin" 13 | ) 14 | 15 | // ui 界面 16 | // 处理静态文件 17 | func (p *Program) handlerStatic(c *gin.Context) { 18 | uri := strings.TrimLeft(c.Request.RequestURI, "/") 19 | uri = strings.TrimRight(uri, "?") 20 | log.Println(uri) 21 | if uri == "ui/" || uri == "ui" { 22 | uri = "dist/index.html" 23 | } else { 24 | uri = strings.Replace(uri, "ui", "dist", 1) 25 | } 26 | // log.Println(uri) 27 | // 读取模版内容 28 | body, err := tpls.Asset(uri) 29 | if err != nil { 30 | logger.Log.Errorw("UI static file reading error", "err", err) 31 | c.Status(http.StatusNotFound) 32 | return 33 | } 34 | mimetype := mime.TypeByExtension(path.Ext(uri)) 35 | if mimetype != "" { 36 | c.Header("Content-Type", mimetype) 37 | } 38 | 39 | c.Writer.Write(body) 40 | } 41 | -------------------------------------------------------------------------------- /program/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "net/url" 5 | "os" 6 | "runtime" 7 | "strings" 8 | 9 | "github.com/shiguanghuxian/etcd-manage/program/common" 10 | "go.uber.org/zap" 11 | "go.uber.org/zap/zapcore" 12 | ) 13 | 14 | // 日志对象 15 | var ( 16 | Log *zap.SugaredLogger 17 | ) 18 | 19 | // InitLogger 日志初始化,用于记录操作日志 20 | func InitLogger(logPath string, isDebug bool) (*zap.SugaredLogger, error) { 21 | infoLogPath := "" 22 | // errorLogPath := "" 23 | if logPath == "" { 24 | logRoot := common.GetRootDir() + "logs" + string(os.PathSeparator) 25 | if isExt, _ := common.PathExists(logRoot); isExt == false { 26 | os.MkdirAll(logRoot, os.ModePerm) 27 | } 28 | infoLogPath = logRoot + "access.log" 29 | } else { 30 | logPath = strings.TrimRight(logPath, string(os.PathSeparator)) 31 | infoLogPath = logPath + string(os.PathSeparator) + "access.log" 32 | } 33 | 34 | // 兼容win根完整路径问题 35 | zap.RegisterSink("winfile", func(u *url.URL) (zap.Sink, error) { 36 | return os.OpenFile(u.Path[1:], os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644) 37 | }) 38 | 39 | cfg := &zap.Config{ 40 | Encoding: "json", 41 | } 42 | cfg.EncoderConfig = zap.NewProductionEncoderConfig() 43 | atom := zap.NewAtomicLevel() 44 | if isDebug == true { 45 | atom.SetLevel(zapcore.DebugLevel) 46 | cfg.OutputPaths = []string{"stdout"} 47 | } else { 48 | atom.SetLevel(zapcore.InfoLevel) 49 | if runtime.GOOS == "windows" { 50 | cfg.OutputPaths = []string{"winfile:///" + infoLogPath} 51 | } else { 52 | cfg.OutputPaths = []string{infoLogPath} 53 | } 54 | } 55 | cfg.Level = atom 56 | logger, err := cfg.Build() 57 | if err != nil { 58 | return nil, err 59 | } 60 | Log = logger.Sugar() 61 | return Log, nil 62 | } 63 | -------------------------------------------------------------------------------- /program/middleware.go: -------------------------------------------------------------------------------- 1 | package program 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "net/url" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "github.com/jinzhu/gorm" 14 | 15 | "github.com/etcd-manage/etcd-manage-server/program/cache" 16 | "github.com/etcd-manage/etcd-manage-server/program/logger" 17 | "github.com/etcd-manage/etcd-manage-server/program/models" 18 | "github.com/gin-gonic/autotls" 19 | gin "github.com/gin-gonic/gin" 20 | ) 21 | 22 | // http服务 23 | 24 | func (p *Program) startAPI() { 25 | router := gin.Default() 26 | 27 | // 跨域问题 28 | router.Use(p.middlewareCORS()) 29 | router.Use(p.middlewareAuth()) 30 | router.Use(p.middlewareRoleAuth()) 31 | 32 | // 设置静态文件目录 33 | router.GET("/ui/*w", p.handlerStatic) 34 | router.GET("/", func(c *gin.Context) { 35 | c.Redirect(301, "/ui") 36 | }) 37 | 38 | // 启动所有版本api 39 | for key, val := range p.vApis { 40 | vAPI := router.Group("/" + key) 41 | vAPI.Use() 42 | val.Register(vAPI) 43 | } 44 | 45 | addr := fmt.Sprintf("%s:%d", p.cfg.HTTP.Address, p.cfg.HTTP.Port) 46 | // 监听 47 | s := &http.Server{ 48 | Addr: addr, 49 | Handler: router, 50 | ReadTimeout: 10 * time.Second, 51 | WriteTimeout: 60 * time.Second, 52 | } 53 | 54 | log.Println("Start HTTP the service:", addr) 55 | var err error 56 | if p.cfg.HTTP.TLSEnable == true { 57 | if p.cfg.HTTP.TLSConfig == nil || p.cfg.HTTP.TLSConfig.CertFile == "" || p.cfg.HTTP.TLSConfig.KeyFile == "" { 58 | log.Fatalln("Enable tls must configure certificate file path") 59 | } 60 | err = s.ListenAndServeTLS(p.cfg.HTTP.TLSConfig.CertFile, p.cfg.HTTP.TLSConfig.KeyFile) 61 | } else if p.cfg.HTTP.TLSEncryptEnable == true { 62 | if len(p.cfg.HTTP.TLSEncryptDomainNames) == 0 { 63 | log.Fatalln("The domain name list cannot be empty") 64 | } 65 | err = autotls.Run(router, p.cfg.HTTP.TLSEncryptDomainNames...) 66 | } else { 67 | err = s.ListenAndServe() 68 | } 69 | if err != nil { 70 | log.Fatalln(err) 71 | } 72 | 73 | } 74 | 75 | // middlewareAuth 获取是否登录 76 | func (p *Program) middlewareAuth() gin.HandlerFunc { 77 | return func(c *gin.Context) { 78 | u, _ := url.ParseRequestURI(c.Request.RequestURI) 79 | if strings.HasPrefix(u.Path, "/v1/passport") == false && strings.HasPrefix(u.Path, "/ui") == false && strings.HasPrefix(u.Path, "/v1/upload") == false { 80 | // log.Println(u.Path) 81 | token := c.Request.Header.Get("Token") 82 | if token == "" { 83 | c.AbortWithStatus(http.StatusUnauthorized) 84 | return 85 | } 86 | // 获取用户信息 87 | key := cache.GetLoginKey(token) 88 | val, exist := cache.DefaultMemCache.Get(key) 89 | if exist == false { 90 | logger.Log.Warnw("用户登录信息不存在") 91 | c.AbortWithStatus(http.StatusUnauthorized) 92 | return 93 | } 94 | // 解析为用户登录信息 95 | user := new(models.UsersModel) 96 | err := json.Unmarshal([]byte(val), user) 97 | if err != nil { 98 | logger.Log.Warnw("用户登录信息解析json错误", "err", err) 99 | c.AbortWithStatus(http.StatusUnauthorized) 100 | return 101 | } 102 | // 存储用户信息到上下文 103 | c.Set("userinfo", user) 104 | } 105 | } 106 | } 107 | 108 | // 跨域中间件 109 | func (p *Program) middlewareCORS() gin.HandlerFunc { 110 | return func(c *gin.Context) { 111 | origin := c.Request.Header.Get("origin") 112 | method := c.Request.Method 113 | 114 | c.Header("Access-Control-Allow-Origin", origin) 115 | c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token, EtcdID") 116 | c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT") 117 | c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type") 118 | c.Header("Access-Control-Allow-Credentials", "true") 119 | 120 | //放行所有OPTIONS方法 121 | if method == "OPTIONS" { 122 | c.AbortWithStatus(http.StatusNoContent) 123 | return 124 | } 125 | // 处理请求 126 | c.Next() 127 | } 128 | } 129 | 130 | // 是否有权限 131 | func (p *Program) middlewareRoleAuth() gin.HandlerFunc { 132 | return func(c *gin.Context) { 133 | // 过滤认证模块 134 | u, _ := url.ParseRequestURI(c.Request.RequestURI) 135 | if strings.HasPrefix(u.Path, "/v1/passport") == true || 136 | strings.HasPrefix(u.Path, "/ui") == true || 137 | strings.HasPrefix(u.Path, "/v1/upload") == true || 138 | strings.HasPrefix(u.Path, "/v1/server") == true { 139 | return 140 | } 141 | // 读取etcdID 142 | etcdId := c.GetHeader("EtcdID") 143 | etcdIdNum, _ := strconv.Atoi(etcdId) 144 | if etcdIdNum == 0 { 145 | c.JSON(http.StatusBadRequest, gin.H{ 146 | "msg": "请选择Etcd服务", 147 | }) 148 | c.Abort() 149 | return 150 | } 151 | // 查询角色权限 GET 请求为只读 152 | userinfoObj, exist := c.Get("userinfo") 153 | if exist == false { 154 | logger.Log.Warnw("用户登录信息不存在") 155 | c.AbortWithStatus(http.StatusUnauthorized) 156 | return 157 | } 158 | userinfo := userinfoObj.(*models.UsersModel) 159 | // get请求为读操作 160 | typ := 1 161 | if c.Request.Method == "GET" { 162 | typ = 0 163 | } 164 | roleEtcdServer := new(models.RoleEtcdServersModel) 165 | err := roleEtcdServer.FirstByRoleIdAndEtcdServerIdAndType(userinfo.RoleId, int32(etcdIdNum), int32(typ)) 166 | if err != nil { 167 | if err == gorm.ErrRecordNotFound { 168 | c.JSON(http.StatusBadRequest, gin.H{ 169 | "msg": "无权限操作此etcd,请更换etcd", 170 | }) 171 | 172 | } else { 173 | c.JSON(http.StatusBadRequest, gin.H{ 174 | "msg": "数据库查询错误", 175 | }) 176 | } 177 | c.Abort() 178 | return 179 | } 180 | c.Next() 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /program/models/client.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | "github.com/etcd-manage/etcd-manage-server/program/config" 10 | _ "github.com/go-sql-driver/mysql" 11 | "github.com/jinzhu/gorm" 12 | ) 13 | 14 | var ( 15 | client *gorm.DB 16 | ) 17 | 18 | // InitClient 客户端创建 19 | func InitClient(dbCfg *config.MySQLConfig) (err error) { 20 | if dbCfg == nil { 21 | err = errors.New("Config is nil") 22 | return 23 | } 24 | // 拼接连接数据库字符串 25 | connStr := fmt.Sprintf("%s:%s@(%s:%d)/%s?charset=utf8&parseTime=True&loc=UTC", 26 | dbCfg.User, 27 | dbCfg.Passwd, 28 | dbCfg.Address, 29 | dbCfg.Port, 30 | dbCfg.DbName) 31 | // 连接数据库 32 | db, err := gorm.Open("mysql", connStr) 33 | if err != nil { 34 | return 35 | } 36 | if dbCfg.Debug == true { 37 | db.Debug() 38 | } 39 | // 禁用表名多元化 40 | db.SingularTable(true) 41 | // 连接池最大连接数 42 | db.DB().SetMaxIdleConns(dbCfg.MaxIdleConns) 43 | // 默认打开连接数 44 | db.DB().SetMaxOpenConns(dbCfg.MaxOpenConns) 45 | // 开启协程ping MySQL数据库查看连接状态 46 | go func() { 47 | for { 48 | // ping 49 | err = db.DB().Ping() 50 | if err != nil { 51 | log.Println("pingdb error ", err) 52 | } 53 | // 间隔5s ping一次 54 | time.Sleep(30 * time.Second) 55 | } 56 | }() 57 | 58 | // 全局变量 59 | client = db 60 | return 61 | } 62 | -------------------------------------------------------------------------------- /program/models/etcdServers.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // EtcdServersModel etcd 服务列表 9 | type EtcdServersModel struct { 10 | ID int32 `json:"id" gorm:"column:id;primary_key"` 11 | Version string `json:"version" gorm:"column:version"` 12 | Name string `json:"name" gorm:"column:name"` 13 | Address string `json:"address" gorm:"column:address"` 14 | TlsEnable string `json:"tls_enable" gorm:"column:tls_enable"` 15 | CertFile string `json:"cert_file" gorm:"column:cert_file"` 16 | KeyFile string `json:"key_file" gorm:"column:key_file"` 17 | CaFile string `json:"ca_file" gorm:"column:ca_file"` 18 | Username string `json:"username" gorm:"column:username"` 19 | Password string `json:"password" gorm:"column:password"` 20 | Desc string `json:"desc" gorm:"column:desc"` 21 | CreatedAt JSONTime `gorm:"column:created_at" json:"created_at"` // 添加时间 22 | UpdatedAt JSONTime `gorm:"column:updated_at" json:"updated_at"` // 更新时间 23 | } 24 | 25 | // TableName 表名 26 | func (EtcdServersModel) TableName() string { 27 | return "etcd_servers" 28 | } 29 | 30 | // All 获取全部 31 | func (m *EtcdServersModel) All(name string, roleId int32) (list []*EtcdServersModel, err error) { 32 | re := new(RoleEtcdServersModel).TableName() 33 | err = client.Table(m.TableName()+" as e").Select("DISTINCT e.id, e.*"). 34 | Joins("LEFT JOIN "+re+" as re on re.etcd_server_id = e.id"). 35 | Where("e.name like ? and re.role_id = ?", fmt.Sprintf("%%%s%%", name), roleId). 36 | Scan(&list).Error 37 | return 38 | } 39 | 40 | // FirstById 根据id查询一个etcd服务 41 | func (m *EtcdServersModel) FirstById(id int32) (one *EtcdServersModel, err error) { 42 | one = new(EtcdServersModel) 43 | err = client.Model(m).Where("id = ?", id).First(one).Error 44 | return 45 | } 46 | 47 | // Insert 添加 48 | func (m *EtcdServersModel) Insert() (err error) { 49 | err = client.Create(m).Error 50 | return 51 | } 52 | 53 | // Update 修改 54 | func (m *EtcdServersModel) Update() (err error) { 55 | edit := make(map[string]interface{}, 0) 56 | js, _ := json.Marshal(m) 57 | json.Unmarshal(js, &edit) 58 | err = client.Model(new(EtcdServersModel)).Where("id = ?", m.ID).Updates(edit).Error 59 | return 60 | } 61 | 62 | // Del 删除 63 | func (m *EtcdServersModel) Del(id int32) (err error) { 64 | err = client.Table(m.TableName()).Where("id = ?", id).Delete(m).Error 65 | new(RoleEtcdServersModel).DelByEtcdId(id) 66 | return 67 | } 68 | -------------------------------------------------------------------------------- /program/models/roleEtcdServers.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jinzhu/gorm" 7 | ) 8 | 9 | const ( 10 | ReadOnly = iota 11 | ReadWrite 12 | ) 13 | 14 | // RoleEtcdServersModel 角色权限表 15 | type RoleEtcdServersModel struct { 16 | Id int32 `gorm:"column:id;primary_key" json:"id"` // 17 | EtcdServerId int32 `gorm:"column:etcd_server_id" json:"etcd_server_id"` // etcd服务id 18 | Type int32 `gorm:"column:type" json:"type"` // 0读 1写 19 | RoleId int32 `gorm:"column:role_id" json:"role_id"` // 角色id 20 | CreatedAt JSONTime `gorm:"column:created_at" json:"created_at"` // 添加时间 21 | UpdatedAt JSONTime `gorm:"column:updated_at" json:"updated_at"` // 更新时间 22 | 23 | } 24 | 25 | // TableName 获取表名 26 | func (RoleEtcdServersModel) TableName() string { 27 | return gorm.DefaultTableNameHandler(nil, "role_etcd_servers") 28 | } 29 | 30 | // FirstByRoleIdAndEtcdServerIdAndType 根据role_id、etcd_server_id和type查询一条数据 31 | func (m *RoleEtcdServersModel) FirstByRoleIdAndEtcdServerIdAndType(roleId, etcdServerId, typ int32) (err error) { 32 | err = client.Table(m.TableName()).Where("role_id = ? and etcd_server_id = ? and type >= ?", roleId, etcdServerId, typ).First(m).Error 33 | return 34 | } 35 | 36 | // UpByEtcdId 根据etcd_id更新角色 37 | func (m *RoleEtcdServersModel) UpByEtcdId(list []*AllByEtcdIdData) (err error) { 38 | tx := client.Begin() 39 | defer func() { 40 | if err != nil { 41 | tx.Rollback() 42 | } else { 43 | tx.Commit() 44 | } 45 | }() 46 | // 先删除再插入 47 | err = tx.Table(m.TableName()).Where("etcd_server_id = ?", list[0].EtcdServerId).Delete(m).Error 48 | if err != nil { 49 | return 50 | } 51 | now := JSONTime(time.Now()) 52 | for _, v := range list { 53 | typ := -1 54 | if v.Write == 1 { 55 | typ = 1 56 | } else if v.Read == 1 { 57 | typ = 0 58 | } 59 | one := &RoleEtcdServersModel{ 60 | EtcdServerId: v.EtcdServerId, 61 | Type: int32(typ), 62 | RoleId: v.RoleId, 63 | CreatedAt: now, 64 | UpdatedAt: now, 65 | } 66 | err = tx.Create(one).Error 67 | if err != nil { 68 | return 69 | } 70 | } 71 | return 72 | } 73 | 74 | type AllByEtcdIdData struct { 75 | EtcdServerId int32 `gorm:"column:etcd_server_id" json:"etcd_server_id"` // etcd服务id 76 | RoleId int32 `gorm:"column:role_id" json:"role_id"` // 角色id 77 | Name string `gorm:"column:name" json:"name"` 78 | Type int32 `gorm:"column:type" json:"type"` 79 | Read int32 `gorm:"column:read" json:"read"` 80 | Write int32 `gorm:"column:write" json:"write"` 81 | } 82 | 83 | // AllByEtcdId 查询etcd服务权限配置列表 84 | func (m *RoleEtcdServersModel) AllByEtcdId(etcdId int32) (list []*AllByEtcdIdData, err error) { 85 | err = client.Table(m.TableName()+" as re").Select("r.name, re.type, re.etcd_server_id, re.role_id"). 86 | Joins(" join "+new(RolesModel).TableName()+" as r on r.id = re.role_id"). 87 | Where("etcd_server_id = ?", etcdId). 88 | Scan(&list).Error 89 | if err == nil { 90 | for _, v := range list { 91 | if v.Type == 1 { 92 | v.Write = 1 93 | v.Read = 1 94 | } else if v.Type == 0 { 95 | v.Write = 0 96 | v.Read = 1 97 | } else { 98 | v.Write = 0 99 | v.Read = 0 100 | } 101 | } 102 | } 103 | rList, _err := new(RolesModel).All() 104 | if _err != nil { 105 | err = _err 106 | return 107 | } 108 | if len(rList) > len(list) { 109 | for _, v := range rList { 110 | exist := false 111 | for _, v1 := range list { 112 | if v1.RoleId == v.Id { 113 | exist = true 114 | } 115 | } 116 | if exist == false { 117 | list = append(list, &AllByEtcdIdData{ 118 | EtcdServerId: etcdId, 119 | RoleId: v.Id, 120 | Name: v.Name, 121 | Type: -1, 122 | Read: 0, 123 | Write: 0, 124 | }) 125 | } 126 | } 127 | } 128 | return 129 | } 130 | 131 | // Save 保存角色信息 132 | func (m *RoleEtcdServersModel) Save() (err error) { 133 | err = client.Table(m.TableName()).Create(m).Error 134 | return 135 | } 136 | 137 | // DelByEtcdId 删除 138 | func (m *RoleEtcdServersModel) DelByEtcdId(etcdId int32) (err error) { 139 | err = client.Table(m.TableName()).Where("etcd_server_id = ?", etcdId).Delete(m).Error 140 | return 141 | } 142 | -------------------------------------------------------------------------------- /program/models/roles.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | // RolesModel 角色表 8 | type RolesModel struct { 9 | Id int32 `gorm:"column:id;primary_key" json:"id"` // 10 | Name string `gorm:"column:name" json:"name"` // 角色名 11 | CreatedAt JSONTime `gorm:"column:created_at" json:"created_at"` // 添加时间 12 | UpdatedAt JSONTime `gorm:"column:updated_at" json:"updated_at"` // 更新时间 13 | 14 | } 15 | 16 | // TableName 获取表名 17 | func (RolesModel) TableName() string { 18 | return gorm.DefaultTableNameHandler(nil, "roles") 19 | } 20 | 21 | // All 查询全部角色 22 | func (m *RolesModel) All() (list []*RolesModel, err error) { 23 | err = client.Table(m.TableName()).Scan(&list).Error 24 | return 25 | } 26 | 27 | // Save 保存 28 | func (m *RolesModel) Save() (err error) { 29 | err = client.Table(m.TableName()).Save(m).Error 30 | return 31 | } 32 | 33 | // Del 删除 34 | func (m *RolesModel) Del(id int32) (err error) { 35 | err = client.Table(m.TableName()).Where("id = ?", id).Delete(m).Error 36 | return 37 | } 38 | -------------------------------------------------------------------------------- /program/models/time.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "database/sql/driver" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | /* 自定义时间格式 */ 10 | var timeFormart = "2006-01-02 15:04:05" // time.RFC3339 11 | 12 | // JSONTime 时间格式别名 13 | type JSONTime time.Time 14 | 15 | // UnmarshalJSON 字节转为JSONTime对象 16 | func (t *JSONTime) UnmarshalJSON(data []byte) (err error) { 17 | now, err := time.ParseInLocation(`"`+timeFormart+`"`, string(data), time.UTC) 18 | *t = JSONTime(now) 19 | return 20 | } 21 | 22 | // MarshalJSON 将时间对象转为字节 23 | func (t JSONTime) MarshalJSON() ([]byte, error) { 24 | b := make([]byte, 0, len(timeFormart)+2) 25 | b = append(b, '"') 26 | b = time.Time(t).Local().AppendFormat(b, timeFormart) 27 | b = append(b, '"') 28 | return b, nil 29 | } 30 | 31 | // String 格式化为文本 32 | func (t JSONTime) String() string { 33 | return time.Time(t).Format(timeFormart) 34 | } 35 | 36 | // Format 格式化函数 37 | func (t JSONTime) Format(format string) string { 38 | return time.Time(t).Format(timeFormart) 39 | } 40 | 41 | // Value insert timestamp into mysql need this function. 42 | func (t JSONTime) Value() (driver.Value, error) { 43 | var zeroTime time.Time 44 | var ti = time.Time(t) 45 | if ti.UnixNano() == zeroTime.UnixNano() { 46 | return nil, nil 47 | } 48 | return ti, nil 49 | } 50 | 51 | // Scan valueof time.Time 52 | func (t *JSONTime) Scan(v interface{}) error { 53 | value, ok := v.(time.Time) 54 | if ok { 55 | *t = JSONTime(value) 56 | return nil 57 | } 58 | return fmt.Errorf("can not convert %v to timestamp", v) 59 | } 60 | -------------------------------------------------------------------------------- /program/models/users.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/jinzhu/gorm" 7 | ) 8 | 9 | // UsersModel 用户表 10 | type UsersModel struct { 11 | Id int32 `gorm:"column:id;primary_key" json:"id"` // 12 | Username string `gorm:"column:username" json:"username"` // 用户名 13 | Password string `gorm:"column:password" json:"password"` // 密码 14 | Email string `gorm:"column:email" json:"email"` // 邮箱 15 | RoleId int32 `gorm:"column:role_id" json:"role_id"` // 角色id 16 | CreatedAt JSONTime `gorm:"column:created_at" json:"created_at"` // 添加时间 17 | UpdatedAt JSONTime `gorm:"column:updated_at" json:"updated_at"` // 更新时间 18 | Token string `gorm:"-" json:"token"` // 登录token 19 | } 20 | 21 | // TableName 获取表名 22 | func (UsersModel) TableName() string { 23 | return gorm.DefaultTableNameHandler(nil, "users") 24 | } 25 | 26 | // UsersJoinRoleModel 用户列表带角色名 27 | type UsersJoinRoleModel struct { 28 | RoleName string `gorm:"column:role_name" json:"role_name"` 29 | UsersModel 30 | } 31 | 32 | // FirstByUsernameAndPassword 根据用户名密码查询数据 33 | func (m *UsersModel) FirstByUsernameAndPassword(username, password string) (err error) { 34 | err = client.Table(m.TableName()).Where("username = ? and password = ?", username, password).First(m).Error 35 | return 36 | } 37 | 38 | // List 分页列表 39 | func (m *UsersModel) List(userId, roleId int32, name string, offset, limit int) (list []*UsersJoinRoleModel, err error) { 40 | where, params := m.listWhere(userId, roleId, name, offset, limit) 41 | err = client.Table(m.TableName()+" as u").Select("u.*, r.name as role_name"). 42 | Joins(fmt.Sprintf("left join %s as r on u.role_id = r.id", new(RolesModel).TableName())). 43 | Where(where, params...). 44 | Offset(offset). 45 | Limit(limit). 46 | Scan(&list).Error 47 | return 48 | } 49 | 50 | // ListCount 分页总数据量 51 | func (m *UsersModel) ListCount(userId, roleId int32, name string, offset, limit int) (_c int32, err error) { 52 | where, params := m.listWhere(userId, roleId, name, offset, limit) 53 | err = client.Table(m.TableName()+" as u").Where(where, params...).Count(&_c).Error 54 | return 55 | } 56 | 57 | // 分页查询条件组织 58 | func (m *UsersModel) listWhere(userId, roleId int32, name string, offset, limit int) (where string, params []interface{}) { 59 | where = "" 60 | params = make([]interface{}, 0) 61 | if userId > 0 { 62 | where = "u.id = ? " 63 | params = append(params, userId) 64 | } 65 | if roleId > 0 { 66 | if where != "" { 67 | where += " and " 68 | } 69 | where += " u.role_id = ? " 70 | params = append(params, roleId) 71 | } 72 | if name != "" { 73 | if where != "" { 74 | where += " and " 75 | } 76 | where += " u.username like ?" 77 | params = append(params, "%"+name+"%") 78 | } 79 | return 80 | } 81 | 82 | // Save 保存 83 | func (m *UsersModel) Save(omit ...string) (err error) { 84 | err = client.Table(m.TableName()).Omit(omit...).Save(m).Error 85 | return 86 | } 87 | 88 | // Del 删除 89 | func (m *UsersModel) Del(id int32) (err error) { 90 | err = client.Table(m.TableName()).Where("id = ?", id).Delete(m).Error 91 | return 92 | } 93 | -------------------------------------------------------------------------------- /program/program.go: -------------------------------------------------------------------------------- 1 | package program 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os/exec" 7 | "runtime" 8 | "time" 9 | 10 | "github.com/etcd-manage/etcd-manage-server/program/api" 11 | v1 "github.com/etcd-manage/etcd-manage-server/program/api/v1" 12 | "github.com/etcd-manage/etcd-manage-server/program/config" 13 | "github.com/etcd-manage/etcd-manage-server/program/logger" 14 | "github.com/etcd-manage/etcd-manage-server/program/models" 15 | ) 16 | 17 | // Program 主程序 18 | type Program struct { 19 | cfg *config.Config // 配置 20 | s *http.Server // http服务-用于程序结束stop 21 | vApis map[string]api.API // 多版本api启动运行 22 | } 23 | 24 | // New 创建主程序 25 | func New() (*Program, error) { 26 | // 配置文件 27 | cfg, err := config.LoadConfig("") 28 | if err != nil { 29 | fmt.Println(err) 30 | return nil, err 31 | } 32 | 33 | // 日志对象 34 | _, err = logger.InitLogger(cfg.LogPath, cfg.Debug) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | // 连接数据库 40 | err = models.InitClient(cfg.DB) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | // 注册api对象 46 | apis := make(map[string]api.API, 0) 47 | apis["v1"] = new(v1.APIV1) 48 | 49 | // js, _ := json.Marshal(cfg) 50 | // fmt.Println(string(js)) 51 | 52 | return &Program{ 53 | cfg: cfg, 54 | vApis: apis, 55 | }, nil 56 | } 57 | 58 | // Run 启动程序 59 | func (p *Program) Run() error { 60 | // 启动http服务 61 | go p.startAPI() 62 | 63 | // 打开浏览器 64 | go func() { 65 | time.Sleep(100 * time.Millisecond) 66 | openUrl(fmt.Sprintf("http://127.0.0.1:%d/ui/", p.cfg.HTTP.Port)) 67 | }() 68 | 69 | return nil 70 | } 71 | 72 | // Stop 停止服务 73 | func (p *Program) Stop() { 74 | if p.s != nil { 75 | p.s.Close() 76 | } 77 | } 78 | 79 | func openUrl(uri string) error { 80 | var ( 81 | commands = map[string]string{ 82 | "windows": "start", 83 | "darwin": "open", 84 | "linux": "xdg-open", 85 | } 86 | ) 87 | run, ok := commands[runtime.GOOS] //获取平台信息 88 | 89 | if !ok { 90 | return fmt.Errorf("don't know how to open things on %s platform", runtime.GOOS) 91 | } 92 | cmd := exec.Command(run, uri) 93 | return cmd.Start() 94 | } 95 | -------------------------------------------------------------------------------- /program/publicFunc/publicFunc.go: -------------------------------------------------------------------------------- 1 | package publicFunc 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/etcd-manage/etcd-manage-server/program/common" 7 | 8 | "github.com/etcd-manage/etcd-manage-server/etcdsdk" 9 | "github.com/etcd-manage/etcd-manage-server/etcdsdk/model" 10 | "github.com/etcd-manage/etcd-manage-server/program/logger" 11 | "github.com/etcd-manage/etcd-manage-server/program/models" 12 | ) 13 | 14 | type PublicF struct { 15 | } 16 | 17 | func (p PublicF) GetEtcdClient(etcdId int) (client etcdsdk.EtcdV3, err error) { 18 | // 查询etcd服务信息 19 | etcdOne := new(models.EtcdServersModel) 20 | etcdOne, err = etcdOne.FirstById(int32(etcdId)) 21 | if err != nil { 22 | logger.Log.Errorw("获取etcd服务信息错误", "EtcdID", etcdId, "err", err) 23 | } 24 | if etcdOne.TlsEnable == "true" { 25 | if !strings.HasPrefix(etcdOne.Address, "https") { 26 | etcdOne.Address = strings.ReplaceAll(etcdOne.Address, "http", "https") 27 | } 28 | } else { 29 | if strings.HasPrefix(etcdOne.Address, "https") { 30 | etcdOne.Address = strings.ReplaceAll(etcdOne.Address, "https", "http") 31 | } 32 | } 33 | // 连接etcd 34 | cfg := model.Config{ 35 | EtcdId: int32(etcdId), 36 | Version: etcdOne.Version, 37 | Address: strings.Split(etcdOne.Address, ","), 38 | TlsEnable: etcdOne.TlsEnable, 39 | CertFile: common.Base64Decode(etcdOne.CertFile), 40 | KeyFile: common.Base64Decode(etcdOne.KeyFile), 41 | CaFile: common.Base64Decode(etcdOne.CaFile), 42 | } 43 | if client, err = etcdsdk.NewClient(cfg); err != nil { 44 | logger.Log.Errorf("初始化etcd client 失败:%s", err.Error()) 45 | } 46 | return 47 | } 48 | -------------------------------------------------------------------------------- /tupian/etcd key树以及value展示图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comqx/k8s-etcd-ui/e860824e994da8c48aea5cd3e9283f374828a8c9/tupian/etcd key树以及value展示图.png -------------------------------------------------------------------------------- /tupian/etcd-key树图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comqx/k8s-etcd-ui/e860824e994da8c48aea5cd3e9283f374828a8c9/tupian/etcd-key树图.png -------------------------------------------------------------------------------- /tupian/etcd管理页面.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comqx/k8s-etcd-ui/e860824e994da8c48aea5cd3e9283f374828a8c9/tupian/etcd管理页面.png -------------------------------------------------------------------------------- /tupian/多个etcd集群健康图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comqx/k8s-etcd-ui/e860824e994da8c48aea5cd3e9283f374828a8c9/tupian/多个etcd集群健康图.png -------------------------------------------------------------------------------- /tupian/添加etcd界面.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comqx/k8s-etcd-ui/e860824e994da8c48aea5cd3e9283f374828a8c9/tupian/添加etcd界面.png --------------------------------------------------------------------------------