From 9391048774c27fc7822d4bfa9dc3d55d1a8ff3f7 Mon Sep 17 00:00:00 2001 From: saltbo Date: Sat, 15 Jun 2019 19:38:14 +0800 Subject: [PATCH 1/5] refactor: clean the code and support hot reload --- .gitignore | 3 +- go.mod | 1 + go.sum | 2 ++ server/client.go | 49 ++++++++++++++++++++++++++ server/main.go | 65 +++++++++++++++++++++++----------- server/process.go | 39 ++++++++++++++++++++ server/robot.go | 18 +++++++--- server/server.go | 90 +++++++++++++++++++++++++++++++++-------------- 8 files changed, 216 insertions(+), 51 deletions(-) create mode 100644 server/client.go create mode 100644 server/process.go diff --git a/.gitignore b/.gitignore index b5e26ad..0f79cb5 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out build -coverage.txt \ No newline at end of file +coverage.txt +gofbot.lock \ No newline at end of file diff --git a/go.mod b/go.mod index 05f04b5..e399237 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,6 @@ require ( github.com/gin-gonic/gin v1.4.0 github.com/satori/go.uuid v1.2.0 github.com/stretchr/testify v1.3.0 + github.com/urfave/cli v1.20.0 gopkg.in/yaml.v2 v2.2.2 ) diff --git a/go.sum b/go.sum index de3ffab..f9f9a94 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,8 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/server/client.go b/server/client.go new file mode 100644 index 0000000..0b07911 --- /dev/null +++ b/server/client.go @@ -0,0 +1,49 @@ +package main + +import ( + `fmt` + `github.com/urfave/cli` + `log` + `syscall` +) + +var ( + repo string + commit string + version string + buildTime string +) + +// define some flags +var flags = []cli.Flag{ + +} +// define some commands +var commands = []cli.Command{ + { + Name: "reload", + Usage: "reload for the config", + Action: func(c *cli.Context) { + p, err := findProcess() + if err != nil { + log.Println(err) + return + } + + if err := p.Signal(syscall.SIGUSR1); err != nil { + log.Println(err) + return + } + }, + }, +} + +func NewClient() *cli.App { + cli.VersionPrinter = func(c *cli.Context) { + fmt.Printf("repo: %s\ncommit: %s\nversion: %s\nbuildTime: %s\n", repo, commit, version, buildTime) + } + app := cli.NewApp() + app.Flags = flags + app.Commands = commands + return app +} diff --git a/server/main.go b/server/main.go index 3b80eb1..d9f93ab 100644 --- a/server/main.go +++ b/server/main.go @@ -1,39 +1,64 @@ package main import ( - "flag" - "fmt" "log" "os" + `os/signal` + `syscall` ) -var ( - showVer bool - repo string - commit string - version string - buildTime string -) - -func init() { - flag.BoolVar(&showVer, "v", false, "show build version") - flag.Parse() +func main() { + if len(os.Args) == 1 { + serverRun() + return + } - if showVer { - fmt.Printf("repo: %s\ncommit: %s\nversion: %s\nbuildTime: %s\n", repo, commit, version, buildTime) - os.Exit(0) + if err := NewClient().Run(os.Args); err != nil { + log.Fatal(err) } } -func main() { +func serverRun() { robots, err := loadRobots("robots") if err != nil { log.Fatal(err) } - if s, err := New(robots); err != nil { + if err := savePid(); err != nil { log.Fatal(err) - } else { - log.Fatal(s.Run(":9613")) } + + server := NewServer() + setupSignalHandler(server) + server.SetupRobots(robots) + if err := server.Run(":9613"); err != nil { + log.Fatal(err) + } +} + +func setupSignalHandler(server *Server) { + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1) + go func() { + for { + sig := <-ch + switch sig { + case syscall.SIGINT, syscall.SIGTERM: + cleanPid(); + signal.Stop(ch) + server.Shutdown() + log.Printf("system exit.") + return + case syscall.SIGUSR1: + // hot reload + robots, err := loadRobots("robots") + if err != nil { + log.Println(robots) + return + } + server.SetupRobots(robots) + log.Printf("config reload.") + } + } + }() } diff --git a/server/process.go b/server/process.go new file mode 100644 index 0000000..9be1dd6 --- /dev/null +++ b/server/process.go @@ -0,0 +1,39 @@ +package main + +import ( + `fmt` + `io/ioutil` + `os` + `strconv` +) + +var pidFile = "gofbot.lock" + +func savePid() error { + _, err := os.Stat(pidFile) // os.Stat获取文件信息 + if (err != nil && os.IsExist(err)) || err == nil { + return fmt.Errorf("gofbot already running.") + } + + pid := strconv.Itoa(os.Getpid()) + return ioutil.WriteFile(pidFile, []byte(pid), 0600) +} + +func cleanPid() error { + return os.Remove(pidFile) +} + +func findProcess() (*os.Process, error) { + data, err := ioutil.ReadFile(pidFile) + if err != nil { + return nil, err + } + + pidStr := string(data) + pid, err := strconv.Atoi(pidStr) + if err != nil { + return nil, err + } + + return os.FindProcess(pid) +} diff --git a/server/robot.go b/server/robot.go index 84ed9b4..7d2f908 100644 --- a/server/robot.go +++ b/server/robot.go @@ -13,6 +13,13 @@ import ( "strings" ) +type Message struct { + Regexp string `yaml:"regexp"` + Template string `yaml:"template"` + + Exp *regexp.Regexp +} + type Robot struct { Name string `yaml:"name"` Alias string `yaml:"uuid"` @@ -21,11 +28,14 @@ type Robot struct { Messages []*Message `yaml:"messages"` } -type Message struct { - Regexp string `yaml:"regexp"` - Template string `yaml:"template"` +func (r *Robot) MatchMessage(body []byte) (*Message, error) { + for _, msg := range r.Messages { + if msg.Exp.Match(body) { + return msg, nil + } + } - Exp *regexp.Regexp + return nil, fmt.Errorf("not found any message") } func newRobot(yamlPath string) (*Robot, error) { diff --git a/server/server.go b/server/server.go index 2cbef43..dbe47a3 100644 --- a/server/server.go +++ b/server/server.go @@ -2,8 +2,10 @@ package main import ( "bytes" + `context` "encoding/json" - "fmt" + `fmt` + `io` "io/ioutil" "net/http" "regexp" @@ -19,23 +21,52 @@ var tplArgExp = regexp.MustCompile(`{{(\s*\$\S+\s*)}}`) type Map map[string]interface{} type Server struct { - *gin.Engine + http.Server + robots map[string]*Robot } -func New(robots []*Robot) (*Server, error) { +func NewServer() *Server { router := gin.Default() + server := &Server{ + Server: http.Server{ + Addr: ":8080", + Handler: router, + }, + robots: make(map[string]*Robot), + } + + router.POST("/incoming/:alias", server.incomingHandler) + return server +} + +func (s *Server) SetupRobots(robots []*Robot) { for _, robot := range robots { - router.POST(fmt.Sprintf("/incoming/%s", robot.Alias), func(context *gin.Context) { - incomingHandler(context, robot) - }) + s.robots[robot.Alias] = robot + } +} + +func (s *Server) Run(addr ...string) error { + if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed { + return err } - return &Server{ - Engine: router, - }, nil + return nil } -func incomingHandler(ctx *gin.Context, robot *Robot) { +func (s *Server) Shutdown() error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + return s.Server.Shutdown(ctx) +} + +func (s *Server) incomingHandler(ctx *gin.Context) { + alias := ctx.Param("alias") + robot, ok := s.robots[alias] + if !ok { + ctx.AbortWithError(http.StatusNotFound, fmt.Errorf("not found your robot")) + return + } + body, err := ioutil.ReadAll(ctx.Request.Body) if err != nil { ctx.AbortWithError(http.StatusBadRequest, err) @@ -44,28 +75,35 @@ func incomingHandler(ctx *gin.Context, robot *Robot) { params := make(Map) if err := json.Unmarshal(body, ¶ms); err != nil { + ctx.AbortWithError(http.StatusBadRequest, err) + return + } + + msg, err := robot.MatchMessage(body) + if err != nil { ctx.AbortWithError(http.StatusInternalServerError, err) return } - for _, msg := range robot.Messages { - if !msg.Exp.Match(body) { - continue - } + msgStr := buildMessage(msg.Template, params) // 正则替换参数 + postBody := buildPostBody(robot.BodyTpl, msgStr) + if err := forwardToRobot(ctx, robot.WebHook, postBody); err != nil { + ctx.AbortWithError(http.StatusInternalServerError, err) + return + } +} - // 正则替换参数 - message := buildMessage(msg.Template, params) - body := buildPostBody(robot.BodyTpl, message) - http.DefaultClient.Timeout = 3 * time.Second - if resp, err := http.DefaultClient.Post(robot.WebHook, "application/json", body); err != nil { - ctx.AbortWithError(http.StatusInternalServerError, err) - return - } else { - defer resp.Body.Close() - rb, _ := ioutil.ReadAll(resp.Body) - ctx.Data(resp.StatusCode, resp.Header.Get("Content-Type"), rb) - } +func forwardToRobot(ctx *gin.Context, url string, body io.Reader) error { + http.DefaultClient.Timeout = 3 * time.Second + resp, err := http.DefaultClient.Post(url, "application/json", body) + if err != nil { + return err } + + defer resp.Body.Close() + rb, _ := ioutil.ReadAll(resp.Body) + ctx.Data(resp.StatusCode, resp.Header.Get("Content-Type"), rb) + return nil } type variable struct { From 40e9c94884bfc5ee81740fa4a94d4cb105f9627a Mon Sep 17 00:00:00 2001 From: saltbo Date: Sat, 15 Jun 2019 19:42:37 +0800 Subject: [PATCH 2/5] test: fix unit test --- server/server.go | 8 +++++--- server/server_test.go | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/server/server.go b/server/server.go index dbe47a3..2332509 100644 --- a/server/server.go +++ b/server/server.go @@ -2,10 +2,10 @@ package main import ( "bytes" - `context` + "context" "encoding/json" - `fmt` - `io` + "fmt" + "io" "io/ioutil" "net/http" "regexp" @@ -22,6 +22,7 @@ type Map map[string]interface{} type Server struct { http.Server + router *gin.Engine robots map[string]*Robot } @@ -32,6 +33,7 @@ func NewServer() *Server { Addr: ":8080", Handler: router, }, + router: router, robots: make(map[string]*Robot), } diff --git a/server/server_test.go b/server/server_test.go index d763f8a..0dff31e 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -60,15 +60,15 @@ func TestServer_Run(t *testing.T) { robots, err := loadRobots("../robots") assert.NoError(t, err) - r, e := New(robots) - assert.NoError(t, e) + server := NewServer() + server.SetupRobots(robots) for _, robot := range robots { // reset the hook to the test server URL robot.WebHook = ts.URL // RUN body := bytes.NewBufferString(`{"name": "saltbo", "sex": "man", "info":{"city": "beijing"}}`) - w := performRequest(r, "POST", fmt.Sprintf("/incoming/%s", robot.Alias), body) + w := performRequest(server.router, "POST", fmt.Sprintf("/incoming/%s", robot.Alias), body) // TEST assert.Equal(t, http.StatusOK, w.Code) From 5d468b19e0f6a95625e62012c3fb9a2cdd16c6b1 Mon Sep 17 00:00:00 2001 From: saltbo Date: Tue, 18 Jun 2019 09:45:27 +0800 Subject: [PATCH 3/5] chore: add Dockerfile --- .dockerignore | 15 +++++++++++++++ Dockerfile | 23 +++++++++++++++++++++++ Makefile | 10 +++++----- 3 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0f79cb5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +build +coverage.txt +gofbot.lock \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e3608d7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM golang:latest AS build-env + +ENV GOPROXY https://goproxy.io +ENV APP_HOME /app +WORKDIR $APP_HOME + +ADD go.mod go.sum Makefile .git $APP_HOME/ +RUN make mod + +ADD . $APP_HOME +RUN make + + +# Runing Environment +FROM debian:9 + +ENV APP_HOME /app +WORKDIR $APP_HOME + +ADD robots $APP_HOME/robots +COPY --from=build-env /app/build/gofbot $APP_HOME/gofbot + +ENTRYPOINT ["./gofbot"] diff --git a/Makefile b/Makefile index 7858dc8..3616e42 100644 --- a/Makefile +++ b/Makefile @@ -20,11 +20,11 @@ mod: build: @echo "-------------- building the program ---------------" - cd ${MKFILE_DIR} && go build -v -ldflags "-s -w \ - -X main.repo=${GIT_REPO_INFO} \ - -X main.commit=${COMMIT} \ - -X main.version=${RELEASE} \ - -X 'main.buildTime=${BUILD_TIME}' \ + cd ${MKFILE_DIR} && go build -v -ldflags "-s -w \ + -X main.repo=${GIT_REPO_INFO} \ + -X main.commit=${COMMIT} \ + -X main.version=${RELEASE} \ + -X 'main.buildTime=${BUILD_TIME}' \ " -o ${TARGET} ${MKFILE_DIR}server @echo "-------------- version detail ---------------" @${TARGET} -v From 76892a7b779685a169942ac4a5d238dadeae5d16 Mon Sep 17 00:00:00 2001 From: saltbo Date: Mon, 9 Sep 2019 17:16:12 +0800 Subject: [PATCH 4/5] fix: none string for the object params --- Makefile | 4 +- {server => api}/server.go | 80 +++-------------- cmd/server.go | 121 ++++++++++++++++++++++++++ deployments/robots/wxwork4gitlab.yaml | 12 +++ go.mod | 2 + pkg/process/process.go | 45 ++++++++++ robot/message.go | 63 ++++++++++++++ robot/message_test.go | 26 ++++++ {server => robot}/robot.go | 81 ++++++++--------- {server => robot}/robot_test.go | 7 +- robots/wxwork4gitlab.yaml | 13 --- server/client.go | 49 ----------- server/main.go | 64 -------------- server/process.go | 39 --------- server/server_test.go | 76 ---------------- 15 files changed, 326 insertions(+), 356 deletions(-) rename {server => api}/server.go (51%) create mode 100644 cmd/server.go create mode 100644 deployments/robots/wxwork4gitlab.yaml create mode 100644 pkg/process/process.go create mode 100644 robot/message.go create mode 100644 robot/message_test.go rename {server => robot}/robot.go (87%) rename {server => robot}/robot_test.go (60%) delete mode 100644 robots/wxwork4gitlab.yaml delete mode 100644 server/client.go delete mode 100644 server/main.go delete mode 100644 server/process.go delete mode 100644 server/server_test.go diff --git a/Makefile b/Makefile index 3616e42..a834a41 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ MKFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST))) MKFILE_DIR := $(dir $(MKFILE_PATH)) TARGET = ${MKFILE_DIR}build/gofbot -RELEASE?=$(shell git describe --tags) +RELEASE := $(shell git describe --tags --always | awk -F '-' '{print $$1}') GIT_REPO_INFO=$(shell git config --get remote.origin.url) ifndef COMMIT @@ -25,7 +25,7 @@ build: -X main.commit=${COMMIT} \ -X main.version=${RELEASE} \ -X 'main.buildTime=${BUILD_TIME}' \ - " -o ${TARGET} ${MKFILE_DIR}server + " -o ${TARGET} ${MKFILE_DIR}cmd/server.go @echo "-------------- version detail ---------------" @${TARGET} -v diff --git a/server/server.go b/api/server.go similarity index 51% rename from server/server.go rename to api/server.go index 2332509..4daa75d 100644 --- a/server/server.go +++ b/api/server.go @@ -1,29 +1,23 @@ -package main +package api import ( - "bytes" "context" "encoding/json" "fmt" "io" "io/ioutil" "net/http" - "regexp" - "strconv" - "strings" "time" "github.com/gin-gonic/gin" -) - -var tplArgExp = regexp.MustCompile(`{{(\s*\$\S+\s*)}}`) -type Map map[string]interface{} + "github.com/saltbo/gofbot/robot" +) type Server struct { http.Server router *gin.Engine - robots map[string]*Robot + robots map[string]*robot.Robot } func NewServer() *Server { @@ -34,16 +28,16 @@ func NewServer() *Server { Handler: router, }, router: router, - robots: make(map[string]*Robot), + robots: make(map[string]*robot.Robot), } router.POST("/incoming/:alias", server.incomingHandler) return server } -func (s *Server) SetupRobots(robots []*Robot) { - for _, robot := range robots { - s.robots[robot.Alias] = robot +func (s *Server) SetupRobots(robots []*robot.Robot) { + for _, bot := range robots { + s.robots[bot.Alias] = bot } } @@ -63,7 +57,7 @@ func (s *Server) Shutdown() error { func (s *Server) incomingHandler(ctx *gin.Context) { alias := ctx.Param("alias") - robot, ok := s.robots[alias] + bot, ok := s.robots[alias] if !ok { ctx.AbortWithError(http.StatusNotFound, fmt.Errorf("not found your robot")) return @@ -75,21 +69,21 @@ func (s *Server) incomingHandler(ctx *gin.Context) { return } - params := make(Map) + params := make(robot.Map) if err := json.Unmarshal(body, ¶ms); err != nil { ctx.AbortWithError(http.StatusBadRequest, err) return } - msg, err := robot.MatchMessage(body) + msg, err := bot.MatchMessage(body) if err != nil { ctx.AbortWithError(http.StatusInternalServerError, err) return } - msgStr := buildMessage(msg.Template, params) // 正则替换参数 - postBody := buildPostBody(robot.BodyTpl, msgStr) - if err := forwardToRobot(ctx, robot.WebHook, postBody); err != nil { + msgStr := robot.BuildMessage(msg.Template, params) // 正则替换参数 + postBody := robot.BuildPostBody(bot.BodyTpl, msgStr) + if err := forwardToRobot(ctx, bot.WebHook, postBody); err != nil { ctx.AbortWithError(http.StatusInternalServerError, err) return } @@ -107,49 +101,3 @@ func forwardToRobot(ctx *gin.Context, url string, body io.Reader) error { ctx.Data(resp.StatusCode, resp.Header.Get("Content-Type"), rb) return nil } - -type variable struct { - full string - name string -} - -func buildMessage(tpl string, params Map) string { - variables := make([]variable, 0) - for _, v := range tplArgExp.FindAllStringSubmatch(tpl, -1) { - variables = append(variables, variable{full: v[0], name: v[1]}) - } - - newMsg := tpl - for _, v := range variables { - newMsg = strings.Replace(newMsg, v.full, extractArgs(params, strings.TrimSpace(v.name)), -1) - } - - if strconv.CanBackquote(newMsg) { - return newMsg - } - - return strconv.Quote(newMsg) -} - -func buildPostBody(bodyTpl string, message string) *bytes.Buffer { - return bytes.NewBufferString(strings.Replace(bodyTpl, "$template", message, -1)) -} - -func extractArgs(params Map, key string) string { - key = strings.Replace(key, "$", "", -1) - keys := strings.Split(key, ".") - for index, k := range keys { - if index == len(keys)-1 { - if v, ok := params[k].(string); ok { - return v - } - return "" - } - - if nextParams, ok := params[k].(Map); ok { - params = nextParams - } - } - - return "" -} diff --git a/cmd/server.go b/cmd/server.go new file mode 100644 index 0000000..7037a9b --- /dev/null +++ b/cmd/server.go @@ -0,0 +1,121 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/signal" + "syscall" + "time" + + "github.com/urfave/cli" + + "github.com/saltbo/gofbot/api" + "github.com/saltbo/gofbot/pkg/process" + "github.com/saltbo/gofbot/robot" +) + +var ( + repo string + commit string + version string + buildTime string +) + +var pidCtrl = process.New("gofbot.lock") + +// define some flags +var flags = []cli.Flag{ + cli.StringFlag{ + Name: "robots", + Value: "deployments/robots", + }, +} + +// define some commands +var commands = []cli.Command{ + { + Name: "reload", + Usage: "reload for the config", + Action: reloadAction, + }, +} + +func main() { + cli.VersionPrinter = func(c *cli.Context) { + fmt.Printf("repo: %s\ncommit: %s\nversion: %s\nbuildTime: %s\n", repo, commit, version, buildTime) + } + app := cli.NewApp() + app.Compiled = time.Now() + app.Copyright = "(c) 2019 yanbo.me" + app.Flags = flags + app.Commands = commands + app.Action = serverRun + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} + +func serverRun(c *cli.Context) { + robotsPath := c.String("robots") + robots, err := robot.LoadAndParse(robotsPath) + if err != nil { + log.Fatal(err) + } + + if err := pidCtrl.Save(); err != nil { + log.Fatal(err) + } + defer pidCtrl.Clean() + + server := api.NewServer() + server.SetupRobots(robots) + setupSignalHandler(server, robotsPath) + + // startup + if err := server.Run(":9613"); err != nil { + log.Fatal(err) + } + + log.Println("normal exited.") +} + +func reloadAction(c *cli.Context) { + p, err := pidCtrl.Find() + if err != nil { + log.Println(err) + return + } + + if err := p.Signal(syscall.SIGUSR1); err != nil { + log.Println(err) + return + } +} + +func setupSignalHandler(server *api.Server, robotsPath string) { + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1) + go func() { + for { + switch <-ch { + case syscall.SIGINT, syscall.SIGTERM: + _ = pidCtrl.Clean() + signal.Stop(ch) + _ = server.Shutdown() + log.Print("system exit.") + return + case syscall.SIGUSR1: + // hot reload + robots, err := robot.LoadAndParse(robotsPath) + if err != nil { + log.Println(err) + return + } + server.SetupRobots(robots) + log.Printf("config reload.") + } + } + }() +} diff --git a/deployments/robots/wxwork4gitlab.yaml b/deployments/robots/wxwork4gitlab.yaml new file mode 100644 index 0000000..d36e016 --- /dev/null +++ b/deployments/robots/wxwork4gitlab.yaml @@ -0,0 +1,12 @@ +name: wxwork4gitlab +webhook: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=0d3ffb7e-3a7a-4f4c-8385-bea0db5e581a +bodytpl: '{"msgtype":"markdown","markdown":{"content":"$template"}}' +messages: + - regexp: push # regexp for match this message. + template: {{ $project.name }} 有新的PUSH,请相关同事注意。\n + >Commit: {{ $project.id }} \n + >Author: {{ $user_name }}({{ $user_email }}) \n + - regexp: merge + template: test merge message. + - regexp: issue + template: test push message. diff --git a/go.mod b/go.mod index e399237..d77d2d5 100644 --- a/go.mod +++ b/go.mod @@ -7,3 +7,5 @@ require ( github.com/urfave/cli v1.20.0 gopkg.in/yaml.v2 v2.2.2 ) + +go 1.13 diff --git a/pkg/process/process.go b/pkg/process/process.go new file mode 100644 index 0000000..7de2cb7 --- /dev/null +++ b/pkg/process/process.go @@ -0,0 +1,45 @@ +package process + +import ( + "fmt" + "io/ioutil" + "os" + "strconv" +) + +type PidCtrl struct { + pidFile string +} + +func New(pidFile string) *PidCtrl { + return &PidCtrl{pidFile: pidFile} +} + +func (p *PidCtrl) Save() error { + _, err := os.Stat(p.pidFile) // os.Stat获取文件信息 + if (err != nil && os.IsExist(err)) || err == nil { + return fmt.Errorf("gofbot already running.") + } + + pid := strconv.Itoa(os.Getpid()) + return ioutil.WriteFile(p.pidFile, []byte(pid), 0600) +} + +func (p *PidCtrl) Clean() error { + return os.Remove(p.pidFile) +} + +func (p *PidCtrl) Find() (*os.Process, error) { + data, err := ioutil.ReadFile(p.pidFile) + if err != nil { + return nil, err + } + + pidStr := string(data) + pid, err := strconv.Atoi(pidStr) + if err != nil { + return nil, err + } + + return os.FindProcess(pid) +} diff --git a/robot/message.go b/robot/message.go new file mode 100644 index 0000000..444f309 --- /dev/null +++ b/robot/message.go @@ -0,0 +1,63 @@ +package robot + +import ( + "bytes" + "fmt" + "regexp" + "strconv" + "strings" +) + +var tplArgExp = regexp.MustCompile(`{{(\s*\$\S+\s*)}}`) + +type Map map[string]interface{} + +type Message struct { + Regexp string `yaml:"regexp"` + Template string `yaml:"template"` + + Exp *regexp.Regexp +} + +type variable struct { + full string + name string +} + +func BuildMessage(tpl string, params Map) string { + variables := make([]variable, 0) + for _, v := range tplArgExp.FindAllStringSubmatch(tpl, -1) { + variables = append(variables, variable{full: v[0], name: v[1]}) + } + + newMsg := tpl + for _, v := range variables { + newMsg = strings.Replace(newMsg, v.full, extractArgs(params, strings.TrimSpace(v.name)), -1) + } + + if strconv.CanBackquote(newMsg) { + return newMsg + } + + return strconv.Quote(newMsg) +} + +func BuildPostBody(bodyTpl string, message string) *bytes.Buffer { + return bytes.NewBufferString(strings.Replace(bodyTpl, "$template", message, -1)) +} + +func extractArgs(params Map, key string) string { + key = strings.Replace(key, "$", "", -1) + keys := strings.Split(key, ".") + for index, k := range keys { + if index == len(keys)-1 { + return fmt.Sprintf("%v", params[k]) + } + + if nextParams, ok := params[k].(map[string]interface{}); ok { + params = nextParams + } + } + + return "" +} diff --git a/robot/message_test.go b/robot/message_test.go new file mode 100644 index 0000000..d5ea3e3 --- /dev/null +++ b/robot/message_test.go @@ -0,0 +1,26 @@ +package robot + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBuildMessage(t *testing.T) { + params := Map{ + "name": "saltbo", + "age": "53", + "info": map[string]interface{}{ + "city": "Beijing", + }, + } + tpl := `name: {{$name}}, age: {{ $age }}, city: {{ $info.city }}` + msg := BuildMessage(tpl, params) + assert.Contains(t, msg, params["name"]) + assert.Contains(t, msg, params["age"]) + assert.Contains(t, msg, params["info"].(map[string]interface{})["city"]) + + bodyTpl := `{"msgtype": "markdown", "content": "$template"}` + body := BuildPostBody(bodyTpl, msg) + assert.Contains(t, body.String(), msg) +} diff --git a/server/robot.go b/robot/robot.go similarity index 87% rename from server/robot.go rename to robot/robot.go index 7d2f908..6885dff 100644 --- a/server/robot.go +++ b/robot/robot.go @@ -1,4 +1,4 @@ -package main +package robot import ( "crypto/md5" @@ -13,11 +13,41 @@ import ( "strings" ) -type Message struct { - Regexp string `yaml:"regexp"` - Template string `yaml:"template"` +func LoadAndParse(robotsPath string) ([]*Robot, error) { + robots := make([]*Robot, 0) + robotCreator := func(filepath string) error { + robot, err := newRobot(filepath) + if err != nil { + return err + } + + robots = append(robots, robot) + return nil + } + + if err := findRobots(robotsPath, robotCreator); err != nil { + return nil, err + } + + if len(robots) == 0 { + return nil, fmt.Errorf("not found any robot.") + } - Exp *regexp.Regexp + return robots, nil +} + +func findRobots(root string, creator func(filepath string) error) error { + return filepath.Walk(root, func(filepath string, info os.FileInfo, err error) error { + if err != nil { + return err + } else if info.IsDir() { + return nil + } else if path.Ext(filepath) != ".yaml" && path.Ext(filepath) != ".yml" { + return nil + } + + return creator(filepath) + }) } type Robot struct { @@ -53,9 +83,9 @@ func newRobot(yamlPath string) (*Robot, error) { robot.Alias = hex.EncodeToString(nameHash[:]) errors := make([]string, 0) for _, msg := range robot.Messages { - exp, err2 := regexp.Compile(msg.Regexp) + exp, err := regexp.Compile(msg.Regexp) if err != nil { - errors = append(errors, err2.Error()) + errors = append(errors, err.Error()) continue } @@ -68,40 +98,3 @@ func newRobot(yamlPath string) (*Robot, error) { return robot, nil } - -func findRobots(root string, creator func(filepath string) error) error { - return filepath.Walk(root, func(filepath string, info os.FileInfo, err error) error { - if err != nil { - return err - } else if info.IsDir() { - return nil - } else if path.Ext(filepath) != ".yaml" && path.Ext(filepath) != ".yml" { - return nil - } - - return creator(filepath) - }) -} - -func loadRobots(robotsPath string) ([]*Robot, error) { - robots := make([]*Robot, 0) - robotCreator := func(filepath string) error { - robot, err := newRobot(filepath) - if err != nil { - return err - } - - robots = append(robots, robot) - return nil - } - - if err := findRobots(robotsPath, robotCreator); err != nil { - return nil, err - } - - if len(robots) == 0 { - return nil, fmt.Errorf("not found any robot.") - } - - return robots, nil -} diff --git a/server/robot_test.go b/robot/robot_test.go similarity index 60% rename from server/robot_test.go rename to robot/robot_test.go index b417ed1..b0a3c74 100644 --- a/server/robot_test.go +++ b/robot/robot_test.go @@ -1,11 +1,12 @@ -package main +package robot import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestNewRobot(t *testing.T) { - _, e := newRobot("../robots/wxwork4gitlab.yaml") + _, e := newRobot("../deployments/robots/wxwork4gitlab.yaml") assert.NoError(t, e) } diff --git a/robots/wxwork4gitlab.yaml b/robots/wxwork4gitlab.yaml deleted file mode 100644 index 38355a9..0000000 --- a/robots/wxwork4gitlab.yaml +++ /dev/null @@ -1,13 +0,0 @@ -name: wxwork4gitlab -webhook: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=0d3ffb7e-3a7a-4f4c-8385-bea0db5e581a -bodytpl: '{"msgtype":"markdown","markdown":{"content":"$template"}}' -messages: - - regexp: name # regexp for match this message. - template: 实时新增用户反馈132例,请相关同事注意。\n - >类型:用户反馈 \n - >普通用户反馈:{{ $test }} \n - >VIP用户反馈:{{ $test2 }}例 - - regexp: merge - template: test merge message. - - regexp: issue - template: test push message. \ No newline at end of file diff --git a/server/client.go b/server/client.go deleted file mode 100644 index 0b07911..0000000 --- a/server/client.go +++ /dev/null @@ -1,49 +0,0 @@ -package main - -import ( - `fmt` - `github.com/urfave/cli` - `log` - `syscall` -) - -var ( - repo string - commit string - version string - buildTime string -) - -// define some flags -var flags = []cli.Flag{ - -} -// define some commands -var commands = []cli.Command{ - { - Name: "reload", - Usage: "reload for the config", - Action: func(c *cli.Context) { - p, err := findProcess() - if err != nil { - log.Println(err) - return - } - - if err := p.Signal(syscall.SIGUSR1); err != nil { - log.Println(err) - return - } - }, - }, -} - -func NewClient() *cli.App { - cli.VersionPrinter = func(c *cli.Context) { - fmt.Printf("repo: %s\ncommit: %s\nversion: %s\nbuildTime: %s\n", repo, commit, version, buildTime) - } - app := cli.NewApp() - app.Flags = flags - app.Commands = commands - return app -} diff --git a/server/main.go b/server/main.go deleted file mode 100644 index d9f93ab..0000000 --- a/server/main.go +++ /dev/null @@ -1,64 +0,0 @@ -package main - -import ( - "log" - "os" - `os/signal` - `syscall` -) - -func main() { - if len(os.Args) == 1 { - serverRun() - return - } - - if err := NewClient().Run(os.Args); err != nil { - log.Fatal(err) - } -} - -func serverRun() { - robots, err := loadRobots("robots") - if err != nil { - log.Fatal(err) - } - - if err := savePid(); err != nil { - log.Fatal(err) - } - - server := NewServer() - setupSignalHandler(server) - server.SetupRobots(robots) - if err := server.Run(":9613"); err != nil { - log.Fatal(err) - } -} - -func setupSignalHandler(server *Server) { - ch := make(chan os.Signal, 1) - signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1) - go func() { - for { - sig := <-ch - switch sig { - case syscall.SIGINT, syscall.SIGTERM: - cleanPid(); - signal.Stop(ch) - server.Shutdown() - log.Printf("system exit.") - return - case syscall.SIGUSR1: - // hot reload - robots, err := loadRobots("robots") - if err != nil { - log.Println(robots) - return - } - server.SetupRobots(robots) - log.Printf("config reload.") - } - } - }() -} diff --git a/server/process.go b/server/process.go deleted file mode 100644 index 9be1dd6..0000000 --- a/server/process.go +++ /dev/null @@ -1,39 +0,0 @@ -package main - -import ( - `fmt` - `io/ioutil` - `os` - `strconv` -) - -var pidFile = "gofbot.lock" - -func savePid() error { - _, err := os.Stat(pidFile) // os.Stat获取文件信息 - if (err != nil && os.IsExist(err)) || err == nil { - return fmt.Errorf("gofbot already running.") - } - - pid := strconv.Itoa(os.Getpid()) - return ioutil.WriteFile(pidFile, []byte(pid), 0600) -} - -func cleanPid() error { - return os.Remove(pidFile) -} - -func findProcess() (*os.Process, error) { - data, err := ioutil.ReadFile(pidFile) - if err != nil { - return nil, err - } - - pidStr := string(data) - pid, err := strconv.Atoi(pidStr) - if err != nil { - return nil, err - } - - return os.FindProcess(pid) -} diff --git a/server/server_test.go b/server/server_test.go deleted file mode 100644 index 0dff31e..0000000 --- a/server/server_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "io" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestBuildMessage(t *testing.T) { - params := Map{ - "name": "saltbo", - "age": "53", - "info": Map{ - "city": "Beijing", - }, - } - tpl := `name: {{$name}}, age: {{ $age }}, city: {{ $info.city }}` - msg := buildMessage(tpl, params) - assert.Contains(t, msg, params["name"]) - assert.Contains(t, msg, params["age"]) - assert.Contains(t, msg, params["info"].(Map)["city"]) - - bodyTpl := `{"msgtype": "markdown", "content": "$template"}` - body := buildPostBody(bodyTpl, msg) - assert.Contains(t, body.String(), msg) -} - -func performRequest(r http.Handler, method, path string, body io.Reader) *httptest.ResponseRecorder { - req := httptest.NewRequest(method, path, body) - w := httptest.NewRecorder() - r.ServeHTTP(w, req) - return w -} - -type testServer struct { -} - -func (ts *testServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { - b, e := ioutil.ReadAll(r.Body) - if e != nil { - w.WriteHeader(http.StatusBadRequest) - return - } - - if _, err := w.Write(b); err != nil { - w.WriteHeader(http.StatusInternalServerError) - } -} - -func TestServer_Run(t *testing.T) { - ts := httptest.NewServer(&testServer{}) - defer ts.Close() - - robots, err := loadRobots("../robots") - assert.NoError(t, err) - - server := NewServer() - server.SetupRobots(robots) - for _, robot := range robots { - // reset the hook to the test server URL - robot.WebHook = ts.URL - - // RUN - body := bytes.NewBufferString(`{"name": "saltbo", "sex": "man", "info":{"city": "beijing"}}`) - w := performRequest(server.router, "POST", fmt.Sprintf("/incoming/%s", robot.Alias), body) - - // TEST - assert.Equal(t, http.StatusOK, w.Code) - } -} From acb7d961c3a2aa1bfa2ae973f55ba6f8821467f6 Mon Sep 17 00:00:00 2001 From: saltbo Date: Mon, 9 Sep 2019 17:21:45 +0800 Subject: [PATCH 5/5] chore: update the dockerfile --- Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index e3608d7..6e0676a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,5 @@ FROM golang:latest AS build-env -ENV GOPROXY https://goproxy.io ENV APP_HOME /app WORKDIR $APP_HOME @@ -17,7 +16,7 @@ FROM debian:9 ENV APP_HOME /app WORKDIR $APP_HOME -ADD robots $APP_HOME/robots +ADD deployments $APP_HOME/deployments COPY --from=build-env /app/build/gofbot $APP_HOME/gofbot ENTRYPOINT ["./gofbot"]