From 857bcdba16b1407e1afe6ff2a8ba57b9097a7253 Mon Sep 17 00:00:00 2001 From: marvzhang Date: Thu, 2 Jan 2020 11:16:42 +0800 Subject: [PATCH 01/31] fixed https://github.com/crawlab-team/crawlab/issues/391 --- backend/services/node.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/services/node.go b/backend/services/node.go index d14ce4ae..6ff68b2d 100644 --- a/backend/services/node.go +++ b/backend/services/node.go @@ -12,6 +12,7 @@ import ( "encoding/json" "fmt" "github.com/apex/log" + "github.com/globalsign/mgo" "github.com/globalsign/mgo/bson" "github.com/gomodule/redigo/redis" "runtime/debug" @@ -116,7 +117,7 @@ func handleNodeInfo(key string, data *Data) { defer s.Close() var node model.Node - if err := c.Find(bson.M{"key": key}).One(&node); err != nil { + if err := c.Find(bson.M{"key": key}).One(&node); err != nil && err != mgo.ErrNotFound { // 数据库不存在该节点 node = model.Node{ Key: key, From af93733bc5aed3b3c7624376cc0916416099b2d1 Mon Sep 17 00:00:00 2001 From: marvzhang Date: Thu, 2 Jan 2020 12:51:21 +0800 Subject: [PATCH 02/31] fixed https://github.com/crawlab-team/crawlab/issues/391 --- backend/services/node.go | 48 ++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/backend/services/node.go b/backend/services/node.go index 6ff68b2d..515ce9c9 100644 --- a/backend/services/node.go +++ b/backend/services/node.go @@ -117,7 +117,7 @@ func handleNodeInfo(key string, data *Data) { defer s.Close() var node model.Node - if err := c.Find(bson.M{"key": key}).One(&node); err != nil && err != mgo.ErrNotFound { + if err := c.Find(bson.M{"key": key}).One(&node); err != nil && err == mgo.ErrNotFound { // 数据库不存在该节点 node = model.Node{ Key: key, @@ -134,7 +134,7 @@ func handleNodeInfo(key string, data *Data) { log.Errorf(err.Error()) return } - } else { + } else if node.Key != "" { // 数据库存在该节点 node.Status = constants.StatusOnline node.UpdateTs = time.Now() @@ -169,33 +169,27 @@ func UpdateNodeData() { return } - //先获取所有Redis的nodekey - list, _ := database.RedisClient.HKeys("nodes") - - if i := utils.Contains(list, key); i == false { - // 构造节点数据 - data := Data{ - Key: key, - Mac: mac, - Ip: ip, - Master: model.IsMaster(), - UpdateTs: time.Now(), - UpdateTsUnix: time.Now().Unix(), - } - - // 注册节点到Redis - dataBytes, err := json.Marshal(&data) - if err != nil { - log.Errorf(err.Error()) - debug.PrintStack() - return - } - if err := database.RedisClient.HSet("nodes", key, utils.BytesToString(dataBytes)); err != nil { - log.Errorf(err.Error()) - return - } + // 构造节点数据 + data := Data{ + Key: key, + Mac: mac, + Ip: ip, + Master: model.IsMaster(), + UpdateTs: time.Now(), + UpdateTsUnix: time.Now().Unix(), } + // 注册节点到Redis + dataBytes, err := json.Marshal(&data) + if err != nil { + log.Errorf(err.Error()) + debug.PrintStack() + return + } + if err := database.RedisClient.HSet("nodes", key, utils.BytesToString(dataBytes)); err != nil { + log.Errorf(err.Error()) + return + } } func MasterNodeCallback(message redis.Message) (err error) { From 836dc1270343779613a9e8f7132b1cb9245db8fa Mon Sep 17 00:00:00 2001 From: marvzhang Date: Thu, 2 Jan 2020 15:53:53 +0800 Subject: [PATCH 03/31] =?UTF-8?q?=E5=8A=A0=E5=85=A5RPC=E6=9C=BA=E5=88=B6?= =?UTF-8?q?=EF=BC=8C=E5=AE=8C=E6=88=90=E9=83=A8=E5=88=86=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/constants/rpc.go | 8 ++ backend/database/redis.go | 29 ++++- backend/main.go | 11 +- backend/routes/system.go | 78 ++++++++++++-- backend/services/rpc.go | 214 +++++++++++++++++++++++++++++++++++++ backend/services/system.go | 106 +++++++++++++----- 6 files changed, 408 insertions(+), 38 deletions(-) create mode 100644 backend/constants/rpc.go create mode 100644 backend/services/rpc.go diff --git a/backend/constants/rpc.go b/backend/constants/rpc.go new file mode 100644 index 00000000..117ed5c8 --- /dev/null +++ b/backend/constants/rpc.go @@ -0,0 +1,8 @@ +package constants + +const ( + RpcInstallDep = "install_dep" + RpcInstallLang = "install_lang" + RpcGetDepList = "get_dep_list" + RpcGetInstalledDepList = "get_installed_dep_list" +) diff --git a/backend/database/redis.go b/backend/database/redis.go index b165aaa3..1a488767 100644 --- a/backend/database/redis.go +++ b/backend/database/redis.go @@ -42,6 +42,17 @@ func (r *Redis) RPush(collection string, value interface{}) error { return nil } +func (r *Redis) LPush(collection string, value interface{}) error { + c := r.pool.Get() + defer utils.Close(c) + + if _, err := c.Do("RPUSH", collection, value); err != nil { + debug.PrintStack() + return err + } + return nil +} + func (r *Redis) LPop(collection string) (string, error) { c := r.pool.Get() defer utils.Close(c) @@ -96,6 +107,20 @@ func (r *Redis) HKeys(collection string) ([]string, error) { return value, nil } +func (r *Redis) BRPop(collection string, timeout int) (string, error) { + if timeout <= 0 { + timeout = 60 + } + c := r.pool.Get() + defer utils.Close(c) + + value, err2 := redis.String(c.Do("BRPOP", collection, timeout)) + if err2 != nil { + return value, err2 + } + return value, nil +} + func NewRedisPool() *redis.Pool { var address = viper.GetString("redis.address") var port = viper.GetString("redis.port") @@ -112,8 +137,8 @@ func NewRedisPool() *redis.Pool { Dial: func() (conn redis.Conn, e error) { return redis.DialURL(url, redis.DialConnectTimeout(time.Second*10), - redis.DialReadTimeout(time.Second*10), - redis.DialWriteTimeout(time.Second*15), + redis.DialReadTimeout(time.Second*600), + redis.DialWriteTimeout(time.Second*10), ) }, TestOnBorrow: func(c redis.Conn, t time.Time) error { diff --git a/backend/main.go b/backend/main.go index 6a807331..8968027c 100644 --- a/backend/main.go +++ b/backend/main.go @@ -110,12 +110,20 @@ func main() { // 初始化依赖服务 if err := services.InitDepsFetcher(); err != nil { - log.Error("init user service error:" + err.Error()) + log.Error("init dependency fetcher error:" + err.Error()) debug.PrintStack() panic(err) } log.Info("initialized dependency fetcher successfully") + // 初始化RPC服务 + if err := services.InitRpcService(); err != nil { + log.Error("init rpc service error:" + err.Error()) + debug.PrintStack() + panic(err) + } + log.Info("initialized rpc service successfully") + // 以下为主节点服务 if model.IsMaster() { // 中间件 @@ -139,6 +147,7 @@ func main() { authGroup.GET("/nodes/:id/langs", routes.GetLangList) // 节点语言环境列表 authGroup.GET("/nodes/:id/deps", routes.GetDepList) // 节点第三方依赖列表 authGroup.GET("/nodes/:id/deps/installed", routes.GetInstalledDepList) // 节点已安装第三方依赖列表 + authGroup.POST("/nodes/:id/deps/install", routes.InstallDep) // 节点安装依赖 // 爬虫 authGroup.GET("/spiders", routes.GetSpiderList) // 爬虫列表 authGroup.GET("/spiders/:id", routes.GetSpider) // 爬虫详情 diff --git a/backend/routes/system.go b/backend/routes/system.go index bcd186f8..409168d2 100644 --- a/backend/routes/system.go +++ b/backend/routes/system.go @@ -26,12 +26,21 @@ func GetDepList(c *gin.Context) { var depList []entity.Dependency if lang == constants.Python { - list, err := services.GetPythonDepList(nodeId, depName) - if err != nil { - HandleError(http.StatusInternalServerError, c, err) - return + if services.IsMasterNode(nodeId) { + list, err := services.GetPythonLocalDepList(nodeId, depName) + if err != nil { + HandleError(http.StatusInternalServerError, c, err) + return + } + depList = list + } else { + list, err := services.GetPythonRemoteDepList(nodeId, depName) + if err != nil { + HandleError(http.StatusInternalServerError, c, err) + return + } + depList = list } - depList = list } else { HandleErrorF(http.StatusBadRequest, c, fmt.Sprintf("%s is not implemented", lang)) return @@ -49,12 +58,21 @@ func GetInstalledDepList(c *gin.Context) { lang := c.Query("lang") var depList []entity.Dependency if lang == constants.Python { - list, err := services.GetPythonInstalledDepList(nodeId) - if err != nil { - HandleError(http.StatusInternalServerError, c, err) - return + if services.IsMasterNode(nodeId) { + list, err := services.GetPythonLocalInstalledDepList(nodeId) + if err != nil { + HandleError(http.StatusInternalServerError, c, err) + return + } + depList = list + } else { + list, err := services.GetPythonRemoteInstalledDepList(nodeId) + if err != nil { + HandleError(http.StatusInternalServerError, c, err) + return + } + depList = list } - depList = list } else { HandleErrorF(http.StatusBadRequest, c, fmt.Sprintf("%s is not implemented", lang)) return @@ -108,3 +126,43 @@ func GetAllDepList(c *gin.Context) { Data: returnList, }) } + +func InstallDep(c *gin.Context) { + type ReqBody struct { + Lang string `json:"lang"` + DepName string `json:"dep_name"` + } + + nodeId := c.Param("id") + + var reqBody ReqBody + if err := c.ShouldBindJSON(&reqBody); err != nil { + HandleError(http.StatusBadRequest, c, err) + } + + if reqBody.Lang == constants.Python { + if services.IsMasterNode(nodeId) { + _, err := services.InstallPythonLocalDep(reqBody.DepName) + if err != nil { + HandleError(http.StatusInternalServerError, c, err) + return + } + } else { + _, err := services.InstallPythonRemoteDep(nodeId, reqBody.DepName) + if err != nil { + HandleError(http.StatusInternalServerError, c, err) + return + } + } + } else { + HandleErrorF(http.StatusBadRequest, c, fmt.Sprintf("%s is not implemented", reqBody.Lang)) + return + } + + // TODO: check if install is successful + + c.JSON(http.StatusOK, Response{ + Status: "ok", + Message: "success", + }) +} diff --git a/backend/services/rpc.go b/backend/services/rpc.go new file mode 100644 index 00000000..cc0762bb --- /dev/null +++ b/backend/services/rpc.go @@ -0,0 +1,214 @@ +package services + +import ( + "crawlab/constants" + "crawlab/database" + "crawlab/entity" + "crawlab/model" + "encoding/json" + "fmt" + "github.com/apex/log" + uuid "github.com/satori/go.uuid" + "runtime/debug" +) + +type RpcMessage struct { + Id string `json:"id"` + Method string `json:"method"` + Params string `json:"params"` + Result string `json:"result"` +} + +func RpcServerInstallLang(msg RpcMessage) RpcMessage { + // install dep rpc + return msg +} + +func RpcServerInstallDep(msg RpcMessage) RpcMessage { + lang := GetRpcParam("lang", msg.Params) + depName := GetRpcParam("dep_name", msg.Params) + if lang == constants.Python { + output, _ := InstallPythonLocalDep(depName) + msg.Result = output + } + return msg +} + +func RpcClientInstallDep(nodeId string, lang string, depName string) (output string, err error) { + params := map[string]string{} + params["lang"] = lang + params["dep_name"] = depName + + data, err := RpcClientFunc(nodeId, params, 10)() + if err != nil { + return + } + + output = data.(string) + + return +} + +func RpcServerGetDepList(nodeId string, msg RpcMessage) RpcMessage { + lang := GetRpcParam("lang", msg.Params) + searchDepName := GetRpcParam("search_dep_name", msg.Params) + if lang == constants.Python { + depList, _ := GetPythonLocalDepList(nodeId, searchDepName) + resultStr, _ := json.Marshal(depList) + msg.Result = string(resultStr) + } + return msg +} + +func RpcClientGetDepList(nodeId string, lang string, searchDepName string) (list []entity.Dependency, err error) { + params := map[string]string{} + params["lang"] = lang + params["search_dep_name"] = searchDepName + + data, err := RpcClientFunc(nodeId, params, 30)() + if err != nil { + return + } + + list = data.([]entity.Dependency) + + return +} + +func RpcServerGetInstalledDepList(nodeId string, msg RpcMessage) RpcMessage { + lang := GetRpcParam("lang", msg.Params) + if lang == constants.Python { + depList, _ := GetPythonLocalInstalledDepList(nodeId) + resultStr, _ := json.Marshal(depList) + msg.Result = string(resultStr) + } + return msg +} + +func RpcClientGetInstalledDepList(nodeId string, lang string) (list []entity.Dependency, err error) { + params := map[string]string{} + params["lang"] = lang + + data, err := RpcClientFunc(nodeId, params, 10)() + if err != nil { + return + } + + list = data.([]entity.Dependency) + + return +} + +func RpcClientFunc(nodeId string, params interface{}, timeout int) func() (interface{}, error) { + return func() (data interface{}, err error) { + // 请求ID + id := uuid.NewV4().String() + + // 构造RPC消息 + msg := RpcMessage{ + Id: id, + Method: constants.RpcGetDepList, + Params: ObjectToString(params), + Result: "", + } + + // 发送RPC消息 + if err := database.RedisClient.LPush(fmt.Sprintf("rpc:%s", nodeId), ObjectToString(msg)); err != nil { + return data, err + } + + // 获取RPC回复消息 + dataStr, err := database.RedisClient.BRPop(fmt.Sprintf("rpc:%s", nodeId), timeout) + if err != nil { + return data, err + } + + // 反序列化消息 + if err := json.Unmarshal([]byte(dataStr), &msg); err != nil { + return data, err + } + + // 反序列化列表 + if err := json.Unmarshal([]byte(msg.Result), &data); err != nil { + return data, err + } + + return data, err + } +} + +func GetRpcParam(key string, params interface{}) string { + var paramsObj map[string]string + if err := json.Unmarshal([]byte(params.(string)), ¶msObj); err != nil { + return "" + } + return paramsObj[key] +} + +func ObjectToString(params interface{}) string { + str, _ := json.Marshal(params) + return string(str) +} + +var IsRpcStopped = false + +func StopRpcService() { + IsRpcStopped = true +} + +func InitRpcService() error { + go func() { + for { + // 获取当前节点 + node, err := model.GetCurrentNode() + if err != nil { + log.Errorf(err.Error()) + debug.PrintStack() + continue + } + + // 获取获取消息队列信息 + dataStr, err := database.RedisClient.BRPop(fmt.Sprintf("rpc:%s", node.Id.Hex()), 300) + if err != nil { + log.Errorf(err.Error()) + debug.PrintStack() + continue + } + + // 反序列化消息 + var msg RpcMessage + if err := json.Unmarshal([]byte(dataStr), &msg); err != nil { + log.Errorf(err.Error()) + debug.PrintStack() + continue + } + + // 根据Method调用本地方法 + var replyMsg RpcMessage + if msg.Method == constants.RpcInstallDep { + replyMsg = RpcServerInstallDep(msg) + } else if msg.Method == constants.RpcInstallLang { + replyMsg = RpcServerInstallLang(msg) + } else if msg.Method == constants.RpcGetDepList { + replyMsg = RpcServerGetDepList(node.Id.Hex(), msg) + } else if msg.Method == constants.RpcGetInstalledDepList { + replyMsg = RpcServerGetInstalledDepList(node.Id.Hex(), msg) + } else { + continue + } + + // 发送返回消息 + if err := database.RedisClient.LPush(fmt.Sprintf("rpc:%s", node.Id.Hex()), ObjectToString(replyMsg)); err != nil { + log.Errorf(err.Error()) + debug.PrintStack() + continue + } + + // 如果停止RPC服务,则返回 + if IsRpcStopped { + return + } + } + }() + return nil +} diff --git a/backend/services/system.go b/backend/services/system.go index 045ecbff..fc7e0bad 100644 --- a/backend/services/system.go +++ b/backend/services/system.go @@ -41,8 +41,10 @@ func (s PythonDepNameDictSlice) Len() int { return len(s) } func (s PythonDepNameDictSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s PythonDepNameDictSlice) Less(i, j int) bool { return s[i].Weight > s[j].Weight } +// 系统信息 chan 映射 var SystemInfoChanMap = utils.NewChanMap() +// 从远端获取系统信息 func GetRemoteSystemInfo(nodeId string) (sysInfo entity.SystemInfo, err error) { // 发送消息 msg := entity.NodeMessage{ @@ -70,6 +72,7 @@ func GetRemoteSystemInfo(nodeId string) (sysInfo entity.SystemInfo, err error) { return sysInfo, nil } +// 获取系统信息 func GetSystemInfo(nodeId string) (sysInfo entity.SystemInfo, err error) { if IsMasterNode(nodeId) { sysInfo, err = model.GetLocalSystemInfo() @@ -79,6 +82,7 @@ func GetSystemInfo(nodeId string) (sysInfo entity.SystemInfo, err error) { return } +// 获取语言列表 func GetLangList(nodeId string) []entity.Lang { list := []entity.Lang{ {Name: "Python", ExecutableName: "python", ExecutablePath: "/usr/local/bin/python", DepExecutablePath: "/usr/local/bin/pip"}, @@ -91,6 +95,7 @@ func GetLangList(nodeId string) []entity.Lang { return list } +// 根据语言名获取语言实例 func GetLangFromLangName(nodeId string, name string) entity.Lang { langList := GetLangList(nodeId) for _, lang := range langList { @@ -101,7 +106,22 @@ func GetLangFromLangName(nodeId string, name string) entity.Lang { return entity.Lang{} } -func GetPythonDepList(nodeId string, searchDepName string) ([]entity.Dependency, error) { +// 是否已安装该依赖 +func IsInstalledLang(nodeId string, lang entity.Lang) bool { + sysInfo, err := GetSystemInfo(nodeId) + if err != nil { + return false + } + for _, exec := range sysInfo.Executables { + if exec.Path == lang.ExecutablePath { + return true + } + } + return false +} + +// 获取Python本地依赖列表 +func GetPythonLocalDepList(nodeId string, searchDepName string) ([]entity.Dependency, error) { var list []entity.Dependency // 先从 Redis 获取 @@ -130,7 +150,7 @@ func GetPythonDepList(nodeId string, searchDepName string) ([]entity.Dependency, } // 获取已安装依赖 - installedDepList, err := GetPythonInstalledDepList(nodeId) + installedDepList, err := GetPythonLocalInstalledDepList(nodeId) if err != nil { return list, err } @@ -170,6 +190,16 @@ func GetPythonDepList(nodeId string, searchDepName string) ([]entity.Dependency, return list, nil } +// 获取Python远端依赖列表 +func GetPythonRemoteDepList(nodeId string, searchDepName string) ([]entity.Dependency, error) { + depList, err := RpcClientGetDepList(nodeId, constants.Python, searchDepName) + if err != nil { + return depList, err + } + return depList, nil +} + +// 从Redis获取Python依赖列表 func GetPythonDepListFromRedis() ([]string, error) { var list []string @@ -192,28 +222,7 @@ func GetPythonDepListFromRedis() ([]string, error) { return list, nil } -func IsInstalledLang(nodeId string, lang entity.Lang) bool { - sysInfo, err := GetSystemInfo(nodeId) - if err != nil { - return false - } - for _, exec := range sysInfo.Executables { - if exec.Path == lang.ExecutablePath { - return true - } - } - return false -} - -func IsInstalledDep(installedDepList []entity.Dependency, dep entity.Dependency) bool { - for _, _dep := range installedDepList { - if strings.ToLower(_dep.Name) == strings.ToLower(dep.Name) { - return true - } - } - return false -} - +// 从Python依赖源获取依赖列表并返回 func FetchPythonDepList() ([]string, error) { // 依赖URL url := "https://pypi.tuna.tsinghua.edu.cn/simple" @@ -251,6 +260,7 @@ func FetchPythonDepList() ([]string, error) { return list, nil } +// 更新Python依赖列表到Redis func UpdatePythonDepList() { // 从依赖源获取列表 list, _ := FetchPythonDepList() @@ -271,7 +281,8 @@ func UpdatePythonDepList() { } } -func GetPythonInstalledDepList(nodeId string) ([]entity.Dependency, error){ +// 获取Python本地已安装的依赖列表 +func GetPythonLocalInstalledDepList(nodeId string) ([]entity.Dependency, error) { var list []entity.Dependency lang := GetLangFromLangName(nodeId, constants.Python) @@ -301,11 +312,56 @@ func GetPythonInstalledDepList(nodeId string) ([]entity.Dependency, error){ return list, nil } +// 获取Python远端依赖列表 +func GetPythonRemoteInstalledDepList(nodeId string) ([]entity.Dependency, error) { + depList, err := RpcClientGetInstalledDepList(nodeId, constants.Python) + if err != nil { + return depList, err + } + return depList, nil +} + +// 是否已安装该依赖 +func IsInstalledDep(installedDepList []entity.Dependency, dep entity.Dependency) bool { + for _, _dep := range installedDepList { + if strings.ToLower(_dep.Name) == strings.ToLower(dep.Name) { + return true + } + } + return false +} + +// 安装Python本地依赖 +func InstallPythonLocalDep(depName string) (string, error) { + // 依赖镜像URL + url := "https://pypi.tuna.tsinghua.edu.cn/simple" + + cmd := exec.Command("pip", "install", depName, "-i", url) + outputBytes, err := cmd.Output() + if err != nil { + return fmt.Sprintf("error: %s", err.Error()), err + } + return string(outputBytes), nil +} + +// 获取Python远端依赖列表 +func InstallPythonRemoteDep(nodeId string, depName string) (string, error) { + output, err := RpcClientInstallDep(nodeId, constants.Python, depName) + if err != nil { + return output, err + } + return output, nil +} + func InitDepsFetcher() error { c := cron.New(cron.WithSeconds()) c.Start() if _, err := c.AddFunc("0 */5 * * * *", UpdatePythonDepList); err != nil { return err } + + go func() { + UpdatePythonDepList() + }() return nil } From 53c77eb6e94d887a6dc33c8c745af8959533d94b Mon Sep 17 00:00:00 2001 From: marvzhang Date: Thu, 2 Jan 2020 17:38:39 +0800 Subject: [PATCH 04/31] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E5=8D=B8=E8=BD=BD?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/constants/rpc.go | 3 +- backend/database/redis.go | 8 +- backend/main.go | 1 + backend/routes/system.go | 59 +++++++++--- backend/services/rpc.go | 77 ++++++++-------- backend/services/system.go | 92 +++++++++++++------ .../src/components/Node/NodeInstallation.vue | 81 +++++++++++++++- frontend/src/i18n/zh.js | 8 ++ 8 files changed, 237 insertions(+), 92 deletions(-) diff --git a/backend/constants/rpc.go b/backend/constants/rpc.go index 117ed5c8..6eebf0d5 100644 --- a/backend/constants/rpc.go +++ b/backend/constants/rpc.go @@ -1,8 +1,9 @@ package constants const ( - RpcInstallDep = "install_dep" RpcInstallLang = "install_lang" + RpcInstallDep = "install_dep" + RpcUninstallDep = "uninstall_dep" RpcGetDepList = "get_dep_list" RpcGetInstalledDepList = "get_installed_dep_list" ) diff --git a/backend/database/redis.go b/backend/database/redis.go index 1a488767..bd4e5c10 100644 --- a/backend/database/redis.go +++ b/backend/database/redis.go @@ -114,11 +114,11 @@ func (r *Redis) BRPop(collection string, timeout int) (string, error) { c := r.pool.Get() defer utils.Close(c) - value, err2 := redis.String(c.Do("BRPOP", collection, timeout)) - if err2 != nil { - return value, err2 + values, err := redis.Strings(c.Do("BRPOP", collection, timeout)) + if err != nil { + return "", err } - return value, nil + return values[1], nil } func NewRedisPool() *redis.Pool { diff --git a/backend/main.go b/backend/main.go index 8968027c..5c3a4e88 100644 --- a/backend/main.go +++ b/backend/main.go @@ -148,6 +148,7 @@ func main() { authGroup.GET("/nodes/:id/deps", routes.GetDepList) // 节点第三方依赖列表 authGroup.GET("/nodes/:id/deps/installed", routes.GetInstalledDepList) // 节点已安装第三方依赖列表 authGroup.POST("/nodes/:id/deps/install", routes.InstallDep) // 节点安装依赖 + authGroup.POST("/nodes/:id/deps/uninstall", routes.UninstallDep) // 节点卸载依赖 // 爬虫 authGroup.GET("/spiders", routes.GetSpiderList) // 爬虫列表 authGroup.GET("/spiders/:id", routes.GetSpider) // 爬虫详情 diff --git a/backend/routes/system.go b/backend/routes/system.go index 409168d2..d883efb3 100644 --- a/backend/routes/system.go +++ b/backend/routes/system.go @@ -26,21 +26,12 @@ func GetDepList(c *gin.Context) { var depList []entity.Dependency if lang == constants.Python { - if services.IsMasterNode(nodeId) { - list, err := services.GetPythonLocalDepList(nodeId, depName) - if err != nil { - HandleError(http.StatusInternalServerError, c, err) - return - } - depList = list - } else { - list, err := services.GetPythonRemoteDepList(nodeId, depName) - if err != nil { - HandleError(http.StatusInternalServerError, c, err) - return - } - depList = list + list, err := services.GetPythonDepList(nodeId, depName) + if err != nil { + HandleError(http.StatusInternalServerError, c, err) + return } + depList = list } else { HandleErrorF(http.StatusBadRequest, c, fmt.Sprintf("%s is not implemented", lang)) return @@ -166,3 +157,43 @@ func InstallDep(c *gin.Context) { Message: "success", }) } + +func UninstallDep(c *gin.Context) { + type ReqBody struct { + Lang string `json:"lang"` + DepName string `json:"dep_name"` + } + + nodeId := c.Param("id") + + var reqBody ReqBody + if err := c.ShouldBindJSON(&reqBody); err != nil { + HandleError(http.StatusBadRequest, c, err) + } + + if reqBody.Lang == constants.Python { + if services.IsMasterNode(nodeId) { + _, err := services.UninstallPythonLocalDep(reqBody.DepName) + if err != nil { + HandleError(http.StatusInternalServerError, c, err) + return + } + } else { + _, err := services.UninstallPythonRemoteDep(nodeId, reqBody.DepName) + if err != nil { + HandleError(http.StatusInternalServerError, c, err) + return + } + } + } else { + HandleErrorF(http.StatusBadRequest, c, fmt.Sprintf("%s is not implemented", reqBody.Lang)) + return + } + + // TODO: check if uninstall is successful + + c.JSON(http.StatusOK, Response{ + Status: "ok", + Message: "success", + }) +} diff --git a/backend/services/rpc.go b/backend/services/rpc.go index cc0762bb..77d0c217 100644 --- a/backend/services/rpc.go +++ b/backend/services/rpc.go @@ -5,6 +5,7 @@ import ( "crawlab/database" "crawlab/entity" "crawlab/model" + "crawlab/utils" "encoding/json" "fmt" "github.com/apex/log" @@ -13,10 +14,10 @@ import ( ) type RpcMessage struct { - Id string `json:"id"` - Method string `json:"method"` - Params string `json:"params"` - Result string `json:"result"` + Id string `json:"id"` + Method string `json:"method"` + Params map[string]string `json:"params"` + Result string `json:"result"` } func RpcServerInstallLang(msg RpcMessage) RpcMessage { @@ -39,38 +40,37 @@ func RpcClientInstallDep(nodeId string, lang string, depName string) (output str params["lang"] = lang params["dep_name"] = depName - data, err := RpcClientFunc(nodeId, params, 10)() + data, err := RpcClientFunc(nodeId, constants.RpcInstallDep, params, 10)() if err != nil { return } - output = data.(string) + output = data return } -func RpcServerGetDepList(nodeId string, msg RpcMessage) RpcMessage { +func RpcServerUninstallDep(msg RpcMessage) RpcMessage { lang := GetRpcParam("lang", msg.Params) - searchDepName := GetRpcParam("search_dep_name", msg.Params) + depName := GetRpcParam("dep_name", msg.Params) if lang == constants.Python { - depList, _ := GetPythonLocalDepList(nodeId, searchDepName) - resultStr, _ := json.Marshal(depList) - msg.Result = string(resultStr) + output, _ := UninstallPythonLocalDep(depName) + msg.Result = output } return msg } -func RpcClientGetDepList(nodeId string, lang string, searchDepName string) (list []entity.Dependency, err error) { +func RpcClientUninstallDep(nodeId string, lang string, depName string) (output string, err error) { params := map[string]string{} params["lang"] = lang - params["search_dep_name"] = searchDepName + params["dep_name"] = depName - data, err := RpcClientFunc(nodeId, params, 30)() + data, err := RpcClientFunc(nodeId, constants.RpcUninstallDep, params, 60)() if err != nil { return } - list = data.([]entity.Dependency) + output = data return } @@ -89,65 +89,60 @@ func RpcClientGetInstalledDepList(nodeId string, lang string) (list []entity.Dep params := map[string]string{} params["lang"] = lang - data, err := RpcClientFunc(nodeId, params, 10)() + data, err := RpcClientFunc(nodeId, constants.RpcGetInstalledDepList, params, 10)() if err != nil { return } - list = data.([]entity.Dependency) + // 反序列化结果 + if err := json.Unmarshal([]byte(data), &list); err != nil { + return list, err + } return } -func RpcClientFunc(nodeId string, params interface{}, timeout int) func() (interface{}, error) { - return func() (data interface{}, err error) { +func RpcClientFunc(nodeId string, method string, params map[string]string, timeout int) func() (string, error) { + return func() (result string, err error) { // 请求ID id := uuid.NewV4().String() // 构造RPC消息 msg := RpcMessage{ Id: id, - Method: constants.RpcGetDepList, - Params: ObjectToString(params), + Method: method, + Params: params, Result: "", } // 发送RPC消息 - if err := database.RedisClient.LPush(fmt.Sprintf("rpc:%s", nodeId), ObjectToString(msg)); err != nil { - return data, err + msgStr := ObjectToString(msg) + if err := database.RedisClient.LPush(fmt.Sprintf("rpc:%s", nodeId), msgStr); err != nil { + return result, err } // 获取RPC回复消息 dataStr, err := database.RedisClient.BRPop(fmt.Sprintf("rpc:%s", nodeId), timeout) if err != nil { - return data, err + return result, err } // 反序列化消息 if err := json.Unmarshal([]byte(dataStr), &msg); err != nil { - return data, err + return result, err } - // 反序列化列表 - if err := json.Unmarshal([]byte(msg.Result), &data); err != nil { - return data, err - } - - return data, err + return msg.Result, err } } -func GetRpcParam(key string, params interface{}) string { - var paramsObj map[string]string - if err := json.Unmarshal([]byte(params.(string)), ¶msObj); err != nil { - return "" - } - return paramsObj[key] +func GetRpcParam(key string, params map[string]string) string { + return params[key] } func ObjectToString(params interface{}) string { - str, _ := json.Marshal(params) - return string(str) + bytes, _ := json.Marshal(params) + return utils.BytesToString(bytes) } var IsRpcStopped = false @@ -187,10 +182,10 @@ func InitRpcService() error { var replyMsg RpcMessage if msg.Method == constants.RpcInstallDep { replyMsg = RpcServerInstallDep(msg) + } else if msg.Method == constants.RpcUninstallDep { + replyMsg = RpcServerUninstallDep(msg) } else if msg.Method == constants.RpcInstallLang { replyMsg = RpcServerInstallLang(msg) - } else if msg.Method == constants.RpcGetDepList { - replyMsg = RpcServerGetDepList(node.Id.Hex(), msg) } else if msg.Method == constants.RpcGetInstalledDepList { replyMsg = RpcServerGetInstalledDepList(node.Id.Hex(), msg) } else { diff --git a/backend/services/system.go b/backend/services/system.go index fc7e0bad..e84a1255 100644 --- a/backend/services/system.go +++ b/backend/services/system.go @@ -121,7 +121,7 @@ func IsInstalledLang(nodeId string, lang entity.Lang) bool { } // 获取Python本地依赖列表 -func GetPythonLocalDepList(nodeId string, searchDepName string) ([]entity.Dependency, error) { +func GetPythonDepList(nodeId string, searchDepName string) ([]entity.Dependency, error) { var list []entity.Dependency // 先从 Redis 获取 @@ -149,22 +149,51 @@ func GetPythonLocalDepList(nodeId string, searchDepName string) ([]entity.Depend } } - // 获取已安装依赖 - installedDepList, err := GetPythonLocalInstalledDepList(nodeId) - if err != nil { - return list, err + // 获取已安装依赖列表 + var installedDepList []entity.Dependency + if IsMasterNode(nodeId) { + installedDepList, err = GetPythonLocalInstalledDepList(nodeId) + if err != nil { + return list, err + } + } else { + installedDepList, err = GetPythonRemoteInstalledDepList(nodeId) + if err != nil { + return list, err + } } - // 从依赖源获取数据 - var goSync sync.WaitGroup + // 根据依赖名排序 sort.Stable(depNameList) + + // 遍历依赖名列表,取前10个 for i, depNameDict := range depNameList { + if i > 10 { + break + } + dep := entity.Dependency{ + Name: depNameDict.Name, + } + dep.Installed = IsInstalledDep(installedDepList, dep) + list = append(list, dep) + } + + // 从依赖源获取信息 + list, err = GetPythonDepListWithInfo(list) + + return list, nil +} + +// 获取Python依赖的源数据信息 +func GetPythonDepListWithInfo(depList []entity.Dependency) ([]entity.Dependency, error) { + var goSync sync.WaitGroup + for i, dep := range depList { if i > 10 { break } goSync.Add(1) - go func(depName string, n *sync.WaitGroup) { - url := fmt.Sprintf("https://pypi.org/pypi/%s/json", depName) + go func(i int, dep entity.Dependency, depList []entity.Dependency, n *sync.WaitGroup) { + url := fmt.Sprintf("https://pypi.org/pypi/%s/json", dep.Name) res, err := req.Get(url) if err != nil { n.Done() @@ -175,27 +204,12 @@ func GetPythonLocalDepList(nodeId string, searchDepName string) ([]entity.Depend n.Done() return } - dep := entity.Dependency{ - Name: depName, - Version: data.Info.Version, - Description: data.Info.Summary, - } - dep.Installed = IsInstalledDep(installedDepList, dep) - list = append(list, dep) + depList[i].Version = data.Info.Version + depList[i].Description = data.Info.Summary n.Done() - }(depNameDict.Name, &goSync) + }(i, dep, depList, &goSync) } goSync.Wait() - - return list, nil -} - -// 获取Python远端依赖列表 -func GetPythonRemoteDepList(nodeId string, searchDepName string) ([]entity.Dependency, error) { - depList, err := RpcClientGetDepList(nodeId, constants.Python, searchDepName) - if err != nil { - return depList, err - } return depList, nil } @@ -339,6 +353,8 @@ func InstallPythonLocalDep(depName string) (string, error) { cmd := exec.Command("pip", "install", depName, "-i", url) outputBytes, err := cmd.Output() if err != nil { + log.Errorf(err.Error()) + debug.PrintStack() return fmt.Sprintf("error: %s", err.Error()), err } return string(outputBytes), nil @@ -353,6 +369,28 @@ func InstallPythonRemoteDep(nodeId string, depName string) (string, error) { return output, nil } +// 安装Python本地依赖 +func UninstallPythonLocalDep(depName string) (string, error) { + cmd := exec.Command("pip", "uninstall", "-y", depName) + outputBytes, err := cmd.Output() + if err != nil { + log.Errorf(err.Error()) + debug.PrintStack() + return fmt.Sprintf("error: %s", err.Error()), err + } + return string(outputBytes), nil +} + +// 获取Python远端依赖列表 +func UninstallPythonRemoteDep(nodeId string, depName string) (string, error) { + output, err := RpcClientUninstallDep(nodeId, constants.Python, depName) + if err != nil { + return output, err + } + return output, nil +} + +// 初始化函数 func InitDepsFetcher() error { c := cron.New(cron.WithSeconds()) c.Start() diff --git a/frontend/src/components/Node/NodeInstallation.vue b/frontend/src/components/Node/NodeInstallation.vue index d9de222d..df487434 100644 --- a/frontend/src/components/Node/NodeInstallation.vue +++ b/frontend/src/components/Node/NodeInstallation.vue @@ -51,10 +51,22 @@ > @@ -63,7 +75,10 @@ - + @@ -41,11 +52,13 @@ import { mapGetters } from 'vuex' import Breadcrumb from '@/components/Breadcrumb' import Hamburger from '@/components/Hamburger' +import GithubButton from 'vue-github-button' export default { components: { Breadcrumb, - Hamburger + Hamburger, + GithubButton }, computed: { ...mapGetters([ @@ -122,6 +135,12 @@ export default { } } + .github { + height: 50px; + margin-right: 35px; + margin-top: -10px; + } + .right { float: right } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 3e7bb8a3..b885732c 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -3932,6 +3932,11 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +github-buttons@^2.3.0: + version "2.6.0" + resolved "https://registry.npm.taobao.org/github-buttons/download/github-buttons-2.6.0.tgz#fa3e031451cee7ba05c3254fa67c73fe783104dc" + integrity sha1-+j4DFFHO57oFwyVPpnxz/ngxBNw= + glob-base@^0.3.0: version "0.3.0" resolved "http://registry.npm.taobao.org/glob-base/download/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" @@ -8587,6 +8592,13 @@ vue-eslint-parser@^5.0.0: esquery "^1.0.1" lodash "^4.17.11" +vue-github-button@^1.1.2: + version "1.1.2" + resolved "https://registry.npm.taobao.org/vue-github-button/download/vue-github-button-1.1.2.tgz#318518c3a31d0fbd278ebcc80fbc5f88d68836e6" + integrity sha1-MYUYw6MdD70njrzID7xfiNaINuY= + dependencies: + github-buttons "^2.3.0" + vue-hot-reload-api@^2.3.0: version "2.3.2" resolved "http://registry.npm.taobao.org/vue-hot-reload-api/download/vue-hot-reload-api-2.3.2.tgz#1fcc1495effe08a790909b46bf7b5c4cfeb6f21b" From bc616cdb18f582b7eecf6a1cf801ce97edbb9b9f Mon Sep 17 00:00:00 2001 From: marvzhang Date: Mon, 6 Jan 2020 21:12:41 +0800 Subject: [PATCH 26/31] updated README --- README-zh.md | 4 ++-- README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README-zh.md b/README-zh.md index 7848db5d..691bda88 100644 --- a/README-zh.md +++ b/README-zh.md @@ -19,8 +19,8 @@ ## 安装 三种方式: -1. [Docker](https://tikazyq.github.io/crawlab-docs/Installation/Docker.html)(推荐) -2. [直接部署](https://tikazyq.github.io/crawlab-docs/Installation/Direct.html)(了解内核) +1. [Docker](http://docs.crawlab.cn/Installation/Docker.html)(推荐) +2. [直接部署](http://docs.crawlab.cn/Installation/Direct.html)(了解内核) 3. [Kubernetes](https://juejin.im/post/5e0a02d851882549884c27ad) (多节点部署) ### 要求(Docker) diff --git a/README.md b/README.md index 6677f4a2..2b07ef1c 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,8 @@ Golang-based distributed web crawler management platform, supporting various lan ## Installation Two methods: -1. [Docker](https://tikazyq.github.io/crawlab-docs/Installation/Docker.html) (Recommended) -2. [Direct Deploy](https://tikazyq.github.io/crawlab-docs/Installation/Direct.html) (Check Internal Kernel) +1. [Docker](http://docs.crawlab.cn/Installation/Docker.html) (Recommended) +2. [Direct Deploy](http://docs.crawlab.cn/Installation/Direct.html) (Check Internal Kernel) 3. [Kubernetes](https://juejin.im/post/5e0a02d851882549884c27ad) (Multi-Node Deployment) ### Pre-requisite (Docker) From fab288034dcf6729f4eeb4e574d1f40228cef084 Mon Sep 17 00:00:00 2001 From: marvzhang Date: Mon, 6 Jan 2020 21:33:18 +0800 Subject: [PATCH 27/31] updated badges --- README-zh.md | 15 ++++++++------- README.md | 15 ++++++++------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/README-zh.md b/README-zh.md index 691bda88..f2d482c2 100644 --- a/README-zh.md +++ b/README-zh.md @@ -1,12 +1,13 @@ # Crawlab -![](http://114.67.75.98:8082/buildStatus/icon?job=crawlab%2Fmaster) -![](https://img.shields.io/github/release/crawlab-team/crawlab.svg) -![](https://img.shields.io/github/last-commit/crawlab-team/crawlab.svg) -![](https://img.shields.io/github/issues/crawlab-team/crawlab.svg) -![](https://img.shields.io/github/contributors/crawlab-team/crawlab.svg) -![](https://img.shields.io/docker/pulls/tikazyq/crawlab) -![](https://img.shields.io/github/license/crawlab-team/crawlab.svg) +![](http://jenkins.crawlab.cn/buildStatus/icon?job=crawlab%2Fmaster) +![](https://img.shields.io/github/release/crawlab-team/crawlab.svg?logo=github) +![](https://img.shields.io/github/last-commit/crawlab-team/crawlab.svg?logo=github) +![](https://img.shields.io/github/issues/crawlab-team/crawlab/bug.svg?logo=github&logoColor=red) +![](https://img.shields.io/github/issues/crawlab-team/crawlab/enhancement.svg?logo=github&logoColor=cyan) +![](https://img.shields.io/github/contributors/crawlab-team/crawlab.svg?logo=github) +![](https://img.shields.io/docker/pulls/tikazyq/crawlab?logo=docker) +![](https://img.shields.io/github/license/crawlab-team/crawlab.svg?logo=FreeBSD&logoColor=yellow) 中文 | [English](https://github.com/crawlab-team/crawlab) diff --git a/README.md b/README.md index 2b07ef1c..aed9d828 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ # Crawlab -![](http://114.67.75.98:8082/buildStatus/icon?job=crawlab%2Fmaster) -![](https://img.shields.io/github/release/crawlab-team/crawlab.svg) -![](https://img.shields.io/github/last-commit/crawlab-team/crawlab.svg) -![](https://img.shields.io/github/issues/crawlab-team/crawlab.svg) -![](https://img.shields.io/github/contributors/crawlab-team/crawlab.svg) -![](https://img.shields.io/docker/pulls/tikazyq/crawlab) -![](https://img.shields.io/github/license/crawlab-team/crawlab.svg) +![](http://jenkins.crawlab.cn/buildStatus/icon?job=crawlab%2Fmaster) +![](https://img.shields.io/github/release/crawlab-team/crawlab.svg?logo=github) +![](https://img.shields.io/github/last-commit/crawlab-team/crawlab.svg?logo=github) +![](https://img.shields.io/github/issues/crawlab-team/crawlab/bug.svg?logo=github&logoColor=red) +![](https://img.shields.io/github/issues/crawlab-team/crawlab/enhancement.svg?logo=github&logoColor=cyan) +![](https://img.shields.io/github/contributors/crawlab-team/crawlab.svg?logo=github) +![](https://img.shields.io/docker/pulls/tikazyq/crawlab?logo=docker) +![](https://img.shields.io/github/license/crawlab-team/crawlab.svg?logo=FreeBSD&logoColor=yellow) [中文](https://github.com/crawlab-team/crawlab/blob/master/README-zh.md) | English From ee0fd649cf0026efbf78f9ed9c6888dad3f8bd4d Mon Sep 17 00:00:00 2001 From: marvzhang Date: Mon, 6 Jan 2020 21:40:11 +0800 Subject: [PATCH 28/31] updated badges --- README-zh.md | 9 ++++----- README.md | 9 ++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/README-zh.md b/README-zh.md index f2d482c2..0dddf0c3 100644 --- a/README-zh.md +++ b/README-zh.md @@ -2,12 +2,11 @@ ![](http://jenkins.crawlab.cn/buildStatus/icon?job=crawlab%2Fmaster) ![](https://img.shields.io/github/release/crawlab-team/crawlab.svg?logo=github) -![](https://img.shields.io/github/last-commit/crawlab-team/crawlab.svg?logo=github) -![](https://img.shields.io/github/issues/crawlab-team/crawlab/bug.svg?logo=github&logoColor=red) -![](https://img.shields.io/github/issues/crawlab-team/crawlab/enhancement.svg?logo=github&logoColor=cyan) -![](https://img.shields.io/github/contributors/crawlab-team/crawlab.svg?logo=github) ![](https://img.shields.io/docker/pulls/tikazyq/crawlab?logo=docker) -![](https://img.shields.io/github/license/crawlab-team/crawlab.svg?logo=FreeBSD&logoColor=yellow) +![](https://img.shields.io/github/last-commit/crawlab-team/crawlab.svg) +![](https://img.shields.io/github/issues/crawlab-team/crawlab/bug.svg?color=red) +![](https://img.shields.io/github/issues/crawlab-team/crawlab/enhancement.svg?color=cyan) +![](https://img.shields.io/github/license/crawlab-team/crawlab.svg) 中文 | [English](https://github.com/crawlab-team/crawlab) diff --git a/README.md b/README.md index aed9d828..a84ea552 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,11 @@ ![](http://jenkins.crawlab.cn/buildStatus/icon?job=crawlab%2Fmaster) ![](https://img.shields.io/github/release/crawlab-team/crawlab.svg?logo=github) -![](https://img.shields.io/github/last-commit/crawlab-team/crawlab.svg?logo=github) -![](https://img.shields.io/github/issues/crawlab-team/crawlab/bug.svg?logo=github&logoColor=red) -![](https://img.shields.io/github/issues/crawlab-team/crawlab/enhancement.svg?logo=github&logoColor=cyan) -![](https://img.shields.io/github/contributors/crawlab-team/crawlab.svg?logo=github) ![](https://img.shields.io/docker/pulls/tikazyq/crawlab?logo=docker) -![](https://img.shields.io/github/license/crawlab-team/crawlab.svg?logo=FreeBSD&logoColor=yellow) +![](https://img.shields.io/github/last-commit/crawlab-team/crawlab.svg) +![](https://img.shields.io/github/issues/crawlab-team/crawlab/bug.svg?color=red) +![](https://img.shields.io/github/issues/crawlab-team/crawlab/enhancement.svg?color=cyan) +![](https://img.shields.io/github/license/crawlab-team/crawlab.svg) [中文](https://github.com/crawlab-team/crawlab/blob/master/README-zh.md) | English From 3cfe5c8668066653bd77a82b7956006ad0444fa7 Mon Sep 17 00:00:00 2001 From: marvzhang Date: Mon, 6 Jan 2020 21:41:53 +0800 Subject: [PATCH 29/31] updated badges --- README-zh.md | 4 ++-- README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README-zh.md b/README-zh.md index 0dddf0c3..12b518b5 100644 --- a/README-zh.md +++ b/README-zh.md @@ -4,8 +4,8 @@ ![](https://img.shields.io/github/release/crawlab-team/crawlab.svg?logo=github) ![](https://img.shields.io/docker/pulls/tikazyq/crawlab?logo=docker) ![](https://img.shields.io/github/last-commit/crawlab-team/crawlab.svg) -![](https://img.shields.io/github/issues/crawlab-team/crawlab/bug.svg?color=red) -![](https://img.shields.io/github/issues/crawlab-team/crawlab/enhancement.svg?color=cyan) +![](https://img.shields.io/github/issues/crawlab-team/crawlab/bug.svg?label=bugs&color=red) +![](https://img.shields.io/github/issues/crawlab-team/crawlab/enhancement.svg?label=enhancements&color=cyan) ![](https://img.shields.io/github/license/crawlab-team/crawlab.svg) 中文 | [English](https://github.com/crawlab-team/crawlab) diff --git a/README.md b/README.md index a84ea552..236e950f 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ ![](https://img.shields.io/github/release/crawlab-team/crawlab.svg?logo=github) ![](https://img.shields.io/docker/pulls/tikazyq/crawlab?logo=docker) ![](https://img.shields.io/github/last-commit/crawlab-team/crawlab.svg) -![](https://img.shields.io/github/issues/crawlab-team/crawlab/bug.svg?color=red) -![](https://img.shields.io/github/issues/crawlab-team/crawlab/enhancement.svg?color=cyan) +![](https://img.shields.io/github/issues/crawlab-team/crawlab/bug.svg?label=bugs&color=red) +![](https://img.shields.io/github/issues/crawlab-team/crawlab/enhancement.svg?label=enhancements&color=cyan) ![](https://img.shields.io/github/license/crawlab-team/crawlab.svg) [中文](https://github.com/crawlab-team/crawlab/blob/master/README-zh.md) | English From fa4e1af1b070d2e0f47245669795717977f8f293 Mon Sep 17 00:00:00 2001 From: marvzhang Date: Mon, 6 Jan 2020 21:43:16 +0800 Subject: [PATCH 30/31] updated badges --- README-zh.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README-zh.md b/README-zh.md index 12b518b5..944019e1 100644 --- a/README-zh.md +++ b/README-zh.md @@ -2,7 +2,7 @@ ![](http://jenkins.crawlab.cn/buildStatus/icon?job=crawlab%2Fmaster) ![](https://img.shields.io/github/release/crawlab-team/crawlab.svg?logo=github) -![](https://img.shields.io/docker/pulls/tikazyq/crawlab?logo=docker) +![](https://img.shields.io/docker/pulls/tikazyq/crawlab?label=pulls&logo=docker) ![](https://img.shields.io/github/last-commit/crawlab-team/crawlab.svg) ![](https://img.shields.io/github/issues/crawlab-team/crawlab/bug.svg?label=bugs&color=red) ![](https://img.shields.io/github/issues/crawlab-team/crawlab/enhancement.svg?label=enhancements&color=cyan) diff --git a/README.md b/README.md index 236e950f..efd21cb5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![](http://jenkins.crawlab.cn/buildStatus/icon?job=crawlab%2Fmaster) ![](https://img.shields.io/github/release/crawlab-team/crawlab.svg?logo=github) -![](https://img.shields.io/docker/pulls/tikazyq/crawlab?logo=docker) +![](https://img.shields.io/docker/pulls/tikazyq/crawlab?label=pulls&logo=docker) ![](https://img.shields.io/github/last-commit/crawlab-team/crawlab.svg) ![](https://img.shields.io/github/issues/crawlab-team/crawlab/bug.svg?label=bugs&color=red) ![](https://img.shields.io/github/issues/crawlab-team/crawlab/enhancement.svg?label=enhancements&color=cyan) From aed16d3edbc759d5e119bd223967580f42889d45 Mon Sep 17 00:00:00 2001 From: marvzhang Date: Mon, 6 Jan 2020 21:49:58 +0800 Subject: [PATCH 31/31] updated badges --- README-zh.md | 4 ++-- README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README-zh.md b/README-zh.md index 944019e1..9057fcc3 100644 --- a/README-zh.md +++ b/README-zh.md @@ -1,8 +1,8 @@ # Crawlab -![](http://jenkins.crawlab.cn/buildStatus/icon?job=crawlab%2Fmaster) -![](https://img.shields.io/github/release/crawlab-team/crawlab.svg?logo=github) +![](https://img.shields.io/docker/cloud/build/tikazyq/crawlab.svg?label=build&logo=docker) ![](https://img.shields.io/docker/pulls/tikazyq/crawlab?label=pulls&logo=docker) +![](https://img.shields.io/github/release/crawlab-team/crawlab.svg?logo=github) ![](https://img.shields.io/github/last-commit/crawlab-team/crawlab.svg) ![](https://img.shields.io/github/issues/crawlab-team/crawlab/bug.svg?label=bugs&color=red) ![](https://img.shields.io/github/issues/crawlab-team/crawlab/enhancement.svg?label=enhancements&color=cyan) diff --git a/README.md b/README.md index efd21cb5..075a80b5 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Crawlab -![](http://jenkins.crawlab.cn/buildStatus/icon?job=crawlab%2Fmaster) -![](https://img.shields.io/github/release/crawlab-team/crawlab.svg?logo=github) +![](https://img.shields.io/docker/cloud/build/tikazyq/crawlab.svg?label=build&logo=docker) ![](https://img.shields.io/docker/pulls/tikazyq/crawlab?label=pulls&logo=docker) +![](https://img.shields.io/github/release/crawlab-team/crawlab.svg?logo=github) ![](https://img.shields.io/github/last-commit/crawlab-team/crawlab.svg) ![](https://img.shields.io/github/issues/crawlab-team/crawlab/bug.svg?label=bugs&color=red) ![](https://img.shields.io/github/issues/crawlab-team/crawlab/enhancement.svg?label=enhancements&color=cyan)